brewery
notashelf /
292491b7388d7799a4e5ed0ddeb0265872b2f9d2

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git
src/render.cC490 lines14.4 KB
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
}