brewery
notashelf /
746e83d3daa43e7f0e03b444345506e460646265

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git
src/render.cC756 lines23.5 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->gl_resources_initialized = true;
282
283
  chroma_log("DEBUG", "Initialized GL resources for output %u", output->id);
284
  return CHROMA_OK;
285
}
286
287
// Create or update texture from image data
288
static int update_texture_from_image(chroma_output_t *output,
289
                                     chroma_image_t *image,
290
                                     chroma_filter_quality_t filter_quality) {
291
  if (!output || !image || !image->loaded) {
292
    return CHROMA_ERROR_INIT;
293
  }
294
295
  // If image data was already freed after previous GPU upload, we can't upload
296
  // again
297
  if (!image->data) {
298
    chroma_log("ERROR",
299
               "Cannot create texture: image data already freed for %s",
300
               image->path);
301
    return CHROMA_ERROR_IMAGE;
302
  }
303
304
  // Delete existing texture if it exists
305
  if (output->texture_id != 0) {
306
    // Estimate texture size for logging
307
    // FIXME: Unfortunately this only works if we have previous image info.
308
    // Could this b made more accurate?
309
    if (output->image && output->image->loaded) {
310
      size_t texture_size = (size_t)output->image->width *
311
                            output->image->height * output->image->channels;
312
      chroma_log_resource_deallocation("gpu_texture", texture_size,
313
                                       "texture replacement");
314
    }
315
    glDeleteTextures(1, &output->texture_id);
316
    output->texture_id = 0;
317
  }
318
319
  // Create new texture
320
  glGenTextures(1, &output->texture_id);
321
  glBindTexture(GL_TEXTURE_2D, output->texture_id);
322
323
  // Log GPU texture allocation
324
  size_t texture_size = (size_t)image->width * image->height * image->channels;
325
  chroma_log_resource_allocation("gpu_texture", texture_size, image->path);
326
327
  // Set texture parameters
328
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
329
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
330
331
  // Use configured filter quality
332
  GLint min_filter, mag_filter;
333
  bool needs_mipmaps;
334
  get_gl_filter_params(filter_quality, &min_filter, &mag_filter,
335
                       &needs_mipmaps);
336
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
337
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);
338
339
  // Upload texture data (always RGBA now)
340
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0,
341
               GL_RGBA, GL_UNSIGNED_BYTE, image->data);
342
343
  // Generate mipmaps for trilinear filtering if they're needed
344
  if (needs_mipmaps) {
345
    glGenerateMipmap(GL_TEXTURE_2D);
346
    chroma_log("DEBUG", "Generated mipmaps for trilinear filtering");
347
  }
348
349
  glBindTexture(GL_TEXTURE_2D, 0);
350
351
  // Mark this output as having uploaded its texture
352
  output->texture_uploaded = true;
353
354
  // Free system RAM copy only when ALL outputs using this image have uploaded
355
  // to GPU
356
  if (image->data) {
357
    // Count total outputs using this image and how many have uploaded
358
    int total_using = 0;
359
    int uploaded_count = 0;
360
361
    chroma_state_t *state = output->state;
362
    for (int i = 0; i < state->output_count; i++) {
363
      if (state->outputs[i].active && state->outputs[i].image == image) {
364
        total_using++;
365
        if (state->outputs[i].texture_uploaded) {
366
          uploaded_count++;
367
        }
368
      }
369
    }
370
371
    // Only free image data when ALL outputs using it have uploaded
372
    if (total_using > 0 && uploaded_count >= total_using) {
373
      size_t freed_bytes =
374
          (size_t)image->width * image->height * image->channels;
375
      stbi_image_free(image->data);
376
      image->data = NULL;
377
      chroma_log("INFO",
378
                 "Freed %.2f MB of image data after all %d outputs uploaded to "
379
                 "GPU: %s",
380
                 (double)freed_bytes / (1024.0 * 1024.0), total_using,
381
                 image->path);
382
      chroma_log_resource_deallocation("image_data", freed_bytes,
383
                                       "post-gpu-upload");
384
      chroma_log_memory_stats("post-gpu-upload");
385
    }
386
  }
387
388
  chroma_log("DEBUG", "Updated texture for output %u (%dx%d)", output->id,
389
             image->width, image->height);
390
  return CHROMA_OK;
391
}
392
393
// Cleanup OpenGL resources for output
394
static void cleanup_gl_resources(chroma_output_t *output) {
395
  if (!output || !output->gl_resources_initialized) {
396
    return;
397
  }
398
399
  if (output->texture_id != 0) {
400
    chroma_log_resource_deallocation("gpu_texture", 0, "cleanup");
401
    glDeleteTextures(1, &output->texture_id);
402
    output->texture_id = 0;
403
  }
404
405
  if (output->shader_program != 0) {
406
    glDeleteProgram(output->shader_program);
407
    output->shader_program = 0;
408
  }
409
410
  if (output->vbo != 0) {
411
    glDeleteBuffers(1, &output->vbo);
412
    output->vbo = 0;
413
  }
414
415
  if (output->ebo != 0) {
416
    glDeleteBuffers(1, &output->ebo);
417
    output->ebo = 0;
418
  }
419
420
  output->gl_resources_initialized = false;
421
  chroma_log("DEBUG", "Cleaned up GL resources for output %u", output->id);
422
}
423
424
// EGL configuration selection
425
static int choose_egl_config(EGLDisplay display, EGLConfig *config) {
426
  EGLint attributes[] = {EGL_SURFACE_TYPE,
427
                         EGL_WINDOW_BIT,
428
                         EGL_RED_SIZE,
429
                         8,
430
                         EGL_GREEN_SIZE,
431
                         8,
432
                         EGL_BLUE_SIZE,
433
                         8,
434
                         EGL_ALPHA_SIZE,
435
                         8,
436
                         EGL_RENDERABLE_TYPE,
437
                         EGL_OPENGL_BIT,
438
                         EGL_NONE};
439
440
  EGLint num_configs;
441
  if (!eglChooseConfig(display, attributes, config, 1, &num_configs)) {
442
    chroma_log("ERROR", "Failed to choose EGL config: 0x%04x", eglGetError());
443
    return -1;
444
  }
445
446
  if (num_configs == 0) {
447
    chroma_log("ERROR", "No suitable EGL configs found");
448
    return -1;
449
  }
450
451
  return 0;
452
}
453
454
// EGL initialization
455
int chroma_egl_init(chroma_state_t *state) {
456
  if (!state || !state->display) {
457
    return CHROMA_ERROR_INIT;
458
  }
459
460
  // Get EGL display
461
  state->egl_display = eglGetDisplay((EGLNativeDisplayType)state->display);
462
  if (state->egl_display == EGL_NO_DISPLAY) {
463
    chroma_log("ERROR", "Failed to get EGL display: 0x%04x", eglGetError());
464
    return CHROMA_ERROR_EGL;
465
  }
466
467
  // Initialize EGL
468
  EGLint major, minor;
469
  if (!eglInitialize(state->egl_display, &major, &minor)) {
470
    chroma_log("ERROR", "Failed to initialize EGL: 0x%04x", eglGetError());
471
    return CHROMA_ERROR_EGL;
472
  }
473
474
  chroma_log("INFO", "EGL initialized: version %d.%d", major, minor);
475
  chroma_log_memory_stats("post-egl-init");
476
477
  // Bind OpenGL API
478
  if (!eglBindAPI(EGL_OPENGL_API)) {
479
    chroma_log("ERROR", "Failed to bind OpenGL API: 0x%04x", eglGetError());
480
    chroma_egl_cleanup(state);
481
    return CHROMA_ERROR_EGL;
482
  }
483
484
  // Choose EGL config
485
  if (choose_egl_config(state->egl_display, &state->egl_config) != 0) {
486
    chroma_egl_cleanup(state);
487
    return CHROMA_ERROR_EGL;
488
  }
489
490
  // Create EGL context
491
  EGLint context_attributes[] = {EGL_CONTEXT_MAJOR_VERSION, 2,
492
                                 EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE};
493
494
  state->egl_context = eglCreateContext(state->egl_display, state->egl_config,
495
                                        EGL_NO_CONTEXT, context_attributes);
496
  if (state->egl_context == EGL_NO_CONTEXT) {
497
    chroma_log("ERROR", "Failed to create EGL context: 0x%04x", eglGetError());
498
    chroma_egl_cleanup(state);
499
    return CHROMA_ERROR_EGL;
500
  }
501
502
  chroma_log("INFO", "EGL context created successfully");
503
  return CHROMA_OK;
504
}
505
506
// EGL cleanup
507
void chroma_egl_cleanup(chroma_state_t *state) {
508
  if (!state) {
509
    return;
510
  }
511
512
  if (state->egl_display != EGL_NO_DISPLAY) {
513
    eglMakeCurrent(state->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
514
                   EGL_NO_CONTEXT);
515
516
    if (state->egl_context != EGL_NO_CONTEXT) {
517
      eglDestroyContext(state->egl_display, state->egl_context);
518
      state->egl_context = EGL_NO_CONTEXT;
519
    }
520
521
    eglTerminate(state->egl_display);
522
    state->egl_display = EGL_NO_DISPLAY;
523
  }
524
525
  chroma_log("INFO", "EGL cleaned up");
526
}
527
528
// Create surface for output
529
int chroma_surface_create(chroma_state_t *state, chroma_output_t *output) {
530
  if (!state || !output || !state->compositor || !state->layer_shell) {
531
    return CHROMA_ERROR_INIT;
532
  }
533
534
  // Create Wayland surface
535
  output->surface = wl_compositor_create_surface(state->compositor);
536
  if (!output->surface) {
537
    chroma_log("ERROR", "Failed to create Wayland surface for output %u",
538
               output->id);
539
    return CHROMA_ERROR_WAYLAND;
540
  }
541
542
  // Create layer surface for wallpaper
543
  output->layer_surface = zwlr_layer_shell_v1_get_layer_surface(
544
      state->layer_shell, output->surface, output->wl_output,
545
      ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, "chroma-wallpaper");
546
547
  if (!output->layer_surface) {
548
    chroma_log("ERROR", "Failed to create layer surface for output %u",
549
               output->id);
550
    chroma_surface_destroy(output);
551
    return CHROMA_ERROR_WAYLAND;
552
  }
553
554
  // Configure layer surface
555
  zwlr_layer_surface_v1_set_size(output->layer_surface, output->width,
556
                                 output->height);
557
  zwlr_layer_surface_v1_set_anchor(output->layer_surface,
558
                                   ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
559
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
560
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM |
561
                                       ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT);
562
  zwlr_layer_surface_v1_set_exclusive_zone(output->layer_surface, -1);
563
  zwlr_layer_surface_v1_set_keyboard_interactivity(
564
      output->layer_surface, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE);
565
566
  // Add layer surface listener
567
  zwlr_layer_surface_v1_add_listener(
568
      output->layer_surface, &chroma_layer_surface_listener_impl, output);
569
570
  // Commit surface to trigger configure event
571
  wl_surface_commit(output->surface);
572
573
  // Wait for configure event
574
  wl_display_roundtrip(state->display);
575
576
  // Create EGL window
577
  output->egl_window =
578
      wl_egl_window_create(output->surface, output->width, output->height);
579
  if (!output->egl_window) {
580
    chroma_log("ERROR", "Failed to create EGL window for output %u",
581
               output->id);
582
    chroma_surface_destroy(output);
583
    return CHROMA_ERROR_EGL;
584
  }
585
586
  // Create EGL surface
587
  output->egl_surface =
588
      eglCreateWindowSurface(state->egl_display, state->egl_config,
589
                             (EGLNativeWindowType)output->egl_window, NULL);
590
  if (output->egl_surface == EGL_NO_SURFACE) {
591
    chroma_log("ERROR", "Failed to create EGL surface for output %u: 0x%04x",
592
               output->id, eglGetError());
593
    chroma_surface_destroy(output);
594
    return CHROMA_ERROR_EGL;
595
  }
596
597
  chroma_log("INFO", "Created surface for output %u (%dx%d)", output->id,
598
             output->width, output->height);
599
600
  // Log surface creation resource allocation
601
  size_t surface_size =
602
      (size_t)output->width * output->height * 4; // estimate RGBA surface
603
  chroma_log_resource_allocation("egl_surface", surface_size, "output surface");
604
605
  return CHROMA_OK;
606
}
607
608
// Destroy surface
609
void chroma_surface_destroy(chroma_output_t *output) {
610
  if (!output) {
611
    return;
612
  }
613
614
  // Clean up OpenGL resources first
615
  cleanup_gl_resources(output);
616
617
  if (output->egl_surface != EGL_NO_SURFACE) {
618
    eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface);
619
    output->egl_surface = EGL_NO_SURFACE;
620
  }
621
622
  if (output->egl_window) {
623
    wl_egl_window_destroy(output->egl_window);
624
    output->egl_window = NULL;
625
  }
626
627
  if (output->layer_surface) {
628
    zwlr_layer_surface_v1_destroy(output->layer_surface);
629
    output->layer_surface = NULL;
630
  }
631
632
  if (output->surface) {
633
    wl_surface_destroy(output->surface);
634
    output->surface = NULL;
635
  }
636
637
  // Log surface destruction
638
  size_t surface_size =
639
      (size_t)output->width * output->height * 4; // estimate RGBA surface
640
  chroma_log_resource_deallocation("egl_surface", surface_size,
641
                                   "output surface cleanup");
642
643
  chroma_log("DEBUG", "Destroyed surface for output %u", output->id);
644
}
645
646
// Render wallpaper to output using cached resources
647
int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
648
  if (!state || !output || !output->image || !output->image->loaded) {
649
    return CHROMA_ERROR_INIT;
650
  }
651
652
  // Make context current
653
  if (!eglMakeCurrent(state->egl_display, output->egl_surface,
654
                      output->egl_surface, state->egl_context)) {
655
    chroma_log("ERROR", "Failed to make EGL context current: 0x%04x",
656
               eglGetError());
657
    return CHROMA_ERROR_EGL;
658
  }
659
660
  if (init_gl_resources(output) != CHROMA_OK) {
661
    return CHROMA_ERROR_EGL;
662
  }
663
664
  if (output->texture_id == 0) {
665
    if (update_texture_from_image(output, output->image,
666
                                  output->filter_quality) != CHROMA_OK) {
667
      chroma_log("ERROR", "Failed to update texture for output %u", output->id);
668
      return CHROMA_ERROR_EGL;
669
    }
670
  }
671
672
  // Set viewport
673
  glViewport(0, 0, output->width, output->height);
674
675
  // Clear screen
676
  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
677
  glClear(GL_COLOR_BUFFER_BIT);
678
679
  // Use cached shader program
680
  glUseProgram(output->shader_program);
681
682
  // Bind cached texture
683
  glActiveTexture(GL_TEXTURE0);
684
  glBindTexture(GL_TEXTURE_2D, output->texture_id);
685
  glUniform1i(glGetUniformLocation(output->shader_program, "texture"), 0);
686
687
  // Calculate texture coordinates based on scaling mode
688
  float tex_coords[8];
689
  calculate_texture_coords(output->scale_mode, output->image->width,
690
                           output->image->height, output->width, output->height,
691
                           tex_coords);
692
693
  // Create dynamic vertex data with calculated texture coordinates
694
  float dynamic_vertices[] = {
695
      // Position  Texcoord
696
      -1.0f, -1.0f, tex_coords[0], tex_coords[1], // bottom-left
697
      1.0f,  -1.0f, tex_coords[2], tex_coords[3], // bottom-right
698
      1.0f,  1.0f,  tex_coords[4], tex_coords[5], // top-right
699
      -1.0f, 1.0f,  tex_coords[6], tex_coords[7]  // top-left
700
  };
701
702
  // Update VBO with dynamic texture coordinates
703
  glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
704
  glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(dynamic_vertices),
705
                  dynamic_vertices);
706
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
707
708
  // Set vertex attributes
709
  GLint position_attr = glGetAttribLocation(output->shader_program, "position");
710
  GLint texcoord_attr = glGetAttribLocation(output->shader_program, "texcoord");
711
712
  glEnableVertexAttribArray(position_attr);
713
  glVertexAttribPointer(position_attr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
714
                        (void *)0);
715
716
  glEnableVertexAttribArray(texcoord_attr);
717
  glVertexAttribPointer(texcoord_attr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
718
                        (void *)(2 * sizeof(float)));
719
720
  // Draw
721
  glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
722
723
  // Unbind resources
724
  glBindBuffer(GL_ARRAY_BUFFER, 0);
725
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
726
  glBindTexture(GL_TEXTURE_2D, 0);
727
  glUseProgram(0);
728
729
  // Swap buffers
730
  if (!eglSwapBuffers(state->egl_display, output->egl_surface)) {
731
    chroma_log("ERROR", "Failed to swap buffers for output %u: 0x%04x",
732
               output->id, eglGetError());
733
    return CHROMA_ERROR_EGL;
734
  }
735
736
  // Commit surface
737
  wl_surface_commit(output->surface);
738
739
  chroma_log("DEBUG", "Rendered wallpaper to output %u (cached resources)",
740
             output->id);
741
  return CHROMA_OK;
742
}
743
744
// Invalidate texture cache for output
745
void chroma_output_invalidate_texture(chroma_output_t *output) {
746
  if (!output || !output->gl_resources_initialized) {
747
    return;
748
  }
749
750
  if (output->texture_id != 0) {
751
    glDeleteTextures(1, &output->texture_id);
752
    output->texture_id = 0;
753
    output->texture_uploaded = false; // reset upload flag
754
    chroma_log("DEBUG", "Invalidated texture cache for output %u", output->id);
755
  }
756
}