render: migrate rendering pipeline from OpenGL to GLES 2.0

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I55267367c8001ffc5ceac2c64015a7686a6a6964
This commit is contained in:
raf 2026-05-01 12:59:53 +03:00
commit f4275cb0f8
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF

View file

@ -187,7 +187,6 @@ static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
// Vertex shader for simple texture rendering
static const char *vertex_shader_source =
"#version 120\n"
"attribute vec2 position;\n"
"attribute vec2 texcoord;\n"
"varying vec2 v_texcoord;\n"
@ -198,7 +197,7 @@ static const char *vertex_shader_source =
// Fragment shader for simple texture rendering
static const char *fragment_shader_source =
"#version 120\n"
"precision mediump float;\n"
"varying vec2 v_texcoord;\n"
"uniform sampler2D texture;\n"
"void main() {\n"
@ -214,7 +213,7 @@ static const float vertices[] = {
-1.0f, 1.0f, 0.0f, 0.0f, // top left
};
static const unsigned int indices[] = {
static const unsigned short indices[] = {
0, 1, 2, // first triangle
2, 3, 0 // second triangle
};
@ -284,17 +283,29 @@ static GLuint create_shader_program(void) {
}
// Initialize OpenGL resources for output
static int init_gl_resources(chroma_output_t *output) {
static int init_gl_resources(chroma_state_t *state, chroma_output_t *output) {
if (!output || output->gl_resources_initialized) {
return CHROMA_OK;
}
// Create shader prog
output->shader_program = create_shader_program();
if (!output->shader_program) {
chroma_log("ERROR", "Failed to create shader program for output %u",
// Use shared shader program from state if available
if (state && state->shader_program != 0) {
output->shader_program = state->shader_program;
chroma_log("DEBUG", "Using shared shader program for output %u",
output->id);
return CHROMA_ERROR_EGL;
} else {
// Create shader program
output->shader_program = create_shader_program();
if (!output->shader_program) {
chroma_log("ERROR", "Failed to create shader program for output %u",
output->id);
return CHROMA_ERROR_EGL;
}
// Store in state for sharing
if (state) {
state->shader_program = output->shader_program;
chroma_log("DEBUG", "Created shared shader program");
}
}
// Create and setup VBO/EBO
@ -308,6 +319,10 @@ static int init_gl_resources(chroma_output_t *output) {
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
GL_STATIC_DRAW);
// Unbind buffers
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
output->texture_id = 0; // will be created when image is assigned
output->vbo_dirty = true; // VBO needs initial update
output->gl_resources_initialized = true;
@ -370,9 +385,24 @@ static int update_texture_from_image(chroma_output_t *output,
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);
// Upload texture data (always RGBA now)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, image->data);
// Upload texture data with appropriate format based on channels
GLint internal_format;
GLenum format;
if (image->channels == 4) {
internal_format = GL_RGBA;
format = GL_RGBA;
} else if (image->channels == 3) {
internal_format = GL_RGB;
format = GL_RGB;
} else if (image->channels == 2) {
internal_format = GL_LUMINANCE_ALPHA;
format = GL_LUMINANCE_ALPHA;
} else {
internal_format = GL_LUMINANCE;
format = GL_LUMINANCE;
}
glTexImage2D(GL_TEXTURE_2D, 0, internal_format, image->width, image->height,
0, format, GL_UNSIGNED_BYTE, image->data);
// Generate mipmaps for trilinear filtering if they're needed
if (needs_mipmaps) {
@ -385,38 +415,24 @@ static int update_texture_from_image(chroma_output_t *output,
// Mark this output as having uploaded its texture
output->texture_uploaded = true;
// Free system RAM copy only when ALL outputs using this image have uploaded
// to GPU
if (image->data) {
// Count total outputs using this image and how many have uploaded
int total_using = 0;
int uploaded_count = 0;
chroma_state_t *state = output->state;
for (int i = 0; i < state->output_count; i++) {
if (state->outputs[i].active && state->outputs[i].image == image) {
total_using++;
if (state->outputs[i].texture_uploaded) {
uploaded_count++;
}
}
}
// Only free image data when ALL outputs using it have uploaded
if (total_using > 0 && uploaded_count >= total_using) {
size_t freed_bytes = (size_t)image->width * (size_t)image->height *
(size_t)image->channels;
stbi_image_free(image->data);
image->data = NULL;
chroma_log("INFO",
"Freed %.2f MB of image data after all %d outputs uploaded to "
"GPU: %s",
(double)freed_bytes / (1024.0 * 1024.0), total_using,
image->path);
chroma_log_resource_deallocation("image_data", freed_bytes,
"post-gpu-upload");
chroma_log_memory_stats("post-gpu-upload");
}
// Decrement reference count and free system RAM when all outputs
// have uploaded their textures
image->ref_count--;
if (image->ref_count <= 0 && image->data) {
size_t freed_bytes =
(size_t)image->width * (size_t)image->height * (size_t)image->channels;
stbi_image_free(image->data);
image->data = NULL;
chroma_log("INFO", "Freed %.2f MB of image data after GPU upload: %s",
(double)freed_bytes / (1024.0 * 1024.0), image->path);
chroma_log_resource_deallocation("image_data", freed_bytes,
"post-gpu-upload");
chroma_log_memory_stats("post-gpu-upload");
} else {
chroma_log("DEBUG",
"Keeping image data for %s (ref_count: %d, waiting for %d more "
"outputs)",
image->path, image->ref_count, image->ref_count);
}
chroma_log("DEBUG", "Updated texture for output %u (%dx%d)", output->id,
@ -425,7 +441,8 @@ static int update_texture_from_image(chroma_output_t *output,
}
// Cleanup OpenGL resources for output
static void cleanup_gl_resources(chroma_output_t *output) {
static void cleanup_gl_resources(chroma_state_t *state,
chroma_output_t *output) {
if (!output || !output->gl_resources_initialized) {
return;
}
@ -436,8 +453,12 @@ static void cleanup_gl_resources(chroma_output_t *output) {
output->texture_id = 0;
}
// Don't delete shared shader program here, it's managed by state
if (output->shader_program != 0) {
glDeleteProgram(output->shader_program);
// Only delete if this output owns the program (not shared)
if (state && state->shader_program != output->shader_program) {
glDeleteProgram(output->shader_program);
}
output->shader_program = 0;
}
@ -466,9 +487,9 @@ static int choose_egl_config(EGLDisplay display, EGLConfig *config) {
EGL_BLUE_SIZE,
8,
EGL_ALPHA_SIZE,
8,
0,
EGL_RENDERABLE_TYPE,
EGL_OPENGL_BIT,
EGL_OPENGL_ES2_BIT,
EGL_NONE};
EGLint num_configs;
@ -508,9 +529,9 @@ int chroma_egl_init(chroma_state_t *state) {
chroma_log("INFO", "EGL initialized: version %d.%d", major, minor);
chroma_log_memory_stats("post-egl-init");
// Bind OpenGL API
if (!eglBindAPI(EGL_OPENGL_API)) {
chroma_log("ERROR", "Failed to bind OpenGL API: 0x%04x", eglGetError());
// Bind OpenGL ES API for better efficiency on embedded/GPU drivers
if (!eglBindAPI(EGL_OPENGL_ES_API)) {
chroma_log("ERROR", "Failed to bind OpenGL ES API: 0x%04x", eglGetError());
chroma_egl_cleanup(state);
return CHROMA_ERROR_EGL;
}
@ -521,9 +542,8 @@ int chroma_egl_init(chroma_state_t *state) {
return CHROMA_ERROR_EGL;
}
// Create EGL context
EGLint context_attributes[] = {EGL_CONTEXT_MAJOR_VERSION, 2,
EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE};
// Create EGL context for GLES 2.0
EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
state->egl_context = eglCreateContext(state->egl_display, state->egl_config,
EGL_NO_CONTEXT, context_attributes);
@ -543,6 +563,12 @@ void chroma_egl_cleanup(chroma_state_t *state) {
return;
}
// Clean up shared shader program while context is still current
if (state->shader_program != 0) {
glDeleteProgram(state->shader_program);
state->shader_program = 0;
}
if (state->egl_display != EGL_NO_DISPLAY) {
eglMakeCurrent(state->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
EGL_NO_CONTEXT);
@ -644,8 +670,8 @@ void chroma_surface_destroy(chroma_output_t *output) {
return;
}
// Clean up OpenGL resources first
cleanup_gl_resources(output);
// Clean up OpenGL resources first. We use output's back-reference to state
cleanup_gl_resources(output->state, output);
if (output->egl_surface != EGL_NO_SURFACE) {
eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface);
@ -667,6 +693,8 @@ void chroma_surface_destroy(chroma_output_t *output) {
output->surface = NULL;
}
output->configured = false;
// Log surface destruction
size_t surface_size = (size_t)output->width * (size_t)output->height * 4;
chroma_log_resource_deallocation("egl_surface", surface_size,
@ -689,7 +717,7 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
return CHROMA_ERROR_EGL;
}
if (init_gl_resources(output) != CHROMA_OK) {
if (init_gl_resources(state, output) != CHROMA_OK) {
return CHROMA_ERROR_EGL;
}
@ -762,7 +790,7 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
4 * sizeof(float), (void *)(2 * sizeof(float)));
// Draw
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
// Unbind resources
glBindBuffer(GL_ARRAY_BUFFER, 0);
@ -770,16 +798,13 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);
// Swap buffers
// Swap buffers; this implicitly commits the surface for EGL on Wayland
if (!eglSwapBuffers(state->egl_display, output->egl_surface)) {
chroma_log("ERROR", "Failed to swap buffers for output %u: 0x%04x",
output->id, eglGetError());
return CHROMA_ERROR_EGL;
}
// Commit surface
wl_surface_commit(output->surface);
chroma_log("DEBUG", "Rendered wallpaper to output %u (cached resources)",
output->id);
return CHROMA_OK;