diff --git a/demos/node-editor/node-editor-window.c b/demos/node-editor/node-editor-window.c index 0335a28c46..03da8c67bd 100644 --- a/demos/node-editor/node-editor-window.c +++ b/demos/node-editor/node-editor-window.c @@ -25,6 +25,7 @@ #include "gsk/gskrendernodeparserprivate.h" #include "gsk/gl/gskglrenderer.h" +#include "gsk/ngl/gsknglrenderer.h" #ifdef GDK_WINDOWING_BROADWAY #include "gsk/broadway/gskbroadwayrenderer.h" #endif @@ -762,6 +763,9 @@ node_editor_window_realize (GtkWidget *widget) node_editor_window_add_renderer (self, gsk_gl_renderer_new (), "OpenGL"); + node_editor_window_add_renderer (self, + gsk_ngl_renderer_new (), + "NGL"); #ifdef GDK_RENDERING_VULKAN node_editor_window_add_renderer (self, gsk_vulkan_renderer_new (), diff --git a/gsk/gskglshader.c b/gsk/gskglshader.c index 181ae32a2d..081e8ac4a7 100644 --- a/gsk/gskglshader.c +++ b/gsk/gskglshader.c @@ -139,7 +139,9 @@ #include "gskglshader.h" #include "gskglshaderprivate.h" #include "gskdebugprivate.h" + #include "gl/gskglrendererprivate.h" +#include "ngl/gsknglrendererprivate.h" static GskGLUniformType uniform_type_from_glsl (const char *str) @@ -542,8 +544,9 @@ gsk_gl_shader_compile (GskGLShader *shader, g_return_val_if_fail (GSK_IS_GL_SHADER (shader), FALSE); if (GSK_IS_GL_RENDERER (renderer)) - return gsk_gl_renderer_try_compile_gl_shader (GSK_GL_RENDERER (renderer), - shader, error); + return gsk_gl_renderer_try_compile_gl_shader (GSK_GL_RENDERER (renderer), shader, error); + else if (GSK_IS_NGL_RENDERER (renderer)) + return gsk_ngl_renderer_try_compile_gl_shader (GSK_NGL_RENDERER (renderer), shader, error); g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "The renderer does not support gl shaders"); diff --git a/gsk/gskrenderer.c b/gsk/gskrenderer.c index dbbfd28f35..9d5dbfcb86 100644 --- a/gsk/gskrenderer.c +++ b/gsk/gskrenderer.c @@ -39,6 +39,7 @@ #include "gskcairorenderer.h" #include "gskdebugprivate.h" #include "gl/gskglrenderer.h" +#include "ngl/gsknglrenderer.h" #include "gskprofilerprivate.h" #include "gskrendernodeprivate.h" @@ -496,6 +497,8 @@ get_renderer_for_name (const char *renderer_name) else if (g_ascii_strcasecmp (renderer_name, "opengl") == 0 || g_ascii_strcasecmp (renderer_name, "gl") == 0) return GSK_TYPE_GL_RENDERER; + else if (g_ascii_strcasecmp (renderer_name, "ngl") == 0) + return GSK_TYPE_NGL_RENDERER; #ifdef GDK_RENDERING_VULKAN else if (g_ascii_strcasecmp (renderer_name, "vulkan") == 0) return GSK_TYPE_VULKAN_RENDERER; @@ -511,6 +514,7 @@ get_renderer_for_name (const char *renderer_name) g_print (" cairo - Use the Cairo fallback renderer\n"); g_print (" opengl - Use the default OpenGL renderer\n"); g_print (" gl - Same as opengl\n"); + g_print (" next - Another OpenGL renderer\n"); #ifdef GDK_RENDERING_VULKAN g_print (" vulkan - Use the Vulkan renderer\n"); #else diff --git a/gsk/meson.build b/gsk/meson.build index 748f4738a1..dd1ac34ff9 100644 --- a/gsk/meson.build +++ b/gsk/meson.build @@ -31,6 +31,7 @@ gsk_public_sources = files([ 'gskroundedrect.c', 'gsktransform.c', 'gl/gskglrenderer.c', + 'ngl/gsknglrenderer.c', ]) gsk_private_sources = files([ @@ -48,6 +49,19 @@ gsk_private_sources = files([ 'gl/gskgliconcache.c', 'gl/opbuffer.c', 'gl/stb_rect_pack.c', + 'ngl/gsknglattachmentstate.c', + 'ngl/gsknglbuffer.c', + 'ngl/gsknglcommandqueue.c', + 'ngl/gsknglcompiler.c', + 'ngl/gskngldriver.c', + 'ngl/gsknglglyphlibrary.c', + 'ngl/gskngliconlibrary.c', + 'ngl/gsknglprogram.c', + 'ngl/gsknglrenderjob.c', + 'ngl/gsknglshadowlibrary.c', + 'ngl/gskngltexturelibrary.c', + 'ngl/gskngluniformstate.c', + 'ngl/gskngltexturepool.c', ]) gsk_public_headers = files([ @@ -64,7 +78,8 @@ gsk_public_headers = files([ install_headers(gsk_public_headers, 'gsk.h', subdir: 'gtk-4.0/gsk') gsk_public_gl_headers = files([ - 'gl/gskglrenderer.h' + 'gl/gskglrenderer.h', + 'ngl/gsknglrenderer.h', ]) install_headers(gsk_public_gl_headers, subdir: 'gtk-4.0/gsk/gl') gsk_public_headers += gsk_public_gl_headers diff --git a/gsk/ngl/gsknglattachmentstate.c b/gsk/ngl/gsknglattachmentstate.c new file mode 100644 index 0000000000..bf37087a69 --- /dev/null +++ b/gsk/ngl/gsknglattachmentstate.c @@ -0,0 +1,106 @@ +/* gsknglattachmentstate.c + * + * Copyright 2020 Christian Hergert + * + * This file 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 file 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 General Public License along + * with this program. If not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "config.h" + +#include "gsknglattachmentstateprivate.h" + +GskNglAttachmentState * +gsk_ngl_attachment_state_new (void) +{ + GskNglAttachmentState *self; + + self = g_atomic_rc_box_new0 (GskNglAttachmentState); + + self->fbo.changed = FALSE; + self->fbo.id = 0; + self->n_changed = 0; + + /* Initialize textures, assume we are 2D by default since it + * doesn't really matter until we bind something other than + * GL_TEXTURE0 to it anyway. + */ + for (guint i = 0; i < G_N_ELEMENTS (self->textures); i++) + { + self->textures[i].target = GL_TEXTURE_2D; + self->textures[i].texture = GL_TEXTURE0; + self->textures[i].id = 0; + self->textures[i].changed = FALSE; + self->textures[i].initial = TRUE; + } + + return self; +} + +GskNglAttachmentState * +gsk_ngl_attachment_state_ref (GskNglAttachmentState *self) +{ + return g_atomic_rc_box_acquire (self); +} + +void +gsk_ngl_attachment_state_unref (GskNglAttachmentState *self) +{ + g_atomic_rc_box_release (self); +} + +void +gsk_ngl_attachment_state_bind_texture (GskNglAttachmentState *self, + GLenum target, + GLenum texture, + guint id) +{ + GskNglBindTexture *attach; + + g_assert (self != NULL); + g_assert (target == GL_TEXTURE_1D || + target == GL_TEXTURE_2D || + target == GL_TEXTURE_3D); + g_assert (texture >= GL_TEXTURE0 && texture <= GL_TEXTURE16); + + attach = &self->textures[texture - GL_TEXTURE0]; + + if (attach->target != target || attach->texture != texture || attach->id != id) + { + attach->target = target; + attach->texture = texture; + attach->id = id; + attach->initial = FALSE; + + if (attach->changed == FALSE) + { + attach->changed = TRUE; + self->n_changed++; + } + } +} + +void +gsk_ngl_attachment_state_bind_framebuffer (GskNglAttachmentState *self, + guint id) +{ + g_assert (self != NULL); + + if (self->fbo.id != id) + { + self->fbo.id = id; + self->fbo.changed = TRUE; + } +} diff --git a/gsk/ngl/gsknglattachmentstateprivate.h b/gsk/ngl/gsknglattachmentstateprivate.h new file mode 100644 index 0000000000..b43e91e09c --- /dev/null +++ b/gsk/ngl/gsknglattachmentstateprivate.h @@ -0,0 +1,71 @@ +/* gsknglattachmentstateprivate.h + * + * Copyright 2020 Christian Hergert + * + * This file 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 file 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 General Public License along + * with this program. If not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef __GSK_NGL_ATTACHMENT_STATE_PRIVATE_H__ +#define __GSK_NGL_ATTACHMENT_STATE_PRIVATE_H__ + +#include "gskngltypesprivate.h" + +G_BEGIN_DECLS + +typedef struct _GskNglAttachmentState GskNglAttachmentState; +typedef struct _GskNglBindFramebuffer GskNglBindFramebuffer; +typedef struct _GskNglBindTexture GskNglBindTexture; + +struct _GskNglBindTexture +{ + guint changed : 1; + guint initial : 1; + GLenum target : 30; + GLenum texture; + guint id; +}; + +G_STATIC_ASSERT (sizeof (GskNglBindTexture) == 12); + +struct _GskNglBindFramebuffer +{ + guint changed : 1; + guint id : 31; +}; + +G_STATIC_ASSERT (sizeof (GskNglBindFramebuffer) == 4); + +struct _GskNglAttachmentState +{ + GskNglBindFramebuffer fbo; + /* Increase if shaders add more textures */ + GskNglBindTexture textures[4]; + guint n_changed; +}; + +GskNglAttachmentState *gsk_ngl_attachment_state_new (void); +GskNglAttachmentState *gsk_ngl_attachment_state_ref (GskNglAttachmentState *self); +void gsk_ngl_attachment_state_unref (GskNglAttachmentState *self); +void gsk_ngl_attachment_state_bind_texture (GskNglAttachmentState *self, + GLenum target, + GLenum texture, + guint id); +void gsk_ngl_attachment_state_bind_framebuffer (GskNglAttachmentState *self, + guint id); + +G_END_DECLS + +#endif /* __GSK_NGL_ATTACHMENT_STATE_PRIVATE_H__ */ diff --git a/gsk/ngl/gsknglbuffer.c b/gsk/ngl/gsknglbuffer.c new file mode 100644 index 0000000000..f65923d003 --- /dev/null +++ b/gsk/ngl/gsknglbuffer.c @@ -0,0 +1,69 @@ +/* gsknglbufferprivate.h + * + * Copyright 2020 Christian Hergert + * + * This file 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 file 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 General Public License along + * with this program. If not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "config.h" + +#include + +#include "gsknglbufferprivate.h" + +/** + * gsk_ngl_buffer_init: + * @target: the target buffer such as %GL_ARRAY_BUFFER or %GL_UNIFORM_BUFFER + * @element_size: the size of elements within the buffer + * + * Creates a new #GskNglBuffer which can be used to deliver data to shaders + * within a GLSL program. You can use this to store vertices such as with + * %GL_ARRAY_BUFFER or uniform data with %GL_UNIFORM_BUFFER. + */ +void +gsk_ngl_buffer_init (GskNglBuffer *self, + GLenum target, + guint element_size) +{ + memset (self, 0, sizeof *self); + + /* Default to 2 pages, power-of-two growth from there */ + self->buffer_len = 4096 * 2; + self->buffer = g_malloc (self->buffer_len); + self->target = target; + self->element_size = element_size; +} + +GLuint +gsk_ngl_buffer_submit (GskNglBuffer *buffer) +{ + GLuint id; + + glGenBuffers (1, &id); + glBindBuffer (buffer->target, id); + glBufferData (buffer->target, buffer->buffer_pos, buffer->buffer, GL_STATIC_DRAW); + + buffer->buffer_pos = 0; + buffer->count = 0; + + return id; +} + +void +gsk_ngl_buffer_destroy (GskNglBuffer *buffer) +{ + g_clear_pointer (&buffer->buffer, g_free); +} diff --git a/gsk/ngl/gsknglbufferprivate.h b/gsk/ngl/gsknglbufferprivate.h new file mode 100644 index 0000000000..fc67bc9e0b --- /dev/null +++ b/gsk/ngl/gsknglbufferprivate.h @@ -0,0 +1,81 @@ +/* gsknglbufferprivate.h + * + * Copyright 2020 Christian Hergert + * + * This file 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 file 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 General Public License along + * with this program. If not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef __GSK_NGL_BUFFER_PRIVATE_H__ +#define __GSK_NGL_BUFFER_PRIVATE_H__ + +#include "gskngltypesprivate.h" + +G_BEGIN_DECLS + +typedef struct _GskNglBuffer +{ + guint8 *buffer; + gsize buffer_pos; + gsize buffer_len; + guint count; + GLenum target; + guint element_size; +} GskNglBuffer; + +void gsk_ngl_buffer_init (GskNglBuffer *self, + GLenum target, + guint element_size); +void gsk_ngl_buffer_destroy (GskNglBuffer *buffer); +GLuint gsk_ngl_buffer_submit (GskNglBuffer *buffer); + +static inline gpointer +gsk_ngl_buffer_advance (GskNglBuffer *buffer, + guint count) +{ + gpointer ret; + gsize to_alloc = count * buffer->element_size; + + if G_UNLIKELY (buffer->buffer_pos + to_alloc > buffer->buffer_len) + { + buffer->buffer_len *= 2; + buffer->buffer = g_realloc (buffer->buffer, buffer->buffer_len); + } + + ret = buffer->buffer + buffer->buffer_pos; + + buffer->buffer_pos += to_alloc; + buffer->count += count; + + return ret; +} + +static inline void +gsk_ngl_buffer_retract (GskNglBuffer *buffer, + guint count) +{ + buffer->buffer_pos -= count * buffer->element_size; + buffer->count -= count; +} + +static inline guint +gsk_ngl_buffer_get_offset (GskNglBuffer *buffer) +{ + return buffer->count; +} + +G_END_DECLS + +#endif /* __GSK_NGL_BUFFER_PRIVATE_H__ */ diff --git a/gsk/ngl/gsknglcommandqueue.c b/gsk/ngl/gsknglcommandqueue.c new file mode 100644 index 0000000000..9426c199b1 --- /dev/null +++ b/gsk/ngl/gsknglcommandqueue.c @@ -0,0 +1,1507 @@ +/* gsknglcommandqueue.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 "gsknglattachmentstateprivate.h" +#include "gsknglbufferprivate.h" +#include "gsknglcommandqueueprivate.h" +#include "gskngluniformstateprivate.h" + +#include "inlinearray.h" + +G_DEFINE_TYPE (GskNglCommandQueue, gsk_ngl_command_queue, G_TYPE_OBJECT) + +G_GNUC_UNUSED static inline void +print_uniform (GskNglUniformFormat 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_NGL_UNIFORM_FORMAT_1F: + g_printerr ("1f<%f>", data->fval[0]); + break; + + case GSK_NGL_UNIFORM_FORMAT_2F: + g_printerr ("2f<%f,%f>", data->fval[0], data->fval[1]); + break; + + case GSK_NGL_UNIFORM_FORMAT_3F: + g_printerr ("3f<%f,%f,%f>", data->fval[0], data->fval[1], data->fval[2]); + break; + + case GSK_NGL_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_NGL_UNIFORM_FORMAT_1I: + case GSK_NGL_UNIFORM_FORMAT_TEXTURE: + g_printerr ("1i<%d>", data->ival[0]); + break; + + case GSK_NGL_UNIFORM_FORMAT_1UI: + g_printerr ("1ui<%u>", data->uval[0]); + break; + + case GSK_NGL_UNIFORM_FORMAT_COLOR: { + char *str = gdk_rgba_to_string (valueptr); + g_printerr ("%s", str); + g_free (str); + break; + } + + case GSK_NGL_UNIFORM_FORMAT_ROUNDED_RECT: { + char *str = gsk_rounded_rect_to_string (valueptr); + g_printerr ("%s", str); + g_free (str); + break; + } + + case GSK_NGL_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_NGL_UNIFORM_FORMAT_1FV: + case GSK_NGL_UNIFORM_FORMAT_2FV: + case GSK_NGL_UNIFORM_FORMAT_3FV: + case GSK_NGL_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_ngl_uniform_format_size (format)); + } + g_printerr ("]"); + break; + + case GSK_NGL_UNIFORM_FORMAT_2I: + g_printerr ("2i<%d,%d>", data->ival[0], data->ival[1]); + break; + + case GSK_NGL_UNIFORM_FORMAT_3I: + g_printerr ("3i<%d,%d,%d>", data->ival[0], data->ival[1], data->ival[2]); + break; + + case GSK_NGL_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_NGL_UNIFORM_FORMAT_LAST: + default: + g_assert_not_reached (); + } +} + +G_GNUC_UNUSED static inline void +gsk_ngl_command_queue_print_batch (GskNglCommandQueue *self, + const GskNglCommandBatch *batch) +{ + static const char *command_kinds[] = { "Clear", NULL, NULL, "Draw", }; + guint framebuffer_id; + + g_assert (GSK_IS_NGL_COMMAND_QUEUE (self)); + g_assert (batch != NULL); + + if (batch->any.kind == GSK_NGL_COMMAND_KIND_CLEAR) + framebuffer_id = batch->clear.framebuffer; + else if (batch->any.kind == GSK_NGL_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_NGL_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 GskNglCommandBind *bind = &self->batch_binds.items[batch->draw.bind_offset + i]; + g_print (" Bind[%d]: %u\n", bind->texture, bind->id); + } + + for (guint i = 0; i < batch->draw.uniform_count; i++) + { + const GskNglCommandUniform *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_ngl_uniform_state_get_uniform_data (self->uniforms, uniform->info.offset)); + g_printerr ("\n"); + } + } + else if (batch->any.kind == GSK_NGL_COMMAND_KIND_CLEAR) + { + g_printerr (" Bits: 0x%x\n", batch->clear.bits); + } + + g_printerr ("}\n"); +} + +G_GNUC_UNUSED static inline void +gsk_ngl_command_queue_capture_png (GskNglCommandQueue *self, + const char *filename, + guint width, + guint height, + gboolean flip_y) +{ + cairo_surface_t *surface; + guint8 *data; + guint stride; + + g_assert (GSK_IS_NGL_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; + } + + surface = cairo_image_surface_create_for_data (data, CAIRO_FORMAT_ARGB32, width, height, stride); + cairo_surface_write_to_png (surface, filename); + + cairo_surface_destroy (surface); + g_free (data); +} + +static inline gboolean +will_ignore_batch (GskNglCommandQueue *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 GskNglAttachmentState *state, + GskNglCommandBinds *array) +{ + GskNglCommandBind *bind = gsk_ngl_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 (GskNglUniformState *state, + GskNglUniformProgram *program, + GskNglCommandUniforms *array) +{ + GskNglCommandUniform *uniform = gsk_ngl_command_uniforms_append_n (array, program->n_sparse); + guint count = 0; + + for (guint i = 0; i < program->n_sparse; i++) + { + guint location = program->sparse[i]; + const GskNglUniformInfo *info = &program->uniforms[location].info; + + if (!info->initial) + { + uniform[count].location = location; + uniform[count].info = *info; + count++; + } + } + + if (count != program->n_sparse) + array->len -= program->n_sparse - count; + + return count; +} + +static inline gboolean +snapshots_equal (GskNglCommandQueue *self, + GskNglCommandBatch *first, + GskNglCommandBatch *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 GskNglCommandBind *fb = &self->batch_binds.items[first->draw.bind_offset+i]; + const GskNglCommandBind *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 GskNglCommandUniform *fu = &self->batch_uniforms.items[first->draw.uniform_offset+i]; + const GskNglCommandUniform *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_ngl_uniform_state_get_uniform_data (self->uniforms, fu->info.offset); + sdata = gsk_ngl_uniform_state_get_uniform_data (self->uniforms, su->info.offset); + + switch (fu->info.format) + { + case GSK_NGL_UNIFORM_FORMAT_1F: + case GSK_NGL_UNIFORM_FORMAT_1FV: + case GSK_NGL_UNIFORM_FORMAT_1I: + case GSK_NGL_UNIFORM_FORMAT_TEXTURE: + case GSK_NGL_UNIFORM_FORMAT_1UI: + len = 4; + break; + + case GSK_NGL_UNIFORM_FORMAT_2F: + case GSK_NGL_UNIFORM_FORMAT_2FV: + case GSK_NGL_UNIFORM_FORMAT_2I: + len = 8; + break; + + case GSK_NGL_UNIFORM_FORMAT_3F: + case GSK_NGL_UNIFORM_FORMAT_3FV: + case GSK_NGL_UNIFORM_FORMAT_3I: + len = 12; + break; + + case GSK_NGL_UNIFORM_FORMAT_4F: + case GSK_NGL_UNIFORM_FORMAT_4FV: + case GSK_NGL_UNIFORM_FORMAT_4I: + len = 16; + break; + + + case GSK_NGL_UNIFORM_FORMAT_MATRIX: + len = sizeof (float) * 16; + break; + + case GSK_NGL_UNIFORM_FORMAT_ROUNDED_RECT: + len = sizeof (float) * 12; + break; + + case GSK_NGL_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_ngl_command_queue_dispose (GObject *object) +{ + GskNglCommandQueue *self = (GskNglCommandQueue *)object; + + g_assert (GSK_IS_NGL_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_ngl_attachment_state_unref); + g_clear_pointer (&self->uniforms, gsk_ngl_uniform_state_unref); + + gsk_ngl_command_batches_clear (&self->batches); + gsk_ngl_command_binds_clear (&self->batch_binds); + gsk_ngl_command_uniforms_clear (&self->batch_uniforms); + + gsk_ngl_buffer_destroy (&self->vertices); + + G_OBJECT_CLASS (gsk_ngl_command_queue_parent_class)->dispose (object); +} + +static void +gsk_ngl_command_queue_class_init (GskNglCommandQueueClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gsk_ngl_command_queue_dispose; +} + +static void +gsk_ngl_command_queue_init (GskNglCommandQueue *self) +{ + self->max_texture_size = -1; + + gsk_ngl_command_batches_init (&self->batches, 128); + gsk_ngl_command_binds_init (&self->batch_binds, 1024); + gsk_ngl_command_uniforms_init (&self->batch_uniforms, 2048); + + self->debug_groups = g_string_chunk_new (4096); + + gsk_ngl_buffer_init (&self->vertices, GL_ARRAY_BUFFER, sizeof (GskNglDrawVertex)); +} + +GskNglCommandQueue * +gsk_ngl_command_queue_new (GdkGLContext *context, + GskNglUniformState *uniforms) +{ + GskNglCommandQueue *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_ngl_attachment_state_new (); + + /* Use shared uniform state if we're provided one */ + if (uniforms != NULL) + self->uniforms = gsk_ngl_uniform_state_ref (uniforms); + else + self->uniforms = gsk_ngl_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 GskNglCommandBatch * +begin_next_batch (GskNglCommandQueue *self) +{ + GskNglCommandBatch *batch; + + g_assert (GSK_IS_NGL_COMMAND_QUEUE (self)); + + /* GskNglCommandBatch 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_ngl_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 (GskNglCommandQueue *self) +{ + guint index; + + g_assert (GSK_IS_NGL_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) + { + GskNglCommandBatch *prev = &self->batches.items[self->tail_batch_index]; + + prev->any.next_batch_index = index; + } + + self->tail_batch_index = index; +} + +static void +discard_batch (GskNglCommandQueue *self) +{ + g_assert (GSK_IS_NGL_COMMAND_QUEUE (self)); + g_assert (self->batches.len > 0); + + self->batches.len--; +} + +void +gsk_ngl_command_queue_begin_draw (GskNglCommandQueue *self, + GskNglUniformProgram *program, + guint width, + guint height) +{ + GskNglCommandBatch *batch; + + g_assert (GSK_IS_NGL_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_NGL_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_ngl_buffer_get_offset (&self->vertices); + + self->fbo_max = MAX (self->fbo_max, batch->draw.framebuffer); + + self->in_draw = TRUE; +} + +void +gsk_ngl_command_queue_end_draw (GskNglCommandQueue *self) +{ + GskNglCommandBatch *last_batch; + GskNglCommandBatch *batch; + + g_assert (GSK_IS_NGL_COMMAND_QUEUE (self)); + g_assert (self->batches.len > 0); + + if (will_ignore_batch (self)) + return; + + batch = gsk_ngl_command_batches_tail (&self->batches); + + g_assert (self->in_draw == TRUE); + g_assert (batch->any.kind == GSK_NGL_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_NGL_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 && + 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_ngl_command_queue_split_draw: + * @self a #GskNglCommandQueue + * + * This function is like calling gsk_ngl_command_queue_end_draw() followed by + * a gsk_ngl_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_ngl_program_split_draw() + * as that is where the begin/end flow happens from the render job. + */ +void +gsk_ngl_command_queue_split_draw (GskNglCommandQueue *self) +{ + GskNglCommandBatch *batch; + GskNglUniformProgram *program; + guint width; + guint height; + + g_assert (GSK_IS_NGL_COMMAND_QUEUE (self)); + g_assert (self->batches.len > 0); + g_assert (self->in_draw == TRUE); + + program = self->program_info; + + batch = gsk_ngl_command_batches_tail (&self->batches); + + g_assert (batch->any.kind == GSK_NGL_COMMAND_KIND_DRAW); + + width = batch->any.viewport.width; + height = batch->any.viewport.height; + + gsk_ngl_command_queue_end_draw (self); + gsk_ngl_command_queue_begin_draw (self, program, width, height); +} + +void +gsk_ngl_command_queue_clear (GskNglCommandQueue *self, + guint clear_bits, + const graphene_rect_t *viewport) +{ + GskNglCommandBatch *batch; + + g_assert (GSK_IS_NGL_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_NGL_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_ngl_command_queue_get_context (GskNglCommandQueue *self) +{ + g_return_val_if_fail (GSK_IS_NGL_COMMAND_QUEUE (self), NULL); + + return self->context; +} + +void +gsk_ngl_command_queue_make_current (GskNglCommandQueue *self) +{ + g_assert (GSK_IS_NGL_COMMAND_QUEUE (self)); + g_assert (GDK_IS_GL_CONTEXT (self->context)); + + gdk_gl_context_make_current (self->context); +} + +void +gsk_ngl_command_queue_delete_program (GskNglCommandQueue *self, + guint program) +{ + g_assert (GSK_IS_NGL_COMMAND_QUEUE (self)); + + glDeleteProgram (program); +} + +static inline void +apply_uniform (gconstpointer dataptr, + GskNglUniformInfo info, + guint location) +{ + g_assert (dataptr != NULL); + g_assert (info.format > 0); + g_assert (location < GL_MAX_UNIFORM_LOCATIONS); + + switch (info.format) + { + case GSK_NGL_UNIFORM_FORMAT_1F: + glUniform1fv (location, 1, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_2F: + glUniform2fv (location, 1, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_3F: + glUniform3fv (location, 1, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_4F: + glUniform4fv (location, 1, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_1FV: + glUniform1fv (location, info.array_count, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_2FV: + glUniform2fv (location, info.array_count, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_3FV: + glUniform3fv (location, info.array_count, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_4FV: + glUniform4fv (location, info.array_count, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_1I: + case GSK_NGL_UNIFORM_FORMAT_TEXTURE: + glUniform1iv (location, 1, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_2I: + glUniform2iv (location, 1, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_3I: + glUniform3iv (location, 1, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_4I: + glUniform4iv (location, 1, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_1UI: + glUniform1uiv (location, 1, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_MATRIX: { + float mat[16]; + graphene_matrix_to_float (dataptr, mat); + glUniformMatrix4fv (location, 1, GL_FALSE, mat); +#if 0 + /* TODO: If Graphene can give us a peek here on platforms + * where the format is float[16] (most/all x86_64?) then + * We can avoid the SIMD operation to convert the format. + */ + G_STATIC_ASSERT (sizeof (graphene_matrix_t) == 16*4); + glUniformMatrix4fv (location, 1, GL_FALSE, dataptr); +#endif + } + break; + + case GSK_NGL_UNIFORM_FORMAT_COLOR: + glUniform4fv (location, 1, dataptr); + break; + + case GSK_NGL_UNIFORM_FORMAT_ROUNDED_RECT: + glUniform4fv (location, 3, dataptr); + break; + + default: + g_assert_not_reached (); + } +} + +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) +{ + g_assert (framebuffer != (guint)-1); + + if (framebuffer != 0 || !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_ngl_command_queue_unlink (GskNglCommandQueue *self, + GskNglCommandBatch *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_ngl_command_queue_insert_before (GskNglCommandQueue *self, + GskNglCommandBatch *batch, + GskNglCommandBatch *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_ngl_command_batches_index_of (&self->batches, batch); + sibling_index = gsk_ngl_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_ngl_command_queue_sort_batches (GskNglCommandQueue *self) +{ + int *seen; + int *seen_free = NULL; + int index; + + g_assert (GSK_IS_NGL_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) + { + GskNglCommandBatch *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_NGL_COMMAND_KIND_DRAW: + fbo = batch->draw.framebuffer; + break; + + case GSK_NGL_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]; + GskNglCommandBatch *mru = &self->batches.items[mru_index]; + + g_assert (mru_index > -1); + + gsk_ngl_command_queue_unlink (self, batch); + + g_assert (batch->any.prev_batch_index == -1); + g_assert (batch->any.next_batch_index == -1); + + gsk_ngl_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_ngl_command_queue_execute: + * @self: a #GskNglCommandQueue + * @surface_height: the height of the backing surface + * @scale_factor: the scale factor of the backing surface + * #scissor: (nullable): the scissor clip if any + * + * Executes all of the batches in the command queue. + */ +void +gsk_ngl_command_queue_execute (GskNglCommandQueue *self, + guint surface_height, + guint scale_factor, + const cairo_region_t *scissor) +{ + 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; + guint n_binds = 0; + guint n_fbos = 0; + guint n_uniforms = 0; + guint vao_id; + guint vbo_id; + int textures[4]; + int framebuffer = -1; + int next_batch_index; + int active = -1; + + g_assert (GSK_IS_NGL_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_ngl_command_queue_sort_batches (self); + + gsk_ngl_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_ngl_buffer_submit (&self->vertices); + + /* 0 = position location */ + glEnableVertexAttribArray (0); + glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE, + sizeof (GskNglDrawVertex), + (void *) G_STRUCT_OFFSET (GskNglDrawVertex, position)); + + /* 1 = texture coord location */ + glEnableVertexAttribArray (1); + glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE, + sizeof (GskNglDrawVertex), + (void *) G_STRUCT_OFFSET (GskNglDrawVertex, uv)); + + /* 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 GskNglCommandBatch *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_NGL_COMMAND_KIND_CLEAR: + if (apply_framebuffer (&framebuffer, batch->clear.framebuffer)) + { + apply_scissor (&scissor_state, framebuffer, &scissor_test, has_scissor); + 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_NGL_COMMAND_KIND_DRAW: + if (batch->any.program != program) + { + program = batch->any.program; + glUseProgram (program); + } + + if (apply_framebuffer (&framebuffer, batch->draw.framebuffer)) + { + apply_scissor (&scissor_state, framebuffer, &scissor_test, has_scissor); + n_fbos++; + } + + apply_viewport (&width, + &height, + batch->any.viewport.width, + batch->any.viewport.height); + + if G_UNLIKELY (batch->draw.bind_count > 0) + { + const GskNglCommandBind *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 GskNglCommandUniform *u = &self->batch_uniforms.items[batch->draw.uniform_offset]; + + for (guint i = 0; i < batch->draw.uniform_count; i++, u++) + apply_uniform (gsk_ngl_uniform_state_get_uniform_data (self->uniforms, u->info.offset), + u->info, u->location); + + 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_NGL_COMMAND_KIND_DRAW || + batch->any.kind == GSK_NGL_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_NGL_COMMAND_KIND_DRAW ? batch->draw.uniform_count : 0, + batch->any.kind == GSK_NGL_COMMAND_KIND_DRAW ? batch->draw.bind_count : 0, + framebuffer, + gdk_gl_context_get_current ()); + gsk_ngl_command_queue_capture_png (self, filename, width, height, TRUE); + gsk_ngl_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_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_ngl_command_queue_begin_frame (GskNglCommandQueue *self) +{ + g_assert (GSK_IS_NGL_COMMAND_QUEUE (self)); + g_assert (self->batches.len == 0); + + gsk_ngl_command_queue_make_current (self); + + self->fbo_max = 0; + self->tail_batch_index = -1; + self->head_batch_index = -1; + self->in_frame = TRUE; +} + +/** + * gsk_ngl_command_queue_end_frame: + * @self: a #GskNglCommandQueue + * + * 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_ngl_command_queue_end_frame (GskNglCommandQueue *self) +{ + g_assert (GSK_IS_NGL_COMMAND_QUEUE (self)); + + gsk_ngl_command_queue_make_current (self); + gsk_ngl_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; + } + } + + g_string_chunk_clear (self->debug_groups); + + 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_ngl_command_queue_create_render_target (GskNglCommandQueue *self, + int width, + int height, + 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_NGL_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_ngl_command_queue_create_texture (self, + width, height, + min_filter, mag_filter); + + if (texture_id == -1) + { + *out_fbo_id = 0; + *out_texture_id = 0; + return FALSE; + } + + fbo_id = gsk_ngl_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_ngl_command_queue_create_texture (GskNglCommandQueue *self, + int width, + int height, + int min_filter, + int mag_filter) +{ + GLuint texture_id = 0; + + g_assert (GSK_IS_NGL_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); + + if (gdk_gl_context_get_use_es (self->context)) + glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + else + glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL); + + /* 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_ngl_command_queue_create_framebuffer (GskNglCommandQueue *self) +{ + GLuint fbo_id; + + g_assert (GSK_IS_NGL_COMMAND_QUEUE (self)); + + glGenFramebuffers (1, &fbo_id); + + return fbo_id; +} + +int +gsk_ngl_command_queue_upload_texture (GskNglCommandQueue *self, + GdkTexture *texture, + guint x_offset, + guint y_offset, + guint width, + guint height, + int min_filter, + int mag_filter) +{ + G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME; + cairo_surface_t *surface = NULL; + GdkMemoryFormat data_format; + const guchar *data; + gsize data_stride; + gsize bpp; + int texture_id; + + g_assert (GSK_IS_NGL_COMMAND_QUEUE (self)); + g_assert (!GDK_IS_GL_TEXTURE (texture)); + g_assert (x_offset + width <= gdk_texture_get_width (texture)); + g_assert (y_offset + height <= gdk_texture_get_height (texture)); + g_assert (min_filter == GL_LINEAR || min_filter == GL_NEAREST); + g_assert (mag_filter == GL_LINEAR || min_filter == GL_NEAREST); + + 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_ngl_command_queue_create_texture (self, width, height, min_filter, mag_filter); + if (texture_id == -1) + return texture_id; + + if (GDK_IS_MEMORY_TEXTURE (texture)) + { + GdkMemoryTexture *memory_texture = GDK_MEMORY_TEXTURE (texture); + data = gdk_memory_texture_get_data (memory_texture); + data_format = gdk_memory_texture_get_format (memory_texture); + data_stride = gdk_memory_texture_get_stride (memory_texture); + } + else + { + /* Fall back to downloading to a surface */ + surface = gdk_texture_download_surface (texture); + cairo_surface_flush (surface); + data = cairo_image_surface_get_data (surface); + data_format = GDK_MEMORY_DEFAULT; + data_stride = cairo_image_surface_get_stride (surface); + } + + self->n_uploads++; + + bpp = gdk_memory_format_bytes_per_pixel (data_format); + + /* Swtich to texture0 as 2D. We'll restore it later. */ + glActiveTexture (GL_TEXTURE0); + glBindTexture (GL_TEXTURE_2D, texture_id); + + gdk_gl_context_upload_texture (gdk_gl_context_get_current (), + data + x_offset * bpp + y_offset * data_stride, + width, height, data_stride, + data_format, 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_ngl_command_queue_set_profiler (GskNglCommandQueue *self, + GskProfiler *profiler) +{ +#ifdef G_ENABLE_DEBUG + g_assert (GSK_IS_NGL_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.queue_depth = gdk_profiler_define_int_counter ("gl-queue-depth", "Depth of GL command batches"); + } +#endif +} diff --git a/gsk/ngl/gsknglcommandqueueprivate.h b/gsk/ngl/gsknglcommandqueueprivate.h new file mode 100644 index 0000000000..9a35326cee --- /dev/null +++ b/gsk/ngl/gsknglcommandqueueprivate.h @@ -0,0 +1,362 @@ +/* gsknglcommandqueueprivate.h + * + * 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 + */ + +#ifndef __GSK_NGL_COMMAND_QUEUE_PRIVATE_H__ +#define __GSK_NGL_COMMAND_QUEUE_PRIVATE_H__ + +#include + +#include "gskngltypesprivate.h" +#include "gsknglbufferprivate.h" +#include "gsknglattachmentstateprivate.h" +#include "gskngluniformstateprivate.h" + +#include "inlinearray.h" + +#include "../gl/gskglprofilerprivate.h" + +G_BEGIN_DECLS + +#define GSK_TYPE_GL_COMMAND_QUEUE (gsk_ngl_command_queue_get_type()) + +G_DECLARE_FINAL_TYPE (GskNglCommandQueue, gsk_ngl_command_queue, GSK, NGL_COMMAND_QUEUE, GObject) + +typedef enum _GskNglCommandKind +{ + /* The batch will perform a glClear() */ + GSK_NGL_COMMAND_KIND_CLEAR, + + /* The batch will perform a glDrawArrays() */ + GSK_NGL_COMMAND_KIND_DRAW, +} GskNglCommandKind; + +typedef struct _GskNglCommandBind +{ + /* @texture is the value passed to glActiveTexture(), the "slot" the + * texture will be placed into. We always use GL_TEXTURE_2D so we don't + * waste any bits here to indicate that. + */ + guint texture : 5; + + /* The identifier for the texture created with glGenTextures(). */ + guint id : 27; +} GskNglCommandBind; + +G_STATIC_ASSERT (sizeof (GskNglCommandBind) == 4); + +typedef struct _GskNglCommandBatchAny +{ + /* A GskNglCommandKind indicating what the batch will do */ + guint kind : 8; + + /* The program's identifier to use for determining if we can merge two + * batches together into a single set of draw operations. We put this + * here instead of the GskNglCommandDraw so that we can use the extra + * bits here without making the structure larger. + */ + guint program : 24; + + /* The index of the next batch following this one. This is used + * as a sort of integer-based linked list to simplify out-of-order + * batching without moving memory around. -1 indicates last batch. + */ + gint16 next_batch_index; + + /* Same but for reverse direction as we sort in reverse to get the + * batches ordered by framebuffer. + */ + gint16 prev_batch_index; + + /* The viewport size of the batch. We check this as we process + * batches to determine if we need to resize the viewport. + */ + struct { + guint16 width; + guint16 height; + } viewport; +} GskNglCommandBatchAny; + +G_STATIC_ASSERT (sizeof (GskNglCommandBatchAny) == 12); + +typedef struct _GskNglCommandDraw +{ + GskNglCommandBatchAny head; + + /* There doesn't seem to be a limit on the framebuffer identifier that + * can be returned, so we have to use a whole unsigned for the framebuffer + * we are drawing to. When processing batches, we check to see if this + * changes and adjust the render target accordingly. Some sorting is + * performed to reduce the amount we change framebuffers. + */ + guint framebuffer; + + /* The number of uniforms to change. This must be less than or equal to + * GL_MAX_UNIFORM_LOCATIONS but only guaranteed up to 1024 by any OpenGL + * implementation to be conformant. + */ + guint uniform_count : 11; + + /* The number of textures to bind, which is only guaranteed up to 16 + * by the OpenGL specification to be conformant. + */ + guint bind_count : 5; + + /* GL_MAX_ELEMENTS_VERTICES specifies 33000 for this which requires 16-bit + * to address all possible counts <= GL_MAX_ELEMENTS_VERTICES. + */ + guint vbo_count : 16; + + /* The offset within the VBO containing @vbo_count vertices to send with + * glDrawArrays(). + */ + guint vbo_offset; + + /* The offset within the array of uniform changes to be made containing + * @uniform_count #GskNglCommandUniform elements to apply. + */ + guint uniform_offset; + + /* The offset within the array of bind changes to be made containing + * @bind_count #GskNglCommandBind elements to apply. + */ + guint bind_offset; +} GskNglCommandDraw; + +G_STATIC_ASSERT (sizeof (GskNglCommandDraw) == 32); + +typedef struct _GskNglCommandClear +{ + GskNglCommandBatchAny any; + guint bits; + guint framebuffer; +} GskNglCommandClear; + +G_STATIC_ASSERT (sizeof (GskNglCommandClear) == 20); + +typedef struct _GskNglCommandUniform +{ + GskNglUniformInfo info; + guint location; +} GskNglCommandUniform; + +G_STATIC_ASSERT (sizeof (GskNglCommandUniform) == 8); + +typedef union _GskNglCommandBatch +{ + GskNglCommandBatchAny any; + GskNglCommandDraw draw; + GskNglCommandClear clear; +} GskNglCommandBatch; + +G_STATIC_ASSERT (sizeof (GskNglCommandBatch) == 32); + +DEFINE_INLINE_ARRAY (GskNglCommandBatches, gsk_ngl_command_batches, GskNglCommandBatch) +DEFINE_INLINE_ARRAY (GskNglCommandBinds, gsk_ngl_command_binds, GskNglCommandBind) +DEFINE_INLINE_ARRAY (GskNglCommandUniforms, gsk_ngl_command_uniforms, GskNglCommandUniform) + +struct _GskNglCommandQueue +{ + GObject parent_instance; + + /* The GdkGLContext we make current before executing GL commands. */ + GdkGLContext *context; + + /* Array of GskNglCommandBatch which is a fixed size structure that will + * point into offsets of other arrays so that all similar data is stored + * together. The idea here is that we reduce the need for pointers so that + * using g_realloc()'d arrays is fine. + */ + GskNglCommandBatches batches; + + /* Contains array of vertices and some wrapper code to help upload them + * to the GL driver. We can also tweak this to use double buffered arrays + * if we find that to be faster on some hardware and/or drivers. + */ + GskNglBuffer vertices; + + /* The GskNglAttachmentState contains information about our FBO and texture + * attachments as we process incoming operations. We snapshot them into + * various batches so that we can compare differences between merge + * candidates. + */ + GskNglAttachmentState *attachments; + + /* The uniform state across all programs. We snapshot this into batches so + * that we can compare uniform state between batches to give us more + * chances at merging draw commands. + */ + GskNglUniformState *uniforms; + + /* Current program if we are in a draw so that we can send commands + * to the uniform state as needed. + */ + GskNglUniformProgram *program_info; + + /* The profiler instance to deliver timing/etc data */ + GskProfiler *profiler; + GskGLProfiler *gl_profiler; + + /* Array of GskNglCommandBind which denote what textures need to be attached + * to which slot. GskNglCommandDraw.bind_offset and bind_count reference this + * array to determine what to attach. + */ + GskNglCommandBinds batch_binds; + + /* Array of GskNglCommandUniform denoting which uniforms must be updated + * before the glDrawArrays() may be called. These are referenced from the + * GskNglCommandDraw.uniform_offset and uniform_count fields. + */ + GskNglCommandUniforms batch_uniforms; + + /* String storage for debug groups */ + GStringChunk *debug_groups; + + /* Discovered max texture size when loading the command queue so that we + * can either scale down or slice textures to fit within this size. Assumed + * to be both height and width. + */ + int max_texture_size; + + /* The index of the last batch in @batches, which may not be the element + * at the end of the array, as batches can be reordered. This is used to + * update the "next" index when adding a new batch. + */ + gint16 tail_batch_index; + gint16 head_batch_index; + + /* Max framebuffer we used, so we can sort items faster */ + guint fbo_max; + + /* Various GSK and GDK metric counter ids */ + struct { + GQuark n_frames; + GQuark cpu_time; + GQuark gpu_time; + guint n_binds; + guint n_fbos; + guint n_uniforms; + guint n_uploads; + guint queue_depth; + } metrics; + + /* Counter for uploads on the frame */ + guint n_uploads; + + /* If we're inside a begin/end_frame pair */ + guint in_frame : 1; + + /* If we're inside of a begin_draw()/end_draw() pair. */ + guint in_draw : 1; + + /* If we've warned about truncating batches */ + guint have_truncated : 1; +}; + +GskNglCommandQueue *gsk_ngl_command_queue_new (GdkGLContext *context, + GskNglUniformState *uniforms); +void gsk_ngl_command_queue_set_profiler (GskNglCommandQueue *self, + GskProfiler *profiler); +GdkGLContext *gsk_ngl_command_queue_get_context (GskNglCommandQueue *self); +void gsk_ngl_command_queue_make_current (GskNglCommandQueue *self); +void gsk_ngl_command_queue_begin_frame (GskNglCommandQueue *self); +void gsk_ngl_command_queue_end_frame (GskNglCommandQueue *self); +void gsk_ngl_command_queue_execute (GskNglCommandQueue *self, + guint surface_height, + guint scale_factor, + const cairo_region_t *scissor); +int gsk_ngl_command_queue_upload_texture (GskNglCommandQueue *self, + GdkTexture *texture, + guint x_offset, + guint y_offset, + guint width, + guint height, + int min_filter, + int mag_filter); +int gsk_ngl_command_queue_create_texture (GskNglCommandQueue *self, + int width, + int height, + int min_filter, + int mag_filter); +guint gsk_ngl_command_queue_create_framebuffer (GskNglCommandQueue *self); +gboolean gsk_ngl_command_queue_create_render_target (GskNglCommandQueue *self, + int width, + int height, + int min_filter, + int mag_filter, + guint *out_fbo_id, + guint *out_texture_id); +void gsk_ngl_command_queue_delete_program (GskNglCommandQueue *self, + guint program_id); +void gsk_ngl_command_queue_clear (GskNglCommandQueue *self, + guint clear_bits, + const graphene_rect_t *viewport); +void gsk_ngl_command_queue_begin_draw (GskNglCommandQueue *self, + GskNglUniformProgram *program_info, + guint width, + guint height); +void gsk_ngl_command_queue_end_draw (GskNglCommandQueue *self); +void gsk_ngl_command_queue_split_draw (GskNglCommandQueue *self); + +static inline GskNglCommandBatch * +gsk_ngl_command_queue_get_batch (GskNglCommandQueue *self) +{ + return gsk_ngl_command_batches_tail (&self->batches); +} + +static inline GskNglDrawVertex * +gsk_ngl_command_queue_add_vertices (GskNglCommandQueue *self) +{ + gsk_ngl_command_queue_get_batch (self)->draw.vbo_count += GSK_NGL_N_VERTICES; + return gsk_ngl_buffer_advance (&self->vertices, GSK_NGL_N_VERTICES); +} + +static inline GskNglDrawVertex * +gsk_ngl_command_queue_add_n_vertices (GskNglCommandQueue *self, + guint count) +{ + /* This is a batch form of gsk_ngl_command_queue_add_vertices(). Note that + * it does *not* add the count to .draw.vbo_count as the caller is responsible + * for that. + */ + return gsk_ngl_buffer_advance (&self->vertices, GSK_NGL_N_VERTICES * count); +} + +static inline void +gsk_ngl_command_queue_retract_n_vertices (GskNglCommandQueue *self, + guint count) +{ + /* Like gsk_ngl_command_queue_add_n_vertices(), this does not tweak + * the draw vbo_count. + */ + gsk_ngl_buffer_retract (&self->vertices, GSK_NGL_N_VERTICES * count); +} + +static inline guint +gsk_ngl_command_queue_bind_framebuffer (GskNglCommandQueue *self, + guint framebuffer) +{ + guint ret = self->attachments->fbo.id; + gsk_ngl_attachment_state_bind_framebuffer (self->attachments, framebuffer); + return ret; +} + +G_END_DECLS + +#endif /* __GSK_NGL_COMMAND_QUEUE_PRIVATE_H__ */ diff --git a/gsk/ngl/gsknglcompiler.c b/gsk/ngl/gsknglcompiler.c new file mode 100644 index 0000000000..c033775c48 --- /dev/null +++ b/gsk/ngl/gsknglcompiler.c @@ -0,0 +1,678 @@ +/* gsknglcompiler.c + * + * 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 "gsknglcommandqueueprivate.h" +#include "gsknglcompilerprivate.h" +#include "gsknglprogramprivate.h" + +#define SHADER_VERSION_GLES 100 +#define SHADER_VERSION_GL2_LEGACY 110 +#define SHADER_VERSION_GL3_LEGACY 130 +#define SHADER_VERSION_GL3 150 + +struct _GskNglCompiler +{ + GObject parent_instance; + + GskNglDriver *driver; + + GBytes *all_preamble; + GBytes *fragment_preamble; + GBytes *vertex_preamble; + GBytes *fragment_source; + GBytes *fragment_suffix; + GBytes *vertex_source; + GBytes *vertex_suffix; + + GArray *attrib_locations; + + int glsl_version; + + guint gl3 : 1; + guint gles : 1; + guint legacy : 1; + guint debug_shaders : 1; +}; + +typedef struct _GskNglProgramAttrib +{ + const char *name; + guint location; +} GskNglProgramAttrib; + +static GBytes *empty_bytes; + +G_DEFINE_TYPE (GskNglCompiler, gsk_ngl_compiler, G_TYPE_OBJECT) + +static void +gsk_ngl_compiler_finalize (GObject *object) +{ + GskNglCompiler *self = (GskNglCompiler *)object; + + g_clear_pointer (&self->all_preamble, g_bytes_unref); + g_clear_pointer (&self->fragment_preamble, g_bytes_unref); + g_clear_pointer (&self->vertex_preamble, g_bytes_unref); + g_clear_pointer (&self->vertex_suffix, g_bytes_unref); + g_clear_pointer (&self->fragment_source, g_bytes_unref); + g_clear_pointer (&self->fragment_suffix, g_bytes_unref); + g_clear_pointer (&self->vertex_source, g_bytes_unref); + g_clear_pointer (&self->attrib_locations, g_array_unref); + g_clear_object (&self->driver); + + G_OBJECT_CLASS (gsk_ngl_compiler_parent_class)->finalize (object); +} + +static void +gsk_ngl_compiler_class_init (GskNglCompilerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsk_ngl_compiler_finalize; + + empty_bytes = g_bytes_new (NULL, 0); +} + +static void +gsk_ngl_compiler_init (GskNglCompiler *self) +{ + self->glsl_version = 150; + self->attrib_locations = g_array_new (FALSE, FALSE, sizeof (GskNglProgramAttrib)); + self->all_preamble = g_bytes_ref (empty_bytes); + self->vertex_preamble = g_bytes_ref (empty_bytes); + self->fragment_preamble = g_bytes_ref (empty_bytes); + self->vertex_source = g_bytes_ref (empty_bytes); + self->vertex_suffix = g_bytes_ref (empty_bytes); + self->fragment_source = g_bytes_ref (empty_bytes); + self->fragment_suffix = g_bytes_ref (empty_bytes); +} + +GskNglCompiler * +gsk_ngl_compiler_new (GskNglDriver *driver, + gboolean debug_shaders) +{ + GskNglCompiler *self; + GdkGLContext *context; + + g_return_val_if_fail (GSK_IS_NGL_DRIVER (driver), NULL); + g_return_val_if_fail (driver->shared_command_queue != NULL, NULL); + + self = g_object_new (GSK_TYPE_GL_COMPILER, NULL); + self->driver = g_object_ref (driver); + self->debug_shaders = !!debug_shaders; + + context = gsk_ngl_command_queue_get_context (self->driver->shared_command_queue); + + if (gdk_gl_context_get_use_es (context)) + { + self->glsl_version = SHADER_VERSION_GLES; + self->gles = TRUE; + } + else if (gdk_gl_context_is_legacy (context)) + { + int maj, min; + + gdk_gl_context_get_version (context, &maj, &min); + + if (maj == 3) + self->glsl_version = SHADER_VERSION_GL3_LEGACY; + else + self->glsl_version = SHADER_VERSION_GL2_LEGACY; + + self->legacy = TRUE; + } + else + { + self->glsl_version = SHADER_VERSION_GL3; + self->gl3 = TRUE; + } + + gsk_ngl_command_queue_make_current (self->driver->shared_command_queue); + + return g_steal_pointer (&self); +} + +void +gsk_ngl_compiler_bind_attribute (GskNglCompiler *self, + const char *name, + guint location) +{ + GskNglProgramAttrib attrib; + + g_return_if_fail (GSK_IS_NGL_COMPILER (self)); + g_return_if_fail (name != NULL); + g_return_if_fail (location < 32); + + attrib.name = g_intern_string (name); + attrib.location = location; + + g_array_append_val (self->attrib_locations, attrib); +} + +void +gsk_ngl_compiler_clear_attributes (GskNglCompiler *self) +{ + g_return_if_fail (GSK_IS_NGL_COMPILER (self)); + + g_array_set_size (self->attrib_locations, 0); +} + +void +gsk_ngl_compiler_set_preamble (GskNglCompiler *self, + GskNglCompilerKind kind, + GBytes *preamble_bytes) +{ + GBytes **loc = NULL; + + g_return_if_fail (GSK_IS_NGL_COMPILER (self)); + g_return_if_fail (preamble_bytes != NULL); + + if (kind == GSK_NGL_COMPILER_ALL) + loc = &self->all_preamble; + else if (kind == GSK_NGL_COMPILER_FRAGMENT) + loc = &self->fragment_preamble; + else if (kind == GSK_NGL_COMPILER_VERTEX) + loc = &self->vertex_preamble; + else + g_return_if_reached (); + + g_assert (loc != NULL); + + if (*loc != preamble_bytes) + { + g_clear_pointer (loc, g_bytes_unref); + *loc = preamble_bytes ? g_bytes_ref (preamble_bytes) : NULL; + } +} + +void +gsk_ngl_compiler_set_preamble_from_resource (GskNglCompiler *self, + GskNglCompilerKind kind, + const char *resource_path) +{ + GError *error = NULL; + GBytes *bytes; + + g_return_if_fail (GSK_IS_NGL_COMPILER (self)); + g_return_if_fail (kind == GSK_NGL_COMPILER_ALL || + kind == GSK_NGL_COMPILER_VERTEX || + kind == GSK_NGL_COMPILER_FRAGMENT); + g_return_if_fail (resource_path != NULL); + + bytes = g_resources_lookup_data (resource_path, + G_RESOURCE_LOOKUP_FLAGS_NONE, + &error); + + if (bytes == NULL) + g_warning ("Cannot set shader from resource: %s", error->message); + else + gsk_ngl_compiler_set_preamble (self, kind, bytes); + + g_clear_pointer (&bytes, g_bytes_unref); + g_clear_error (&error); +} + +void +gsk_ngl_compiler_set_source (GskNglCompiler *self, + GskNglCompilerKind kind, + GBytes *source_bytes) +{ + GBytes **loc = NULL; + + g_return_if_fail (GSK_IS_NGL_COMPILER (self)); + g_return_if_fail (kind == GSK_NGL_COMPILER_ALL || + kind == GSK_NGL_COMPILER_VERTEX || + kind == GSK_NGL_COMPILER_FRAGMENT); + + if (source_bytes == NULL) + source_bytes = empty_bytes; + + /* If kind is ALL, then we need to split the fragment and + * vertex shaders from the bytes and assign them individually. + * This safely scans for FRAGMENT_SHADER and VERTEX_SHADER as + * specified within the GLSL resources. Some care is taken to + * use GBytes which reference the original bytes instead of + * copying them. + */ + if (kind == GSK_NGL_COMPILER_ALL) + { + gsize len = 0; + const char *source; + const char *vertex_shader_start; + const char *fragment_shader_start; + const char *endpos; + GBytes *fragment_bytes; + GBytes *vertex_bytes; + + g_clear_pointer (&self->fragment_source, g_bytes_unref); + g_clear_pointer (&self->vertex_source, g_bytes_unref); + + source = g_bytes_get_data (source_bytes, &len); + endpos = source + len; + vertex_shader_start = g_strstr_len (source, len, "VERTEX_SHADER"); + fragment_shader_start = g_strstr_len (source, len, "FRAGMENT_SHADER"); + + if (vertex_shader_start == NULL) + { + g_warning ("Failed to locate VERTEX_SHADER in shader source"); + return; + } + + if (fragment_shader_start == NULL) + { + g_warning ("Failed to locate FRAGMENT_SHADER in shader source"); + return; + } + + if (vertex_shader_start > fragment_shader_start) + { + g_warning ("VERTEX_SHADER must come before FRAGMENT_SHADER"); + return; + } + + /* Locate next newlines */ + while (vertex_shader_start < endpos && vertex_shader_start[0] != '\n') + vertex_shader_start++; + while (fragment_shader_start < endpos && fragment_shader_start[0] != '\n') + fragment_shader_start++; + + vertex_bytes = g_bytes_new_from_bytes (source_bytes, + vertex_shader_start - source, + fragment_shader_start - vertex_shader_start); + fragment_bytes = g_bytes_new_from_bytes (source_bytes, + fragment_shader_start - source, + endpos - fragment_shader_start); + + gsk_ngl_compiler_set_source (self, GSK_NGL_COMPILER_VERTEX, vertex_bytes); + gsk_ngl_compiler_set_source (self, GSK_NGL_COMPILER_FRAGMENT, fragment_bytes); + + g_bytes_unref (fragment_bytes); + g_bytes_unref (vertex_bytes); + + return; + } + + if (kind == GSK_NGL_COMPILER_FRAGMENT) + loc = &self->fragment_source; + else if (kind == GSK_NGL_COMPILER_VERTEX) + loc = &self->vertex_source; + else + g_return_if_reached (); + + if (*loc != source_bytes) + { + g_clear_pointer (loc, g_bytes_unref); + *loc = g_bytes_ref (source_bytes); + } +} + +void +gsk_ngl_compiler_set_source_from_resource (GskNglCompiler *self, + GskNglCompilerKind kind, + const char *resource_path) +{ + GError *error = NULL; + GBytes *bytes; + + g_return_if_fail (GSK_IS_NGL_COMPILER (self)); + g_return_if_fail (kind == GSK_NGL_COMPILER_ALL || + kind == GSK_NGL_COMPILER_VERTEX || + kind == GSK_NGL_COMPILER_FRAGMENT); + g_return_if_fail (resource_path != NULL); + + bytes = g_resources_lookup_data (resource_path, + G_RESOURCE_LOOKUP_FLAGS_NONE, + &error); + + if (bytes == NULL) + g_warning ("Cannot set shader from resource: %s", error->message); + else + gsk_ngl_compiler_set_source (self, kind, bytes); + + g_clear_pointer (&bytes, g_bytes_unref); + g_clear_error (&error); +} + +void +gsk_ngl_compiler_set_suffix (GskNglCompiler *self, + GskNglCompilerKind kind, + GBytes *suffix_bytes) +{ + GBytes **loc; + + g_return_if_fail (GSK_IS_NGL_COMPILER (self)); + g_return_if_fail (kind == GSK_NGL_COMPILER_VERTEX || + kind == GSK_NGL_COMPILER_FRAGMENT); + g_return_if_fail (suffix_bytes != NULL); + + if (suffix_bytes == NULL) + suffix_bytes = empty_bytes; + + if (kind == GSK_NGL_COMPILER_FRAGMENT) + loc = &self->fragment_suffix; + else if (kind == GSK_NGL_COMPILER_VERTEX) + loc = &self->vertex_suffix; + else + g_return_if_reached (); + + if (*loc != suffix_bytes) + { + g_clear_pointer (loc, g_bytes_unref); + *loc = g_bytes_ref (suffix_bytes); + } +} + +void +gsk_ngl_compiler_set_suffix_from_resource (GskNglCompiler *self, + GskNglCompilerKind kind, + const char *resource_path) +{ + GError *error = NULL; + GBytes *bytes; + + g_return_if_fail (GSK_IS_NGL_COMPILER (self)); + g_return_if_fail (kind == GSK_NGL_COMPILER_VERTEX || + kind == GSK_NGL_COMPILER_FRAGMENT); + g_return_if_fail (resource_path != NULL); + + bytes = g_resources_lookup_data (resource_path, + G_RESOURCE_LOOKUP_FLAGS_NONE, + &error); + + if (bytes == NULL) + g_warning ("Cannot set suffix from resource: %s", error->message); + else + gsk_ngl_compiler_set_suffix (self, kind, bytes); + + g_clear_pointer (&bytes, g_bytes_unref); + g_clear_error (&error); +} + +static void +prepend_line_numbers (char *code, + GString *s) +{ + char *p; + int line; + + p = code; + line = 1; + while (*p) + { + char *end = strchr (p, '\n'); + if (end) + end = end + 1; /* Include newline */ + else + end = p + strlen (p); + + g_string_append_printf (s, "%3d| ", line++); + g_string_append_len (s, p, end - p); + + p = end; + } +} + +static gboolean +check_shader_error (int shader_id, + GError **error) +{ + GLint status; + GLint log_len; + GLint code_len; + char *buffer; + char *code; + GString *s; + + glGetShaderiv (shader_id, GL_COMPILE_STATUS, &status); + + if G_LIKELY (status == GL_TRUE) + return TRUE; + + glGetShaderiv (shader_id, GL_INFO_LOG_LENGTH, &log_len); + buffer = g_malloc0 (log_len + 1); + glGetShaderInfoLog (shader_id, log_len, NULL, buffer); + + glGetShaderiv (shader_id, GL_SHADER_SOURCE_LENGTH, &code_len); + code = g_malloc0 (code_len + 1); + glGetShaderSource (shader_id, code_len, NULL, code); + + s = g_string_new (""); + prepend_line_numbers (code, s); + + g_set_error (error, + GDK_GL_ERROR, + GDK_GL_ERROR_COMPILATION_FAILED, + "Compilation failure in shader.\n" + "Source Code: %s\n" + "\n" + "Error Message:\n" + "%s\n" + "\n", + s->str, + buffer); + + g_string_free (s, TRUE); + g_free (buffer); + g_free (code); + + return FALSE; +} + +static void +print_shader_info (const char *prefix, + int shader_id, + const char *name) +{ + if (GSK_DEBUG_CHECK(SHADERS)) + { + int code_len; + + glGetShaderiv (shader_id, GL_SHADER_SOURCE_LENGTH, &code_len); + + if (code_len > 0) + { + char *code; + GString *s; + + code = g_malloc0 (code_len + 1); + glGetShaderSource (shader_id, code_len, NULL, code); + + s = g_string_new (NULL); + prepend_line_numbers (code, s); + + g_message ("%s %d, %s:\n%s", + prefix, shader_id, + name ? name : "unnamed", + s->str); + g_string_free (s, TRUE); + g_free (code); + } + } +} + +static const char * +get_shader_string (GBytes *bytes) +{ + /* 0 length bytes will give us NULL back */ + const char *str = g_bytes_get_data (bytes, NULL); + return str ? str : ""; +} + +GskNglProgram * +gsk_ngl_compiler_compile (GskNglCompiler *self, + const char *name, + GError **error) +{ + char version[32]; + const char *debug = ""; + const char *legacy = ""; + const char *gl3 = ""; + const char *gles = ""; + int program_id; + int vertex_id; + int fragment_id; + int status; + + g_return_val_if_fail (GSK_IS_NGL_COMPILER (self), NULL); + g_return_val_if_fail (self->all_preamble != NULL, NULL); + g_return_val_if_fail (self->fragment_preamble != NULL, NULL); + g_return_val_if_fail (self->vertex_preamble != NULL, NULL); + g_return_val_if_fail (self->fragment_source != NULL, NULL); + g_return_val_if_fail (self->vertex_source != NULL, NULL); + g_return_val_if_fail (self->driver != NULL, NULL); + + gsk_ngl_command_queue_make_current (self->driver->command_queue); + + g_snprintf (version, sizeof version, "#version %d\n", self->glsl_version); + + if (self->debug_shaders) + debug = "#define GSK_DEBUG 1\n"; + + if (self->legacy) + legacy = "#define GSK_LEGACY 1\n"; + + if (self->gles) + gles = "#define GSK_NGLES 1\n"; + + if (self->gl3) + gl3 = "#define GSK_NGL3 1\n"; + + vertex_id = glCreateShader (GL_VERTEX_SHADER); + glShaderSource (vertex_id, + 9, + (const char *[]) { + version, debug, legacy, gl3, gles, + get_shader_string (self->all_preamble), + get_shader_string (self->vertex_preamble), + get_shader_string (self->vertex_source), + get_shader_string (self->vertex_suffix), + }, + (int[]) { + strlen (version), + strlen (debug), + strlen (legacy), + strlen (gl3), + strlen (gles), + g_bytes_get_size (self->all_preamble), + g_bytes_get_size (self->vertex_preamble), + g_bytes_get_size (self->vertex_source), + g_bytes_get_size (self->vertex_suffix), + }); + glCompileShader (vertex_id); + + if (!check_shader_error (vertex_id, error)) + { + glDeleteShader (vertex_id); + return NULL; + } + + print_shader_info ("Vertex shader", vertex_id, name); + + fragment_id = glCreateShader (GL_FRAGMENT_SHADER); + glShaderSource (fragment_id, + 9, + (const char *[]) { + version, debug, legacy, gl3, gles, + get_shader_string (self->all_preamble), + get_shader_string (self->fragment_preamble), + get_shader_string (self->fragment_source), + get_shader_string (self->fragment_suffix), + }, + (int[]) { + strlen (version), + strlen (debug), + strlen (legacy), + strlen (gl3), + strlen (gles), + g_bytes_get_size (self->all_preamble), + g_bytes_get_size (self->fragment_preamble), + g_bytes_get_size (self->fragment_source), + g_bytes_get_size (self->fragment_suffix), + }); + glCompileShader (fragment_id); + + if (!check_shader_error (fragment_id, error)) + { + glDeleteShader (vertex_id); + glDeleteShader (fragment_id); + return NULL; + } + + print_shader_info ("Fragment shader", fragment_id, name); + + program_id = glCreateProgram (); + glAttachShader (program_id, vertex_id); + glAttachShader (program_id, fragment_id); + + for (guint i = 0; i < self->attrib_locations->len; i++) + { + const GskNglProgramAttrib *attrib; + + attrib = &g_array_index (self->attrib_locations, GskNglProgramAttrib, i); + glBindAttribLocation (program_id, attrib->location, attrib->name); + } + + glLinkProgram (program_id); + + glGetProgramiv (program_id, GL_LINK_STATUS, &status); + + glDetachShader (program_id, vertex_id); + glDeleteShader (vertex_id); + + glDetachShader (program_id, fragment_id); + glDeleteShader (fragment_id); + + if (status == GL_FALSE) + { + char *buffer = NULL; + int log_len = 0; + + glGetProgramiv (program_id, GL_INFO_LOG_LENGTH, &log_len); + + if (log_len > 0) + { + /* log_len includes NULL */ + buffer = g_malloc0 (log_len); + glGetProgramInfoLog (program_id, log_len, NULL, buffer); + } + + g_warning ("Linking failure in shader:\n%s", + buffer ? buffer : ""); + + g_set_error (error, + GDK_GL_ERROR, + GDK_GL_ERROR_LINK_FAILED, + "Linking failure in shader: %s", + buffer ? buffer : ""); + + g_free (buffer); + + glDeleteProgram (program_id); + + return NULL; + } + + return gsk_ngl_program_new (self->driver, name, program_id); +} diff --git a/gsk/ngl/gsknglcompilerprivate.h b/gsk/ngl/gsknglcompilerprivate.h new file mode 100644 index 0000000000..d61dce2697 --- /dev/null +++ b/gsk/ngl/gsknglcompilerprivate.h @@ -0,0 +1,69 @@ +/* gsknglcompilerprivate.h + * + * 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 + */ + +#ifndef __GSK_NGL_COMPILER_PRIVATE_H__ +#define __GSK_NGL_COMPILER_PRIVATE_H__ + +#include "gskngltypesprivate.h" + +G_BEGIN_DECLS + +typedef enum _GskNglCompilerKind +{ + GSK_NGL_COMPILER_ALL, + GSK_NGL_COMPILER_FRAGMENT, + GSK_NGL_COMPILER_VERTEX, +} GskNglCompilerKind; + +#define GSK_TYPE_GL_COMPILER (gsk_ngl_compiler_get_type()) + +G_DECLARE_FINAL_TYPE (GskNglCompiler, gsk_ngl_compiler, GSK, NGL_COMPILER, GObject) + +GskNglCompiler *gsk_ngl_compiler_new (GskNglDriver *driver, + gboolean debug); +void gsk_ngl_compiler_set_preamble (GskNglCompiler *self, + GskNglCompilerKind kind, + GBytes *preamble_bytes); +void gsk_ngl_compiler_set_preamble_from_resource (GskNglCompiler *self, + GskNglCompilerKind kind, + const char *resource_path); +void gsk_ngl_compiler_set_source (GskNglCompiler *self, + GskNglCompilerKind kind, + GBytes *source_bytes); +void gsk_ngl_compiler_set_source_from_resource (GskNglCompiler *self, + GskNglCompilerKind kind, + const char *resource_path); +void gsk_ngl_compiler_set_suffix (GskNglCompiler *self, + GskNglCompilerKind kind, + GBytes *suffix_bytes); +void gsk_ngl_compiler_set_suffix_from_resource (GskNglCompiler *self, + GskNglCompilerKind kind, + const char *resource_path); +void gsk_ngl_compiler_bind_attribute (GskNglCompiler *self, + const char *name, + guint location); +void gsk_ngl_compiler_clear_attributes (GskNglCompiler *self); +GskNglProgram *gsk_ngl_compiler_compile (GskNglCompiler *self, + const char *name, + GError **error); + +G_END_DECLS + +#endif /* __GSK_NGL_COMPILER_PRIVATE_H__ */ diff --git a/gsk/ngl/gskngldriver.c b/gsk/ngl/gskngldriver.c new file mode 100644 index 0000000000..a6720754be --- /dev/null +++ b/gsk/ngl/gskngldriver.c @@ -0,0 +1,1296 @@ +/* gskngldriver.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 "gsknglcommandqueueprivate.h" +#include "gsknglcompilerprivate.h" +#include "gskngldriverprivate.h" +#include "gsknglglyphlibraryprivate.h" +#include "gskngliconlibraryprivate.h" +#include "gsknglprogramprivate.h" +#include "gsknglshadowlibraryprivate.h" +#include "gskngltexturepoolprivate.h" + +#define ATLAS_SIZE 512 + +typedef struct _GskNglTextureState +{ + GdkGLContext *context; + GLuint texture_id; +} GskNglTextureState; + +G_DEFINE_TYPE (GskNglDriver, gsk_ngl_driver, G_TYPE_OBJECT) + +static guint +texture_key_hash (gconstpointer v) +{ + const GskTextureKey *k = (const GskTextureKey *)v; + + /* Optimize for 0..3 where 0 is the scaled out case. Usually + * we'll be squarely on 1 or 2 for standard vs HiDPI. When rendering + * to a texture scaled out like in node-editor, we might be < 1. + */ + guint scale_x = floorf (k->scale_x); + guint scale_y = floorf (k->scale_y); + + return GPOINTER_TO_SIZE (k->pointer) ^ + ((scale_x << 8) | + (scale_y << 6) | + (k->filter << 1) | + k->pointer_is_child); +} + +static gboolean +texture_key_equal (gconstpointer v1, + gconstpointer v2) +{ + const GskTextureKey *k1 = (const GskTextureKey *)v1; + const GskTextureKey *k2 = (const GskTextureKey *)v2; + + return k1->pointer == k2->pointer && + k1->scale_x == k2->scale_x && + k1->scale_y == k2->scale_y && + k1->filter == k2->filter && + k1->pointer_is_child == k2->pointer_is_child && + (!k1->pointer_is_child || memcmp (&k1->parent_rect, &k2->parent_rect, sizeof k1->parent_rect) == 0); +} + +static void +remove_texture_key_for_id (GskNglDriver *self, + guint texture_id) +{ + GskTextureKey *key; + + g_assert (GSK_IS_NGL_DRIVER (self)); + g_assert (texture_id > 0); + + /* g_hash_table_remove() will cause @key to be freed */ + if (g_hash_table_steal_extended (self->texture_id_to_key, + GUINT_TO_POINTER (texture_id), + NULL, + (gpointer *)&key)) + g_hash_table_remove (self->key_to_texture_id, key); +} + +static void +gsk_ngl_texture_destroyed (gpointer data) +{ + ((GskNglTexture *)data)->user = NULL; +} + +static guint +gsk_ngl_driver_collect_unused_textures (GskNglDriver *self, + gint64 watermark) +{ + GHashTableIter iter; + gpointer k, v; + guint old_size; + guint collected; + + g_assert (GSK_IS_NGL_DRIVER (self)); + + old_size = g_hash_table_size (self->textures); + + g_hash_table_iter_init (&iter, self->textures); + while (g_hash_table_iter_next (&iter, &k, &v)) + { + GskNglTexture *t = v; + + if (t->user || t->permanent) + continue; + + if (t->last_used_in_frame <= watermark) + { + g_hash_table_iter_steal (&iter); + + g_assert (t->link.prev == NULL); + g_assert (t->link.next == NULL); + g_assert (t->link.data == t); + + /* Steal this texture and put it back into the pool */ + remove_texture_key_for_id (self, t->texture_id); + gsk_ngl_texture_pool_put (&self->texture_pool, t); + } + } + + collected = old_size - g_hash_table_size (self->textures); + + return collected; +} + +static void +gsk_ngl_texture_atlas_free (GskNglTextureAtlas *atlas) +{ + if (atlas->texture_id != 0) + { + glDeleteTextures (1, &atlas->texture_id); + atlas->texture_id = 0; + } + + g_clear_pointer (&atlas->nodes, g_free); + g_slice_free (GskNglTextureAtlas, atlas); +} + +GskNglTextureAtlas * +gsk_ngl_driver_create_atlas (GskNglDriver *self) +{ + GskNglTextureAtlas *atlas; + + g_return_val_if_fail (GSK_IS_NGL_DRIVER (self), NULL); + + atlas = g_slice_new0 (GskNglTextureAtlas); + atlas->width = ATLAS_SIZE; + atlas->height = ATLAS_SIZE; + /* TODO: We might want to change the strategy about the amount of + * nodes here? stb_rect_pack.h says width is optimal. */ + atlas->nodes = g_malloc0_n (atlas->width, sizeof (struct stbrp_node)); + stbrp_init_target (&atlas->context, atlas->width, atlas->height, atlas->nodes, atlas->width); + atlas->texture_id = gsk_ngl_command_queue_create_texture (self->command_queue, + atlas->width, + atlas->height, + GL_LINEAR, + GL_LINEAR); + + gdk_gl_context_label_object_printf (gdk_gl_context_get_current (), + GL_TEXTURE, atlas->texture_id, + "Texture atlas %d", + atlas->texture_id); + + g_ptr_array_add (self->atlases, atlas); + + return atlas; +} + +static void +remove_program (gpointer data) +{ + GskNglProgram *program = data; + + g_assert (!program || GSK_IS_NGL_PROGRAM (program)); + + if (program != NULL) + { + gsk_ngl_program_delete (program); + g_object_unref (program); + } +} + +static void +gsk_ngl_driver_shader_weak_cb (gpointer data, + GObject *where_object_was) +{ + GskNglDriver *self = data; + + g_assert (GSK_IS_NGL_DRIVER (self)); + + if (self->shader_cache != NULL) + g_hash_table_remove (self->shader_cache, where_object_was); +} + +static void +gsk_ngl_driver_dispose (GObject *object) +{ + GskNglDriver *self = (GskNglDriver *)object; + + g_assert (GSK_IS_NGL_DRIVER (self)); + g_assert (self->in_frame == FALSE); + +#define GSK_NGL_NO_UNIFORMS +#define GSK_NGL_ADD_UNIFORM(pos, KEY, name) +#define GSK_NGL_DEFINE_PROGRAM(name, resource, uniforms) \ + G_STMT_START { \ + if (self->name) \ + gsk_ngl_program_delete (self->name); \ + g_clear_object (&self->name); \ + } G_STMT_END; +# include "gsknglprograms.defs" +#undef GSK_NGL_NO_UNIFORMS +#undef GSK_NGL_ADD_UNIFORM +#undef GSK_NGL_DEFINE_PROGRAM + + if (self->shader_cache != NULL) + { + GHashTableIter iter; + gpointer k, v; + + g_hash_table_iter_init (&iter, self->shader_cache); + while (g_hash_table_iter_next (&iter, &k, &v)) + { + GskGLShader *shader = k; + g_object_weak_unref (G_OBJECT (shader), + gsk_ngl_driver_shader_weak_cb, + self); + g_hash_table_iter_remove (&iter); + } + + g_clear_pointer (&self->shader_cache, g_hash_table_unref); + } + + if (self->command_queue != NULL) + { + gsk_ngl_command_queue_make_current (self->command_queue); + gsk_ngl_driver_collect_unused_textures (self, 0); + g_clear_object (&self->command_queue); + } + + if (self->autorelease_framebuffers->len > 0) + { + glDeleteFramebuffers (self->autorelease_framebuffers->len, + (GLuint *)(gpointer)self->autorelease_framebuffers->data); + self->autorelease_framebuffers->len = 0; + } + + gsk_ngl_texture_pool_clear (&self->texture_pool); + + g_assert (!self->textures || g_hash_table_size (self->textures) == 0); + g_assert (!self->texture_id_to_key || g_hash_table_size (self->texture_id_to_key) == 0); + g_assert (!self->key_to_texture_id|| g_hash_table_size (self->key_to_texture_id) == 0); + + g_clear_object (&self->glyphs); + g_clear_object (&self->icons); + g_clear_object (&self->shadows); + + g_clear_pointer (&self->atlases, g_ptr_array_unref); + g_clear_pointer (&self->autorelease_framebuffers, g_array_unref); + g_clear_pointer (&self->key_to_texture_id, g_hash_table_unref); + g_clear_pointer (&self->textures, g_hash_table_unref); + g_clear_pointer (&self->key_to_texture_id, g_hash_table_unref); + g_clear_pointer (&self->texture_id_to_key, g_hash_table_unref); + g_clear_pointer (&self->render_targets, g_ptr_array_unref); + g_clear_pointer (&self->shader_cache, g_hash_table_unref); + + g_clear_object (&self->command_queue); + g_clear_object (&self->shared_command_queue); + + G_OBJECT_CLASS (gsk_ngl_driver_parent_class)->dispose (object); +} + +static void +gsk_ngl_driver_class_init (GskNglDriverClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gsk_ngl_driver_dispose; +} + +static void +gsk_ngl_driver_init (GskNglDriver *self) +{ + self->autorelease_framebuffers = g_array_new (FALSE, FALSE, sizeof (guint)); + self->textures = g_hash_table_new_full (NULL, NULL, NULL, + (GDestroyNotify)gsk_ngl_texture_free); + self->texture_id_to_key = g_hash_table_new (NULL, NULL); + self->key_to_texture_id = g_hash_table_new_full (texture_key_hash, + texture_key_equal, + g_free, + NULL); + self->shader_cache = g_hash_table_new_full (NULL, NULL, NULL, remove_program); + gsk_ngl_texture_pool_init (&self->texture_pool); + self->render_targets = g_ptr_array_new (); + self->atlases = g_ptr_array_new_with_free_func ((GDestroyNotify)gsk_ngl_texture_atlas_free); +} + +static gboolean +gsk_ngl_driver_load_programs (GskNglDriver *self, + GError **error) +{ + GskNglCompiler *compiler; + gboolean ret = FALSE; + + g_assert (GSK_IS_NGL_DRIVER (self)); + g_assert (GSK_IS_NGL_COMMAND_QUEUE (self->command_queue)); + + compiler = gsk_ngl_compiler_new (self, self->debug); + + /* Setup preambles that are shared by all shaders */ + gsk_ngl_compiler_set_preamble_from_resource (compiler, + GSK_NGL_COMPILER_ALL, + "/org/gtk/libgsk/glsl/preamble.glsl"); + gsk_ngl_compiler_set_preamble_from_resource (compiler, + GSK_NGL_COMPILER_VERTEX, + "/org/gtk/libgsk/glsl/preamble.vs.glsl"); + gsk_ngl_compiler_set_preamble_from_resource (compiler, + GSK_NGL_COMPILER_FRAGMENT, + "/org/gtk/libgsk/glsl/preamble.fs.glsl"); + + /* Setup attributes that are provided via VBO */ + gsk_ngl_compiler_bind_attribute (compiler, "aPosition", 0); + gsk_ngl_compiler_bind_attribute (compiler, "aUv", 1); + + /* Use XMacros to register all of our programs and their uniforms */ +#define GSK_NGL_NO_UNIFORMS +#define GSK_NGL_ADD_UNIFORM(pos, KEY, name) \ + gsk_ngl_program_add_uniform (program, #name, UNIFORM_##KEY); +#define GSK_NGL_DEFINE_PROGRAM(name, resource, uniforms) \ + G_STMT_START { \ + GskNglProgram *program; \ + gboolean have_alpha; \ + gboolean have_source; \ + \ + gsk_ngl_compiler_set_source_from_resource (compiler, GSK_NGL_COMPILER_ALL, resource); \ + \ + if (!(program = gsk_ngl_compiler_compile (compiler, #name, error))) \ + goto failure; \ + \ + have_alpha = gsk_ngl_program_add_uniform (program, "u_alpha", UNIFORM_SHARED_ALPHA); \ + have_source = gsk_ngl_program_add_uniform (program, "u_source", UNIFORM_SHARED_SOURCE); \ + gsk_ngl_program_add_uniform (program, "u_clip_rect", UNIFORM_SHARED_CLIP_RECT); \ + gsk_ngl_program_add_uniform (program, "u_viewport", UNIFORM_SHARED_VIEWPORT); \ + gsk_ngl_program_add_uniform (program, "u_projection", UNIFORM_SHARED_PROJECTION); \ + gsk_ngl_program_add_uniform (program, "u_modelview", UNIFORM_SHARED_MODELVIEW); \ + \ + uniforms \ + \ + gsk_ngl_program_uniforms_added (program, have_source); \ + \ + if (have_alpha) \ + gsk_ngl_program_set_uniform1f (program, UNIFORM_SHARED_ALPHA, 0, 1.0f); \ + \ + *(GskNglProgram **)(((guint8 *)self) + G_STRUCT_OFFSET (GskNglDriver, name)) = \ + g_steal_pointer (&program); \ + } G_STMT_END; +# include "gsknglprograms.defs" +#undef GSK_NGL_DEFINE_PROGRAM +#undef GSK_NGL_ADD_UNIFORM + + ret = TRUE; + +failure: + g_clear_object (&compiler); + + return ret; +} + +/** + * gsk_ngl_driver_autorelease_framebuffer: + * @self: a #GskNglDriver + * @framebuffer_id: the id of the OpenGL framebuffer + * + * Marks @framebuffer_id to be deleted when the current frame has cmopleted. + */ +static void +gsk_ngl_driver_autorelease_framebuffer (GskNglDriver *self, + guint framebuffer_id) +{ + g_assert (GSK_IS_NGL_DRIVER (self)); + + g_array_append_val (self->autorelease_framebuffers, framebuffer_id); +} + +static GskNglDriver * +gsk_ngl_driver_new (GskNglCommandQueue *command_queue, + gboolean debug_shaders, + GError **error) +{ + GskNglDriver *self; + GdkGLContext *context; + + g_return_val_if_fail (GSK_IS_NGL_COMMAND_QUEUE (command_queue), NULL); + + context = gsk_ngl_command_queue_get_context (command_queue); + + gdk_gl_context_make_current (context); + + self = g_object_new (GSK_TYPE_NGL_DRIVER, NULL); + self->command_queue = g_object_ref (command_queue); + self->shared_command_queue = g_object_ref (command_queue); + self->debug = !!debug_shaders; + + if (!gsk_ngl_driver_load_programs (self, error)) + { + g_object_unref (self); + return NULL; + } + + self->glyphs = gsk_ngl_glyph_library_new (self); + self->icons = gsk_ngl_icon_library_new (self); + self->shadows = gsk_ngl_shadow_library_new (self); + + return g_steal_pointer (&self); +} + +/** + * gsk_ngl_driver_from_shared_context: + * @context: a shared #GdkGLContext retrieved with gdk_gl_context_get_shared_context() + * @debug_shaders: if debug information for shaders should be displayed + * @error: location for error information + * + * Retrieves a driver for a shared context. Generally this is shared across all GL + * contexts for a display so that fewer programs are necessary for driving output. + * + * Returns: (transfer full): a #GskNglDriver if successful; otherwise %NULL and + * @error is set. + */ +GskNglDriver * +gsk_ngl_driver_from_shared_context (GdkGLContext *context, + gboolean debug_shaders, + GError **error) +{ + GskNglCommandQueue *command_queue = NULL; + GskNglDriver *driver; + + g_return_val_if_fail (GDK_IS_GL_CONTEXT (context), NULL); + + if ((driver = g_object_get_data (G_OBJECT (context), "GSK_NGL_DRIVER"))) + return g_object_ref (driver); + + gdk_gl_context_make_current (context); + + /* Initially we create a command queue using the shared context. However, + * as frames are processed this will be replaced with the command queue + * for a given renderer. But since the programs are compiled into the + * shared context, all other contexts sharing with it will have access + * to those programs. + */ + command_queue = gsk_ngl_command_queue_new (context, NULL); + + if (!(driver = gsk_ngl_driver_new (command_queue, debug_shaders, error))) + goto failure; + + g_object_set_data_full (G_OBJECT (context), + "GSK_NGL_DRIVER", + g_object_ref (driver), + g_object_unref); + +failure: + g_clear_object (&command_queue); + + return g_steal_pointer (&driver); +} + +/** + * gsk_ngl_driver_begin_frame: + * @self: a #GskNglDriver + * @command_queue: A #GskNglCommandQueue from the renderer + * + * Begin a new frame. + * + * Texture atlases, pools, and other resources will be prepared to draw the + * next frame. The command queue should be one that was created for the + * target context to be drawn into (the context of the renderer's surface). + */ +void +gsk_ngl_driver_begin_frame (GskNglDriver *self, + GskNglCommandQueue *command_queue) +{ + gint64 last_frame_id; + + g_return_if_fail (GSK_IS_NGL_DRIVER (self)); + g_return_if_fail (GSK_IS_NGL_COMMAND_QUEUE (command_queue)); + g_return_if_fail (self->in_frame == FALSE); + + last_frame_id = self->current_frame_id; + + self->in_frame = TRUE; + self->current_frame_id++; + + g_set_object (&self->command_queue, command_queue); + + gsk_ngl_command_queue_begin_frame (self->command_queue); + + gsk_ngl_texture_library_begin_frame (GSK_NGL_TEXTURE_LIBRARY (self->icons)); + gsk_ngl_texture_library_begin_frame (GSK_NGL_TEXTURE_LIBRARY (self->glyphs)); + gsk_ngl_shadow_library_begin_frame (self->shadows); + + /* Remove all textures that are from a previous frame or are no + * longer used by linked GdkTexture. We do this at the beginning + * of the following frame instead of the end so that we reduce chances + * we block on any resources while delivering our frames. + */ + gsk_ngl_driver_collect_unused_textures (self, last_frame_id - 1); +} + +/** + * gsk_ngl_driver_end_frame: + * @self: a #GskNglDriver + * + * Clean up resources from drawing the current frame. + * + * Temporary resources used while drawing will be released. + */ +void +gsk_ngl_driver_end_frame (GskNglDriver *self) +{ + g_return_if_fail (GSK_IS_NGL_DRIVER (self)); + g_return_if_fail (self->in_frame == TRUE); + + gsk_ngl_command_queue_make_current (self->command_queue); + gsk_ngl_command_queue_end_frame (self->command_queue); + + gsk_ngl_texture_library_end_frame (GSK_NGL_TEXTURE_LIBRARY (self->icons)); + gsk_ngl_texture_library_end_frame (GSK_NGL_TEXTURE_LIBRARY (self->glyphs)); + + self->in_frame = FALSE; +} + +/** + * gsk_ngl_driver_after_frame: + * @self: a #GskNglDriver + * + * This function does post-frame cleanup operations. + * + * To reduce the chances of blocking on the driver it is performed + * after the frame has swapped buffers. + */ +void +gsk_ngl_driver_after_frame (GskNglDriver *self) +{ + g_return_if_fail (GSK_IS_NGL_DRIVER (self)); + g_return_if_fail (self->in_frame == FALSE); + + /* Release any render targets (possibly adding them to + * self->autorelease_framebuffers) so we can release the FBOs immediately + * afterwards. + */ + while (self->render_targets->len > 0) + { + GskNglRenderTarget *render_target = g_ptr_array_index (self->render_targets, self->render_targets->len - 1); + + gsk_ngl_driver_autorelease_framebuffer (self, render_target->framebuffer_id); + glDeleteTextures (1, &render_target->texture_id); + g_slice_free (GskNglRenderTarget, render_target); + + self->render_targets->len--; + } + + /* Now that we have collected render targets, release all the FBOs */ + if (self->autorelease_framebuffers->len > 0) + { + glDeleteFramebuffers (self->autorelease_framebuffers->len, + (GLuint *)(gpointer)self->autorelease_framebuffers->data); + self->autorelease_framebuffers->len = 0; + } + + /* Release any cached textures we used during the frame */ + gsk_ngl_texture_pool_clear (&self->texture_pool); + + /* Reset command queue to our shared queue incase we have operations + * that need to be processed outside of a frame (such as callbacks + * from external systems such as GDK). + */ + g_set_object (&self->command_queue, self->shared_command_queue); +} + +GdkGLContext * +gsk_ngl_driver_get_context (GskNglDriver *self) +{ + g_return_val_if_fail (GSK_IS_NGL_DRIVER (self), NULL); + g_return_val_if_fail (GSK_IS_NGL_COMMAND_QUEUE (self->command_queue), NULL); + + return gsk_ngl_command_queue_get_context (self->command_queue); +} + +/** + * gsk_ngl_driver_cache_texture: + * @self: a #GskNglDriver + * @key: the key for the texture + * @texture_id: the id of the texture to be cached + * + * Inserts @texture_id into the texture cache using @key. + * + * Textures can be looked up by @key after calling this function using + * gsk_ngl_driver_lookup_texture(). + * + * Textures that have not been used within a number of frames will be + * purged from the texture cache automatically. + */ +void +gsk_ngl_driver_cache_texture (GskNglDriver *self, + const GskTextureKey *key, + guint texture_id) +{ + GskTextureKey *k; + + g_assert (GSK_IS_NGL_DRIVER (self)); + g_assert (key != NULL); + g_assert (texture_id > 0); + g_assert (g_hash_table_contains (self->textures, GUINT_TO_POINTER (texture_id))); + + k = g_memdup (key, sizeof *key); + + g_hash_table_insert (self->key_to_texture_id, k, GUINT_TO_POINTER (texture_id)); + g_hash_table_insert (self->texture_id_to_key, GUINT_TO_POINTER (texture_id), k); +} + +/** + * gsk_ngl_driver_load_texture: + * @self: a #GdkTexture + * @texture: a #GdkTexture + * @min_filter: GL_NEAREST or GL_LINEAR + * @mag_filter: GL_NEAREST or GL_LINEAR + * + * Loads a #GdkTexture by uploading the contents to the GPU when + * necessary. If @texture is a #GdkGLTexture, it can be used without + * uploading contents to the GPU. + * + * If the texture has already been uploaded and not yet released + * from cache, this function returns that texture id without further + * work. + * + * If the texture has not been used for a number of frames, it will + * be removed from cache. + * + * There is no need to release the resulting texture identifier after + * using it. It will be released automatically. + * + * Returns: a texture identifier + */ +guint +gsk_ngl_driver_load_texture (GskNglDriver *self, + GdkTexture *texture, + int min_filter, + int mag_filter) +{ + GdkGLContext *context; + GdkTexture *downloaded_texture = NULL; + GdkTexture *source_texture; + GskNglTexture *t; + guint texture_id; + int height; + int width; + + g_return_val_if_fail (GSK_IS_NGL_DRIVER (self), 0); + g_return_val_if_fail (GDK_IS_TEXTURE (texture), 0); + g_return_val_if_fail (GSK_IS_NGL_COMMAND_QUEUE (self->command_queue), 0); + + context = self->command_queue->context; + + if (GDK_IS_GL_TEXTURE (texture)) + { + GdkGLContext *texture_context = gdk_gl_texture_get_context ((GdkGLTexture *)texture); + GdkGLContext *shared_context = gdk_gl_context_get_shared_context (context); + + if (texture_context == context || + (shared_context != NULL && + shared_context == gdk_gl_context_get_shared_context (texture_context))) + + { + /* A GL texture from the same GL context is a simple task... */ + return gdk_gl_texture_get_id ((GdkGLTexture *)texture); + } + else + { + cairo_surface_t *surface; + + /* In this case, we have to temporarily make the texture's + * context the current one, download its data into our context + * and then create a texture from it. */ + if (texture_context != NULL) + gdk_gl_context_make_current (texture_context); + + surface = gdk_texture_download_surface (texture); + downloaded_texture = gdk_texture_new_for_surface (surface); + cairo_surface_destroy (surface); + + gdk_gl_context_make_current (context); + + source_texture = downloaded_texture; + } + } + else + { + if ((t = gdk_texture_get_render_data (texture, self))) + { + if (t->min_filter == min_filter && t->mag_filter == mag_filter) + return t->texture_id; + } + + source_texture = texture; + } + + width = gdk_texture_get_width (texture); + height = gdk_texture_get_height (texture); + texture_id = gsk_ngl_command_queue_upload_texture (self->command_queue, + source_texture, + 0, + 0, + width, + height, + min_filter, + mag_filter); + + t = gsk_ngl_texture_new (texture_id, + width, height, min_filter, mag_filter, + self->current_frame_id); + + g_hash_table_insert (self->textures, GUINT_TO_POINTER (texture_id), t); + + if (gdk_texture_set_render_data (texture, self, t, gsk_ngl_texture_destroyed)) + t->user = texture; + + gdk_gl_context_label_object_printf (context, GL_TEXTURE, t->texture_id, + "GdkTexture<%p> %d", texture, t->texture_id); + + g_clear_object (&downloaded_texture); + + return texture_id; +} + +/** + * gsk_ngl_driver_create_texture: + * @self: a #GskNglDriver + * @width: the width of the texture + * @height: the height of the texture + * @min_filter: GL_NEAREST or GL_LINEAR + * @mag_filter: GL_NEAREST or GL_FILTER + * + * Creates a new texture immediately that can be used by the caller + * to upload data, map to a framebuffer, or other uses which may + * modify the texture immediately. + * + * Use gsk_ngl_driver_release_texture() to release this texture back into + * the pool so it may be reused later in the pipeline. + * + * Returns: a #GskNglTexture which can be returned to the pool with + * gsk_ngl_driver_release_texture(). + */ +GskNglTexture * +gsk_ngl_driver_create_texture (GskNglDriver *self, + float width, + float height, + int min_filter, + int mag_filter) +{ + GskNglTexture *texture; + + g_return_val_if_fail (GSK_IS_NGL_DRIVER (self), NULL); + + texture = gsk_ngl_texture_pool_get (&self->texture_pool, + width, height, + min_filter, mag_filter); + g_hash_table_insert (self->textures, + GUINT_TO_POINTER (texture->texture_id), + texture); + texture->last_used_in_frame = self->current_frame_id; + return texture; +} + +/** + * gsk_ngl_driver_release_texture: + * @self: a #GskNglDriver + * @texture: a #GskNglTexture + * + * Releases @texture back into the pool so that it can be used later + * in the command stream by future batches. This helps reduce VRAM + * usage on the GPU. + * + * When the frame has completed, pooled textures will be released + * to free additional VRAM back to the system. + */ +void +gsk_ngl_driver_release_texture (GskNglDriver *self, + GskNglTexture *texture) +{ + guint texture_id; + + g_assert (GSK_IS_NGL_DRIVER (self)); + g_assert (texture != NULL); + + texture_id = texture->texture_id; + + if (texture_id > 0) + remove_texture_key_for_id (self, texture_id); + + g_hash_table_steal (self->textures, GUINT_TO_POINTER (texture_id)); + gsk_ngl_texture_pool_put (&self->texture_pool, texture); +} + +/** + * gsk_ngl_driver_create_render_target: + * @self: a #GskNglDriver + * @width: the width for the render target + * @height: the height for the render target + * @min_filter: the min filter to use for the texture + * @mag_filter: the mag filter to use for the texture + * @out_render_target: (out): a location for the render target + * + * Creates a new render target which contains a framebuffer and a texture + * bound to that framebuffer of the size @width x @height and using the + * appropriate filters. + * + * Use gsk_ngl_driver_release_render_target() when you are finished with + * the render target to release it. You may steal the texture from the + * render target when releasing it. + * + * Returns: %TRUE if successful; otherwise %FALSE and @out_fbo_id and + * @out_texture_id are undefined. + */ +gboolean +gsk_ngl_driver_create_render_target (GskNglDriver *self, + int width, + int height, + int min_filter, + int mag_filter, + GskNglRenderTarget **out_render_target) +{ + guint framebuffer_id; + guint texture_id; + + g_return_val_if_fail (GSK_IS_NGL_DRIVER (self), FALSE); + g_return_val_if_fail (GSK_IS_NGL_COMMAND_QUEUE (self->command_queue), FALSE); + g_return_val_if_fail (out_render_target != NULL, FALSE); + +#if 0 + if (self->render_targets->len > 0) + { + for (guint i = self->render_targets->len; i > 0; i--) + { + GskNglRenderTarget *render_target = g_ptr_array_index (self->render_targets, i-1); + + if (render_target->width == width && + render_target->height == height && + render_target->min_filter == min_filter && + render_target->mag_filter == mag_filter) + { + *out_render_target = g_ptr_array_steal_index_fast (self->render_targets, i-1); + return TRUE; + } + } + } +#endif + + if (gsk_ngl_command_queue_create_render_target (self->command_queue, + width, height, + min_filter, mag_filter, + &framebuffer_id, &texture_id)) + { + GskNglRenderTarget *render_target; + + render_target = g_slice_new0 (GskNglRenderTarget); + render_target->min_filter = min_filter; + render_target->mag_filter = mag_filter; + render_target->width = width; + render_target->height = height; + render_target->framebuffer_id = framebuffer_id; + render_target->texture_id = texture_id; + + *out_render_target = render_target; + + return TRUE; + } + + *out_render_target = NULL; + + return FALSE; +} + +/** + * gsk_ngl_driver_release_render_target: + * @self: a #GskNglDriver + * @render_target: a #GskNglRenderTarget created with + * gsk_ngl_driver_create_render_target(). + * @release_texture: if the texture should also be released + * + * Releases a render target that was previously created. An attempt may + * be made to cache the render target so that future creations of render + * targets are performed faster. + * + * If @release_texture is %FALSE, the backing texture id is returned and + * the framebuffer is released. Otherwise, both the texture and framebuffer + * are released or cached until the end of the frame. + * + * This may be called when building the render job as the texture or + * framebuffer will not be removed immediately. + * + * Returns: a texture id if @release_texture is %FALSE, otherwise zero. + */ +guint +gsk_ngl_driver_release_render_target (GskNglDriver *self, + GskNglRenderTarget *render_target, + gboolean release_texture) +{ + guint texture_id; + + g_return_val_if_fail (GSK_IS_NGL_DRIVER (self), 0); + g_return_val_if_fail (render_target != NULL, 0); + + if (release_texture) + { + texture_id = 0; + g_ptr_array_add (self->render_targets, render_target); + } + else + { + GskNglTexture *texture; + + texture_id = render_target->texture_id; + + texture = gsk_ngl_texture_new (render_target->texture_id, + render_target->width, + render_target->height, + render_target->min_filter, + render_target->mag_filter, + self->current_frame_id); + g_hash_table_insert (self->textures, + GUINT_TO_POINTER (texture_id), + g_steal_pointer (&texture)); + + gsk_ngl_driver_autorelease_framebuffer (self, render_target->framebuffer_id); + g_slice_free (GskNglRenderTarget, render_target); + + } + + return texture_id; +} + +/** + * gsk_ngl_driver_lookup_shader: + * @self: a #GskNglDriver + * @shader: the shader to lookup or load + * @error: a location for a #GError, or %NULL + * + * Attepts to load @shader from the shader cache. + * + * If it has not been loaded, then it will compile the shader on demand. + * + * Returns: (transfer none): a #GskGLShader if successful; otherwise + * %NULL and @error is set. + */ +GskNglProgram * +gsk_ngl_driver_lookup_shader (GskNglDriver *self, + GskGLShader *shader, + GError **error) +{ + GskNglProgram *program; + + g_return_val_if_fail (self != NULL, NULL); + g_return_val_if_fail (shader != NULL, NULL); + + program = g_hash_table_lookup (self->shader_cache, shader); + + if (program == NULL) + { + const GskGLUniform *uniforms; + GskNglCompiler *compiler; + GBytes *suffix; + int n_required_textures; + int n_uniforms; + + uniforms = gsk_gl_shader_get_uniforms (shader, &n_uniforms); + if (n_uniforms > G_N_ELEMENTS (program->args_locations)) + { + g_set_error (error, + GDK_GL_ERROR, + GDK_GL_ERROR_UNSUPPORTED_FORMAT, + "Tried to use %d uniforms, while only %d is supported", + n_uniforms, + (int)G_N_ELEMENTS (program->args_locations)); + return NULL; + } + + n_required_textures = gsk_gl_shader_get_n_textures (shader); + if (n_required_textures > G_N_ELEMENTS (program->texture_locations)) + { + g_set_error (error, + GDK_GL_ERROR, + GDK_GL_ERROR_UNSUPPORTED_FORMAT, + "Tried to use %d textures, while only %d is supported", + n_required_textures, + (int)(G_N_ELEMENTS (program->texture_locations))); + return NULL; + } + + compiler = gsk_ngl_compiler_new (self, FALSE); + suffix = gsk_gl_shader_get_source (shader); + + gsk_ngl_compiler_set_preamble_from_resource (compiler, + GSK_NGL_COMPILER_ALL, + "/org/gtk/libgsk/glsl/preamble.glsl"); + gsk_ngl_compiler_set_preamble_from_resource (compiler, + GSK_NGL_COMPILER_VERTEX, + "/org/gtk/libgsk/glsl/preamble.vs.glsl"); + gsk_ngl_compiler_set_preamble_from_resource (compiler, + GSK_NGL_COMPILER_FRAGMENT, + "/org/gtk/libgsk/glsl/preamble.fs.glsl"); + gsk_ngl_compiler_set_source_from_resource (compiler, + GSK_NGL_COMPILER_ALL, + "/org/gtk/libgsk/glsl/custom.glsl"); + gsk_ngl_compiler_set_suffix (compiler, GSK_NGL_COMPILER_FRAGMENT, suffix); + + /* Setup attributes that are provided via VBO */ + gsk_ngl_compiler_bind_attribute (compiler, "aPosition", 0); + gsk_ngl_compiler_bind_attribute (compiler, "aUv", 1); + + if ((program = gsk_ngl_compiler_compile (compiler, NULL, error))) + { + gboolean have_alpha; + + gsk_ngl_program_add_uniform (program, "u_source", UNIFORM_SHARED_SOURCE); + gsk_ngl_program_add_uniform (program, "u_clip_rect", UNIFORM_SHARED_CLIP_RECT); + gsk_ngl_program_add_uniform (program, "u_viewport", UNIFORM_SHARED_VIEWPORT); + gsk_ngl_program_add_uniform (program, "u_projection", UNIFORM_SHARED_PROJECTION); + gsk_ngl_program_add_uniform (program, "u_modelview", UNIFORM_SHARED_MODELVIEW); + have_alpha = gsk_ngl_program_add_uniform (program, "u_alpha", UNIFORM_SHARED_ALPHA); + + gsk_ngl_program_add_uniform (program, "u_size", UNIFORM_CUSTOM_SIZE); + gsk_ngl_program_add_uniform (program, "u_texture1", UNIFORM_CUSTOM_TEXTURE1); + gsk_ngl_program_add_uniform (program, "u_texture2", UNIFORM_CUSTOM_TEXTURE2); + gsk_ngl_program_add_uniform (program, "u_texture3", UNIFORM_CUSTOM_TEXTURE3); + gsk_ngl_program_add_uniform (program, "u_texture4", UNIFORM_CUSTOM_TEXTURE4); + for (guint i = 0; i < n_uniforms; i++) + gsk_ngl_program_add_uniform (program, uniforms[i].name, UNIFORM_CUSTOM_LAST+i); + + program->size_location = gsk_ngl_program_get_uniform_location (program, UNIFORM_CUSTOM_SIZE); + program->texture_locations[0] = gsk_ngl_program_get_uniform_location (program, UNIFORM_CUSTOM_TEXTURE1); + program->texture_locations[1] = gsk_ngl_program_get_uniform_location (program, UNIFORM_CUSTOM_TEXTURE2); + program->texture_locations[2] = gsk_ngl_program_get_uniform_location (program, UNIFORM_CUSTOM_TEXTURE3); + program->texture_locations[3] = gsk_ngl_program_get_uniform_location (program, UNIFORM_CUSTOM_TEXTURE4); + for (guint i = 0; i < n_uniforms; i++) + program->args_locations[i] = gsk_ngl_program_get_uniform_location (program, UNIFORM_CUSTOM_LAST+i); + for (guint i = n_uniforms; i < G_N_ELEMENTS (program->args_locations); i++) + program->args_locations[i] = -1; + + gsk_ngl_program_uniforms_added (program, TRUE); + + if (have_alpha) + gsk_ngl_program_set_uniform1f (program, UNIFORM_SHARED_ALPHA, 0, 1.0f); + + g_hash_table_insert (self->shader_cache, shader, program); + g_object_weak_ref (G_OBJECT (shader), + gsk_ngl_driver_shader_weak_cb, + self); + } + + g_object_unref (compiler); + } + + return program; +} + +#ifdef G_ENABLE_DEBUG +static void +write_atlas_to_png (GskNglTextureAtlas *atlas, + const char *filename) +{ + int stride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, atlas->width); + guchar *data = g_malloc (atlas->height * stride); + cairo_surface_t *s; + + glBindTexture (GL_TEXTURE_2D, atlas->texture_id); + glGetTexImage (GL_TEXTURE_2D, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, data); + s = cairo_image_surface_create_for_data (data, CAIRO_FORMAT_ARGB32, atlas->width, atlas->height, stride); + cairo_surface_write_to_png (s, filename); + + cairo_surface_destroy (s); + g_free (data); +} + +void +gsk_ngl_driver_save_atlases_to_png (GskNglDriver *self, + const char *directory) +{ + g_return_if_fail (GSK_IS_NGL_DRIVER (self)); + + if (directory == NULL) + directory = "."; + + for (guint i = 0; i < self->atlases->len; i++) + { + GskNglTextureAtlas *atlas = g_ptr_array_index (self->atlases, i); + char *filename = g_strdup_printf ("%s%sframe-%d-atlas-%d.png", + directory, + G_DIR_SEPARATOR_S, + (int)self->current_frame_id, + atlas->texture_id); + write_atlas_to_png (atlas, filename); + g_free (filename); + } +} +#endif + +GskNglCommandQueue * +gsk_ngl_driver_create_command_queue (GskNglDriver *self, + GdkGLContext *context) +{ + g_return_val_if_fail (GSK_IS_NGL_DRIVER (self), NULL); + g_return_val_if_fail (GDK_IS_GL_CONTEXT (context), NULL); + + return gsk_ngl_command_queue_new (context, self->shared_command_queue->uniforms); +} + +void +gsk_ngl_driver_add_texture_slices (GskNglDriver *self, + GdkTexture *texture, + GskNglTextureSlice **out_slices, + guint *out_n_slices) +{ + int max_texture_size; + GskNglTextureSlice *slices; + GskNglTexture *t; + guint n_slices; + guint cols; + guint rows; + int tex_width; + int tex_height; + int x = 0, y = 0; + + g_assert (GSK_IS_NGL_DRIVER (self)); + g_assert (GDK_IS_TEXTURE (texture)); + g_assert (out_slices != NULL); + g_assert (out_n_slices != NULL); + + /* XXX: Too much? */ + max_texture_size = self->command_queue->max_texture_size / 4; + + tex_width = texture->width; + tex_height = texture->height; + cols = (texture->width / max_texture_size) + 1; + rows = (texture->height / max_texture_size) + 1; + + if ((t = gdk_texture_get_render_data (texture, self))) + { + *out_slices = t->slices; + *out_n_slices = t->n_slices; + return; + } + + n_slices = cols * rows; + slices = g_new0 (GskNglTextureSlice, n_slices); + + for (guint col = 0; col < cols; col ++) + { + int slice_width = MIN (max_texture_size, texture->width - x); + + for (guint row = 0; row < rows; row ++) + { + int slice_height = MIN (max_texture_size, texture->height - y); + int slice_index = (col * rows) + row; + guint texture_id; + + texture_id = gsk_ngl_command_queue_upload_texture (self->command_queue, + texture, + x, y, + slice_width, slice_height, + GL_NEAREST, GL_NEAREST); + + slices[slice_index].rect.x = x; + slices[slice_index].rect.y = y; + slices[slice_index].rect.width = slice_width; + slices[slice_index].rect.height = slice_height; + slices[slice_index].texture_id = texture_id; + + y += slice_height; + } + + y = 0; + x += slice_width; + } + + /* Allocate one Texture for the entire thing. */ + t = gsk_ngl_texture_new (0, + tex_width, tex_height, + GL_NEAREST, GL_NEAREST, + self->current_frame_id); + + /* Use gsk_ngl_texture_free() as destroy notify here since we are + * not inserting this GskNglTexture into self->textures! + */ + gdk_texture_set_render_data (texture, self, t, + (GDestroyNotify)gsk_ngl_texture_free); + + t->slices = *out_slices = slices; + t->n_slices = *out_n_slices = n_slices; +} + +GskNglTexture * +gsk_ngl_driver_mark_texture_permanent (GskNglDriver *self, + guint texture_id) +{ + GskNglTexture *t; + + g_return_val_if_fail (GSK_IS_NGL_DRIVER (self), NULL); + g_return_val_if_fail (texture_id > 0, NULL); + + if ((t = g_hash_table_lookup (self->textures, GUINT_TO_POINTER (texture_id)))) + t->permanent = TRUE; + + return t; +} + +void +gsk_ngl_driver_release_texture_by_id (GskNglDriver *self, + guint texture_id) +{ + GskNglTexture *texture; + + g_return_if_fail (GSK_IS_NGL_DRIVER (self)); + g_return_if_fail (texture_id > 0); + + remove_texture_key_for_id (self, texture_id); + + if ((texture = g_hash_table_lookup (self->textures, GUINT_TO_POINTER (texture_id)))) + gsk_ngl_driver_release_texture (self, texture); +} + + +static void +create_texture_from_texture_destroy (gpointer data) +{ + GskNglTextureState *state = data; + + g_assert (state != NULL); + g_assert (GDK_IS_GL_CONTEXT (state->context)); + + gdk_gl_context_make_current (state->context); + glDeleteTextures (1, &state->texture_id); + g_clear_object (&state->context); + g_slice_free (GskNglTextureState, state); +} + +GdkTexture * +gsk_ngl_driver_create_gdk_texture (GskNglDriver *self, + guint texture_id) +{ + GskNglTextureState *state; + GskNglTexture *texture; + + g_return_val_if_fail (GSK_IS_NGL_DRIVER (self), NULL); + g_return_val_if_fail (self->command_queue != NULL, NULL); + g_return_val_if_fail (GDK_IS_GL_CONTEXT (self->command_queue->context), NULL); + g_return_val_if_fail (texture_id > 0, NULL); + g_return_val_if_fail (!g_hash_table_contains (self->texture_id_to_key, GUINT_TO_POINTER (texture_id)), NULL); + + /* We must be tracking this texture_id already to use it */ + if (!(texture = g_hash_table_lookup (self->textures, GUINT_TO_POINTER (texture_id)))) + g_return_val_if_reached (NULL); + + state = g_slice_new0 (GskNglTextureState); + state->texture_id = texture_id; + state->context = g_object_ref (self->command_queue->context); + + g_hash_table_steal (self->textures, GUINT_TO_POINTER (texture_id)); + + return gdk_gl_texture_new (self->command_queue->context, + texture_id, + texture->width, + texture->height, + create_texture_from_texture_destroy, + state); +} diff --git a/gsk/ngl/gskngldriverprivate.h b/gsk/ngl/gskngldriverprivate.h new file mode 100644 index 0000000000..e5cda21adc --- /dev/null +++ b/gsk/ngl/gskngldriverprivate.h @@ -0,0 +1,234 @@ +/* gskngldriverprivate.h + * + * Copyright 2020 Christian Hergert + * + * This file 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 file 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 General Public License along + * with this program. If not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef __GSK_NGL_DRIVER_PRIVATE_H__ +#define __GSK_NGL_DRIVER_PRIVATE_H__ + +#include + +#include "gskngltypesprivate.h" +#include "gskngltexturepoolprivate.h" + +G_BEGIN_DECLS + +enum { + UNIFORM_SHARED_ALPHA, + UNIFORM_SHARED_SOURCE, + UNIFORM_SHARED_CLIP_RECT, + UNIFORM_SHARED_VIEWPORT, + UNIFORM_SHARED_PROJECTION, + UNIFORM_SHARED_MODELVIEW, + + UNIFORM_SHARED_LAST +}; + +enum { + UNIFORM_CUSTOM_SIZE = UNIFORM_SHARED_LAST, + UNIFORM_CUSTOM_TEXTURE1, + UNIFORM_CUSTOM_TEXTURE2, + UNIFORM_CUSTOM_TEXTURE3, + UNIFORM_CUSTOM_TEXTURE4, + + UNIFORM_CUSTOM_LAST +}; + +typedef struct { + gconstpointer pointer; + float scale_x; + float scale_y; + int filter; + int pointer_is_child; + graphene_rect_t parent_rect; /* Valid when pointer_is_child */ +} GskTextureKey; + +#define GSL_GK_NO_UNIFORMS UNIFORM_INVALID_##__COUNTER__ +#define GSK_NGL_ADD_UNIFORM(pos, KEY, name) UNIFORM_##KEY = UNIFORM_SHARED_LAST + pos, +#define GSK_NGL_DEFINE_PROGRAM(name, resource, uniforms) enum { uniforms }; +# include "gsknglprograms.defs" +#undef GSK_NGL_DEFINE_PROGRAM +#undef GSK_NGL_ADD_UNIFORM +#undef GSL_GK_NO_UNIFORMS + +#define GSK_TYPE_NGL_DRIVER (gsk_ngl_driver_get_type()) + +G_DECLARE_FINAL_TYPE (GskNglDriver, gsk_ngl_driver, GSK, NGL_DRIVER, GObject) + +struct _GskNglRenderTarget +{ + guint framebuffer_id; + guint texture_id; + int min_filter; + int mag_filter; + int width; + int height; +}; + +struct _GskNglDriver +{ + GObject parent_instance; + + GskNglCommandQueue *shared_command_queue; + GskNglCommandQueue *command_queue; + + GskNglTexturePool texture_pool; + + GskNglGlyphLibrary *glyphs; + GskNglIconLibrary *icons; + GskNglShadowLibrary *shadows; + + GHashTable *textures; + GHashTable *key_to_texture_id; + GHashTable *texture_id_to_key; + + GPtrArray *atlases; + + GHashTable *shader_cache; + + GArray *autorelease_framebuffers; + GPtrArray *render_targets; + +#define GSK_NGL_NO_UNIFORMS +#define GSK_NGL_ADD_UNIFORM(pos, KEY, name) +#define GSK_NGL_DEFINE_PROGRAM(name, resource, uniforms) GskNglProgram *name; +# include "gsknglprograms.defs" +#undef GSK_NGL_NO_UNIFORMS +#undef GSK_NGL_ADD_UNIFORM +#undef GSK_NGL_DEFINE_PROGRAM + + gint64 current_frame_id; + + /* Used to reduce number of comparisons */ + guint stamps[UNIFORM_SHARED_LAST]; + + guint debug : 1; + guint in_frame : 1; +}; + +GskNglDriver *gsk_ngl_driver_from_shared_context (GdkGLContext *context, + gboolean debug_shaders, + GError **error); +GskNglCommandQueue *gsk_ngl_driver_create_command_queue (GskNglDriver *self, + GdkGLContext *context); +GdkGLContext *gsk_ngl_driver_get_context (GskNglDriver *self); +gboolean gsk_ngl_driver_create_render_target (GskNglDriver *self, + int width, + int height, + int min_filter, + int mag_filter, + GskNglRenderTarget **render_target); +guint gsk_ngl_driver_release_render_target (GskNglDriver *self, + GskNglRenderTarget *render_target, + gboolean release_texture); +void gsk_ngl_driver_begin_frame (GskNglDriver *self, + GskNglCommandQueue *command_queue); +void gsk_ngl_driver_end_frame (GskNglDriver *self); +void gsk_ngl_driver_after_frame (GskNglDriver *self); +GdkTexture *gsk_ngl_driver_create_gdk_texture (GskNglDriver *self, + guint texture_id); +void gsk_ngl_driver_cache_texture (GskNglDriver *self, + const GskTextureKey *key, + guint texture_id); +guint gsk_ngl_driver_load_texture (GskNglDriver *self, + GdkTexture *texture, + int min_filter, + int mag_filter); +GskNglTexture *gsk_ngl_driver_create_texture (GskNglDriver *self, + float width, + float height, + int min_filter, + int mag_filter); +void gsk_ngl_driver_release_texture (GskNglDriver *self, + GskNglTexture *texture); +void gsk_ngl_driver_release_texture_by_id (GskNglDriver *self, + guint texture_id); +GskNglTexture *gsk_ngl_driver_mark_texture_permanent (GskNglDriver *self, + guint texture_id); +void gsk_ngl_driver_add_texture_slices (GskNglDriver *self, + GdkTexture *texture, + GskNglTextureSlice **out_slices, + guint *out_n_slices); +GskNglProgram *gsk_ngl_driver_lookup_shader (GskNglDriver *self, + GskGLShader *shader, + GError **error); +GskNglTextureAtlas *gsk_ngl_driver_create_atlas (GskNglDriver *self); + +#ifdef G_ENABLE_DEBUG +void gsk_ngl_driver_save_atlases_to_png (GskNglDriver *self, + const char *directory); +#endif + +static inline GskNglTexture * +gsk_ngl_driver_get_texture_by_id (GskNglDriver *self, + guint texture_id) +{ + return g_hash_table_lookup (self->textures, GUINT_TO_POINTER (texture_id)); +} + +/** + * gsk_ngl_driver_lookup_texture: + * @self: a #GskNglDriver + * @key: the key for the texture + * + * Looks up a texture in the texture cache by @key. + * + * If the texture could not be found, then zero is returned. + * + * Returns: a positive integer if the texture was found; otherwise 0. + */ +static inline guint +gsk_ngl_driver_lookup_texture (GskNglDriver *self, + const GskTextureKey *key) +{ + gpointer id; + + if (g_hash_table_lookup_extended (self->key_to_texture_id, key, NULL, &id)) + { + GskNglTexture *texture = g_hash_table_lookup (self->textures, id); + + if (texture != NULL) + texture->last_used_in_frame = self->current_frame_id; + + return GPOINTER_TO_UINT (id); + } + + return 0; +} + +static inline void +gsk_ngl_driver_slice_texture (GskNglDriver *self, + GdkTexture *texture, + GskNglTextureSlice **out_slices, + guint *out_n_slices) +{ + GskNglTexture *t; + + if ((t = gdk_texture_get_render_data (texture, self))) + { + *out_slices = t->slices; + *out_n_slices = t->n_slices; + return; + } + + gsk_ngl_driver_add_texture_slices (self, texture, out_slices, out_n_slices); +} + +G_END_DECLS + +#endif /* __GSK_NGL_DRIVER_PRIVATE_H__ */ diff --git a/gsk/ngl/gsknglglyphlibrary.c b/gsk/ngl/gsknglglyphlibrary.c new file mode 100644 index 0000000000..5cb2a3fe3b --- /dev/null +++ b/gsk/ngl/gsknglglyphlibrary.c @@ -0,0 +1,325 @@ +/* gsknglglyphlibrary.c + * + * 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 "gsknglcommandqueueprivate.h" +#include "gskngldriverprivate.h" +#include "gsknglglyphlibraryprivate.h" + +#define MAX_GLYPH_SIZE 128 + +G_DEFINE_TYPE (GskNglGlyphLibrary, gsk_ngl_glyph_library, GSK_TYPE_GL_TEXTURE_LIBRARY) + +GskNglGlyphLibrary * +gsk_ngl_glyph_library_new (GskNglDriver *driver) +{ + g_return_val_if_fail (GSK_IS_NGL_DRIVER (driver), NULL); + + return g_object_new (GSK_TYPE_GL_GLYPH_LIBRARY, + "driver", driver, + NULL); +} + +static guint +gsk_ngl_glyph_key_hash (gconstpointer data) +{ + const GskNglGlyphKey *key = data; + + /* We do not store the hash within the key because GHashTable will already + * store the hash value for us and so this is called only a single time per + * cached item. This saves an extra 4 bytes per GskNglGlyphKey which means on + * 64-bit, we fit nicely within 2 pointers (the smallest allocation size + * for GSlice). + */ + + return GPOINTER_TO_UINT (key->font) ^ + key->glyph ^ + (key->xshift << 24) ^ + (key->yshift << 26) ^ + key->scale; +} + +static gboolean +gsk_ngl_glyph_key_equal (gconstpointer v1, + gconstpointer v2) +{ + return memcmp (v1, v2, sizeof (GskNglGlyphKey)) == 0; +} + +static void +gsk_ngl_glyph_key_free (gpointer data) +{ + GskNglGlyphKey *key = data; + + g_clear_object (&key->font); + g_slice_free (GskNglGlyphKey, key); +} + +static void +gsk_ngl_glyph_value_free (gpointer data) +{ + g_slice_free (GskNglGlyphValue, data); +} + +static void +gsk_ngl_glyph_library_finalize (GObject *object) +{ + GskNglGlyphLibrary *self = (GskNglGlyphLibrary *)object; + + g_clear_pointer (&self->hash_table, g_hash_table_unref); + g_clear_pointer (&self->surface_data, g_free); + + G_OBJECT_CLASS (gsk_ngl_glyph_library_parent_class)->finalize (object); +} + +static void +gsk_ngl_glyph_library_class_init (GskNglGlyphLibraryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsk_ngl_glyph_library_finalize; +} + +static void +gsk_ngl_glyph_library_init (GskNglGlyphLibrary *self) +{ + GSK_NGL_TEXTURE_LIBRARY (self)->max_entry_size = MAX_GLYPH_SIZE; + gsk_ngl_texture_library_set_funcs (GSK_NGL_TEXTURE_LIBRARY (self), + gsk_ngl_glyph_key_hash, + gsk_ngl_glyph_key_equal, + gsk_ngl_glyph_key_free, + gsk_ngl_glyph_value_free); +} + +static cairo_surface_t * +gsk_ngl_glyph_library_create_surface (GskNglGlyphLibrary *self, + int stride, + int width, + int height, + double device_scale) +{ + cairo_surface_t *surface; + gsize n_bytes; + + g_assert (GSK_IS_NGL_GLYPH_LIBRARY (self)); + g_assert (width > 0); + g_assert (height > 0); + + n_bytes = stride * height; + + if G_LIKELY (n_bytes > self->surface_data_len) + { + self->surface_data = g_realloc (self->surface_data, n_bytes); + self->surface_data_len = n_bytes; + } + + memset (self->surface_data, 0, n_bytes); + surface = cairo_image_surface_create_for_data (self->surface_data, + CAIRO_FORMAT_ARGB32, + width, height, stride); + cairo_surface_set_device_scale (surface, device_scale, device_scale); + + return surface; +} + +static void +render_glyph (cairo_surface_t *surface, + const cairo_scaled_font_t *scaled_font, + const GskNglGlyphKey *key, + const GskNglGlyphValue *value) +{ + cairo_t *cr; + PangoGlyphString glyph_string; + PangoGlyphInfo glyph_info; + + g_assert (surface != NULL); + g_assert (scaled_font != NULL); + + cr = cairo_create (surface); + cairo_set_scaled_font (cr, scaled_font); + cairo_set_source_rgba (cr, 1, 1, 1, 1); + + glyph_info.glyph = key->glyph; + glyph_info.geometry.width = value->ink_rect.width * 1024; + if (glyph_info.glyph & PANGO_GLYPH_UNKNOWN_FLAG) + glyph_info.geometry.x_offset = 250 * key->xshift; + else + glyph_info.geometry.x_offset = 250 * key->xshift - value->ink_rect.x * 1024; + glyph_info.geometry.y_offset = 250 * key->yshift - value->ink_rect.y * 1024; + + glyph_string.num_glyphs = 1; + glyph_string.glyphs = &glyph_info; + + pango_cairo_show_glyph_string (cr, key->font, &glyph_string); + cairo_destroy (cr); + + cairo_surface_flush (surface); +} + +static void +gsk_ngl_glyph_library_upload_glyph (GskNglGlyphLibrary *self, + const GskNglGlyphKey *key, + const GskNglGlyphValue *value, + int width, + int height, + double device_scale) +{ + G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME; + cairo_scaled_font_t *scaled_font; + GskNglTextureAtlas *atlas; + cairo_surface_t *surface; + guchar *pixel_data; + guchar *free_data = NULL; + guint gl_format; + guint gl_type; + guint texture_id; + gsize stride; + int x, y; + + g_assert (GSK_IS_NGL_GLYPH_LIBRARY (self)); + g_assert (key != NULL); + g_assert (value != NULL); + + scaled_font = pango_cairo_font_get_scaled_font ((PangoCairoFont *)key->font); + if G_UNLIKELY (scaled_font == NULL || + cairo_scaled_font_status (scaled_font) != CAIRO_STATUS_SUCCESS) + return; + + stride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, width); + atlas = value->entry.is_atlased ? value->entry.atlas : NULL; + + gdk_gl_context_push_debug_group_printf (gdk_gl_context_get_current (), + "Uploading glyph %d", + key->glyph); + + surface = gsk_ngl_glyph_library_create_surface (self, stride, width, height, device_scale); + render_glyph (surface, scaled_font, key, value); + + texture_id = GSK_NGL_TEXTURE_ATLAS_ENTRY_TEXTURE (value); + + g_assert (texture_id > 0); + + glPixelStorei (GL_UNPACK_ROW_LENGTH, stride / 4); + glBindTexture (GL_TEXTURE_2D, texture_id); + + if G_UNLIKELY (gdk_gl_context_get_use_es (gdk_gl_context_get_current ())) + { + pixel_data = free_data = g_malloc (width * height * 4); + gdk_memory_convert (pixel_data, + width * 4, + GDK_MEMORY_R8G8B8A8_PREMULTIPLIED, + cairo_image_surface_get_data (surface), + width * 4, + GDK_MEMORY_DEFAULT, + width, height); + gl_format = GL_RGBA; + gl_type = GL_UNSIGNED_BYTE; + } + else + { + pixel_data = cairo_image_surface_get_data (surface); + gl_format = GL_BGRA; + gl_type = GL_UNSIGNED_INT_8_8_8_8_REV; + } + + if G_LIKELY (atlas != NULL) + { + x = atlas->width * value->entry.area.x; + y = atlas->width * value->entry.area.y; + } + else + { + x = 0; + y = 0; + } + + glTexSubImage2D (GL_TEXTURE_2D, 0, x, y, width, height, + gl_format, gl_type, pixel_data); + glPixelStorei (GL_UNPACK_ROW_LENGTH, 0); + + cairo_surface_destroy (surface); + g_free (free_data); + + gdk_gl_context_pop_debug_group (gdk_gl_context_get_current ()); + + GSK_NGL_TEXTURE_LIBRARY (self)->driver->command_queue->n_uploads++; + + if (gdk_profiler_is_running ()) + { + char message[64]; + g_snprintf (message, sizeof message, "Size %dx%d", width, height); + gdk_profiler_add_mark (start_time, GDK_PROFILER_CURRENT_TIME-start_time, "Upload Glyph", message); + } +} + +gboolean +gsk_ngl_glyph_library_add (GskNglGlyphLibrary *self, + GskNglGlyphKey *key, + const GskNglGlyphValue **out_value) +{ + PangoRectangle ink_rect; + GskNglGlyphValue *value; + int width; + int height; + guint packed_x; + guint packed_y; + + g_assert (GSK_IS_NGL_GLYPH_LIBRARY (self)); + g_assert (key != NULL); + g_assert (out_value != NULL); + + pango_font_get_glyph_extents (key->font, key->glyph, &ink_rect, NULL); + pango_extents_to_pixels (&ink_rect, NULL); + + if (key->xshift != 0) + ink_rect.width++; + if (key->yshift != 0) + ink_rect.height++; + + width = ink_rect.width * key->scale / 1024; + height = ink_rect.height * key->scale / 1024; + + value = gsk_ngl_texture_library_pack (GSK_NGL_TEXTURE_LIBRARY (self), + key, + sizeof *value, + width, + height, + 0, + &packed_x, &packed_y); + + memcpy (&value->ink_rect, &ink_rect, sizeof ink_rect); + + if (key->scale > 0 && width > 0 && height > 0) + gsk_ngl_glyph_library_upload_glyph (self, + key, + value, + width, + height, + key->scale / 1024.0); + + *out_value = value; + + return GSK_NGL_TEXTURE_ATLAS_ENTRY_TEXTURE (value) != 0; +} diff --git a/gsk/ngl/gsknglglyphlibraryprivate.h b/gsk/ngl/gsknglglyphlibraryprivate.h new file mode 100644 index 0000000000..9df9526f99 --- /dev/null +++ b/gsk/ngl/gsknglglyphlibraryprivate.h @@ -0,0 +1,119 @@ +/* gsknglglyphlibraryprivate.h + * + * 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 + */ + +#ifndef __GSK_NGL_GLYPH_LIBRARY_PRIVATE_H__ +#define __GSK_NGL_GLYPH_LIBRARY_PRIVATE_H__ + +#include + +#include "gskngltexturelibraryprivate.h" + +G_BEGIN_DECLS + +#define GSK_TYPE_GL_GLYPH_LIBRARY (gsk_ngl_glyph_library_get_type()) + +typedef struct _GskNglGlyphKey +{ + PangoFont *font; + PangoGlyph glyph; + guint xshift : 3; + guint yshift : 3; + guint scale : 26; /* times 1024 */ +} GskNglGlyphKey; + +typedef struct _GskNglGlyphValue +{ + GskNglTextureAtlasEntry entry; + PangoRectangle ink_rect; +} GskNglGlyphValue; + +#if GLIB_SIZEOF_VOID_P == 8 +G_STATIC_ASSERT (sizeof (GskNglGlyphKey) == 16); +#elif GLIB_SIZEOF_VOID_P == 4 +G_STATIC_ASSERT (sizeof (GskNglGlyphKey) == 12); +#endif + +G_DECLARE_FINAL_TYPE (GskNglGlyphLibrary, gsk_ngl_glyph_library, GSK, NGL_GLYPH_LIBRARY, GskNglTextureLibrary) + +struct _GskNglGlyphLibrary +{ + GskNglTextureLibrary parent_instance; + GHashTable *hash_table; + guint8 *surface_data; + gsize surface_data_len; + struct { + GskNglGlyphKey key; + const GskNglGlyphValue *value; + } front[256]; +}; + +GskNglGlyphLibrary *gsk_ngl_glyph_library_new (GskNglDriver *driver); +gboolean gsk_ngl_glyph_library_add (GskNglGlyphLibrary *self, + GskNglGlyphKey *key, + const GskNglGlyphValue **out_value); + +static inline int +gsk_ngl_glyph_key_phase (float value) +{ + return floor (4 * (value + 0.125)) - 4 * floor (value + 0.125); +} + +static inline void +gsk_ngl_glyph_key_set_glyph_and_shift (GskNglGlyphKey *key, + PangoGlyph glyph, + float x, + float y) +{ + key->glyph = glyph; + key->xshift = gsk_ngl_glyph_key_phase (x); + key->yshift = gsk_ngl_glyph_key_phase (y); +} + +static inline gboolean +gsk_ngl_glyph_library_lookup_or_add (GskNglGlyphLibrary *self, + const GskNglGlyphKey *key, + const GskNglGlyphValue **out_value) +{ + GskNglTextureAtlasEntry *entry; + guint front_index = key->glyph & 0xFF; + + if (memcmp (key, &self->front[front_index], sizeof *key) == 0) + { + *out_value = self->front[front_index].value; + } + else if (gsk_ngl_texture_library_lookup ((GskNglTextureLibrary *)self, key, &entry)) + { + *out_value = (GskNglGlyphValue *)entry; + self->front[front_index].key = *key; + self->front[front_index].value = *out_value; + } + else + { + GskNglGlyphKey *k = g_slice_copy (sizeof *key, key); + g_object_ref (k->font); + gsk_ngl_glyph_library_add (self, k, out_value); + } + + return GSK_NGL_TEXTURE_ATLAS_ENTRY_TEXTURE (*out_value) != 0; +} + +G_END_DECLS + +#endif /* __GSK_NGL_GLYPH_LIBRARY_PRIVATE_H__ */ diff --git a/gsk/ngl/gskngliconlibrary.c b/gsk/ngl/gskngliconlibrary.c new file mode 100644 index 0000000000..4d84cb2354 --- /dev/null +++ b/gsk/ngl/gskngliconlibrary.c @@ -0,0 +1,213 @@ +/* gskngliconlibrary.c + * + * 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 "gsknglcommandqueueprivate.h" +#include "gskngldriverprivate.h" +#include "gskngliconlibraryprivate.h" + +struct _GskNglIconLibrary +{ + GskNglTextureLibrary parent_instance; +}; + +G_DEFINE_TYPE (GskNglIconLibrary, gsk_ngl_icon_library, GSK_TYPE_GL_TEXTURE_LIBRARY) + +GskNglIconLibrary * +gsk_ngl_icon_library_new (GskNglDriver *driver) +{ + g_return_val_if_fail (GSK_IS_NGL_DRIVER (driver), NULL); + + return g_object_new (GSK_TYPE_GL_ICON_LIBRARY, + "driver", driver, + NULL); +} + +static void +gsk_ngl_icon_data_free (gpointer data) +{ + GskNglIconData *icon_data = data; + + g_clear_object (&icon_data->source_texture); + g_slice_free (GskNglIconData, icon_data); +} + +static void +gsk_ngl_icon_library_class_init (GskNglIconLibraryClass *klass) +{ +} + +static void +gsk_ngl_icon_library_init (GskNglIconLibrary *self) +{ + GSK_NGL_TEXTURE_LIBRARY (self)->max_entry_size = 128; + gsk_ngl_texture_library_set_funcs (GSK_NGL_TEXTURE_LIBRARY (self), + NULL, NULL, NULL, + gsk_ngl_icon_data_free); +} + +void +gsk_ngl_icon_library_add (GskNglIconLibrary *self, + GdkTexture *key, + const GskNglIconData **out_value) +{ + G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME; + cairo_surface_t *surface; + GskNglIconData *icon_data; + guint8 *pixel_data; + guint8 *surface_data; + guint8 *free_data = NULL; + guint gl_format; + guint gl_type; + guint packed_x; + guint packed_y; + int width; + int height; + guint texture_id; + + g_assert (GSK_IS_NGL_ICON_LIBRARY (self)); + g_assert (GDK_IS_TEXTURE (key)); + g_assert (out_value != NULL); + + width = key->width; + height = key->height; + + icon_data = gsk_ngl_texture_library_pack (GSK_NGL_TEXTURE_LIBRARY (self), + key, + sizeof (GskNglIconData), + width, height, 1, + &packed_x, &packed_y); + icon_data->source_texture = g_object_ref (key); + + /* actually upload the texture */ + surface = gdk_texture_download_surface (key); + surface_data = cairo_image_surface_get_data (surface); + gdk_gl_context_push_debug_group_printf (gdk_gl_context_get_current (), + "Uploading texture"); + + if (gdk_gl_context_get_use_es (gdk_gl_context_get_current ())) + { + pixel_data = free_data = g_malloc (width * height * 4); + gdk_memory_convert (pixel_data, width * 4, + GDK_MEMORY_R8G8B8A8_PREMULTIPLIED, + surface_data, cairo_image_surface_get_stride (surface), + GDK_MEMORY_DEFAULT, width, height); + gl_format = GL_RGBA; + gl_type = GL_UNSIGNED_BYTE; + } + else + { + pixel_data = surface_data; + gl_format = GL_BGRA; + gl_type = GL_UNSIGNED_INT_8_8_8_8_REV; + } + + texture_id = GSK_NGL_TEXTURE_ATLAS_ENTRY_TEXTURE (icon_data); + + glBindTexture (GL_TEXTURE_2D, texture_id); + + glTexSubImage2D (GL_TEXTURE_2D, 0, + packed_x + 1, packed_y + 1, + width, height, + gl_format, gl_type, + pixel_data); + /* Padding top */ + glTexSubImage2D (GL_TEXTURE_2D, 0, + packed_x + 1, packed_y, + width, 1, + gl_format, gl_type, + pixel_data); + /* Padding left */ + glTexSubImage2D (GL_TEXTURE_2D, 0, + packed_x, packed_y + 1, + 1, height, + gl_format, gl_type, + pixel_data); + /* Padding top left */ + glTexSubImage2D (GL_TEXTURE_2D, 0, + packed_x, packed_y, + 1, 1, + gl_format, gl_type, + pixel_data); + + /* Padding right */ + glPixelStorei (GL_UNPACK_ROW_LENGTH, width); + glPixelStorei (GL_UNPACK_SKIP_PIXELS, width - 1); + glTexSubImage2D (GL_TEXTURE_2D, 0, + packed_x + width + 1, packed_y + 1, + 1, height, + gl_format, gl_type, + pixel_data); + /* Padding top right */ + glTexSubImage2D (GL_TEXTURE_2D, 0, + packed_x + width + 1, packed_y, + 1, 1, + gl_format, gl_type, + pixel_data); + /* Padding bottom */ + glPixelStorei (GL_UNPACK_SKIP_PIXELS, 0); + glPixelStorei (GL_UNPACK_ROW_LENGTH, 0); + glPixelStorei (GL_UNPACK_SKIP_ROWS, height - 1); + glTexSubImage2D (GL_TEXTURE_2D, 0, + packed_x + 1, packed_y + 1 + height, + width, 1, + gl_format, gl_type, + pixel_data); + /* Padding bottom left */ + glTexSubImage2D (GL_TEXTURE_2D, 0, + packed_x, packed_y + 1 + height, + 1, 1, + gl_format, gl_type, + pixel_data); + /* Padding bottom right */ + glPixelStorei (GL_UNPACK_ROW_LENGTH, width); + glPixelStorei (GL_UNPACK_SKIP_PIXELS, width - 1); + glTexSubImage2D (GL_TEXTURE_2D, 0, + packed_x + 1 + width, packed_y + 1 + height, + 1, 1, + gl_format, gl_type, + pixel_data); + /* Reset this */ + glPixelStorei (GL_UNPACK_SKIP_PIXELS, 0); + glPixelStorei (GL_UNPACK_ROW_LENGTH, 0); + glPixelStorei (GL_UNPACK_SKIP_ROWS, 0); + + gdk_gl_context_pop_debug_group (gdk_gl_context_get_current ()); + + *out_value = icon_data; + + cairo_surface_destroy (surface); + g_free (free_data); + + GSK_NGL_TEXTURE_LIBRARY (self)->driver->command_queue->n_uploads++; + + if (gdk_profiler_is_running ()) + { + char message[64]; + g_snprintf (message, sizeof message, "Size %dx%d", width, height); + gdk_profiler_add_mark (start_time, GDK_PROFILER_CURRENT_TIME-start_time, "Upload Icon", message); + } +} diff --git a/gsk/ngl/gskngliconlibraryprivate.h b/gsk/ngl/gskngliconlibraryprivate.h new file mode 100644 index 0000000000..5fd1cb273d --- /dev/null +++ b/gsk/ngl/gskngliconlibraryprivate.h @@ -0,0 +1,60 @@ +/* gskngliconlibraryprivate.h + * + * 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 + */ + +#ifndef __GSK_NGL_ICON_LIBRARY_PRIVATE_H__ +#define __GSK_NGL_ICON_LIBRARY_PRIVATE_H__ + +#include + +#include "gskngltexturelibraryprivate.h" + +G_BEGIN_DECLS + +#define GSK_TYPE_GL_ICON_LIBRARY (gsk_ngl_icon_library_get_type()) + +typedef struct _GskNglIconData +{ + GskNglTextureAtlasEntry entry; + GdkTexture *source_texture; +} GskNglIconData; + +G_DECLARE_FINAL_TYPE (GskNglIconLibrary, gsk_ngl_icon_library, GSK, NGL_ICON_LIBRARY, GskNglTextureLibrary) + +GskNglIconLibrary *gsk_ngl_icon_library_new (GskNglDriver *driver); +void gsk_ngl_icon_library_add (GskNglIconLibrary *self, + GdkTexture *key, + const GskNglIconData **out_value); + +static inline void +gsk_ngl_icon_library_lookup_or_add (GskNglIconLibrary *self, + GdkTexture *key, + const GskNglIconData **out_value) +{ + GskNglTextureAtlasEntry *entry; + + if G_LIKELY (gsk_ngl_texture_library_lookup ((GskNglTextureLibrary *)self, key, &entry)) + *out_value = (GskNglIconData *)entry; + else + gsk_ngl_icon_library_add (self, key, out_value); +} + +G_END_DECLS + +#endif /* __GSK_NGL_ICON_LIBRARY_PRIVATE_H__ */ diff --git a/gsk/ngl/gsknglprogram.c b/gsk/ngl/gsknglprogram.c new file mode 100644 index 0000000000..1343eb35a4 --- /dev/null +++ b/gsk/ngl/gsknglprogram.c @@ -0,0 +1,176 @@ +/* gsknglprogram.c + * + * 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 "gsknglcommandqueueprivate.h" +#include "gsknglprogramprivate.h" +#include "gskngluniformstateprivate.h" + +G_DEFINE_TYPE (GskNglProgram, gsk_ngl_program, G_TYPE_OBJECT) + +GskNglProgram * +gsk_ngl_program_new (GskNglDriver *driver, + const char *name, + int program_id) +{ + GskNglProgram *self; + + g_return_val_if_fail (GSK_IS_NGL_DRIVER (driver), NULL); + g_return_val_if_fail (program_id >= -1, NULL); + + self = g_object_new (GSK_TYPE_GL_PROGRAM, NULL); + self->id = program_id; + self->name = g_strdup (name); + self->driver = g_object_ref (driver); + self->n_uniforms = 0; + + return self; +} + +static void +gsk_ngl_program_finalize (GObject *object) +{ + GskNglProgram *self = (GskNglProgram *)object; + + if (self->id >= 0) + g_warning ("Leaking GLSL program %d (%s)", + self->id, + self->name ? self->name : ""); + + g_clear_pointer (&self->name, g_free); + g_clear_object (&self->driver); + + G_OBJECT_CLASS (gsk_ngl_program_parent_class)->finalize (object); +} + +static void +gsk_ngl_program_class_init (GskNglProgramClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gsk_ngl_program_finalize; +} + +static void +gsk_ngl_program_init (GskNglProgram *self) +{ + self->id = -1; + + for (guint i = 0; i < G_N_ELEMENTS (self->uniform_locations); i++) + self->uniform_locations[i] = -1; +} + +/** + * gsk_ngl_program_add_uniform: + * @self: a #GskNglProgram + * @name: the name of the uniform such as "u_source" + * @key: the identifier to use for the uniform + * + * This method will create a mapping between @key and the location + * of the uniform on the GPU. This simplifies calling code to not + * need to know where the uniform location is and only register it + * when creating the program. + * + * You might use this with an enum of all your uniforms for the + * program and then register each of them like: + * + * ``` + * gsk_ngl_program_add_uniform (program, "u_source", UNIFORM_SOURCE); + * ``` + * + * That allows you to set values for the program with something + * like the following: + * + * ``` + * gsk_ngl_program_set_uniform1i (program, UNIFORM_SOURCE, 1); + * ``` + * + * Returns: %TRUE if the uniform was found; otherwise %FALSE + */ +gboolean +gsk_ngl_program_add_uniform (GskNglProgram *self, + const char *name, + guint key) +{ + GLint location; + + g_return_val_if_fail (GSK_IS_NGL_PROGRAM (self), FALSE); + g_return_val_if_fail (name != NULL, FALSE); + g_return_val_if_fail (key < 1024, FALSE); + + if (-1 == (location = glGetUniformLocation (self->id, name))) + return FALSE; + + self->uniform_locations[key] = location; + + if (location >= self->n_uniforms) + self->n_uniforms = location + 1; + +#if 0 + g_print ("program [%d] %s uniform %s at location %d.\n", + self->id, self->name, name, location); +#endif + + return TRUE; +} + +/** + * gsk_ngl_program_delete: + * @self: a #GskNglProgram + * + * Deletes the GLSL program. + * + * You must call gsk_ngl_program_use() before and + * gsk_ngl_program_unuse() after this function. + */ +void +gsk_ngl_program_delete (GskNglProgram *self) +{ + g_return_if_fail (GSK_IS_NGL_PROGRAM (self)); + g_return_if_fail (self->driver->command_queue != NULL); + + gsk_ngl_command_queue_delete_program (self->driver->command_queue, self->id); + self->id = -1; +} + +/** + * gsk_ngl_program_uniforms_added: + * @self: a #GskNglProgram + * @has_attachments: if any uniform is for a bind/texture attachment + * + * This function should be called after all of the uniforms ahve + * been added with gsk_ngl_program_add_uniform(). + * + * This function will setup the uniform state so that the program + * has fast access to the data buffers without as many lookups at + * runtime for comparison data. + */ +void +gsk_ngl_program_uniforms_added (GskNglProgram *self, + gboolean has_attachments) +{ + g_return_if_fail (GSK_IS_NGL_PROGRAM (self)); + g_return_if_fail (self->uniforms == NULL); + + self->uniforms = self->driver->command_queue->uniforms; + self->program_info = gsk_ngl_uniform_state_get_program (self->uniforms, self->id, self->n_uniforms); + self->program_info->has_attachments = has_attachments; +} diff --git a/gsk/ngl/gsknglprogramprivate.h b/gsk/ngl/gsknglprogramprivate.h new file mode 100644 index 0000000000..47bb0e314f --- /dev/null +++ b/gsk/ngl/gsknglprogramprivate.h @@ -0,0 +1,273 @@ +/* gsknglprogramprivate.h + * + * 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 + */ + +#ifndef __GSK_NGL_PROGRAM_PRIVATE_H__ +#define __GSK_NGL_PROGRAM_PRIVATE_H__ + +#include "gskngltypesprivate.h" + +#include "gsknglcommandqueueprivate.h" +#include "gskngldriverprivate.h" + +G_BEGIN_DECLS + +#define GSK_TYPE_GL_PROGRAM (gsk_ngl_program_get_type()) + +G_DECLARE_FINAL_TYPE (GskNglProgram, gsk_ngl_program, GSK, NGL_PROGRAM, GObject) + +struct _GskNglProgram +{ + GObject parent_instance; + + int id; + char *name; + GskNglDriver *driver; + + /* In reality, this is the largest uniform position + * as returned after linking so that we can use direct + * indexes based on location. + */ + guint n_uniforms; + + /* Cached pointer to avoid lots of pointer chasing/lookups */ + GskNglUniformState *uniforms; + GskNglUniformProgram *program_info; + + /* For custom programs */ + int texture_locations[4]; + int args_locations[8]; + int size_location; + + /* Static array for key->location transforms */ + int uniform_locations[32]; +}; + +GskNglProgram *gsk_ngl_program_new (GskNglDriver *driver, + const char *name, + int program_id); +gboolean gsk_ngl_program_add_uniform (GskNglProgram *self, + const char *name, + guint key); +void gsk_ngl_program_uniforms_added (GskNglProgram *self, + gboolean has_attachments); +void gsk_ngl_program_delete (GskNglProgram *self); + +#define gsk_ngl_program_get_uniform_location(s,k) ((s)->uniform_locations[(k)]) + +static inline void +gsk_ngl_program_set_uniform1fv (GskNglProgram *self, + guint key, + guint stamp, + guint count, + const float *values) +{ + gsk_ngl_uniform_state_set1fv (self->uniforms, self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, count, values); +} + +static inline void +gsk_ngl_program_set_uniform2fv (GskNglProgram *self, + guint key, + guint stamp, + guint count, + const float *values) +{ + gsk_ngl_uniform_state_set2fv (self->uniforms, self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, count, values); +} + +static inline void +gsk_ngl_program_set_uniform4fv (GskNglProgram *self, + guint key, + guint stamp, + guint count, + const float *values) +{ + gsk_ngl_uniform_state_set4fv (self->uniforms, self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, count, values); +} + +static inline void +gsk_ngl_program_set_uniform_rounded_rect (GskNglProgram *self, + guint key, + guint stamp, + const GskRoundedRect *rounded_rect) +{ + gsk_ngl_uniform_state_set_rounded_rect (self->uniforms, self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, rounded_rect); +} + +static inline void +gsk_ngl_program_set_uniform1i (GskNglProgram *self, + guint key, + guint stamp, + int value0) +{ + gsk_ngl_uniform_state_set1i (self->uniforms, + self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, value0); +} + +static inline void +gsk_ngl_program_set_uniform2i (GskNglProgram *self, + guint key, + guint stamp, + int value0, + int value1) +{ + gsk_ngl_uniform_state_set2i (self->uniforms, + self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, value0, value1); +} + +static inline void +gsk_ngl_program_set_uniform3i (GskNglProgram *self, + guint key, + guint stamp, + int value0, + int value1, + int value2) +{ + gsk_ngl_uniform_state_set3i (self->uniforms, + self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, value0, value1, value2); +} + +static inline void +gsk_ngl_program_set_uniform4i (GskNglProgram *self, + guint key, + guint stamp, + int value0, + int value1, + int value2, + int value3) +{ + gsk_ngl_uniform_state_set4i (self->uniforms, + self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, value0, value1, value2, value3); +} + +static inline void +gsk_ngl_program_set_uniform1f (GskNglProgram *self, + guint key, + guint stamp, + float value0) +{ + gsk_ngl_uniform_state_set1f (self->uniforms, + self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, value0); +} + +static inline void +gsk_ngl_program_set_uniform2f (GskNglProgram *self, + guint key, + guint stamp, + float value0, + float value1) +{ + gsk_ngl_uniform_state_set2f (self->uniforms, + self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, value0, value1); +} + +static inline void +gsk_ngl_program_set_uniform3f (GskNglProgram *self, + guint key, + guint stamp, + float value0, + float value1, + float value2) +{ + gsk_ngl_uniform_state_set3f (self->uniforms, + self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, value0, value1, value2); +} + +static inline void +gsk_ngl_program_set_uniform4f (GskNglProgram *self, + guint key, + guint stamp, + float value0, + float value1, + float value2, + float value3) +{ + gsk_ngl_uniform_state_set4f (self->uniforms, + self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, value0, value1, value2, value3); +} + +static inline void +gsk_ngl_program_set_uniform_color (GskNglProgram *self, + guint key, + guint stamp, + const GdkRGBA *color) +{ + gsk_ngl_uniform_state_set_color (self->uniforms, + self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, color); +} + +static inline void +gsk_ngl_program_set_uniform_texture (GskNglProgram *self, + guint key, + guint stamp, + GLenum texture_target, + GLenum texture_slot, + guint texture_id) +{ + gsk_ngl_attachment_state_bind_texture (self->driver->command_queue->attachments, + texture_target, + texture_slot, + texture_id); + gsk_ngl_uniform_state_set_texture (self->uniforms, + self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, texture_slot); +} + +static inline void +gsk_ngl_program_set_uniform_matrix (GskNglProgram *self, + guint key, + guint stamp, + const graphene_matrix_t *matrix) +{ + gsk_ngl_uniform_state_set_matrix (self->uniforms, + self->program_info, + gsk_ngl_program_get_uniform_location (self, key), + stamp, matrix); +} + +G_END_DECLS + +#endif /* __GSK_NGL_PROGRAM_PRIVATE_H__ */ diff --git a/gsk/ngl/gsknglprograms.defs b/gsk/ngl/gsknglprograms.defs new file mode 100644 index 0000000000..b0b797f0e9 --- /dev/null +++ b/gsk/ngl/gsknglprograms.defs @@ -0,0 +1,83 @@ +GSK_NGL_DEFINE_PROGRAM (blend, + "/org/gtk/libgsk/glsl/blend.glsl", + GSK_NGL_ADD_UNIFORM (1, BLEND_SOURCE2, u_source2) + GSK_NGL_ADD_UNIFORM (2, BLEND_MODE, u_mode)) + +GSK_NGL_DEFINE_PROGRAM (blit, + "/org/gtk/libgsk/glsl/blit.glsl", + GSK_NGL_NO_UNIFORMS) + +GSK_NGL_DEFINE_PROGRAM (blur, + "/org/gtk/libgsk/glsl/blur.glsl", + GSK_NGL_ADD_UNIFORM (1, BLUR_RADIUS, u_blur_radius) + GSK_NGL_ADD_UNIFORM (2, BLUR_SIZE, u_blur_size) + GSK_NGL_ADD_UNIFORM (3, BLUR_DIR, u_blur_dir)) + +GSK_NGL_DEFINE_PROGRAM (border, + "/org/gtk/libgsk/glsl/border.glsl", + GSK_NGL_ADD_UNIFORM (1, BORDER_COLOR, u_color) + GSK_NGL_ADD_UNIFORM (2, BORDER_WIDTHS, u_widths) + GSK_NGL_ADD_UNIFORM (3, BORDER_OUTLINE_RECT, u_outline_rect)) + +GSK_NGL_DEFINE_PROGRAM (color, + "/org/gtk/libgsk/glsl/color.glsl", + GSK_NGL_ADD_UNIFORM (1, COLOR_COLOR, u_color)) + +GSK_NGL_DEFINE_PROGRAM (coloring, + "/org/gtk/libgsk/glsl/coloring.glsl", + GSK_NGL_ADD_UNIFORM (1, COLORING_COLOR, u_color)) + +GSK_NGL_DEFINE_PROGRAM (color_matrix, + "/org/gtk/libgsk/glsl/color_matrix.glsl", + GSK_NGL_ADD_UNIFORM (1, COLOR_MATRIX_COLOR_MATRIX, u_color_matrix) + GSK_NGL_ADD_UNIFORM (2, COLOR_MATRIX_COLOR_OFFSET, u_color_offset)) + +GSK_NGL_DEFINE_PROGRAM (conic_gradient, + "/org/gtk/libgsk/glsl/conic_gradient.glsl", + GSK_NGL_ADD_UNIFORM (1, CONIC_GRADIENT_COLOR_STOPS, u_color_stops) + GSK_NGL_ADD_UNIFORM (2, CONIC_GRADIENT_NUM_COLOR_STOPS, u_num_color_stops) + GSK_NGL_ADD_UNIFORM (3, CONIC_GRADIENT_GEOMETRY, u_geometry)) + +GSK_NGL_DEFINE_PROGRAM (cross_fade, + "/org/gtk/libgsk/glsl/cross_fade.glsl", + GSK_NGL_ADD_UNIFORM (1, CROSS_FADE_PROGRESS, u_progress) + GSK_NGL_ADD_UNIFORM (2, CROSS_FADE_SOURCE2, u_source2)) + +GSK_NGL_DEFINE_PROGRAM (inset_shadow, + "/org/gtk/libgsk/glsl/inset_shadow.glsl", + GSK_NGL_ADD_UNIFORM (1, INSET_SHADOW_COLOR, u_color) + GSK_NGL_ADD_UNIFORM (2, INSET_SHADOW_SPREAD, u_spread) + GSK_NGL_ADD_UNIFORM (3, INSET_SHADOW_OFFSET, u_offset) + GSK_NGL_ADD_UNIFORM (4, INSET_SHADOW_OUTLINE_RECT, u_outline_rect)) + +GSK_NGL_DEFINE_PROGRAM (linear_gradient, + "/org/gtk/libgsk/glsl/linear_gradient.glsl", + GSK_NGL_ADD_UNIFORM (1, LINEAR_GRADIENT_COLOR_STOPS, u_color_stops) + GSK_NGL_ADD_UNIFORM (2, LINEAR_GRADIENT_NUM_COLOR_STOPS, u_num_color_stops) + GSK_NGL_ADD_UNIFORM (3, LINEAR_GRADIENT_POINTS, u_points) + GSK_NGL_ADD_UNIFORM (4, LINEAR_GRADIENT_REPEAT, u_repeat)) + +GSK_NGL_DEFINE_PROGRAM (outset_shadow, + "/org/gtk/libgsk/glsl/outset_shadow.glsl", + GSK_NGL_ADD_UNIFORM (1, OUTSET_SHADOW_COLOR, u_color) + GSK_NGL_ADD_UNIFORM (2, OUTSET_SHADOW_OUTLINE_RECT, u_outline_rect)) + +GSK_NGL_DEFINE_PROGRAM (radial_gradient, + "/org/gtk/libgsk/glsl/radial_gradient.glsl", + GSK_NGL_ADD_UNIFORM (1, RADIAL_GRADIENT_COLOR_STOPS, u_color_stops) + GSK_NGL_ADD_UNIFORM (2, RADIAL_GRADIENT_NUM_COLOR_STOPS, u_num_color_stops) + GSK_NGL_ADD_UNIFORM (3, RADIAL_GRADIENT_REPEAT, u_repeat) + GSK_NGL_ADD_UNIFORM (4, RADIAL_GRADIENT_RANGE, u_range) + GSK_NGL_ADD_UNIFORM (5, RADIAL_GRADIENT_GEOMETRY, u_geometry)) + +GSK_NGL_DEFINE_PROGRAM (repeat, + "/org/gtk/libgsk/glsl/repeat.glsl", + GSK_NGL_ADD_UNIFORM (1, REPEAT_CHILD_BOUNDS, u_child_bounds) + GSK_NGL_ADD_UNIFORM (2, REPEAT_TEXTURE_RECT, u_texture_rect)) + +GSK_NGL_DEFINE_PROGRAM (unblurred_outset_shadow, + "/org/gtk/libgsk/glsl/unblurred_outset_shadow.glsl", + GSK_NGL_ADD_UNIFORM (1, UNBLURRED_OUTSET_SHADOW_COLOR, u_color) + GSK_NGL_ADD_UNIFORM (2, UNBLURRED_OUTSET_SHADOW_SPREAD, u_spread) + GSK_NGL_ADD_UNIFORM (3, UNBLURRED_OUTSET_SHADOW_OFFSET, u_offset) + GSK_NGL_ADD_UNIFORM (4, UNBLURRED_OUTSET_SHADOW_OUTLINE_RECT, u_outline_rect)) diff --git a/gsk/ngl/gsknglrenderer.c b/gsk/ngl/gsknglrenderer.c new file mode 100644 index 0000000000..c20660f09e --- /dev/null +++ b/gsk/ngl/gsknglrenderer.c @@ -0,0 +1,312 @@ +/* gsknglrenderer.c + * + * Copyright 2020 Christian Hergert + * + * This file 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 file 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 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 "gsknglcommandqueueprivate.h" +#include "gskngldriverprivate.h" +#include "gsknglprogramprivate.h" +#include "gsknglrenderjobprivate.h" +#include "gsknglrendererprivate.h" + +struct _GskNglRendererClass +{ + GskRendererClass parent_class; +}; + +struct _GskNglRenderer +{ + GskRenderer parent_instance; + + /* This context is used to swap buffers when we are rendering directly + * to a GDK surface. It is also used to locate the shared driver for + * the display that we use to drive the command queue. + */ + GdkGLContext *context; + + /* Our command queue is private to this renderer and talks to the GL + * context for our target surface. This ensure that framebuffer 0 matches + * the surface we care about. Since the context is shared with other + * contexts from other renderers on the display, texture atlases, + * programs, and other objects are available to them all. + */ + GskNglCommandQueue *command_queue; + + /* The driver manages our program state and command queues. It also + * deals with caching textures, shaders, shadows, glyph, and icon + * caches through various helpers. + */ + GskNglDriver *driver; +}; + +G_DEFINE_TYPE (GskNglRenderer, gsk_ngl_renderer, GSK_TYPE_RENDERER) + +GskRenderer * +gsk_ngl_renderer_new (void) +{ + return g_object_new (GSK_TYPE_NGL_RENDERER, NULL); +} + +static gboolean +gsk_ngl_renderer_realize (GskRenderer *renderer, + GdkSurface *surface, + GError **error) +{ + G_GNUC_UNUSED gint64 start_time = GDK_PROFILER_CURRENT_TIME; + GskNglRenderer *self = (GskNglRenderer *)renderer; + GdkGLContext *context = NULL; + GdkGLContext *shared_context; + GskNglDriver *driver = NULL; + gboolean ret = FALSE; + gboolean debug_shaders = FALSE; + + g_assert (GSK_IS_NGL_RENDERER (self)); + g_assert (GDK_IS_SURFACE (surface)); + + if (self->context != NULL) + return TRUE; + + g_assert (self->driver == NULL); + g_assert (self->context == NULL); + g_assert (self->command_queue == NULL); + + if (!(context = gdk_surface_create_gl_context (surface, error)) || + !gdk_gl_context_realize (context, error)) + goto failure; + + if (!(shared_context = gdk_surface_get_shared_data_gl_context (surface))) + { + g_set_error (error, + GDK_GL_ERROR, + GDK_GL_ERROR_NOT_AVAILABLE, + "Failed to locate shared GL context for driver"); + goto failure; + } + +#ifdef G_ENABLE_DEBUG + if (GSK_RENDERER_DEBUG_CHECK (GSK_RENDERER (self), SHADERS)) + debug_shaders = TRUE; +#endif + + if (!(driver = gsk_ngl_driver_from_shared_context (shared_context, debug_shaders, error))) + goto failure; + + self->command_queue = gsk_ngl_driver_create_command_queue (driver, context); + self->context = g_steal_pointer (&context); + self->driver = g_steal_pointer (&driver); + + gsk_ngl_command_queue_set_profiler (self->command_queue, + gsk_renderer_get_profiler (renderer)); + + ret = TRUE; + +failure: + g_clear_object (&driver); + g_clear_object (&context); + + gdk_profiler_end_mark (start_time, "GskNglRenderer realize", NULL); + + return ret; +} + +static void +gsk_ngl_renderer_unrealize (GskRenderer *renderer) +{ + GskNglRenderer *self = (GskNglRenderer *)renderer; + + g_assert (GSK_IS_NGL_RENDERER (renderer)); + + g_clear_object (&self->driver); + g_clear_object (&self->context); + g_clear_object (&self->command_queue); +} + +static cairo_region_t * +get_render_region (GdkSurface *surface, + GdkGLContext *context) +{ + const cairo_region_t *damage; + GdkRectangle whole_surface; + GdkRectangle extents; + + g_assert (GDK_IS_SURFACE (surface)); + g_assert (GDK_IS_GL_CONTEXT (context)); + + whole_surface.x = 0; + whole_surface.y = 0; + whole_surface.width = gdk_surface_get_width (surface); + whole_surface.height = gdk_surface_get_height (surface); + + /* Damage does not have scale factor applied so we can compare it to + * @whole_surface which also doesn't have the scale factor applied. + */ + damage = gdk_draw_context_get_frame_region (GDK_DRAW_CONTEXT (context)); + + if (cairo_region_contains_rectangle (damage, &whole_surface) == CAIRO_REGION_OVERLAP_IN) + return NULL; + + /* If the extents match the full-scene, do the same as above */ + cairo_region_get_extents (damage, &extents); + if (gdk_rectangle_equal (&extents, &whole_surface)) + return NULL; + + /* Draw clipped to the bounding-box of the region. */ + return cairo_region_create_rectangle (&extents); +} + +static void +gsk_ngl_renderer_render (GskRenderer *renderer, + GskRenderNode *root, + const cairo_region_t *update_area) +{ + GskNglRenderer *self = (GskNglRenderer *)renderer; + cairo_region_t *render_region; + graphene_rect_t viewport; + GskNglRenderJob *job; + GdkSurface *surface; + float scale_factor; + + g_assert (GSK_IS_NGL_RENDERER (renderer)); + g_assert (root != NULL); + + surface = gdk_draw_context_get_surface (GDK_DRAW_CONTEXT (self->context)); + scale_factor = gdk_surface_get_scale_factor (surface); + + viewport.origin.x = 0; + viewport.origin.y = 0; + viewport.size.width = gdk_surface_get_width (surface) * scale_factor; + viewport.size.height = gdk_surface_get_height (surface) * scale_factor; + + gdk_gl_context_make_current (self->context); + gdk_draw_context_begin_frame (GDK_DRAW_CONTEXT (self->context), update_area); + + /* Must be called *AFTER* gdk_draw_context_begin_frame() */ + render_region = get_render_region (surface, self->context); + + gsk_ngl_driver_begin_frame (self->driver, self->command_queue); + job = gsk_ngl_render_job_new (self->driver, &viewport, scale_factor, render_region, 0); +#ifdef G_ENABLE_DEBUG + if (GSK_RENDERER_DEBUG_CHECK (GSK_RENDERER (self), FALLBACK)) + gsk_ngl_render_job_set_debug_fallback (job, TRUE); +#endif + gsk_ngl_render_job_render (job, root); + gsk_ngl_driver_end_frame (self->driver); + gsk_ngl_render_job_free (job); + + gdk_gl_context_make_current (self->context); + gdk_draw_context_end_frame (GDK_DRAW_CONTEXT (self->context)); + + gsk_ngl_driver_after_frame (self->driver); + + cairo_region_destroy (render_region); +} + +static GdkTexture * +gsk_ngl_renderer_render_texture (GskRenderer *renderer, + GskRenderNode *root, + const graphene_rect_t *viewport) +{ + GskNglRenderer *self = (GskNglRenderer *)renderer; + GskNglRenderTarget *render_target; + GskNglRenderJob *job; + GdkTexture *texture = NULL; + guint texture_id; + int width; + int height; + + g_assert (GSK_IS_NGL_RENDERER (renderer)); + g_assert (root != NULL); + + width = ceilf (viewport->size.width); + height = ceilf (viewport->size.height); + + if (gsk_ngl_driver_create_render_target (self->driver, + width, height, + GL_NEAREST, GL_NEAREST, + &render_target)) + { + gsk_ngl_driver_begin_frame (self->driver, self->command_queue); + job = gsk_ngl_render_job_new (self->driver, viewport, 1, NULL, render_target->framebuffer_id); +#ifdef G_ENABLE_DEBUG + if (GSK_RENDERER_DEBUG_CHECK (GSK_RENDERER (self), FALLBACK)) + gsk_ngl_render_job_set_debug_fallback (job, TRUE); +#endif + gsk_ngl_render_job_render_flipped (job, root); + texture_id = gsk_ngl_driver_release_render_target (self->driver, render_target, FALSE); + texture = gsk_ngl_driver_create_gdk_texture (self->driver, texture_id); + gsk_ngl_driver_end_frame (self->driver); + gsk_ngl_render_job_free (job); + + gsk_ngl_driver_after_frame (self->driver); + } + + return g_steal_pointer (&texture); +} + +static void +gsk_ngl_renderer_dispose (GObject *object) +{ +#ifdef G_ENABLE_DEBUG + GskNglRenderer *self = (GskNglRenderer *)object; + + g_assert (self->driver == NULL); +#endif + + G_OBJECT_CLASS (gsk_ngl_renderer_parent_class)->dispose (object); +} + +static void +gsk_ngl_renderer_class_init (GskNglRendererClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GskRendererClass *renderer_class = GSK_RENDERER_CLASS (klass); + + object_class->dispose = gsk_ngl_renderer_dispose; + + renderer_class->realize = gsk_ngl_renderer_realize; + renderer_class->unrealize = gsk_ngl_renderer_unrealize; + renderer_class->render = gsk_ngl_renderer_render; + renderer_class->render_texture = gsk_ngl_renderer_render_texture; +} + +static void +gsk_ngl_renderer_init (GskNglRenderer *self) +{ +} + +gboolean +gsk_ngl_renderer_try_compile_gl_shader (GskNglRenderer *renderer, + GskGLShader *shader, + GError **error) +{ + GskNglProgram *program; + + g_return_val_if_fail (GSK_IS_NGL_RENDERER (renderer), FALSE); + g_return_val_if_fail (shader != NULL, FALSE); + + program = gsk_ngl_driver_lookup_shader (renderer->driver, shader, error); + + return program != NULL; +} diff --git a/gsk/ngl/gsknglrenderer.h b/gsk/ngl/gsknglrenderer.h new file mode 100644 index 0000000000..014c7755c8 --- /dev/null +++ b/gsk/ngl/gsknglrenderer.h @@ -0,0 +1,46 @@ +/* gsknglrenderer.h + * + * 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 + */ + +#ifndef __GSK_NGL_RENDERER_H__ +#define __GSK_NGL_RENDERER_H__ + +#include + +G_BEGIN_DECLS + +#define GSK_TYPE_NGL_RENDERER (gsk_ngl_renderer_get_type()) + +#define GSK_NGL_RENDERER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GSK_TYPE_NGL_RENDERER, GskNglRenderer)) +#define GSK_IS_NGL_RENDERER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GSK_TYPE_NGL_RENDERER)) +#define GSK_NGL_RENDERER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GSK_TYPE_NGL_RENDERER, GskNglRendererClass)) +#define GSK_IS_NGL_RENDERER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GSK_TYPE_NGL_RENDERER)) +#define GSK_NGL_RENDERER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GSK_TYPE_NGL_RENDERER, GskNglRendererClass)) + +typedef struct _GskNglRenderer GskNglRenderer; +typedef struct _GskNglRendererClass GskNglRendererClass; + +GDK_AVAILABLE_IN_ALL +GType gsk_ngl_renderer_get_type (void) G_GNUC_CONST; +GDK_AVAILABLE_IN_ALL +GskRenderer *gsk_ngl_renderer_new (void); + +G_END_DECLS + +#endif /* __GSK_NGL_RENDERER__ */ diff --git a/gsk/ngl/gsknglrendererprivate.h b/gsk/ngl/gsknglrendererprivate.h new file mode 100644 index 0000000000..a0a861f0aa --- /dev/null +++ b/gsk/ngl/gsknglrendererprivate.h @@ -0,0 +1,34 @@ +/* gsknglrendererprivate.h + * + * Copyright 2021 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 + */ + +#ifndef __GSK_NGL_RENDERER_PRIVATE_H__ +#define __GSK_NGL_RENDERER_PRIVATE_H__ + +#include "gsknglrenderer.h" + +G_BEGIN_DECLS + +gboolean gsk_ngl_renderer_try_compile_gl_shader (GskNglRenderer *renderer, + GskGLShader *shader, + GError **error); + +G_END_DECLS + +#endif /* __GSK_NGL_RENDERER_PRIVATE_H__ */ diff --git a/gsk/ngl/gsknglrenderjob.c b/gsk/ngl/gsknglrenderjob.c new file mode 100644 index 0000000000..b8f983f862 --- /dev/null +++ b/gsk/ngl/gsknglrenderjob.c @@ -0,0 +1,3760 @@ +/* gsknglrenderjob.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 +#include +#include + +#include "gsknglcommandqueueprivate.h" +#include "gskngldriverprivate.h" +#include "gsknglglyphlibraryprivate.h" +#include "gskngliconlibraryprivate.h" +#include "gsknglprogramprivate.h" +#include "gsknglrenderjobprivate.h" +#include "gsknglshadowlibraryprivate.h" + +#include "ninesliceprivate.h" + +#define ORTHO_NEAR_PLANE -10000 +#define ORTHO_FAR_PLANE 10000 +#define MAX_GRADIENT_STOPS 6 +#define SHADOW_EXTRA_SIZE 4 + +/* Make sure gradient stops fits in packed array_count */ +G_STATIC_ASSERT ((MAX_GRADIENT_STOPS * 5) < (1 << GSK_NGL_UNIFORM_ARRAY_BITS)); + +#define rounded_rect_top_left(r) \ + (GRAPHENE_RECT_INIT(r->bounds.origin.x, \ + r->bounds.origin.y, \ + r->corner[0].width, r->corner[0].height)) +#define rounded_rect_top_right(r) \ + (GRAPHENE_RECT_INIT(r->bounds.origin.x + r->bounds.size.width - r->corner[1].width, \ + r->bounds.origin.y, \ + r->corner[1].width, r->corner[1].height)) +#define rounded_rect_bottom_right(r) \ + (GRAPHENE_RECT_INIT(r->bounds.origin.x + r->bounds.size.width - r->corner[2].width, \ + r->bounds.origin.y + r->bounds.size.height - r->corner[2].height, \ + r->corner[2].width, r->corner[2].height)) +#define rounded_rect_bottom_left(r) \ + (GRAPHENE_RECT_INIT(r->bounds.origin.x, \ + r->bounds.origin.y + r->bounds.size.height - r->corner[2].height, \ + r->corner[3].width, r->corner[3].height)) +#define rounded_rect_corner0(r) rounded_rect_top_left(r) +#define rounded_rect_corner1(r) rounded_rect_top_right(r) +#define rounded_rect_corner2(r) rounded_rect_bottom_right(r) +#define rounded_rect_corner3(r) rounded_rect_bottom_left(r) +#define rounded_rect_corner(r, i) (rounded_rect_corner##i(r)) +#define ALPHA_IS_CLEAR(alpha) ((alpha) < ((float) 0x00ff / (float) 0xffff)) +#define RGBA_IS_CLEAR(rgba) ALPHA_IS_CLEAR((rgba)->alpha) + +typedef struct _GskNglRenderClip +{ + GskRoundedRect rect; + guint is_rectilinear : 1; +} GskNglRenderClip; + +typedef struct _GskNglRenderModelview +{ + GskTransform *transform; + float scale_x; + float scale_y; + float offset_x_before; + float offset_y_before; + graphene_matrix_t matrix; +} GskNglRenderModelview; + +struct _GskNglRenderJob +{ + /* The context containing the framebuffer we are drawing to. Generally this + * is the context of the surface but may be a shared context if rendering to + * an offscreen texture such as gsk_ngl_renderer_render_texture(). + */ + GdkGLContext *context; + + /* The driver to be used. This is shared among all the renderers on a given + * GdkDisplay and uses the shared GL context to send commands. + */ + GskNglDriver *driver; + + /* The command queue (which is just a faster pointer to the driver's + * command queue. + */ + GskNglCommandQueue *command_queue; + + /* The region that we are clipping. Normalized to a single rectangle region. */ + cairo_region_t *region; + + /* The framebuffer to draw to in the @context GL context. So 0 would be the + * default framebuffer of @context. This is important to note as many other + * operations could be done using objects shared from the command queues + * GL context. + */ + guint framebuffer; + + /* The viewport we are using. This state is updated as we process render + * nodes in the specific visitor callbacks. + */ + graphene_rect_t viewport; + + /* The current projection, updated as we process nodes */ + graphene_matrix_t projection; + + /* An array of GskNglRenderModelview updated as nodes are processed. The + * current modelview is the last element. + */ + GArray *modelview; + + /* An array of GskNglRenderClip updated as nodes are processed. The + * current clip is the last element. + */ + GArray *clip; + + /* Our current alpha state as we process nodes */ + float alpha; + + /* Offset (delta x,y) as we process nodes. Occasionally this is merged into + * a transform that is referenced from child transform nodes. + */ + float offset_x; + float offset_y; + + /* The scale we are processing, possibly updated by transforms */ + float scale_x; + float scale_y; + + /* Cached pointers */ + const GskNglRenderClip *current_clip; + const GskNglRenderModelview *current_modelview; + + /* If we should be rendering red zones over fallback nodes */ + guint debug_fallback : 1; +}; + +typedef struct _GskNglRenderOffscreen +{ + const graphene_rect_t *bounds; + struct { + float x; + float y; + float x2; + float y2; + } area; + guint texture_id; + guint force_offscreen : 1; + guint reset_clip : 1; + guint do_not_cache : 1; + guint linear_filter : 1; + guint was_offscreen : 1; +} GskNglRenderOffscreen; + +static void gsk_ngl_render_job_visit_node (GskNglRenderJob *job, + const GskRenderNode *node); +static gboolean gsk_ngl_render_job_visit_node_with_offscreen (GskNglRenderJob *job, + const GskRenderNode *node, + GskNglRenderOffscreen *offscreen); + +static inline void +init_full_texture_region (GskNglRenderOffscreen *offscreen) +{ + offscreen->area.x = 0; + offscreen->area.y = 0; + offscreen->area.x2 = 1; + offscreen->area.y2 = 1; +} + +static inline gboolean G_GNUC_PURE +node_is_invisible (const GskRenderNode *node) +{ + return node->bounds.size.width == 0.0f || + node->bounds.size.height == 0.0f; +} + +static inline void +gsk_rounded_rect_shrink_to_minimum (GskRoundedRect *self) +{ + self->bounds.size.width = MAX (self->corner[0].width + self->corner[1].width, + self->corner[3].width + self->corner[2].width); + self->bounds.size.height = MAX (self->corner[0].height + self->corner[3].height, + self->corner[1].height + self->corner[2].height); +} + +static inline gboolean G_GNUC_PURE +node_supports_transform (const GskRenderNode *node) +{ + /* Some nodes can't handle non-trivial transforms without being + * rendered to a texture (e.g. rotated clips, etc.). Some however work + * just fine, mostly because they already draw their child to a + * texture and just render the texture manipulated in some way, think + * opacity or color matrix. + */ + + switch ((int)gsk_render_node_get_node_type (node)) + { + case GSK_COLOR_NODE: + case GSK_OPACITY_NODE: + case GSK_COLOR_MATRIX_NODE: + case GSK_TEXTURE_NODE: + case GSK_CROSS_FADE_NODE: + case GSK_LINEAR_GRADIENT_NODE: + case GSK_DEBUG_NODE: + case GSK_TEXT_NODE: + return TRUE; + + case GSK_TRANSFORM_NODE: + return node_supports_transform (gsk_transform_node_get_child (node)); + + default: + return FALSE; + } +} + +static inline gboolean G_GNUC_PURE +color_matrix_modifies_alpha (const GskRenderNode *node) +{ + const graphene_matrix_t *matrix = gsk_color_matrix_node_get_color_matrix (node); + const graphene_vec4_t *offset = gsk_color_matrix_node_get_color_offset (node); + graphene_vec4_t row3; + + if (graphene_vec4_get_w (offset) != 0.0f) + return TRUE; + + graphene_matrix_get_row (matrix, 3, &row3); + + return !graphene_vec4_equal (graphene_vec4_w_axis (), &row3); +} + +static inline gboolean G_GNUC_PURE +rect_contains_rect (const graphene_rect_t *r1, + const graphene_rect_t *r2) +{ + return r2->origin.x >= r1->origin.x && + (r2->origin.x + r2->size.width) <= (r1->origin.x + r1->size.width) && + r2->origin.y >= r1->origin.y && + (r2->origin.y + r2->size.height) <= (r1->origin.y + r1->size.height); +} + +static inline gboolean +rounded_inner_rect_contains_rect (const GskRoundedRect *rounded, + const graphene_rect_t *rect) +{ + const graphene_rect_t *rounded_bounds = &rounded->bounds; + graphene_rect_t inner; + float offset_x; + float offset_y; + + /* TODO: This is pretty conservative and we could go further, + * more fine-grained checks to avoid offscreen drawing. + */ + + offset_x = MAX (rounded->corner[GSK_CORNER_TOP_LEFT].width, + rounded->corner[GSK_CORNER_BOTTOM_LEFT].width); + offset_y = MAX (rounded->corner[GSK_CORNER_TOP_LEFT].height, + rounded->corner[GSK_CORNER_TOP_RIGHT].height); + + inner.origin.x = rounded_bounds->origin.x + offset_x; + inner.origin.y = rounded_bounds->origin.y + offset_y; + inner.size.width = rounded_bounds->size.width - offset_x - + MAX (rounded->corner[GSK_CORNER_TOP_RIGHT].width, + rounded->corner[GSK_CORNER_BOTTOM_RIGHT].width); + inner.size.height = rounded_bounds->size.height - offset_y - + MAX (rounded->corner[GSK_CORNER_BOTTOM_LEFT].height, + rounded->corner[GSK_CORNER_BOTTOM_RIGHT].height); + + return rect_contains_rect (&inner, rect); +} + +static inline gboolean G_GNUC_PURE +rect_intersects (const graphene_rect_t *r1, + const graphene_rect_t *r2) +{ + /* Assume both rects are already normalized, as they usually are */ + if (r1->origin.x > (r2->origin.x + r2->size.width) || + (r1->origin.x + r1->size.width) < r2->origin.x) + return FALSE; + else if (r1->origin.y > (r2->origin.y + r2->size.height) || + (r1->origin.y + r1->size.height) < r2->origin.y) + return FALSE; + else + return TRUE; +} + +static inline gboolean +rounded_rect_has_corner (const GskRoundedRect *r, + guint i) +{ + return r->corner[i].width > 0 && r->corner[i].height > 0; +} + +/* Current clip is NOT rounded but new one is definitely! */ +static inline gboolean +intersect_rounded_rectilinear (const graphene_rect_t *non_rounded, + const GskRoundedRect *rounded, + GskRoundedRect *result) +{ + gboolean corners[4]; + + /* Intersects with top left corner? */ + corners[0] = rounded_rect_has_corner (rounded, 0) && + rect_intersects (non_rounded, + &rounded_rect_corner (rounded, 0)); + /* top right? */ + corners[1] = rounded_rect_has_corner (rounded, 1) && + rect_intersects (non_rounded, + &rounded_rect_corner (rounded, 1)); + /* bottom right? */ + corners[2] = rounded_rect_has_corner (rounded, 2) && + rect_intersects (non_rounded, + &rounded_rect_corner (rounded, 2)); + /* bottom left */ + corners[3] = rounded_rect_has_corner (rounded, 3) && + rect_intersects (non_rounded, + &rounded_rect_corner (rounded, 3)); + + if (corners[0] && !rect_contains_rect (non_rounded, &rounded_rect_corner (rounded, 0))) + return FALSE; + if (corners[1] && !rect_contains_rect (non_rounded, &rounded_rect_corner (rounded, 1))) + return FALSE; + if (corners[2] && !rect_contains_rect (non_rounded, &rounded_rect_corner (rounded, 2))) + return FALSE; + if (corners[3] && !rect_contains_rect (non_rounded, &rounded_rect_corner (rounded, 3))) + return FALSE; + + /* We do intersect with at least one of the corners, but in such a way that the + * intersection between the two clips can still be represented by a single rounded + * rect in a trivial way. do that. */ + graphene_rect_intersection (non_rounded, &rounded->bounds, &result->bounds); + + for (guint i = 0; i < 4; i++) + { + if (corners[i]) + result->corner[i] = rounded->corner[i]; + else + result->corner[i].width = result->corner[i].height = 0; + } + + return TRUE; +} + +static inline void +init_projection_matrix (graphene_matrix_t *projection, + const graphene_rect_t *viewport) +{ + graphene_matrix_init_ortho (projection, + viewport->origin.x, + viewport->origin.x + viewport->size.width, + viewport->origin.y, + viewport->origin.y + viewport->size.height, + ORTHO_NEAR_PLANE, + ORTHO_FAR_PLANE); + graphene_matrix_scale (projection, 1, -1, 1); +} + +static inline float +gsk_ngl_render_job_set_alpha (GskNglRenderJob *job, + float alpha) +{ + if (job->alpha != alpha) + { + float ret = job->alpha; + job->alpha = alpha; + job->driver->stamps[UNIFORM_SHARED_ALPHA]++; + return ret; + } + + return alpha; +} + +static void +extract_matrix_metadata (GskNglRenderModelview *modelview) +{ + float dummy; + graphene_matrix_t m; + + gsk_transform_to_matrix (modelview->transform, &modelview->matrix); + + switch (gsk_transform_get_category (modelview->transform)) + { + case GSK_TRANSFORM_CATEGORY_IDENTITY: + case GSK_TRANSFORM_CATEGORY_2D_TRANSLATE: + modelview->scale_x = 1; + modelview->scale_y = 1; + break; + + case GSK_TRANSFORM_CATEGORY_2D_AFFINE: + gsk_transform_to_affine (modelview->transform, + &modelview->scale_x, &modelview->scale_y, + &dummy, &dummy); + break; + + case GSK_TRANSFORM_CATEGORY_UNKNOWN: + case GSK_TRANSFORM_CATEGORY_ANY: + case GSK_TRANSFORM_CATEGORY_3D: + case GSK_TRANSFORM_CATEGORY_2D: + { + graphene_vec3_t col1; + graphene_vec3_t col2; + + /* TODO: 90% sure this is incorrect. But we should never hit this code + * path anyway. */ + graphene_vec3_init (&col1, + graphene_matrix_get_value (&m, 0, 0), + graphene_matrix_get_value (&m, 1, 0), + graphene_matrix_get_value (&m, 2, 0)); + + graphene_vec3_init (&col2, + graphene_matrix_get_value (&m, 0, 1), + graphene_matrix_get_value (&m, 1, 1), + graphene_matrix_get_value (&m, 2, 1)); + + modelview->scale_x = graphene_vec3_length (&col1); + modelview->scale_y = graphene_vec3_length (&col2); + } + break; + + default: + break; + } +} + +static void +gsk_ngl_render_job_set_modelview (GskNglRenderJob *job, + GskTransform *transform) +{ + GskNglRenderModelview *modelview; + + g_assert (job != NULL); + g_assert (job->modelview != NULL); + + job->driver->stamps[UNIFORM_SHARED_MODELVIEW]++; + + g_array_set_size (job->modelview, job->modelview->len + 1); + + modelview = &g_array_index (job->modelview, + GskNglRenderModelview, + job->modelview->len - 1); + + modelview->transform = transform; + + modelview->offset_x_before = job->offset_x; + modelview->offset_y_before = job->offset_y; + + extract_matrix_metadata (modelview); + + job->offset_x = 0; + job->offset_y = 0; + job->scale_x = modelview->scale_x; + job->scale_y = modelview->scale_y; + + job->current_modelview = modelview; +} + +static void +gsk_ngl_render_job_push_modelview (GskNglRenderJob *job, + GskTransform *transform) +{ + GskNglRenderModelview *modelview; + + g_assert (job != NULL); + g_assert (job->modelview != NULL); + g_assert (transform != NULL); + + job->driver->stamps[UNIFORM_SHARED_MODELVIEW]++; + + g_array_set_size (job->modelview, job->modelview->len + 1); + + modelview = &g_array_index (job->modelview, + GskNglRenderModelview, + job->modelview->len - 1); + + if G_LIKELY (job->modelview->len > 1) + { + GskNglRenderModelview *last; + GskTransform *t = NULL; + + last = &g_array_index (job->modelview, + GskNglRenderModelview, + job->modelview->len - 2); + + /* Multiply given matrix with our previews modelview */ + t = gsk_transform_translate (gsk_transform_ref (last->transform), + &(graphene_point_t) { + job->offset_x, + job->offset_y + }); + t = gsk_transform_transform (t, transform); + modelview->transform = t; + } + else + { + modelview->transform = gsk_transform_ref (transform); + } + + modelview->offset_x_before = job->offset_x; + modelview->offset_y_before = job->offset_y; + + extract_matrix_metadata (modelview); + + job->offset_x = 0; + job->offset_y = 0; + job->scale_x = modelview->scale_x; + job->scale_y = modelview->scale_y; + + job->current_modelview = modelview; +} + +static void +gsk_ngl_render_job_pop_modelview (GskNglRenderJob *job) +{ + const GskNglRenderModelview *head; + + g_assert (job != NULL); + g_assert (job->modelview); + g_assert (job->modelview->len > 0); + + job->driver->stamps[UNIFORM_SHARED_MODELVIEW]++; + + head = job->current_modelview; + + job->offset_x = head->offset_x_before; + job->offset_y = head->offset_y_before; + + gsk_transform_unref (head->transform); + + job->modelview->len--; + + if (job->modelview->len >= 1) + { + head = &g_array_index (job->modelview, GskNglRenderModelview, job->modelview->len - 1); + + job->scale_x = head->scale_x; + job->scale_y = head->scale_y; + + job->current_modelview = head; + } + else + { + job->current_modelview = NULL; + } +} + +static void +gsk_ngl_render_job_push_clip (GskNglRenderJob *job, + const GskRoundedRect *rect) +{ + GskNglRenderClip *clip; + + g_assert (job != NULL); + g_assert (job->clip != NULL); + g_assert (rect != NULL); + + job->driver->stamps[UNIFORM_SHARED_CLIP_RECT]++; + + g_array_set_size (job->clip, job->clip->len + 1); + + clip = &g_array_index (job->clip, GskNglRenderClip, job->clip->len - 1); + memcpy (&clip->rect, rect, sizeof *rect); + clip->is_rectilinear = gsk_rounded_rect_is_rectilinear (rect); + + job->current_clip = clip; +} + +static void +gsk_ngl_render_job_pop_clip (GskNglRenderJob *job) +{ + g_assert (job != NULL); + g_assert (job->clip != NULL); + g_assert (job->clip->len > 0); + + job->driver->stamps[UNIFORM_SHARED_CLIP_RECT]++; + job->current_clip--; + job->clip->len--; +} + +static inline void +gsk_ngl_render_job_offset (GskNglRenderJob *job, + float offset_x, + float offset_y) +{ + if (offset_x || offset_y) + { + job->offset_x += offset_x; + job->offset_y += offset_y; + } +} + +static inline void +gsk_ngl_render_job_set_projection (GskNglRenderJob *job, + const graphene_matrix_t *projection) +{ + memcpy (&job->projection, projection, sizeof job->projection); + job->driver->stamps[UNIFORM_SHARED_PROJECTION]++; +} + +static inline void +gsk_ngl_render_job_set_projection_from_rect (GskNglRenderJob *job, + const graphene_rect_t *rect, + graphene_matrix_t *prev_projection) +{ + if (prev_projection) + memcpy (prev_projection, &job->projection, sizeof *prev_projection); + init_projection_matrix (&job->projection, rect); + job->driver->stamps[UNIFORM_SHARED_PROJECTION]++; +} + +static inline void +gsk_ngl_render_job_set_projection_for_size (GskNglRenderJob *job, + float width, + float height, + graphene_matrix_t *prev_projection) +{ + if (prev_projection) + memcpy (prev_projection, &job->projection, sizeof *prev_projection); + graphene_matrix_init_ortho (&job->projection, 0, width, 0, height, ORTHO_NEAR_PLANE, ORTHO_FAR_PLANE); + graphene_matrix_scale (&job->projection, 1, -1, 1); + job->driver->stamps[UNIFORM_SHARED_PROJECTION]++; +} + +static inline void +gsk_ngl_render_job_set_viewport (GskNglRenderJob *job, + const graphene_rect_t *viewport, + graphene_rect_t *prev_viewport) +{ + if (prev_viewport) + memcpy (prev_viewport, &job->viewport, sizeof *prev_viewport); + memcpy (&job->viewport, viewport, sizeof job->viewport); + job->driver->stamps[UNIFORM_SHARED_VIEWPORT]++; +} + +static inline void +gsk_ngl_render_job_set_viewport_for_size (GskNglRenderJob *job, + float width, + float height, + graphene_rect_t *prev_viewport) +{ + if (prev_viewport) + memcpy (prev_viewport, &job->viewport, sizeof *prev_viewport); + job->viewport.origin.x = 0; + job->viewport.origin.y = 0; + job->viewport.size.width = width; + job->viewport.size.height = height; + job->driver->stamps[UNIFORM_SHARED_VIEWPORT]++; +} + +static inline void +gsk_ngl_render_job_transform_bounds (GskNglRenderJob *job, + const graphene_rect_t *rect, + graphene_rect_t *out_rect) +{ + GskTransform *transform; + GskTransformCategory category; + + g_assert (job != NULL); + g_assert (job->modelview->len > 0); + g_assert (rect != NULL); + g_assert (out_rect != NULL); + + transform = job->current_modelview->transform; + category = gsk_transform_get_category (transform); + + /* Our most common transform is 2d-affine, so inline it. + * Both identity and 2d-translate are virtually unseen here. + */ + if G_LIKELY (category == GSK_TRANSFORM_CATEGORY_2D_AFFINE) + { + float dx, dy, scale_x, scale_y; + + gsk_transform_to_affine (transform, &scale_x, &scale_y, &dx, &dy); + + /* Init directly into out rect */ + out_rect->origin.x = ((rect->origin.x + job->offset_x) * scale_x) + dx; + out_rect->origin.y = ((rect->origin.y + job->offset_y) * scale_y) + dy; + out_rect->size.width = rect->size.width * scale_x; + out_rect->size.height = rect->size.height * scale_y; + + /* Normaize in place */ + if (out_rect->size.width < 0.f) + { + float size = fabsf (out_rect->size.width); + + out_rect->origin.x -= size; + out_rect->size.width = size; + } + + if (out_rect->size.height < 0.f) + { + float size = fabsf (out_rect->size.height); + + out_rect->origin.y -= size; + out_rect->size.height = size; + } + } + else + { + graphene_rect_t r; + + r.origin.x = rect->origin.x + job->offset_x; + r.origin.y = rect->origin.y + job->offset_y; + r.size.width = rect->size.width; + r.size.height = rect->size.height; + + gsk_transform_transform_bounds (transform, &r, out_rect); + } +} + +static inline void +gsk_ngl_render_job_transform_rounded_rect (GskNglRenderJob *job, + const GskRoundedRect *rect, + GskRoundedRect *out_rect) +{ + out_rect->bounds.origin.x = job->offset_x + rect->bounds.origin.x; + out_rect->bounds.origin.y = job->offset_y + rect->bounds.origin.y; + out_rect->bounds.size.width = rect->bounds.size.width; + out_rect->bounds.size.height = rect->bounds.size.height; + memcpy (out_rect->corner, rect->corner, sizeof rect->corner); +} + +static inline gboolean +gsk_ngl_render_job_node_overlaps_clip (GskNglRenderJob *job, + const GskRenderNode *node) +{ + graphene_rect_t transformed_bounds; + gsk_ngl_render_job_transform_bounds (job, &node->bounds, &transformed_bounds); + return rect_intersects (&job->current_clip->rect.bounds, &transformed_bounds); +} + +/* load_vertex_data_with_region */ +static inline void +gsk_ngl_render_job_load_vertices_from_offscreen (GskNglRenderJob *job, + const graphene_rect_t *bounds, + const GskNglRenderOffscreen *offscreen) +{ + GskNglDrawVertex *vertices = gsk_ngl_command_queue_add_vertices (job->command_queue); + float min_x = job->offset_x + bounds->origin.x; + float min_y = job->offset_y + bounds->origin.y; + float max_x = min_x + bounds->size.width; + float max_y = min_y + bounds->size.height; + float y1 = offscreen->was_offscreen ? offscreen->area.y2 : offscreen->area.y; + float y2 = offscreen->was_offscreen ? offscreen->area.y : offscreen->area.y2; + + vertices[0].position[0] = min_x; + vertices[0].position[1] = min_y; + vertices[0].uv[0] = offscreen->area.x; + vertices[0].uv[1] = y1; + + vertices[1].position[0] = min_x; + vertices[1].position[1] = max_y; + vertices[1].uv[0] = offscreen->area.x; + vertices[1].uv[1] = y2; + + vertices[2].position[0] = max_x; + vertices[2].position[1] = min_y; + vertices[2].uv[0] = offscreen->area.x2; + vertices[2].uv[1] = y1; + + vertices[3].position[0] = max_x; + vertices[3].position[1] = max_y; + vertices[3].uv[0] = offscreen->area.x2; + vertices[3].uv[1] = y2; + + vertices[4].position[0] = min_x; + vertices[4].position[1] = max_y; + vertices[4].uv[0] = offscreen->area.x; + vertices[4].uv[1] = y2; + + vertices[5].position[0] = max_x; + vertices[5].position[1] = min_y; + vertices[5].uv[0] = offscreen->area.x2; + vertices[5].uv[1] = y1; +} + +/* load_float_vertex_data */ +static inline void +gsk_ngl_render_job_draw (GskNglRenderJob *job, + float x, + float y, + float width, + float height) +{ + GskNglDrawVertex *vertices = gsk_ngl_command_queue_add_vertices (job->command_queue); + float min_x = job->offset_x + x; + float min_y = job->offset_y + y; + float max_x = min_x + width; + float max_y = min_y + height; + + vertices[0].position[0] = min_x; + vertices[0].position[1] = min_y; + vertices[0].uv[0] = 0; + vertices[0].uv[1] = 0; + + vertices[1].position[0] = min_x; + vertices[1].position[1] = max_y; + vertices[1].uv[0] = 0; + vertices[1].uv[1] = 1; + + vertices[2].position[0] = max_x; + vertices[2].position[1] = min_y; + vertices[2].uv[0] = 1; + vertices[2].uv[1] = 0; + + vertices[3].position[0] = max_x; + vertices[3].position[1] = max_y; + vertices[3].uv[0] = 1; + vertices[3].uv[1] = 1; + + vertices[4].position[0] = min_x; + vertices[4].position[1] = max_y; + vertices[4].uv[0] = 0; + vertices[4].uv[1] = 1; + + vertices[5].position[0] = max_x; + vertices[5].position[1] = min_y; + vertices[5].uv[0] = 1; + vertices[5].uv[1] = 0; +} + +/* load_vertex_data */ +static inline void +gsk_ngl_render_job_draw_rect (GskNglRenderJob *job, + const graphene_rect_t *bounds) +{ + gsk_ngl_render_job_draw (job, + bounds->origin.x, + bounds->origin.y, + bounds->size.width, + bounds->size.height); +} + +/* fill_vertex_data */ +static void +gsk_ngl_render_job_draw_coords (GskNglRenderJob *job, + float min_x, + float min_y, + float max_x, + float max_y) +{ + GskNglDrawVertex *vertices = gsk_ngl_command_queue_add_vertices (job->command_queue); + + vertices[0].position[0] = min_x; + vertices[0].position[1] = min_y; + vertices[0].uv[0] = 0; + vertices[0].uv[1] = 1; + + vertices[1].position[0] = min_x; + vertices[1].position[1] = max_y; + vertices[1].uv[0] = 0; + vertices[1].uv[1] = 0; + + vertices[2].position[0] = max_x; + vertices[2].position[1] = min_y; + vertices[2].uv[0] = 1; + vertices[2].uv[1] = 1; + + vertices[3].position[0] = max_x; + vertices[3].position[1] = max_y; + vertices[3].uv[0] = 1; + vertices[3].uv[1] = 0; + + vertices[4].position[0] = min_x; + vertices[4].position[1] = max_y; + vertices[4].uv[0] = 0; + vertices[4].uv[1] = 0; + + vertices[5].position[0] = max_x; + vertices[5].position[1] = min_y; + vertices[5].uv[0] = 1; + vertices[5].uv[1] = 1; +} + +/* load_offscreen_vertex_data */ +static inline void +gsk_ngl_render_job_draw_offscreen_rect (GskNglRenderJob *job, + const graphene_rect_t *bounds) +{ + float min_x = job->offset_x + bounds->origin.x; + float min_y = job->offset_y + bounds->origin.y; + float max_x = min_x + bounds->size.width; + float max_y = min_y + bounds->size.height; + + gsk_ngl_render_job_draw_coords (job, min_x, min_y, max_x, max_y); +} + +static inline void +gsk_ngl_render_job_begin_draw (GskNglRenderJob *job, + GskNglProgram *program) +{ + gsk_ngl_command_queue_begin_draw (job->command_queue, + program->program_info, + job->viewport.size.width, + job->viewport.size.height); + + if (program->uniform_locations[UNIFORM_SHARED_VIEWPORT] > -1) + gsk_ngl_uniform_state_set4fv (program->uniforms, + program->program_info, + program->uniform_locations[UNIFORM_SHARED_VIEWPORT], + job->driver->stamps[UNIFORM_SHARED_VIEWPORT], + 1, + (const float *)&job->viewport); + + if (program->uniform_locations[UNIFORM_SHARED_MODELVIEW] > -1) + gsk_ngl_uniform_state_set_matrix (program->uniforms, + program->program_info, + program->uniform_locations[UNIFORM_SHARED_MODELVIEW], + job->driver->stamps[UNIFORM_SHARED_MODELVIEW], + &job->current_modelview->matrix); + + if (program->uniform_locations[UNIFORM_SHARED_PROJECTION] > -1) + gsk_ngl_uniform_state_set_matrix (program->uniforms, + program->program_info, + program->uniform_locations[UNIFORM_SHARED_PROJECTION], + job->driver->stamps[UNIFORM_SHARED_PROJECTION], + &job->projection); + + if (program->uniform_locations[UNIFORM_SHARED_CLIP_RECT] > -1) + gsk_ngl_uniform_state_set_rounded_rect (program->uniforms, + program->program_info, + program->uniform_locations[UNIFORM_SHARED_CLIP_RECT], + job->driver->stamps[UNIFORM_SHARED_CLIP_RECT], + &job->current_clip->rect); + + if (program->uniform_locations[UNIFORM_SHARED_ALPHA] > -1) + gsk_ngl_uniform_state_set1f (program->uniforms, + program->program_info, + program->uniform_locations[UNIFORM_SHARED_ALPHA], + job->driver->stamps[UNIFORM_SHARED_ALPHA], + job->alpha); +} + +static inline void +gsk_ngl_render_job_split_draw (GskNglRenderJob *job) +{ + gsk_ngl_command_queue_split_draw (job->command_queue); +} + +static inline void +gsk_ngl_render_job_end_draw (GskNglRenderJob *job) +{ + gsk_ngl_command_queue_end_draw (job->command_queue); +} + +static inline void +gsk_ngl_render_job_visit_as_fallback (GskNglRenderJob *job, + const GskRenderNode *node) +{ + float scale_x = job->scale_x; + float scale_y = job->scale_y; + int surface_width = ceilf (node->bounds.size.width * scale_x); + int surface_height = ceilf (node->bounds.size.height * scale_y); + GdkTexture *texture; + cairo_surface_t *surface; + cairo_surface_t *rendered_surface; + cairo_t *cr; + int cached_id; + int texture_id; + GskTextureKey key; + + if (surface_width <= 0 || surface_height <= 0) + return; + + key.pointer = node; + key.pointer_is_child = FALSE; + key.scale_x = scale_x; + key.scale_y = scale_y; + key.filter = GL_NEAREST; + + cached_id = gsk_ngl_driver_lookup_texture (job->driver, &key); + + if (cached_id != 0) + { + gsk_ngl_render_job_begin_draw (job, job->driver->blit); + gsk_ngl_program_set_uniform_texture (job->driver->blit, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, GL_TEXTURE0, cached_id); + gsk_ngl_render_job_draw_offscreen_rect (job, &node->bounds); + gsk_ngl_render_job_end_draw (job); + return; + } + + /* We first draw the recording surface on an image surface, + * just because the scaleY(-1) later otherwise screws up the + * rendering... */ + { + rendered_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + surface_width, + surface_height); + + cairo_surface_set_device_scale (rendered_surface, scale_x, scale_y); + cr = cairo_create (rendered_surface); + + cairo_save (cr); + cairo_translate (cr, - floorf (node->bounds.origin.x), - floorf (node->bounds.origin.y)); + /* Render nodes don't modify state, so casting away the const is fine here */ + gsk_render_node_draw ((GskRenderNode *)node, cr); + cairo_restore (cr); + cairo_destroy (cr); + } + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + surface_width, + surface_height); + cairo_surface_set_device_scale (surface, scale_x, scale_y); + cr = cairo_create (surface); + + /* We draw upside down here, so it matches what GL does. */ + cairo_save (cr); + cairo_scale (cr, 1, -1); + cairo_translate (cr, 0, - surface_height / scale_y); + cairo_set_source_surface (cr, rendered_surface, 0, 0); + cairo_rectangle (cr, 0, 0, surface_width / scale_x, surface_height / scale_y); + cairo_fill (cr); + cairo_restore (cr); + +#ifdef G_ENABLE_DEBUG + if (job->debug_fallback) + { + cairo_move_to (cr, 0, 0); + cairo_rectangle (cr, 0, 0, node->bounds.size.width, node->bounds.size.height); + if (gsk_render_node_get_node_type (node) == GSK_CAIRO_NODE) + cairo_set_source_rgba (cr, 0.3, 0, 1, 0.25); + else + cairo_set_source_rgba (cr, 1, 0, 0, 0.25); + cairo_fill_preserve (cr); + if (gsk_render_node_get_node_type (node) == GSK_CAIRO_NODE) + cairo_set_source_rgba (cr, 0.3, 0, 1, 1); + else + cairo_set_source_rgba (cr, 1, 0, 0, 1); + cairo_stroke (cr); + } +#endif + cairo_destroy (cr); + + /* Create texture to upload */ + texture = gdk_texture_new_for_surface (surface); + texture_id = gsk_ngl_driver_load_texture (job->driver, texture, + GL_NEAREST, GL_NEAREST); + + if (gdk_gl_context_has_debug (job->command_queue->context)) + gdk_gl_context_label_object_printf (job->command_queue->context, GL_TEXTURE, texture_id, + "Fallback %s %d", + g_type_name_from_instance ((GTypeInstance *) node), + texture_id); + + g_object_unref (texture); + cairo_surface_destroy (surface); + cairo_surface_destroy (rendered_surface); + + gsk_ngl_driver_cache_texture (job->driver, &key, texture_id); + + gsk_ngl_render_job_begin_draw (job, job->driver->blit); + gsk_ngl_program_set_uniform_texture (job->driver->blit, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + texture_id); + gsk_ngl_render_job_draw_offscreen_rect (job, &node->bounds); + gsk_ngl_render_job_end_draw (job); +} + +static guint +blur_offscreen (GskNglRenderJob *job, + GskNglRenderOffscreen *offscreen, + int texture_to_blur_width, + int texture_to_blur_height, + float blur_radius_x, + float blur_radius_y) +{ + const GskRoundedRect new_clip = GSK_ROUNDED_RECT_INIT (0, 0, texture_to_blur_width, texture_to_blur_height); + GskNglRenderTarget *pass1; + GskNglRenderTarget *pass2; + graphene_matrix_t prev_projection; + graphene_rect_t prev_viewport; + guint prev_fbo; + + g_assert (blur_radius_x > 0); + g_assert (blur_radius_y > 0); + g_assert (offscreen->texture_id > 0); + g_assert (offscreen->area.x2 > offscreen->area.x); + g_assert (offscreen->area.y2 > offscreen->area.y); + + if (!gsk_ngl_driver_create_render_target (job->driver, + MAX (texture_to_blur_width, 1), + MAX (texture_to_blur_height, 1), + GL_NEAREST, GL_NEAREST, + &pass1)) + return 0; + + if (texture_to_blur_width <= 0 || texture_to_blur_height <= 0) + return gsk_ngl_driver_release_render_target (job->driver, pass1, FALSE); + + if (!gsk_ngl_driver_create_render_target (job->driver, + texture_to_blur_width, + texture_to_blur_height, + GL_NEAREST, GL_NEAREST, + &pass2)) + return gsk_ngl_driver_release_render_target (job->driver, pass1, FALSE); + + gsk_ngl_render_job_set_viewport (job, &new_clip.bounds, &prev_viewport); + gsk_ngl_render_job_set_projection_from_rect (job, &new_clip.bounds, &prev_projection); + gsk_ngl_render_job_set_modelview (job, NULL); + gsk_ngl_render_job_push_clip (job, &new_clip); + + /* Bind new framebuffer and clear it */ + prev_fbo = gsk_ngl_command_queue_bind_framebuffer (job->command_queue, pass1->framebuffer_id); + gsk_ngl_command_queue_clear (job->command_queue, 0, &job->viewport); + + /* Begin drawing the first horizontal pass, using offscreen as the + * source texture for the program. + */ + gsk_ngl_render_job_begin_draw (job, job->driver->blur); + gsk_ngl_program_set_uniform_texture (job->driver->blur, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + offscreen->texture_id); + gsk_ngl_program_set_uniform1f (job->driver->blur, + UNIFORM_BLUR_RADIUS, 0, + blur_radius_x); + gsk_ngl_program_set_uniform2f (job->driver->blur, + UNIFORM_BLUR_SIZE, 0, + texture_to_blur_width, + texture_to_blur_height); + gsk_ngl_program_set_uniform2f (job->driver->blur, + UNIFORM_BLUR_DIR, 0, + 1, 0); + gsk_ngl_render_job_draw_coords (job, 0, 0, texture_to_blur_width, texture_to_blur_height); + gsk_ngl_render_job_end_draw (job); + + /* Bind second pass framebuffer and clear it */ + gsk_ngl_command_queue_bind_framebuffer (job->command_queue, pass2->framebuffer_id); + gsk_ngl_command_queue_clear (job->command_queue, 0, &job->viewport); + + /* Draw using blur program with first pass as source texture */ + gsk_ngl_render_job_begin_draw (job, job->driver->blur); + gsk_ngl_program_set_uniform_texture (job->driver->blur, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + pass1->texture_id); + gsk_ngl_program_set_uniform1f (job->driver->blur, + UNIFORM_BLUR_RADIUS, 0, + blur_radius_y); + gsk_ngl_program_set_uniform2f (job->driver->blur, + UNIFORM_BLUR_SIZE, 0, + texture_to_blur_width, + texture_to_blur_height); + gsk_ngl_program_set_uniform2f (job->driver->blur, + UNIFORM_BLUR_DIR, 0, + 0, 1); + gsk_ngl_render_job_draw_coords (job, 0, 0, texture_to_blur_width, texture_to_blur_height); + gsk_ngl_render_job_end_draw (job); + + gsk_ngl_render_job_pop_modelview (job); + gsk_ngl_render_job_pop_clip (job); + gsk_ngl_render_job_set_viewport (job, &prev_viewport, NULL); + gsk_ngl_render_job_set_projection (job, &prev_projection); + gsk_ngl_command_queue_bind_framebuffer (job->command_queue, prev_fbo); + + gsk_ngl_driver_release_render_target (job->driver, pass1, TRUE); + + return gsk_ngl_driver_release_render_target (job->driver, pass2, FALSE); +} + +static void +blur_node (GskNglRenderJob *job, + GskNglRenderOffscreen *offscreen, + const GskRenderNode *node, + float blur_radius, + float *min_x, + float *max_x, + float *min_y, + float *max_y) +{ + const float blur_extra = blur_radius * 2.0; /* 2.0 = shader radius_multiplier */ + const float half_blur_extra = (blur_extra / 2.0); + float scale_x = job->scale_x; + float scale_y = job->scale_y; + float texture_width; + float texture_height; + + g_assert (blur_radius > 0); + + /* Increase texture size for the given blur radius and scale it */ + texture_width = ceilf ((node->bounds.size.width + blur_extra)); + texture_height = ceilf ((node->bounds.size.height + blur_extra)); + + /* Only blur this if the out region has no texture id yet */ + if (offscreen->texture_id == 0) + { + const graphene_rect_t bounds = GRAPHENE_RECT_INIT (node->bounds.origin.x - half_blur_extra, + node->bounds.origin.y - half_blur_extra, + texture_width, texture_height); + + offscreen->bounds = &bounds; + offscreen->reset_clip = TRUE; + offscreen->force_offscreen = TRUE; + + if (!gsk_ngl_render_job_visit_node_with_offscreen (job, node, offscreen)) + g_assert_not_reached (); + + /* Ensure that we actually got a real texture_id */ + g_assert (offscreen->texture_id != 0); + + offscreen->texture_id = blur_offscreen (job, + offscreen, + texture_width * scale_x, + texture_height * scale_y, + blur_radius * scale_x, + blur_radius * scale_y); + init_full_texture_region (offscreen); + } + + *min_x = job->offset_x + node->bounds.origin.x - half_blur_extra; + *max_x = job->offset_x + node->bounds.origin.x + node->bounds.size.width + half_blur_extra; + *min_y = job->offset_y + node->bounds.origin.y - half_blur_extra; + *max_y = job->offset_y + node->bounds.origin.y + node->bounds.size.height + half_blur_extra; +} + +static inline void +gsk_ngl_render_job_visit_color_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + gsk_ngl_render_job_begin_draw (job, job->driver->color); + gsk_ngl_program_set_uniform_color (job->driver->color, + UNIFORM_COLOR_COLOR, 0, + gsk_color_node_get_color (node)); + gsk_ngl_render_job_draw_rect (job, &node->bounds); + gsk_ngl_render_job_end_draw (job); +} + +static inline void +gsk_ngl_render_job_visit_linear_gradient_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const GskColorStop *stops = gsk_linear_gradient_node_get_color_stops (node, NULL); + const graphene_point_t *start = gsk_linear_gradient_node_get_start (node); + const graphene_point_t *end = gsk_linear_gradient_node_get_end (node); + int n_color_stops = gsk_linear_gradient_node_get_n_color_stops (node); + gboolean repeat = gsk_render_node_get_node_type (node) == GSK_REPEATING_LINEAR_GRADIENT_NODE; + float x1 = job->offset_x + start->x; + float x2 = job->offset_x + end->x; + float y1 = job->offset_y + start->y; + float y2 = job->offset_y + end->y; + + g_assert (n_color_stops < MAX_GRADIENT_STOPS); + + gsk_ngl_render_job_begin_draw (job, job->driver->linear_gradient); + gsk_ngl_program_set_uniform1i (job->driver->linear_gradient, + UNIFORM_LINEAR_GRADIENT_NUM_COLOR_STOPS, 0, + n_color_stops); + gsk_ngl_program_set_uniform1fv (job->driver->linear_gradient, + UNIFORM_LINEAR_GRADIENT_COLOR_STOPS, 0, + n_color_stops * 5, + (const float *)stops); + gsk_ngl_program_set_uniform4f (job->driver->linear_gradient, + UNIFORM_LINEAR_GRADIENT_POINTS, 0, + x1, y1, x2 - x1, y2 - y1); + gsk_ngl_program_set_uniform1i (job->driver->linear_gradient, + UNIFORM_LINEAR_GRADIENT_REPEAT, 0, + repeat); + gsk_ngl_render_job_draw_rect (job, &node->bounds); + gsk_ngl_render_job_end_draw (job); +} + +static inline void +gsk_ngl_render_job_visit_conic_gradient_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + static const float scale = 0.5f * M_1_PI; + + const GskColorStop *stops = gsk_conic_gradient_node_get_color_stops (node, NULL); + const graphene_point_t *center = gsk_conic_gradient_node_get_center (node); + int n_color_stops = gsk_conic_gradient_node_get_n_color_stops (node); + float angle = gsk_conic_gradient_node_get_angle (node); + float bias = angle * scale + 2.0f; + + g_assert (n_color_stops < MAX_GRADIENT_STOPS); + + gsk_ngl_render_job_begin_draw (job, job->driver->conic_gradient); + gsk_ngl_program_set_uniform1i (job->driver->conic_gradient, + UNIFORM_CONIC_GRADIENT_NUM_COLOR_STOPS, 0, + n_color_stops); + gsk_ngl_program_set_uniform1fv (job->driver->conic_gradient, + UNIFORM_CONIC_GRADIENT_COLOR_STOPS, 0, + n_color_stops * 5, + (const float *)stops); + gsk_ngl_program_set_uniform4f (job->driver->conic_gradient, + UNIFORM_CONIC_GRADIENT_GEOMETRY, 0, + job->offset_x + center->x, + job->offset_y + center->y, + scale, + bias); + gsk_ngl_render_job_draw_rect (job, &node->bounds); + gsk_ngl_render_job_end_draw (job); +} + +static inline void +gsk_ngl_render_job_visit_radial_gradient_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + int n_color_stops = gsk_radial_gradient_node_get_n_color_stops (node); + const GskColorStop *stops = gsk_radial_gradient_node_get_color_stops (node, NULL); + const graphene_point_t *center = gsk_radial_gradient_node_get_center (node); + float start = gsk_radial_gradient_node_get_start (node); + float end = gsk_radial_gradient_node_get_end (node); + float hradius = gsk_radial_gradient_node_get_hradius (node); + float vradius = gsk_radial_gradient_node_get_vradius (node); + gboolean repeat = gsk_render_node_get_node_type (node) == GSK_REPEATING_RADIAL_GRADIENT_NODE; + float scale = 1.0f / (end - start); + float bias = -start * scale; + + g_assert (n_color_stops < MAX_GRADIENT_STOPS); + + gsk_ngl_render_job_begin_draw (job, job->driver->radial_gradient); + gsk_ngl_program_set_uniform1i (job->driver->radial_gradient, + UNIFORM_RADIAL_GRADIENT_NUM_COLOR_STOPS, 0, + n_color_stops); + gsk_ngl_program_set_uniform1fv (job->driver->radial_gradient, + UNIFORM_RADIAL_GRADIENT_COLOR_STOPS, 0, + n_color_stops * 5, + (const float *)stops); + gsk_ngl_program_set_uniform1i (job->driver->radial_gradient, + UNIFORM_RADIAL_GRADIENT_REPEAT, 0, + repeat); + gsk_ngl_program_set_uniform2f (job->driver->radial_gradient, + UNIFORM_RADIAL_GRADIENT_RANGE, 0, + scale, bias); + gsk_ngl_program_set_uniform4f (job->driver->radial_gradient, + UNIFORM_RADIAL_GRADIENT_GEOMETRY, 0, + job->offset_x + center->x, + job->offset_y + center->y, + 1.0f / (hradius * job->scale_x), + 1.0f / (vradius * job->scale_y)); + gsk_ngl_render_job_draw_rect (job, &node->bounds); + gsk_ngl_render_job_end_draw (job); +} + +static inline void +gsk_ngl_render_job_visit_clipped_child (GskNglRenderJob *job, + const GskRenderNode *child, + const graphene_rect_t *clip) +{ + graphene_rect_t transformed_clip; + GskRoundedRect intersection; + + gsk_ngl_render_job_transform_bounds (job, clip, &transformed_clip); + + if (job->current_clip->is_rectilinear) + { + memset (&intersection.corner, 0, sizeof intersection.corner); + graphene_rect_intersection (&transformed_clip, + &job->current_clip->rect.bounds, + &intersection.bounds); + + gsk_ngl_render_job_push_clip (job, &intersection); + gsk_ngl_render_job_visit_node (job, child); + gsk_ngl_render_job_pop_clip (job); + } + else if (intersect_rounded_rectilinear (&transformed_clip, + &job->current_clip->rect, + &intersection)) + { + gsk_ngl_render_job_push_clip (job, &intersection); + gsk_ngl_render_job_visit_node (job, child); + gsk_ngl_render_job_pop_clip (job); + } + else + { + GskRoundedRect scaled_clip; + GskNglRenderOffscreen offscreen = {0}; + + offscreen.bounds = &child->bounds; + offscreen.force_offscreen = TRUE; + + scaled_clip = GSK_ROUNDED_RECT_INIT ((job->offset_x + clip->origin.x) * job->scale_x, + (job->offset_y + clip->origin.y) * job->scale_y, + clip->size.width * job->scale_x, + clip->size.height * job->scale_y); + + gsk_ngl_render_job_push_clip (job, &scaled_clip); + gsk_ngl_render_job_visit_node_with_offscreen (job, child, &offscreen); + gsk_ngl_render_job_pop_clip (job); + + g_assert (offscreen.texture_id); + + gsk_ngl_render_job_begin_draw (job, job->driver->blit); + gsk_ngl_program_set_uniform_texture (job->driver->blit, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + offscreen.texture_id); + gsk_ngl_render_job_draw_offscreen_rect (job, &child->bounds); + gsk_ngl_render_job_end_draw (job); + } +} + +static inline void +gsk_ngl_render_job_visit_clip_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const graphene_rect_t *clip = gsk_clip_node_get_clip (node); + const GskRenderNode *child = gsk_clip_node_get_child (node); + + gsk_ngl_render_job_visit_clipped_child (job, child, clip); +} + +static inline void +gsk_ngl_render_job_visit_rounded_clip_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const GskRenderNode *child = gsk_rounded_clip_node_get_child (node); + const GskRoundedRect *clip = gsk_rounded_clip_node_get_clip (node); + GskRoundedRect transformed_clip; + float scale_x = job->scale_x; + float scale_y = job->scale_y; + gboolean need_offscreen; + + if (node_is_invisible (child)) + return; + + gsk_ngl_render_job_transform_bounds (job, &clip->bounds, &transformed_clip.bounds); + + for (guint i = 0; i < G_N_ELEMENTS (transformed_clip.corner); i++) + { + transformed_clip.corner[i].width = clip->corner[i].width * scale_x; + transformed_clip.corner[i].height = clip->corner[i].height * scale_y; + } + + if (job->current_clip->is_rectilinear) + { + GskRoundedRect intersected_clip; + + if (intersect_rounded_rectilinear (&job->current_clip->rect.bounds, + &transformed_clip, + &intersected_clip)) + { + gsk_ngl_render_job_push_clip (job, &intersected_clip); + gsk_ngl_render_job_visit_node (job, child); + gsk_ngl_render_job_pop_clip (job); + return; + } + } + + /* After this point we are really working with a new and a current clip + * which both have rounded corners. + */ + + if (job->clip->len <= 1) + need_offscreen = FALSE; + else if (rounded_inner_rect_contains_rect (&job->current_clip->rect, &transformed_clip.bounds)) + need_offscreen = FALSE; + else + need_offscreen = TRUE; + + if (!need_offscreen) + { + /* If the new clip entirely contains the current clip, the intersection is simply + * the current clip, so we can ignore the new one. + */ + if (rounded_inner_rect_contains_rect (&transformed_clip, &job->current_clip->rect.bounds)) + { + gsk_ngl_render_job_visit_node (job, child); + return; + } + + gsk_ngl_render_job_push_clip (job, &transformed_clip); + gsk_ngl_render_job_visit_node (job, child); + gsk_ngl_render_job_pop_clip (job); + } + else + { + GskNglRenderOffscreen offscreen = {0}; + + offscreen.bounds = &node->bounds; + offscreen.force_offscreen = TRUE; + + gsk_ngl_render_job_push_clip (job, &transformed_clip); + if (!gsk_ngl_render_job_visit_node_with_offscreen (job, child, &offscreen)) + g_assert_not_reached (); + gsk_ngl_render_job_pop_clip (job); + + g_assert (offscreen.texture_id); + + gsk_ngl_render_job_begin_draw (job, job->driver->blit); + gsk_ngl_program_set_uniform_texture (job->driver->blit, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + offscreen.texture_id); + gsk_ngl_render_job_load_vertices_from_offscreen (job, &node->bounds, &offscreen); + gsk_ngl_render_job_end_draw (job); + } +} + +static inline void +sort_border_sides (const GdkRGBA *colors, + int *indices) +{ + gboolean done[4] = {0, 0, 0, 0}; + guint cur = 0; + + for (guint i = 0; i < 3; i++) + { + if (done[i]) + continue; + + indices[cur] = i; + done[i] = TRUE; + cur++; + + for (guint k = i + 1; k < 4; k ++) + { + if (memcmp (&colors[k], &colors[i], sizeof (GdkRGBA)) == 0) + { + indices[cur] = k; + done[k] = TRUE; + cur++; + } + } + + if (cur >= 4) + break; + } +} + +static inline void +gsk_ngl_render_job_visit_uniform_border_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const GskRoundedRect *rounded_outline = gsk_border_node_get_outline (node); + const GdkRGBA *colors = gsk_border_node_get_colors (node); + const float *widths = gsk_border_node_get_widths (node); + GskRoundedRect outline; + + gsk_ngl_render_job_transform_rounded_rect (job, rounded_outline, &outline); + + gsk_ngl_render_job_begin_draw (job, job->driver->inset_shadow); + gsk_ngl_program_set_uniform_rounded_rect (job->driver->inset_shadow, + UNIFORM_INSET_SHADOW_OUTLINE_RECT, 0, + &outline); + gsk_ngl_program_set_uniform_color (job->driver->inset_shadow, + UNIFORM_INSET_SHADOW_COLOR, 0, + &colors[0]); + gsk_ngl_program_set_uniform1f (job->driver->inset_shadow, + UNIFORM_INSET_SHADOW_SPREAD, 0, + widths[0]); + gsk_ngl_program_set_uniform2f (job->driver->inset_shadow, + UNIFORM_INSET_SHADOW_OFFSET, 0, + 0, 0); + gsk_ngl_render_job_draw_rect (job, &node->bounds); + gsk_ngl_render_job_end_draw (job); +} + +static inline void +gsk_ngl_render_job_visit_border_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const GskRoundedRect *rounded_outline = gsk_border_node_get_outline (node); + const GdkRGBA *colors = gsk_border_node_get_colors (node); + const float *widths = gsk_border_node_get_widths (node); + struct { + float w; + float h; + } sizes[4]; + + /* Top left */ + if (widths[3] > 0) + sizes[0].w = MAX (widths[3], rounded_outline->corner[0].width); + else + sizes[0].w = 0; + + if (widths[0] > 0) + sizes[0].h = MAX (widths[0], rounded_outline->corner[0].height); + else + sizes[0].h = 0; + + /* Top right */ + if (widths[1] > 0) + sizes[1].w = MAX (widths[1], rounded_outline->corner[1].width); + else + sizes[1].w = 0; + + if (widths[0] > 0) + sizes[1].h = MAX (widths[0], rounded_outline->corner[1].height); + else + sizes[1].h = 0; + + /* Bottom right */ + if (widths[1] > 0) + sizes[2].w = MAX (widths[1], rounded_outline->corner[2].width); + else + sizes[2].w = 0; + + if (widths[2] > 0) + sizes[2].h = MAX (widths[2], rounded_outline->corner[2].height); + else + sizes[2].h = 0; + + + /* Bottom left */ + if (widths[3] > 0) + sizes[3].w = MAX (widths[3], rounded_outline->corner[3].width); + else + sizes[3].w = 0; + + if (widths[2] > 0) + sizes[3].h = MAX (widths[2], rounded_outline->corner[3].height); + else + sizes[3].h = 0; + + { + float min_x = job->offset_x + node->bounds.origin.x; + float min_y = job->offset_y + node->bounds.origin.y; + float max_x = min_x + node->bounds.size.width; + float max_y = min_y + node->bounds.size.height; + const GskNglDrawVertex side_data[4][6] = { + /* Top */ + { + { { min_x, min_y }, { 0, 1 }, }, /* Upper left */ + { { min_x + sizes[0].w, min_y + sizes[0].h }, { 0, 0 }, }, /* Lower left */ + { { max_x, min_y }, { 1, 1 }, }, /* Upper right */ + + { { max_x - sizes[1].w, min_y + sizes[1].h }, { 1, 0 }, }, /* Lower right */ + { { min_x + sizes[0].w, min_y + sizes[0].h }, { 0, 0 }, }, /* Lower left */ + { { max_x, min_y }, { 1, 1 }, }, /* Upper right */ + }, + /* Right */ + { + { { max_x - sizes[1].w, min_y + sizes[1].h }, { 0, 1 }, }, /* Upper left */ + { { max_x - sizes[2].w, max_y - sizes[2].h }, { 0, 0 }, }, /* Lower left */ + { { max_x, min_y }, { 1, 1 }, }, /* Upper right */ + + { { max_x, max_y }, { 1, 0 }, }, /* Lower right */ + { { max_x - sizes[2].w, max_y - sizes[2].h }, { 0, 0 }, }, /* Lower left */ + { { max_x, min_y }, { 1, 1 }, }, /* Upper right */ + }, + /* Bottom */ + { + { { min_x + sizes[3].w, max_y - sizes[3].h }, { 0, 1 }, }, /* Upper left */ + { { min_x, max_y }, { 0, 0 }, }, /* Lower left */ + { { max_x - sizes[2].w, max_y - sizes[2].h }, { 1, 1 }, }, /* Upper right */ + + { { max_x, max_y }, { 1, 0 }, }, /* Lower right */ + { { min_x , max_y }, { 0, 0 }, }, /* Lower left */ + { { max_x - sizes[2].w, max_y - sizes[2].h }, { 1, 1 }, }, /* Upper right */ + }, + /* Left */ + { + { { min_x, min_y }, { 0, 1 }, }, /* Upper left */ + { { min_x, max_y }, { 0, 0 }, }, /* Lower left */ + { { min_x + sizes[0].w, min_y + sizes[0].h }, { 1, 1 }, }, /* Upper right */ + + { { min_x + sizes[3].w, max_y - sizes[3].h }, { 1, 0 }, }, /* Lower right */ + { { min_x, max_y }, { 0, 0 }, }, /* Lower left */ + { { min_x + sizes[0].w, min_y + sizes[0].h }, { 1, 1 }, }, /* Upper right */ + } + }; + int indices[4] = { 0, 1, 2, 3 }; + GskRoundedRect outline; + + /* We sort them by color */ + sort_border_sides (colors, indices); + + /* Prepare outline */ + gsk_ngl_render_job_transform_rounded_rect (job, rounded_outline, &outline); + + gsk_ngl_program_set_uniform4fv (job->driver->border, + UNIFORM_BORDER_WIDTHS, 0, + 1, + widths); + gsk_ngl_program_set_uniform_rounded_rect (job->driver->border, + UNIFORM_BORDER_OUTLINE_RECT, 0, + &outline); + + for (guint i = 0; i < 4; i++) + { + GskNglDrawVertex *vertices; + + if (widths[indices[i]] <= 0) + continue; + + gsk_ngl_render_job_begin_draw (job, job->driver->border); + gsk_ngl_program_set_uniform4fv (job->driver->border, + UNIFORM_BORDER_COLOR, 0, + 1, + (const float *)&colors[indices[i]]); + vertices = gsk_ngl_command_queue_add_vertices (job->command_queue); + memcpy (vertices, side_data[indices[i]], sizeof (GskNglDrawVertex) * GSK_NGL_N_VERTICES); + gsk_ngl_render_job_end_draw (job); + } + } +} + +/* Returns TRUE if applying @transform to @bounds + * yields an axis-aligned rectangle + */ +static gboolean +result_is_axis_aligned (GskTransform *transform, + const graphene_rect_t *bounds) +{ + graphene_matrix_t m; + graphene_quad_t q; + graphene_rect_t b; + graphene_point_t b1, b2; + const graphene_point_t *p; + + gsk_transform_to_matrix (transform, &m); + gsk_matrix_transform_rect (&m, bounds, &q); + graphene_quad_bounds (&q, &b); + graphene_rect_get_top_left (&b, &b1); + graphene_rect_get_bottom_right (&b, &b2); + + for (guint i = 0; i < 4; i++) + { + p = graphene_quad_get_point (&q, i); + if (fabs (p->x - b1.x) > FLT_EPSILON && fabs (p->x - b2.x) > FLT_EPSILON) + return FALSE; + if (fabs (p->y - b1.y) > FLT_EPSILON && fabs (p->y - b2.y) > FLT_EPSILON) + return FALSE; + } + + return TRUE; +} + +static inline void +gsk_ngl_render_job_visit_transform_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + GskTransform *transform = gsk_transform_node_get_transform (node); + const GskTransformCategory category = gsk_transform_get_category (transform); + const GskRenderNode *child = gsk_transform_node_get_child (node); + + switch (category) + { + case GSK_TRANSFORM_CATEGORY_IDENTITY: + gsk_ngl_render_job_visit_node (job, child); + break; + + case GSK_TRANSFORM_CATEGORY_2D_TRANSLATE: + { + float dx, dy; + + gsk_transform_to_translate (transform, &dx, &dy); + gsk_ngl_render_job_offset (job, dx, dy); + gsk_ngl_render_job_visit_node (job, child); + gsk_ngl_render_job_offset (job, -dx, -dy); + } + break; + + case GSK_TRANSFORM_CATEGORY_2D_AFFINE: + { + gsk_ngl_render_job_push_modelview (job, transform); + gsk_ngl_render_job_visit_node (job, child); + gsk_ngl_render_job_pop_modelview (job); + } + break; + + case GSK_TRANSFORM_CATEGORY_2D: + case GSK_TRANSFORM_CATEGORY_3D: + case GSK_TRANSFORM_CATEGORY_ANY: + case GSK_TRANSFORM_CATEGORY_UNKNOWN: + if (node_supports_transform (child)) + { + gsk_ngl_render_job_push_modelview (job, transform); + gsk_ngl_render_job_visit_node (job, child); + gsk_ngl_render_job_pop_modelview (job); + } + else + { + GskNglRenderOffscreen offscreen = {0}; + + offscreen.bounds = &child->bounds; + offscreen.reset_clip = TRUE; + + if (!result_is_axis_aligned (transform, &child->bounds)) + offscreen.linear_filter = TRUE; + + if (gsk_ngl_render_job_visit_node_with_offscreen (job, child, &offscreen)) + { + /* For non-trivial transforms, we draw everything on a texture and then + * draw the texture transformed. */ + /* TODO: We should compute a modelview containing only the "non-trivial" + * part (e.g. the rotation) and use that. We want to keep the scale + * for the texture. + */ + gsk_ngl_render_job_push_modelview (job, transform); + + gsk_ngl_render_job_begin_draw (job, job->driver->blit); + gsk_ngl_program_set_uniform_texture (job->driver->blit, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + offscreen.texture_id); + gsk_ngl_render_job_load_vertices_from_offscreen (job, &child->bounds, &offscreen); + gsk_ngl_render_job_end_draw (job); + + gsk_ngl_render_job_pop_modelview (job); + } + } + break; + + default: + g_assert_not_reached (); + } +} + +static inline void +gsk_ngl_render_job_visit_unblurred_inset_shadow_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const GskRoundedRect *outline = gsk_inset_shadow_node_get_outline (node); + GskRoundedRect transformed_outline; + + gsk_ngl_render_job_transform_rounded_rect (job, outline, &transformed_outline); + + gsk_ngl_render_job_begin_draw (job, job->driver->inset_shadow); + gsk_ngl_program_set_uniform_rounded_rect (job->driver->inset_shadow, + UNIFORM_INSET_SHADOW_OUTLINE_RECT, 0, + &transformed_outline); + gsk_ngl_program_set_uniform_color (job->driver->inset_shadow, + UNIFORM_INSET_SHADOW_COLOR, 0, + gsk_inset_shadow_node_get_color (node)); + gsk_ngl_program_set_uniform1f (job->driver->inset_shadow, + UNIFORM_INSET_SHADOW_SPREAD, 0, + gsk_inset_shadow_node_get_spread (node)); + gsk_ngl_program_set_uniform2f (job->driver->inset_shadow, + UNIFORM_INSET_SHADOW_OFFSET, 0, + gsk_inset_shadow_node_get_dx (node), + gsk_inset_shadow_node_get_dy (node)); + gsk_ngl_render_job_draw_rect (job, &node->bounds); + gsk_ngl_render_job_end_draw (job); +} + +static inline void +gsk_ngl_render_job_visit_blurred_inset_shadow_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const GskRoundedRect *node_outline = gsk_inset_shadow_node_get_outline (node); + float blur_radius = gsk_inset_shadow_node_get_blur_radius (node); + float offset_x = gsk_inset_shadow_node_get_dx (node); + float offset_y = gsk_inset_shadow_node_get_dy (node); + float scale_x = job->scale_x; + float scale_y = job->scale_y; + float blur_extra = blur_radius * 2.0; /* 2.0 = shader radius_multiplier */ + float half_blur_extra = blur_radius; + float texture_width; + float texture_height; + int blurred_texture_id; + GskTextureKey key; + GskNglRenderOffscreen offscreen = {0}; + + g_assert (blur_radius > 0); + + texture_width = ceilf ((node_outline->bounds.size.width + blur_extra) * scale_x); + texture_height = ceilf ((node_outline->bounds.size.height + blur_extra) * scale_y); + + key.pointer = node; + key.pointer_is_child = FALSE; + key.scale_x = scale_x; + key.scale_y = scale_y; + key.filter = GL_NEAREST; + + blurred_texture_id = gsk_ngl_driver_lookup_texture (job->driver, &key); + + if (blurred_texture_id == 0) + { + float spread = gsk_inset_shadow_node_get_spread (node) + half_blur_extra; + GskRoundedRect transformed_outline; + GskRoundedRect outline_to_blur; + GskNglRenderTarget *render_target; + graphene_matrix_t prev_projection; + graphene_rect_t prev_viewport; + guint prev_fbo; + + /* TODO: In the following code, we have to be careful about where we apply the scale. + * We're manually scaling stuff (e.g. the outline) so we can later use texture_width + * and texture_height (which are already scaled) as the geometry and keep the modelview + * at a scale of 1. That's kinda complicated though... */ + + /* Outline of what we actually want to blur later. + * Spread grows inside, so we don't need to account for that. But the blur will need + * to read outside of the inset shadow, so we need to draw some color in there. */ + outline_to_blur = *node_outline; + gsk_rounded_rect_shrink (&outline_to_blur, + -half_blur_extra, + -half_blur_extra, + -half_blur_extra, + -half_blur_extra); + + /* Fit to our texture */ + outline_to_blur.bounds.origin.x = 0; + outline_to_blur.bounds.origin.y = 0; + outline_to_blur.bounds.size.width *= scale_x; + outline_to_blur.bounds.size.height *= scale_y; + + for (guint i = 0; i < 4; i ++) + { + outline_to_blur.corner[i].width *= scale_x; + outline_to_blur.corner[i].height *= scale_y; + } + + if (!gsk_ngl_driver_create_render_target (job->driver, + texture_width, texture_height, + GL_NEAREST, GL_NEAREST, + &render_target)) + g_assert_not_reached (); + + gsk_ngl_render_job_set_viewport_for_size (job, texture_width, texture_height, &prev_viewport); + gsk_ngl_render_job_set_projection_for_size (job, texture_width, texture_height, &prev_projection); + gsk_ngl_render_job_set_modelview (job, NULL); + gsk_ngl_render_job_push_clip (job, &GSK_ROUNDED_RECT_INIT (0, 0, texture_width, texture_height)); + + prev_fbo = gsk_ngl_command_queue_bind_framebuffer (job->command_queue, render_target->framebuffer_id); + gsk_ngl_command_queue_clear (job->command_queue, 0, &job->viewport); + + gsk_ngl_render_job_transform_rounded_rect (job, &outline_to_blur, &transformed_outline); + + /* Actual inset shadow outline drawing */ + gsk_ngl_render_job_begin_draw (job, job->driver->inset_shadow); + gsk_ngl_program_set_uniform_rounded_rect (job->driver->inset_shadow, + UNIFORM_INSET_SHADOW_OUTLINE_RECT, 0, + &transformed_outline); + gsk_ngl_program_set_uniform_color (job->driver->inset_shadow, + UNIFORM_INSET_SHADOW_COLOR, 0, + gsk_inset_shadow_node_get_color (node)); + gsk_ngl_program_set_uniform1f (job->driver->inset_shadow, + UNIFORM_INSET_SHADOW_SPREAD, 0, + spread * MAX (scale_x, scale_y)); + gsk_ngl_program_set_uniform2f (job->driver->inset_shadow, + UNIFORM_INSET_SHADOW_OFFSET, 0, + offset_x * scale_x, + offset_y * scale_y); + gsk_ngl_render_job_draw (job, 0, 0, texture_width, texture_height); + gsk_ngl_render_job_end_draw (job); + + gsk_ngl_render_job_pop_modelview (job); + gsk_ngl_render_job_pop_clip (job); + gsk_ngl_render_job_set_projection (job, &prev_projection); + gsk_ngl_render_job_set_viewport (job, &prev_viewport, NULL); + gsk_ngl_command_queue_bind_framebuffer (job->command_queue, prev_fbo); + + offscreen.texture_id = render_target->texture_id; + init_full_texture_region (&offscreen); + + blurred_texture_id = blur_offscreen (job, + &offscreen, + texture_width, + texture_height, + blur_radius * scale_x, + blur_radius * scale_y); + + gsk_ngl_driver_release_render_target (job->driver, render_target, TRUE); + + gsk_ngl_driver_cache_texture (job->driver, &key, blurred_texture_id); + } + + g_assert (blurred_texture_id != 0); + + /* Blur the rendered unblurred inset shadow */ + /* Use a clip to cut away the unwanted parts outside of the original outline */ + { + const gboolean needs_clip = !gsk_rounded_rect_is_rectilinear (node_outline); + const float tx1 = half_blur_extra * scale_x / texture_width; + const float tx2 = 1.0 - tx1; + const float ty1 = half_blur_extra * scale_y / texture_height; + const float ty2 = 1.0 - ty1; + + if (needs_clip) + { + GskRoundedRect node_clip; + + gsk_ngl_render_job_transform_bounds (job, &node_outline->bounds, &node_clip.bounds); + + for (guint i = 0; i < 4; i ++) + { + node_clip.corner[i].width = node_outline->corner[i].width * scale_x; + node_clip.corner[i].height = node_outline->corner[i].height * scale_y; + } + + gsk_ngl_render_job_push_clip (job, &node_clip); + } + + offscreen.was_offscreen = TRUE; + offscreen.area.x = tx1; + offscreen.area.y = ty1; + offscreen.area.x2 = tx2; + offscreen.area.y2 = ty2; + + gsk_ngl_render_job_begin_draw (job, job->driver->blit); + gsk_ngl_program_set_uniform_texture (job->driver->blit, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + blurred_texture_id); + gsk_ngl_render_job_load_vertices_from_offscreen (job, &node->bounds, &offscreen); + gsk_ngl_render_job_end_draw (job); + + if (needs_clip) + gsk_ngl_render_job_pop_clip (job); + } +} + +static inline void +gsk_ngl_render_job_visit_unblurred_outset_shadow_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const GskRoundedRect *outline = gsk_outset_shadow_node_get_outline (node); + GskRoundedRect transformed_outline; + float x = node->bounds.origin.x; + float y = node->bounds.origin.y; + float w = node->bounds.size.width; + float h = node->bounds.size.height; + float spread = gsk_outset_shadow_node_get_spread (node); + float dx = gsk_outset_shadow_node_get_dx (node); + float dy = gsk_outset_shadow_node_get_dy (node); + const float edge_sizes[] = { // Top, right, bottom, left + spread - dy, spread + dx, spread + dy, spread - dx + }; + const float corner_sizes[][2] = { // top left, top right, bottom right, bottom left + { outline->corner[0].width + spread - dx, outline->corner[0].height + spread - dy }, + { outline->corner[1].width + spread + dx, outline->corner[1].height + spread - dy }, + { outline->corner[2].width + spread + dx, outline->corner[2].height + spread + dy }, + { outline->corner[3].width + spread - dx, outline->corner[3].height + spread + dy }, + }; + + gsk_ngl_render_job_transform_rounded_rect (job, outline, &transformed_outline); + + gsk_ngl_render_job_begin_draw (job, job->driver->unblurred_outset_shadow); + gsk_ngl_program_set_uniform_rounded_rect (job->driver->unblurred_outset_shadow, + UNIFORM_UNBLURRED_OUTSET_SHADOW_OUTLINE_RECT, 0, + &transformed_outline); + gsk_ngl_program_set_uniform_color (job->driver->unblurred_outset_shadow, + UNIFORM_UNBLURRED_OUTSET_SHADOW_COLOR, 0, + gsk_outset_shadow_node_get_color (node)); + gsk_ngl_program_set_uniform1f (job->driver->unblurred_outset_shadow, + UNIFORM_UNBLURRED_OUTSET_SHADOW_SPREAD, 0, + spread); + gsk_ngl_program_set_uniform2f (job->driver->unblurred_outset_shadow, + UNIFORM_UNBLURRED_OUTSET_SHADOW_OFFSET, 0, + dx, dy); + + /* Corners... */ + if (corner_sizes[0][0] > 0 && corner_sizes[0][1] > 0) /* Top left */ + gsk_ngl_render_job_draw (job, + x, y, + corner_sizes[0][0], corner_sizes[0][1]); + if (corner_sizes[1][0] > 0 && corner_sizes[1][1] > 0) /* Top right */ + gsk_ngl_render_job_draw (job, + x + w - corner_sizes[1][0], y, + corner_sizes[1][0], corner_sizes[1][1]); + if (corner_sizes[2][0] > 0 && corner_sizes[2][1] > 0) /* Bottom right */ + gsk_ngl_render_job_draw (job, + x + w - corner_sizes[2][0], y + h - corner_sizes[2][1], + corner_sizes[2][0], corner_sizes[2][1]); + if (corner_sizes[3][0] > 0 && corner_sizes[3][1] > 0) /* Bottom left */ + gsk_ngl_render_job_draw (job, + x, y + h - corner_sizes[3][1], + corner_sizes[3][0], corner_sizes[3][1]); + /* Edges... */; + if (edge_sizes[0] > 0) /* Top */ + gsk_ngl_render_job_draw (job, + x + corner_sizes[0][0], y, + w - corner_sizes[0][0] - corner_sizes[1][0], edge_sizes[0]); + if (edge_sizes[1] > 0) /* Right */ + gsk_ngl_render_job_draw (job, + x + w - edge_sizes[1], y + corner_sizes[1][1], + edge_sizes[1], h - corner_sizes[1][1] - corner_sizes[2][1]); + if (edge_sizes[2] > 0) /* Bottom */ + gsk_ngl_render_job_draw (job, + x + corner_sizes[3][0], y + h - edge_sizes[2], + w - corner_sizes[3][0] - corner_sizes[2][0], edge_sizes[2]); + if (edge_sizes[3] > 0) /* Left */ + gsk_ngl_render_job_draw (job, + x, y + corner_sizes[0][1], + edge_sizes[3], h - corner_sizes[0][1] - corner_sizes[3][1]); + + gsk_ngl_render_job_end_draw (job); +} + +static inline void +gsk_ngl_render_job_visit_blurred_outset_shadow_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + static const GdkRGBA white = { 1, 1, 1, 1 }; + + const GskRoundedRect *outline = gsk_outset_shadow_node_get_outline (node); + const GdkRGBA *color = gsk_outset_shadow_node_get_color (node); + float scale_x = job->scale_x; + float scale_y = job->scale_y; + float blur_radius = gsk_outset_shadow_node_get_blur_radius (node); + float blur_extra = blur_radius * 2.0f; /* 2.0 = shader radius_multiplier */ + float half_blur_extra = blur_extra / 2.0f; + int extra_blur_pixels = ceilf (half_blur_extra * scale_x); + float spread = gsk_outset_shadow_node_get_spread (node); + float dx = gsk_outset_shadow_node_get_dx (node); + float dy = gsk_outset_shadow_node_get_dy (node); + GskRoundedRect scaled_outline; + GskRoundedRect transformed_outline; + GskNglRenderOffscreen offscreen = {0}; + int texture_width, texture_height; + int blurred_texture_id; + int cached_tid; + gboolean do_slicing; + + /* scaled_outline is the minimal outline we need to draw the given drop shadow, + * enlarged by the spread and offset by the blur radius. */ + scaled_outline = *outline; + + if (outline->bounds.size.width < blur_extra || + outline->bounds.size.height < blur_extra) + { + do_slicing = FALSE; + gsk_rounded_rect_shrink (&scaled_outline, -spread, -spread, -spread, -spread); + } + else + { + /* Shrink our outline to the minimum size that can still hold all the border radii */ + gsk_rounded_rect_shrink_to_minimum (&scaled_outline); + /* Increase by the spread */ + gsk_rounded_rect_shrink (&scaled_outline, -spread, -spread, -spread, -spread); + /* Grow bounds but don't grow corners */ + graphene_rect_inset (&scaled_outline.bounds, - blur_extra / 2.0, - blur_extra / 2.0); + /* For the center part, we add a few pixels */ + scaled_outline.bounds.size.width += SHADOW_EXTRA_SIZE; + scaled_outline.bounds.size.height += SHADOW_EXTRA_SIZE; + + do_slicing = TRUE; + } + + texture_width = (int)ceil ((scaled_outline.bounds.size.width + blur_extra) * scale_x); + texture_height = (int)ceil ((scaled_outline.bounds.size.height + blur_extra) * scale_y); + + scaled_outline.bounds.origin.x = extra_blur_pixels; + scaled_outline.bounds.origin.y = extra_blur_pixels; + scaled_outline.bounds.size.width = texture_width - (extra_blur_pixels * 2); + scaled_outline.bounds.size.height = texture_height - (extra_blur_pixels * 2); + + for (guint i = 0; i < G_N_ELEMENTS (scaled_outline.corner); i++) + { + scaled_outline.corner[i].width *= scale_x; + scaled_outline.corner[i].height *= scale_y; + } + + cached_tid = gsk_ngl_shadow_library_lookup (job->driver->shadows, &scaled_outline, blur_radius); + + if (cached_tid == 0) + { + GdkGLContext *context = job->command_queue->context; + GskNglRenderTarget *render_target; + graphene_matrix_t prev_projection; + graphene_rect_t prev_viewport; + guint prev_fbo; + + gsk_ngl_driver_create_render_target (job->driver, + texture_width, texture_height, + GL_NEAREST, GL_NEAREST, + &render_target); + + if (gdk_gl_context_has_debug (context)) + { + gdk_gl_context_label_object_printf (context, + GL_TEXTURE, + render_target->texture_id, + "Outset Shadow Temp %d", + render_target->texture_id); + gdk_gl_context_label_object_printf (context, + GL_FRAMEBUFFER, + render_target->framebuffer_id, + "Outset Shadow FB Temp %d", + render_target->framebuffer_id); + } + + /* Change state for offscreen */ + gsk_ngl_render_job_set_projection_for_size (job, texture_width, texture_height, &prev_projection); + gsk_ngl_render_job_set_viewport_for_size (job, texture_width, texture_height, &prev_viewport); + gsk_ngl_render_job_set_modelview (job, NULL); + gsk_ngl_render_job_push_clip (job, &scaled_outline); + + /* Bind render target and clear it */ + prev_fbo = gsk_ngl_command_queue_bind_framebuffer (job->command_queue, render_target->framebuffer_id); + gsk_ngl_command_queue_clear (job->command_queue, 0, &job->viewport); + + /* Draw the outline using color program */ + gsk_ngl_render_job_begin_draw (job, job->driver->color); + gsk_ngl_program_set_uniform_color (job->driver->color, + UNIFORM_COLOR_COLOR, 0, + &white); + gsk_ngl_render_job_draw (job, 0, 0, texture_width, texture_height); + gsk_ngl_render_job_end_draw (job); + + /* Reset state from offscreen */ + gsk_ngl_render_job_pop_clip (job); + gsk_ngl_render_job_pop_modelview (job); + gsk_ngl_render_job_set_viewport (job, &prev_viewport, NULL); + gsk_ngl_render_job_set_projection (job, &prev_projection); + + /* Now blur the outline */ + init_full_texture_region (&offscreen); + offscreen.texture_id = gsk_ngl_driver_release_render_target (job->driver, render_target, FALSE); + blurred_texture_id = blur_offscreen (job, + &offscreen, + texture_width, + texture_height, + blur_radius * scale_x, + blur_radius * scale_y); + + gsk_ngl_shadow_library_insert (job->driver->shadows, + &scaled_outline, + blur_radius, + blurred_texture_id); + + gsk_ngl_command_queue_bind_framebuffer (job->command_queue, prev_fbo); + } + else + { + blurred_texture_id = cached_tid; + } + + gsk_ngl_render_job_transform_rounded_rect (job, outline, &transformed_outline); + + if (!do_slicing) + { + float min_x = floorf (outline->bounds.origin.x - spread - half_blur_extra + dx); + float min_y = floorf (outline->bounds.origin.y - spread - half_blur_extra + dy); + + offscreen.was_offscreen = FALSE; + offscreen.texture_id = blurred_texture_id; + init_full_texture_region (&offscreen); + + gsk_ngl_render_job_begin_draw (job, job->driver->outset_shadow); + gsk_ngl_program_set_uniform_color (job->driver->outset_shadow, + UNIFORM_OUTSET_SHADOW_COLOR, 0, + color); + gsk_ngl_program_set_uniform_texture (job->driver->outset_shadow, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + blurred_texture_id); + gsk_ngl_program_set_uniform_rounded_rect (job->driver->outset_shadow, + UNIFORM_OUTSET_SHADOW_OUTLINE_RECT, 0, + &transformed_outline); + gsk_ngl_render_job_load_vertices_from_offscreen (job, + &GRAPHENE_RECT_INIT (min_x, + min_y, + texture_width / scale_x, + texture_height / scale_y), + &offscreen); + gsk_ngl_render_job_end_draw (job); + + return; + } + + gsk_ngl_render_job_begin_draw (job, job->driver->outset_shadow); + gsk_ngl_program_set_uniform_color (job->driver->outset_shadow, + UNIFORM_OUTSET_SHADOW_COLOR, 0, + color); + gsk_ngl_program_set_uniform_texture (job->driver->outset_shadow, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + blurred_texture_id); + gsk_ngl_program_set_uniform_rounded_rect (job->driver->outset_shadow, + UNIFORM_OUTSET_SHADOW_OUTLINE_RECT, 0, + &transformed_outline); + + { + float min_x = floorf (outline->bounds.origin.x - spread - half_blur_extra + dx); + float min_y = floorf (outline->bounds.origin.y - spread - half_blur_extra + dy); + float max_x = ceilf (outline->bounds.origin.x + outline->bounds.size.width + + half_blur_extra + dx + spread); + float max_y = ceilf (outline->bounds.origin.y + outline->bounds.size.height + + half_blur_extra + dy + spread); + const GskNglTextureNineSlice *slices; + GskNglTexture *texture; + + texture = gsk_ngl_driver_get_texture_by_id (job->driver, blurred_texture_id); + slices = gsk_ngl_texture_get_nine_slice (texture, &scaled_outline, extra_blur_pixels); + + offscreen.was_offscreen = TRUE; + + /* Our texture coordinates MUST be scaled, while the actual vertex coords + * MUST NOT be scaled. */ + + /* Top left */ + if (nine_slice_is_visible (&slices[NINE_SLICE_TOP_LEFT])) + { + memcpy (&offscreen.area, &slices[NINE_SLICE_TOP_LEFT].area, sizeof offscreen.area); + gsk_ngl_render_job_load_vertices_from_offscreen (job, + &GRAPHENE_RECT_INIT (min_x, min_y, + slices[NINE_SLICE_TOP_LEFT].rect.width / scale_x, + slices[NINE_SLICE_TOP_LEFT].rect.height / scale_y), + &offscreen); + } + + /* Top center */ + if (nine_slice_is_visible (&slices[NINE_SLICE_TOP_CENTER])) + { + memcpy (&offscreen.area, &slices[NINE_SLICE_TOP_CENTER].area, sizeof offscreen.area); + float width = (max_x - min_x) - (slices[NINE_SLICE_TOP_LEFT].rect.width / scale_x + + slices[NINE_SLICE_TOP_RIGHT].rect.width / scale_x); + gsk_ngl_render_job_load_vertices_from_offscreen (job, + &GRAPHENE_RECT_INIT (min_x + (slices[NINE_SLICE_TOP_LEFT].rect.width / scale_x), + min_y, + width, + slices[NINE_SLICE_TOP_CENTER].rect.height / scale_y), + &offscreen); + } + + /* Top right */ + if (nine_slice_is_visible (&slices[NINE_SLICE_TOP_RIGHT])) + { + memcpy (&offscreen.area, &slices[NINE_SLICE_TOP_RIGHT].area, sizeof offscreen.area); + gsk_ngl_render_job_load_vertices_from_offscreen (job, + &GRAPHENE_RECT_INIT (max_x - (slices[NINE_SLICE_TOP_RIGHT].rect.width / scale_x), + min_y, + slices[NINE_SLICE_TOP_RIGHT].rect.width / scale_x, + slices[NINE_SLICE_TOP_RIGHT].rect.height / scale_y), + &offscreen); + } + + /* Bottom right */ + if (nine_slice_is_visible (&slices[NINE_SLICE_BOTTOM_RIGHT])) + { + memcpy (&offscreen.area, &slices[NINE_SLICE_BOTTOM_RIGHT].area, sizeof offscreen.area); + gsk_ngl_render_job_load_vertices_from_offscreen (job, + &GRAPHENE_RECT_INIT (max_x - (slices[NINE_SLICE_BOTTOM_RIGHT].rect.width / scale_x), + max_y - (slices[NINE_SLICE_BOTTOM_RIGHT].rect.height / scale_y), + slices[NINE_SLICE_BOTTOM_RIGHT].rect.width / scale_x, + slices[NINE_SLICE_BOTTOM_RIGHT].rect.height / scale_y), + &offscreen); + } + + /* Bottom left */ + if (nine_slice_is_visible (&slices[NINE_SLICE_BOTTOM_LEFT])) + { + memcpy (&offscreen.area, &slices[NINE_SLICE_BOTTOM_LEFT].area, sizeof offscreen.area); + gsk_ngl_render_job_load_vertices_from_offscreen (job, + &GRAPHENE_RECT_INIT (min_x, + max_y - (slices[NINE_SLICE_BOTTOM_LEFT].rect.height / scale_y), + slices[NINE_SLICE_BOTTOM_LEFT].rect.width / scale_x, + slices[NINE_SLICE_BOTTOM_LEFT].rect.height / scale_y), + &offscreen); + } + + /* Left side */ + if (nine_slice_is_visible (&slices[NINE_SLICE_LEFT_CENTER])) + { + memcpy (&offscreen.area, &slices[NINE_SLICE_LEFT_CENTER].area, sizeof offscreen.area); + float height = (max_y - min_y) - (slices[NINE_SLICE_TOP_LEFT].rect.height / scale_y + + slices[NINE_SLICE_BOTTOM_LEFT].rect.height / scale_y); + gsk_ngl_render_job_load_vertices_from_offscreen (job, + &GRAPHENE_RECT_INIT (min_x, + min_y + (slices[NINE_SLICE_TOP_LEFT].rect.height / scale_y), + slices[NINE_SLICE_LEFT_CENTER].rect.width / scale_x, + height), + &offscreen); + } + + /* Right side */ + if (nine_slice_is_visible (&slices[NINE_SLICE_RIGHT_CENTER])) + { + memcpy (&offscreen.area, &slices[NINE_SLICE_RIGHT_CENTER].area, sizeof offscreen.area); + float height = (max_y - min_y) - (slices[NINE_SLICE_TOP_RIGHT].rect.height / scale_y + + slices[NINE_SLICE_BOTTOM_RIGHT].rect.height / scale_y); + gsk_ngl_render_job_load_vertices_from_offscreen (job, + &GRAPHENE_RECT_INIT (max_x - (slices[NINE_SLICE_RIGHT_CENTER].rect.width / scale_x), + min_y + (slices[NINE_SLICE_TOP_LEFT].rect.height / scale_y), + slices[NINE_SLICE_RIGHT_CENTER].rect.width / scale_x, + height), + &offscreen); + } + + /* Bottom side */ + if (nine_slice_is_visible (&slices[NINE_SLICE_BOTTOM_CENTER])) + { + memcpy (&offscreen.area, &slices[NINE_SLICE_BOTTOM_CENTER].area, sizeof offscreen.area); + float width = (max_x - min_x) - (slices[NINE_SLICE_BOTTOM_LEFT].rect.width / scale_x + + slices[NINE_SLICE_BOTTOM_RIGHT].rect.width / scale_x); + gsk_ngl_render_job_load_vertices_from_offscreen (job, + &GRAPHENE_RECT_INIT (min_x + (slices[NINE_SLICE_BOTTOM_LEFT].rect.width / scale_x), + max_y - (slices[NINE_SLICE_BOTTOM_CENTER].rect.height / scale_y), + width, + slices[NINE_SLICE_BOTTOM_CENTER].rect.height / scale_y), + &offscreen); + } + + /* Middle */ + if (nine_slice_is_visible (&slices[NINE_SLICE_CENTER])) + { + memcpy (&offscreen.area, &slices[NINE_SLICE_CENTER].area, sizeof offscreen.area); + float width = (max_x - min_x) - (slices[NINE_SLICE_LEFT_CENTER].rect.width / scale_x + + slices[NINE_SLICE_RIGHT_CENTER].rect.width / scale_x); + float height = (max_y - min_y) - (slices[NINE_SLICE_TOP_CENTER].rect.height / scale_y + + slices[NINE_SLICE_BOTTOM_CENTER].rect.height / scale_y); + gsk_ngl_render_job_load_vertices_from_offscreen (job, + &GRAPHENE_RECT_INIT (min_x + (slices[NINE_SLICE_LEFT_CENTER].rect.width / scale_x), + min_y + (slices[NINE_SLICE_TOP_CENTER].rect.height / scale_y), + width, height), + &offscreen); + } + } + + gsk_ngl_render_job_end_draw (job); +} + +static inline gboolean G_GNUC_PURE +equal_texture_nodes (const GskRenderNode *node1, + const GskRenderNode *node2) +{ + if (gsk_render_node_get_node_type (node1) != GSK_TEXTURE_NODE || + gsk_render_node_get_node_type (node2) != GSK_TEXTURE_NODE) + return FALSE; + + if (gsk_texture_node_get_texture (node1) != + gsk_texture_node_get_texture (node2)) + return FALSE; + + return graphene_rect_equal (&node1->bounds, &node2->bounds); +} + +static inline void +gsk_ngl_render_job_visit_cross_fade_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const GskRenderNode *start_node = gsk_cross_fade_node_get_start_child (node); + const GskRenderNode *end_node = gsk_cross_fade_node_get_end_child (node); + float progress = gsk_cross_fade_node_get_progress (node); + GskNglRenderOffscreen offscreen_start = {0}; + GskNglRenderOffscreen offscreen_end = {0}; + + g_assert (progress > 0.0); + g_assert (progress < 1.0); + + offscreen_start.force_offscreen = TRUE; + offscreen_start.reset_clip = TRUE; + offscreen_start.bounds = &node->bounds; + + offscreen_end.force_offscreen = TRUE; + offscreen_end.reset_clip = TRUE; + offscreen_end.bounds = &node->bounds; + + if (!gsk_ngl_render_job_visit_node_with_offscreen (job, start_node, &offscreen_start)) + { + gsk_ngl_render_job_visit_node (job, end_node); + return; + } + + g_assert (offscreen_start.texture_id); + + if (!gsk_ngl_render_job_visit_node_with_offscreen (job, end_node, &offscreen_end)) + { + float prev_alpha = gsk_ngl_render_job_set_alpha (job, job->alpha * progress); + gsk_ngl_render_job_visit_node (job, start_node); + gsk_ngl_render_job_set_alpha (job, prev_alpha); + return; + } + + g_assert (offscreen_end.texture_id); + + gsk_ngl_render_job_begin_draw (job, job->driver->cross_fade); + gsk_ngl_program_set_uniform_texture (job->driver->cross_fade, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + offscreen_start.texture_id); + gsk_ngl_program_set_uniform_texture (job->driver->cross_fade, + UNIFORM_CROSS_FADE_SOURCE2, 0, + GL_TEXTURE_2D, + GL_TEXTURE1, + offscreen_end.texture_id); + gsk_ngl_program_set_uniform1f (job->driver->cross_fade, + UNIFORM_CROSS_FADE_PROGRESS, 0, + progress); + gsk_ngl_render_job_load_vertices_from_offscreen (job, &node->bounds, &offscreen_end); + gsk_ngl_render_job_end_draw (job); +} + +static inline void +gsk_ngl_render_job_visit_opacity_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const GskRenderNode *child = gsk_opacity_node_get_child (node); + float opacity = gsk_opacity_node_get_opacity (node); + float new_alpha = job->alpha * opacity; + + if (!ALPHA_IS_CLEAR (new_alpha)) + { + float prev_alpha = gsk_ngl_render_job_set_alpha (job, new_alpha); + + if (gsk_render_node_get_node_type (child) == GSK_CONTAINER_NODE) + { + GskNglRenderOffscreen offscreen = {0}; + + offscreen.bounds = &child->bounds; + offscreen.force_offscreen = TRUE; + offscreen.reset_clip = TRUE; + + /* The semantics of an opacity node mandate that when, e.g., two + * color nodes overlap, there may not be any blending between them. + */ + if (!gsk_ngl_render_job_visit_node_with_offscreen (job, child, &offscreen)) + return; + + g_assert (offscreen.texture_id); + + gsk_ngl_render_job_begin_draw (job, job->driver->blit); + gsk_ngl_program_set_uniform_texture (job->driver->blit, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + offscreen.texture_id); + gsk_ngl_render_job_load_vertices_from_offscreen (job, &node->bounds, &offscreen); + gsk_ngl_render_job_end_draw (job); + } + else + { + gsk_ngl_render_job_visit_node (job, child); + } + + gsk_ngl_render_job_set_alpha (job, prev_alpha); + } +} + +static inline void +gsk_ngl_render_job_visit_text_node (GskNglRenderJob *job, + const GskRenderNode *node, + const GdkRGBA *color, + gboolean force_color) +{ + const PangoFont *font = gsk_text_node_get_font (node); + const PangoGlyphInfo *glyphs = gsk_text_node_get_glyphs (node, NULL); + const graphene_point_t *offset = gsk_text_node_get_offset (node); + float text_scale = MAX (job->scale_x, job->scale_y); /* TODO: Fix for uneven scales? */ + guint num_glyphs = gsk_text_node_get_num_glyphs (node); + float x = offset->x + job->offset_x; + float y = offset->y + job->offset_y; + GskNglGlyphLibrary *library = job->driver->glyphs; + GskNglCommandBatch *batch; + GskNglProgram *program; + int x_position = 0; + GskNglGlyphKey lookup; + guint last_texture = 0; + GskNglDrawVertex *vertices; + guint used = 0; + + if (num_glyphs == 0) + return; + + /* If the font has color glyphs, we don't need to recolor anything */ + if (!force_color && gsk_text_node_has_color_glyphs (node)) + { + program = job->driver->blit; + } + else + { + program = job->driver->coloring; + gsk_ngl_program_set_uniform_color (program, UNIFORM_COLORING_COLOR, 0, color); + } + + lookup.font = (PangoFont *)font; + lookup.scale = (guint) (text_scale * 1024); + + gsk_ngl_render_job_begin_draw (job, program); + batch = gsk_ngl_command_queue_get_batch (job->command_queue); + vertices = gsk_ngl_command_queue_add_n_vertices (job->command_queue, num_glyphs); + + /* We use one quad per character */ + for (guint i = 0; i < num_glyphs; i++) + { + const PangoGlyphInfo *gi = &glyphs[i]; + const GskNglGlyphValue *glyph; + guint base = used * GSK_NGL_N_VERTICES; + float glyph_x, glyph_y, glyph_x2, glyph_y2; + float tx, ty, tx2, ty2; + float cx; + float cy; + guint texture_id; + + if (gi->glyph == PANGO_GLYPH_EMPTY) + continue; + + cx = (float)(x_position + gi->geometry.x_offset) / PANGO_SCALE; + cy = (float)(gi->geometry.y_offset) / PANGO_SCALE; + + gsk_ngl_glyph_key_set_glyph_and_shift (&lookup, gi->glyph, x + cx, y + cy); + + if (!gsk_ngl_glyph_library_lookup_or_add (library, &lookup, &glyph)) + goto next; + + texture_id = GSK_NGL_TEXTURE_ATLAS_ENTRY_TEXTURE (glyph); + + if G_UNLIKELY (last_texture != texture_id) + { + g_assert (texture_id > 0); + + if G_LIKELY (last_texture != 0) + { + guint vbo_offset = batch->draw.vbo_offset + batch->draw.vbo_count; + + /* Since we have batched added our VBO vertices to avoid repeated + * calls to the buffer, we need to manually tweak the vbo offset + * of the new batch as otherwise it will point at the end of our + * vbo array. + */ + gsk_ngl_render_job_split_draw (job); + batch = gsk_ngl_command_queue_get_batch (job->command_queue); + batch->draw.vbo_offset = vbo_offset; + } + + gsk_ngl_program_set_uniform_texture (program, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + texture_id); + last_texture = texture_id; + } + + tx = glyph->entry.area.x; + ty = glyph->entry.area.y; + tx2 = glyph->entry.area.x2; + ty2 = glyph->entry.area.y2; + + glyph_x = floorf (x + cx + 0.125) + glyph->ink_rect.x; + glyph_y = floorf (y + cy + 0.125) + glyph->ink_rect.y; + glyph_x2 = glyph_x + glyph->ink_rect.width; + glyph_y2 = glyph_y + glyph->ink_rect.height; + + vertices[base+0].position[0] = glyph_x; + vertices[base+0].position[1] = glyph_y; + vertices[base+0].uv[0] = tx; + vertices[base+0].uv[1] = ty; + + vertices[base+1].position[0] = glyph_x; + vertices[base+1].position[1] = glyph_y2; + vertices[base+1].uv[0] = tx; + vertices[base+1].uv[1] = ty2; + + vertices[base+2].position[0] = glyph_x2; + vertices[base+2].position[1] = glyph_y; + vertices[base+2].uv[0] = tx2; + vertices[base+2].uv[1] = ty; + + vertices[base+3].position[0] = glyph_x2; + vertices[base+3].position[1] = glyph_y2; + vertices[base+3].uv[0] = tx2; + vertices[base+3].uv[1] = ty2; + + vertices[base+4].position[0] = glyph_x; + vertices[base+4].position[1] = glyph_y2; + vertices[base+4].uv[0] = tx; + vertices[base+4].uv[1] = ty2; + + vertices[base+5].position[0] = glyph_x2; + vertices[base+5].position[1] = glyph_y; + vertices[base+5].uv[0] = tx2; + vertices[base+5].uv[1] = ty; + + batch->draw.vbo_count += GSK_NGL_N_VERTICES; + used++; + +next: + x_position += gi->geometry.width; + } + + if (used != num_glyphs) + gsk_ngl_command_queue_retract_n_vertices (job->command_queue, num_glyphs - used); + + gsk_ngl_render_job_end_draw (job); +} + +static inline void +gsk_ngl_render_job_visit_shadow_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const gsize n_shadows = gsk_shadow_node_get_n_shadows (node); + const GskRenderNode *original_child = gsk_shadow_node_get_child (node); + const GskRenderNode *shadow_child = original_child; + + /* Shadow nodes recolor every pixel of the source texture, but leave the alpha in tact. + * If the child is a color matrix node that doesn't touch the alpha, we can throw that away. */ + if (gsk_render_node_get_node_type (shadow_child) == GSK_COLOR_MATRIX_NODE && + !color_matrix_modifies_alpha (shadow_child)) + shadow_child = gsk_color_matrix_node_get_child (shadow_child); + + for (guint i = 0; i < n_shadows; i++) + { + const GskShadow *shadow = gsk_shadow_node_get_shadow (node, i); + const float dx = shadow->dx; + const float dy = shadow->dy; + GskNglRenderOffscreen offscreen = {0}; + graphene_rect_t bounds; + + if (shadow->radius == 0 && + gsk_render_node_get_node_type (shadow_child) == GSK_TEXT_NODE) + { + gsk_ngl_render_job_offset (job, dx, dy); + gsk_ngl_render_job_visit_text_node (job, shadow_child, &shadow->color, TRUE); + gsk_ngl_render_job_offset (job, -dx, -dy); + continue; + } + + if (RGBA_IS_CLEAR (&shadow->color)) + continue; + + if (node_is_invisible (shadow_child)) + continue; + + if (shadow->radius > 0) + { + float min_x; + float min_y; + float max_x; + float max_y; + + offscreen.do_not_cache = TRUE; + + blur_node (job, + &offscreen, + shadow_child, + shadow->radius, + &min_x, &max_x, + &min_y, &max_y); + + bounds.origin.x = min_x - job->offset_x; + bounds.origin.y = min_y - job->offset_y; + bounds.size.width = max_x - min_x; + bounds.size.height = max_y - min_y; + + offscreen.was_offscreen = TRUE; + } + else if (dx == 0 && dy == 0) + { + continue; /* Invisible anyway */ + } + else + { + offscreen.bounds = &shadow_child->bounds; + offscreen.reset_clip = TRUE; + offscreen.do_not_cache = TRUE; + + if (!gsk_ngl_render_job_visit_node_with_offscreen (job, shadow_child, &offscreen)) + g_assert_not_reached (); + + bounds = shadow_child->bounds; + } + + gsk_ngl_render_job_offset (job, dx, dy); + gsk_ngl_render_job_begin_draw (job, job->driver->coloring); + gsk_ngl_program_set_uniform_texture (job->driver->coloring, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + offscreen.texture_id); + gsk_ngl_program_set_uniform_color (job->driver->coloring, + UNIFORM_COLORING_COLOR, 0, + &shadow->color); + gsk_ngl_render_job_load_vertices_from_offscreen (job, &bounds, &offscreen); + gsk_ngl_render_job_end_draw (job); + gsk_ngl_render_job_offset (job, -dx, -dy); + } + + /* Now draw the child normally */ + gsk_ngl_render_job_visit_node (job, original_child); +} + +static inline void +gsk_ngl_render_job_visit_blur_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const GskRenderNode *child = gsk_blur_node_get_child (node); + float blur_radius = gsk_blur_node_get_radius (node); + GskNglRenderOffscreen offscreen = {0}; + GskTextureKey key; + gboolean cache_texture; + float min_x; + float max_x; + float min_y; + float max_y; + + g_assert (blur_radius > 0); + + if (node_is_invisible (child)) + return; + + key.pointer = node; + key.pointer_is_child = FALSE; + key.scale_x = job->scale_x; + key.scale_y = job->scale_y; + key.filter = GL_NEAREST; + + offscreen.texture_id = gsk_ngl_driver_lookup_texture (job->driver, &key); + cache_texture = offscreen.texture_id == 0; + + blur_node (job, + &offscreen, + child, + blur_radius, + &min_x, &max_x, &min_y, &max_y); + + g_assert (offscreen.texture_id != 0); + + if (cache_texture) + gsk_ngl_driver_cache_texture (job->driver, &key, offscreen.texture_id); + + gsk_ngl_render_job_begin_draw (job, job->driver->blit); + gsk_ngl_program_set_uniform_texture (job->driver->blit, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + offscreen.texture_id); + gsk_ngl_render_job_draw_coords (job, min_x, min_y, max_x, max_y); + gsk_ngl_render_job_end_draw (job); +} + +static inline void +gsk_ngl_render_job_visit_blend_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const GskRenderNode *top_child = gsk_blend_node_get_top_child (node); + const GskRenderNode *bottom_child = gsk_blend_node_get_bottom_child (node); + GskNglRenderOffscreen top_offscreen = {0}; + GskNglRenderOffscreen bottom_offscreen = {0}; + + top_offscreen.bounds = &node->bounds; + top_offscreen.force_offscreen = TRUE; + top_offscreen.reset_clip = TRUE; + + bottom_offscreen.bounds = &node->bounds; + bottom_offscreen.force_offscreen = TRUE; + bottom_offscreen.reset_clip = TRUE; + + /* TODO: We create 2 textures here as big as the blend node, but both the + * start and the end node might be a lot smaller than that. */ + if (!gsk_ngl_render_job_visit_node_with_offscreen (job, bottom_child, &bottom_offscreen)) + { + gsk_ngl_render_job_visit_node (job, top_child); + return; + } + + g_assert (bottom_offscreen.was_offscreen); + + if (!gsk_ngl_render_job_visit_node_with_offscreen (job, top_child, &top_offscreen)) + { + gsk_ngl_render_job_begin_draw (job, job->driver->blit); + gsk_ngl_program_set_uniform_texture (job->driver->blit, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + bottom_offscreen.texture_id); + gsk_ngl_render_job_load_vertices_from_offscreen (job, &node->bounds, &bottom_offscreen); + gsk_ngl_render_job_end_draw (job); + return; + } + + g_assert (top_offscreen.was_offscreen); + + gsk_ngl_render_job_begin_draw (job, job->driver->blend); + gsk_ngl_program_set_uniform_texture (job->driver->blend, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + bottom_offscreen.texture_id); + gsk_ngl_program_set_uniform_texture (job->driver->blend, + UNIFORM_BLEND_SOURCE2, 0, + GL_TEXTURE_2D, + GL_TEXTURE1, + top_offscreen.texture_id); + gsk_ngl_program_set_uniform1i (job->driver->blend, + UNIFORM_BLEND_MODE, 0, + gsk_blend_node_get_blend_mode (node)); + gsk_ngl_render_job_draw_offscreen_rect (job, &node->bounds); + gsk_ngl_render_job_end_draw (job); +} + +static inline void +gsk_ngl_render_job_visit_color_matrix_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const GskRenderNode *child = gsk_color_matrix_node_get_child (node); + GskNglRenderOffscreen offscreen = {0}; + float offset[4]; + + if (node_is_invisible (child)) + return; + + offscreen.bounds = &node->bounds; + offscreen.reset_clip = TRUE; + + if (!gsk_ngl_render_job_visit_node_with_offscreen (job, child, &offscreen)) + g_assert_not_reached (); + + g_assert (offscreen.texture_id > 0); + + graphene_vec4_to_float (gsk_color_matrix_node_get_color_offset (node), offset); + + gsk_ngl_render_job_begin_draw (job, job->driver->color_matrix); + gsk_ngl_program_set_uniform_texture (job->driver->color_matrix, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + offscreen.texture_id); + gsk_ngl_program_set_uniform_matrix (job->driver->color_matrix, + UNIFORM_COLOR_MATRIX_COLOR_MATRIX, 0, + gsk_color_matrix_node_get_color_matrix (node)); + gsk_ngl_program_set_uniform4fv (job->driver->color_matrix, + UNIFORM_COLOR_MATRIX_COLOR_OFFSET, 0, + 1, + offset); + gsk_ngl_render_job_load_vertices_from_offscreen (job, &node->bounds, &offscreen); + gsk_ngl_render_job_end_draw (job); +} + +static inline void +gsk_ngl_render_job_visit_gl_shader_node_fallback (GskNglRenderJob *job, + const GskRenderNode *node) +{ + static const GdkRGBA pink = { 255 / 255., 105 / 255., 180 / 255., 1.0 }; + + gsk_ngl_render_job_begin_draw (job, job->driver->color); + gsk_ngl_program_set_uniform_color (job->driver->color, + UNIFORM_COLOR_COLOR, 0, + &pink); + gsk_ngl_render_job_draw_rect (job, &node->bounds); + gsk_ngl_render_job_end_draw (job); +} + +static inline void +gsk_ngl_render_job_visit_gl_shader_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + GError *error = NULL; + GskGLShader *shader; + GskNglProgram *program; + int n_children; + + shader = gsk_gl_shader_node_get_shader (node); + program = gsk_ngl_driver_lookup_shader (job->driver, shader, &error); + n_children = gsk_gl_shader_node_get_n_children (node); + + if G_UNLIKELY (program == NULL) + { + if (g_object_get_data (G_OBJECT (shader), "gsk-did-warn") == NULL) + { + g_object_set_data (G_OBJECT (shader), "gsk-did-warn", GUINT_TO_POINTER (1)); + g_warning ("Failed to compile gl shader: %s", error->message); + } + gsk_ngl_render_job_visit_gl_shader_node_fallback (job, node); + g_clear_error (&error); + } + else + { + GskNglRenderOffscreen offscreens[4] = {{0}}; + const GskGLUniform *uniforms; + const guint8 *base; + GBytes *args; + int n_uniforms; + + g_assert (n_children < G_N_ELEMENTS (offscreens)); + + for (guint i = 0; i < n_children; i++) + { + const GskRenderNode *child = gsk_gl_shader_node_get_child (node, i); + + offscreens[i].bounds = &node->bounds; + offscreens[i].force_offscreen = TRUE; + offscreens[i].reset_clip = TRUE; + + if (!gsk_ngl_render_job_visit_node_with_offscreen (job, child, &offscreens[i])) + return; + } + + args = gsk_gl_shader_node_get_args (node); + base = g_bytes_get_data (args, NULL); + uniforms = gsk_gl_shader_get_uniforms (shader, &n_uniforms); + + gsk_ngl_render_job_begin_draw (job, program); + for (guint i = 0; i < n_children; i++) + gsk_ngl_program_set_uniform_texture (program, + UNIFORM_CUSTOM_TEXTURE1 + i, 0, + GL_TEXTURE_2D, + GL_TEXTURE0 + i, + offscreens[i].texture_id); + gsk_ngl_program_set_uniform2f (program, + UNIFORM_CUSTOM_SIZE, 0, + node->bounds.size.width, + node->bounds.size.height); + for (guint i = 0; i < n_uniforms; i++) + { + const GskGLUniform *u = &uniforms[i]; + const guint8 *data = base + u->offset; + + /* Ignore unused uniforms */ + if (program->args_locations[i] == -1) + continue; + + switch (u->type) + { + default: + case GSK_GL_UNIFORM_TYPE_NONE: + break; + case GSK_GL_UNIFORM_TYPE_FLOAT: + gsk_ngl_uniform_state_set1fv (job->command_queue->uniforms, + program->program_info, + program->args_locations[i], + 0, 1, (const float *)data); + break; + case GSK_GL_UNIFORM_TYPE_INT: + gsk_ngl_uniform_state_set1i (job->command_queue->uniforms, + program->program_info, + program->args_locations[i], + 0, *(const gint32 *)data); + break; + case GSK_GL_UNIFORM_TYPE_UINT: + case GSK_GL_UNIFORM_TYPE_BOOL: + gsk_ngl_uniform_state_set1ui (job->command_queue->uniforms, + program->program_info, + program->args_locations[i], + 0, *(const guint32 *)data); + break; + case GSK_GL_UNIFORM_TYPE_VEC2: + gsk_ngl_uniform_state_set2fv (job->command_queue->uniforms, + program->program_info, + program->args_locations[i], + 0, 1, (const float *)data); + break; + case GSK_GL_UNIFORM_TYPE_VEC3: + gsk_ngl_uniform_state_set3fv (job->command_queue->uniforms, + program->program_info, + program->args_locations[i], + 0, 1, (const float *)data); + break; + case GSK_GL_UNIFORM_TYPE_VEC4: + gsk_ngl_uniform_state_set4fv (job->command_queue->uniforms, + program->program_info, + program->args_locations[i], + 0, 1, (const float *)data); + break; + } + } + gsk_ngl_render_job_draw_offscreen_rect (job, &node->bounds); + gsk_ngl_render_job_end_draw (job); + } +} + +static void +gsk_ngl_render_job_upload_texture (GskNglRenderJob *job, + GdkTexture *texture, + GskNglRenderOffscreen *offscreen) +{ + if (gsk_ngl_texture_library_can_cache (GSK_NGL_TEXTURE_LIBRARY (job->driver->icons), + texture->width, + texture->height) && + !GDK_IS_GL_TEXTURE (texture)) + { + const GskNglIconData *icon_data; + + gsk_ngl_icon_library_lookup_or_add (job->driver->icons, texture, &icon_data); + offscreen->texture_id = GSK_NGL_TEXTURE_ATLAS_ENTRY_TEXTURE (icon_data); + memcpy (&offscreen->area, &icon_data->entry.area, sizeof offscreen->area); + } + else + { + offscreen->texture_id = gsk_ngl_driver_load_texture (job->driver, texture, GL_LINEAR, GL_LINEAR); + init_full_texture_region (offscreen); + } +} + +static inline void +gsk_ngl_render_job_visit_texture_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + GdkTexture *texture = gsk_texture_node_get_texture (node); + int max_texture_size = job->command_queue->max_texture_size; + + if G_LIKELY (texture->width <= max_texture_size && + texture->height <= max_texture_size) + { + GskNglRenderOffscreen offscreen = {0}; + + gsk_ngl_render_job_upload_texture (job, texture, &offscreen); + + g_assert (offscreen.texture_id); + g_assert (offscreen.was_offscreen == FALSE); + + gsk_ngl_render_job_begin_draw (job, job->driver->blit); + gsk_ngl_program_set_uniform_texture (job->driver->blit, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + offscreen.texture_id); + gsk_ngl_render_job_load_vertices_from_offscreen (job, &node->bounds, &offscreen); + gsk_ngl_render_job_end_draw (job); + } + else + { + float min_x = job->offset_x + node->bounds.origin.x; + float min_y = job->offset_y + node->bounds.origin.y; + float max_x = min_x + node->bounds.size.width; + float max_y = min_y + node->bounds.size.height; + float scale_x = (max_x - min_x) / texture->width; + float scale_y = (max_y - min_y) / texture->height; + GskNglTextureSlice *slices = NULL; + guint n_slices = 0; + + gsk_ngl_driver_slice_texture (job->driver, texture, &slices, &n_slices); + + g_assert (slices != NULL); + g_assert (n_slices > 0); + + gsk_ngl_render_job_begin_draw (job, job->driver->blit); + + for (guint i = 0; i < n_slices; i ++) + { + GskNglDrawVertex *vertices; + const GskNglTextureSlice *slice = &slices[i]; + float x1, x2, y1, y2; + + x1 = min_x + (scale_x * slice->rect.x); + x2 = x1 + (slice->rect.width * scale_x); + y1 = min_y + (scale_y * slice->rect.y); + y2 = y1 + (slice->rect.height * scale_y); + + if (i > 0) + gsk_ngl_render_job_split_draw (job); + gsk_ngl_program_set_uniform_texture (job->driver->blit, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + slice->texture_id); + vertices = gsk_ngl_command_queue_add_vertices (job->command_queue); + + vertices[0].position[0] = x1; + vertices[0].position[1] = y1; + vertices[0].uv[0] = 0; + vertices[0].uv[1] = 0; + + vertices[1].position[0] = x1; + vertices[1].position[1] = y2; + vertices[1].uv[0] = 0; + vertices[1].uv[1] = 1; + + vertices[2].position[0] = x2; + vertices[2].position[1] = y1; + vertices[2].uv[0] = 1; + vertices[2].uv[1] = 0; + + vertices[3].position[0] = x2; + vertices[3].position[1] = y2; + vertices[3].uv[0] = 1; + vertices[3].uv[1] = 1; + + vertices[4].position[0] = x1; + vertices[4].position[1] = y2; + vertices[4].uv[0] = 0; + vertices[4].uv[1] = 1; + + vertices[5].position[0] = x2; + vertices[5].position[1] = y1; + vertices[5].uv[0] = 1; + vertices[5].uv[1] = 0; + } + + gsk_ngl_render_job_end_draw (job); + } +} + +static inline void +gsk_ngl_render_job_visit_repeat_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + const GskRenderNode *child = gsk_repeat_node_get_child (node); + const graphene_rect_t *child_bounds = gsk_repeat_node_get_child_bounds (node); + GskNglRenderOffscreen offscreen = {0}; + + if (node_is_invisible (child)) + return; + + if (!graphene_rect_equal (child_bounds, &child->bounds)) + { + /* TODO: implement these repeat nodes. */ + gsk_ngl_render_job_visit_as_fallback (job, node); + return; + } + + /* If the size of the repeat node is smaller than the size of the + * child node, we don't repeat at all and can just draw that part + * of the child texture... */ + if (rect_contains_rect (child_bounds, &node->bounds)) + { + gsk_ngl_render_job_visit_clipped_child (job, child, &node->bounds); + return; + } + + offscreen.bounds = &child->bounds; + offscreen.reset_clip = TRUE; + + if (!gsk_ngl_render_job_visit_node_with_offscreen (job, child, &offscreen)) + g_assert_not_reached (); + + gsk_ngl_render_job_begin_draw (job, job->driver->repeat); + gsk_ngl_program_set_uniform_texture (job->driver->repeat, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + offscreen.texture_id); + gsk_ngl_program_set_uniform4f (job->driver->repeat, + UNIFORM_REPEAT_CHILD_BOUNDS, 0, + (node->bounds.origin.x - child_bounds->origin.x) / child_bounds->size.width, + (node->bounds.origin.y - child_bounds->origin.y) / child_bounds->size.height, + node->bounds.size.width / child_bounds->size.width, + node->bounds.size.height / child_bounds->size.height); + gsk_ngl_program_set_uniform4f (job->driver->repeat, + UNIFORM_REPEAT_TEXTURE_RECT, 0, + offscreen.area.x, + offscreen.was_offscreen ? offscreen.area.y2 : offscreen.area.y, + offscreen.area.x2, + offscreen.was_offscreen ? offscreen.area.y : offscreen.area.y2); + gsk_ngl_render_job_load_vertices_from_offscreen (job, &node->bounds, &offscreen); + gsk_ngl_render_job_end_draw (job); +} + +static void +gsk_ngl_render_job_visit_node (GskNglRenderJob *job, + const GskRenderNode *node) +{ + g_assert (job != NULL); + g_assert (node != NULL); + g_assert (GSK_IS_NGL_DRIVER (job->driver)); + g_assert (GSK_IS_NGL_COMMAND_QUEUE (job->command_queue)); + + if (node_is_invisible (node) || + !gsk_ngl_render_job_node_overlaps_clip (job, node)) + return; + + switch (gsk_render_node_get_node_type (node)) + { + case GSK_BLEND_NODE: + gsk_ngl_render_job_visit_blend_node (job, node); + break; + + case GSK_BLUR_NODE: + if (gsk_blur_node_get_radius (node) > 0) + gsk_ngl_render_job_visit_blur_node (job, node); + else + gsk_ngl_render_job_visit_node (job, gsk_blur_node_get_child (node)); + break; + + case GSK_BORDER_NODE: + if (gsk_border_node_get_uniform (node)) + gsk_ngl_render_job_visit_uniform_border_node (job, node); + else + gsk_ngl_render_job_visit_border_node (job, node); + break; + + case GSK_CLIP_NODE: + gsk_ngl_render_job_visit_clip_node (job, node); + break; + + case GSK_COLOR_NODE: + gsk_ngl_render_job_visit_color_node (job, node); + break; + + case GSK_COLOR_MATRIX_NODE: + gsk_ngl_render_job_visit_color_matrix_node (job, node); + break; + + case GSK_CONIC_GRADIENT_NODE: + if (gsk_conic_gradient_node_get_n_color_stops (node) < MAX_GRADIENT_STOPS) + gsk_ngl_render_job_visit_conic_gradient_node (job, node); + else + gsk_ngl_render_job_visit_as_fallback (job, node); + break; + + case GSK_CONTAINER_NODE: + { + guint n_children = gsk_container_node_get_n_children (node); + + for (guint i = 0; i < n_children; i++) + { + const GskRenderNode *child = gsk_container_node_get_child (node, i); + gsk_ngl_render_job_visit_node (job, child); + } + } + break; + + case GSK_CROSS_FADE_NODE: + { + const GskRenderNode *start_node = gsk_cross_fade_node_get_start_child (node); + const GskRenderNode *end_node = gsk_cross_fade_node_get_end_child (node); + float progress = gsk_cross_fade_node_get_progress (node); + + if (progress <= 0.0f) + gsk_ngl_render_job_visit_node (job, gsk_cross_fade_node_get_start_child (node)); + else if (progress >= 1.0f || equal_texture_nodes (start_node, end_node)) + gsk_ngl_render_job_visit_node (job, gsk_cross_fade_node_get_end_child (node)); + else + gsk_ngl_render_job_visit_cross_fade_node (job, node); + } + break; + + case GSK_DEBUG_NODE: + /* Debug nodes are ignored because draws get reordered anyway */ + gsk_ngl_render_job_visit_node (job, gsk_debug_node_get_child (node)); + break; + + case GSK_GL_SHADER_NODE: + gsk_ngl_render_job_visit_gl_shader_node (job, node); + break; + + case GSK_INSET_SHADOW_NODE: + if (gsk_inset_shadow_node_get_blur_radius (node) > 0) + gsk_ngl_render_job_visit_blurred_inset_shadow_node (job, node); + else + gsk_ngl_render_job_visit_unblurred_inset_shadow_node (job, node); + break; + + case GSK_LINEAR_GRADIENT_NODE: + case GSK_REPEATING_LINEAR_GRADIENT_NODE: + if (gsk_linear_gradient_node_get_n_color_stops (node) < MAX_GRADIENT_STOPS) + gsk_ngl_render_job_visit_linear_gradient_node (job, node); + else + gsk_ngl_render_job_visit_as_fallback (job, node); + break; + + case GSK_OPACITY_NODE: + gsk_ngl_render_job_visit_opacity_node (job, node); + break; + + case GSK_OUTSET_SHADOW_NODE: + if (gsk_outset_shadow_node_get_blur_radius (node) > 0) + gsk_ngl_render_job_visit_blurred_outset_shadow_node (job, node); + else + gsk_ngl_render_job_visit_unblurred_outset_shadow_node (job, node); + break; + + case GSK_RADIAL_GRADIENT_NODE: + case GSK_REPEATING_RADIAL_GRADIENT_NODE: + gsk_ngl_render_job_visit_radial_gradient_node (job, node); + break; + + case GSK_REPEAT_NODE: + gsk_ngl_render_job_visit_repeat_node (job, node); + break; + + case GSK_ROUNDED_CLIP_NODE: + gsk_ngl_render_job_visit_rounded_clip_node (job, node); + break; + + case GSK_SHADOW_NODE: + gsk_ngl_render_job_visit_shadow_node (job, node); + break; + + case GSK_TEXT_NODE: + gsk_ngl_render_job_visit_text_node (job, + node, + gsk_text_node_get_color (node), + FALSE); + break; + + case GSK_TEXTURE_NODE: + gsk_ngl_render_job_visit_texture_node (job, node); + break; + + case GSK_TRANSFORM_NODE: + gsk_ngl_render_job_visit_transform_node (job, node); + break; + + case GSK_CAIRO_NODE: + gsk_ngl_render_job_visit_as_fallback (job, node); + break; + + case GSK_NOT_A_RENDER_NODE: + default: + g_assert_not_reached (); + break; + } +} + +static gboolean +gsk_ngl_render_job_visit_node_with_offscreen (GskNglRenderJob *job, + const GskRenderNode *node, + GskNglRenderOffscreen *offscreen) +{ + GskTextureKey key; + guint cached_id; + int filter; + + g_assert (job != NULL); + g_assert (node != NULL); + g_assert (offscreen != NULL); + g_assert (offscreen->texture_id == 0); + g_assert (offscreen->bounds != NULL); + + if (node_is_invisible (node)) + { + /* Just to be safe. */ + offscreen->texture_id = 0; + init_full_texture_region (offscreen); + offscreen->was_offscreen = FALSE; + return FALSE; + } + + if (gsk_render_node_get_node_type (node) == GSK_TEXTURE_NODE && + offscreen->force_offscreen == FALSE) + { + GdkTexture *texture = gsk_texture_node_get_texture (node); + gsk_ngl_render_job_upload_texture (job, texture, offscreen); + g_assert (offscreen->was_offscreen == FALSE); + return TRUE; + } + + filter = offscreen->linear_filter ? GL_LINEAR : GL_NEAREST; + + /* Check if we've already cached the drawn texture. */ + key.pointer = node; + key.pointer_is_child = TRUE; /* Don't conflict with the child using the cache too */ + key.parent_rect = *offscreen->bounds; + key.scale_x = job->scale_x; + key.scale_y = job->scale_y; + key.filter = filter; + + cached_id = gsk_ngl_driver_lookup_texture (job->driver, &key); + + if (cached_id != 0) + { + offscreen->texture_id = cached_id; + init_full_texture_region (offscreen); + /* We didn't render it offscreen, but hand out an offscreen texture id */ + offscreen->was_offscreen = TRUE; + return TRUE; + } + + float scaled_width; + float scaled_height; + float scale_x = job->scale_x; + float scale_y = job->scale_y; + + g_assert (job->command_queue->max_texture_size > 0); + + /* Tweak the scale factor so that the required texture doesn't + * exceed the max texture limit. This will render with a lower + * resolution, but this is better than clipping. + */ + { + int max_texture_size = job->command_queue->max_texture_size; + + scaled_width = ceilf (offscreen->bounds->size.width * scale_x); + if (scaled_width > max_texture_size) + { + scale_x *= (float)max_texture_size / scaled_width; + scaled_width = max_texture_size; + } + + scaled_height = ceilf (offscreen->bounds->size.height * scale_y); + if (scaled_height > max_texture_size) + { + scale_y *= (float)max_texture_size / scaled_height; + scaled_height = max_texture_size; + } + } + + GskNglRenderTarget *render_target; + graphene_matrix_t prev_projection; + graphene_rect_t prev_viewport; + graphene_rect_t viewport; + float offset_x = job->offset_x; + float offset_y = job->offset_y; + float prev_alpha; + guint prev_fbo; + + if (!gsk_ngl_driver_create_render_target (job->driver, + scaled_width, scaled_height, + filter, filter, + &render_target)) + g_assert_not_reached (); + + if (gdk_gl_context_has_debug (job->command_queue->context)) + { + gdk_gl_context_label_object_printf (job->command_queue->context, + GL_TEXTURE, + render_target->texture_id, + "Offscreen<%s> %d", + g_type_name_from_instance ((GTypeInstance *) node), + render_target->texture_id); + gdk_gl_context_label_object_printf (job->command_queue->context, + GL_FRAMEBUFFER, + render_target->framebuffer_id, + "Offscreen<%s> FB %d", + g_type_name_from_instance ((GTypeInstance *) node), + render_target->framebuffer_id); + } + + gsk_ngl_render_job_transform_bounds (job, offscreen->bounds, &viewport); + /* Code above will scale the size with the scale we use in the render ops, + * but for the viewport size, we need our own size limited by the texture size */ + viewport.size.width = scaled_width; + viewport.size.height = scaled_height; + + gsk_ngl_render_job_set_viewport (job, &viewport, &prev_viewport); + gsk_ngl_render_job_set_projection_from_rect (job, &job->viewport, &prev_projection); + gsk_ngl_render_job_set_modelview (job, gsk_transform_scale (NULL, scale_x, scale_y)); + prev_alpha = gsk_ngl_render_job_set_alpha (job, 1.0f); + job->offset_x = offset_x; + job->offset_y = offset_y; + + prev_fbo = gsk_ngl_command_queue_bind_framebuffer (job->command_queue, render_target->framebuffer_id); + gsk_ngl_command_queue_clear (job->command_queue, 0, &job->viewport); + + if (offscreen->reset_clip) + gsk_ngl_render_job_push_clip (job, &GSK_ROUNDED_RECT_INIT_FROM_RECT (job->viewport)); + + gsk_ngl_render_job_visit_node (job, node); + + if (offscreen->reset_clip) + gsk_ngl_render_job_pop_clip (job); + + gsk_ngl_render_job_pop_modelview (job); + gsk_ngl_render_job_set_viewport (job, &prev_viewport, NULL); + gsk_ngl_render_job_set_projection (job, &prev_projection); + gsk_ngl_render_job_set_alpha (job, prev_alpha); + gsk_ngl_command_queue_bind_framebuffer (job->command_queue, prev_fbo); + + job->offset_x = offset_x; + job->offset_y = offset_y; + + offscreen->was_offscreen = TRUE; + offscreen->texture_id = gsk_ngl_driver_release_render_target (job->driver, + render_target, + FALSE); + + init_full_texture_region (offscreen); + + if (!offscreen->do_not_cache) + gsk_ngl_driver_cache_texture (job->driver, &key, offscreen->texture_id); + + return TRUE; +} + +void +gsk_ngl_render_job_render_flipped (GskNglRenderJob *job, + GskRenderNode *root) +{ + graphene_matrix_t proj; + guint framebuffer_id; + guint texture_id; + guint surface_height; + + g_return_if_fail (job != NULL); + g_return_if_fail (root != NULL); + g_return_if_fail (GSK_IS_NGL_DRIVER (job->driver)); + + surface_height = job->viewport.size.height; + + graphene_matrix_init_ortho (&proj, + job->viewport.origin.x, + job->viewport.origin.x + job->viewport.size.width, + job->viewport.origin.y, + job->viewport.origin.y + job->viewport.size.height, + ORTHO_NEAR_PLANE, + ORTHO_FAR_PLANE); + graphene_matrix_scale (&proj, 1, -1, 1); + + if (!gsk_ngl_command_queue_create_render_target (job->command_queue, + MAX (1, job->viewport.size.width), + MAX (1, job->viewport.size.height), + GL_NEAREST, GL_NEAREST, + &framebuffer_id, &texture_id)) + return; + + /* Setup drawing to our offscreen texture/framebuffer which is flipped */ + gsk_ngl_command_queue_bind_framebuffer (job->command_queue, framebuffer_id); + gsk_ngl_command_queue_clear (job->command_queue, 0, &job->viewport); + + /* Visit all nodes creating batches */ + gdk_gl_context_push_debug_group (job->command_queue->context, "Building command queue"); + gsk_ngl_render_job_visit_node (job, root); + gdk_gl_context_pop_debug_group (job->command_queue->context); + + /* Now draw to our real destination, but flipped */ + gsk_ngl_render_job_set_alpha (job, 1.0f); + gsk_ngl_command_queue_bind_framebuffer (job->command_queue, job->framebuffer); + gsk_ngl_command_queue_clear (job->command_queue, 0, &job->viewport); + gsk_ngl_render_job_begin_draw (job, job->driver->blit); + gsk_ngl_program_set_uniform_texture (job->driver->blit, + UNIFORM_SHARED_SOURCE, 0, + GL_TEXTURE_2D, + GL_TEXTURE0, + texture_id); + gsk_ngl_render_job_draw_rect (job, &job->viewport); + gsk_ngl_render_job_end_draw (job); + + gdk_gl_context_push_debug_group (job->command_queue->context, "Executing command queue"); + gsk_ngl_command_queue_execute (job->command_queue, surface_height, 1, NULL); + gdk_gl_context_pop_debug_group (job->command_queue->context); + + glDeleteFramebuffers (1, &framebuffer_id); + glDeleteTextures (1, &texture_id); +} + +void +gsk_ngl_render_job_render (GskNglRenderJob *job, + GskRenderNode *root) +{ + G_GNUC_UNUSED gint64 start_time; + guint scale_factor; + guint surface_height; + + g_return_if_fail (job != NULL); + g_return_if_fail (root != NULL); + g_return_if_fail (GSK_IS_NGL_DRIVER (job->driver)); + + scale_factor = MAX (job->scale_x, job->scale_y); + surface_height = job->viewport.size.height; + + gsk_ngl_command_queue_make_current (job->command_queue); + + /* Build the command queue using the shared GL context for all renderers + * on the same display. + */ + start_time = GDK_PROFILER_CURRENT_TIME; + gdk_gl_context_push_debug_group (job->command_queue->context, "Building command queue"); + gsk_ngl_command_queue_bind_framebuffer (job->command_queue, job->framebuffer); + gsk_ngl_command_queue_clear (job->command_queue, 0, &job->viewport); + gsk_ngl_render_job_visit_node (job, root); + gdk_gl_context_pop_debug_group (job->command_queue->context); + gdk_profiler_add_mark (start_time, GDK_PROFILER_CURRENT_TIME-start_time, "Build GL command queue", ""); + +#if 0 + /* At this point the atlases have uploaded content while we processed + * nodes but have not necessarily been used by the commands in the queue. + */ + gsk_ngl_driver_save_atlases_to_png (job->driver, NULL); +#endif + + /* But now for executing the command queue, we want to use the context + * that was provided to us when creating the render job as framebuffer 0 + * is bound to that context. + */ + start_time = GDK_PROFILER_CURRENT_TIME; + gsk_ngl_command_queue_make_current (job->command_queue); + gdk_gl_context_push_debug_group (job->command_queue->context, "Executing command queue"); + gsk_ngl_command_queue_execute (job->command_queue, surface_height, scale_factor, job->region); + gdk_gl_context_pop_debug_group (job->command_queue->context); + gdk_profiler_add_mark (start_time, GDK_PROFILER_CURRENT_TIME-start_time, "Execute GL command queue", ""); +} + +void +gsk_ngl_render_job_set_debug_fallback (GskNglRenderJob *job, + gboolean debug_fallback) +{ + g_return_if_fail (job != NULL); + + job->debug_fallback = !!debug_fallback; +} + +GskNglRenderJob * +gsk_ngl_render_job_new (GskNglDriver *driver, + const graphene_rect_t *viewport, + float scale_factor, + const cairo_region_t *region, + guint framebuffer) +{ + const graphene_rect_t *clip_rect = viewport; + graphene_rect_t transformed_extents; + GskNglRenderJob *job; + + g_return_val_if_fail (GSK_IS_NGL_DRIVER (driver), NULL); + g_return_val_if_fail (viewport != NULL, NULL); + g_return_val_if_fail (scale_factor > 0, NULL); + + job = g_slice_new0 (GskNglRenderJob); + job->driver = g_object_ref (driver); + job->command_queue = job->driver->command_queue; + job->clip = g_array_sized_new (FALSE, FALSE, sizeof (GskNglRenderClip), 16); + job->modelview = g_array_sized_new (FALSE, FALSE, sizeof (GskNglRenderModelview), 16); + job->framebuffer = framebuffer; + job->offset_x = 0; + job->offset_y = 0; + job->scale_x = scale_factor; + job->scale_y = scale_factor; + job->viewport = *viewport; + + gsk_ngl_render_job_set_alpha (job, 1.0); + gsk_ngl_render_job_set_projection_from_rect (job, viewport, NULL); + gsk_ngl_render_job_set_modelview (job, gsk_transform_scale (NULL, scale_factor, scale_factor)); + + /* Setup our initial clip. If region is NULL then we are drawing the + * whole viewport. Otherwise, we need to convert the region to a + * bounding box and clip based on that. + */ + + if (region != NULL) + { + cairo_rectangle_int_t extents; + + cairo_region_get_extents (region, &extents); + gsk_ngl_render_job_transform_bounds (job, + &GRAPHENE_RECT_INIT (extents.x, + extents.y, + extents.width, + extents.height), + &transformed_extents); + clip_rect = &transformed_extents; + job->region = cairo_region_create_rectangle (&extents); + } + + gsk_ngl_render_job_push_clip (job, + &GSK_ROUNDED_RECT_INIT (clip_rect->origin.x, + clip_rect->origin.y, + clip_rect->size.width, + clip_rect->size.height)); + + return job; +} + +void +gsk_ngl_render_job_free (GskNglRenderJob *job) +{ + job->current_modelview = NULL; + job->current_clip = NULL; + + while (job->modelview->len > 0) + { + GskNglRenderModelview *modelview = &g_array_index (job->modelview, GskNglRenderModelview, job->modelview->len-1); + g_clear_pointer (&modelview->transform, gsk_transform_unref); + job->modelview->len--; + } + + g_clear_object (&job->driver); + g_clear_pointer (&job->region, cairo_region_destroy); + g_clear_pointer (&job->modelview, g_array_unref); + g_clear_pointer (&job->clip, g_array_unref); + g_slice_free (GskNglRenderJob, job); +} diff --git a/gsk/ngl/gsknglrenderjobprivate.h b/gsk/ngl/gsknglrenderjobprivate.h new file mode 100644 index 0000000000..ba3f3e49b7 --- /dev/null +++ b/gsk/ngl/gsknglrenderjobprivate.h @@ -0,0 +1,39 @@ +/* gsknglrenderjobprivate.h + * + * 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 + */ + +#ifndef __GSK_NGL_RENDER_JOB_H__ +#define __GSK_NGL_RENDER_JOB_H__ + +#include "gskngltypesprivate.h" + +GskNglRenderJob *gsk_ngl_render_job_new (GskNglDriver *driver, + const graphene_rect_t *viewport, + float scale_factor, + const cairo_region_t *region, + guint framebuffer); +void gsk_ngl_render_job_free (GskNglRenderJob *job); +void gsk_ngl_render_job_render (GskNglRenderJob *job, + GskRenderNode *root); +void gsk_ngl_render_job_render_flipped (GskNglRenderJob *job, + GskRenderNode *root); +void gsk_ngl_render_job_set_debug_fallback (GskNglRenderJob *job, + gboolean debug_fallback); + +#endif /* __GSK_NGL_RENDER_JOB_H__ */ diff --git a/gsk/ngl/gsknglshadowlibrary.c b/gsk/ngl/gsknglshadowlibrary.c new file mode 100644 index 0000000000..bcf524c8b7 --- /dev/null +++ b/gsk/ngl/gsknglshadowlibrary.c @@ -0,0 +1,228 @@ +/* gsknglshadowlibrary.c + * + * 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 "gskngldriverprivate.h" +#include "gsknglshadowlibraryprivate.h" + +#define MAX_UNUSED_FRAMES (16 * 5) + +struct _GskNglShadowLibrary +{ + GObject parent_instance; + GskNglDriver *driver; + GArray *shadows; +}; + +typedef struct _Shadow +{ + GskRoundedRect outline; + float blur_radius; + guint texture_id; + gint64 last_used_in_frame; +} Shadow; + +G_DEFINE_TYPE (GskNglShadowLibrary, gsk_ngl_shadow_library, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_DRIVER, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +GskNglShadowLibrary * +gsk_ngl_shadow_library_new (GskNglDriver *driver) +{ + g_return_val_if_fail (GSK_IS_NGL_DRIVER (driver), NULL); + + return g_object_new (GSK_TYPE_GL_SHADOW_LIBRARY, + "driver", driver, + NULL); +} + +static void +gsk_ngl_shadow_library_dispose (GObject *object) +{ + GskNglShadowLibrary *self = (GskNglShadowLibrary *)object; + + for (guint i = 0; i < self->shadows->len; i++) + { + const Shadow *shadow = &g_array_index (self->shadows, Shadow, i); + gsk_ngl_driver_release_texture_by_id (self->driver, shadow->texture_id); + } + + g_clear_pointer (&self->shadows, g_array_unref); + g_clear_object (&self->driver); + + G_OBJECT_CLASS (gsk_ngl_shadow_library_parent_class)->dispose (object); +} + +static void +gsk_ngl_shadow_library_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GskNglShadowLibrary *self = GSK_NGL_SHADOW_LIBRARY (object); + + switch (prop_id) + { + case PROP_DRIVER: + g_value_set_object (value, self->driver); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gsk_ngl_shadow_library_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GskNglShadowLibrary *self = GSK_NGL_SHADOW_LIBRARY (object); + + switch (prop_id) + { + case PROP_DRIVER: + self->driver = g_value_dup_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gsk_ngl_shadow_library_class_init (GskNglShadowLibraryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = gsk_ngl_shadow_library_dispose; + object_class->get_property = gsk_ngl_shadow_library_get_property; + object_class->set_property = gsk_ngl_shadow_library_set_property; + + properties [PROP_DRIVER] = + g_param_spec_object ("driver", + "Driver", + "Driver", + GSK_TYPE_NGL_DRIVER, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +gsk_ngl_shadow_library_init (GskNglShadowLibrary *self) +{ + self->shadows = g_array_new (FALSE, FALSE, sizeof (Shadow)); +} + +void +gsk_ngl_shadow_library_insert (GskNglShadowLibrary *self, + const GskRoundedRect *outline, + float blur_radius, + guint texture_id) +{ + Shadow *shadow; + + g_assert (GSK_IS_NGL_SHADOW_LIBRARY (self)); + g_assert (outline != NULL); + g_assert (texture_id != 0); + + gsk_ngl_driver_mark_texture_permanent (self->driver, texture_id); + + g_array_set_size (self->shadows, self->shadows->len + 1); + + shadow = &g_array_index (self->shadows, Shadow, self->shadows->len - 1); + shadow->outline = *outline; + shadow->blur_radius = blur_radius; + shadow->texture_id = texture_id; + shadow->last_used_in_frame = self->driver->current_frame_id; +} + +guint +gsk_ngl_shadow_library_lookup (GskNglShadowLibrary *self, + const GskRoundedRect *outline, + float blur_radius) +{ + Shadow *ret = NULL; + + g_assert (GSK_IS_NGL_SHADOW_LIBRARY (self)); + g_assert (outline != NULL); + + /* Ensure GskRoundedRect is 12 packed floats without padding + * so that we can use memcmp instead of float comparisons. + */ + G_STATIC_ASSERT (sizeof *outline == (sizeof (float) * 12)); + + for (guint i = 0; i < self->shadows->len; i++) + { + Shadow *shadow = &g_array_index (self->shadows, Shadow, i); + + if (blur_radius == shadow->blur_radius && + memcmp (outline, &shadow->outline, sizeof *outline) == 0) + { + ret = shadow; + break; + } + } + + if (ret == NULL) + return 0; + + g_assert (ret->texture_id != 0); + + ret->last_used_in_frame = self->driver->current_frame_id; + + return ret->texture_id; +} + +void +gsk_ngl_shadow_library_begin_frame (GskNglShadowLibrary *self) +{ + gint64 watermark; + int i; + int p; + + g_return_if_fail (GSK_IS_NGL_SHADOW_LIBRARY (self)); + + watermark = self->driver->current_frame_id - MAX_UNUSED_FRAMES; + + for (i = 0, p = self->shadows->len; i < p; i++) + { + const Shadow *shadow = &g_array_index (self->shadows, Shadow, i); + + if (shadow->last_used_in_frame < watermark) + { + gsk_ngl_driver_release_texture_by_id (self->driver, shadow->texture_id); + g_array_remove_index_fast (self->shadows, i); + p--; + i--; + } + } +} diff --git a/gsk/ngl/gsknglshadowlibraryprivate.h b/gsk/ngl/gsknglshadowlibraryprivate.h new file mode 100644 index 0000000000..3c534663dc --- /dev/null +++ b/gsk/ngl/gsknglshadowlibraryprivate.h @@ -0,0 +1,44 @@ +/* gsknglshadowlibraryprivate.h + * + * 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 + */ + +#ifndef __GSK_NGL_SHADOW_LIBRARY_PRIVATE_H__ +#define __GSK_NGL_SHADOW_LIBRARY_PRIVATE_H__ + +#include "gskngltexturelibraryprivate.h" + +G_BEGIN_DECLS + +#define GSK_TYPE_GL_SHADOW_LIBRARY (gsk_ngl_shadow_library_get_type()) + +G_DECLARE_FINAL_TYPE (GskNglShadowLibrary, gsk_ngl_shadow_library, GSK, NGL_SHADOW_LIBRARY, GObject) + +GskNglShadowLibrary *gsk_ngl_shadow_library_new (GskNglDriver *driver); +void gsk_ngl_shadow_library_begin_frame (GskNglShadowLibrary *self); +guint gsk_ngl_shadow_library_lookup (GskNglShadowLibrary *self, + const GskRoundedRect *outline, + float blur_radius); +void gsk_ngl_shadow_library_insert (GskNglShadowLibrary *self, + const GskRoundedRect *outline, + float blur_radius, + guint texture_id); + +G_END_DECLS + +#endif /* __GSK_NGL_SHADOW_LIBRARY_PRIVATE_H__ */ diff --git a/gsk/ngl/gskngltexturelibrary.c b/gsk/ngl/gskngltexturelibrary.c new file mode 100644 index 0000000000..dc9303f373 --- /dev/null +++ b/gsk/ngl/gskngltexturelibrary.c @@ -0,0 +1,315 @@ +/* gskngltexturelibrary.c + * + * 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 "gsknglcommandqueueprivate.h" +#include "gskngldriverprivate.h" +#include "gskngltexturelibraryprivate.h" + +G_DEFINE_ABSTRACT_TYPE (GskNglTextureLibrary, gsk_ngl_texture_library, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_DRIVER, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + +static void +gsk_ngl_texture_library_constructed (GObject *object) +{ + G_OBJECT_CLASS (gsk_ngl_texture_library_parent_class)->constructed (object); + + g_assert (GSK_NGL_TEXTURE_LIBRARY (object)->hash_table != NULL); +} + +static void +gsk_ngl_texture_library_dispose (GObject *object) +{ + GskNglTextureLibrary *self = (GskNglTextureLibrary *)object; + + g_clear_object (&self->driver); + + G_OBJECT_CLASS (gsk_ngl_texture_library_parent_class)->dispose (object); +} + +static void +gsk_ngl_texture_library_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GskNglTextureLibrary *self = GSK_NGL_TEXTURE_LIBRARY (object); + + switch (prop_id) + { + case PROP_DRIVER: + g_value_set_object (value, self->driver); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gsk_ngl_texture_library_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GskNglTextureLibrary *self = GSK_NGL_TEXTURE_LIBRARY (object); + + switch (prop_id) + { + case PROP_DRIVER: + self->driver = g_value_dup_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gsk_ngl_texture_library_class_init (GskNglTextureLibraryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = gsk_ngl_texture_library_constructed; + object_class->dispose = gsk_ngl_texture_library_dispose; + object_class->get_property = gsk_ngl_texture_library_get_property; + object_class->set_property = gsk_ngl_texture_library_set_property; + + properties [PROP_DRIVER] = + g_param_spec_object ("driver", + "Driver", + "Driver", + GSK_TYPE_NGL_DRIVER, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +gsk_ngl_texture_library_init (GskNglTextureLibrary *self) +{ +} + +void +gsk_ngl_texture_library_set_funcs (GskNglTextureLibrary *self, + GHashFunc hash_func, + GEqualFunc equal_func, + GDestroyNotify key_destroy, + GDestroyNotify value_destroy) +{ + g_return_if_fail (GSK_IS_NGL_TEXTURE_LIBRARY (self)); + g_return_if_fail (self->hash_table == NULL); + + self->hash_table = g_hash_table_new_full (hash_func, equal_func, + key_destroy, value_destroy); +} + +void +gsk_ngl_texture_library_begin_frame (GskNglTextureLibrary *self) +{ + g_return_if_fail (GSK_IS_NGL_TEXTURE_LIBRARY (self)); + + if (GSK_NGL_TEXTURE_LIBRARY_GET_CLASS (self)->begin_frame) + GSK_NGL_TEXTURE_LIBRARY_GET_CLASS (self)->begin_frame (self); +} + +void +gsk_ngl_texture_library_end_frame (GskNglTextureLibrary *self) +{ + g_return_if_fail (GSK_IS_NGL_TEXTURE_LIBRARY (self)); + + if (GSK_NGL_TEXTURE_LIBRARY_GET_CLASS (self)->end_frame) + GSK_NGL_TEXTURE_LIBRARY_GET_CLASS (self)->end_frame (self); +} + +static GskNglTexture * +gsk_ngl_texture_library_pack_one (GskNglTextureLibrary *self, + guint width, + guint height) +{ + GskNglTexture *texture; + + g_assert (GSK_IS_NGL_TEXTURE_LIBRARY (self)); + + if (width > self->driver->command_queue->max_texture_size || + height > self->driver->command_queue->max_texture_size) + { + g_warning ("Clipping requested texture of size %ux%u to maximum allowable size %u.", + width, height, self->driver->command_queue->max_texture_size); + width = MIN (width, self->driver->command_queue->max_texture_size); + height = MIN (height, self->driver->command_queue->max_texture_size); + } + + texture = gsk_ngl_driver_create_texture (self->driver, width, height, GL_LINEAR, GL_LINEAR); + texture->permanent = TRUE; + + return texture; +} + +static inline gboolean +gsk_ngl_texture_atlas_pack (GskNglTextureAtlas *self, + int width, + int height, + int *out_x, + int *out_y) +{ + stbrp_rect rect; + + rect.w = width; + rect.h = height; + + stbrp_pack_rects (&self->context, &rect, 1); + + if (rect.was_packed) + { + *out_x = rect.x; + *out_y = rect.y; + } + + return rect.was_packed; +} + +static void +gsk_ngl_texture_atlases_pack (GskNglDriver *driver, + int width, + int height, + GskNglTextureAtlas **out_atlas, + int *out_x, + int *out_y) +{ + GskNglTextureAtlas *atlas = NULL; + int x, y; + + for (guint i = 0; i < driver->atlases->len; i++) + { + atlas = g_ptr_array_index (driver->atlases, i); + + if (gsk_ngl_texture_atlas_pack (atlas, width, height, &x, &y)) + break; + + atlas = NULL; + } + + if (atlas == NULL) + { + /* No atlas has enough space, so create a new one... */ + atlas = gsk_ngl_driver_create_atlas (driver); + + /* Pack it onto that one, which surely has enough space... */ + if (!gsk_ngl_texture_atlas_pack (atlas, width, height, &x, &y)) + g_assert_not_reached (); + } + + *out_atlas = atlas; + *out_x = x; + *out_y = y; +} + +gpointer +gsk_ngl_texture_library_pack (GskNglTextureLibrary *self, + gpointer key, + gsize valuelen, + guint width, + guint height, + int padding, + guint *out_packed_x, + guint *out_packed_y) +{ + GskNglTextureAtlasEntry *entry; + GskNglTextureAtlas *atlas = NULL; + + g_assert (GSK_IS_NGL_TEXTURE_LIBRARY (self)); + g_assert (key != NULL); + g_assert (valuelen > sizeof (GskNglTextureAtlasEntry)); + g_assert (out_packed_x != NULL); + g_assert (out_packed_y != NULL); + + entry = g_slice_alloc0 (valuelen); + entry->n_pixels = width * height; + entry->accessed = TRUE; + + /* If our size is invisible then we just want an entry in the + * cache for faster lookups, but do not actually spend any texture + * allocations on this entry. + */ + if (width <= 0 && height <= 0) + { + entry->is_atlased = FALSE; + entry->texture = NULL; + entry->area.x = 0.0f; + entry->area.y = 0.0f; + entry->area.x2 = 0.0f; + entry->area.y2 = 0.0f; + + *out_packed_x = 0; + *out_packed_y = 0; + } + else if (width <= self->max_entry_size && height <= self->max_entry_size) + { + int packed_x; + int packed_y; + + gsk_ngl_texture_atlases_pack (self->driver, + padding + width + padding, + padding + height + padding, + &atlas, + &packed_x, + &packed_y); + + entry->atlas = atlas; + entry->is_atlased = TRUE; + entry->area.x = (float)(packed_x + padding) / atlas->width; + entry->area.y = (float)(packed_y + padding) / atlas->height; + entry->area.x2 = entry->area.x + (float)width / atlas->width; + entry->area.y2 = entry->area.y + (float)height / atlas->height; + + *out_packed_x = packed_x; + *out_packed_y = packed_y; + } + else + { + GskNglTexture *texture = gsk_ngl_texture_library_pack_one (self, + padding + width + padding, + padding + height + padding); + + entry->texture = texture; + entry->is_atlased = FALSE; + entry->accessed = TRUE; + entry->area.x = 0.0f; + entry->area.y = 0.0f; + entry->area.x2 = 1.0f; + entry->area.y2 = 1.0f; + + *out_packed_x = padding; + *out_packed_y = padding; + } + + g_hash_table_insert (self->hash_table, key, entry); + + return entry; +} diff --git a/gsk/ngl/gskngltexturelibraryprivate.h b/gsk/ngl/gskngltexturelibraryprivate.h new file mode 100644 index 0000000000..56c3d604cc --- /dev/null +++ b/gsk/ngl/gskngltexturelibraryprivate.h @@ -0,0 +1,202 @@ +/* gskngltexturelibraryprivate.h + * + * Copyright 2020 Christian Hergert + * + * This file 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 file 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 General Public License along + * with this program. If not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef __GSK_NGL_TEXTURE_LIBRARY_PRIVATE_H__ +#define __GSK_NGL_TEXTURE_LIBRARY_PRIVATE_H__ + +#include "gskngltypesprivate.h" +#include "gskngltexturepoolprivate.h" + +#include "../gl/stb_rect_pack.h" + +G_BEGIN_DECLS + +#define GSK_TYPE_GL_TEXTURE_LIBRARY (gsk_ngl_texture_library_get_type ()) +#define GSK_NGL_TEXTURE_LIBRARY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GSK_TYPE_GL_TEXTURE_LIBRARY, GskNglTextureLibrary)) +#define GSK_IS_NGL_TEXTURE_LIBRARY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GSK_TYPE_GL_TEXTURE_LIBRARY)) +#define GSK_NGL_TEXTURE_LIBRARY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GSK_TYPE_GL_TEXTURE_LIBRARY, GskNglTextureLibraryClass)) +#define GSK_IS_NGL_TEXTURE_LIBRARY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GSK_TYPE_GL_TEXTURE_LIBRARY)) +#define GSK_NGL_TEXTURE_LIBRARY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GSK_TYPE_GL_TEXTURE_LIBRARY, GskNglTextureLibraryClass)) + +typedef struct _GskNglTextureAtlas +{ + struct stbrp_context context; + struct stbrp_node *nodes; + + int width; + int height; + + guint texture_id; + + /* Pixels of rects that have been used at some point, + * But are now unused. + */ + int unused_pixels; + + void *user_data; +} GskNglTextureAtlas; + +typedef struct _GskNglTextureAtlasEntry +{ + /* A backreference to either the atlas or texture containing + * the contents of the atlas entry. For larger items, no atlas + * is used and instead a direct texture. + */ + union { + GskNglTextureAtlas *atlas; + GskNglTexture *texture; + }; + + /* The area within the atlas translated to 0..1 bounds */ + struct { + float x; + float y; + float x2; + float y2; + } area; + + /* Number of pixels in the entry, used to calculate usage + * of an atlas while processing. + */ + guint n_pixels : 29; + + /* If entry has marked pixels as used in the atlas this frame */ + guint used : 1; + + /* If entry was accessed this frame */ + guint accessed : 1; + + /* When true, backref is an atlas, otherwise texture */ + guint is_atlased : 1; + + /* Suffix data that is per-library specific. gpointer used to + * guarantee the alignment for the entries using this. + */ + gpointer data[0]; +} GskNglTextureAtlasEntry; + +typedef struct _GskNglTextureLibrary +{ + GObject parent_instance; + GskNglDriver *driver; + GHashTable *hash_table; + guint max_entry_size; +} GskNglTextureLibrary; + +typedef struct _GskNglTextureLibraryClass +{ + GObjectClass parent_class; + + void (*begin_frame) (GskNglTextureLibrary *library); + void (*end_frame) (GskNglTextureLibrary *library); +} GskNglTextureLibraryClass; + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GskNglTextureLibrary, g_object_unref) + +GType gsk_ngl_texture_library_get_type (void) G_GNUC_CONST; +void gsk_ngl_texture_library_set_funcs (GskNglTextureLibrary *self, + GHashFunc hash_func, + GEqualFunc equal_func, + GDestroyNotify key_destroy, + GDestroyNotify value_destroy); +void gsk_ngl_texture_library_begin_frame (GskNglTextureLibrary *self); +void gsk_ngl_texture_library_end_frame (GskNglTextureLibrary *self); +gpointer gsk_ngl_texture_library_pack (GskNglTextureLibrary *self, + gpointer key, + gsize valuelen, + guint width, + guint height, + int padding, + guint *out_packed_x, + guint *out_packed_y); + +static inline void +gsk_ngl_texture_atlas_mark_unused (GskNglTextureAtlas *self, + int n_pixels) +{ + self->unused_pixels += n_pixels; +} + +static inline void +gsk_ngl_texture_atlas_mark_used (GskNglTextureAtlas *self, + int n_pixels) +{ + self->unused_pixels -= n_pixels; +} + +static inline gboolean +gsk_ngl_texture_library_lookup (GskNglTextureLibrary *self, + gconstpointer key, + GskNglTextureAtlasEntry **out_entry) +{ + GskNglTextureAtlasEntry *entry = g_hash_table_lookup (self->hash_table, key); + + if G_LIKELY (entry != NULL && entry->accessed && entry->used) + { + *out_entry = entry; + return TRUE; + } + + if (entry != NULL) + { + if (!entry->used && entry->is_atlased) + { + g_assert (entry->atlas != NULL); + gsk_ngl_texture_atlas_mark_used (entry->atlas, entry->n_pixels); + entry->used = TRUE; + } + + entry->accessed = TRUE; + *out_entry = entry; + return TRUE; + } + + return FALSE; +} + +static inline guint +GSK_NGL_TEXTURE_ATLAS_ENTRY_TEXTURE (gconstpointer d) +{ + const GskNglTextureAtlasEntry *e = d; + + return e->is_atlased ? e->atlas->texture_id + : e->texture ? e->texture->texture_id : 0; +} + +static inline double +gsk_ngl_texture_atlas_get_unused_ratio (const GskNglTextureAtlas *self) +{ + if (self->unused_pixels > 0) + return (double)(self->unused_pixels) / (double)(self->width * self->height); + return 0.0; +} + +static inline gboolean +gsk_ngl_texture_library_can_cache (GskNglTextureLibrary *self, + int width, + int height) +{ + g_assert (self->max_entry_size > 0); + return width <= self->max_entry_size && height <= self->max_entry_size; +} + +G_END_DECLS + +#endif /* __GSK_NGL_TEXTURE_LIBRARY_PRIVATE_H__ */ diff --git a/gsk/ngl/gskngltexturepool.c b/gsk/ngl/gskngltexturepool.c new file mode 100644 index 0000000000..707ae37455 --- /dev/null +++ b/gsk/ngl/gskngltexturepool.c @@ -0,0 +1,188 @@ +/* gskngltexturepool.c + * + * Copyright 2020 Christian Hergert + * + * This file 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 file 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 General Public License along + * with this program. If not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "config.h" + +#include + +#include "gskngltexturepoolprivate.h" +#include "ninesliceprivate.h" + +void +gsk_ngl_texture_free (GskNglTexture *texture) +{ + if (texture != NULL) + { + g_assert (texture->link.prev == NULL); + g_assert (texture->link.next == NULL); + + if (texture->user) + g_clear_pointer (&texture->user, gdk_texture_clear_render_data); + + if (texture->texture_id != 0) + { + glDeleteTextures (1, &texture->texture_id); + texture->texture_id = 0; + } + + for (guint i = 0; i < texture->n_slices; i++) + { + glDeleteTextures (1, &texture->slices[i].texture_id); + texture->slices[i].texture_id = 0; + } + + g_clear_pointer (&texture->slices, g_free); + g_clear_pointer (&texture->nine_slice, g_free); + + g_slice_free (GskNglTexture, texture); + } +} + +void +gsk_ngl_texture_pool_init (GskNglTexturePool *self) +{ + g_queue_init (&self->queue); +} + +void +gsk_ngl_texture_pool_clear (GskNglTexturePool *self) +{ + guint *free_me = NULL; + guint *texture_ids; + guint i = 0; + + if G_LIKELY (self->queue.length <= 1024) + texture_ids = g_newa (guint, self->queue.length); + else + texture_ids = free_me = g_new (guint, self->queue.length); + + while (self->queue.length > 0) + { + GskNglTexture *head = g_queue_peek_head (&self->queue); + + g_queue_unlink (&self->queue, &head->link); + + texture_ids[i++] = head->texture_id; + head->texture_id = 0; + + gsk_ngl_texture_free (head); + } + + g_assert (self->queue.length == 0); + + if (i > 0) + glDeleteTextures (i, texture_ids); + + g_free (free_me); +} + +void +gsk_ngl_texture_pool_put (GskNglTexturePool *self, + GskNglTexture *texture) +{ + g_assert (self != NULL); + g_assert (texture != NULL); + g_assert (texture->user == NULL); + g_assert (texture->link.prev == NULL); + g_assert (texture->link.next == NULL); + g_assert (texture->link.data == texture); + + if (texture->permanent) + gsk_ngl_texture_free (texture); + else + g_queue_push_tail_link (&self->queue, &texture->link); +} + +GskNglTexture * +gsk_ngl_texture_pool_get (GskNglTexturePool *self, + int width, + int height, + int min_filter, + int mag_filter) +{ + GskNglTexture *texture; + + g_assert (self != NULL); + + texture = g_slice_new0 (GskNglTexture); + texture->link.data = texture; + texture->min_filter = min_filter; + texture->mag_filter = mag_filter; + + glGenTextures (1, &texture->texture_id); + + glActiveTexture (GL_TEXTURE0); + glBindTexture (GL_TEXTURE_2D, texture->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); + + if (gdk_gl_context_get_use_es (gdk_gl_context_get_current ())) + glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + else + glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL); + + glBindTexture (GL_TEXTURE_2D, 0); + + return texture; +} + +GskNglTexture * +gsk_ngl_texture_new (guint texture_id, + int width, + int height, + int min_filter, + int mag_filter, + gint64 frame_id) +{ + GskNglTexture *texture; + + texture = g_slice_new0 (GskNglTexture); + texture->texture_id = texture_id; + texture->link.data = texture; + texture->min_filter = min_filter; + texture->mag_filter = mag_filter; + texture->width = width; + texture->height = height; + texture->last_used_in_frame = frame_id; + + return texture; +} + +const GskNglTextureNineSlice * +gsk_ngl_texture_get_nine_slice (GskNglTexture *texture, + const GskRoundedRect *outline, + float extra_pixels) +{ + g_assert (texture != NULL); + g_assert (outline != NULL); + + if G_UNLIKELY (texture->nine_slice == NULL) + { + texture->nine_slice = g_new0 (GskNglTextureNineSlice, 9); + + nine_slice_rounded_rect (texture->nine_slice, outline); + nine_slice_grow (texture->nine_slice, extra_pixels); + nine_slice_to_texture_coords (texture->nine_slice, texture->width, texture->height); + } + + return texture->nine_slice; +} diff --git a/gsk/ngl/gskngltexturepoolprivate.h b/gsk/ngl/gskngltexturepoolprivate.h new file mode 100644 index 0000000000..8b39ec5440 --- /dev/null +++ b/gsk/ngl/gskngltexturepoolprivate.h @@ -0,0 +1,102 @@ +/* gskngltexturepoolprivate.h + * + * Copyright 2020 Christian Hergert + * + * This file 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 file 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 General Public License along + * with this program. If not, see . + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#ifndef _GSK_NGL_TEXTURE_POOL_PRIVATE_H__ +#define _GSK_NGL_TEXTURE_POOL_PRIVATE_H__ + +#include "gskngltypesprivate.h" + +G_BEGIN_DECLS + +typedef struct _GskNglTexturePool +{ + GQueue queue; +} GskNglTexturePool; + +struct _GskNglTextureSlice +{ + cairo_rectangle_int_t rect; + guint texture_id; +}; + +struct _GskNglTextureNineSlice +{ + cairo_rectangle_int_t rect; + struct { + float x; + float y; + float x2; + float y2; + } area; +}; + +struct _GskNglTexture +{ + /* Used to insert into queue */ + GList link; + + /* Identifier of the frame that created it */ + gint64 last_used_in_frame; + + /* Backpointer to texture (can be cleared asynchronously) */ + GdkTexture *user; + + /* Only used by sliced textures */ + GskNglTextureSlice *slices; + guint n_slices; + + /* Only used by nine-slice textures */ + GskNglTextureNineSlice *nine_slice; + + /* The actual GL texture identifier in some shared context */ + guint texture_id; + + int width; + int height; + int min_filter; + int mag_filter; + + /* Set when used by an atlas so we don't drop the texture */ + guint permanent : 1; +}; + +void gsk_ngl_texture_pool_init (GskNglTexturePool *self); +void gsk_ngl_texture_pool_clear (GskNglTexturePool *self); +GskNglTexture *gsk_ngl_texture_pool_get (GskNglTexturePool *self, + int width, + int height, + int min_filter, + int mag_filter); +void gsk_ngl_texture_pool_put (GskNglTexturePool *self, + GskNglTexture *texture); +GskNglTexture *gsk_ngl_texture_new (guint texture_id, + int width, + int height, + int min_filter, + int mag_filter, + gint64 frame_id); +const GskNglTextureNineSlice *gsk_ngl_texture_get_nine_slice (GskNglTexture *texture, + const GskRoundedRect *outline, + float extra_pixels); +void gsk_ngl_texture_free (GskNglTexture *texture); + +G_END_DECLS + +#endif /* _GSK_NGL_TEXTURE_POOL_PRIVATE_H__ */ diff --git a/gsk/ngl/gskngltypesprivate.h b/gsk/ngl/gskngltypesprivate.h new file mode 100644 index 0000000000..aba6f2f4c9 --- /dev/null +++ b/gsk/ngl/gskngltypesprivate.h @@ -0,0 +1,62 @@ +/* gskngltypesprivate.h + * + * 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 + */ + +#ifndef __GSK_NGL_TYPES_PRIVATE_H__ +#define __GSK_NGL_TYPES_PRIVATE_H__ + +#include +#include +#include +#include + +G_BEGIN_DECLS + +#define GSK_NGL_N_VERTICES 6 + +typedef struct _GskNglAttachmentState GskNglAttachmentState; +typedef struct _GskNglBuffer GskNglBuffer; +typedef struct _GskNglCommandQueue GskNglCommandQueue; +typedef struct _GskNglCompiler GskNglCompiler; +typedef struct _GskNglDrawVertex GskNglDrawVertex; +typedef struct _GskNglRenderTarget GskNglRenderTarget; +typedef struct _GskNglGlyphLibrary GskNglGlyphLibrary; +typedef struct _GskNglIconLibrary GskNglIconLibrary; +typedef struct _GskNglProgram GskNglProgram; +typedef struct _GskNglRenderJob GskNglRenderJob; +typedef struct _GskNglShadowLibrary GskNglShadowLibrary; +typedef struct _GskNglTexture GskNglTexture; +typedef struct _GskNglTextureSlice GskNglTextureSlice; +typedef struct _GskNglTextureAtlas GskNglTextureAtlas; +typedef struct _GskNglTextureLibrary GskNglTextureLibrary; +typedef struct _GskNglTextureNineSlice GskNglTextureNineSlice; +typedef struct _GskNglUniformInfo GskNglUniformInfo; +typedef struct _GskNglUniformProgram GskNglUniformProgram; +typedef struct _GskNglUniformState GskNglUniformState; +typedef struct _GskNglDriver GskNglDriver; + +struct _GskNglDrawVertex +{ + float position[2]; + float uv[2]; +}; + +G_END_DECLS + +#endif /* __GSK_NGL_TYPES_PRIVATE_H__ */ diff --git a/gsk/ngl/gskngluniformstate.c b/gsk/ngl/gskngluniformstate.c new file mode 100644 index 0000000000..9b896e7d1a --- /dev/null +++ b/gsk/ngl/gskngluniformstate.c @@ -0,0 +1,270 @@ +/* gskngluniformstate.c + * + * 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 "gskngluniformstateprivate.h" + +static const guint8 uniform_sizes[] = { + 0, + + sizeof (Uniform1f), + sizeof (Uniform2f), + sizeof (Uniform3f), + sizeof (Uniform4f), + + sizeof (Uniform1f), + sizeof (Uniform2f), + sizeof (Uniform3f), + sizeof (Uniform4f), + + sizeof (Uniform1i), + sizeof (Uniform2i), + sizeof (Uniform3i), + sizeof (Uniform4i), + + sizeof (Uniform1ui), + + sizeof (guint), + + sizeof (graphene_matrix_t), + sizeof (GskRoundedRect), + sizeof (GdkRGBA), + + 0, +}; + +GskNglUniformState * +gsk_ngl_uniform_state_new (void) +{ + GskNglUniformState *state; + + state = g_atomic_rc_box_new0 (GskNglUniformState); + state->programs = g_hash_table_new_full (NULL, NULL, NULL, g_free); + state->values_len = 4096; + state->values_pos = 0; + state->values_buf = g_malloc (4096); + + return g_steal_pointer (&state); +} + +GskNglUniformState * +gsk_ngl_uniform_state_ref (GskNglUniformState *state) +{ + return g_atomic_rc_box_acquire (state); +} + +static void +gsk_ngl_uniform_state_finalize (gpointer data) +{ + GskNglUniformState *state = data; + + g_clear_pointer (&state->programs, g_hash_table_unref); + g_clear_pointer (&state->values_buf, g_free); +} + +void +gsk_ngl_uniform_state_unref (GskNglUniformState *state) +{ + g_atomic_rc_box_release_full (state, gsk_ngl_uniform_state_finalize); +} + +gpointer +gsk_ngl_uniform_state_init_value (GskNglUniformState *state, + GskNglUniformProgram *program, + GskNglUniformFormat format, + guint array_count, + guint location, + GskNglUniformInfoElement **infoptr) +{ + GskNglUniformInfoElement *info; + guint offset; + + g_assert (state != NULL); + g_assert (array_count < 32); + g_assert ((int)format >= 0 && format < GSK_NGL_UNIFORM_FORMAT_LAST); + g_assert (format > 0); + g_assert (program != NULL); + g_assert (program->sparse != NULL); + g_assert (program->n_sparse <= program->n_uniforms); + g_assert (location < GL_MAX_UNIFORM_LOCATIONS || location == (guint)-1); + g_assert (location < program->n_uniforms); + + /* Handle unused uniforms gracefully */ + if G_UNLIKELY (location == (guint)-1) + return NULL; + + info = &program->uniforms[location]; + + if G_LIKELY (format == info->info.format) + { + if G_LIKELY (array_count <= info->info.array_count) + { + *infoptr = info; + return GSK_NGL_UNIFORM_VALUE (state->values_buf, info->info.offset); + } + + /* We found the uniform, but there is not enough space for the + * amount that was requested. Instead, allocate new space and + * set the value to "initial" so that the caller just writes + * over the previous value. + * + * This can happen when using dynamic array lengths like the + * "n_color_stops" in gradient shaders. + */ + goto setup_info; + } + else if (info->info.format == 0) + { + goto setup_info; + } + else + { + g_critical ("Attempt to access uniform with different type of value " + "than it was initialized with. Program %u Location %u. " + "Was %d now %d (array length %d now %d).", + program->program_id, location, info->info.format, format, + info->info.array_count, array_count); + *infoptr = NULL; + return NULL; + } + +setup_info: + + gsk_ngl_uniform_state_realloc (state, + uniform_sizes[format] * MAX (1, array_count), + &offset); + + /* we have 21 bits for offset */ + g_assert (offset < (1 << GSK_NGL_UNIFORM_OFFSET_BITS)); + + /* We could once again be setting up this info if the array size grew. + * So make sure that we have space in our space array for the value. + */ + g_assert (info->info.format != 0 || program->n_sparse < program->n_uniforms); + if (info->info.format == 0) + program->sparse[program->n_sparse++] = location; + + info->info.format = format; + info->info.offset = offset; + info->info.array_count = array_count; + info->info.initial = TRUE; + info->stamp = 0; + + *infoptr = info; + + return GSK_NGL_UNIFORM_VALUE (state->values_buf, info->info.offset); +} + +void +gsk_ngl_uniform_state_end_frame (GskNglUniformState *state) +{ + GHashTableIter iter; + GskNglUniformProgram *program; + guint allocator = 0; + + g_return_if_fail (state != NULL); + + /* After a frame finishes, we want to remove all our copies of uniform + * data that isn't needed any longer. Since we treat it as uninitialized + * after this frame (to reset it on first use next frame) we can just + * discard it but keep an allocation around to reuse. + */ + + g_hash_table_iter_init (&iter, state->programs); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&program)) + { + for (guint j = 0; j < program->n_sparse; j++) + { + guint location = program->sparse[j]; + GskNglUniformInfoElement *info = &program->uniforms[location]; + guint size; + + g_assert (info->info.format > 0); + + /* Calculate how much size is needed for the uniform, including arrays */ + size = uniform_sizes[info->info.format] * MAX (1, info->info.array_count); + + /* Adjust alignment for value */ + allocator += gsk_ngl_uniform_state_align (allocator, size); + + /* Offset is in slots of 4 bytes */ + info->info.offset = allocator / 4; + info->info.initial = TRUE; + info->stamp = 0; + + /* Now advance for this items data */ + allocator += size; + } + } + + state->values_pos = allocator; + + g_assert (allocator <= state->values_len); +} + +gsize +gsk_ngl_uniform_format_size (GskNglUniformFormat format) +{ + g_assert (format > 0); + g_assert (format < GSK_NGL_UNIFORM_FORMAT_LAST); + + return uniform_sizes[format]; +} + +GskNglUniformProgram * +gsk_ngl_uniform_state_get_program (GskNglUniformState *state, + guint program, + guint n_uniforms) +{ + GskNglUniformProgram *ret; + + g_return_val_if_fail (state != NULL, NULL); + g_return_val_if_fail (program > 0, NULL); + g_return_val_if_fail (program < G_MAXUINT, NULL); + + ret = g_hash_table_lookup (state->programs, GUINT_TO_POINTER (program)); + + if (ret == NULL) + { + gsize uniform_size = n_uniforms * sizeof (GskNglUniformInfoElement); + gsize sparse_size = n_uniforms * sizeof (guint); + gsize size = sizeof (GskNglUniformProgram) + uniform_size + sparse_size; + + /* Must be multiple of 4 for space pointer to align */ + G_STATIC_ASSERT (sizeof (GskNglUniformInfoElement) == 8); + + ret = g_malloc0 (size); + ret->program_id = program; + ret->n_uniforms = n_uniforms; + ret->n_sparse = 0; + ret->sparse = (guint *)&ret->uniforms[n_uniforms]; + + for (guint i = 0; i < n_uniforms; i++) + ret->uniforms[i].info.initial = TRUE; + + g_hash_table_insert (state->programs, GUINT_TO_POINTER (program), ret); + } + + return ret; +} diff --git a/gsk/ngl/gskngluniformstateprivate.h b/gsk/ngl/gskngluniformstateprivate.h new file mode 100644 index 0000000000..1385f93dac --- /dev/null +++ b/gsk/ngl/gskngluniformstateprivate.h @@ -0,0 +1,685 @@ +/* gskngluniformstateprivate.h + * + * 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 + */ + +#ifndef GSK_NGL_UNIFORM_STATE_PRIVATE_H +#define GSK_NGL_UNIFORM_STATE_PRIVATE_H + +#include "gskngltypesprivate.h" + +G_BEGIN_DECLS + +typedef struct { float v0; } Uniform1f; +typedef struct { float v0; float v1; } Uniform2f; +typedef struct { float v0; float v1; float v2; } Uniform3f; +typedef struct { float v0; float v1; float v2; float v3; } Uniform4f; + +typedef struct { int v0; } Uniform1i; +typedef struct { int v0; int v1; } Uniform2i; +typedef struct { int v0; int v1; int v2; } Uniform3i; +typedef struct { int v0; int v1; int v2; int v3; } Uniform4i; + +typedef struct { guint v0; } Uniform1ui; + +#define GSK_NGL_UNIFORM_ARRAY_BITS 5 +#define GSK_NGL_UNIFORM_FORMAT_BITS 5 +#define GSK_NGL_UNIFORM_OFFSET_BITS 21 + +typedef struct _GskNglUniformInfo +{ + guint initial : 1; + guint format : GSK_NGL_UNIFORM_FORMAT_BITS; + guint array_count : GSK_NGL_UNIFORM_ARRAY_BITS; + guint offset : GSK_NGL_UNIFORM_OFFSET_BITS; +} GskNglUniformInfo; + +G_STATIC_ASSERT (sizeof (GskNglUniformInfo) == 4); + +typedef struct _GskNglUniformInfoElement +{ + GskNglUniformInfo info; + guint stamp; +} GskNglUniformInfoElement; + +G_STATIC_ASSERT (sizeof (GskNglUniformInfoElement) == 8); + +typedef struct _GskNglUniformProgram +{ + guint program_id; + guint n_uniforms : 12; + guint has_attachments : 1; + + /* To avoid walking our 1:1 array of location->uniform slots, we have + * a sparse index that allows us to skip the empty zones. + */ + guint *sparse; + guint n_sparse; + + /* Uniforms are provided inline at the end of structure to avoid + * an extra dereference. + */ + GskNglUniformInfoElement uniforms[0]; +} GskNglUniformProgram; + +typedef struct _GskNglUniformState +{ + GHashTable *programs; + guint8 *values_buf; + guint values_pos; + guint values_len; +} GskNglUniformState; + +/** + * GskNglUniformStateCallback: + * @info: a pointer to the information about the uniform + * @location: the location of the uniform within the GPU program. + * @user_data: closure data for the callback + * + * This callback can be used to snapshot state of a program which + * is useful when batching commands so that the state may be compared + * with future evocations of the program. + */ +typedef void (*GskNglUniformStateCallback) (const GskNglUniformInfo *info, + guint location, + gpointer user_data); + +typedef enum _GskNglUniformKind +{ + GSK_NGL_UNIFORM_FORMAT_1F = 1, + GSK_NGL_UNIFORM_FORMAT_2F, + GSK_NGL_UNIFORM_FORMAT_3F, + GSK_NGL_UNIFORM_FORMAT_4F, + + GSK_NGL_UNIFORM_FORMAT_1FV, + GSK_NGL_UNIFORM_FORMAT_2FV, + GSK_NGL_UNIFORM_FORMAT_3FV, + GSK_NGL_UNIFORM_FORMAT_4FV, + + GSK_NGL_UNIFORM_FORMAT_1I, + GSK_NGL_UNIFORM_FORMAT_2I, + GSK_NGL_UNIFORM_FORMAT_3I, + GSK_NGL_UNIFORM_FORMAT_4I, + + GSK_NGL_UNIFORM_FORMAT_1UI, + + GSK_NGL_UNIFORM_FORMAT_TEXTURE, + + GSK_NGL_UNIFORM_FORMAT_MATRIX, + GSK_NGL_UNIFORM_FORMAT_ROUNDED_RECT, + GSK_NGL_UNIFORM_FORMAT_COLOR, + + GSK_NGL_UNIFORM_FORMAT_LAST +} GskNglUniformFormat; + +G_STATIC_ASSERT (GSK_NGL_UNIFORM_FORMAT_LAST < (1 << GSK_NGL_UNIFORM_FORMAT_BITS)); + +GskNglUniformState *gsk_ngl_uniform_state_new (void); +GskNglUniformState *gsk_ngl_uniform_state_ref (GskNglUniformState *state); +void gsk_ngl_uniform_state_unref (GskNglUniformState *state); +GskNglUniformProgram *gsk_ngl_uniform_state_get_program (GskNglUniformState *state, + guint program, + guint n_uniforms); +void gsk_ngl_uniform_state_end_frame (GskNglUniformState *state); +gsize gsk_ngl_uniform_format_size (GskNglUniformFormat format); +gpointer gsk_ngl_uniform_state_init_value (GskNglUniformState *state, + GskNglUniformProgram *program, + GskNglUniformFormat format, + guint array_count, + guint location, + GskNglUniformInfoElement **infoptr); + +#define GSK_NGL_UNIFORM_VALUE(base, offset) ((gpointer)((base) + ((offset) * 4))) +#define gsk_ngl_uniform_state_get_uniform_data(state,offset) GSK_NGL_UNIFORM_VALUE((state)->values_buf, offset) +#define gsk_ngl_uniform_state_snapshot(state, program_info, callback, user_data) \ + G_STMT_START { \ + for (guint z = 0; z < program_info->n_sparse; z++) \ + { \ + guint location = program_info->sparse[z]; \ + GskNglUniformInfoElement *info = &program_info->uniforms[location]; \ + \ + g_assert (location < GL_MAX_UNIFORM_LOCATIONS); \ + g_assert (location < program_info->n_uniforms); \ + \ + if (info->info.format > 0) \ + callback (&info->info, location, user_data); \ + } \ + } G_STMT_END + +static inline gpointer +gsk_ngl_uniform_state_get_value (GskNglUniformState *state, + GskNglUniformProgram *program, + GskNglUniformFormat format, + guint array_count, + guint location, + guint stamp, + GskNglUniformInfoElement **infoptr) +{ + GskNglUniformInfoElement *info; + + if (location == (guint)-1) + return NULL; + + /* If the stamp is the same, then we can ignore the request + * and short-circuit as early as possible. This requires the + * caller to increment their private stamp when they change + * internal state. + * + * This is generally used for the shared uniforms like projection, + * modelview, clip, etc to avoid so many comparisons which cost + * considerable CPU. + */ + info = &program->uniforms[location]; + if (stamp != 0 && stamp == info->stamp) + return NULL; + + if G_LIKELY (format == info->info.format && array_count <= info->info.array_count) + { + *infoptr = info; + return GSK_NGL_UNIFORM_VALUE (state->values_buf, info->info.offset); + } + + return gsk_ngl_uniform_state_init_value (state, program, format, array_count, location, infoptr); +} + +static inline guint +gsk_ngl_uniform_state_align (guint current_pos, + guint size) +{ + guint align = size > 8 ? 16 : (size > 4 ? 8 : 4); + guint masked = current_pos & (align - 1); + + g_assert (size > 0); + g_assert (align == 4 || align == 8 || align == 16); + g_assert (masked < align); + + return align - masked; +} + +static inline gpointer +gsk_ngl_uniform_state_realloc (GskNglUniformState *state, + guint size, + guint *offset) +{ + guint padding = gsk_ngl_uniform_state_align (state->values_pos, size); + + if G_UNLIKELY (state->values_len - padding - size < state->values_pos) + { + state->values_len *= 2; + state->values_buf = g_realloc (state->values_buf, state->values_len); + } + + /* offsets are in slots of 4 to use fewer bits */ + g_assert ((state->values_pos + padding) % 4 == 0); + *offset = (state->values_pos + padding) / 4; + state->values_pos += padding + size; + + return GSK_NGL_UNIFORM_VALUE (state->values_buf, *offset); +} + +#define GSK_NGL_UNIFORM_STATE_REPLACE(info, u, type, count) \ + G_STMT_START { \ + if ((info)->info.initial && count == (info)->info.array_count) \ + { \ + u = GSK_NGL_UNIFORM_VALUE (state->values_buf, (info)->info.offset); \ + } \ + else \ + { \ + guint offset; \ + u = gsk_ngl_uniform_state_realloc (state, sizeof(type) * MAX (1, count), &offset); \ + g_assert (offset < (1 << GSK_NGL_UNIFORM_OFFSET_BITS)); \ + (info)->info.offset = offset; \ + /* We might have increased array length */ \ + (info)->info.array_count = count; \ + } \ + } G_STMT_END + +static inline void +gsk_ngl_uniform_info_changed (GskNglUniformInfoElement *info, + guint location, + guint stamp) +{ + info->stamp = stamp; + info->info.initial = FALSE; +} + +static inline void +gsk_ngl_uniform_state_set1f (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + float value0) +{ + Uniform1f *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != 0); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_1F, 1, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, Uniform1f , 1); + u->v0 = value0; + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set2f (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + float value0, + float value1) +{ + Uniform2f *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_2F, 1, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, Uniform2f, 1); + u->v0 = value0; + u->v1 = value1; + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set3f (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + float value0, + float value1, + float value2) +{ + Uniform3f *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_3F, 1, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, Uniform3f, 1); + u->v0 = value0; + u->v1 = value1; + u->v2 = value2; + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set4f (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + float value0, + float value1, + float value2, + float value3) +{ + Uniform4f *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_4F, 1, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, Uniform4f, 1); + u->v0 = value0; + u->v1 = value1; + u->v2 = value2; + u->v3 = value3; + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set1ui (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + guint value0) +{ + Uniform1ui *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_1UI, 1, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, Uniform1ui, 1); + u->v0 = value0; + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set1i (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + int value0) +{ + Uniform1i *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_1I, 1, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, Uniform1i, 1); + u->v0 = value0; + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set2i (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + int value0, + int value1) +{ + Uniform2i *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_2I, 1, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, Uniform2i, 1); + u->v0 = value0; + u->v1 = value1; + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set3i (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + int value0, + int value1, + int value2) +{ + Uniform3i *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_3I, 1, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, Uniform3i, 1); + u->v0 = value0; + u->v1 = value1; + u->v2 = value2; + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set4i (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + int value0, + int value1, + int value2, + int value3) +{ + Uniform4i *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_4I, 1, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, Uniform4i, 1); + u->v0 = value0; + u->v1 = value1; + u->v2 = value2; + u->v3 = value3; + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set_rounded_rect (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + const GskRoundedRect *rounded_rect) +{ + GskRoundedRect *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + g_assert (rounded_rect != NULL); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_ROUNDED_RECT, 1, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, GskRoundedRect, 1); + memcpy (u, rounded_rect, sizeof *rounded_rect); + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set_matrix (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + const graphene_matrix_t *matrix) +{ + graphene_matrix_t *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + g_assert (matrix != NULL); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_MATRIX, 1, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, graphene_matrix_t, 1); + memcpy (u, matrix, sizeof *matrix); + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +/** + * gsk_ngl_uniform_state_set_texture: + * @state: a #GskNglUniformState + * @program: the program id + * @location: the location of the texture + * @texture_slot: a texturing slot such as GL_TEXTURE0 + * + * Sets the uniform expecting a texture to @texture_slot. This API + * expects a texture slot such as GL_TEXTURE0 to reduce chances of + * miss-use by the caller. + * + * The value stored to the uniform is in the form of 0 for GL_TEXTURE0, + * 1 for GL_TEXTURE1, and so on. + */ +static inline void +gsk_ngl_uniform_state_set_texture (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + guint texture_slot) +{ + GskNglUniformInfoElement *info; + guint *u; + + g_assert (texture_slot >= GL_TEXTURE0); + g_assert (texture_slot < GL_TEXTURE16); + + texture_slot -= GL_TEXTURE0; + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_TEXTURE, 1, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, guint, 1); + *u = texture_slot; + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +/** + * gsk_ngl_uniform_state_set_color: + * @state: a #GskNglUniformState + * @program: a program id > 0 + * @location: the uniform location + * @color: a color to set or %NULL for transparent + * + * Sets a uniform to the color described by @color. This is a convenience + * function to allow callers to avoid having to translate colors to floats + * in other portions of the renderer. + */ +static inline void +gsk_ngl_uniform_state_set_color (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + const GdkRGBA *color) +{ + static const GdkRGBA transparent = {0}; + GskNglUniformInfoElement *info; + GdkRGBA *u; + + g_assert (state != NULL); + g_assert (program != NULL); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_COLOR, 1, location, stamp, &info))) + { + if (color == NULL) + color = &transparent; + + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, GdkRGBA, 1); + memcpy (u, color, sizeof *color); + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set1fv (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + guint count, + const float *value) +{ + Uniform1f *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + g_assert (count > 0); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_1FV, count, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, Uniform1f, count); + memcpy (u, value, sizeof (Uniform1f) * count); + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set2fv (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + guint count, + const float *value) +{ + Uniform2f *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + g_assert (count > 0); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_2FV, count, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, Uniform2f, count); + memcpy (u, value, sizeof (Uniform2f) * count); + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set3fv (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + guint count, + const float *value) +{ + Uniform3f *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + g_assert (count > 0); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_3FV, count, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, Uniform3f, count); + memcpy (u, value, sizeof (Uniform3f) * count); + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +static inline void +gsk_ngl_uniform_state_set4fv (GskNglUniformState *state, + GskNglUniformProgram *program, + guint location, + guint stamp, + guint count, + const float *value) +{ + Uniform4f *u; + GskNglUniformInfoElement *info; + + g_assert (state != NULL); + g_assert (program != NULL); + g_assert (count > 0); + + if ((u = gsk_ngl_uniform_state_get_value (state, program, GSK_NGL_UNIFORM_FORMAT_4FV, count, location, stamp, &info))) + { + GSK_NGL_UNIFORM_STATE_REPLACE (info, u, Uniform4f, count); + memcpy (u, value, sizeof (Uniform4f) * count); + gsk_ngl_uniform_info_changed (info, location, stamp); + } +} + +G_END_DECLS + +#endif /* GSK_NGL_UNIFORM_STATE_PRIVATE_H */ diff --git a/gsk/ngl/inlinearray.h b/gsk/ngl/inlinearray.h new file mode 100644 index 0000000000..98a9255c68 --- /dev/null +++ b/gsk/ngl/inlinearray.h @@ -0,0 +1,77 @@ +#ifndef __INLINE_ARRAY_H__ +#define __INLINE_ARRAY_H__ + +#define DEFINE_INLINE_ARRAY(Type, prefix, ElementType) \ + typedef struct _##Type { \ + gsize len; \ + gsize allocated; \ + ElementType *items; \ + } Type; \ + \ + static inline void \ + prefix##_init (Type *ar, \ + gsize initial_size) \ + { \ + ar->len = 0; \ + ar->allocated = initial_size ? initial_size : 16; \ + ar->items = g_new0 (ElementType, ar->allocated); \ + } \ + \ + static inline void \ + prefix##_clear (Type *ar) \ + { \ + ar->len = 0; \ + ar->allocated = 0; \ + g_clear_pointer (&ar->items, g_free); \ + } \ + \ + static inline ElementType * \ + prefix##_head (Type *ar) \ + { \ + return &ar->items[0]; \ + } \ + \ + static inline ElementType * \ + prefix##_tail (Type *ar) \ + { \ + return &ar->items[ar->len-1]; \ + } \ + \ + static inline ElementType * \ + prefix##_append (Type *ar) \ + { \ + if G_UNLIKELY (ar->len == ar->allocated) \ + { \ + ar->allocated *= 2; \ + ar->items = g_renew (ElementType, ar->items, ar->allocated);\ + } \ + \ + ar->len++; \ + \ + return prefix##_tail (ar); \ + } \ + \ + static inline ElementType * \ + prefix##_append_n (Type *ar, \ + gsize n) \ + { \ + if G_UNLIKELY ((ar->len + n) > ar->allocated) \ + { \ + while ((ar->len + n) > ar->allocated) \ + ar->allocated *= 2; \ + ar->items = g_renew (ElementType, ar->items, ar->allocated);\ + } \ + \ + ar->len += n; \ + \ + return &ar->items[ar->len-n]; \ + } \ + \ + static inline gsize \ + prefix##_index_of (Type *ar, \ + const ElementType *element) \ + { \ + return element - &ar->items[0]; \ + } + +#endif /* __INLINE_ARRAY_H__ */ diff --git a/gsk/ngl/ninesliceprivate.h b/gsk/ngl/ninesliceprivate.h new file mode 100644 index 0000000000..fe787acc1d --- /dev/null +++ b/gsk/ngl/ninesliceprivate.h @@ -0,0 +1,309 @@ +/* ninesliceprivate.h + * + * Copyright 2017 Timm Bäder + * Copyright 2021 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 + */ + +#ifndef __NINE_SLICE_PRIVATE_H__ +#define __NINE_SLICE_PRIVATE_H__ + +#include "gskngltexturepoolprivate.h" + +#if 0 +# define DEBUG_NINE_SLICE +#endif + +G_BEGIN_DECLS + +enum { + NINE_SLICE_TOP_LEFT = 0, + NINE_SLICE_TOP_CENTER = 1, + NINE_SLICE_TOP_RIGHT = 2, + NINE_SLICE_LEFT_CENTER = 3, + NINE_SLICE_CENTER = 4, + NINE_SLICE_RIGHT_CENTER = 5, + NINE_SLICE_BOTTOM_LEFT = 6, + NINE_SLICE_BOTTOM_CENTER = 7, + NINE_SLICE_BOTTOM_RIGHT = 8, +}; + +static inline bool G_GNUC_PURE +nine_slice_is_visible (const GskNglTextureNineSlice *slice) +{ + return slice->rect.width > 0 && slice->rect.height > 0; +} + +static inline void +nine_slice_rounded_rect (GskNglTextureNineSlice *slices, + const GskRoundedRect *rect) +{ + const graphene_point_t *origin = &rect->bounds.origin; + const graphene_size_t *size = &rect->bounds.size; + int top_height = ceilf (MAX (rect->corner[GSK_CORNER_TOP_LEFT].height, + rect->corner[GSK_CORNER_TOP_RIGHT].height)); + int bottom_height = ceilf (MAX (rect->corner[GSK_CORNER_BOTTOM_LEFT].height, + rect->corner[GSK_CORNER_BOTTOM_RIGHT].height)); + int right_width = ceilf (MAX (rect->corner[GSK_CORNER_TOP_RIGHT].width, + rect->corner[GSK_CORNER_BOTTOM_RIGHT].width)); + int left_width = ceilf (MAX (rect->corner[GSK_CORNER_TOP_LEFT].width, + rect->corner[GSK_CORNER_BOTTOM_LEFT].width)); + + /* Top left */ + slices[0].rect.x = origin->x; + slices[0].rect.y = origin->y; + slices[0].rect.width = left_width; + slices[0].rect.height = top_height; + + /* Top center */ + slices[1].rect.x = origin->x + size->width / 2.0 - 0.5; + slices[1].rect.y = origin->y; + slices[1].rect.width = 1; + slices[1].rect.height = top_height; + + /* Top right */ + slices[2].rect.x = origin->x + size->width - right_width; + slices[2].rect.y = origin->y; + slices[2].rect.width = right_width; + slices[2].rect.height = top_height; + + /* Left center */ + slices[3].rect.x = origin->x; + slices[3].rect.y = origin->y + size->height / 2; + slices[3].rect.width = left_width; + slices[3].rect.height = 1; + + /* center */ + slices[4].rect.x = origin->x + size->width / 2.0 - 0.5; + slices[4].rect.y = origin->y + size->height / 2.0 - 0.5; + slices[4].rect.width = 1; + slices[4].rect.height = 1; + + /* Right center */ + slices[5].rect.x = origin->x + size->width - right_width; + slices[5].rect.y = origin->y + (size->height / 2.0) - 0.5; + slices[5].rect.width = right_width; + slices[5].rect.height = 1; + + /* Bottom Left */ + slices[6].rect.x = origin->x; + slices[6].rect.y = origin->y + size->height - bottom_height; + slices[6].rect.width = left_width; + slices[6].rect.height = bottom_height; + + /* Bottom center */ + slices[7].rect.x = origin->x + (size->width / 2.0) - 0.5; + slices[7].rect.y = origin->y + size->height - bottom_height; + slices[7].rect.width = 1; + slices[7].rect.height = bottom_height; + + /* Bottom right */ + slices[8].rect.x = origin->x + size->width - right_width; + slices[8].rect.y = origin->y + size->height - bottom_height; + slices[8].rect.width = right_width; + slices[8].rect.height = bottom_height; + +#ifdef DEBUG_NINE_SLICE + /* These only hold true when the values from ceilf() above + * are greater than one. Otherwise they fail, like will happen + * with the node editor viewing the textures zoomed out. + */ + if (size->width > 1) + g_assert_cmpfloat (size->width, >=, left_width + right_width); + if (size->height > 1) + g_assert_cmpfloat (size->height, >=, top_height + bottom_height); +#endif +} + +static inline void +nine_slice_to_texture_coords (GskNglTextureNineSlice *slices, + int texture_width, + int texture_height) +{ + float fw = texture_width; + float fh = texture_height; + + for (guint i = 0; i < 9; i++) + { + GskNglTextureNineSlice *slice = &slices[i]; + + slice->area.x = slice->rect.x / fw; + slice->area.y = 1.0 - ((slice->rect.y + slice->rect.height) / fh); + slice->area.x2 = ((slice->rect.x + slice->rect.width) / fw); + slice->area.y2 = (1.0 - (slice->rect.y / fh)); + +#ifdef DEBUG_NINE_SLICE + g_assert_cmpfloat (slice->area.x, >=, 0); + g_assert_cmpfloat (slice->area.x, <=, 1); + g_assert_cmpfloat (slice->area.y, >=, 0); + g_assert_cmpfloat (slice->area.y, <=, 1); + g_assert_cmpfloat (slice->area.x2, >, slice->area.x); + g_assert_cmpfloat (slice->area.y2, >, slice->area.y); +#endif + } +} + +static inline void +nine_slice_grow (GskNglTextureNineSlice *slices, + int amount) +{ + if (amount == 0) + return; + + /* top left */ + slices[0].rect.x -= amount; + slices[0].rect.y -= amount; + if (amount > slices[0].rect.width) + slices[0].rect.width += amount * 2; + else + slices[0].rect.width += amount; + + if (amount > slices[0].rect.height) + slices[0].rect.height += amount * 2; + else + slices[0].rect.height += amount; + + + /* Top center */ + slices[1].rect.y -= amount; + if (amount > slices[1].rect.height) + slices[1].rect.height += amount * 2; + else + slices[1].rect.height += amount; + + /* top right */ + slices[2].rect.y -= amount; + if (amount > slices[2].rect.width) + { + slices[2].rect.x -= amount; + slices[2].rect.width += amount * 2; + } + else + { + slices[2].rect.width += amount; + } + + if (amount > slices[2].rect.height) + slices[2].rect.height += amount * 2; + else + slices[2].rect.height += amount; + + + + slices[3].rect.x -= amount; + if (amount > slices[3].rect.width) + slices[3].rect.width += amount * 2; + else + slices[3].rect.width += amount; + + /* Leave center alone */ + + if (amount > slices[5].rect.width) + { + slices[5].rect.x -= amount; + slices[5].rect.width += amount * 2; + } + else + { + slices[5].rect.width += amount; + } + + + /* Bottom left */ + slices[6].rect.x -= amount; + if (amount > slices[6].rect.width) + { + slices[6].rect.width += amount * 2; + } + else + { + slices[6].rect.width += amount; + } + + if (amount > slices[6].rect.height) + { + slices[6].rect.y -= amount; + slices[6].rect.height += amount * 2; + } + else + { + slices[6].rect.height += amount; + } + + + /* Bottom center */ + if (amount > slices[7].rect.height) + { + slices[7].rect.y -= amount; + slices[7].rect.height += amount * 2; + } + else + { + slices[7].rect.height += amount; + } + + if (amount > slices[8].rect.width) + { + slices[8].rect.x -= amount; + slices[8].rect.width += amount * 2; + } + else + { + slices[8].rect.width += amount; + } + + if (amount > slices[8].rect.height) + { + slices[8].rect.y -= amount; + slices[8].rect.height += amount * 2; + } + else + { + slices[8].rect.height += amount; + } + +#ifdef DEBUG_NINE_SLICE + /* These cannot be relied on in all cases right now, specifically + * when viewing data zoomed out. + */ + for (guint i = 0; i < 9; i ++) + { + g_assert_cmpint (slices[i].rect.x, >=, 0); + g_assert_cmpint (slices[i].rect.y, >=, 0); + g_assert_cmpint (slices[i].rect.width, >=, 0); + g_assert_cmpint (slices[i].rect.height, >=, 0); + } + + /* Rows don't overlap */ + for (guint i = 0; i < 3; i++) + { + int lhs = slices[i * 3 + 0].rect.x + slices[i * 3 + 0].rect.width; + int rhs = slices[i * 3 + 1].rect.x; + + /* Ignore the case where we are scaled out and the + * positioning is degenerate, such as from node-editor. + */ + if (rhs > 1) + g_assert_cmpint (lhs, <, rhs); + } +#endif + +} + +G_END_DECLS + +#endif /* __NINE_SLICE_PRIVATE_H__ */ diff --git a/gtk/gtktestutils.c b/gtk/gtktestutils.c index 690931a153..35dcff797c 100644 --- a/gtk/gtktestutils.c +++ b/gtk/gtktestutils.c @@ -41,6 +41,7 @@ #define GTK_COMPILATION #include +#include #ifdef GDK_WINDOWING_BROADWAY #include diff --git a/gtk/inspector/general.c b/gtk/inspector/general.c index 4cbee80b8e..451300bb5a 100644 --- a/gtk/inspector/general.c +++ b/gtk/inspector/general.c @@ -147,6 +147,8 @@ init_version (GtkInspectorGeneral *gen) renderer = "Vulkan"; else if (strcmp (G_OBJECT_TYPE_NAME (gsk_renderer), "GskGLRenderer") == 0) renderer = "GL"; + else if (strcmp (G_OBJECT_TYPE_NAME (gsk_renderer), "GskNgltRenderer") == 0) + renderer = "NGL"; else if (strcmp (G_OBJECT_TYPE_NAME (gsk_renderer), "GskCairoRenderer") == 0) renderer = "Cairo"; else diff --git a/testsuite/gsk/meson.build b/testsuite/gsk/meson.build index 844e76c475..f78a71fc39 100644 --- a/testsuite/gsk/meson.build +++ b/testsuite/gsk/meson.build @@ -90,6 +90,7 @@ informative_render_tests = [ renderers = [ # name exclude term [ 'opengl', '' ], + [ 'next', '' ], [ 'broadway', '-3d' ], [ 'cairo', '-3d' ], ]