| 1 | #include <errno.h> |
| 2 | #include <fcntl.h> |
| 3 | #include <math.h> |
| 4 | #include <stdint.h> |
| 5 | #include <stdio.h> |
| 6 | #include <stdlib.h> |
| 7 | #include <string.h> |
| 8 | #include <sys/mman.h> |
| 9 | #include <sys/stat.h> |
| 10 | #include <unistd.h> |
| 11 | |
| 12 | #include "../include/chroma.h" |
| 13 | #include "../include/vendor/stb_image.h" |
| 14 | |
| 15 | // Destroy shared memory buffer for an output |
| 16 | void chroma_buffer_destroy(chroma_output_t *output) { |
| 17 | if (!output) { |
| 18 | return; |
| 19 | } |
| 20 | |
| 21 | if (output->shm_buffer) { |
| 22 | wl_buffer_destroy(output->shm_buffer); |
| 23 | output->shm_buffer = NULL; |
| 24 | } |
| 25 | |
| 26 | if (output->shm_data) { |
| 27 | if (munmap(output->shm_data, output->shm_size) < 0) { |
| 28 | chroma_log("WARN", "munmap failed: %s", strerror(errno)); |
| 29 | } |
| 30 | output->shm_data = NULL; |
| 31 | output->shm_size = 0; |
| 32 | output->shm_stride = 0; |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | // Unmap client-side memory while keeping the wl_buffer proxy alive. |
| 37 | // The compositor retains its own fd reference and mapping, so the |
| 38 | // wallpaper remains visible after this call. |
| 39 | void chroma_buffer_unmap(chroma_output_t *output) { |
| 40 | if (!output || !output->shm_data) { |
| 41 | return; |
| 42 | } |
| 43 | |
| 44 | if (munmap(output->shm_data, output->shm_size) < 0) { |
| 45 | chroma_log("WARN", "munmap failed: %s", strerror(errno)); |
| 46 | } |
| 47 | output->shm_data = NULL; |
| 48 | output->shm_size = 0; |
| 49 | output->shm_stride = 0; |
| 50 | } |
| 51 | |
| 52 | // Create shared memory buffer for an output |
| 53 | int chroma_buffer_create(chroma_state_t *state, chroma_output_t *output) { |
| 54 | if (!state || !output || !state->shm) { |
| 55 | return CHROMA_ERROR_INIT; |
| 56 | } |
| 57 | |
| 58 | int width = output->width; |
| 59 | int height = output->height; |
| 60 | if (width <= 0 || height <= 0) { |
| 61 | return CHROMA_ERROR_INIT; |
| 62 | } |
| 63 | |
| 64 | int stride = width * 4; |
| 65 | size_t size = (size_t)stride * height; |
| 66 | |
| 67 | int fd = memfd_create("chroma-shm", MFD_CLOEXEC | MFD_ALLOW_SEALING); |
| 68 | if (fd < 0) { |
| 69 | // Fallback to shm_open if memfd_create is unavailable |
| 70 | char name[64]; |
| 71 | snprintf(name, sizeof(name), "/chroma-shm-%d-%d", getpid(), rand()); |
| 72 | fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); |
| 73 | if (fd >= 0) { |
| 74 | shm_unlink(name); |
| 75 | } |
| 76 | } |
| 77 | if (fd < 0) { |
| 78 | chroma_log("ERROR", "Failed to create shared memory fd: %s", |
| 79 | strerror(errno)); |
| 80 | return CHROMA_ERROR_MEMORY; |
| 81 | } |
| 82 | |
| 83 | if (ftruncate(fd, (off_t)size) < 0) { |
| 84 | chroma_log("ERROR", "ftruncate failed: %s", strerror(errno)); |
| 85 | close(fd); |
| 86 | return CHROMA_ERROR_MEMORY; |
| 87 | } |
| 88 | |
| 89 | void *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); |
| 90 | if (data == MAP_FAILED) { |
| 91 | chroma_log("ERROR", "mmap failed: %s", strerror(errno)); |
| 92 | close(fd); |
| 93 | return CHROMA_ERROR_MEMORY; |
| 94 | } |
| 95 | |
| 96 | // Fill with black |
| 97 | memset(data, 0, size); |
| 98 | |
| 99 | struct wl_shm_pool *pool = wl_shm_create_pool(state->shm, fd, (int32_t)size); |
| 100 | if (!pool) { |
| 101 | chroma_log("ERROR", "wl_shm_create_pool failed"); |
| 102 | munmap(data, size); |
| 103 | close(fd); |
| 104 | return CHROMA_ERROR_WAYLAND; |
| 105 | } |
| 106 | |
| 107 | struct wl_buffer *buffer = wl_shm_pool_create_buffer( |
| 108 | pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888); |
| 109 | wl_shm_pool_destroy(pool); |
| 110 | close(fd); |
| 111 | |
| 112 | if (!buffer) { |
| 113 | chroma_log("ERROR", "wl_shm_pool_create_buffer failed"); |
| 114 | munmap(data, size); |
| 115 | return CHROMA_ERROR_WAYLAND; |
| 116 | } |
| 117 | |
| 118 | output->shm_buffer = buffer; |
| 119 | output->shm_data = data; |
| 120 | output->shm_size = size; |
| 121 | output->shm_stride = stride; |
| 122 | |
| 123 | return CHROMA_OK; |
| 124 | } |
| 125 | |
| 126 | // Sample a pixel from image data (grayscale, grayscale+alpha, RGB, or RGBA) |
| 127 | static inline uint32_t sample_pixel(const chroma_image_t *image, int x, int y) { |
| 128 | if (!image || !image->data || x < 0 || y < 0 || x >= image->width || |
| 129 | y >= image->height) { |
| 130 | return 0xFF000000; // black with full alpha |
| 131 | } |
| 132 | |
| 133 | int idx = (y * image->width + x) * image->channels; |
| 134 | unsigned char r, g, b, a; |
| 135 | |
| 136 | switch (image->channels) { |
| 137 | case 1: |
| 138 | r = g = b = image->data[idx]; |
| 139 | a = 0xFF; |
| 140 | break; |
| 141 | case 2: |
| 142 | r = g = b = image->data[idx]; |
| 143 | a = image->data[idx + 1]; |
| 144 | break; |
| 145 | case 3: |
| 146 | r = image->data[idx]; |
| 147 | g = image->data[idx + 1]; |
| 148 | b = image->data[idx + 2]; |
| 149 | a = 0xFF; |
| 150 | break; |
| 151 | default: // 4 or more |
| 152 | r = image->data[idx]; |
| 153 | g = image->data[idx + 1]; |
| 154 | b = image->data[idx + 2]; |
| 155 | a = image->data[idx + 3]; |
| 156 | break; |
| 157 | } |
| 158 | |
| 159 | return ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | |
| 160 | (uint32_t)b; |
| 161 | } |
| 162 | |
| 163 | // Draw scaled image into the shared memory buffer |
| 164 | // When a transition is active, blends between prev buffer and new image |
| 165 | static void draw_scaled_image(chroma_output_t *output, chroma_image_t *image) { |
| 166 | if (!output || !image || !output->shm_data) { |
| 167 | return; |
| 168 | } |
| 169 | |
| 170 | int out_w = output->width; |
| 171 | int out_h = output->height; |
| 172 | int img_w = image->width; |
| 173 | int img_h = image->height; |
| 174 | uint32_t *dst = (uint32_t *)output->shm_data; |
| 175 | |
| 176 | // Save current buffer as previous frame for transition |
| 177 | bool has_prev = false; |
| 178 | unsigned char *prev_copy = NULL; |
| 179 | if (output->in_transition && output->shm_size > 0) { |
| 180 | prev_copy = malloc(output->shm_size); |
| 181 | if (prev_copy) { |
| 182 | memcpy(prev_copy, output->shm_data, output->shm_size); |
| 183 | has_prev = true; |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | // Pre-fill with black |
| 188 | memset(output->shm_data, 0, output->shm_size); |
| 189 | |
| 190 | float anchor_x = output->anchor_x; |
| 191 | float anchor_y = output->anchor_y; |
| 192 | |
| 193 | switch (output->scale_mode) { |
| 194 | case CHROMA_SCALE_STRETCH: { |
| 195 | for (int y = 0; y < out_h; y++) { |
| 196 | for (int x = 0; x < out_w; x++) { |
| 197 | int sx = (int)((float)x * img_w / out_w); |
| 198 | int sy = (int)((float)y * img_h / out_h); |
| 199 | sx = (sx < 0) ? 0 : (sx >= img_w) ? img_w - 1 : sx; |
| 200 | sy = (sy < 0) ? 0 : (sy >= img_h) ? img_h - 1 : sy; |
| 201 | dst[y * out_w + x] = sample_pixel(image, sx, sy); |
| 202 | } |
| 203 | } |
| 204 | break; |
| 205 | } |
| 206 | |
| 207 | case CHROMA_SCALE_FILL: { |
| 208 | float img_aspect = (float)img_w / (float)img_h; |
| 209 | float out_aspect = (float)out_w / (float)out_h; |
| 210 | |
| 211 | float crop_w, crop_h, crop_x, crop_y; |
| 212 | |
| 213 | if (img_aspect > out_aspect) { |
| 214 | // Image is wider - crop left/right |
| 215 | crop_h = (float)img_h; |
| 216 | crop_w = crop_h * out_aspect; |
| 217 | } else { |
| 218 | // Image is taller - crop top/bottom |
| 219 | crop_w = (float)img_w; |
| 220 | crop_h = crop_w / out_aspect; |
| 221 | } |
| 222 | |
| 223 | crop_x = (anchor_x / 100.0f) * ((float)img_w - crop_w); |
| 224 | crop_y = (anchor_y / 100.0f) * ((float)img_h - crop_h); |
| 225 | |
| 226 | // Clamp crop region |
| 227 | if (crop_x < 0) |
| 228 | crop_x = 0; |
| 229 | if (crop_y < 0) |
| 230 | crop_y = 0; |
| 231 | if (crop_x > (float)img_w - crop_w) |
| 232 | crop_x = (float)img_w - crop_w; |
| 233 | if (crop_y > (float)img_h - crop_h) |
| 234 | crop_y = (float)img_h - crop_h; |
| 235 | |
| 236 | for (int y = 0; y < out_h; y++) { |
| 237 | for (int x = 0; x < out_w; x++) { |
| 238 | float sx = crop_x + (float)x * crop_w / (float)out_w; |
| 239 | float sy = crop_y + (float)y * crop_h / (float)out_h; |
| 240 | int ix = (int)sx; |
| 241 | int iy = (int)sy; |
| 242 | dst[y * out_w + x] = sample_pixel(image, ix, iy); |
| 243 | } |
| 244 | } |
| 245 | break; |
| 246 | } |
| 247 | |
| 248 | case CHROMA_SCALE_FIT: { |
| 249 | float scale_x = (float)out_w / (float)img_w; |
| 250 | float scale_y = (float)out_h / (float)img_h; |
| 251 | float scale = (scale_x < scale_y) ? scale_x : scale_y; |
| 252 | |
| 253 | float scaled_w = (float)img_w * scale; |
| 254 | float scaled_h = (float)img_h * scale; |
| 255 | |
| 256 | float offset_x = (anchor_x / 100.0f) * ((float)out_w - scaled_w); |
| 257 | float offset_y = (anchor_y / 100.0f) * ((float)out_h - scaled_h); |
| 258 | |
| 259 | int start_x = (int)offset_x; |
| 260 | int start_y = (int)offset_y; |
| 261 | int end_x = (int)(offset_x + scaled_w + 0.5f); |
| 262 | int end_y = (int)(offset_y + scaled_h + 0.5f); |
| 263 | |
| 264 | if (start_x < 0) |
| 265 | start_x = 0; |
| 266 | if (start_y < 0) |
| 267 | start_y = 0; |
| 268 | if (end_x > out_w) |
| 269 | end_x = out_w; |
| 270 | if (end_y > out_h) |
| 271 | end_y = out_h; |
| 272 | |
| 273 | for (int y = start_y; y < end_y; y++) { |
| 274 | for (int x = start_x; x < end_x; x++) { |
| 275 | float sx = ((float)x - offset_x) / scale; |
| 276 | float sy = ((float)y - offset_y) / scale; |
| 277 | int ix = (int)sx; |
| 278 | int iy = (int)sy; |
| 279 | ix = (ix < 0) ? 0 : (ix >= img_w) ? img_w - 1 : ix; |
| 280 | iy = (iy < 0) ? 0 : (iy >= img_h) ? img_h - 1 : iy; |
| 281 | dst[y * out_w + x] = sample_pixel(image, ix, iy); |
| 282 | } |
| 283 | } |
| 284 | break; |
| 285 | } |
| 286 | |
| 287 | case CHROMA_SCALE_CENTER: |
| 288 | default: { |
| 289 | if (img_w <= out_w && img_h <= out_h) { |
| 290 | // Image fits entirely - position with anchor |
| 291 | float offset_x = (anchor_x / 100.0f) * ((float)out_w - (float)img_w); |
| 292 | float offset_y = (anchor_y / 100.0f) * ((float)out_h - (float)img_h); |
| 293 | |
| 294 | for (int y = 0; y < img_h; y++) { |
| 295 | for (int x = 0; x < img_w; x++) { |
| 296 | int dx = (int)(offset_x + x); |
| 297 | int dy = (int)(offset_y + y); |
| 298 | if (dx >= 0 && dx < out_w && dy >= 0 && dy < out_h) { |
| 299 | dst[dy * out_w + dx] = sample_pixel(image, x, y); |
| 300 | } |
| 301 | } |
| 302 | } |
| 303 | } else { |
| 304 | // Image is larger than output - clip with anchor |
| 305 | float src_offset_x = (anchor_x / 100.0f) * ((float)img_w - (float)out_w); |
| 306 | float src_offset_y = (anchor_y / 100.0f) * ((float)img_h - (float)out_h); |
| 307 | |
| 308 | if (src_offset_x < 0) |
| 309 | src_offset_x = 0; |
| 310 | if (src_offset_y < 0) |
| 311 | src_offset_y = 0; |
| 312 | if (src_offset_x > (float)img_w - (float)out_w) |
| 313 | src_offset_x = (float)img_w - (float)out_w; |
| 314 | if (src_offset_y > (float)img_h - (float)out_h) |
| 315 | src_offset_y = (float)img_h - (float)out_h; |
| 316 | |
| 317 | for (int y = 0; y < out_h; y++) { |
| 318 | for (int x = 0; x < out_w; x++) { |
| 319 | int sx = (int)(src_offset_x + x); |
| 320 | int sy = (int)(src_offset_y + y); |
| 321 | dst[y * out_w + x] = sample_pixel(image, sx, sy); |
| 322 | } |
| 323 | } |
| 324 | } |
| 325 | break; |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | // Apply crossfade transition blend |
| 330 | if (has_prev && prev_copy) { |
| 331 | float alpha = chroma_transition_get_alpha(output, chroma_get_time_ms()); |
| 332 | uint32_t *prev = (uint32_t *)prev_copy; |
| 333 | |
| 334 | for (int i = 0; i < out_w * out_h; i++) { |
| 335 | uint32_t new_px = dst[i]; |
| 336 | uint32_t old_px = prev[i]; |
| 337 | |
| 338 | unsigned char na = (unsigned char)(new_px >> 24); |
| 339 | unsigned char nr = (unsigned char)(new_px >> 16); |
| 340 | unsigned char ng = (unsigned char)(new_px >> 8); |
| 341 | unsigned char nb = (unsigned char)(new_px); |
| 342 | |
| 343 | unsigned char oa = (unsigned char)(old_px >> 24); |
| 344 | unsigned char or_ = (unsigned char)(old_px >> 16); |
| 345 | unsigned char og = (unsigned char)(old_px >> 8); |
| 346 | unsigned char ob = (unsigned char)(old_px); |
| 347 | |
| 348 | unsigned char fa = |
| 349 | (unsigned char)(oa + |
| 350 | (unsigned char)((float)((int)na - (int)oa) * alpha)); |
| 351 | unsigned char fr = |
| 352 | (unsigned char)(or_ + |
| 353 | (unsigned char)((float)((int)nr - (int)or_) * alpha)); |
| 354 | unsigned char fg = |
| 355 | (unsigned char)(og + |
| 356 | (unsigned char)((float)((int)ng - (int)og) * alpha)); |
| 357 | unsigned char fb = |
| 358 | (unsigned char)(ob + |
| 359 | (unsigned char)((float)((int)nb - (int)ob) * alpha)); |
| 360 | |
| 361 | dst[i] = ((uint32_t)fa << 24) | ((uint32_t)fr << 16) | |
| 362 | ((uint32_t)fg << 8) | (uint32_t)fb; |
| 363 | } |
| 364 | |
| 365 | free(prev_copy); |
| 366 | } |
| 367 | } |
| 368 | |
| 369 | // Create surface for output |
| 370 | int chroma_surface_create(chroma_state_t *state, chroma_output_t *output) { |
| 371 | if (!state || !output || !state->compositor || !state->layer_shell) { |
| 372 | return CHROMA_ERROR_INIT; |
| 373 | } |
| 374 | |
| 375 | // Create Wayland surface |
| 376 | output->surface = wl_compositor_create_surface(state->compositor); |
| 377 | if (!output->surface) { |
| 378 | chroma_log("ERROR", "Failed to create Wayland surface for output %u", |
| 379 | output->id); |
| 380 | return CHROMA_ERROR_WAYLAND; |
| 381 | } |
| 382 | |
| 383 | // Create layer surface for wallpaper |
| 384 | output->layer_surface = zwlr_layer_shell_v1_get_layer_surface( |
| 385 | state->layer_shell, output->surface, output->wl_output, |
| 386 | ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, "chroma-wallpaper"); |
| 387 | |
| 388 | if (!output->layer_surface) { |
| 389 | chroma_log("ERROR", "Failed to create layer surface for output %u", |
| 390 | output->id); |
| 391 | chroma_surface_destroy(output); |
| 392 | return CHROMA_ERROR_WAYLAND; |
| 393 | } |
| 394 | |
| 395 | // Configure layer surface |
| 396 | zwlr_layer_surface_v1_set_size(output->layer_surface, (uint32_t)output->width, |
| 397 | (uint32_t)output->height); |
| 398 | zwlr_layer_surface_v1_set_anchor(output->layer_surface, |
| 399 | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | |
| 400 | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT | |
| 401 | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | |
| 402 | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); |
| 403 | zwlr_layer_surface_v1_set_exclusive_zone(output->layer_surface, -1); |
| 404 | zwlr_layer_surface_v1_set_keyboard_interactivity( |
| 405 | output->layer_surface, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE); |
| 406 | |
| 407 | // Add layer surface listener |
| 408 | zwlr_layer_surface_v1_add_listener( |
| 409 | output->layer_surface, &chroma_layer_surface_listener_impl, output); |
| 410 | |
| 411 | // Commit surface to trigger configure event |
| 412 | wl_surface_commit(output->surface); |
| 413 | |
| 414 | // Wait for configure event |
| 415 | wl_display_roundtrip(state->display); |
| 416 | |
| 417 | chroma_log("INFO", "Created surface for output %u (%dx%d)", output->id, |
| 418 | output->width, output->height); |
| 419 | |
| 420 | return CHROMA_OK; |
| 421 | } |
| 422 | |
| 423 | // Destroy surface |
| 424 | void chroma_surface_destroy(chroma_output_t *output) { |
| 425 | if (!output) { |
| 426 | return; |
| 427 | } |
| 428 | |
| 429 | chroma_buffer_destroy(output); |
| 430 | |
| 431 | if (output->layer_surface) { |
| 432 | zwlr_layer_surface_v1_destroy(output->layer_surface); |
| 433 | output->layer_surface = NULL; |
| 434 | } |
| 435 | |
| 436 | if (output->surface) { |
| 437 | wl_surface_destroy(output->surface); |
| 438 | output->surface = NULL; |
| 439 | } |
| 440 | |
| 441 | output->configured = false; |
| 442 | |
| 443 | chroma_log("DEBUG", "Destroyed surface for output %u", output->id); |
| 444 | } |
| 445 | |
| 446 | // Render wallpaper to output |
| 447 | int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { |
| 448 | if (!state || !output || !output->image || !output->image->loaded) { |
| 449 | return CHROMA_ERROR_INIT; |
| 450 | } |
| 451 | |
| 452 | if (!output->configured) { |
| 453 | chroma_log("WARN", "Output %u not yet configured, skipping render", |
| 454 | output->id); |
| 455 | return CHROMA_ERROR_INIT; |
| 456 | } |
| 457 | |
| 458 | // Clean up old buffer |
| 459 | chroma_buffer_destroy(output); |
| 460 | |
| 461 | // Create new shared memory buffer |
| 462 | int ret = chroma_buffer_create(state, output); |
| 463 | if (ret != CHROMA_OK) { |
| 464 | chroma_log("ERROR", "Failed to create shm buffer for output %u", |
| 465 | output->id); |
| 466 | return ret; |
| 467 | } |
| 468 | |
| 469 | // Draw scaled image into buffer |
| 470 | draw_scaled_image(output, output->image); |
| 471 | |
| 472 | // Attach buffer to surface |
| 473 | wl_surface_attach(output->surface, output->shm_buffer, 0, 0); |
| 474 | wl_surface_damage_buffer(output->surface, 0, 0, output->width, |
| 475 | output->height); |
| 476 | wl_surface_commit(output->surface); |
| 477 | if (wl_display_flush(state->display) < 0) { |
| 478 | chroma_log("ERROR", "Failed to flush Wayland display for output %u: %s", |
| 479 | output->id, strerror(errno)); |
| 480 | return CHROMA_ERROR_WAYLAND; |
| 481 | } |
| 482 | |
| 483 | chroma_log("INFO", "Rendered wallpaper to output %u (%dx%d)", output->id, |
| 484 | output->width, output->height); |
| 485 | |
| 486 | // Unmap client-side memory; compositor keeps its own fd reference |
| 487 | chroma_buffer_unmap(output); |
| 488 | |
| 489 | return CHROMA_OK; |
| 490 | } |