brewery
notashelf /
b6780cc1804075f6863dcf0a370a5bf645082014

chroma

public

Lightweight wallpaper daemon for Wayland

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