brewery
notashelf /
dc9f159470cb818cdddff1cd0dd6ee083be50eed

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git
src/render.cC433 lines12.1 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
// 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
}