| 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 | // Create shared memory buffer for an output |
| 37 | int chroma_buffer_create(chroma_state_t *state, chroma_output_t *output) { |
| 38 | if (!state || !output || !state->shm) { |
| 39 | return CHROMA_ERROR_INIT; |
| 40 | } |
| 41 | |
| 42 | int width = output->width; |
| 43 | int height = output->height; |
| 44 | if (width <= 0 || height <= 0) { |
| 45 | return CHROMA_ERROR_INIT; |
| 46 | } |
| 47 | |
| 48 | int stride = width * 4; |
| 49 | size_t size = (size_t)stride * height; |
| 50 | |
| 51 | int fd = memfd_create("chroma-shm", MFD_CLOEXEC | MFD_ALLOW_SEALING); |
| 52 | if (fd < 0) { |
| 53 | // Fallback to shm_open if memfd_create is unavailable |
| 54 | char name[64]; |
| 55 | snprintf(name, sizeof(name), "/chroma-shm-%d-%d", getpid(), rand()); |
| 56 | fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); |
| 57 | if (fd >= 0) { |
| 58 | shm_unlink(name); |
| 59 | } |
| 60 | } |
| 61 | if (fd < 0) { |
| 62 | chroma_log("ERROR", "Failed to create shared memory fd: %s", |
| 63 | strerror(errno)); |
| 64 | return CHROMA_ERROR_MEMORY; |
| 65 | } |
| 66 | |
| 67 | if (ftruncate(fd, (off_t)size) < 0) { |
| 68 | chroma_log("ERROR", "ftruncate failed: %s", strerror(errno)); |
| 69 | close(fd); |
| 70 | return CHROMA_ERROR_MEMORY; |
| 71 | } |
| 72 | |
| 73 | void *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); |
| 74 | if (data == MAP_FAILED) { |
| 75 | chroma_log("ERROR", "mmap failed: %s", strerror(errno)); |
| 76 | close(fd); |
| 77 | return CHROMA_ERROR_MEMORY; |
| 78 | } |
| 79 | |
| 80 | // Fill with black |
| 81 | memset(data, 0, size); |
| 82 | |
| 83 | struct wl_shm_pool *pool = |
| 84 | wl_shm_create_pool(state->shm, fd, (int32_t)size); |
| 85 | if (!pool) { |
| 86 | chroma_log("ERROR", "wl_shm_create_pool failed"); |
| 87 | munmap(data, size); |
| 88 | close(fd); |
| 89 | return CHROMA_ERROR_WAYLAND; |
| 90 | } |
| 91 | |
| 92 | struct wl_buffer *buffer = wl_shm_pool_create_buffer( |
| 93 | pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888); |
| 94 | wl_shm_pool_destroy(pool); |
| 95 | close(fd); |
| 96 | |
| 97 | if (!buffer) { |
| 98 | chroma_log("ERROR", "wl_shm_pool_create_buffer failed"); |
| 99 | munmap(data, size); |
| 100 | return CHROMA_ERROR_WAYLAND; |
| 101 | } |
| 102 | |
| 103 | output->shm_buffer = buffer; |
| 104 | output->shm_data = data; |
| 105 | output->shm_size = size; |
| 106 | output->shm_stride = stride; |
| 107 | |
| 108 | return CHROMA_OK; |
| 109 | } |
| 110 | |
| 111 | // Sample a pixel from image data (grayscale, grayscale+alpha, RGB, or RGBA) |
| 112 | static inline uint32_t sample_pixel(const chroma_image_t *image, int x, |
| 113 | int y) { |
| 114 | if (!image || !image->data || x < 0 || y < 0 || x >= image->width || |
| 115 | y >= image->height) { |
| 116 | return 0xFF000000; // black with full alpha |
| 117 | } |
| 118 | |
| 119 | int idx = (y * image->width + x) * image->channels; |
| 120 | unsigned char r, g, b, a; |
| 121 | |
| 122 | switch (image->channels) { |
| 123 | case 1: |
| 124 | r = g = b = image->data[idx]; |
| 125 | a = 0xFF; |
| 126 | break; |
| 127 | case 2: |
| 128 | r = g = b = image->data[idx]; |
| 129 | a = image->data[idx + 1]; |
| 130 | break; |
| 131 | case 3: |
| 132 | r = image->data[idx]; |
| 133 | g = image->data[idx + 1]; |
| 134 | b = image->data[idx + 2]; |
| 135 | a = 0xFF; |
| 136 | break; |
| 137 | default: // 4 or more |
| 138 | r = image->data[idx]; |
| 139 | g = image->data[idx + 1]; |
| 140 | b = image->data[idx + 2]; |
| 141 | a = image->data[idx + 3]; |
| 142 | break; |
| 143 | } |
| 144 | |
| 145 | return ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | |
| 146 | (uint32_t)b; |
| 147 | } |
| 148 | |
| 149 | // Draw scaled image into the shared memory buffer |
| 150 | static void draw_scaled_image(chroma_output_t *output, |
| 151 | chroma_image_t *image) { |
| 152 | if (!output || !image || !output->shm_data) { |
| 153 | return; |
| 154 | } |
| 155 | |
| 156 | int out_w = output->width; |
| 157 | int out_h = output->height; |
| 158 | int img_w = image->width; |
| 159 | int img_h = image->height; |
| 160 | uint32_t *dst = (uint32_t *)output->shm_data; |
| 161 | |
| 162 | // Pre-fill with black |
| 163 | memset(output->shm_data, 0, output->shm_size); |
| 164 | |
| 165 | float anchor_x = output->anchor_x; |
| 166 | float anchor_y = output->anchor_y; |
| 167 | |
| 168 | switch (output->scale_mode) { |
| 169 | case CHROMA_SCALE_STRETCH: { |
| 170 | for (int y = 0; y < out_h; y++) { |
| 171 | for (int x = 0; x < out_w; x++) { |
| 172 | int sx = (int)((float)x * img_w / out_w); |
| 173 | int sy = (int)((float)y * img_h / out_h); |
| 174 | sx = (sx < 0) ? 0 : (sx >= img_w) ? img_w - 1 : sx; |
| 175 | sy = (sy < 0) ? 0 : (sy >= img_h) ? img_h - 1 : sy; |
| 176 | dst[y * out_w + x] = sample_pixel(image, sx, sy); |
| 177 | } |
| 178 | } |
| 179 | break; |
| 180 | } |
| 181 | |
| 182 | case CHROMA_SCALE_FILL: { |
| 183 | float img_aspect = (float)img_w / (float)img_h; |
| 184 | float out_aspect = (float)out_w / (float)out_h; |
| 185 | |
| 186 | float crop_w, crop_h, crop_x, crop_y; |
| 187 | |
| 188 | if (img_aspect > out_aspect) { |
| 189 | // Image is wider - crop left/right |
| 190 | crop_h = (float)img_h; |
| 191 | crop_w = crop_h * out_aspect; |
| 192 | } else { |
| 193 | // Image is taller - crop top/bottom |
| 194 | crop_w = (float)img_w; |
| 195 | crop_h = crop_w / out_aspect; |
| 196 | } |
| 197 | |
| 198 | crop_x = (anchor_x / 100.0f) * ((float)img_w - crop_w); |
| 199 | crop_y = (anchor_y / 100.0f) * ((float)img_h - crop_h); |
| 200 | |
| 201 | // Clamp crop region |
| 202 | if (crop_x < 0) |
| 203 | crop_x = 0; |
| 204 | if (crop_y < 0) |
| 205 | crop_y = 0; |
| 206 | if (crop_x > (float)img_w - crop_w) |
| 207 | crop_x = (float)img_w - crop_w; |
| 208 | if (crop_y > (float)img_h - crop_h) |
| 209 | crop_y = (float)img_h - crop_h; |
| 210 | |
| 211 | for (int y = 0; y < out_h; y++) { |
| 212 | for (int x = 0; x < out_w; x++) { |
| 213 | float sx = crop_x + (float)x * crop_w / (float)out_w; |
| 214 | float sy = crop_y + (float)y * crop_h / (float)out_h; |
| 215 | int ix = (int)sx; |
| 216 | int iy = (int)sy; |
| 217 | dst[y * out_w + x] = sample_pixel(image, ix, iy); |
| 218 | } |
| 219 | } |
| 220 | break; |
| 221 | } |
| 222 | |
| 223 | case CHROMA_SCALE_FIT: { |
| 224 | float scale_x = (float)out_w / (float)img_w; |
| 225 | float scale_y = (float)out_h / (float)img_h; |
| 226 | float scale = (scale_x < scale_y) ? scale_x : scale_y; |
| 227 | |
| 228 | float scaled_w = (float)img_w * scale; |
| 229 | float scaled_h = (float)img_h * scale; |
| 230 | |
| 231 | float offset_x = (anchor_x / 100.0f) * ((float)out_w - scaled_w); |
| 232 | float offset_y = (anchor_y / 100.0f) * ((float)out_h - scaled_h); |
| 233 | |
| 234 | int start_x = (int)offset_x; |
| 235 | int start_y = (int)offset_y; |
| 236 | int end_x = (int)(offset_x + scaled_w + 0.5f); |
| 237 | int end_y = (int)(offset_y + scaled_h + 0.5f); |
| 238 | |
| 239 | if (start_x < 0) |
| 240 | start_x = 0; |
| 241 | if (start_y < 0) |
| 242 | start_y = 0; |
| 243 | if (end_x > out_w) |
| 244 | end_x = out_w; |
| 245 | if (end_y > out_h) |
| 246 | end_y = out_h; |
| 247 | |
| 248 | for (int y = start_y; y < end_y; y++) { |
| 249 | for (int x = start_x; x < end_x; x++) { |
| 250 | float sx = ((float)x - offset_x) / scale; |
| 251 | float sy = ((float)y - offset_y) / scale; |
| 252 | int ix = (int)sx; |
| 253 | int iy = (int)sy; |
| 254 | ix = (ix < 0) ? 0 : (ix >= img_w) ? img_w - 1 : ix; |
| 255 | iy = (iy < 0) ? 0 : (iy >= img_h) ? img_h - 1 : iy; |
| 256 | dst[y * out_w + x] = sample_pixel(image, ix, iy); |
| 257 | } |
| 258 | } |
| 259 | break; |
| 260 | } |
| 261 | |
| 262 | case CHROMA_SCALE_CENTER: |
| 263 | default: { |
| 264 | if (img_w <= out_w && img_h <= out_h) { |
| 265 | // Image fits entirely - position with anchor |
| 266 | float offset_x = |
| 267 | (anchor_x / 100.0f) * ((float)out_w - (float)img_w); |
| 268 | float offset_y = |
| 269 | (anchor_y / 100.0f) * ((float)out_h - (float)img_h); |
| 270 | |
| 271 | for (int y = 0; y < img_h; y++) { |
| 272 | for (int x = 0; x < img_w; x++) { |
| 273 | int dx = (int)(offset_x + x); |
| 274 | int dy = (int)(offset_y + y); |
| 275 | if (dx >= 0 && dx < out_w && dy >= 0 && dy < out_h) { |
| 276 | dst[dy * out_w + dx] = sample_pixel(image, x, y); |
| 277 | } |
| 278 | } |
| 279 | } |
| 280 | } else { |
| 281 | // Image is larger than output - clip with anchor |
| 282 | float src_offset_x = |
| 283 | (anchor_x / 100.0f) * ((float)img_w - (float)out_w); |
| 284 | float src_offset_y = |
| 285 | (anchor_y / 100.0f) * ((float)img_h - (float)out_h); |
| 286 | |
| 287 | if (src_offset_x < 0) |
| 288 | src_offset_x = 0; |
| 289 | if (src_offset_y < 0) |
| 290 | src_offset_y = 0; |
| 291 | if (src_offset_x > (float)img_w - (float)out_w) |
| 292 | src_offset_x = (float)img_w - (float)out_w; |
| 293 | if (src_offset_y > (float)img_h - (float)out_h) |
| 294 | src_offset_y = (float)img_h - (float)out_h; |
| 295 | |
| 296 | for (int y = 0; y < out_h; y++) { |
| 297 | for (int x = 0; x < out_w; x++) { |
| 298 | int sx = (int)(src_offset_x + x); |
| 299 | int sy = (int)(src_offset_y + y); |
| 300 | dst[y * out_w + x] = sample_pixel(image, sx, sy); |
| 301 | } |
| 302 | } |
| 303 | } |
| 304 | break; |
| 305 | } |
| 306 | } |
| 307 | } |
| 308 | |
| 309 | // Create surface for output |
| 310 | int chroma_surface_create(chroma_state_t *state, chroma_output_t *output) { |
| 311 | if (!state || !output || !state->compositor || !state->layer_shell) { |
| 312 | return CHROMA_ERROR_INIT; |
| 313 | } |
| 314 | |
| 315 | // Create Wayland surface |
| 316 | output->surface = wl_compositor_create_surface(state->compositor); |
| 317 | if (!output->surface) { |
| 318 | chroma_log("ERROR", "Failed to create Wayland surface for output %u", |
| 319 | output->id); |
| 320 | return CHROMA_ERROR_WAYLAND; |
| 321 | } |
| 322 | |
| 323 | // Create layer surface for wallpaper |
| 324 | output->layer_surface = zwlr_layer_shell_v1_get_layer_surface( |
| 325 | state->layer_shell, output->surface, output->wl_output, |
| 326 | ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, "chroma-wallpaper"); |
| 327 | |
| 328 | if (!output->layer_surface) { |
| 329 | chroma_log("ERROR", "Failed to create layer surface for output %u", |
| 330 | output->id); |
| 331 | chroma_surface_destroy(output); |
| 332 | return CHROMA_ERROR_WAYLAND; |
| 333 | } |
| 334 | |
| 335 | // Configure layer surface |
| 336 | zwlr_layer_surface_v1_set_size(output->layer_surface, |
| 337 | (uint32_t)output->width, |
| 338 | (uint32_t)output->height); |
| 339 | zwlr_layer_surface_v1_set_anchor( |
| 340 | output->layer_surface, |
| 341 | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | |
| 342 | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT | |
| 343 | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | |
| 344 | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); |
| 345 | zwlr_layer_surface_v1_set_exclusive_zone(output->layer_surface, -1); |
| 346 | zwlr_layer_surface_v1_set_keyboard_interactivity( |
| 347 | output->layer_surface, |
| 348 | ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE); |
| 349 | |
| 350 | // Add layer surface listener |
| 351 | zwlr_layer_surface_v1_add_listener( |
| 352 | output->layer_surface, &chroma_layer_surface_listener_impl, output); |
| 353 | |
| 354 | // Commit surface to trigger configure event |
| 355 | wl_surface_commit(output->surface); |
| 356 | |
| 357 | // Wait for configure event |
| 358 | wl_display_roundtrip(state->display); |
| 359 | |
| 360 | chroma_log("INFO", "Created surface for output %u (%dx%d)", output->id, |
| 361 | output->width, output->height); |
| 362 | |
| 363 | return CHROMA_OK; |
| 364 | } |
| 365 | |
| 366 | // Destroy surface |
| 367 | void chroma_surface_destroy(chroma_output_t *output) { |
| 368 | if (!output) { |
| 369 | return; |
| 370 | } |
| 371 | |
| 372 | chroma_buffer_destroy(output); |
| 373 | |
| 374 | if (output->layer_surface) { |
| 375 | zwlr_layer_surface_v1_destroy(output->layer_surface); |
| 376 | output->layer_surface = NULL; |
| 377 | } |
| 378 | |
| 379 | if (output->surface) { |
| 380 | wl_surface_destroy(output->surface); |
| 381 | output->surface = NULL; |
| 382 | } |
| 383 | |
| 384 | output->configured = false; |
| 385 | |
| 386 | chroma_log("DEBUG", "Destroyed surface for output %u", output->id); |
| 387 | } |
| 388 | |
| 389 | // Render wallpaper to output |
| 390 | int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { |
| 391 | if (!state || !output || !output->image || !output->image->loaded) { |
| 392 | return CHROMA_ERROR_INIT; |
| 393 | } |
| 394 | |
| 395 | if (!output->configured) { |
| 396 | chroma_log("WARN", "Output %u not yet configured, skipping render", |
| 397 | output->id); |
| 398 | return CHROMA_ERROR_INIT; |
| 399 | } |
| 400 | |
| 401 | // Clean up old buffer |
| 402 | chroma_buffer_destroy(output); |
| 403 | |
| 404 | // Create new shared memory buffer |
| 405 | int ret = chroma_buffer_create(state, output); |
| 406 | if (ret != CHROMA_OK) { |
| 407 | chroma_log("ERROR", "Failed to create shm buffer for output %u", |
| 408 | output->id); |
| 409 | return ret; |
| 410 | } |
| 411 | |
| 412 | // Draw scaled image into buffer |
| 413 | draw_scaled_image(output, output->image); |
| 414 | |
| 415 | // Attach buffer to surface |
| 416 | wl_surface_attach(output->surface, output->shm_buffer, 0, 0); |
| 417 | wl_surface_damage_buffer(output->surface, 0, 0, output->width, |
| 418 | output->height); |
| 419 | wl_surface_commit(output->surface); |
| 420 | wl_display_flush(state->display); |
| 421 | |
| 422 | chroma_log("INFO", "Rendered wallpaper to output %u (%dx%d)", output->id, |
| 423 | output->width, output->height); |
| 424 | |
| 425 | // Release image reference and free CPU data if no longer needed |
| 426 | if (output->image) { |
| 427 | chroma_image_t *image = output->image; |
| 428 | chroma_image_release(image); |
| 429 | output->image = NULL; |
| 430 | } |
| 431 | |
| 432 | return CHROMA_OK; |
| 433 | } |