brewery
notashelf /
308c82f479ec29a9681e1702478bf2123fcaf0d5

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git
src/render.cC446 lines12.8 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
static void draw_scaled_image(chroma_output_t *output, chroma_image_t *image) {
165
  if (!output || !image || !output->shm_data) {
166
    return;
167
  }
168
169
  int out_w = output->width;
170
  int out_h = output->height;
171
  int img_w = image->width;
172
  int img_h = image->height;
173
  uint32_t *dst = (uint32_t *)output->shm_data;
174
175
  // Pre-fill with black
176
  memset(output->shm_data, 0, output->shm_size);
177
178
  float anchor_x = output->anchor_x;
179
  float anchor_y = output->anchor_y;
180
181
  switch (output->scale_mode) {
182
  case CHROMA_SCALE_STRETCH: {
183
    for (int y = 0; y < out_h; y++) {
184
      for (int x = 0; x < out_w; x++) {
185
        int sx = (int)((float)x * img_w / out_w);
186
        int sy = (int)((float)y * img_h / out_h);
187
        sx = (sx < 0) ? 0 : (sx >= img_w) ? img_w - 1 : sx;
188
        sy = (sy < 0) ? 0 : (sy >= img_h) ? img_h - 1 : sy;
189
        dst[y * out_w + x] = sample_pixel(image, sx, sy);
190
      }
191
    }
192
    break;
193
  }
194
195
  case CHROMA_SCALE_FILL: {
196
    float img_aspect = (float)img_w / (float)img_h;
197
    float out_aspect = (float)out_w / (float)out_h;
198
199
    float crop_w, crop_h, crop_x, crop_y;
200
201
    if (img_aspect > out_aspect) {
202
      // Image is wider - crop left/right
203
      crop_h = (float)img_h;
204
      crop_w = crop_h * out_aspect;
205
    } else {
206
      // Image is taller - crop top/bottom
207
      crop_w = (float)img_w;
208
      crop_h = crop_w / out_aspect;
209
    }
210
211
    crop_x = (anchor_x / 100.0f) * ((float)img_w - crop_w);
212
    crop_y = (anchor_y / 100.0f) * ((float)img_h - crop_h);
213
214
    // Clamp crop region
215
    if (crop_x < 0)
216
      crop_x = 0;
217
    if (crop_y < 0)
218
      crop_y = 0;
219
    if (crop_x > (float)img_w - crop_w)
220
      crop_x = (float)img_w - crop_w;
221
    if (crop_y > (float)img_h - crop_h)
222
      crop_y = (float)img_h - crop_h;
223
224
    for (int y = 0; y < out_h; y++) {
225
      for (int x = 0; x < out_w; x++) {
226
        float sx = crop_x + (float)x * crop_w / (float)out_w;
227
        float sy = crop_y + (float)y * crop_h / (float)out_h;
228
        int ix = (int)sx;
229
        int iy = (int)sy;
230
        dst[y * out_w + x] = sample_pixel(image, ix, iy);
231
      }
232
    }
233
    break;
234
  }
235
236
  case CHROMA_SCALE_FIT: {
237
    float scale_x = (float)out_w / (float)img_w;
238
    float scale_y = (float)out_h / (float)img_h;
239
    float scale = (scale_x < scale_y) ? scale_x : scale_y;
240
241
    float scaled_w = (float)img_w * scale;
242
    float scaled_h = (float)img_h * scale;
243
244
    float offset_x = (anchor_x / 100.0f) * ((float)out_w - scaled_w);
245
    float offset_y = (anchor_y / 100.0f) * ((float)out_h - scaled_h);
246
247
    int start_x = (int)offset_x;
248
    int start_y = (int)offset_y;
249
    int end_x = (int)(offset_x + scaled_w + 0.5f);
250
    int end_y = (int)(offset_y + scaled_h + 0.5f);
251
252
    if (start_x < 0)
253
      start_x = 0;
254
    if (start_y < 0)
255
      start_y = 0;
256
    if (end_x > out_w)
257
      end_x = out_w;
258
    if (end_y > out_h)
259
      end_y = out_h;
260
261
    for (int y = start_y; y < end_y; y++) {
262
      for (int x = start_x; x < end_x; x++) {
263
        float sx = ((float)x - offset_x) / scale;
264
        float sy = ((float)y - offset_y) / scale;
265
        int ix = (int)sx;
266
        int iy = (int)sy;
267
        ix = (ix < 0) ? 0 : (ix >= img_w) ? img_w - 1 : ix;
268
        iy = (iy < 0) ? 0 : (iy >= img_h) ? img_h - 1 : iy;
269
        dst[y * out_w + x] = sample_pixel(image, ix, iy);
270
      }
271
    }
272
    break;
273
  }
274
275
  case CHROMA_SCALE_CENTER:
276
  default: {
277
    if (img_w <= out_w && img_h <= out_h) {
278
      // Image fits entirely - position with anchor
279
      float offset_x = (anchor_x / 100.0f) * ((float)out_w - (float)img_w);
280
      float offset_y = (anchor_y / 100.0f) * ((float)out_h - (float)img_h);
281
282
      for (int y = 0; y < img_h; y++) {
283
        for (int x = 0; x < img_w; x++) {
284
          int dx = (int)(offset_x + x);
285
          int dy = (int)(offset_y + y);
286
          if (dx >= 0 && dx < out_w && dy >= 0 && dy < out_h) {
287
            dst[dy * out_w + dx] = sample_pixel(image, x, y);
288
          }
289
        }
290
      }
291
    } else {
292
      // Image is larger than output - clip with anchor
293
      float src_offset_x = (anchor_x / 100.0f) * ((float)img_w - (float)out_w);
294
      float src_offset_y = (anchor_y / 100.0f) * ((float)img_h - (float)out_h);
295
296
      if (src_offset_x < 0)
297
        src_offset_x = 0;
298
      if (src_offset_y < 0)
299
        src_offset_y = 0;
300
      if (src_offset_x > (float)img_w - (float)out_w)
301
        src_offset_x = (float)img_w - (float)out_w;
302
      if (src_offset_y > (float)img_h - (float)out_h)
303
        src_offset_y = (float)img_h - (float)out_h;
304
305
      for (int y = 0; y < out_h; y++) {
306
        for (int x = 0; x < out_w; x++) {
307
          int sx = (int)(src_offset_x + x);
308
          int sy = (int)(src_offset_y + y);
309
          dst[y * out_w + x] = sample_pixel(image, sx, sy);
310
        }
311
      }
312
    }
313
    break;
314
  }
315
  }
316
}
317
318
// Create surface for output
319
int chroma_surface_create(chroma_state_t *state, chroma_output_t *output) {
320
  if (!state || !output || !state->compositor || !state->layer_shell) {
321
    return CHROMA_ERROR_INIT;
322
  }
323
324
  // Create Wayland surface
325
  output->surface = wl_compositor_create_surface(state->compositor);
326
  if (!output->surface) {
327
    chroma_log("ERROR", "Failed to create Wayland surface for output %u",
328
               output->id);
329
    return CHROMA_ERROR_WAYLAND;
330
  }
331
332
  // Create layer surface for wallpaper
333
  output->layer_surface = zwlr_layer_shell_v1_get_layer_surface(
334
      state->layer_shell, output->surface, output->wl_output,
335
      ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, "chroma-wallpaper");
336
337
  if (!output->layer_surface) {
338
    chroma_log("ERROR", "Failed to create layer surface for output %u",
339
               output->id);
340
    chroma_surface_destroy(output);
341
    return CHROMA_ERROR_WAYLAND;
342
  }
343
344
  // Configure layer surface
345
  zwlr_layer_surface_v1_set_size(output->layer_surface, (uint32_t)output->width,
346
                                 (uint32_t)output->height);
347
  zwlr_layer_surface_v1_set_anchor(output->layer_surface,
348
                                   ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
349
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
350
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM |
351
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT);
352
  zwlr_layer_surface_v1_set_exclusive_zone(output->layer_surface, -1);
353
  zwlr_layer_surface_v1_set_keyboard_interactivity(
354
      output->layer_surface, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE);
355
356
  // Add layer surface listener
357
  zwlr_layer_surface_v1_add_listener(
358
      output->layer_surface, &chroma_layer_surface_listener_impl, output);
359
360
  // Commit surface to trigger configure event
361
  wl_surface_commit(output->surface);
362
363
  // Wait for configure event
364
  wl_display_roundtrip(state->display);
365
366
  chroma_log("INFO", "Created surface for output %u (%dx%d)", output->id,
367
             output->width, output->height);
368
369
  return CHROMA_OK;
370
}
371
372
// Destroy surface
373
void chroma_surface_destroy(chroma_output_t *output) {
374
  if (!output) {
375
    return;
376
  }
377
378
  chroma_buffer_destroy(output);
379
380
  if (output->layer_surface) {
381
    zwlr_layer_surface_v1_destroy(output->layer_surface);
382
    output->layer_surface = NULL;
383
  }
384
385
  if (output->surface) {
386
    wl_surface_destroy(output->surface);
387
    output->surface = NULL;
388
  }
389
390
  output->configured = false;
391
392
  chroma_log("DEBUG", "Destroyed surface for output %u", output->id);
393
}
394
395
// Render wallpaper to output
396
int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
397
  if (!state || !output || !output->image || !output->image->loaded) {
398
    return CHROMA_ERROR_INIT;
399
  }
400
401
  if (!output->configured) {
402
    chroma_log("WARN", "Output %u not yet configured, skipping render",
403
               output->id);
404
    return CHROMA_ERROR_INIT;
405
  }
406
407
  // Clean up old buffer
408
  chroma_buffer_destroy(output);
409
410
  // Create new shared memory buffer
411
  int ret = chroma_buffer_create(state, output);
412
  if (ret != CHROMA_OK) {
413
    chroma_log("ERROR", "Failed to create shm buffer for output %u",
414
               output->id);
415
    return ret;
416
  }
417
418
  // Draw scaled image into buffer
419
  draw_scaled_image(output, output->image);
420
421
  // Attach buffer to surface
422
  wl_surface_attach(output->surface, output->shm_buffer, 0, 0);
423
  wl_surface_damage_buffer(output->surface, 0, 0, output->width,
424
                           output->height);
425
  wl_surface_commit(output->surface);
426
  if (wl_display_flush(state->display) < 0) {
427
    chroma_log("ERROR", "Failed to flush Wayland display for output %u: %s",
428
               output->id, strerror(errno));
429
    return CHROMA_ERROR_WAYLAND;
430
  }
431
432
  chroma_log("INFO", "Rendered wallpaper to output %u (%dx%d)", output->id,
433
             output->width, output->height);
434
435
  // Unmap client-side memory; compositor keeps its own fd reference
436
  chroma_buffer_unmap(output);
437
438
  // Release image reference and free CPU data if no longer needed
439
  if (output->image) {
440
    chroma_image_t *image = output->image;
441
    chroma_image_release(image);
442
    output->image = NULL;
443
  }
444
445
  return CHROMA_OK;
446
}