gtk/gsk/gl/gskglcommandqueue.c
Christian Hergert f9268e8137 gsk/gl: support non-standard default framebuffer
There are situations where our "default framebuffer" is not actually
zero, yet we still want to apply a scissor rect.

Generally, 0 is the default framebuffer. But on platforms where we need
to bind a platform-specific feature to a GL_FRAMEBUFFER, we might have a
default that is not 0. For example, on macOS we bind an IOSurfaceRef to
a GL_TEXTURE_RECTANGLE which then is assigned as the backing store for a
framebuffer. This is different than using gsk_gl_renderer_render_texture()
in that we don't want to incur an extra copy to the destination surface
nor do we even have a way to pass a texture_id into render_texture().
2022-02-22 12:01:24 -08:00

1507 lines
46 KiB
C

/* gskglcommandqueue.c
*
* Copyright 2017 Timm Bäder <mail@baedert.org>
* Copyright 2018 Matthias Clasen <mclasen@redhat.com>
* Copyright 2018 Alexander Larsson <alexl@redhat.com>
* Copyright 2020 Christian Hergert <chergert@redhat.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "config.h"
#include <string.h>
#include <gdk/gdkglcontextprivate.h>
#include <gdk/gdkmemoryformatprivate.h>
#include <gdk/gdkmemorytextureprivate.h>
#include <gdk/gdkprofilerprivate.h>
#include <gsk/gskdebugprivate.h>
#include <gsk/gskroundedrectprivate.h>
#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->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))
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;
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 (min_filter == GL_LINEAR || min_filter == GL_NEAREST);
g_assert (mag_filter == GL_LINEAR || min_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);
/* 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
}