brewery
notashelf /
74fed80a26f39bb04966d12c649a55150724965f

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git
src/render.cC579 lines17.7 KB
1
#include <stdio.h>
2
#include <stdlib.h>
3
#include <string.h>
4
5
#include <EGL/egl.h>
6
#include <GLES2/gl2.h>
7
8
#include "../include/chroma.h"
9
#include "../include/stb_image.h"
10
11
// Vertex shader for simple texture rendering
12
static const char *vertex_shader_source =
13
    "#version 120\n"
14
    "attribute vec2 position;\n"
15
    "attribute vec2 texcoord;\n"
16
    "varying vec2 v_texcoord;\n"
17
    "void main() {\n"
18
    "    gl_Position = vec4(position, 0.0, 1.0);\n"
19
    "    v_texcoord = texcoord;\n"
20
    "}\n";
21
22
// Fragment shader for simple texture rendering
23
static const char *fragment_shader_source =
24
    "#version 120\n"
25
    "varying vec2 v_texcoord;\n"
26
    "uniform sampler2D texture;\n"
27
    "void main() {\n"
28
    "    gl_FragColor = texture2D(texture, v_texcoord);\n"
29
    "}\n";
30
31
// Vertices for a fullscreen quad
32
static const float vertices[] = {
33
    // Position  Texcoord
34
    -1.0f, -1.0f, 0.0f, 1.0f, // bottom left
35
    1.0f,  -1.0f, 1.0f, 1.0f, // bottom right
36
    1.0f,  1.0f,  1.0f, 0.0f, // top right
37
    -1.0f, 1.0f,  0.0f, 0.0f, // top left
38
};
39
40
static const unsigned int indices[] = {
41
    0, 1, 2, // first triangle
42
    2, 3, 0  // second triangle
43
};
44
45
static GLuint compile_shader(GLenum type, const char *source) {
46
  GLuint shader = glCreateShader(type);
47
  if (!shader) {
48
    chroma_log("ERROR", "Failed to create shader");
49
    return 0;
50
  }
51
52
  glShaderSource(shader, 1, &source, NULL);
53
  glCompileShader(shader);
54
55
  GLint status;
56
  glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
57
  if (status != GL_TRUE) {
58
    char log[512];
59
    glGetShaderInfoLog(shader, sizeof(log), NULL, log);
60
    chroma_log("ERROR", "Shader compilation failed: %s", log);
61
    glDeleteShader(shader);
62
    return 0;
63
  }
64
65
  return shader;
66
}
67
68
static GLuint create_shader_program(void) {
69
  GLuint vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_shader_source);
70
  if (!vertex_shader) {
71
    return 0;
72
  }
73
74
  GLuint fragment_shader =
75
      compile_shader(GL_FRAGMENT_SHADER, fragment_shader_source);
76
  if (!fragment_shader) {
77
    glDeleteShader(vertex_shader);
78
    return 0;
79
  }
80
81
  GLuint program = glCreateProgram();
82
  if (!program) {
83
    chroma_log("ERROR", "Failed to create shader program");
84
    glDeleteShader(vertex_shader);
85
    glDeleteShader(fragment_shader);
86
    return 0;
87
  }
88
89
  glAttachShader(program, vertex_shader);
90
  glAttachShader(program, fragment_shader);
91
  glLinkProgram(program);
92
93
  GLint status;
94
  glGetProgramiv(program, GL_LINK_STATUS, &status);
95
  if (status != GL_TRUE) {
96
    char log[512];
97
    glGetProgramInfoLog(program, sizeof(log), NULL, log);
98
    chroma_log("ERROR", "Shader program linking failed: %s", log);
99
    glDeleteProgram(program);
100
    program = 0;
101
  }
102
103
  glDeleteShader(vertex_shader);
104
  glDeleteShader(fragment_shader);
105
106
  return program;
107
}
108
109
// Initialize OpenGL resources for output
110
static int init_gl_resources(chroma_output_t *output) {
111
  if (!output || output->gl_resources_initialized) {
112
    return CHROMA_OK;
113
  }
114
115
  // Create shader prog
116
  output->shader_program = create_shader_program();
117
  if (!output->shader_program) {
118
    chroma_log("ERROR", "Failed to create shader program for output %u",
119
               output->id);
120
    return CHROMA_ERROR_EGL;
121
  }
122
123
  // Create and setup VBO/EBO
124
  glGenBuffers(1, &output->vbo);
125
  glGenBuffers(1, &output->ebo);
126
127
  glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
128
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
129
130
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
131
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
132
               GL_STATIC_DRAW);
133
134
  output->texture_id = 0; // will be created when image is assigned
135
  output->gl_resources_initialized = true;
136
137
  chroma_log("DEBUG", "Initialized GL resources for output %u", output->id);
138
  return CHROMA_OK;
139
}
140
141
// Create or update texture from image data
142
static int update_texture_from_image(chroma_output_t *output,
143
                                     chroma_image_t *image) {
144
  if (!output || !image || !image->loaded) {
145
    return CHROMA_ERROR_INIT;
146
  }
147
148
  // If image data was already freed after previous GPU upload, we can't upload
149
  // again
150
  if (!image->data) {
151
    chroma_log("ERROR",
152
               "Cannot create texture: image data already freed for %s",
153
               image->path);
154
    return CHROMA_ERROR_IMAGE;
155
  }
156
157
  // Delete existing texture if it exists
158
  if (output->texture_id != 0) {
159
    // Estimate texture size for logging
160
    // FIXME: Unfortunately this only works if we have previous image info.
161
    // Could this b made more accurate?
162
    if (output->image && output->image->loaded) {
163
      size_t texture_size = (size_t)output->image->width *
164
                            output->image->height * output->image->channels;
165
      chroma_log_resource_deallocation("gpu_texture", texture_size,
166
                                       "texture replacement");
167
    }
168
    glDeleteTextures(1, &output->texture_id);
169
    output->texture_id = 0;
170
  }
171
172
  // Create new texture
173
  glGenTextures(1, &output->texture_id);
174
  glBindTexture(GL_TEXTURE_2D, output->texture_id);
175
176
  // Log GPU texture allocation
177
  size_t texture_size = (size_t)image->width * image->height * image->channels;
178
  chroma_log_resource_allocation("gpu_texture", texture_size, image->path);
179
180
  // Set texture parameters
181
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
182
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
183
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
184
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
185
186
  // Upload texture data (always RGBA now)
187
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0,
188
               GL_RGBA, GL_UNSIGNED_BYTE, image->data);
189
190
  glBindTexture(GL_TEXTURE_2D, 0);
191
192
  // Mark this output as having uploaded its texture
193
  output->texture_uploaded = true;
194
195
  // Free system RAM copy only when ALL outputs using this image have uploaded
196
  // to GPU
197
  if (image->data) {
198
    // Count total outputs using this image and how many have uploaded
199
    int total_using = 0;
200
    int uploaded_count = 0;
201
202
    chroma_state_t *state = output->state;
203
    for (int i = 0; i < state->output_count; i++) {
204
      if (state->outputs[i].active && state->outputs[i].image == image) {
205
        total_using++;
206
        if (state->outputs[i].texture_uploaded) {
207
          uploaded_count++;
208
        }
209
      }
210
    }
211
212
    // Only free image data when ALL outputs using it have uploaded
213
    if (total_using > 0 && uploaded_count >= total_using) {
214
      size_t freed_bytes =
215
          (size_t)image->width * image->height * image->channels;
216
      stbi_image_free(image->data);
217
      image->data = NULL;
218
      chroma_log("INFO",
219
                 "Freed %.2f MB of image data after all %d outputs uploaded to "
220
                 "GPU: %s",
221
                 (double)freed_bytes / (1024.0 * 1024.0), total_using,
222
                 image->path);
223
      chroma_log_resource_deallocation("image_data", freed_bytes,
224
                                       "post-gpu-upload");
225
      chroma_log_memory_stats("post-gpu-upload");
226
    }
227
  }
228
229
  chroma_log("DEBUG", "Updated texture for output %u (%dx%d)", output->id,
230
             image->width, image->height);
231
  return CHROMA_OK;
232
}
233
234
// Cleanup OpenGL resources for output
235
static void cleanup_gl_resources(chroma_output_t *output) {
236
  if (!output || !output->gl_resources_initialized) {
237
    return;
238
  }
239
240
  if (output->texture_id != 0) {
241
    chroma_log_resource_deallocation("gpu_texture", 0, "cleanup");
242
    glDeleteTextures(1, &output->texture_id);
243
    output->texture_id = 0;
244
  }
245
246
  if (output->shader_program != 0) {
247
    glDeleteProgram(output->shader_program);
248
    output->shader_program = 0;
249
  }
250
251
  if (output->vbo != 0) {
252
    glDeleteBuffers(1, &output->vbo);
253
    output->vbo = 0;
254
  }
255
256
  if (output->ebo != 0) {
257
    glDeleteBuffers(1, &output->ebo);
258
    output->ebo = 0;
259
  }
260
261
  output->gl_resources_initialized = false;
262
  chroma_log("DEBUG", "Cleaned up GL resources for output %u", output->id);
263
}
264
265
// EGL configuration selection
266
static int choose_egl_config(EGLDisplay display, EGLConfig *config) {
267
  EGLint attributes[] = {EGL_SURFACE_TYPE,
268
                         EGL_WINDOW_BIT,
269
                         EGL_RED_SIZE,
270
                         8,
271
                         EGL_GREEN_SIZE,
272
                         8,
273
                         EGL_BLUE_SIZE,
274
                         8,
275
                         EGL_ALPHA_SIZE,
276
                         8,
277
                         EGL_RENDERABLE_TYPE,
278
                         EGL_OPENGL_BIT,
279
                         EGL_NONE};
280
281
  EGLint num_configs;
282
  if (!eglChooseConfig(display, attributes, config, 1, &num_configs)) {
283
    chroma_log("ERROR", "Failed to choose EGL config: 0x%04x", eglGetError());
284
    return -1;
285
  }
286
287
  if (num_configs == 0) {
288
    chroma_log("ERROR", "No suitable EGL configs found");
289
    return -1;
290
  }
291
292
  return 0;
293
}
294
295
// EGL initialization
296
int chroma_egl_init(chroma_state_t *state) {
297
  if (!state || !state->display) {
298
    return CHROMA_ERROR_INIT;
299
  }
300
301
  // Get EGL display
302
  state->egl_display = eglGetDisplay((EGLNativeDisplayType)state->display);
303
  if (state->egl_display == EGL_NO_DISPLAY) {
304
    chroma_log("ERROR", "Failed to get EGL display: 0x%04x", eglGetError());
305
    return CHROMA_ERROR_EGL;
306
  }
307
308
  // Initialize EGL
309
  EGLint major, minor;
310
  if (!eglInitialize(state->egl_display, &major, &minor)) {
311
    chroma_log("ERROR", "Failed to initialize EGL: 0x%04x", eglGetError());
312
    return CHROMA_ERROR_EGL;
313
  }
314
315
  chroma_log("INFO", "EGL initialized: version %d.%d", major, minor);
316
  chroma_log_memory_stats("post-egl-init");
317
318
  // Bind OpenGL API
319
  if (!eglBindAPI(EGL_OPENGL_API)) {
320
    chroma_log("ERROR", "Failed to bind OpenGL API: 0x%04x", eglGetError());
321
    chroma_egl_cleanup(state);
322
    return CHROMA_ERROR_EGL;
323
  }
324
325
  // Choose EGL config
326
  if (choose_egl_config(state->egl_display, &state->egl_config) != 0) {
327
    chroma_egl_cleanup(state);
328
    return CHROMA_ERROR_EGL;
329
  }
330
331
  // Create EGL context
332
  EGLint context_attributes[] = {EGL_CONTEXT_MAJOR_VERSION, 2,
333
                                 EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE};
334
335
  state->egl_context = eglCreateContext(state->egl_display, state->egl_config,
336
                                        EGL_NO_CONTEXT, context_attributes);
337
  if (state->egl_context == EGL_NO_CONTEXT) {
338
    chroma_log("ERROR", "Failed to create EGL context: 0x%04x", eglGetError());
339
    chroma_egl_cleanup(state);
340
    return CHROMA_ERROR_EGL;
341
  }
342
343
  chroma_log("INFO", "EGL context created successfully");
344
  return CHROMA_OK;
345
}
346
347
// EGL cleanup
348
void chroma_egl_cleanup(chroma_state_t *state) {
349
  if (!state) {
350
    return;
351
  }
352
353
  if (state->egl_display != EGL_NO_DISPLAY) {
354
    eglMakeCurrent(state->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
355
                   EGL_NO_CONTEXT);
356
357
    if (state->egl_context != EGL_NO_CONTEXT) {
358
      eglDestroyContext(state->egl_display, state->egl_context);
359
      state->egl_context = EGL_NO_CONTEXT;
360
    }
361
362
    eglTerminate(state->egl_display);
363
    state->egl_display = EGL_NO_DISPLAY;
364
  }
365
366
  chroma_log("INFO", "EGL cleaned up");
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, output->width,
397
                                 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
  // Create EGL window
418
  output->egl_window =
419
      wl_egl_window_create(output->surface, output->width, output->height);
420
  if (!output->egl_window) {
421
    chroma_log("ERROR", "Failed to create EGL window for output %u",
422
               output->id);
423
    chroma_surface_destroy(output);
424
    return CHROMA_ERROR_EGL;
425
  }
426
427
  // Create EGL surface
428
  output->egl_surface =
429
      eglCreateWindowSurface(state->egl_display, state->egl_config,
430
                             (EGLNativeWindowType)output->egl_window, NULL);
431
  if (output->egl_surface == EGL_NO_SURFACE) {
432
    chroma_log("ERROR", "Failed to create EGL surface for output %u: 0x%04x",
433
               output->id, eglGetError());
434
    chroma_surface_destroy(output);
435
    return CHROMA_ERROR_EGL;
436
  }
437
438
  chroma_log("INFO", "Created surface for output %u (%dx%d)", output->id,
439
             output->width, output->height);
440
441
  // Log surface creation resource allocation
442
  size_t surface_size =
443
      (size_t)output->width * output->height * 4; // estimate RGBA surface
444
  chroma_log_resource_allocation("egl_surface", surface_size, "output surface");
445
446
  return CHROMA_OK;
447
}
448
449
// Destroy surface
450
void chroma_surface_destroy(chroma_output_t *output) {
451
  if (!output) {
452
    return;
453
  }
454
455
  // Clean up OpenGL resources first
456
  cleanup_gl_resources(output);
457
458
  if (output->egl_surface != EGL_NO_SURFACE) {
459
    eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface);
460
    output->egl_surface = EGL_NO_SURFACE;
461
  }
462
463
  if (output->egl_window) {
464
    wl_egl_window_destroy(output->egl_window);
465
    output->egl_window = NULL;
466
  }
467
468
  if (output->layer_surface) {
469
    zwlr_layer_surface_v1_destroy(output->layer_surface);
470
    output->layer_surface = NULL;
471
  }
472
473
  if (output->surface) {
474
    wl_surface_destroy(output->surface);
475
    output->surface = NULL;
476
  }
477
478
  // Log surface destruction
479
  size_t surface_size =
480
      (size_t)output->width * output->height * 4; // estimate RGBA surface
481
  chroma_log_resource_deallocation("egl_surface", surface_size,
482
                                   "output surface cleanup");
483
484
  chroma_log("DEBUG", "Destroyed surface for output %u", output->id);
485
}
486
487
// Render wallpaper to output using cached resources
488
int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
489
  if (!state || !output || !output->image || !output->image->loaded) {
490
    return CHROMA_ERROR_INIT;
491
  }
492
493
  // Make context current
494
  if (!eglMakeCurrent(state->egl_display, output->egl_surface,
495
                      output->egl_surface, state->egl_context)) {
496
    chroma_log("ERROR", "Failed to make EGL context current: 0x%04x",
497
               eglGetError());
498
    return CHROMA_ERROR_EGL;
499
  }
500
501
  if (init_gl_resources(output) != CHROMA_OK) {
502
    return CHROMA_ERROR_EGL;
503
  }
504
505
  if (output->texture_id == 0) {
506
    if (update_texture_from_image(output, output->image) != CHROMA_OK) {
507
      chroma_log("ERROR", "Failed to update texture for output %u", output->id);
508
      return CHROMA_ERROR_EGL;
509
    }
510
  }
511
512
  // Set viewport
513
  glViewport(0, 0, output->width, output->height);
514
515
  // Clear screen
516
  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
517
  glClear(GL_COLOR_BUFFER_BIT);
518
519
  // Use cached shader program
520
  glUseProgram(output->shader_program);
521
522
  // Bind cached texture
523
  glActiveTexture(GL_TEXTURE0);
524
  glBindTexture(GL_TEXTURE_2D, output->texture_id);
525
  glUniform1i(glGetUniformLocation(output->shader_program, "texture"), 0);
526
527
  // Use cached VBO/EBO
528
  glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
529
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
530
531
  // Set vertex attributes
532
  GLint position_attr = glGetAttribLocation(output->shader_program, "position");
533
  GLint texcoord_attr = glGetAttribLocation(output->shader_program, "texcoord");
534
535
  glEnableVertexAttribArray(position_attr);
536
  glVertexAttribPointer(position_attr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
537
                        (void *)0);
538
539
  glEnableVertexAttribArray(texcoord_attr);
540
  glVertexAttribPointer(texcoord_attr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
541
                        (void *)(2 * sizeof(float)));
542
543
  // Draw
544
  glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
545
546
  // Unbind resources
547
  glBindBuffer(GL_ARRAY_BUFFER, 0);
548
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
549
  glBindTexture(GL_TEXTURE_2D, 0);
550
  glUseProgram(0);
551
552
  // Swap buffers
553
  if (!eglSwapBuffers(state->egl_display, output->egl_surface)) {
554
    chroma_log("ERROR", "Failed to swap buffers for output %u: 0x%04x",
555
               output->id, eglGetError());
556
    return CHROMA_ERROR_EGL;
557
  }
558
559
  // Commit surface
560
  wl_surface_commit(output->surface);
561
562
  chroma_log("DEBUG", "Rendered wallpaper to output %u (cached resources)",
563
             output->id);
564
  return CHROMA_OK;
565
}
566
567
// Invalidate texture cache for output
568
void chroma_output_invalidate_texture(chroma_output_t *output) {
569
  if (!output || !output->gl_resources_initialized) {
570
    return;
571
  }
572
573
  if (output->texture_id != 0) {
574
    glDeleteTextures(1, &output->texture_id);
575
    output->texture_id = 0;
576
    output->texture_uploaded = false; // reset upload flag
577
    chroma_log("DEBUG", "Invalidated texture cache for output %u", output->id);
578
  }
579
}