brewery
notashelf /
4d100741815950891775083d36a2f08d8da3b500

chroma

public

Lightweight wallpaper daemon for Wayland

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