From 5ba4407367ee95b5861d396a7e175731fd6b47bc Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 1 May 2026 13:47:02 +0300 Subject: [PATCH] render: replace EGL/OpenGL pipeline with `wl_shm` shared memory Signed-off-by: NotAShelf Change-Id: I15dbcca9d12b5c24ed8c82ecc375f4046a6a6964 --- Makefile | 4 +- include/chroma.h | 37 +- src/core.c | 23 +- src/main.c | 10 - src/render.c | 1047 +++++++++++++++------------------------------- src/wayland.c | 21 +- 6 files changed, 363 insertions(+), 779 deletions(-) diff --git a/Makefile b/Makefile index c1bffd4..4b5af90 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ DEBUG_CFLAGS += -D_GNU_SOURCE -DCHROMA_VERSION=\"$(VERSION)-debug\" DEBUG_CFLAGS += -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer # Libraries using pkg-config -PKG_DEPS = wayland-client wayland-egl egl glesv2 wayland-protocols +PKG_DEPS = wayland-client wayland-protocols CFLAGS += $(shell pkg-config --cflags $(PKG_DEPS)) LDFLAGS += $(shell pkg-config --libs $(PKG_DEPS)) @@ -117,7 +117,7 @@ static: $(TARGET) # Check if required dependencies are available check-deps: @echo "Checking dependencies..." - @pkg-config --exists $(PKG_DEPS) || (echo "Missing dependencies. Install: wayland-protocols libwayland-dev libegl1-mesa-dev libgl1-mesa-dev wayland-scanner" && exit 1) + @pkg-config --exists $(PKG_DEPS) || (echo "Missing dependencies. Install: wayland-protocols libwayland-dev wayland-scanner" && exit 1) @which wayland-scanner >/dev/null || (echo "wayland-scanner not found. Please install wayland-scanner." && exit 1) @echo "All dependencies found." diff --git a/include/chroma.h b/include/chroma.h index f8e2623..393c62b 100644 --- a/include/chroma.h +++ b/include/chroma.h @@ -3,14 +3,11 @@ #include "wlr-layer-shell-unstable-v1.h" #include "xdg-shell.h" -#include -#include #include #include #include #include #include -#include #include "chroma_version.h" @@ -73,6 +70,7 @@ typedef struct { int channels; char path[MAX_PATH_LEN]; bool loaded; + bool data_from_stbi; // true if data was allocated by stbi_load int ref_count; // Number of outputs using this image } chroma_image_t; @@ -94,10 +92,14 @@ typedef struct { // Rendering context struct wl_surface *surface; struct zwlr_layer_surface_v1 *layer_surface; - struct wl_egl_window *egl_window; - EGLSurface egl_surface; uint32_t configure_serial; + // Shared memory buffer + struct wl_buffer *shm_buffer; + void *shm_data; + size_t shm_size; + int shm_stride; + // Associated wallpaper chroma_image_t *image; @@ -108,15 +110,6 @@ typedef struct { float anchor_x; // custom X offset (0-100, 50=center) float anchor_y; // custom Y offset (0-100, 50=center) bool config_loaded; - - // OpenGL resource cache - GLuint texture_id; - GLuint shader_program; - GLuint vbo; - GLuint ebo; - bool gl_resources_initialized; - bool texture_uploaded; - bool vbo_dirty; // track VBO needs update bool configured; // track if initial configure received } chroma_output_t; @@ -159,14 +152,7 @@ typedef struct chroma_state { struct wl_registry *registry; struct wl_compositor *compositor; struct zwlr_layer_shell_v1 *layer_shell; - - // EGL context - EGLDisplay egl_display; - EGLContext egl_context; - EGLConfig egl_config; - - // Shared OpenGL resources - GLuint shader_program; + struct wl_shm *shm; // Outputs chroma_output_t outputs[MAX_OUTPUTS]; @@ -221,13 +207,12 @@ void chroma_output_description(void *data, struct wl_output *output, const char *description); void chroma_output_done(void *data, struct wl_output *output); -// EGL and rendering -int chroma_egl_init(chroma_state_t *state); -void chroma_egl_cleanup(chroma_state_t *state); +// Rendering and surface management int chroma_surface_create(chroma_state_t *state, chroma_output_t *output); void chroma_surface_destroy(chroma_output_t *output); +int chroma_buffer_create(chroma_state_t *state, chroma_output_t *output); +void chroma_buffer_destroy(chroma_output_t *output); int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output); -void chroma_output_invalidate_texture(chroma_output_t *output); // Layer shell functions void chroma_layer_surface_configure(void *data, diff --git a/src/core.c b/src/core.c index 9d499ba..6974ec4 100644 --- a/src/core.c +++ b/src/core.c @@ -24,8 +24,6 @@ int chroma_init(chroma_state_t *state) { // Set initial state state->running = false; state->initialized = false; - state->egl_display = EGL_NO_DISPLAY; - state->egl_context = EGL_NO_CONTEXT; // Initialize stb_image chroma_image_init_stb(); @@ -53,9 +51,6 @@ void chroma_cleanup(chroma_state_t *state) { // Clean up all images chroma_images_cleanup(state); - // Clean up EGL - chroma_egl_cleanup(state); - // Clean up Wayland chroma_wayland_disconnect(state); @@ -104,24 +99,16 @@ static int assign_wallpaper_to_output(chroma_state_t *state, return CHROMA_ERROR_IMAGE; } - // Check if image changed and invalidate texture cache if neceessary + // Check if image changed bool image_changed = (output->image != image); if (image_changed && output->image) { chroma_image_release(output->image); - chroma_output_invalidate_texture(output); - output->vbo_dirty = true; // VBO needs update for new image - chroma_log("DEBUG", "Image changed for output %u, invalidated texture", - output->id); + chroma_log("DEBUG", "Image changed for output %u", output->id); } // Assign image to output output->image = image; - // Mark VBO as dirty if image changed - if (image_changed) { - output->vbo_dirty = true; - } - // Store old configuration values for comparison chroma_scale_mode_t old_scale_mode = output->scale_mode; chroma_filter_quality_t old_filter_quality = output->filter_quality; @@ -144,16 +131,14 @@ static int assign_wallpaper_to_output(chroma_state_t *state, output->anchor, (double)output->anchor_x, (double)output->anchor_y); - // Check if configuration changed and invalidate texture if needed + // Check if configuration changed if (had_config && (old_scale_mode != output->scale_mode || old_filter_quality != output->filter_quality || old_anchor != output->anchor || old_anchor_x != output->anchor_x || old_anchor_y != output->anchor_y)) { - chroma_output_invalidate_texture(output); - output->vbo_dirty = true; // VBO needs update for new scale mode chroma_log("DEBUG", - "Configuration changed for output %u, invalidated texture", + "Configuration changed for output %u", output->id); } } else { diff --git a/src/main.c b/src/main.c index 9f1fe92..f2edbf8 100644 --- a/src/main.c +++ b/src/main.c @@ -178,16 +178,6 @@ int main(int argc, char *argv[]) { } chroma_log_memory_stats("post-wayland-connect"); - // Initialize EGL - ret = chroma_egl_init(&state); - if (ret != CHROMA_OK) { - chroma_log("ERROR", "Failed to initialize EGL: %s", - chroma_error_string(ret)); - chroma_cleanup(&state); - return 1; - } - chroma_log_memory_stats("post-egl-init"); - chroma_log("INFO", "Chroma daemon initialized successfully"); chroma_log_memory_stats("pre-main-loop"); diff --git a/src/render.c b/src/render.c index 732a1ae..626d693 100644 --- a/src/render.c +++ b/src/render.c @@ -1,588 +1,309 @@ +#include +#include +#include +#include #include #include #include - -#include -#include +#include +#include +#include #include "../include/chroma.h" #include "../include/vendor/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; +// Destroy shared memory buffer for an output +void chroma_buffer_destroy(chroma_output_t *output) { + if (!output) { + return; + } + + if (output->shm_buffer) { + wl_buffer_destroy(output->shm_buffer); + output->shm_buffer = NULL; + } + + if (output->shm_data) { + if (munmap(output->shm_data, output->shm_size) < 0) { + chroma_log("WARN", "munmap failed: %s", strerror(errno)); + } + output->shm_data = NULL; + output->shm_size = 0; + output->shm_stride = 0; } } -// 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 +// Create shared memory buffer for an output +int chroma_buffer_create(chroma_state_t *state, chroma_output_t *output) { + if (!state || !output || !state->shm) { + return CHROMA_ERROR_INIT; + } - 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; + int width = output->width; + int height = output->height; + if (width <= 0 || height <= 0) { + return CHROMA_ERROR_INIT; + } + + int stride = width * 4; + size_t size = (size_t)stride * height; + + int fd = memfd_create("chroma-shm", MFD_CLOEXEC | MFD_ALLOW_SEALING); + if (fd < 0) { + // Fallback to shm_open if memfd_create is unavailable + char name[64]; + snprintf(name, sizeof(name), "/chroma-shm-%d-%d", getpid(), rand()); + fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd >= 0) { + shm_unlink(name); + } + } + if (fd < 0) { + chroma_log("ERROR", "Failed to create shared memory fd: %s", + strerror(errno)); + return CHROMA_ERROR_MEMORY; + } + + if (ftruncate(fd, (off_t)size) < 0) { + chroma_log("ERROR", "ftruncate failed: %s", strerror(errno)); + close(fd); + return CHROMA_ERROR_MEMORY; + } + + void *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (data == MAP_FAILED) { + chroma_log("ERROR", "mmap failed: %s", strerror(errno)); + close(fd); + return CHROMA_ERROR_MEMORY; + } + + // Fill with black + memset(data, 0, size); + + struct wl_shm_pool *pool = + wl_shm_create_pool(state->shm, fd, (int32_t)size); + if (!pool) { + chroma_log("ERROR", "wl_shm_create_pool failed"); + munmap(data, size); + close(fd); + return CHROMA_ERROR_WAYLAND; + } + + struct wl_buffer *buffer = wl_shm_pool_create_buffer( + pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888); + wl_shm_pool_destroy(pool); + close(fd); + + if (!buffer) { + chroma_log("ERROR", "wl_shm_pool_create_buffer failed"); + munmap(data, size); + return CHROMA_ERROR_WAYLAND; + } + + output->shm_buffer = buffer; + output->shm_data = data; + output->shm_size = size; + output->shm_stride = stride; + + return CHROMA_OK; +} + +// Sample a pixel from image data (grayscale, grayscale+alpha, RGB, or RGBA) +static inline uint32_t sample_pixel(const chroma_image_t *image, int x, + int y) { + if (!image || !image->data || x < 0 || y < 0 || x >= image->width || + y >= image->height) { + return 0xFF000000; // black with full alpha + } + + int idx = (y * image->width + x) * image->channels; + unsigned char r, g, b, a; + + switch (image->channels) { + case 1: + r = g = b = image->data[idx]; + a = 0xFF; break; + case 2: + r = g = b = image->data[idx]; + a = image->data[idx + 1]; + break; + case 3: + r = image->data[idx]; + g = image->data[idx + 1]; + b = image->data[idx + 2]; + a = 0xFF; + break; + default: // 4 or more + r = image->data[idx]; + g = image->data[idx + 1]; + b = image->data[idx + 2]; + a = image->data[idx + 3]; + break; + } + + return ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | + (uint32_t)b; +} + +// Draw scaled image into the shared memory buffer +static void draw_scaled_image(chroma_output_t *output, + chroma_image_t *image) { + if (!output || !image || !output->shm_data) { + return; + } + + int out_w = output->width; + int out_h = output->height; + int img_w = image->width; + int img_h = image->height; + uint32_t *dst = (uint32_t *)output->shm_data; + + // Pre-fill with black + memset(output->shm_data, 0, output->shm_size); + + float anchor_x = output->anchor_x; + float anchor_y = output->anchor_y; + + switch (output->scale_mode) { + case CHROMA_SCALE_STRETCH: { + for (int y = 0; y < out_h; y++) { + for (int x = 0; x < out_w; x++) { + int sx = (int)((float)x * img_w / out_w); + int sy = (int)((float)y * img_h / out_h); + sx = (sx < 0) ? 0 : (sx >= img_w) ? img_w - 1 : sx; + sy = (sy < 0) ? 0 : (sy >= img_h) ? img_h - 1 : sy; + dst[y * out_w + x] = sample_pixel(image, sx, sy); + } + } + break; + } + + case CHROMA_SCALE_FILL: { + float img_aspect = (float)img_w / (float)img_h; + float out_aspect = (float)out_w / (float)out_h; + + float crop_w, crop_h, crop_x, crop_y; + + if (img_aspect > out_aspect) { + // Image is wider - crop left/right + crop_h = (float)img_h; + crop_w = crop_h * out_aspect; + } else { + // Image is taller - crop top/bottom + crop_w = (float)img_w; + crop_h = crop_w / out_aspect; + } + + crop_x = (anchor_x / 100.0f) * ((float)img_w - crop_w); + crop_y = (anchor_y / 100.0f) * ((float)img_h - crop_h); + + // Clamp crop region + if (crop_x < 0) + crop_x = 0; + if (crop_y < 0) + crop_y = 0; + if (crop_x > (float)img_w - crop_w) + crop_x = (float)img_w - crop_w; + if (crop_y > (float)img_h - crop_h) + crop_y = (float)img_h - crop_h; + + for (int y = 0; y < out_h; y++) { + for (int x = 0; x < out_w; x++) { + float sx = crop_x + (float)x * crop_w / (float)out_w; + float sy = crop_y + (float)y * crop_h / (float)out_h; + int ix = (int)sx; + int iy = (int)sy; + dst[y * out_w + x] = sample_pixel(image, ix, iy); + } + } + break; + } + + case CHROMA_SCALE_FIT: { + float scale_x = (float)out_w / (float)img_w; + float scale_y = (float)out_h / (float)img_h; + float scale = (scale_x < scale_y) ? scale_x : scale_y; + + float scaled_w = (float)img_w * scale; + float scaled_h = (float)img_h * scale; + + float offset_x = (anchor_x / 100.0f) * ((float)out_w - scaled_w); + float offset_y = (anchor_y / 100.0f) * ((float)out_h - scaled_h); + + int start_x = (int)offset_x; + int start_y = (int)offset_y; + int end_x = (int)(offset_x + scaled_w + 0.5f); + int end_y = (int)(offset_y + scaled_h + 0.5f); + + if (start_x < 0) + start_x = 0; + if (start_y < 0) + start_y = 0; + if (end_x > out_w) + end_x = out_w; + if (end_y > out_h) + end_y = out_h; + + for (int y = start_y; y < end_y; y++) { + for (int x = start_x; x < end_x; x++) { + float sx = ((float)x - offset_x) / scale; + float sy = ((float)y - offset_y) / scale; + int ix = (int)sx; + int iy = (int)sy; + ix = (ix < 0) ? 0 : (ix >= img_w) ? img_w - 1 : ix; + iy = (iy < 0) ? 0 : (iy >= img_h) ? img_h - 1 : iy; + dst[y * out_w + x] = sample_pixel(image, ix, iy); + } + } + break; + } case CHROMA_SCALE_CENTER: - // Center image at original size - // Calculate how much of the texture to show - { - float image_aspect = (float)image_width / (float)image_height; - float output_aspect = (float)output_width / (float)output_height; + default: { + if (img_w <= out_w && img_h <= out_h) { + // Image fits entirely - position with anchor + float offset_x = + (anchor_x / 100.0f) * ((float)out_w - (float)img_w); + float offset_y = + (anchor_y / 100.0f) * ((float)out_h - (float)img_h); - 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 = ((float)image_height - visible_height) / - (2.0f * (float)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 = - ((float)image_width - visible_width) / (2.0f * (float)image_width); - u1 = u_offset; - v1 = 0.0f; - u2 = 1.0f - u_offset; - v2 = 1.0f; + for (int y = 0; y < img_h; y++) { + for (int x = 0; x < img_w; x++) { + int dx = (int)(offset_x + x); + int dy = (int)(offset_y + y); + if (dx >= 0 && dx < out_w && dy >= 0 && dy < out_h) { + dst[dy * out_w + dx] = sample_pixel(image, x, y); + } + } } - } - break; + } else { + // Image is larger than output - clip with anchor + float src_offset_x = + (anchor_x / 100.0f) * ((float)img_w - (float)out_w); + float src_offset_y = + (anchor_y / 100.0f) * ((float)img_h - (float)out_h); - case CHROMA_SCALE_FIT: - // Fit image within output, maintaining aspect ratio - { - float image_aspect = (float)image_width / (float)image_height; - float output_aspect = (float)output_width / (float)output_height; + if (src_offset_x < 0) + src_offset_x = 0; + if (src_offset_y < 0) + src_offset_y = 0; + if (src_offset_x > (float)img_w - (float)out_w) + src_offset_x = (float)img_w - (float)out_w; + if (src_offset_y > (float)img_h - (float)out_h) + src_offset_y = (float)img_h - (float)out_h; - 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 = ((float)output_height - scaled_height) / - (2.0f * (float)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 = - ((float)output_width - scaled_width) / (2.0f * (float)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 / (float)image_height; - float output_aspect = (float)output_width / (float)output_height; - - if (image_aspect > output_aspect) { - // Image is wider - crop left/right - float crop_width = (float)image_height * output_aspect; - float u_crop = - ((float)image_width - crop_width) / (2.0f * (float)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 = (float)image_width / output_aspect; - float v_crop = - ((float)image_height - crop_height) / (2.0f * (float)image_height); - u1 = 0.0f; - v1 = v_crop; - u2 = 1.0f; - v2 = 1.0f - v_crop; + for (int y = 0; y < out_h; y++) { + for (int x = 0; x < out_w; x++) { + int sx = (int)(src_offset_x + x); + int sy = (int)(src_offset_y + y); + dst[y * out_w + x] = sample_pixel(image, sx, sy); + } } } 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 = - "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 = - "precision mediump float;\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 short 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_state_t *state, chroma_output_t *output) { - if (!output || output->gl_resources_initialized) { - return CHROMA_OK; - } - - // Use shared shader program from state if available - if (state && state->shader_program != 0) { - output->shader_program = state->shader_program; - chroma_log("DEBUG", "Using shared shader program for output %u", - output->id); - } else { - // Create shader program - 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; - } - // Store in state for sharing - if (state) { - state->shader_program = output->shader_program; - chroma_log("DEBUG", "Created shared shader program"); - } - } - - // 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); - - // Unbind buffers - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - - 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 * - (size_t)output->image->height * - (size_t)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 * (size_t)image->height * (size_t)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 with appropriate format based on channels - GLint internal_format; - GLenum format; - if (image->channels == 4) { - internal_format = GL_RGBA; - format = GL_RGBA; - } else if (image->channels == 3) { - internal_format = GL_RGB; - format = GL_RGB; - } else if (image->channels == 2) { - internal_format = GL_LUMINANCE_ALPHA; - format = GL_LUMINANCE_ALPHA; - } else { - internal_format = GL_LUMINANCE; - format = GL_LUMINANCE; - } - glTexImage2D(GL_TEXTURE_2D, 0, internal_format, image->width, image->height, - 0, format, 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; - - // Decrement reference count and free system RAM when all outputs - // have uploaded their textures - image->ref_count--; - if (image->ref_count <= 0 && image->data) { - size_t freed_bytes = - (size_t)image->width * (size_t)image->height * (size_t)image->channels; - stbi_image_free(image->data); - image->data = NULL; - chroma_log("INFO", "Freed %.2f MB of image data after GPU upload: %s", - (double)freed_bytes / (1024.0 * 1024.0), image->path); - chroma_log_resource_deallocation("image_data", freed_bytes, - "post-gpu-upload"); - chroma_log_memory_stats("post-gpu-upload"); - } else { - chroma_log("DEBUG", - "Keeping image data for %s (ref_count: %d, waiting for %d more " - "outputs)", - image->path, image->ref_count, image->ref_count); - } - - 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_state_t *state, - 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; - } - - // Don't delete shared shader program here, it's managed by state - if (output->shader_program != 0) { - // Only delete if this output owns the program (not shared) - if (state && state->shader_program != output->shader_program) { - 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, - 0, - EGL_RENDERABLE_TYPE, - EGL_OPENGL_ES2_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 ES API for better efficiency on embedded/GPU drivers - if (!eglBindAPI(EGL_OPENGL_ES_API)) { - chroma_log("ERROR", "Failed to bind OpenGL ES 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 for GLES 2.0 - EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, 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; - } - - // Clean up shared shader program while context is still current - if (state->shader_program != 0) { - glDeleteProgram(state->shader_program); - state->shader_program = 0; - } - - 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 @@ -612,16 +333,19 @@ int chroma_surface_create(chroma_state_t *state, chroma_output_t *output) { } // Configure layer surface - zwlr_layer_surface_v1_set_size(output->layer_surface, (uint32_t)output->width, + zwlr_layer_surface_v1_set_size(output->layer_surface, + (uint32_t)output->width, (uint32_t)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_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); + output->layer_surface, + ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE); // Add layer surface listener zwlr_layer_surface_v1_add_listener( @@ -633,34 +357,9 @@ int chroma_surface_create(chroma_state_t *state, chroma_output_t *output) { // 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 * (size_t)output->height * 4; - chroma_log_resource_allocation("egl_surface", surface_size, "output surface"); - return CHROMA_OK; } @@ -670,18 +369,7 @@ void chroma_surface_destroy(chroma_output_t *output) { return; } - // Clean up OpenGL resources first. We use output's back-reference to state - cleanup_gl_resources(output->state, 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; - } + chroma_buffer_destroy(output); if (output->layer_surface) { zwlr_layer_surface_v1_destroy(output->layer_surface); @@ -695,134 +383,51 @@ void chroma_surface_destroy(chroma_output_t *output) { output->configured = false; - // Log surface destruction - size_t surface_size = (size_t)output->width * (size_t)output->height * 4; - 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 +// Render wallpaper to output 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 (!output->configured) { + chroma_log("WARN", "Output %u not yet configured, skipping render", + output->id); + return CHROMA_ERROR_INIT; } - if (init_gl_resources(state, output) != CHROMA_OK) { - return CHROMA_ERROR_EGL; + // Clean up old buffer + chroma_buffer_destroy(output); + + // Create new shared memory buffer + int ret = chroma_buffer_create(state, output); + if (ret != CHROMA_OK) { + chroma_log("ERROR", "Failed to create shm buffer for output %u", + output->id); + return ret; } - 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; - } + // Draw scaled image into buffer + draw_scaled_image(output, output->image); + + // Attach buffer to surface + wl_surface_attach(output->surface, output->shm_buffer, 0, 0); + wl_surface_damage_buffer(output->surface, 0, 0, output->width, + output->height); + wl_surface_commit(output->surface); + wl_display_flush(state->display); + + chroma_log("INFO", "Rendered wallpaper to output %u (%dx%d)", output->id, + output->width, output->height); + + // Release image reference and free CPU data if no longer needed + if (output->image) { + chroma_image_t *image = output->image; + chroma_image_release(image); + output->image = NULL; } - // 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((GLuint)position_attr); - glVertexAttribPointer((GLuint)position_attr, 2, GL_FLOAT, GL_FALSE, - 4 * sizeof(float), (void *)0); - - glEnableVertexAttribArray((GLuint)texcoord_attr); - glVertexAttribPointer((GLuint)texcoord_attr, 2, GL_FLOAT, GL_FALSE, - 4 * sizeof(float), (void *)(2 * sizeof(float))); - - // Draw - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); - - // Unbind resources - glBindBuffer(GL_ARRAY_BUFFER, 0); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - glBindTexture(GL_TEXTURE_2D, 0); - glUseProgram(0); - - // Swap buffers; this implicitly commits the surface for EGL on Wayland - 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; - } - - 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; -} diff --git a/src/wayland.c b/src/wayland.c index c5fa252..88cdf32 100644 --- a/src/wayland.c +++ b/src/wayland.c @@ -37,6 +37,13 @@ static void registry_global(void *data, struct wl_registry *registry, "TRACE", "wlr-layer-shell protocol available, wallpaper support enabled"); } + } else if (strcmp(interface, wl_shm_interface.name) == 0) { + state->shm = wl_registry_bind(registry, id, &wl_shm_interface, 1); + if (!state->shm) { + chroma_log("ERROR", "Failed to bind shm"); + } else { + chroma_log("INFO", "Bound shm (version %u)", version); + } } else if (strcmp(interface, wl_output_interface.name) == 0) { struct wl_output *output = wl_registry_bind( registry, id, &wl_output_interface, version < 4 ? version : 4); @@ -91,7 +98,7 @@ static void layer_surface_configure(void *data, chroma_log("TRACE", "Sent configure acknowledgment for output %u serial %u", output->id, serial); - // Mark as configured - actual commit happens in render via eglSwapBuffers + // Mark as configured - actual commit happens in render via wl_surface_commit output->configured = true; chroma_log("DEBUG", "Acknowledged layer surface configure for output %u", @@ -284,6 +291,13 @@ int chroma_wayland_connect(chroma_state_t *state) { return CHROMA_ERROR_WAYLAND; } + // Check if we got shm + if (!state->shm) { + chroma_log("ERROR", "No shm available"); + chroma_wayland_disconnect(state); + return CHROMA_ERROR_WAYLAND; + } + chroma_log("INFO", "Connected to Wayland display, found %d outputs", state->output_count); return CHROMA_OK; @@ -316,6 +330,11 @@ void chroma_wayland_disconnect(chroma_state_t *state) { state->layer_shell = NULL; } + if (state->shm) { + wl_shm_destroy(state->shm); + state->shm = NULL; + } + if (state->compositor) { wl_compositor_destroy(state->compositor); state->compositor = NULL;