brewery
notashelf /
786389000243517c774749f96ce9985958f891ad

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git
src/render.cC828 lines26.4 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/vendor/stb_image.h"
10
11
// Convert filter quality enum to OpenGL parameters
12
static void get_gl_filter_params(chroma_filter_quality_t quality,
13
                                 GLint *min_filter, GLint *mag_filter,
14
                                 bool *needs_mipmaps) {
15
  switch (quality) {
16
  case CHROMA_FILTER_NEAREST:
17
    *min_filter = GL_NEAREST;
18
    *mag_filter = GL_NEAREST;
19
    *needs_mipmaps = false;
20
    break;
21
  case CHROMA_FILTER_LINEAR:
22
    *min_filter = GL_LINEAR;
23
    *mag_filter = GL_LINEAR;
24
    *needs_mipmaps = false;
25
    break;
26
  case CHROMA_FILTER_BILINEAR:
27
    *min_filter = GL_LINEAR;
28
    *mag_filter = GL_LINEAR;
29
    *needs_mipmaps = false;
30
    break;
31
  case CHROMA_FILTER_TRILINEAR:
32
    *min_filter = GL_LINEAR_MIPMAP_LINEAR;
33
    *mag_filter = GL_LINEAR;
34
    *needs_mipmaps = true;
35
    break;
36
  default:
37
    *min_filter = GL_LINEAR;
38
    *mag_filter = GL_LINEAR;
39
    *needs_mipmaps = false;
40
    break;
41
  }
42
}
43
44
// Calculate texture coordinates based on scaling mode and anchor position
45
static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
46
                                     float anchor_x, float anchor_y,
47
                                     int image_width, int image_height,
48
                                     int output_width, int output_height,
49
                                     float tex_coords[8]) {
50
  // Default texture coordinates (full texture)
51
  float u1 = 0.0f, v1 = 0.0f; // top-left
52
  float u2 = 1.0f, v2 = 1.0f; // bottom-right
53
54
  switch (scale_mode) {
55
  case CHROMA_SCALE_STRETCH:
56
    // Use full texture, stretch to fit
57
    u1 = 0.0f;
58
    v1 = 0.0f;
59
    u2 = 1.0f;
60
    v2 = 1.0f;
61
    break;
62
63
  case CHROMA_SCALE_CENTER:
64
    // Center image at original size
65
    // Calculate how much of the texture to show
66
    {
67
      float image_aspect = (float)image_width / (float)image_height;
68
      float output_aspect = (float)output_width / (float)output_height;
69
70
      if (image_aspect > output_aspect) {
71
        // Image is wider - fit width, show center portion vertically
72
        float visible_height = (float)image_width / output_aspect;
73
        float v_offset = ((float)image_height - visible_height) /
74
                         (2.0f * (float)image_height);
75
        u1 = 0.0f;
76
        v1 = v_offset;
77
        u2 = 1.0f;
78
        v2 = 1.0f - v_offset;
79
      } else {
80
        // Image is taller - fit height, show center portion horizontally
81
        float visible_width = (float)image_height * output_aspect;
82
        float u_offset =
83
            ((float)image_width - visible_width) / (2.0f * (float)image_width);
84
        u1 = u_offset;
85
        v1 = 0.0f;
86
        u2 = 1.0f - u_offset;
87
        v2 = 1.0f;
88
      }
89
    }
90
    break;
91
92
  case CHROMA_SCALE_FIT:
93
    // Fit image within output, maintaining aspect ratio
94
    {
95
      float image_aspect = (float)image_width / (float)image_height;
96
      float output_aspect = (float)output_width / (float)output_height;
97
98
      if (image_aspect > output_aspect) {
99
        // Image is wider - fit width, add borders top/bottom
100
        float scaled_height = (float)output_width / image_aspect;
101
        float v_border = ((float)output_height - scaled_height) /
102
                         (2.0f * (float)output_height);
103
        u1 = 0.0f;
104
        v1 = v_border;
105
        u2 = 1.0f;
106
        v2 = 1.0f - v_border;
107
      } else {
108
        // Image is taller - fit height, add borders left/right
109
        float scaled_width = (float)output_height * image_aspect;
110
        float u_border =
111
            ((float)output_width - scaled_width) / (2.0f * (float)output_width);
112
        u1 = u_border;
113
        v1 = 0.0f;
114
        u2 = 1.0f - u_border;
115
        v2 = 1.0f;
116
      }
117
    }
118
    break;
119
120
  case CHROMA_SCALE_FILL:
121
  default:
122
    // Fill entire output, crop if necessary
123
    {
124
      float image_aspect = (float)image_width / (float)image_height;
125
      float output_aspect = (float)output_width / (float)output_height;
126
127
      if (image_aspect > output_aspect) {
128
        // Image is wider - crop left/right
129
        float crop_width = (float)image_height * output_aspect;
130
        float u_crop =
131
            ((float)image_width - crop_width) / (2.0f * (float)image_width);
132
        u1 = u_crop;
133
        v1 = 0.0f;
134
        u2 = 1.0f - u_crop;
135
        v2 = 1.0f;
136
      } else {
137
        // Image is taller - crop top/bottom
138
        float crop_height = (float)image_width / output_aspect;
139
        float v_crop =
140
            ((float)image_height - crop_height) / (2.0f * (float)image_height);
141
        u1 = 0.0f;
142
        v1 = v_crop;
143
        u2 = 1.0f;
144
        v2 = 1.0f - v_crop;
145
      }
146
    }
147
    break;
148
  }
149
150
  // Apply anchor-based offset using anchor_x and anchor_y (0-100, 50=center)
151
  // anchor_x: 0=left edge, 50=center, 100=right edge
152
  // anchor_y: 0=top edge, 50=center, 100=bottom edge
153
  // u_shift: negative moves view left (shows more right side of image)
154
  // v_shift: negative moves view up (shows more top of image)
155
  float u_shift = (anchor_x - 50.0f) / 50.0f; // -1 to 1
156
  float v_shift = (50.0f - anchor_y) / 50.0f; // -1 to 1 (inverted for top-down)
157
158
  // Calculate the shift amount based on crop/border space available
159
  float u_crop = 1.0f - (u2 - u1);
160
  float v_crop = 1.0f - (v2 - v1);
161
  u1 += u_shift * u_crop;
162
  u2 += u_shift * u_crop;
163
  v1 += v_shift * v_crop;
164
  v2 += v_shift * v_crop;
165
166
  // Clamp to valid range [0, 1]
167
  if (u1 < 0.0f)
168
    u1 = 0.0f;
169
  if (u2 > 1.0f)
170
    u2 = 1.0f;
171
  if (v1 < 0.0f)
172
    v1 = 0.0f;
173
  if (v2 > 1.0f)
174
    v2 = 1.0f;
175
176
  // Set texture coordinates for quad (bottom-left, bottom-right, top-right,
177
  // top-left)
178
  tex_coords[0] = u1;
179
  tex_coords[1] = v2; // bottom-left
180
  tex_coords[2] = u2;
181
  tex_coords[3] = v2; // bottom-right
182
  tex_coords[4] = u2;
183
  tex_coords[5] = v1; // top-right
184
  tex_coords[6] = u1;
185
  tex_coords[7] = v1; // top-left
186
}
187
188
// Vertex shader for simple texture rendering
189
static const char *vertex_shader_source =
190
    "attribute vec2 position;\n"
191
    "attribute vec2 texcoord;\n"
192
    "varying vec2 v_texcoord;\n"
193
    "void main() {\n"
194
    "    gl_Position = vec4(position, 0.0, 1.0);\n"
195
    "    v_texcoord = texcoord;\n"
196
    "}\n";
197
198
// Fragment shader for simple texture rendering
199
static const char *fragment_shader_source =
200
    "precision mediump float;\n"
201
    "varying vec2 v_texcoord;\n"
202
    "uniform sampler2D texture;\n"
203
    "void main() {\n"
204
    "    gl_FragColor = texture2D(texture, v_texcoord);\n"
205
    "}\n";
206
207
// Vertices for a fullscreen quad
208
static const float vertices[] = {
209
    // Position  Texcoord
210
    -1.0f, -1.0f, 0.0f, 1.0f, // bottom left
211
    1.0f,  -1.0f, 1.0f, 1.0f, // bottom right
212
    1.0f,  1.0f,  1.0f, 0.0f, // top right
213
    -1.0f, 1.0f,  0.0f, 0.0f, // top left
214
};
215
216
static const unsigned short indices[] = {
217
    0, 1, 2, // first triangle
218
    2, 3, 0  // second triangle
219
};
220
221
static GLuint compile_shader(GLenum type, const char *source) {
222
  GLuint shader = glCreateShader(type);
223
  if (!shader) {
224
    chroma_log("ERROR", "Failed to create shader");
225
    return 0;
226
  }
227
228
  glShaderSource(shader, 1, &source, NULL);
229
  glCompileShader(shader);
230
231
  GLint status;
232
  glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
233
  if (status != GL_TRUE) {
234
    char log[512];
235
    glGetShaderInfoLog(shader, sizeof(log), NULL, log);
236
    chroma_log("ERROR", "Shader compilation failed: %s", log);
237
    glDeleteShader(shader);
238
    return 0;
239
  }
240
241
  return shader;
242
}
243
244
static GLuint create_shader_program(void) {
245
  GLuint vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_shader_source);
246
  if (!vertex_shader) {
247
    return 0;
248
  }
249
250
  GLuint fragment_shader =
251
      compile_shader(GL_FRAGMENT_SHADER, fragment_shader_source);
252
  if (!fragment_shader) {
253
    glDeleteShader(vertex_shader);
254
    return 0;
255
  }
256
257
  GLuint program = glCreateProgram();
258
  if (!program) {
259
    chroma_log("ERROR", "Failed to create shader program");
260
    glDeleteShader(vertex_shader);
261
    glDeleteShader(fragment_shader);
262
    return 0;
263
  }
264
265
  glAttachShader(program, vertex_shader);
266
  glAttachShader(program, fragment_shader);
267
  glLinkProgram(program);
268
269
  GLint status;
270
  glGetProgramiv(program, GL_LINK_STATUS, &status);
271
  if (status != GL_TRUE) {
272
    char log[512];
273
    glGetProgramInfoLog(program, sizeof(log), NULL, log);
274
    chroma_log("ERROR", "Shader program linking failed: %s", log);
275
    glDeleteProgram(program);
276
    program = 0;
277
  }
278
279
  glDeleteShader(vertex_shader);
280
  glDeleteShader(fragment_shader);
281
282
  return program;
283
}
284
285
// Initialize OpenGL resources for output
286
static int init_gl_resources(chroma_state_t *state, chroma_output_t *output) {
287
  if (!output || output->gl_resources_initialized) {
288
    return CHROMA_OK;
289
  }
290
291
  // Use shared shader program from state if available
292
  if (state && state->shader_program != 0) {
293
    output->shader_program = state->shader_program;
294
    chroma_log("DEBUG", "Using shared shader program for output %u",
295
               output->id);
296
  } else {
297
    // Create shader program
298
    output->shader_program = create_shader_program();
299
    if (!output->shader_program) {
300
      chroma_log("ERROR", "Failed to create shader program for output %u",
301
                 output->id);
302
      return CHROMA_ERROR_EGL;
303
    }
304
    // Store in state for sharing
305
    if (state) {
306
      state->shader_program = output->shader_program;
307
      chroma_log("DEBUG", "Created shared shader program");
308
    }
309
  }
310
311
  // Create and setup VBO/EBO
312
  glGenBuffers(1, &output->vbo);
313
  glGenBuffers(1, &output->ebo);
314
315
  glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
316
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW);
317
318
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
319
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
320
               GL_STATIC_DRAW);
321
322
  // Unbind buffers
323
  glBindBuffer(GL_ARRAY_BUFFER, 0);
324
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
325
326
  output->texture_id = 0;   // will be created when image is assigned
327
  output->vbo_dirty = true; // VBO needs initial update
328
  output->gl_resources_initialized = true;
329
330
  chroma_log("DEBUG", "Initialized GL resources for output %u", output->id);
331
  return CHROMA_OK;
332
}
333
334
// Create or update texture from image data
335
static int update_texture_from_image(chroma_output_t *output,
336
                                     chroma_image_t *image,
337
                                     chroma_filter_quality_t filter_quality) {
338
  if (!output || !image || !image->loaded) {
339
    return CHROMA_ERROR_INIT;
340
  }
341
342
  // If image data was already freed after previous GPU upload, we can't upload
343
  // again
344
  if (!image->data) {
345
    chroma_log("ERROR",
346
               "Cannot create texture: image data already freed for %s",
347
               image->path);
348
    return CHROMA_ERROR_IMAGE;
349
  }
350
351
  // Delete existing texture if it exists
352
  if (output->texture_id != 0) {
353
    // Estimate texture size for logging
354
    // FIXME: Unfortunately this only works if we have previous image info.
355
    // Could this b made more accurate?
356
    if (output->image && output->image->loaded) {
357
      size_t texture_size = (size_t)output->image->width *
358
                            (size_t)output->image->height *
359
                            (size_t)output->image->channels;
360
      chroma_log_resource_deallocation("gpu_texture", texture_size,
361
                                       "texture replacement");
362
    }
363
    glDeleteTextures(1, &output->texture_id);
364
    output->texture_id = 0;
365
  }
366
367
  // Create new texture
368
  glGenTextures(1, &output->texture_id);
369
  glBindTexture(GL_TEXTURE_2D, output->texture_id);
370
371
  // Log GPU texture allocation
372
  size_t texture_size =
373
      (size_t)image->width * (size_t)image->height * (size_t)image->channels;
374
  chroma_log_resource_allocation("gpu_texture", texture_size, image->path);
375
376
  // Set texture parameters
377
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
378
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
379
380
  // Use configured filter quality
381
  GLint min_filter, mag_filter;
382
  bool needs_mipmaps;
383
  get_gl_filter_params(filter_quality, &min_filter, &mag_filter,
384
                       &needs_mipmaps);
385
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
386
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);
387
388
  // Upload texture data with appropriate format based on channels
389
  GLint internal_format;
390
  GLenum format;
391
  if (image->channels == 4) {
392
    internal_format = GL_RGBA;
393
    format = GL_RGBA;
394
  } else if (image->channels == 3) {
395
    internal_format = GL_RGB;
396
    format = GL_RGB;
397
  } else if (image->channels == 2) {
398
    internal_format = GL_LUMINANCE_ALPHA;
399
    format = GL_LUMINANCE_ALPHA;
400
  } else {
401
    internal_format = GL_LUMINANCE;
402
    format = GL_LUMINANCE;
403
  }
404
  glTexImage2D(GL_TEXTURE_2D, 0, internal_format, image->width, image->height,
405
               0, format, GL_UNSIGNED_BYTE, image->data);
406
407
  // Generate mipmaps for trilinear filtering if they're needed
408
  if (needs_mipmaps) {
409
    glGenerateMipmap(GL_TEXTURE_2D);
410
    chroma_log("DEBUG", "Generated mipmaps for trilinear filtering");
411
  }
412
413
  glBindTexture(GL_TEXTURE_2D, 0);
414
415
  // Mark this output as having uploaded its texture
416
  output->texture_uploaded = true;
417
418
  // Decrement reference count and free system RAM when all outputs
419
  // have uploaded their textures
420
  image->ref_count--;
421
  if (image->ref_count <= 0 && image->data) {
422
    size_t freed_bytes =
423
        (size_t)image->width * (size_t)image->height * (size_t)image->channels;
424
    stbi_image_free(image->data);
425
    image->data = NULL;
426
    chroma_log("INFO", "Freed %.2f MB of image data after GPU upload: %s",
427
               (double)freed_bytes / (1024.0 * 1024.0), image->path);
428
    chroma_log_resource_deallocation("image_data", freed_bytes,
429
                                     "post-gpu-upload");
430
    chroma_log_memory_stats("post-gpu-upload");
431
  } else {
432
    chroma_log("DEBUG",
433
               "Keeping image data for %s (ref_count: %d, waiting for %d more "
434
               "outputs)",
435
               image->path, image->ref_count, image->ref_count);
436
  }
437
438
  chroma_log("DEBUG", "Updated texture for output %u (%dx%d)", output->id,
439
             image->width, image->height);
440
  return CHROMA_OK;
441
}
442
443
// Cleanup OpenGL resources for output
444
static void cleanup_gl_resources(chroma_state_t *state,
445
                                 chroma_output_t *output) {
446
  if (!output || !output->gl_resources_initialized) {
447
    return;
448
  }
449
450
  if (output->texture_id != 0) {
451
    chroma_log_resource_deallocation("gpu_texture", 0, "cleanup");
452
    glDeleteTextures(1, &output->texture_id);
453
    output->texture_id = 0;
454
  }
455
456
  // Don't delete shared shader program here, it's managed by state
457
  if (output->shader_program != 0) {
458
    // Only delete if this output owns the program (not shared)
459
    if (state && state->shader_program != output->shader_program) {
460
      glDeleteProgram(output->shader_program);
461
    }
462
    output->shader_program = 0;
463
  }
464
465
  if (output->vbo != 0) {
466
    glDeleteBuffers(1, &output->vbo);
467
    output->vbo = 0;
468
  }
469
470
  if (output->ebo != 0) {
471
    glDeleteBuffers(1, &output->ebo);
472
    output->ebo = 0;
473
  }
474
475
  output->gl_resources_initialized = false;
476
  chroma_log("DEBUG", "Cleaned up GL resources for output %u", output->id);
477
}
478
479
// EGL configuration selection
480
static int choose_egl_config(EGLDisplay display, EGLConfig *config) {
481
  EGLint attributes[] = {EGL_SURFACE_TYPE,
482
                         EGL_WINDOW_BIT,
483
                         EGL_RED_SIZE,
484
                         8,
485
                         EGL_GREEN_SIZE,
486
                         8,
487
                         EGL_BLUE_SIZE,
488
                         8,
489
                         EGL_ALPHA_SIZE,
490
                         0,
491
                         EGL_RENDERABLE_TYPE,
492
                         EGL_OPENGL_ES2_BIT,
493
                         EGL_NONE};
494
495
  EGLint num_configs;
496
  if (!eglChooseConfig(display, attributes, config, 1, &num_configs)) {
497
    chroma_log("ERROR", "Failed to choose EGL config: 0x%04x", eglGetError());
498
    return -1;
499
  }
500
501
  if (num_configs == 0) {
502
    chroma_log("ERROR", "No suitable EGL configs found");
503
    return -1;
504
  }
505
506
  return 0;
507
}
508
509
// EGL initialization
510
int chroma_egl_init(chroma_state_t *state) {
511
  if (!state || !state->display) {
512
    return CHROMA_ERROR_INIT;
513
  }
514
515
  // Get EGL display
516
  state->egl_display = eglGetDisplay((EGLNativeDisplayType)state->display);
517
  if (state->egl_display == EGL_NO_DISPLAY) {
518
    chroma_log("ERROR", "Failed to get EGL display: 0x%04x", eglGetError());
519
    return CHROMA_ERROR_EGL;
520
  }
521
522
  // Initialize EGL
523
  EGLint major, minor;
524
  if (!eglInitialize(state->egl_display, &major, &minor)) {
525
    chroma_log("ERROR", "Failed to initialize EGL: 0x%04x", eglGetError());
526
    return CHROMA_ERROR_EGL;
527
  }
528
529
  chroma_log("INFO", "EGL initialized: version %d.%d", major, minor);
530
  chroma_log_memory_stats("post-egl-init");
531
532
  // Bind OpenGL ES API for better efficiency on embedded/GPU drivers
533
  if (!eglBindAPI(EGL_OPENGL_ES_API)) {
534
    chroma_log("ERROR", "Failed to bind OpenGL ES API: 0x%04x", eglGetError());
535
    chroma_egl_cleanup(state);
536
    return CHROMA_ERROR_EGL;
537
  }
538
539
  // Choose EGL config
540
  if (choose_egl_config(state->egl_display, &state->egl_config) != 0) {
541
    chroma_egl_cleanup(state);
542
    return CHROMA_ERROR_EGL;
543
  }
544
545
  // Create EGL context for GLES 2.0
546
  EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
547
548
  state->egl_context = eglCreateContext(state->egl_display, state->egl_config,
549
                                        EGL_NO_CONTEXT, context_attributes);
550
  if (state->egl_context == EGL_NO_CONTEXT) {
551
    chroma_log("ERROR", "Failed to create EGL context: 0x%04x", eglGetError());
552
    chroma_egl_cleanup(state);
553
    return CHROMA_ERROR_EGL;
554
  }
555
556
  chroma_log("INFO", "EGL context created successfully");
557
  return CHROMA_OK;
558
}
559
560
// EGL cleanup
561
void chroma_egl_cleanup(chroma_state_t *state) {
562
  if (!state) {
563
    return;
564
  }
565
566
  // Clean up shared shader program while context is still current
567
  if (state->shader_program != 0) {
568
    glDeleteProgram(state->shader_program);
569
    state->shader_program = 0;
570
  }
571
572
  if (state->egl_display != EGL_NO_DISPLAY) {
573
    eglMakeCurrent(state->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
574
                   EGL_NO_CONTEXT);
575
576
    if (state->egl_context != EGL_NO_CONTEXT) {
577
      eglDestroyContext(state->egl_display, state->egl_context);
578
      state->egl_context = EGL_NO_CONTEXT;
579
    }
580
581
    eglTerminate(state->egl_display);
582
    state->egl_display = EGL_NO_DISPLAY;
583
  }
584
585
  chroma_log("INFO", "EGL cleaned up");
586
}
587
588
// Create surface for output
589
int chroma_surface_create(chroma_state_t *state, chroma_output_t *output) {
590
  if (!state || !output || !state->compositor || !state->layer_shell) {
591
    return CHROMA_ERROR_INIT;
592
  }
593
594
  // Create Wayland surface
595
  output->surface = wl_compositor_create_surface(state->compositor);
596
  if (!output->surface) {
597
    chroma_log("ERROR", "Failed to create Wayland surface for output %u",
598
               output->id);
599
    return CHROMA_ERROR_WAYLAND;
600
  }
601
602
  // Create layer surface for wallpaper
603
  output->layer_surface = zwlr_layer_shell_v1_get_layer_surface(
604
      state->layer_shell, output->surface, output->wl_output,
605
      ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, "chroma-wallpaper");
606
607
  if (!output->layer_surface) {
608
    chroma_log("ERROR", "Failed to create layer surface for output %u",
609
               output->id);
610
    chroma_surface_destroy(output);
611
    return CHROMA_ERROR_WAYLAND;
612
  }
613
614
  // Configure layer surface
615
  zwlr_layer_surface_v1_set_size(output->layer_surface, (uint32_t)output->width,
616
                                 (uint32_t)output->height);
617
  zwlr_layer_surface_v1_set_anchor(output->layer_surface,
618
                                   ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
619
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
620
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM |
621
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT);
622
  zwlr_layer_surface_v1_set_exclusive_zone(output->layer_surface, -1);
623
  zwlr_layer_surface_v1_set_keyboard_interactivity(
624
      output->layer_surface, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE);
625
626
  // Add layer surface listener
627
  zwlr_layer_surface_v1_add_listener(
628
      output->layer_surface, &chroma_layer_surface_listener_impl, output);
629
630
  // Commit surface to trigger configure event
631
  wl_surface_commit(output->surface);
632
633
  // Wait for configure event
634
  wl_display_roundtrip(state->display);
635
636
  // Create EGL window
637
  output->egl_window =
638
      wl_egl_window_create(output->surface, output->width, output->height);
639
  if (!output->egl_window) {
640
    chroma_log("ERROR", "Failed to create EGL window for output %u",
641
               output->id);
642
    chroma_surface_destroy(output);
643
    return CHROMA_ERROR_EGL;
644
  }
645
646
  // Create EGL surface
647
  output->egl_surface =
648
      eglCreateWindowSurface(state->egl_display, state->egl_config,
649
                             (EGLNativeWindowType)output->egl_window, NULL);
650
  if (output->egl_surface == EGL_NO_SURFACE) {
651
    chroma_log("ERROR", "Failed to create EGL surface for output %u: 0x%04x",
652
               output->id, eglGetError());
653
    chroma_surface_destroy(output);
654
    return CHROMA_ERROR_EGL;
655
  }
656
657
  chroma_log("INFO", "Created surface for output %u (%dx%d)", output->id,
658
             output->width, output->height);
659
660
  // Log surface creation resource allocation
661
  size_t surface_size = (size_t)output->width * (size_t)output->height * 4;
662
  chroma_log_resource_allocation("egl_surface", surface_size, "output surface");
663
664
  return CHROMA_OK;
665
}
666
667
// Destroy surface
668
void chroma_surface_destroy(chroma_output_t *output) {
669
  if (!output) {
670
    return;
671
  }
672
673
  // Clean up OpenGL resources first. We use output's back-reference to state
674
  cleanup_gl_resources(output->state, output);
675
676
  if (output->egl_surface != EGL_NO_SURFACE) {
677
    eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface);
678
    output->egl_surface = EGL_NO_SURFACE;
679
  }
680
681
  if (output->egl_window) {
682
    wl_egl_window_destroy(output->egl_window);
683
    output->egl_window = NULL;
684
  }
685
686
  if (output->layer_surface) {
687
    zwlr_layer_surface_v1_destroy(output->layer_surface);
688
    output->layer_surface = NULL;
689
  }
690
691
  if (output->surface) {
692
    wl_surface_destroy(output->surface);
693
    output->surface = NULL;
694
  }
695
696
  output->configured = false;
697
698
  // Log surface destruction
699
  size_t surface_size = (size_t)output->width * (size_t)output->height * 4;
700
  chroma_log_resource_deallocation("egl_surface", surface_size,
701
                                   "output surface cleanup");
702
703
  chroma_log("DEBUG", "Destroyed surface for output %u", output->id);
704
}
705
706
// Render wallpaper to output using cached resources
707
int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
708
  if (!state || !output || !output->image || !output->image->loaded) {
709
    return CHROMA_ERROR_INIT;
710
  }
711
712
  // Make context current
713
  if (!eglMakeCurrent(state->egl_display, output->egl_surface,
714
                      output->egl_surface, state->egl_context)) {
715
    chroma_log("ERROR", "Failed to make EGL context current: 0x%04x",
716
               eglGetError());
717
    return CHROMA_ERROR_EGL;
718
  }
719
720
  if (init_gl_resources(state, output) != CHROMA_OK) {
721
    return CHROMA_ERROR_EGL;
722
  }
723
724
  if (output->texture_id == 0) {
725
    if (update_texture_from_image(output, output->image,
726
                                  output->filter_quality) != CHROMA_OK) {
727
      chroma_log("ERROR", "Failed to update texture for output %u", output->id);
728
      return CHROMA_ERROR_EGL;
729
    }
730
  }
731
732
  // Set viewport
733
  glViewport(0, 0, output->width, output->height);
734
735
  // Clear screen
736
  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
737
  glClear(GL_COLOR_BUFFER_BIT);
738
739
  // Use cached shader program
740
  glUseProgram(output->shader_program);
741
742
  // Bind cached texture
743
  glActiveTexture(GL_TEXTURE0);
744
  glBindTexture(GL_TEXTURE_2D, output->texture_id);
745
  glUniform1i(glGetUniformLocation(output->shader_program, "texture"), 0);
746
747
  // Update VBO only if needed. E.g, image changed, scale mode changed, or first
748
  // render
749
  if (output->vbo_dirty) {
750
    // Calculate texture coordinates based on scaling mode, anchor, and anchor
751
    // coords
752
    float tex_coords[8];
753
    calculate_texture_coords(output->scale_mode, output->anchor_x,
754
                             output->anchor_y, output->image->width,
755
                             output->image->height, output->width,
756
                             output->height, tex_coords);
757
758
    // Create dynamic vertex data with calculated texture coordinates
759
    float dynamic_vertices[] = {
760
        // Position  Texcoord
761
        -1.0f, -1.0f, tex_coords[0], tex_coords[1], // bottom-left
762
        1.0f,  -1.0f, tex_coords[2], tex_coords[3], // bottom-right
763
        1.0f,  1.0f,  tex_coords[4], tex_coords[5], // top-right
764
        -1.0f, 1.0f,  tex_coords[6], tex_coords[7]  // top-left
765
    };
766
767
    // Update VBO with dynamic texture coordinates
768
    glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
769
    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(dynamic_vertices),
770
                    dynamic_vertices);
771
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
772
773
    output->vbo_dirty = false; // mark VBO as up to date
774
  } else {
775
    // Just bind the existing buffers
776
    glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
777
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
778
  }
779
780
  // Set vertex attributes
781
  GLint position_attr = glGetAttribLocation(output->shader_program, "position");
782
  GLint texcoord_attr = glGetAttribLocation(output->shader_program, "texcoord");
783
784
  glEnableVertexAttribArray((GLuint)position_attr);
785
  glVertexAttribPointer((GLuint)position_attr, 2, GL_FLOAT, GL_FALSE,
786
                        4 * sizeof(float), (void *)0);
787
788
  glEnableVertexAttribArray((GLuint)texcoord_attr);
789
  glVertexAttribPointer((GLuint)texcoord_attr, 2, GL_FLOAT, GL_FALSE,
790
                        4 * sizeof(float), (void *)(2 * sizeof(float)));
791
792
  // Draw
793
  glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
794
795
  // Unbind resources
796
  glBindBuffer(GL_ARRAY_BUFFER, 0);
797
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
798
  glBindTexture(GL_TEXTURE_2D, 0);
799
  glUseProgram(0);
800
801
  // Swap buffers; this implicitly commits the surface for EGL on Wayland
802
  if (!eglSwapBuffers(state->egl_display, output->egl_surface)) {
803
    chroma_log("ERROR", "Failed to swap buffers for output %u: 0x%04x",
804
               output->id, eglGetError());
805
    return CHROMA_ERROR_EGL;
806
  }
807
808
  chroma_log("DEBUG", "Rendered wallpaper to output %u (cached resources)",
809
             output->id);
810
  return CHROMA_OK;
811
}
812
813
// Invalidate texture cache for output
814
void chroma_output_invalidate_texture(chroma_output_t *output) {
815
  if (!output || !output->gl_resources_initialized) {
816
    return;
817
  }
818
819
  if (output->texture_id != 0) {
820
    glDeleteTextures(1, &output->texture_id);
821
    output->texture_id = 0;
822
    output->texture_uploaded = false; // reset upload flag
823
    chroma_log("DEBUG", "Invalidated texture cache for output %u", output->id);
824
  }
825
826
  // Mark VBO as dirty since texture coordinates may need recalculation
827
  output->vbo_dirty = true;
828
}