brewery
notashelf /
1f9050b69ec8435d340c92ea91db9a17d1bb7e2e

chroma

public

Lightweight wallpaper daemon for Wayland

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