/* gskglcommandqueue.c * * Copyright 2017 Timm Bäder * Copyright 2018 Matthias Clasen * Copyright 2018 Alexander Larsson * Copyright 2020 Christian Hergert * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program. If not, see . * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include #include #include #include #include #include #include #include "gskglattachmentstateprivate.h" #include "gskglbufferprivate.h" #include "gskglcommandqueueprivate.h" #include "gskgluniformstateprivate.h" #include "inlinearray.h" G_DEFINE_TYPE (GskGLCommandQueue, gsk_gl_command_queue, G_TYPE_OBJECT) G_GNUC_UNUSED static inline void print_uniform (GskGLUniformFormat format, guint array_count, gconstpointer valueptr) { const union { graphene_matrix_t matrix[0]; GskRoundedRect rounded_rect[0]; float fval[0]; int ival[0]; guint uval[0]; } *data = valueptr; switch (format) { case GSK_GL_UNIFORM_FORMAT_1F: g_printerr ("1f<%f>", data->fval[0]); break; case GSK_GL_UNIFORM_FORMAT_2F: g_printerr ("2f<%f,%f>", data->fval[0], data->fval[1]); break; case GSK_GL_UNIFORM_FORMAT_3F: g_printerr ("3f<%f,%f,%f>", data->fval[0], data->fval[1], data->fval[2]); break; case GSK_GL_UNIFORM_FORMAT_4F: g_printerr ("4f<%f,%f,%f,%f>", data->fval[0], data->fval[1], data->fval[2], data->fval[3]); break; case GSK_GL_UNIFORM_FORMAT_1I: case GSK_GL_UNIFORM_FORMAT_TEXTURE: g_printerr ("1i<%d>", data->ival[0]); break; case GSK_GL_UNIFORM_FORMAT_1UI: g_printerr ("1ui<%u>", data->uval[0]); break; case GSK_GL_UNIFORM_FORMAT_COLOR: { char *str = gdk_rgba_to_string (valueptr); g_printerr ("%s", str); g_free (str); break; } case GSK_GL_UNIFORM_FORMAT_ROUNDED_RECT: { char *str = gsk_rounded_rect_to_string (valueptr); g_printerr ("%s", str); g_free (str); break; } case GSK_GL_UNIFORM_FORMAT_MATRIX: { float mat[16]; graphene_matrix_to_float (&data->matrix[0], mat); g_printerr ("matrix<"); for (guint i = 0; i < G_N_ELEMENTS (mat)-1; i++) g_printerr ("%f,", mat[i]); g_printerr ("%f>", mat[G_N_ELEMENTS (mat)-1]); break; } case GSK_GL_UNIFORM_FORMAT_1FV: case GSK_GL_UNIFORM_FORMAT_2FV: case GSK_GL_UNIFORM_FORMAT_3FV: case GSK_GL_UNIFORM_FORMAT_4FV: /* non-V variants are -4 from V variants */ format -= 4; g_printerr ("["); for (guint i = 0; i < array_count; i++) { print_uniform (format, 0, valueptr); if (i + 1 != array_count) g_printerr (","); valueptr = ((guint8*)valueptr + gsk_gl_uniform_format_size (format)); } g_printerr ("]"); break; case GSK_GL_UNIFORM_FORMAT_2I: g_printerr ("2i<%d,%d>", data->ival[0], data->ival[1]); break; case GSK_GL_UNIFORM_FORMAT_3I: g_printerr ("3i<%d,%d,%d>", data->ival[0], data->ival[1], data->ival[2]); break; case GSK_GL_UNIFORM_FORMAT_4I: g_printerr ("3i<%d,%d,%d,%d>", data->ival[0], data->ival[1], data->ival[2], data->ival[3]); break; case GSK_GL_UNIFORM_FORMAT_LAST: default: g_assert_not_reached (); } } G_GNUC_UNUSED static inline void gsk_gl_command_queue_print_batch (GskGLCommandQueue *self, const GskGLCommandBatch *batch) { static const char *command_kinds[] = { "Clear", "Draw", }; guint framebuffer_id; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (batch != NULL); if (batch->any.kind == GSK_GL_COMMAND_KIND_CLEAR) framebuffer_id = batch->clear.framebuffer; else if (batch->any.kind == GSK_GL_COMMAND_KIND_DRAW) framebuffer_id = batch->draw.framebuffer; else return; g_printerr ("Batch {\n"); g_printerr (" Kind: %s\n", command_kinds[batch->any.kind]); g_printerr (" Viewport: %dx%d\n", batch->any.viewport.width, batch->any.viewport.height); g_printerr (" Framebuffer: %d\n", framebuffer_id); if (batch->any.kind == GSK_GL_COMMAND_KIND_DRAW) { g_printerr (" Program: %d\n", batch->any.program); g_printerr (" Vertices: %d\n", batch->draw.vbo_count); for (guint i = 0; i < batch->draw.bind_count; i++) { const GskGLCommandBind *bind = &self->batch_binds.items[batch->draw.bind_offset + i]; g_printerr (" Bind[%d]: %u\n", bind->texture, bind->id); } for (guint i = 0; i < batch->draw.uniform_count; i++) { const GskGLCommandUniform *uniform = &self->batch_uniforms.items[batch->draw.uniform_offset + i]; g_printerr (" Uniform[%02d]: ", uniform->location); print_uniform (uniform->info.format, uniform->info.array_count, gsk_gl_uniform_state_get_uniform_data (self->uniforms, uniform->info.offset)); g_printerr ("\n"); } } else if (batch->any.kind == GSK_GL_COMMAND_KIND_CLEAR) { g_printerr (" Bits: 0x%x\n", batch->clear.bits); } g_printerr ("}\n"); } G_GNUC_UNUSED static inline void gsk_gl_command_queue_capture_png (GskGLCommandQueue *self, const char *filename, guint width, guint height, gboolean flip_y) { guint stride; guint8 *data; GBytes *bytes; GdkTexture *texture; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (filename != NULL); stride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, width); data = g_malloc_n (height, stride); glReadPixels (0, 0, width, height, GL_BGRA, GL_UNSIGNED_BYTE, data); if (flip_y) { guint8 *flipped = g_malloc_n (height, stride); for (guint i = 0; i < height; i++) memcpy (flipped + (height * stride) - ((i + 1) * stride), data + (stride * i), stride); g_free (data); data = flipped; } bytes = g_bytes_new_take (data, height * stride); texture = gdk_memory_texture_new (width, height, GDK_MEMORY_DEFAULT, bytes, stride); g_bytes_unref (bytes); gdk_texture_save_to_png (texture, filename); g_object_unref (texture); } static inline gboolean will_ignore_batch (GskGLCommandQueue *self) { if G_LIKELY (self->batches.len < G_MAXINT16) return FALSE; if (!self->have_truncated) { self->have_truncated = TRUE; g_critical ("GL command queue too large, truncating further batches."); } return TRUE; } static inline guint snapshot_attachments (const GskGLAttachmentState *state, GskGLCommandBinds *array) { GskGLCommandBind *bind = gsk_gl_command_binds_append_n (array, G_N_ELEMENTS (state->textures)); guint count = 0; for (guint i = 0; i < G_N_ELEMENTS (state->textures); i++) { if (state->textures[i].id) { bind[count].id = state->textures[i].id; bind[count].texture = state->textures[i].texture; count++; } } if (count != G_N_ELEMENTS (state->textures)) array->len -= G_N_ELEMENTS (state->textures) - count; return count; } static inline guint snapshot_uniforms (GskGLUniformState *state, GskGLUniformProgram *program, GskGLCommandUniforms *array) { GskGLCommandUniform *uniform = gsk_gl_command_uniforms_append_n (array, program->n_mappings); guint count = 0; for (guint i = 0; i < program->n_mappings; i++) { const GskGLUniformMapping *mapping = &program->mappings[i]; if (!mapping->info.initial && mapping->info.format && mapping->location > -1) { uniform[count].location = mapping->location; uniform[count].info = mapping->info; count++; } } if (count != program->n_mappings) array->len -= program->n_mappings - count; return count; } static inline gboolean snapshots_equal (GskGLCommandQueue *self, GskGLCommandBatch *first, GskGLCommandBatch *second) { if (first->draw.bind_count != second->draw.bind_count || first->draw.uniform_count != second->draw.uniform_count) return FALSE; for (guint i = 0; i < first->draw.bind_count; i++) { const GskGLCommandBind *fb = &self->batch_binds.items[first->draw.bind_offset+i]; const GskGLCommandBind *sb = &self->batch_binds.items[second->draw.bind_offset+i]; if (fb->id != sb->id || fb->texture != sb->texture) return FALSE; } for (guint i = 0; i < first->draw.uniform_count; i++) { const GskGLCommandUniform *fu = &self->batch_uniforms.items[first->draw.uniform_offset+i]; const GskGLCommandUniform *su = &self->batch_uniforms.items[second->draw.uniform_offset+i]; gconstpointer fdata; gconstpointer sdata; gsize len; /* Short circuit if we'd end up with the same memory */ if (fu->info.offset == su->info.offset) continue; if (fu->info.format != su->info.format || fu->info.array_count != su->info.array_count) return FALSE; fdata = gsk_gl_uniform_state_get_uniform_data (self->uniforms, fu->info.offset); sdata = gsk_gl_uniform_state_get_uniform_data (self->uniforms, su->info.offset); switch (fu->info.format) { case GSK_GL_UNIFORM_FORMAT_1F: case GSK_GL_UNIFORM_FORMAT_1FV: case GSK_GL_UNIFORM_FORMAT_1I: case GSK_GL_UNIFORM_FORMAT_TEXTURE: case GSK_GL_UNIFORM_FORMAT_1UI: len = 4; break; case GSK_GL_UNIFORM_FORMAT_2F: case GSK_GL_UNIFORM_FORMAT_2FV: case GSK_GL_UNIFORM_FORMAT_2I: len = 8; break; case GSK_GL_UNIFORM_FORMAT_3F: case GSK_GL_UNIFORM_FORMAT_3FV: case GSK_GL_UNIFORM_FORMAT_3I: len = 12; break; case GSK_GL_UNIFORM_FORMAT_4F: case GSK_GL_UNIFORM_FORMAT_4FV: case GSK_GL_UNIFORM_FORMAT_4I: len = 16; break; case GSK_GL_UNIFORM_FORMAT_MATRIX: len = sizeof (float) * 16; break; case GSK_GL_UNIFORM_FORMAT_ROUNDED_RECT: len = sizeof (float) * 12; break; case GSK_GL_UNIFORM_FORMAT_COLOR: len = sizeof (float) * 4; break; default: g_assert_not_reached (); } len *= fu->info.array_count; if (memcmp (fdata, sdata, len) != 0) return FALSE; } return TRUE; } static void gsk_gl_command_queue_dispose (GObject *object) { GskGLCommandQueue *self = (GskGLCommandQueue *)object; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_clear_object (&self->profiler); g_clear_object (&self->gl_profiler); g_clear_object (&self->context); g_clear_pointer (&self->attachments, gsk_gl_attachment_state_unref); g_clear_pointer (&self->uniforms, gsk_gl_uniform_state_unref); gsk_gl_command_batches_clear (&self->batches); gsk_gl_command_binds_clear (&self->batch_binds); gsk_gl_command_uniforms_clear (&self->batch_uniforms); gsk_gl_buffer_destroy (&self->vertices); G_OBJECT_CLASS (gsk_gl_command_queue_parent_class)->dispose (object); } static void gsk_gl_command_queue_class_init (GskGLCommandQueueClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = gsk_gl_command_queue_dispose; } static void gsk_gl_command_queue_init (GskGLCommandQueue *self) { self->max_texture_size = -1; gsk_gl_command_batches_init (&self->batches, 128); gsk_gl_command_binds_init (&self->batch_binds, 1024); gsk_gl_command_uniforms_init (&self->batch_uniforms, 2048); gsk_gl_buffer_init (&self->vertices, GL_ARRAY_BUFFER, sizeof (GskGLDrawVertex)); } GskGLCommandQueue * gsk_gl_command_queue_new (GdkGLContext *context, GskGLUniformState *uniforms) { GskGLCommandQueue *self; g_return_val_if_fail (GDK_IS_GL_CONTEXT (context), NULL); self = g_object_new (GSK_TYPE_GL_COMMAND_QUEUE, NULL); self->context = g_object_ref (context); self->attachments = gsk_gl_attachment_state_new (); /* Use shared uniform state if we're provided one */ if (uniforms != NULL) self->uniforms = gsk_gl_uniform_state_ref (uniforms); else self->uniforms = gsk_gl_uniform_state_new (); /* Determine max texture size immediately and restore context */ gdk_gl_context_make_current (context); glGetIntegerv (GL_MAX_TEXTURE_SIZE, &self->max_texture_size); return g_steal_pointer (&self); } static inline GskGLCommandBatch * begin_next_batch (GskGLCommandQueue *self) { GskGLCommandBatch *batch; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); /* GskGLCommandBatch contains an embedded linked list using integers into the * self->batches array. We can't use pointer because the batches could be * realloc()'d at runtime. * * Before we execute the command queue, we sort the batches by framebuffer but * leave the batches in place as we can just tweak the links via prev/next. * * Generally we only traverse forwards, so we could ignore the previous field. * But to optimize the reordering of batches by framebuffer we walk backwards * so we sort by most-recently-seen framebuffer to ensure draws happen in the * proper order. */ batch = gsk_gl_command_batches_append (&self->batches); batch->any.next_batch_index = -1; batch->any.prev_batch_index = self->tail_batch_index; return batch; } static void enqueue_batch (GskGLCommandQueue *self) { guint index; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (self->batches.len > 0); /* Batches are linked lists but using indexes into the batches array instead * of pointers. This is for two main reasons. First, 16-bit indexes allow us * to store the information in 4 bytes, where as two pointers would take 16 * bytes. Furthermore, we have an array here so pointers would get * invalidated if we realloc()'d (and that can happen from time to time). */ index = self->batches.len - 1; if (self->head_batch_index == -1) self->head_batch_index = index; if (self->tail_batch_index != -1) { GskGLCommandBatch *prev = &self->batches.items[self->tail_batch_index]; prev->any.next_batch_index = index; } self->tail_batch_index = index; } static void discard_batch (GskGLCommandQueue *self) { g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (self->batches.len > 0); self->batches.len--; } void gsk_gl_command_queue_begin_draw (GskGLCommandQueue *self, GskGLUniformProgram *program, guint width, guint height) { GskGLCommandBatch *batch; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (self->in_draw == FALSE); g_assert (width <= G_MAXUINT16); g_assert (height <= G_MAXUINT16); /* Our internal links use 16-bits, so that is our max number * of batches we can have in one frame. */ if (will_ignore_batch (self)) return; self->program_info = program; batch = begin_next_batch (self); batch->any.kind = GSK_GL_COMMAND_KIND_DRAW; batch->any.program = program->program_id; batch->any.next_batch_index = -1; batch->any.viewport.width = width; batch->any.viewport.height = height; batch->draw.framebuffer = 0; batch->draw.uniform_count = 0; batch->draw.uniform_offset = self->batch_uniforms.len; batch->draw.bind_count = 0; batch->draw.bind_offset = self->batch_binds.len; batch->draw.vbo_count = 0; batch->draw.vbo_offset = gsk_gl_buffer_get_offset (&self->vertices); self->fbo_max = MAX (self->fbo_max, batch->draw.framebuffer); self->in_draw = TRUE; } void gsk_gl_command_queue_end_draw (GskGLCommandQueue *self) { GskGLCommandBatch *last_batch; GskGLCommandBatch *batch; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (self->batches.len > 0); if (will_ignore_batch (self)) { self->in_draw = FALSE; return; } batch = gsk_gl_command_batches_tail (&self->batches); g_assert (self->in_draw == TRUE); g_assert (batch->any.kind == GSK_GL_COMMAND_KIND_DRAW); if G_UNLIKELY (batch->draw.vbo_count == 0) { discard_batch (self); self->in_draw = FALSE; return; } /* Track the destination framebuffer in case it changed */ batch->draw.framebuffer = self->attachments->fbo.id; self->attachments->fbo.changed = FALSE; self->fbo_max = MAX (self->fbo_max, self->attachments->fbo.id); /* Save our full uniform state for this draw so we can possibly * reorder the draw later. */ batch->draw.uniform_offset = self->batch_uniforms.len; batch->draw.uniform_count = snapshot_uniforms (self->uniforms, self->program_info, &self->batch_uniforms); /* Track the bind attachments that changed */ if (self->program_info->has_attachments) { batch->draw.bind_offset = self->batch_binds.len; batch->draw.bind_count = snapshot_attachments (self->attachments, &self->batch_binds); } else { batch->draw.bind_offset = 0; batch->draw.bind_count = 0; } if (self->batches.len > 1) last_batch = &self->batches.items[self->batches.len - 2]; else last_batch = NULL; /* Do simple chaining of draw to last batch. */ if (last_batch != NULL && last_batch->any.kind == GSK_GL_COMMAND_KIND_DRAW && last_batch->any.program == batch->any.program && last_batch->any.viewport.width == batch->any.viewport.width && last_batch->any.viewport.height == batch->any.viewport.height && last_batch->draw.framebuffer == batch->draw.framebuffer && last_batch->draw.vbo_offset + last_batch->draw.vbo_count == batch->draw.vbo_offset && last_batch->draw.vbo_count + batch->draw.vbo_count <= 0xffff && snapshots_equal (self, last_batch, batch)) { last_batch->draw.vbo_count += batch->draw.vbo_count; discard_batch (self); } else { enqueue_batch (self); } self->in_draw = FALSE; self->program_info = NULL; } /** * gsk_gl_command_queue_split_draw: * @self a `GskGLCommandQueue` * * This function is like calling gsk_gl_command_queue_end_draw() followed by * a gsk_gl_command_queue_begin_draw() with the same parameters as a * previous begin draw (if shared uniforms where not changed further). * * This is useful to avoid comparisons inside of loops where we know shared * uniforms are not changing. * * This generally should just be called from gsk_gl_program_split_draw() * as that is where the begin/end flow happens from the render job. */ void gsk_gl_command_queue_split_draw (GskGLCommandQueue *self) { GskGLCommandBatch *batch; GskGLUniformProgram *program; guint width; guint height; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (self->batches.len > 0); g_assert (self->in_draw == TRUE); program = self->program_info; batch = gsk_gl_command_batches_tail (&self->batches); g_assert (batch->any.kind == GSK_GL_COMMAND_KIND_DRAW); width = batch->any.viewport.width; height = batch->any.viewport.height; gsk_gl_command_queue_end_draw (self); gsk_gl_command_queue_begin_draw (self, program, width, height); } void gsk_gl_command_queue_clear (GskGLCommandQueue *self, guint clear_bits, const graphene_rect_t *viewport) { GskGLCommandBatch *batch; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (self->in_draw == FALSE); if (will_ignore_batch (self)) return; if (clear_bits == 0) clear_bits = GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT; batch = begin_next_batch (self); batch->any.kind = GSK_GL_COMMAND_KIND_CLEAR; batch->any.viewport.width = viewport->size.width; batch->any.viewport.height = viewport->size.height; batch->clear.bits = clear_bits; batch->clear.framebuffer = self->attachments->fbo.id; batch->any.next_batch_index = -1; batch->any.program = 0; self->fbo_max = MAX (self->fbo_max, batch->clear.framebuffer); enqueue_batch (self); self->attachments->fbo.changed = FALSE; } GdkGLContext * gsk_gl_command_queue_get_context (GskGLCommandQueue *self) { g_return_val_if_fail (GSK_IS_GL_COMMAND_QUEUE (self), NULL); return self->context; } void gsk_gl_command_queue_make_current (GskGLCommandQueue *self) { g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (GDK_IS_GL_CONTEXT (self->context)); gdk_gl_context_make_current (self->context); } void gsk_gl_command_queue_delete_program (GskGLCommandQueue *self, guint program) { g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); glDeleteProgram (program); } static inline void apply_viewport (guint *current_width, guint *current_height, guint width, guint height) { if G_UNLIKELY (*current_width != width || *current_height != height) { *current_width = width; *current_height = height; glViewport (0, 0, width, height); } } static inline void apply_scissor (gboolean *state, guint framebuffer, const graphene_rect_t *scissor, gboolean has_scissor, guint default_framebuffer) { g_assert (framebuffer != (guint)-1); if (framebuffer != default_framebuffer || !has_scissor) { if (*state != FALSE) { glDisable (GL_SCISSOR_TEST); *state = FALSE; } } else { if (*state != TRUE) { glEnable (GL_SCISSOR_TEST); glScissor (scissor->origin.x, scissor->origin.y, scissor->size.width, scissor->size.height); *state = TRUE; } } } static inline gboolean apply_framebuffer (int *framebuffer, guint new_framebuffer) { if G_UNLIKELY (new_framebuffer != *framebuffer) { *framebuffer = new_framebuffer; glBindFramebuffer (GL_FRAMEBUFFER, new_framebuffer); return TRUE; } return FALSE; } static inline void gsk_gl_command_queue_unlink (GskGLCommandQueue *self, GskGLCommandBatch *batch) { if (batch->any.prev_batch_index == -1) self->head_batch_index = batch->any.next_batch_index; else self->batches.items[batch->any.prev_batch_index].any.next_batch_index = batch->any.next_batch_index; if (batch->any.next_batch_index == -1) self->tail_batch_index = batch->any.prev_batch_index; else self->batches.items[batch->any.next_batch_index].any.prev_batch_index = batch->any.prev_batch_index; batch->any.prev_batch_index = -1; batch->any.next_batch_index = -1; } static inline void gsk_gl_command_queue_insert_before (GskGLCommandQueue *self, GskGLCommandBatch *batch, GskGLCommandBatch *sibling) { int sibling_index; int index; g_assert (batch >= self->batches.items); g_assert (batch < &self->batches.items[self->batches.len]); g_assert (sibling >= self->batches.items); g_assert (sibling < &self->batches.items[self->batches.len]); index = gsk_gl_command_batches_index_of (&self->batches, batch); sibling_index = gsk_gl_command_batches_index_of (&self->batches, sibling); batch->any.next_batch_index = sibling_index; batch->any.prev_batch_index = sibling->any.prev_batch_index; if (batch->any.prev_batch_index > -1) self->batches.items[batch->any.prev_batch_index].any.next_batch_index = index; sibling->any.prev_batch_index = index; if (batch->any.prev_batch_index == -1) self->head_batch_index = index; } static void gsk_gl_command_queue_sort_batches (GskGLCommandQueue *self) { int *seen; int *seen_free = NULL; int index; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (self->tail_batch_index >= 0); g_assert (self->fbo_max >= 0); /* Create our seen list with most recent index set to -1, * meaning we haven't yet seen that framebuffer. */ if (self->fbo_max < 1024) seen = g_alloca (sizeof (int) * (self->fbo_max + 1)); else seen = seen_free = g_new0 (int, (self->fbo_max + 1)); for (int i = 0; i <= self->fbo_max; i++) seen[i] = -1; /* Walk in reverse, and if we've seen that framebuffer before, we want to * delay this operation until right before the last batch we saw for that * framebuffer. * * We can do this because we don't use a framebuffer's texture until it has * been completely drawn. */ index = self->tail_batch_index; while (index >= 0) { GskGLCommandBatch *batch = &self->batches.items[index]; int cur_index = index; int fbo = -1; g_assert (index > -1); g_assert (index < self->batches.len); switch (batch->any.kind) { case GSK_GL_COMMAND_KIND_DRAW: fbo = batch->draw.framebuffer; break; case GSK_GL_COMMAND_KIND_CLEAR: fbo = batch->clear.framebuffer; break; default: g_assert_not_reached (); } index = batch->any.prev_batch_index; g_assert (index >= -1); g_assert (index < (int)self->batches.len); g_assert (fbo >= -1); if (fbo == -1) continue; g_assert (fbo <= self->fbo_max); g_assert (seen[fbo] >= -1); g_assert (seen[fbo] < (int)self->batches.len); if (seen[fbo] != -1 && seen[fbo] != batch->any.next_batch_index) { int mru_index = seen[fbo]; GskGLCommandBatch *mru = &self->batches.items[mru_index]; g_assert (mru_index > -1); gsk_gl_command_queue_unlink (self, batch); g_assert (batch->any.prev_batch_index == -1); g_assert (batch->any.next_batch_index == -1); gsk_gl_command_queue_insert_before (self, batch, mru); g_assert (batch->any.prev_batch_index > -1 || self->head_batch_index == cur_index); g_assert (batch->any.next_batch_index == seen[fbo]); } g_assert (cur_index > -1); g_assert (seen[fbo] >= -1); seen[fbo] = cur_index; } g_free (seen_free); } /** * gsk_gl_command_queue_execute: * @self: a `GskGLCommandQueue` * @surface_height: the height of the backing surface * @scale_factor: the scale factor of the backing surface * @scissor: (nullable): the scissor clip if any * @default_framebuffer: the default framebuffer id if not zero * * Executes all of the batches in the command queue. * * Typically, the scissor rect is only applied when rendering to the default * framebuffer (zero in most cases). However, if @default_framebuffer is not * zero, it will be checked to see if the rendering target matches so that * the scissor rect is applied. This should be used in cases where rendering * to the backbuffer for display is not the default GL framebuffer of zero. * Currently, this happens when rendering on macOS using IOSurface. */ void gsk_gl_command_queue_execute (GskGLCommandQueue *self, guint surface_height, guint scale_factor, const cairo_region_t *scissor, guint default_framebuffer) { G_GNUC_UNUSED guint count = 0; graphene_rect_t scissor_test = GRAPHENE_RECT_INIT (0, 0, 0, 0); gboolean has_scissor = scissor != NULL; gboolean scissor_state = -1; guint program = 0; guint width = 0; guint height = 0; G_GNUC_UNUSED guint n_binds = 0; guint n_fbos = 0; G_GNUC_UNUSED guint n_uniforms = 0; guint n_programs = 0; guint vao_id; guint vbo_id; int textures[4]; int framebuffer = -1; int next_batch_index; int active = -1; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (self->in_draw == FALSE); if (self->batches.len == 0) return; for (guint i = 0; i < G_N_ELEMENTS (textures); i++) textures[i] = -1; gsk_gl_command_queue_sort_batches (self); gsk_gl_command_queue_make_current (self); #ifdef G_ENABLE_DEBUG gsk_gl_profiler_begin_gpu_region (self->gl_profiler); gsk_profiler_timer_begin (self->profiler, self->metrics.cpu_time); #endif glEnable (GL_DEPTH_TEST); glDepthFunc (GL_LEQUAL); /* Pre-multiplied alpha */ glEnable (GL_BLEND); glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glBlendEquation (GL_FUNC_ADD); glGenVertexArrays (1, &vao_id); glBindVertexArray (vao_id); vbo_id = gsk_gl_buffer_submit (&self->vertices); /* 0 = position location */ glEnableVertexAttribArray (0); glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE, sizeof (GskGLDrawVertex), (void *) G_STRUCT_OFFSET (GskGLDrawVertex, position)); /* 1 = texture coord location */ glEnableVertexAttribArray (1); glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE, sizeof (GskGLDrawVertex), (void *) G_STRUCT_OFFSET (GskGLDrawVertex, uv)); /* 2 = color location */ glEnableVertexAttribArray (2); glVertexAttribPointer (2, 4, GL_HALF_FLOAT, GL_FALSE, sizeof (GskGLDrawVertex), (void *) G_STRUCT_OFFSET (GskGLDrawVertex, color)); /* 3 = color2 location */ glEnableVertexAttribArray (3); glVertexAttribPointer (3, 4, GL_HALF_FLOAT, GL_FALSE, sizeof (GskGLDrawVertex), (void *) G_STRUCT_OFFSET (GskGLDrawVertex, color2)); /* Setup initial scissor clip */ if (scissor != NULL) { cairo_rectangle_int_t r; g_assert (cairo_region_num_rectangles (scissor) == 1); cairo_region_get_rectangle (scissor, 0, &r); scissor_test.origin.x = r.x * scale_factor; scissor_test.origin.y = surface_height - (r.height * scale_factor) - (r.y * scale_factor); scissor_test.size.width = r.width * scale_factor; scissor_test.size.height = r.height * scale_factor; } next_batch_index = self->head_batch_index; while (next_batch_index >= 0) { const GskGLCommandBatch *batch = &self->batches.items[next_batch_index]; g_assert (next_batch_index >= 0); g_assert (next_batch_index < self->batches.len); g_assert (batch->any.next_batch_index != next_batch_index); count++; switch (batch->any.kind) { case GSK_GL_COMMAND_KIND_CLEAR: if (apply_framebuffer (&framebuffer, batch->clear.framebuffer)) { apply_scissor (&scissor_state, framebuffer, &scissor_test, has_scissor, default_framebuffer); n_fbos++; } apply_viewport (&width, &height, batch->any.viewport.width, batch->any.viewport.height); glClearColor (0, 0, 0, 0); glClear (batch->clear.bits); break; case GSK_GL_COMMAND_KIND_DRAW: if (batch->any.program != program) { program = batch->any.program; glUseProgram (program); n_programs++; } if (apply_framebuffer (&framebuffer, batch->draw.framebuffer)) { apply_scissor (&scissor_state, framebuffer, &scissor_test, has_scissor, default_framebuffer); n_fbos++; } apply_viewport (&width, &height, batch->any.viewport.width, batch->any.viewport.height); if G_UNLIKELY (batch->draw.bind_count > 0) { const GskGLCommandBind *bind = &self->batch_binds.items[batch->draw.bind_offset]; for (guint i = 0; i < batch->draw.bind_count; i++) { if (textures[bind->texture] != bind->id) { if (active != bind->texture) { active = bind->texture; glActiveTexture (GL_TEXTURE0 + bind->texture); } glBindTexture (GL_TEXTURE_2D, bind->id); textures[bind->texture] = bind->id; } bind++; } n_binds += batch->draw.bind_count; } if (batch->draw.uniform_count > 0) { const GskGLCommandUniform *u = &self->batch_uniforms.items[batch->draw.uniform_offset]; for (guint i = 0; i < batch->draw.uniform_count; i++, u++) gsk_gl_uniform_state_apply (self->uniforms, program, u->location, u->info); n_uniforms += batch->draw.uniform_count; } glDrawArrays (GL_TRIANGLES, batch->draw.vbo_offset, batch->draw.vbo_count); break; default: g_assert_not_reached (); } #if 0 if (batch->any.kind == GSK_GL_COMMAND_KIND_DRAW || batch->any.kind == GSK_GL_COMMAND_KIND_CLEAR) { char filename[128]; g_snprintf (filename, sizeof filename, "capture%03u_batch%03d_kind%u_program%u_u%u_b%u_fb%u_ctx%p.png", count, next_batch_index, batch->any.kind, batch->any.program, batch->any.kind == GSK_GL_COMMAND_KIND_DRAW ? batch->draw.uniform_count : 0, batch->any.kind == GSK_GL_COMMAND_KIND_DRAW ? batch->draw.bind_count : 0, framebuffer, gdk_gl_context_get_current ()); gsk_gl_command_queue_capture_png (self, filename, width, height, TRUE); gsk_gl_command_queue_print_batch (self, batch); } #endif next_batch_index = batch->any.next_batch_index; } glDeleteBuffers (1, &vbo_id); glDeleteVertexArrays (1, &vao_id); gdk_profiler_set_int_counter (self->metrics.n_binds, n_binds); gdk_profiler_set_int_counter (self->metrics.n_uniforms, n_uniforms); gdk_profiler_set_int_counter (self->metrics.n_fbos, n_fbos); gdk_profiler_set_int_counter (self->metrics.n_programs, n_programs); gdk_profiler_set_int_counter (self->metrics.n_uploads, self->n_uploads); gdk_profiler_set_int_counter (self->metrics.queue_depth, self->batches.len); #ifdef G_ENABLE_DEBUG { gint64 start_time G_GNUC_UNUSED = gsk_profiler_timer_get_start (self->profiler, self->metrics.cpu_time); gint64 cpu_time = gsk_profiler_timer_end (self->profiler, self->metrics.cpu_time); gint64 gpu_time = gsk_gl_profiler_end_gpu_region (self->gl_profiler); gsk_profiler_timer_set (self->profiler, self->metrics.gpu_time, gpu_time); gsk_profiler_timer_set (self->profiler, self->metrics.cpu_time, cpu_time); gsk_profiler_counter_inc (self->profiler, self->metrics.n_frames); gsk_profiler_push_samples (self->profiler); } #endif } void gsk_gl_command_queue_begin_frame (GskGLCommandQueue *self) { g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (self->batches.len == 0); gsk_gl_command_queue_make_current (self); self->fbo_max = 0; self->tail_batch_index = -1; self->head_batch_index = -1; self->in_frame = TRUE; } /** * gsk_gl_command_queue_end_frame: * @self: a `GskGLCommandQueue` * * This function performs cleanup steps that need to be done after * a frame has finished. This is not performed as part of the command * queue execution to allow for the frame to be submitted as soon * as possible. * * However, it should be executed after the draw contexts end_frame * has been called to swap the OpenGL framebuffers. */ void gsk_gl_command_queue_end_frame (GskGLCommandQueue *self) { g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); gsk_gl_command_queue_make_current (self); gsk_gl_uniform_state_end_frame (self->uniforms); /* Reset attachments so we don't hold on to any textures * that might be released after the frame. */ for (guint i = 0; i < G_N_ELEMENTS (self->attachments->textures); i++) { if (self->attachments->textures[i].id != 0) { glActiveTexture (GL_TEXTURE0 + i); glBindTexture (GL_TEXTURE_2D, 0); self->attachments->textures[i].id = 0; self->attachments->textures[i].changed = FALSE; self->attachments->textures[i].initial = TRUE; } } self->batches.len = 0; self->batch_binds.len = 0; self->batch_uniforms.len = 0; self->n_uploads = 0; self->tail_batch_index = -1; self->in_frame = FALSE; } gboolean gsk_gl_command_queue_create_render_target (GskGLCommandQueue *self, int width, int height, int format, int min_filter, int mag_filter, guint *out_fbo_id, guint *out_texture_id) { GLuint fbo_id = 0; GLint texture_id; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (width > 0); g_assert (height > 0); g_assert (out_fbo_id != NULL); g_assert (out_texture_id != NULL); texture_id = gsk_gl_command_queue_create_texture (self, width, height, format, min_filter, mag_filter); if (texture_id == -1) { *out_fbo_id = 0; *out_texture_id = 0; return FALSE; } fbo_id = gsk_gl_command_queue_create_framebuffer (self); glBindFramebuffer (GL_FRAMEBUFFER, fbo_id); glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_id, 0); g_assert_cmphex (glCheckFramebufferStatus (GL_FRAMEBUFFER), ==, GL_FRAMEBUFFER_COMPLETE); *out_fbo_id = fbo_id; *out_texture_id = texture_id; return TRUE; } int gsk_gl_command_queue_create_texture (GskGLCommandQueue *self, int width, int height, int format, int min_filter, int mag_filter) { GLuint texture_id = 0; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); if G_UNLIKELY (self->max_texture_size == -1) glGetIntegerv (GL_MAX_TEXTURE_SIZE, &self->max_texture_size); if (width > self->max_texture_size || height > self->max_texture_size) return -1; glGenTextures (1, &texture_id); glActiveTexture (GL_TEXTURE0); glBindTexture (GL_TEXTURE_2D, texture_id); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); switch (format) { case GL_RGBA8: glTexImage2D (GL_TEXTURE_2D, 0, format, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); break; case GL_RGBA16F: glTexImage2D (GL_TEXTURE_2D, 0, format, width, height, 0, GL_RGBA, GL_HALF_FLOAT, NULL); break; case GL_RGBA32F: glTexImage2D (GL_TEXTURE_2D, 0, format, width, height, 0, GL_RGBA, GL_FLOAT, NULL); break; default: /* If you add new formats, make sure to set the correct format and type here * so that GLES doesn't barf invalid operations at you. * Because it is very important that these 3 values match when data is set to * NULL, do you hear me? */ g_assert_not_reached (); break; } /* Restore the previous texture if it was set */ if (self->attachments->textures[0].id != 0) glBindTexture (GL_TEXTURE_2D, self->attachments->textures[0].id); return (int)texture_id; } guint gsk_gl_command_queue_create_framebuffer (GskGLCommandQueue *self) { GLuint fbo_id; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); glGenFramebuffers (1, &fbo_id); return fbo_id; } static void gsk_gl_command_queue_do_upload_texture (GskGLCommandQueue *self, GdkTexture *texture) { GdkGLContext *context; const guchar *data; gsize stride; GdkMemoryTexture *memtex; GdkMemoryFormat data_format; int width, height; GLenum gl_internalformat; GLenum gl_format; GLenum gl_type; gsize bpp; gboolean use_es; context = gdk_gl_context_get_current (); use_es = gdk_gl_context_get_use_es (context); data_format = gdk_texture_get_format (texture); width = gdk_texture_get_width (texture); height = gdk_texture_get_height (texture); if (!gdk_memory_format_gl_format (data_format, use_es, &gl_internalformat, &gl_format, &gl_type)) { if (gdk_memory_format_prefers_high_depth (data_format)) data_format = GDK_MEMORY_R32G32B32A32_FLOAT_PREMULTIPLIED; else data_format = GDK_MEMORY_R8G8B8A8_PREMULTIPLIED; if (!gdk_memory_format_gl_format (data_format, use_es, &gl_internalformat, &gl_format, &gl_type)) { g_assert_not_reached (); } } memtex = gdk_memory_texture_from_texture (texture, data_format); data = gdk_memory_texture_get_data (memtex); stride = gdk_memory_texture_get_stride (memtex); bpp = gdk_memory_format_bytes_per_pixel (data_format); glPixelStorei (GL_UNPACK_ALIGNMENT, gdk_memory_format_alignment (data_format)); /* GL_UNPACK_ROW_LENGTH is available on desktop GL, OpenGL ES >= 3.0, or if * the GL_EXT_unpack_subimage extension for OpenGL ES 2.0 is available */ if (stride == width * bpp) { glTexImage2D (GL_TEXTURE_2D, 0, gl_internalformat, width, height, 0, gl_format, gl_type, data); } else if (stride % bpp == 0 && (gdk_gl_context_check_version (context, 0, 0, 3, 0) || gdk_gl_context_has_unpack_subimage (context))) { glPixelStorei (GL_UNPACK_ROW_LENGTH, stride / bpp); glTexImage2D (GL_TEXTURE_2D, 0, gl_internalformat, width, height, 0, gl_format, gl_type, data); glPixelStorei (GL_UNPACK_ROW_LENGTH, 0); } else { int i; glTexImage2D (GL_TEXTURE_2D, 0, gl_internalformat, width, height, 0, gl_format, gl_type, NULL); for (i = 0; i < height; i++) glTexSubImage2D (GL_TEXTURE_2D, 0, 0, i, width, 1, gl_format, gl_type, data + (i * stride)); } glPixelStorei (GL_UNPACK_ALIGNMENT, 4); g_object_unref (memtex); } int gsk_gl_command_queue_upload_texture (GskGLCommandQueue *self, GdkTexture *texture, int min_filter, int mag_filter) { G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME; cairo_surface_t *surface = NULL; int width, height; int texture_id; g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (!GDK_IS_GL_TEXTURE (texture)); g_assert (mag_filter == GL_LINEAR || mag_filter == GL_NEAREST); width = gdk_texture_get_width (texture); height = gdk_texture_get_height (texture); if (width > self->max_texture_size || height > self->max_texture_size) { g_warning ("Attempt to create texture of size %ux%u but max size is %d. " "Clipping will occur.", width, height, self->max_texture_size); width = MAX (width, self->max_texture_size); height = MAX (height, self->max_texture_size); } texture_id = gsk_gl_command_queue_create_texture (self, width, height, GL_RGBA8, min_filter, mag_filter); if (texture_id == -1) return texture_id; self->n_uploads++; /* Switch to texture0 as 2D. We'll restore it later. */ glActiveTexture (GL_TEXTURE0); glBindTexture (GL_TEXTURE_2D, texture_id); gsk_gl_command_queue_do_upload_texture (self, texture); if (min_filter == GL_LINEAR_MIPMAP_LINEAR) glGenerateMipmap (GL_TEXTURE_2D); /* Restore previous texture state if any */ if (self->attachments->textures[0].id > 0) glBindTexture (self->attachments->textures[0].target, self->attachments->textures[0].id); g_clear_pointer (&surface, cairo_surface_destroy); if (gdk_profiler_is_running ()) gdk_profiler_add_markf (start_time, GDK_PROFILER_CURRENT_TIME-start_time, "Upload Texture", "Size %dx%d", width, height); return texture_id; } void gsk_gl_command_queue_set_profiler (GskGLCommandQueue *self, GskProfiler *profiler) { #ifdef G_ENABLE_DEBUG g_assert (GSK_IS_GL_COMMAND_QUEUE (self)); g_assert (GSK_IS_PROFILER (profiler)); if (g_set_object (&self->profiler, profiler)) { self->gl_profiler = gsk_gl_profiler_new (self->context); self->metrics.n_frames = gsk_profiler_add_counter (profiler, "frames", "Frames", FALSE); self->metrics.cpu_time = gsk_profiler_add_timer (profiler, "cpu-time", "CPU Time", FALSE, TRUE); self->metrics.gpu_time = gsk_profiler_add_timer (profiler, "gpu-time", "GPU Time", FALSE, TRUE); self->metrics.n_binds = gdk_profiler_define_int_counter ("attachments", "Number of texture attachments"); self->metrics.n_fbos = gdk_profiler_define_int_counter ("fbos", "Number of framebuffers attached"); self->metrics.n_uniforms = gdk_profiler_define_int_counter ("uniforms", "Number of uniforms changed"); self->metrics.n_uploads = gdk_profiler_define_int_counter ("uploads", "Number of texture uploads"); self->metrics.n_programs = gdk_profiler_define_int_counter ("programs", "Number of program changes"); self->metrics.queue_depth = gdk_profiler_define_int_counter ("gl-queue-depth", "Depth of GL command batches"); } #endif }