brewery
notashelf /
40227627c1a3b0945213400878d0933308e8d985

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git
src/render.cC803 lines25.3 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
    "#version 120\n"
191
    "attribute vec2 position;\n"
192
    "attribute vec2 texcoord;\n"
193
    "varying vec2 v_texcoord;\n"
194
    "void main() {\n"
195
    "    gl_Position = vec4(position, 0.0, 1.0);\n"
196
    "    v_texcoord = texcoord;\n"
197
    "}\n";
198
199
// Fragment shader for simple texture rendering
200
static const char *fragment_shader_source =
201
    "#version 120\n"
202
    "varying vec2 v_texcoord;\n"
203
    "uniform sampler2D texture;\n"
204
    "void main() {\n"
205
    "    gl_FragColor = texture2D(texture, v_texcoord);\n"
206
    "}\n";
207
208
// Vertices for a fullscreen quad
209
static const float vertices[] = {
210
    // Position  Texcoord
211
    -1.0f, -1.0f, 0.0f, 1.0f, // bottom left
212
    1.0f,  -1.0f, 1.0f, 1.0f, // bottom right
213
    1.0f,  1.0f,  1.0f, 0.0f, // top right
214
    -1.0f, 1.0f,  0.0f, 0.0f, // top left
215
};
216
217
static const unsigned int indices[] = {
218
    0, 1, 2, // first triangle
219
    2, 3, 0  // second triangle
220
};
221
222
static GLuint compile_shader(GLenum type, const char *source) {
223
  GLuint shader = glCreateShader(type);
224
  if (!shader) {
225
    chroma_log("ERROR", "Failed to create shader");
226
    return 0;
227
  }
228
229
  glShaderSource(shader, 1, &source, NULL);
230
  glCompileShader(shader);
231
232
  GLint status;
233
  glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
234
  if (status != GL_TRUE) {
235
    char log[512];
236
    glGetShaderInfoLog(shader, sizeof(log), NULL, log);
237
    chroma_log("ERROR", "Shader compilation failed: %s", log);
238
    glDeleteShader(shader);
239
    return 0;
240
  }
241
242
  return shader;
243
}
244
245
static GLuint create_shader_program(void) {
246
  GLuint vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_shader_source);
247
  if (!vertex_shader) {
248
    return 0;
249
  }
250
251
  GLuint fragment_shader =
252
      compile_shader(GL_FRAGMENT_SHADER, fragment_shader_source);
253
  if (!fragment_shader) {
254
    glDeleteShader(vertex_shader);
255
    return 0;
256
  }
257
258
  GLuint program = glCreateProgram();
259
  if (!program) {
260
    chroma_log("ERROR", "Failed to create shader program");
261
    glDeleteShader(vertex_shader);
262
    glDeleteShader(fragment_shader);
263
    return 0;
264
  }
265
266
  glAttachShader(program, vertex_shader);
267
  glAttachShader(program, fragment_shader);
268
  glLinkProgram(program);
269
270
  GLint status;
271
  glGetProgramiv(program, GL_LINK_STATUS, &status);
272
  if (status != GL_TRUE) {
273
    char log[512];
274
    glGetProgramInfoLog(program, sizeof(log), NULL, log);
275
    chroma_log("ERROR", "Shader program linking failed: %s", log);
276
    glDeleteProgram(program);
277
    program = 0;
278
  }
279
280
  glDeleteShader(vertex_shader);
281
  glDeleteShader(fragment_shader);
282
283
  return program;
284
}
285
286
// Initialize OpenGL resources for output
287
static int init_gl_resources(chroma_output_t *output) {
288
  if (!output || output->gl_resources_initialized) {
289
    return CHROMA_OK;
290
  }
291
292
  // Create shader prog
293
  output->shader_program = create_shader_program();
294
  if (!output->shader_program) {
295
    chroma_log("ERROR", "Failed to create shader program for output %u",
296
               output->id);
297
    return CHROMA_ERROR_EGL;
298
  }
299
300
  // Create and setup VBO/EBO
301
  glGenBuffers(1, &output->vbo);
302
  glGenBuffers(1, &output->ebo);
303
304
  glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
305
  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW);
306
307
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
308
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
309
               GL_STATIC_DRAW);
310
311
  output->texture_id = 0;   // will be created when image is assigned
312
  output->vbo_dirty = true; // VBO needs initial update
313
  output->gl_resources_initialized = true;
314
315
  chroma_log("DEBUG", "Initialized GL resources for output %u", output->id);
316
  return CHROMA_OK;
317
}
318
319
// Create or update texture from image data
320
static int update_texture_from_image(chroma_output_t *output,
321
                                     chroma_image_t *image,
322
                                     chroma_filter_quality_t filter_quality) {
323
  if (!output || !image || !image->loaded) {
324
    return CHROMA_ERROR_INIT;
325
  }
326
327
  // If image data was already freed after previous GPU upload, we can't upload
328
  // again
329
  if (!image->data) {
330
    chroma_log("ERROR",
331
               "Cannot create texture: image data already freed for %s",
332
               image->path);
333
    return CHROMA_ERROR_IMAGE;
334
  }
335
336
  // Delete existing texture if it exists
337
  if (output->texture_id != 0) {
338
    // Estimate texture size for logging
339
    // FIXME: Unfortunately this only works if we have previous image info.
340
    // Could this b made more accurate?
341
    if (output->image && output->image->loaded) {
342
      size_t texture_size = (size_t)output->image->width *
343
                            (size_t)output->image->height *
344
                            (size_t)output->image->channels;
345
      chroma_log_resource_deallocation("gpu_texture", texture_size,
346
                                       "texture replacement");
347
    }
348
    glDeleteTextures(1, &output->texture_id);
349
    output->texture_id = 0;
350
  }
351
352
  // Create new texture
353
  glGenTextures(1, &output->texture_id);
354
  glBindTexture(GL_TEXTURE_2D, output->texture_id);
355
356
  // Log GPU texture allocation
357
  size_t texture_size =
358
      (size_t)image->width * (size_t)image->height * (size_t)image->channels;
359
  chroma_log_resource_allocation("gpu_texture", texture_size, image->path);
360
361
  // Set texture parameters
362
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
363
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
364
365
  // Use configured filter quality
366
  GLint min_filter, mag_filter;
367
  bool needs_mipmaps;
368
  get_gl_filter_params(filter_quality, &min_filter, &mag_filter,
369
                       &needs_mipmaps);
370
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
371
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);
372
373
  // Upload texture data (always RGBA now)
374
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0,
375
               GL_RGBA, GL_UNSIGNED_BYTE, image->data);
376
377
  // Generate mipmaps for trilinear filtering if they're needed
378
  if (needs_mipmaps) {
379
    glGenerateMipmap(GL_TEXTURE_2D);
380
    chroma_log("DEBUG", "Generated mipmaps for trilinear filtering");
381
  }
382
383
  glBindTexture(GL_TEXTURE_2D, 0);
384
385
  // Mark this output as having uploaded its texture
386
  output->texture_uploaded = true;
387
388
  // Free system RAM copy only when ALL outputs using this image have uploaded
389
  // to GPU
390
  if (image->data) {
391
    // Count total outputs using this image and how many have uploaded
392
    int total_using = 0;
393
    int uploaded_count = 0;
394
395
    chroma_state_t *state = output->state;
396
    for (int i = 0; i < state->output_count; i++) {
397
      if (state->outputs[i].active && state->outputs[i].image == image) {
398
        total_using++;
399
        if (state->outputs[i].texture_uploaded) {
400
          uploaded_count++;
401
        }
402
      }
403
    }
404
405
    // Only free image data when ALL outputs using it have uploaded
406
    if (total_using > 0 && uploaded_count >= total_using) {
407
      size_t freed_bytes = (size_t)image->width * (size_t)image->height *
408
                           (size_t)image->channels;
409
      stbi_image_free(image->data);
410
      image->data = NULL;
411
      chroma_log("INFO",
412
                 "Freed %.2f MB of image data after all %d outputs uploaded to "
413
                 "GPU: %s",
414
                 (double)freed_bytes / (1024.0 * 1024.0), total_using,
415
                 image->path);
416
      chroma_log_resource_deallocation("image_data", freed_bytes,
417
                                       "post-gpu-upload");
418
      chroma_log_memory_stats("post-gpu-upload");
419
    }
420
  }
421
422
  chroma_log("DEBUG", "Updated texture for output %u (%dx%d)", output->id,
423
             image->width, image->height);
424
  return CHROMA_OK;
425
}
426
427
// Cleanup OpenGL resources for output
428
static void cleanup_gl_resources(chroma_output_t *output) {
429
  if (!output || !output->gl_resources_initialized) {
430
    return;
431
  }
432
433
  if (output->texture_id != 0) {
434
    chroma_log_resource_deallocation("gpu_texture", 0, "cleanup");
435
    glDeleteTextures(1, &output->texture_id);
436
    output->texture_id = 0;
437
  }
438
439
  if (output->shader_program != 0) {
440
    glDeleteProgram(output->shader_program);
441
    output->shader_program = 0;
442
  }
443
444
  if (output->vbo != 0) {
445
    glDeleteBuffers(1, &output->vbo);
446
    output->vbo = 0;
447
  }
448
449
  if (output->ebo != 0) {
450
    glDeleteBuffers(1, &output->ebo);
451
    output->ebo = 0;
452
  }
453
454
  output->gl_resources_initialized = false;
455
  chroma_log("DEBUG", "Cleaned up GL resources for output %u", output->id);
456
}
457
458
// EGL configuration selection
459
static int choose_egl_config(EGLDisplay display, EGLConfig *config) {
460
  EGLint attributes[] = {EGL_SURFACE_TYPE,
461
                         EGL_WINDOW_BIT,
462
                         EGL_RED_SIZE,
463
                         8,
464
                         EGL_GREEN_SIZE,
465
                         8,
466
                         EGL_BLUE_SIZE,
467
                         8,
468
                         EGL_ALPHA_SIZE,
469
                         8,
470
                         EGL_RENDERABLE_TYPE,
471
                         EGL_OPENGL_BIT,
472
                         EGL_NONE};
473
474
  EGLint num_configs;
475
  if (!eglChooseConfig(display, attributes, config, 1, &num_configs)) {
476
    chroma_log("ERROR", "Failed to choose EGL config: 0x%04x", eglGetError());
477
    return -1;
478
  }
479
480
  if (num_configs == 0) {
481
    chroma_log("ERROR", "No suitable EGL configs found");
482
    return -1;
483
  }
484
485
  return 0;
486
}
487
488
// EGL initialization
489
int chroma_egl_init(chroma_state_t *state) {
490
  if (!state || !state->display) {
491
    return CHROMA_ERROR_INIT;
492
  }
493
494
  // Get EGL display
495
  state->egl_display = eglGetDisplay((EGLNativeDisplayType)state->display);
496
  if (state->egl_display == EGL_NO_DISPLAY) {
497
    chroma_log("ERROR", "Failed to get EGL display: 0x%04x", eglGetError());
498
    return CHROMA_ERROR_EGL;
499
  }
500
501
  // Initialize EGL
502
  EGLint major, minor;
503
  if (!eglInitialize(state->egl_display, &major, &minor)) {
504
    chroma_log("ERROR", "Failed to initialize EGL: 0x%04x", eglGetError());
505
    return CHROMA_ERROR_EGL;
506
  }
507
508
  chroma_log("INFO", "EGL initialized: version %d.%d", major, minor);
509
  chroma_log_memory_stats("post-egl-init");
510
511
  // Bind OpenGL API
512
  if (!eglBindAPI(EGL_OPENGL_API)) {
513
    chroma_log("ERROR", "Failed to bind OpenGL API: 0x%04x", eglGetError());
514
    chroma_egl_cleanup(state);
515
    return CHROMA_ERROR_EGL;
516
  }
517
518
  // Choose EGL config
519
  if (choose_egl_config(state->egl_display, &state->egl_config) != 0) {
520
    chroma_egl_cleanup(state);
521
    return CHROMA_ERROR_EGL;
522
  }
523
524
  // Create EGL context
525
  EGLint context_attributes[] = {EGL_CONTEXT_MAJOR_VERSION, 2,
526
                                 EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE};
527
528
  state->egl_context = eglCreateContext(state->egl_display, state->egl_config,
529
                                        EGL_NO_CONTEXT, context_attributes);
530
  if (state->egl_context == EGL_NO_CONTEXT) {
531
    chroma_log("ERROR", "Failed to create EGL context: 0x%04x", eglGetError());
532
    chroma_egl_cleanup(state);
533
    return CHROMA_ERROR_EGL;
534
  }
535
536
  chroma_log("INFO", "EGL context created successfully");
537
  return CHROMA_OK;
538
}
539
540
// EGL cleanup
541
void chroma_egl_cleanup(chroma_state_t *state) {
542
  if (!state) {
543
    return;
544
  }
545
546
  if (state->egl_display != EGL_NO_DISPLAY) {
547
    eglMakeCurrent(state->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
548
                   EGL_NO_CONTEXT);
549
550
    if (state->egl_context != EGL_NO_CONTEXT) {
551
      eglDestroyContext(state->egl_display, state->egl_context);
552
      state->egl_context = EGL_NO_CONTEXT;
553
    }
554
555
    eglTerminate(state->egl_display);
556
    state->egl_display = EGL_NO_DISPLAY;
557
  }
558
559
  chroma_log("INFO", "EGL cleaned up");
560
}
561
562
// Create surface for output
563
int chroma_surface_create(chroma_state_t *state, chroma_output_t *output) {
564
  if (!state || !output || !state->compositor || !state->layer_shell) {
565
    return CHROMA_ERROR_INIT;
566
  }
567
568
  // Create Wayland surface
569
  output->surface = wl_compositor_create_surface(state->compositor);
570
  if (!output->surface) {
571
    chroma_log("ERROR", "Failed to create Wayland surface for output %u",
572
               output->id);
573
    return CHROMA_ERROR_WAYLAND;
574
  }
575
576
  // Create layer surface for wallpaper
577
  output->layer_surface = zwlr_layer_shell_v1_get_layer_surface(
578
      state->layer_shell, output->surface, output->wl_output,
579
      ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, "chroma-wallpaper");
580
581
  if (!output->layer_surface) {
582
    chroma_log("ERROR", "Failed to create layer surface for output %u",
583
               output->id);
584
    chroma_surface_destroy(output);
585
    return CHROMA_ERROR_WAYLAND;
586
  }
587
588
  // Configure layer surface
589
  zwlr_layer_surface_v1_set_size(output->layer_surface, (uint32_t)output->width,
590
                                 (uint32_t)output->height);
591
  zwlr_layer_surface_v1_set_anchor(output->layer_surface,
592
                                   ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
593
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
594
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM |
595
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT);
596
  zwlr_layer_surface_v1_set_exclusive_zone(output->layer_surface, -1);
597
  zwlr_layer_surface_v1_set_keyboard_interactivity(
598
      output->layer_surface, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE);
599
600
  // Add layer surface listener
601
  zwlr_layer_surface_v1_add_listener(
602
      output->layer_surface, &chroma_layer_surface_listener_impl, output);
603
604
  // Commit surface to trigger configure event
605
  wl_surface_commit(output->surface);
606
607
  // Wait for configure event
608
  wl_display_roundtrip(state->display);
609
610
  // Create EGL window
611
  output->egl_window =
612
      wl_egl_window_create(output->surface, output->width, output->height);
613
  if (!output->egl_window) {
614
    chroma_log("ERROR", "Failed to create EGL window for output %u",
615
               output->id);
616
    chroma_surface_destroy(output);
617
    return CHROMA_ERROR_EGL;
618
  }
619
620
  // Create EGL surface
621
  output->egl_surface =
622
      eglCreateWindowSurface(state->egl_display, state->egl_config,
623
                             (EGLNativeWindowType)output->egl_window, NULL);
624
  if (output->egl_surface == EGL_NO_SURFACE) {
625
    chroma_log("ERROR", "Failed to create EGL surface for output %u: 0x%04x",
626
               output->id, eglGetError());
627
    chroma_surface_destroy(output);
628
    return CHROMA_ERROR_EGL;
629
  }
630
631
  chroma_log("INFO", "Created surface for output %u (%dx%d)", output->id,
632
             output->width, output->height);
633
634
  // Log surface creation resource allocation
635
  size_t surface_size = (size_t)output->width * (size_t)output->height * 4;
636
  chroma_log_resource_allocation("egl_surface", surface_size, "output surface");
637
638
  return CHROMA_OK;
639
}
640
641
// Destroy surface
642
void chroma_surface_destroy(chroma_output_t *output) {
643
  if (!output) {
644
    return;
645
  }
646
647
  // Clean up OpenGL resources first
648
  cleanup_gl_resources(output);
649
650
  if (output->egl_surface != EGL_NO_SURFACE) {
651
    eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface);
652
    output->egl_surface = EGL_NO_SURFACE;
653
  }
654
655
  if (output->egl_window) {
656
    wl_egl_window_destroy(output->egl_window);
657
    output->egl_window = NULL;
658
  }
659
660
  if (output->layer_surface) {
661
    zwlr_layer_surface_v1_destroy(output->layer_surface);
662
    output->layer_surface = NULL;
663
  }
664
665
  if (output->surface) {
666
    wl_surface_destroy(output->surface);
667
    output->surface = NULL;
668
  }
669
670
  // Log surface destruction
671
  size_t surface_size = (size_t)output->width * (size_t)output->height * 4;
672
  chroma_log_resource_deallocation("egl_surface", surface_size,
673
                                   "output surface cleanup");
674
675
  chroma_log("DEBUG", "Destroyed surface for output %u", output->id);
676
}
677
678
// Render wallpaper to output using cached resources
679
int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
680
  if (!state || !output || !output->image || !output->image->loaded) {
681
    return CHROMA_ERROR_INIT;
682
  }
683
684
  // Make context current
685
  if (!eglMakeCurrent(state->egl_display, output->egl_surface,
686
                      output->egl_surface, state->egl_context)) {
687
    chroma_log("ERROR", "Failed to make EGL context current: 0x%04x",
688
               eglGetError());
689
    return CHROMA_ERROR_EGL;
690
  }
691
692
  if (init_gl_resources(output) != CHROMA_OK) {
693
    return CHROMA_ERROR_EGL;
694
  }
695
696
  if (output->texture_id == 0) {
697
    if (update_texture_from_image(output, output->image,
698
                                  output->filter_quality) != CHROMA_OK) {
699
      chroma_log("ERROR", "Failed to update texture for output %u", output->id);
700
      return CHROMA_ERROR_EGL;
701
    }
702
  }
703
704
  // Set viewport
705
  glViewport(0, 0, output->width, output->height);
706
707
  // Clear screen
708
  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
709
  glClear(GL_COLOR_BUFFER_BIT);
710
711
  // Use cached shader program
712
  glUseProgram(output->shader_program);
713
714
  // Bind cached texture
715
  glActiveTexture(GL_TEXTURE0);
716
  glBindTexture(GL_TEXTURE_2D, output->texture_id);
717
  glUniform1i(glGetUniformLocation(output->shader_program, "texture"), 0);
718
719
  // Update VBO only if needed. E.g, image changed, scale mode changed, or first
720
  // render
721
  if (output->vbo_dirty) {
722
    // Calculate texture coordinates based on scaling mode, anchor, and anchor
723
    // coords
724
    float tex_coords[8];
725
    calculate_texture_coords(output->scale_mode, output->anchor_x,
726
                             output->anchor_y, output->image->width,
727
                             output->image->height, output->width,
728
                             output->height, tex_coords);
729
730
    // Create dynamic vertex data with calculated texture coordinates
731
    float dynamic_vertices[] = {
732
        // Position  Texcoord
733
        -1.0f, -1.0f, tex_coords[0], tex_coords[1], // bottom-left
734
        1.0f,  -1.0f, tex_coords[2], tex_coords[3], // bottom-right
735
        1.0f,  1.0f,  tex_coords[4], tex_coords[5], // top-right
736
        -1.0f, 1.0f,  tex_coords[6], tex_coords[7]  // top-left
737
    };
738
739
    // Update VBO with dynamic texture coordinates
740
    glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
741
    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(dynamic_vertices),
742
                    dynamic_vertices);
743
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
744
745
    output->vbo_dirty = false; // mark VBO as up to date
746
  } else {
747
    // Just bind the existing buffers
748
    glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
749
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
750
  }
751
752
  // Set vertex attributes
753
  GLint position_attr = glGetAttribLocation(output->shader_program, "position");
754
  GLint texcoord_attr = glGetAttribLocation(output->shader_program, "texcoord");
755
756
  glEnableVertexAttribArray((GLuint)position_attr);
757
  glVertexAttribPointer((GLuint)position_attr, 2, GL_FLOAT, GL_FALSE,
758
                        4 * sizeof(float), (void *)0);
759
760
  glEnableVertexAttribArray((GLuint)texcoord_attr);
761
  glVertexAttribPointer((GLuint)texcoord_attr, 2, GL_FLOAT, GL_FALSE,
762
                        4 * sizeof(float), (void *)(2 * sizeof(float)));
763
764
  // Draw
765
  glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
766
767
  // Unbind resources
768
  glBindBuffer(GL_ARRAY_BUFFER, 0);
769
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
770
  glBindTexture(GL_TEXTURE_2D, 0);
771
  glUseProgram(0);
772
773
  // Swap buffers
774
  if (!eglSwapBuffers(state->egl_display, output->egl_surface)) {
775
    chroma_log("ERROR", "Failed to swap buffers for output %u: 0x%04x",
776
               output->id, eglGetError());
777
    return CHROMA_ERROR_EGL;
778
  }
779
780
  // Commit surface
781
  wl_surface_commit(output->surface);
782
783
  chroma_log("DEBUG", "Rendered wallpaper to output %u (cached resources)",
784
             output->id);
785
  return CHROMA_OK;
786
}
787
788
// Invalidate texture cache for output
789
void chroma_output_invalidate_texture(chroma_output_t *output) {
790
  if (!output || !output->gl_resources_initialized) {
791
    return;
792
  }
793
794
  if (output->texture_id != 0) {
795
    glDeleteTextures(1, &output->texture_id);
796
    output->texture_id = 0;
797
    output->texture_uploaded = false; // reset upload flag
798
    chroma_log("DEBUG", "Invalidated texture cache for output %u", output->id);
799
  }
800
801
  // Mark VBO as dirty since texture coordinates may need recalculation
802
  output->vbo_dirty = true;
803
}