brewery
notashelf /
7ccb21af79777e31a67a668f2efc18d7be603241

chroma

public

Lightweight wallpaper daemon for Wayland

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