/* gskglrenderjob.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 #include #include #include #include #include #include "gskglcommandqueueprivate.h" #include "gskgldriverprivate.h" #include "gskglglyphlibraryprivate.h" #include "gskgliconlibraryprivate.h" #include "gskglprogramprivate.h" #include "gskglrenderjobprivate.h" #include "gskglshadowlibraryprivate.h" #include "gskdebugprivate.h" #include "ninesliceprivate.h" #include "fp16private.h" #define ALLOW_OFFLOAD_FOR_ANY_TEXTURE 1 #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_GL_UNIFORM_ARRAY_BITS)); #define ALPHA_IS_CLEAR(alpha) ((alpha) < ((float) 0x00ff / (float) 0xffff)) #define RGBA_IS_CLEAR(rgba) ALPHA_IS_CLEAR((rgba)->alpha) typedef struct _GskGLRenderClip { GskRoundedRect rect; guint is_rectilinear : 1; guint is_fully_contained : 1; } GskGLRenderClip; #define GDK_ARRAY_NAME clips #define GDK_ARRAY_TYPE_NAME Clips #define GDK_ARRAY_ELEMENT_TYPE GskGLRenderClip #define GDK_ARRAY_BY_VALUE 1 #define GDK_ARRAY_PREALLOC 16 #define GDK_ARRAY_NO_MEMSET #include "gdk/gdkarrayimpl.c" typedef struct _GskGLRenderModelview { GskTransform *transform; float scale_x; float scale_y; float dx; float dy; float offset_x_before; float offset_y_before; graphene_matrix_t matrix; } GskGLRenderModelview; #define GDK_ARRAY_NAME modelviews #define GDK_ARRAY_TYPE_NAME Modelviews #define GDK_ARRAY_ELEMENT_TYPE GskGLRenderModelview #define GDK_ARRAY_BY_VALUE 1 #define GDK_ARRAY_PREALLOC 16 #define GDK_ARRAY_NO_MEMSET #include "gdk/gdkarrayimpl.c" struct _GskGLRenderJob { /* 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_gl_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. */ GskGLDriver *driver; /* The command queue (which is just a faster pointer to the driver's * command queue. */ GskGLCommandQueue *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; guint default_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 GskGLRenderModelview updated as nodes are processed. The * current modelview is the last element. */ Modelviews modelview; /* An array of GskGLRenderClip updated as nodes are processed. The * current clip is the last element. */ Clips 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 GskGLRenderClip *current_clip; const GskGLRenderModelview *current_modelview; GskGLProgram *current_program; guint source_is_glyph_atlas : 1; /* If we should be rendering red zones over fallback nodes */ guint debug_fallback : 1; /* In some cases we might want to avoid clearing the framebuffer * because we're going to render over the existing contents. */ guint clear_framebuffer : 1; /* Format we want to use for intermediate textures, determined by * looking at the format of the framebuffer we are rendering on. */ int target_format; }; typedef struct _GskGLRenderOffscreen { /* The bounds to render */ const graphene_rect_t *bounds; /* Return location for texture coordinates */ struct { float x; float y; float x2; float y2; } area; /* Return location for texture ID */ guint texture_id; gpointer sync; /* Whether to force creating a new texture, even if the * input already is a texture */ guint force_offscreen : 1; guint reset_clip : 1; guint do_not_cache : 1; /* Return location for whether we created a texture */ guint was_offscreen : 1; guint has_mipmap : 1; } GskGLRenderOffscreen; static void gsk_gl_render_job_visit_node (GskGLRenderJob *job, const GskRenderNode *node); static gboolean gsk_gl_render_job_visit_node_with_offscreen (GskGLRenderJob *job, const GskRenderNode *node, GskGLRenderOffscreen *offscreen); static void gsk_gl_render_job_upload_texture (GskGLRenderJob *job, GdkTexture *texture, gboolean ensure_mipmap, GskGLRenderOffscreen *offscreen); static inline GskGLRenderClip * clips_grow_one (Clips *clips) { guint len = clips_get_size (clips); clips_set_size (clips, len + 1); return clips_get (clips, len); } static inline GskGLRenderModelview * modelviews_grow_one (Modelviews *modelviews) { guint len = modelviews_get_size (modelviews); modelviews_set_size (modelviews, len + 1); return modelviews_get (modelviews, len); } static inline int get_target_format (GskGLRenderJob *job, const GskRenderNode *node) { if (gsk_render_node_get_preferred_depth (node) != GDK_MEMORY_U8) return job->target_format; return GL_RGBA8; } static inline void init_full_texture_region (GskGLRenderOffscreen *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 gboolean G_GNUC_PURE rounded_rect_equal (const GskRoundedRect *r1, const GskRoundedRect *r2) { return memcmp (r1, r2, sizeof (GskRoundedRect)) == 0; } 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_2d_transform (const GskRenderNode *node) { switch (GSK_RENDER_NODE_TYPE (node)) { case GSK_COLOR_NODE: case GSK_OPACITY_NODE: case GSK_COLOR_MATRIX_NODE: case GSK_TEXTURE_NODE: case GSK_TEXTURE_SCALE_NODE: case GSK_CROSS_FADE_NODE: case GSK_LINEAR_GRADIENT_NODE: case GSK_REPEATING_LINEAR_GRADIENT_NODE: case GSK_CONIC_GRADIENT_NODE: case GSK_RADIAL_GRADIENT_NODE: case GSK_REPEATING_RADIAL_GRADIENT_NODE: case GSK_DEBUG_NODE: case GSK_TEXT_NODE: case GSK_CAIRO_NODE: case GSK_BLEND_NODE: case GSK_BLUR_NODE: case GSK_MASK_NODE: case GSK_FILL_NODE: case GSK_STROKE_NODE: case GSK_SUBSURFACE_NODE: return TRUE; case GSK_SHADOW_NODE: return node_supports_2d_transform (gsk_shadow_node_get_child (node)); case GSK_TRANSFORM_NODE: return node_supports_2d_transform (gsk_transform_node_get_child (node)); case GSK_CONTAINER_NODE: for (guint i = 0, p = gsk_container_node_get_n_children (node); i < p; i++) { if (!node_supports_2d_transform (gsk_container_node_get_child (node, i))) return FALSE; } return TRUE; case GSK_BORDER_NODE: case GSK_INSET_SHADOW_NODE: case GSK_OUTSET_SHADOW_NODE: case GSK_REPEAT_NODE: case GSK_CLIP_NODE: case GSK_ROUNDED_CLIP_NODE: case GSK_GL_SHADER_NODE: return FALSE; case GSK_NOT_A_RENDER_NODE: default: g_assert_not_reached (); } } 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 (GSK_RENDER_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_DEBUG_NODE: case GSK_TEXT_NODE: case GSK_CAIRO_NODE: case GSK_BLEND_NODE: case GSK_BLUR_NODE: case GSK_MASK_NODE: case GSK_FILL_NODE: case GSK_STROKE_NODE: case GSK_SUBSURFACE_NODE: return TRUE; case GSK_SHADOW_NODE: return node_supports_transform (gsk_shadow_node_get_child (node)); case GSK_TRANSFORM_NODE: return node_supports_transform (gsk_transform_node_get_child (node)); case GSK_CONTAINER_NODE: case GSK_LINEAR_GRADIENT_NODE: case GSK_REPEATING_LINEAR_GRADIENT_NODE: case GSK_RADIAL_GRADIENT_NODE: case GSK_REPEATING_RADIAL_GRADIENT_NODE: case GSK_CONIC_GRADIENT_NODE: case GSK_BORDER_NODE: case GSK_INSET_SHADOW_NODE: case GSK_OUTSET_SHADOW_NODE: case GSK_REPEAT_NODE: case GSK_CLIP_NODE: case GSK_ROUNDED_CLIP_NODE: case GSK_GL_SHADER_NODE: case GSK_TEXTURE_SCALE_NODE: return FALSE; case GSK_NOT_A_RENDER_NODE: default: g_assert_not_reached (); } } 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 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_gl_render_job_set_alpha (GskGLRenderJob *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 (GskGLRenderModelview *modelview) { gsk_transform_to_matrix (modelview->transform, &modelview->matrix); switch (gsk_transform_get_category (modelview->transform)) { case GSK_TRANSFORM_CATEGORY_IDENTITY: modelview->scale_x = 1; modelview->scale_y = 1; modelview->dx = 0; modelview->dy = 0; break; case GSK_TRANSFORM_CATEGORY_2D_TRANSLATE: modelview->scale_x = 1; modelview->scale_y = 1; gsk_transform_to_translate (modelview->transform, &modelview->dx, &modelview->dy); break; case GSK_TRANSFORM_CATEGORY_2D_AFFINE: gsk_transform_to_affine (modelview->transform, &modelview->scale_x, &modelview->scale_y, &modelview->dx, &modelview->dy); break; case GSK_TRANSFORM_CATEGORY_2D: { float skew_x, skew_y, angle, dx, dy; gsk_transform_to_2d_components (modelview->transform, &skew_x, &skew_y, &modelview->scale_x, &modelview->scale_y, &angle, &dx, &dy); modelview->dx = 0; modelview->dy = 0; } break; case GSK_TRANSFORM_CATEGORY_UNKNOWN: case GSK_TRANSFORM_CATEGORY_ANY: case GSK_TRANSFORM_CATEGORY_3D: { graphene_quaternion_t rotation; graphene_vec4_t perspective; graphene_vec3_t translation; graphene_vec3_t scale; graphene_vec3_t shear; graphene_matrix_decompose (&modelview->matrix, &translation, &scale, &rotation, &shear, &perspective); modelview->scale_x = graphene_vec3_get_x (&scale); modelview->scale_y = graphene_vec3_get_y (&scale); modelview->dx = 0; modelview->dy = 0; } break; default: break; } } /* takes ownership of transform */ static void gsk_gl_render_job_set_modelview (GskGLRenderJob *job, GskTransform *transform) { GskGLRenderModelview *modelview; g_assert (job != NULL); job->driver->stamps[UNIFORM_SHARED_MODELVIEW]++; modelview = modelviews_grow_one (&job->modelview); 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; } /* doesn't take ownership of transform */ static void gsk_gl_render_job_push_modelview (GskGLRenderJob *job, GskTransform *transform) { GskGLRenderModelview *modelview; g_assert (job != NULL); g_assert (transform != NULL); job->driver->stamps[UNIFORM_SHARED_MODELVIEW]++; modelview = modelviews_grow_one (&job->modelview); if G_LIKELY (modelviews_get_size (&job->modelview) > 1) { GskGLRenderModelview *last = job->modelview.end - 2; GskTransform *t = NULL; /* Multiply given matrix with our previous 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_gl_render_job_pop_modelview (GskGLRenderJob *job) { const GskGLRenderModelview *head; g_assert (job != NULL); g_assert (modelviews_get_size (&job->modelview) > 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.end--; if (modelviews_get_size (&job->modelview) >= 1) { head = job->modelview.end - 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_gl_render_job_push_clip (GskGLRenderJob *job, const GskRoundedRect *rect) { GskGLRenderClip *clip; g_assert (job != NULL); g_assert (rect != NULL); job->driver->stamps[UNIFORM_SHARED_CLIP_RECT]++; clip = clips_grow_one (&job->clip); memcpy (&clip->rect, rect, sizeof *rect); clip->is_rectilinear = gsk_rounded_rect_is_rectilinear (rect); clip->is_fully_contained = FALSE; job->current_clip = clip; } static void gsk_gl_render_job_push_contained_clip (GskGLRenderJob *job) { GskGLRenderClip *clip; GskGLRenderClip *old_clip; g_assert (job != NULL); g_assert (clips_get_size (&job->clip) > 0); job->driver->stamps[UNIFORM_SHARED_CLIP_RECT]++; clip = clips_grow_one (&job->clip); old_clip = clips_get (&job->clip, clips_get_size (&job->clip) - 2); memcpy (&clip->rect.bounds, &old_clip->rect.bounds, sizeof (graphene_rect_t)); memset (clip->rect.corner, 0, sizeof clip->rect.corner); clip->is_rectilinear = TRUE; clip->is_fully_contained = TRUE; job->current_clip = clip; } static void gsk_gl_render_job_pop_clip (GskGLRenderJob *job) { g_assert (job != NULL); g_assert (clips_get_size (&job->clip) > 0); job->driver->stamps[UNIFORM_SHARED_CLIP_RECT]++; job->current_clip--; job->clip.end--; } static inline void gsk_gl_render_job_offset (GskGLRenderJob *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_gl_render_job_set_projection (GskGLRenderJob *job, const graphene_matrix_t *projection) { memcpy (&job->projection, projection, sizeof job->projection); job->driver->stamps[UNIFORM_SHARED_PROJECTION]++; } static inline void gsk_gl_render_job_set_projection_from_rect (GskGLRenderJob *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_gl_render_job_set_projection_for_size (GskGLRenderJob *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_gl_render_job_set_viewport (GskGLRenderJob *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_gl_render_job_set_viewport_for_size (GskGLRenderJob *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_gl_render_job_transform_bounds (GskGLRenderJob *job, const graphene_rect_t *rect, graphene_rect_t *out_rect) { GskTransform *transform; GskTransformCategory category; g_assert (job != NULL); g_assert (modelviews_get_size (&job->modelview) > 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 scale_x = job->current_modelview->scale_x; float scale_y = job->current_modelview->scale_y; float dx = job->current_modelview->dx; float dy = job->current_modelview->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; /* Normalize 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_gl_render_job_untransform_bounds (GskGLRenderJob *job, const graphene_rect_t *rect, graphene_rect_t *out_rect) { GskTransform *transform; transform = gsk_transform_invert (gsk_transform_ref (job->current_modelview->transform)); gsk_transform_transform_bounds (transform, rect, out_rect); out_rect->origin.x -= job->offset_x; out_rect->origin.y -= job->offset_y; gsk_transform_unref (transform); } static inline void gsk_gl_render_job_translate_rounded_rect (GskGLRenderJob *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 void rounded_rect_scale_corners (const GskRoundedRect *rect, GskRoundedRect *out_rect, float scale_x, float scale_y) { for (guint i = 0; i < G_N_ELEMENTS (out_rect->corner); i++) { out_rect->corner[i].width = rect->corner[i].width * fabs (scale_x); out_rect->corner[i].height = rect->corner[i].height * fabs (scale_y); } if (scale_x < 0) { graphene_size_t p; p = out_rect->corner[GSK_CORNER_TOP_LEFT]; out_rect->corner[GSK_CORNER_TOP_LEFT] = out_rect->corner[GSK_CORNER_TOP_RIGHT]; out_rect->corner[GSK_CORNER_TOP_RIGHT] = p; p = out_rect->corner[GSK_CORNER_BOTTOM_LEFT]; out_rect->corner[GSK_CORNER_BOTTOM_LEFT] = out_rect->corner[GSK_CORNER_BOTTOM_RIGHT]; out_rect->corner[GSK_CORNER_BOTTOM_RIGHT] = p; } if (scale_y < 0) { graphene_size_t p; p = out_rect->corner[GSK_CORNER_TOP_LEFT]; out_rect->corner[GSK_CORNER_TOP_LEFT] = out_rect->corner[GSK_CORNER_BOTTOM_LEFT]; out_rect->corner[GSK_CORNER_BOTTOM_LEFT] = p; p = out_rect->corner[GSK_CORNER_TOP_RIGHT]; out_rect->corner[GSK_CORNER_TOP_RIGHT] = out_rect->corner[GSK_CORNER_BOTTOM_RIGHT]; out_rect->corner[GSK_CORNER_BOTTOM_RIGHT] = p; } } static inline void gsk_gl_render_job_transform_rounded_rect (GskGLRenderJob *job, const GskRoundedRect *rect, GskRoundedRect *out_rect) { gsk_gl_render_job_transform_bounds (job, &rect->bounds, &out_rect->bounds); rounded_rect_scale_corners (rect, out_rect, job->scale_x, job->scale_y); } static inline void rounded_rect_get_inner (const GskRoundedRect *rect, graphene_rect_t *inner) { float left = MAX (rect->corner[GSK_CORNER_TOP_LEFT].width, rect->corner[GSK_CORNER_BOTTOM_LEFT].width); float right = MAX (rect->corner[GSK_CORNER_TOP_RIGHT].width, rect->corner[GSK_CORNER_BOTTOM_RIGHT].width); float top = MAX (rect->corner[GSK_CORNER_TOP_LEFT].height, rect->corner[GSK_CORNER_TOP_RIGHT].height); float bottom = MAX (rect->corner[GSK_CORNER_BOTTOM_LEFT].height, rect->corner[GSK_CORNER_BOTTOM_RIGHT].height); inner->origin.x = rect->bounds.origin.x + left; inner->size.width = rect->bounds.size.width - (left + right); inner->origin.y = rect->bounds.origin.y + top; inner->size.height = rect->bounds.size.height - (top + bottom); } static inline gboolean interval_contains (float p1, float w1, float p2, float w2) { if (p2 < p1) return FALSE; if (p2 + w2 > p1 + w1) return FALSE; return TRUE; } static inline gboolean gsk_gl_render_job_update_clip (GskGLRenderJob *job, const graphene_rect_t *bounds, gboolean *pushed_clip) { graphene_rect_t transformed_bounds; gboolean no_clip = FALSE; gboolean rect_clip = FALSE; *pushed_clip = FALSE; if (job->current_clip->is_fully_contained) { /* Already fully contained - no further checks needed */ return TRUE; } gsk_gl_render_job_transform_bounds (job, bounds, &transformed_bounds); if (!gsk_rect_intersects (&job->current_clip->rect.bounds, &transformed_bounds)) { /* Completely clipped away */ return FALSE; } if (job->current_clip->is_rectilinear) { if (gsk_rect_contains_rect (&job->current_clip->rect.bounds, &transformed_bounds)) no_clip = TRUE; else rect_clip = TRUE; } else if (gsk_rounded_rect_contains_rect (&job->current_clip->rect, &transformed_bounds)) { no_clip = TRUE; } else { graphene_rect_t inner; rounded_rect_get_inner (&job->current_clip->rect, &inner); if (interval_contains (inner.origin.x, inner.size.width, transformed_bounds.origin.x, transformed_bounds.size.width) || interval_contains (inner.origin.y, inner.size.height, transformed_bounds.origin.y, transformed_bounds.size.height)) rect_clip = TRUE; } if (no_clip) { /* This node is completely contained inside the clip. * Record this fact on the clip stack, so we don't do more work * for child nodes. */ gsk_gl_render_job_push_contained_clip (job); *pushed_clip = TRUE; } else if (rect_clip && !job->current_clip->is_rectilinear) { graphene_rect_t rect; /* The clip gets simpler for this node */ gsk_rect_intersection (&job->current_clip->rect.bounds, &transformed_bounds, &rect); gsk_gl_render_job_push_clip (job, &GSK_ROUNDED_RECT_INIT_FROM_RECT (rect)); *pushed_clip = TRUE; } return TRUE; } static inline void rgba_to_half (const GdkRGBA *rgba, guint16 h[4]) { float_to_half4 ((const float *)rgba, h); } /* fill_vertex_data */ static void gsk_gl_render_job_draw_coords (GskGLRenderJob *job, float min_x, float min_y, float max_x, float max_y, float min_u, float min_v, float max_u, float max_v, guint16 c[4]) { GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue); vertices[0] = (GskGLDrawVertex) { .position = { min_x, min_y }, .uv = { min_u, min_v }, .color = { c[0], c[1], c[2], c[3] } }; vertices[1] = (GskGLDrawVertex) { .position = { min_x, max_y }, .uv = { min_u, max_v }, .color = { c[0], c[1], c[2], c[3] } }; vertices[2] = (GskGLDrawVertex) { .position = { max_x, min_y }, .uv = { max_u, min_v }, .color = { c[0], c[1], c[2], c[3] } }; vertices[3] = (GskGLDrawVertex) { .position = { max_x, max_y }, .uv = { max_u, max_v }, .color = { c[0], c[1], c[2], c[3] } }; vertices[4] = (GskGLDrawVertex) { .position = { min_x, max_y }, .uv = { min_u, max_v }, .color = { c[0], c[1], c[2], c[3] } }; vertices[5] = (GskGLDrawVertex) { .position = { max_x, min_y }, .uv = { max_u, min_v }, .color = { c[0], c[1], c[2], c[3] } }; } /* load_vertex_data_with_region */ static inline void gsk_gl_render_job_draw_offscreen_with_color (GskGLRenderJob *job, const graphene_rect_t *bounds, const GskGLRenderOffscreen *offscreen, guint16 color[4]) { 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; gsk_gl_render_job_draw_coords (job, min_x, min_y, max_x, max_y, offscreen->area.x, y1, offscreen->area.x2, y2, color); } static inline void gsk_gl_render_job_draw_offscreen (GskGLRenderJob *job, const graphene_rect_t *bounds, const GskGLRenderOffscreen *offscreen) { gsk_gl_render_job_draw_offscreen_with_color (job, bounds, offscreen, (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO }); } /* load_float_vertex_data */ static inline void gsk_gl_render_job_draw_with_color (GskGLRenderJob *job, float x, float y, float width, float height, guint16 color[4]) { 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; gsk_gl_render_job_draw_coords (job, min_x, min_y, max_x, max_y, 0, 0, 1, 1, color); } static inline void gsk_gl_render_job_draw (GskGLRenderJob *job, float x, float y, float width, float height) { gsk_gl_render_job_draw_with_color (job, x, y, width, height, (guint16[]) { FP_ZERO, FP_ZERO, FP_ZERO, FP_ZERO }); } /* load_vertex_data */ static inline void gsk_gl_render_job_draw_rect_with_color (GskGLRenderJob *job, const graphene_rect_t *bounds, guint16 color[4]) { gsk_gl_render_job_draw_with_color (job, bounds->origin.x, bounds->origin.y, bounds->size.width, bounds->size.height, color); } static inline void gsk_gl_render_job_draw_rect (GskGLRenderJob *job, const graphene_rect_t *bounds) { gsk_gl_render_job_draw (job, bounds->origin.x, bounds->origin.y, bounds->size.width, bounds->size.height); } /* load_offscreen_vertex_data */ static inline void gsk_gl_render_job_draw_offscreen_rect (GskGLRenderJob *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; guint16 color[4] = { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO }; gsk_gl_render_job_draw_coords (job, min_x, min_y, max_x, max_y, 0, 1, 1, 0, color); } static inline gboolean gsk_gl_render_job_begin_draw (GskGLRenderJob *job, GskGLProgram *program) { job->current_program = program; if (!gsk_gl_command_queue_begin_draw (job->command_queue, program->program_info, job->viewport.size.width, job->viewport.size.height)) return FALSE; gsk_gl_uniform_state_set4fv (program->uniforms, program->program_info, UNIFORM_SHARED_VIEWPORT, job->driver->stamps[UNIFORM_SHARED_VIEWPORT], 1, (const float *)&job->viewport); gsk_gl_uniform_state_set_matrix (program->uniforms, program->program_info, UNIFORM_SHARED_MODELVIEW, job->driver->stamps[UNIFORM_SHARED_MODELVIEW], &job->current_modelview->matrix); gsk_gl_uniform_state_set_matrix (program->uniforms, program->program_info, UNIFORM_SHARED_PROJECTION, job->driver->stamps[UNIFORM_SHARED_PROJECTION], &job->projection); gsk_gl_uniform_state_set_rounded_rect (program->uniforms, program->program_info, UNIFORM_SHARED_CLIP_RECT, job->driver->stamps[UNIFORM_SHARED_CLIP_RECT], &job->current_clip->rect); gsk_gl_uniform_state_set1f (program->uniforms, program->program_info, UNIFORM_SHARED_ALPHA, job->driver->stamps[UNIFORM_SHARED_ALPHA], job->alpha); return TRUE; } #define CHOOSE_PROGRAM(job,name) \ (job->current_clip->is_fully_contained \ ? job->driver->name ## _no_clip \ : (job->current_clip->is_rectilinear \ ? job->driver->name ## _rect_clip \ : job->driver->name)) static inline void gsk_gl_render_job_split_draw (GskGLRenderJob *job) { gsk_gl_command_queue_split_draw (job->command_queue); } static inline void gsk_gl_render_job_end_draw (GskGLRenderJob *job) { gsk_gl_command_queue_end_draw (job->command_queue); job->current_program = NULL; } static inline void gsk_gl_render_job_visit_as_fallback (GskGLRenderJob *job, const GskRenderNode *node) { float scale_x = job->scale_x; float scale_y = job->scale_y; int surface_width = ceilf (node->bounds.size.width * fabs (scale_x)); int surface_height = ceilf (node->bounds.size.height * fabs (scale_y)); GdkTexture *texture; cairo_surface_t *surface; cairo_surface_t *rendered_surface; cairo_t *cr; 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; texture_id = gsk_gl_driver_lookup_texture (job->driver, &key); if (texture_id != 0) goto done; /* 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, fabs (scale_x), fabs (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, fabs (scale_x), fabs (scale_y)); cr = cairo_create (surface); /* We draw upside down here, so it matches what GL does. */ cairo_save (cr); cairo_scale (cr, scale_x < 0 ? -1 : 1, scale_y < 0 ? 1 : -1); cairo_translate (cr, scale_x < 0 ? - surface_width / fabs (scale_x) : 0, scale_y < 0 ? 0 : - surface_height / fabs (scale_y)); cairo_set_source_surface (cr, rendered_surface, 0, 0); cairo_rectangle (cr, 0, 0, surface_width / fabs (scale_x), surface_height / fabs (scale_y)); cairo_fill (cr); cairo_restore (cr); 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_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_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); } cairo_destroy (cr); /* Create texture to upload */ texture = gdk_texture_new_for_surface (surface); texture_id = gsk_gl_driver_load_texture (job->driver, texture, FALSE); 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_gl_driver_cache_texture (job->driver, &key, texture_id); done: if (scale_x < 0 || scale_y < 0) { GskTransform *transform = gsk_transform_translate (gsk_transform_scale (NULL, scale_x < 0 ? -1 : 1, scale_y < 0 ? -1 : 1), &GRAPHENE_POINT_INIT (scale_x < 0 ? - (node->bounds.size.width + 2 * node->bounds.origin.x) : 0, scale_y < 0 ? - (node->bounds.size.height + 2 * node->bounds.origin.y) : 0)); gsk_gl_render_job_push_modelview (job, transform); gsk_transform_unref (transform); } if (!gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) goto out; gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_offscreen_rect (job, &node->bounds); gsk_gl_render_job_end_draw (job); out: if (scale_x < 0 || scale_y < 0) gsk_gl_render_job_pop_modelview (job); } static guint blur_offscreen (GskGLRenderJob *job, GskGLRenderOffscreen *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); GskGLRenderTarget *pass1; GskGLRenderTarget *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_gl_driver_create_render_target (job->driver, MAX (texture_to_blur_width, 1), MAX (texture_to_blur_height, 1), job->target_format, &pass1)) return 0; if (texture_to_blur_width <= 0 || texture_to_blur_height <= 0) return gsk_gl_driver_release_render_target (job->driver, pass1, FALSE); if (!gsk_gl_driver_create_render_target (job->driver, texture_to_blur_width, texture_to_blur_height, job->target_format, &pass2)) return gsk_gl_driver_release_render_target (job->driver, pass1, FALSE); gsk_gl_render_job_set_viewport (job, &new_clip.bounds, &prev_viewport); gsk_gl_render_job_set_projection_from_rect (job, &new_clip.bounds, &prev_projection); gsk_gl_render_job_set_modelview (job, NULL); gsk_gl_render_job_push_clip (job, &new_clip); /* Bind new framebuffer and clear it */ prev_fbo = gsk_gl_command_queue_bind_framebuffer (job->command_queue, pass1->framebuffer_id); gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport); /* Begin drawing the first horizontal pass, using offscreen as the * source texture for the program. */ if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blur))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, offscreen->texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_program_set_uniform1f (job->current_program, UNIFORM_BLUR_RADIUS, 0, blur_radius_x); gsk_gl_program_set_uniform2f (job->current_program, UNIFORM_BLUR_SIZE, 0, texture_to_blur_width, texture_to_blur_height); gsk_gl_program_set_uniform2f (job->current_program, UNIFORM_BLUR_DIR, 0, 1, 0); gsk_gl_render_job_draw_coords (job, 0, 0, texture_to_blur_width, texture_to_blur_height, 0, 1, 1, 0, (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO }); gsk_gl_render_job_end_draw (job); } /* Bind second pass framebuffer and clear it */ gsk_gl_command_queue_bind_framebuffer (job->command_queue, pass2->framebuffer_id); gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport); /* Draw using blur program with first pass as source texture */ if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blur))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, pass1->texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_program_set_uniform1f (job->current_program, UNIFORM_BLUR_RADIUS, 0, blur_radius_y); gsk_gl_program_set_uniform2f (job->current_program, UNIFORM_BLUR_SIZE, 0, texture_to_blur_width, texture_to_blur_height); gsk_gl_program_set_uniform2f (job->current_program, UNIFORM_BLUR_DIR, 0, 0, 1); gsk_gl_render_job_draw_coords (job, 0, 0, texture_to_blur_width, texture_to_blur_height, 0, 1, 1, 0, (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO }); gsk_gl_render_job_end_draw (job); } gsk_gl_render_job_pop_modelview (job); gsk_gl_render_job_pop_clip (job); gsk_gl_render_job_set_viewport (job, &prev_viewport, NULL); gsk_gl_render_job_set_projection (job, &prev_projection); gsk_gl_command_queue_bind_framebuffer (job->command_queue, prev_fbo); gsk_gl_driver_release_render_target (job->driver, pass1, TRUE); return gsk_gl_driver_release_render_target (job->driver, pass2, FALSE); } static void blur_node (GskGLRenderJob *job, GskGLRenderOffscreen *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_gl_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 * fabs (scale_x), texture_height * fabs (scale_y), blur_radius * fabs (scale_x), blur_radius * fabs (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; } #define ATLAS_SIZE 512 static inline void gsk_gl_render_job_visit_color_node (GskGLRenderJob *job, const GskRenderNode *node) { const GdkRGBA *rgba; guint16 color[4]; GskGLProgram *program; GskGLCommandBatch *batch; rgba = gsk_color_node_get_color (node); if (RGBA_IS_CLEAR (rgba)) return; rgba_to_half (rgba, color); /* Avoid switching away from the coloring program for * rendering a solid color. */ program = CHOOSE_PROGRAM (job, coloring); batch = gsk_gl_command_queue_get_batch (job->command_queue); /* Limit the size, or we end up with a coordinate overflow somewhere. */ if (job->source_is_glyph_atlas && node->bounds.size.width < 300 && node->bounds.size.height < 300 && batch->any.kind == GSK_GL_COMMAND_KIND_DRAW && batch->any.program == program->id) { GskGLRenderOffscreen offscreen = {0}; if (gsk_gl_render_job_begin_draw (job, program)) { /* The top left few pixels in our atlases are always * solid white, so we can use it here, without * having to choose any particular atlas texture. */ offscreen.was_offscreen = FALSE; offscreen.area.x = 1.f / ATLAS_SIZE; offscreen.area.y = 1.f / ATLAS_SIZE; offscreen.area.x2 = 2.f / ATLAS_SIZE; offscreen.area.y2 = 2.f / ATLAS_SIZE; gsk_gl_render_job_draw_offscreen_with_color (job, &node->bounds, &offscreen, color); gsk_gl_render_job_end_draw (job); } } else { if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color))) { gsk_gl_render_job_draw_rect_with_color (job, &node->bounds, color); gsk_gl_render_job_end_draw (job); } } } static inline void gsk_gl_render_job_visit_linear_gradient_node (GskGLRenderJob *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_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); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, linear_gradient))) { gsk_gl_program_set_uniform1i (job->current_program, UNIFORM_LINEAR_GRADIENT_NUM_COLOR_STOPS, 0, n_color_stops); gsk_gl_program_set_uniform1fv (job->current_program, UNIFORM_LINEAR_GRADIENT_COLOR_STOPS, 0, n_color_stops * 5, (const float *)stops); gsk_gl_program_set_uniform4f (job->current_program, UNIFORM_LINEAR_GRADIENT_POINTS, 0, x1, y1, x2 - x1, y2 - y1); gsk_gl_program_set_uniform1i (job->current_program, UNIFORM_LINEAR_GRADIENT_REPEAT, 0, repeat); gsk_gl_render_job_draw_rect (job, &node->bounds); gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_conic_gradient_node (GskGLRenderJob *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); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, conic_gradient))) { gsk_gl_program_set_uniform1i (job->current_program, UNIFORM_CONIC_GRADIENT_NUM_COLOR_STOPS, 0, n_color_stops); gsk_gl_program_set_uniform1fv (job->current_program, UNIFORM_CONIC_GRADIENT_COLOR_STOPS, 0, n_color_stops * 5, (const float *)stops); gsk_gl_program_set_uniform4f (job->current_program, UNIFORM_CONIC_GRADIENT_GEOMETRY, 0, job->offset_x + center->x, job->offset_y + center->y, scale, bias); gsk_gl_render_job_draw_rect (job, &node->bounds); gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_radial_gradient_node (GskGLRenderJob *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_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); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, radial_gradient))) { gsk_gl_program_set_uniform1i (job->current_program, UNIFORM_RADIAL_GRADIENT_NUM_COLOR_STOPS, 0, n_color_stops); gsk_gl_program_set_uniform1fv (job->current_program, UNIFORM_RADIAL_GRADIENT_COLOR_STOPS, 0, n_color_stops * 5, (const float *)stops); gsk_gl_program_set_uniform1i (job->current_program, UNIFORM_RADIAL_GRADIENT_REPEAT, 0, repeat); gsk_gl_program_set_uniform2f (job->current_program, UNIFORM_RADIAL_GRADIENT_RANGE, 0, scale, bias); gsk_gl_program_set_uniform4f (job->current_program, 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_gl_render_job_draw_rect (job, &node->bounds); gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_clipped_child (GskGLRenderJob *job, const GskRenderNode *child, const graphene_rect_t *clip) { graphene_rect_t transformed_clip; GskRoundedRect intersection; GskRoundedRectIntersection result; gsk_gl_render_job_transform_bounds (job, clip, &transformed_clip); if (job->current_clip->is_rectilinear) { memset (&intersection.corner, 0, sizeof intersection.corner); gsk_rect_intersection (&transformed_clip, &job->current_clip->rect.bounds, &intersection.bounds); gsk_gl_render_job_push_clip (job, &intersection); gsk_gl_render_job_visit_node (job, child); gsk_gl_render_job_pop_clip (job); return; } result = gsk_rounded_rect_intersect_with_rect (&job->current_clip->rect, &transformed_clip, &intersection); if (result == GSK_INTERSECTION_EMPTY) return; if (result == GSK_INTERSECTION_NONEMPTY) { gsk_gl_render_job_push_clip (job, &intersection); gsk_gl_render_job_visit_node (job, child); gsk_gl_render_job_pop_clip (job); } else { GskGLRenderOffscreen offscreen = {0}; offscreen.bounds = clip; offscreen.force_offscreen = TRUE; offscreen.reset_clip = TRUE; offscreen.do_not_cache = TRUE; gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen); g_assert (offscreen.texture_id); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, offscreen.texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_offscreen_rect (job, clip); gsk_gl_render_job_end_draw (job); } } } static inline void gsk_gl_render_job_visit_clip_node (GskGLRenderJob *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_gl_render_job_visit_clipped_child (job, child, clip); } static inline void gsk_gl_render_job_visit_rounded_clip_node (GskGLRenderJob *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; gboolean need_offscreen; if (node_is_invisible (child)) return; gsk_gl_render_job_transform_rounded_rect (job, clip, &transformed_clip); if (job->current_clip->is_rectilinear) { GskRoundedRect intersected_clip; GskRoundedRectIntersection result; result = gsk_rounded_rect_intersect_with_rect (&transformed_clip, &job->current_clip->rect.bounds, &intersected_clip); if (result == GSK_INTERSECTION_EMPTY) return; if (result == GSK_INTERSECTION_NONEMPTY) { gsk_gl_render_job_push_clip (job, &intersected_clip); gsk_gl_render_job_visit_node (job, child); gsk_gl_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 (clips_get_size (&job->clip) <= 1) need_offscreen = FALSE; else if (gsk_rounded_rect_contains_rect (&job->current_clip->rect, &transformed_clip.bounds)) need_offscreen = FALSE; else need_offscreen = TRUE; if (!need_offscreen) { gsk_gl_render_job_push_clip (job, &transformed_clip); gsk_gl_render_job_visit_node (job, child); gsk_gl_render_job_pop_clip (job); } else { GskGLRenderOffscreen offscreen = {0}; offscreen.bounds = &node->bounds; offscreen.force_offscreen = TRUE; offscreen.reset_clip = FALSE; gsk_gl_render_job_push_clip (job, &transformed_clip); if (!gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen)) g_assert_not_reached (); gsk_gl_render_job_pop_clip (job); g_assert (offscreen.texture_id); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, offscreen.texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen); gsk_gl_render_job_end_draw (job); } } } static inline void gsk_gl_render_job_visit_rect_border_node (GskGLRenderJob *job, const GskRenderNode *node) { const GdkRGBA *colors = gsk_border_node_get_colors (node); const float *widths = gsk_border_node_get_widths (node); const graphene_point_t *origin = &node->bounds.origin; const graphene_size_t *size = &node->bounds.size; guint16 color[4]; if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color))) { if (widths[0] > 0) { rgba_to_half (&colors[0], color); gsk_gl_render_job_draw_rect_with_color (job, &GRAPHENE_RECT_INIT (origin->x, origin->y, size->width - widths[1], widths[0]), color); } if (widths[1] > 0) { rgba_to_half (&colors[1], color); gsk_gl_render_job_draw_rect_with_color (job, &GRAPHENE_RECT_INIT (origin->x + size->width - widths[1], origin->y, widths[1], size->height - widths[2]), color); } if (widths[2] > 0) { rgba_to_half (&colors[2], color); gsk_gl_render_job_draw_rect_with_color (job, &GRAPHENE_RECT_INIT (origin->x + widths[3], origin->y + size->height - widths[2], size->width - widths[3], widths[2]), color); } if (widths[3] > 0) { rgba_to_half (&colors[3], color); gsk_gl_render_job_draw_rect_with_color (job, &GRAPHENE_RECT_INIT (origin->x, origin->y + widths[0], widths[3], size->height - widths[0]), color); } gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_border_node (GskGLRenderJob *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]; 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; GskRoundedRect outline; guint16 color[4]; memset (sizes, 0, sizeof sizes); if (widths[0] > 0) { sizes[0].h = MAX (widths[0], rounded_outline->corner[0].height); sizes[1].h = MAX (widths[0], rounded_outline->corner[1].height); } if (widths[1] > 0) { sizes[1].w = MAX (widths[1], rounded_outline->corner[1].width); sizes[2].w = MAX (widths[1], rounded_outline->corner[2].width); } if (widths[2] > 0) { sizes[2].h = MAX (widths[2], rounded_outline->corner[2].height); sizes[3].h = MAX (widths[2], rounded_outline->corner[3].height); } if (widths[3] > 0) { sizes[0].w = MAX (widths[3], rounded_outline->corner[0].width); sizes[3].w = MAX (widths[3], rounded_outline->corner[3].width); } gsk_gl_render_job_translate_rounded_rect (job, rounded_outline, &outline); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, border))) { gsk_gl_program_set_uniform4fv (job->current_program, UNIFORM_BORDER_WIDTHS, 0, 1, widths); gsk_gl_program_set_uniform_rounded_rect (job->current_program, UNIFORM_BORDER_OUTLINE_RECT, 0, &outline); if (widths[0] > 0) { GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue); rgba_to_half (&colors[0], color); vertices[0] = (GskGLDrawVertex) { .position = { min_x, min_y }, .uv = { 0, 1 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[1] = (GskGLDrawVertex) { .position = { min_x + sizes[0].w, min_y + sizes[0].h }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[2] = (GskGLDrawVertex) { .position = { max_x, min_y }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[3] = (GskGLDrawVertex) { .position = { max_x - sizes[1].w, min_y + sizes[1].h }, .uv = { 1, 0 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[4] = (GskGLDrawVertex) { .position = { min_x + sizes[0].w, min_y + sizes[0].h }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[5] = (GskGLDrawVertex) { .position = { max_x, min_y }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } }; } if (widths[1] > 0) { GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue); rgba_to_half (&colors[1], color); vertices[0] = (GskGLDrawVertex) { .position = { max_x - sizes[1].w, min_y + sizes[1].h }, .uv = { 0, 1 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[1] = (GskGLDrawVertex) { .position = { max_x - sizes[2].w, max_y - sizes[2].h }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[2] = (GskGLDrawVertex) { .position = { max_x, min_y }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[3] = (GskGLDrawVertex) { .position = { max_x, max_y }, .uv = { 1, 0 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[4] = (GskGLDrawVertex) { .position = { max_x - sizes[2].w, max_y - sizes[2].h }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[5] = (GskGLDrawVertex) { .position = { max_x, min_y }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } }; } if (widths[2] > 0) { GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue); rgba_to_half (&colors[2], color); vertices[0] = (GskGLDrawVertex) { .position = { min_x + sizes[3].w, max_y - sizes[3].h }, .uv = { 0, 1 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[1] = (GskGLDrawVertex) { .position = { min_x, max_y }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[2] = (GskGLDrawVertex) { .position = { max_x - sizes[2].w, max_y - sizes[2].h }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[3] = (GskGLDrawVertex) { .position = { max_x, max_y }, .uv = { 1, 0 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[4] = (GskGLDrawVertex) { .position = { min_x , max_y }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[5] = (GskGLDrawVertex) { .position = { max_x - sizes[2].w, max_y - sizes[2].h }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } }; } if (widths[3] > 0) { GskGLDrawVertex *vertices = gsk_gl_command_queue_add_vertices (job->command_queue); rgba_to_half (&colors[3], color); vertices[0] = (GskGLDrawVertex) { .position = { min_x, min_y }, .uv = { 0, 1 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[1] = (GskGLDrawVertex) { .position = { min_x, max_y }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[2] = (GskGLDrawVertex) { .position = { min_x + sizes[0].w, min_y + sizes[0].h }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[3] = (GskGLDrawVertex) { .position = { min_x + sizes[3].w, max_y - sizes[3].h }, .uv = { 1, 0 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[4] = (GskGLDrawVertex) { .position = { min_x, max_y }, .uv = { 0, 0 }, .color = { color[0], color[1], color[2], color[3] } }; vertices[5] = (GskGLDrawVertex) { .position = { min_x + sizes[0].w, min_y + sizes[0].h }, .uv = { 1, 1 }, .color = { color[0], color[1], color[2], color[3] } }; } gsk_gl_render_job_end_draw (job); } } /* A special case for a pattern that occurs frequently with CSS * backgrounds: two sibling nodes, the first of which is a rounded * clip node with a color node as child, and the second one is a * border node, with the same outline as the clip node. We render * this using the filled_border shader. */ static void gsk_gl_render_job_visit_css_background (GskGLRenderJob *job, const GskRenderNode *node, const GskRenderNode *node2) { const GskRenderNode *child = gsk_rounded_clip_node_get_child (node); const GskRoundedRect *rounded_outline = gsk_border_node_get_outline (node2); const float *widths = gsk_border_node_get_widths (node2); float min_x = job->offset_x + node2->bounds.origin.x; float min_y = job->offset_y + node2->bounds.origin.y; float max_x = min_x + node2->bounds.size.width; float max_y = min_y + node2->bounds.size.height; GskRoundedRect outline; GskGLDrawVertex *vertices; guint16 color[4]; guint16 color2[4]; if (node_is_invisible (node2)) return; rgba_to_half (&gsk_border_node_get_colors (node2)[0], color); rgba_to_half (gsk_color_node_get_color (child), color2); gsk_gl_render_job_translate_rounded_rect (job, rounded_outline, &outline); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, filled_border))) { gsk_gl_program_set_uniform4fv (job->current_program, UNIFORM_FILLED_BORDER_WIDTHS, 0, 1, widths); gsk_gl_program_set_uniform_rounded_rect (job->current_program, UNIFORM_FILLED_BORDER_OUTLINE_RECT, 0, &outline); vertices = gsk_gl_command_queue_add_vertices (job->command_queue); vertices[0] = (GskGLDrawVertex) { .position = { min_x, min_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } }; vertices[1] = (GskGLDrawVertex) { .position = { min_x, max_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } }; vertices[2] = (GskGLDrawVertex) { .position = { max_x, min_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } }; vertices[3] = (GskGLDrawVertex) { .position = { max_x, max_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } }; vertices[4] = (GskGLDrawVertex) { .position = { min_x, max_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } }; vertices[5] = (GskGLDrawVertex) { .position = { max_x, min_y }, .color = { color[0], color[1], color[2], color[3] }, .color2 = { color2[0], color2[1], color2[2], color2[3] } }; gsk_gl_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_gl_render_job_visit_transform_node (GskGLRenderJob *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_gl_render_job_visit_node (job, child); break; case GSK_TRANSFORM_CATEGORY_2D_TRANSLATE: { float dx, dy; gsk_transform_node_get_translate (node, &dx, &dy); gsk_gl_render_job_offset (job, dx, dy); gsk_gl_render_job_visit_node (job, child); gsk_gl_render_job_offset (job, -dx, -dy); } break; case GSK_TRANSFORM_CATEGORY_2D_AFFINE: { gsk_gl_render_job_push_modelview (job, transform); gsk_gl_render_job_visit_node (job, child); gsk_gl_render_job_pop_modelview (job); } break; case GSK_TRANSFORM_CATEGORY_2D: if (node_supports_2d_transform (child)) { gsk_gl_render_job_push_modelview (job, transform); gsk_gl_render_job_visit_node (job, child); gsk_gl_render_job_pop_modelview (job); return; } G_GNUC_FALLTHROUGH; case GSK_TRANSFORM_CATEGORY_3D: case GSK_TRANSFORM_CATEGORY_ANY: case GSK_TRANSFORM_CATEGORY_UNKNOWN: if (node_supports_transform (child)) { gsk_gl_render_job_push_modelview (job, transform); gsk_gl_render_job_visit_node (job, child); gsk_gl_render_job_pop_modelview (job); } else { GskGLRenderOffscreen offscreen = {0}; float sx = 1, sy = 1; gboolean linear_filter = FALSE; offscreen.bounds = &child->bounds; offscreen.force_offscreen = FALSE; offscreen.reset_clip = TRUE; if (!result_is_axis_aligned (transform, &child->bounds)) linear_filter = TRUE; if (category == GSK_TRANSFORM_CATEGORY_2D) { graphene_matrix_t m; double a, b, c, d, tx, ty; g_assert (transform != NULL); gsk_transform_to_matrix (transform, &m); if (graphene_matrix_to_2d (&m, &a, &b, &c, &d, &tx, &ty)) { sx = sqrt (a * a + b * b); sy = sqrt (c * c + d * d); } else sx = sy = 1; if (sx != 1 || sy != 1) { GskTransform *scale; scale = gsk_transform_translate (gsk_transform_scale (NULL, sx, sy), &GRAPHENE_POINT_INIT (tx, ty)); gsk_gl_render_job_push_modelview (job, scale); transform = gsk_transform_transform (gsk_transform_invert (scale), transform); } } if (gsk_gl_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. */ if (transform) gsk_gl_render_job_push_modelview (job, transform); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) { gsk_gl_program_set_uniform_texture_with_filter (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, offscreen.texture_id, linear_filter ? GL_LINEAR : GL_NEAREST, linear_filter ? GL_LINEAR : GL_NEAREST); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_offscreen (job, &child->bounds, &offscreen); gsk_gl_render_job_end_draw (job); } if (transform) gsk_gl_render_job_pop_modelview (job); } if (category == GSK_TRANSFORM_CATEGORY_2D) { if (sx != 1 || sy != 1) { gsk_gl_render_job_pop_modelview (job); gsk_transform_unref (transform); } } } break; default: g_assert_not_reached (); } } static inline void gsk_gl_render_job_visit_unblurred_inset_shadow_node (GskGLRenderJob *job, const GskRenderNode *node) { const GskRoundedRect *outline = gsk_inset_shadow_node_get_outline (node); GskRoundedRect transformed_outline; guint16 color[4]; gsk_gl_render_job_translate_rounded_rect (job, outline, &transformed_outline); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, inset_shadow))) { gsk_gl_program_set_uniform_rounded_rect (job->current_program, UNIFORM_INSET_SHADOW_OUTLINE_RECT, 0, &transformed_outline); gsk_gl_program_set_uniform1f (job->current_program, UNIFORM_INSET_SHADOW_SPREAD, 0, gsk_inset_shadow_node_get_spread (node)); gsk_gl_program_set_uniform2f (job->current_program, UNIFORM_INSET_SHADOW_OFFSET, 0, gsk_inset_shadow_node_get_dx (node), gsk_inset_shadow_node_get_dy (node)); rgba_to_half (gsk_inset_shadow_node_get_color (node), color); gsk_gl_render_job_draw_rect_with_color (job, &node->bounds, color); gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_blurred_inset_shadow_node (GskGLRenderJob *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; GskGLRenderOffscreen offscreen = {0}; guint16 color[4]; 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; blurred_texture_id = gsk_gl_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; GskGLRenderTarget *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_gl_driver_create_render_target (job->driver, texture_width, texture_height, get_target_format (job, node), &render_target)) g_assert_not_reached (); gsk_gl_render_job_set_viewport_for_size (job, texture_width, texture_height, &prev_viewport); gsk_gl_render_job_set_projection_for_size (job, texture_width, texture_height, &prev_projection); gsk_gl_render_job_set_modelview (job, NULL); gsk_gl_render_job_push_clip (job, &GSK_ROUNDED_RECT_INIT (0, 0, texture_width, texture_height)); prev_fbo = gsk_gl_command_queue_bind_framebuffer (job->command_queue, render_target->framebuffer_id); gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport); gsk_gl_render_job_translate_rounded_rect (job, &outline_to_blur, &transformed_outline); /* Actual inset shadow outline drawing */ if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, inset_shadow))) { gsk_gl_program_set_uniform_rounded_rect (job->current_program, UNIFORM_INSET_SHADOW_OUTLINE_RECT, 0, &transformed_outline); gsk_gl_program_set_uniform1f (job->current_program, UNIFORM_INSET_SHADOW_SPREAD, 0, spread * MAX (scale_x, scale_y)); gsk_gl_program_set_uniform2f (job->current_program, UNIFORM_INSET_SHADOW_OFFSET, 0, offset_x * scale_x, offset_y * scale_y); rgba_to_half (gsk_inset_shadow_node_get_color (node), color); gsk_gl_render_job_draw_with_color (job, 0, 0, texture_width, texture_height, color); gsk_gl_render_job_end_draw (job); } gsk_gl_render_job_pop_modelview (job); gsk_gl_render_job_pop_clip (job); gsk_gl_render_job_set_projection (job, &prev_projection); gsk_gl_render_job_set_viewport (job, &prev_viewport, NULL); gsk_gl_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 * fabs (scale_x), blur_radius * fabs (scale_y)); gsk_gl_driver_release_render_target (job->driver, render_target, TRUE); gsk_gl_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_gl_render_job_translate_rounded_rect (job, node_outline, &node_clip); gsk_gl_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; if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, blurred_texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen); gsk_gl_render_job_end_draw (job); } if (needs_clip) gsk_gl_render_job_pop_clip (job); } } static inline void gsk_gl_render_job_visit_unblurred_outset_shadow_node (GskGLRenderJob *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); guint16 color[4]; 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 }, }; rgba_to_half (gsk_outset_shadow_node_get_color (node), color); gsk_gl_render_job_translate_rounded_rect (job, outline, &transformed_outline); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, unblurred_outset_shadow))) { gsk_gl_program_set_uniform_rounded_rect (job->current_program, UNIFORM_UNBLURRED_OUTSET_SHADOW_OUTLINE_RECT, 0, &transformed_outline); gsk_gl_program_set_uniform1f (job->current_program, UNIFORM_UNBLURRED_OUTSET_SHADOW_SPREAD, 0, spread); gsk_gl_program_set_uniform2f (job->current_program, UNIFORM_UNBLURRED_OUTSET_SHADOW_OFFSET, 0, dx, dy); /* Corners... */ if (corner_sizes[0][0] > 0 && corner_sizes[0][1] > 0) /* Top left */ gsk_gl_render_job_draw_with_color (job, x, y, corner_sizes[0][0], corner_sizes[0][1], color); if (corner_sizes[1][0] > 0 && corner_sizes[1][1] > 0) /* Top right */ gsk_gl_render_job_draw_with_color (job, x + w - corner_sizes[1][0], y, corner_sizes[1][0], corner_sizes[1][1], color); if (corner_sizes[2][0] > 0 && corner_sizes[2][1] > 0) /* Bottom right */ gsk_gl_render_job_draw_with_color (job, x + w - corner_sizes[2][0], y + h - corner_sizes[2][1], corner_sizes[2][0], corner_sizes[2][1], color); if (corner_sizes[3][0] > 0 && corner_sizes[3][1] > 0) /* Bottom left */ gsk_gl_render_job_draw_with_color (job, x, y + h - corner_sizes[3][1], corner_sizes[3][0], corner_sizes[3][1], color); /* Edges... */; if (edge_sizes[0] > 0) /* Top */ gsk_gl_render_job_draw_with_color (job, x + corner_sizes[0][0], y, w - corner_sizes[0][0] - corner_sizes[1][0], edge_sizes[0], color); if (edge_sizes[1] > 0) /* Right */ gsk_gl_render_job_draw_with_color (job, x + w - edge_sizes[1], y + corner_sizes[1][1], edge_sizes[1], h - corner_sizes[1][1] - corner_sizes[2][1], color); if (edge_sizes[2] > 0) /* Bottom */ gsk_gl_render_job_draw_with_color (job, x + corner_sizes[3][0], y + h - edge_sizes[2], w - corner_sizes[3][0] - corner_sizes[2][0], edge_sizes[2], color); if (edge_sizes[3] > 0) /* Left */ gsk_gl_render_job_draw_with_color (job, x, y + corner_sizes[0][1], edge_sizes[3], h - corner_sizes[0][1] - corner_sizes[3][1], color); gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_blurred_outset_shadow_node (GskGLRenderJob *job, const GskRenderNode *node) { const GskRoundedRect *outline = gsk_outset_shadow_node_get_outline (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_x = ceilf (half_blur_extra * scale_x); int extra_blur_pixels_y = ceilf (half_blur_extra * scale_y); 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; GskGLRenderOffscreen offscreen = {0}; int texture_width, texture_height; int blurred_texture_id; int cached_tid; gboolean do_slicing; guint16 color[4]; float half_width = outline->bounds.size.width / 2; float half_height = outline->bounds.size.height / 2; rgba_to_half (gsk_outset_shadow_node_get_color (node), color); /* 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 || outline->corner[0].width >= half_width || outline->corner[1].width >= half_width || outline->corner[2].width >= half_width || outline->corner[3].width >= half_width || outline->corner[0].height >= half_height || outline->corner[1].height >= half_height || outline->corner[2].height >= half_height || outline->corner[3].height >= half_height) { 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_x; scaled_outline.bounds.origin.y = extra_blur_pixels_y; scaled_outline.bounds.size.width = texture_width - (extra_blur_pixels_x * 2); scaled_outline.bounds.size.height = texture_height - (extra_blur_pixels_y * 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_gl_shadow_library_lookup (job->driver->shadows_library, &scaled_outline, blur_radius); if (cached_tid == 0) { GdkGLContext *context = job->command_queue->context; GskGLRenderTarget *render_target; graphene_matrix_t prev_projection; graphene_rect_t prev_viewport; guint prev_fbo; gsk_gl_driver_create_render_target (job->driver, texture_width, texture_height, get_target_format (job, node), &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_gl_render_job_set_projection_for_size (job, texture_width, texture_height, &prev_projection); gsk_gl_render_job_set_viewport_for_size (job, texture_width, texture_height, &prev_viewport); gsk_gl_render_job_set_modelview (job, NULL); gsk_gl_render_job_push_clip (job, &scaled_outline); /* Bind render target and clear it */ prev_fbo = gsk_gl_command_queue_bind_framebuffer (job->command_queue, render_target->framebuffer_id); gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport); /* Draw the outline using color program */ if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color))) { gsk_gl_render_job_draw_with_color (job, 0, 0, texture_width, texture_height, (guint16[]){ FP16_ONE, FP16_ONE, FP16_ONE, FP16_ONE }); gsk_gl_render_job_end_draw (job); } /* Reset state from offscreen */ gsk_gl_render_job_pop_clip (job); gsk_gl_render_job_pop_modelview (job); gsk_gl_render_job_set_viewport (job, &prev_viewport, NULL); gsk_gl_render_job_set_projection (job, &prev_projection); /* Now blur the outline */ init_full_texture_region (&offscreen); offscreen.texture_id = gsk_gl_driver_release_render_target (job->driver, render_target, FALSE); blurred_texture_id = blur_offscreen (job, &offscreen, texture_width, texture_height, blur_radius * fabs (scale_x), blur_radius * fabs (scale_y)); gsk_gl_shadow_library_insert (job->driver->shadows_library, &scaled_outline, blur_radius, blurred_texture_id); gsk_gl_command_queue_bind_framebuffer (job->command_queue, prev_fbo); } else { blurred_texture_id = cached_tid; } gsk_gl_render_job_translate_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 = TRUE; offscreen.texture_id = blurred_texture_id; init_full_texture_region (&offscreen); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, outset_shadow))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, blurred_texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_program_set_uniform_rounded_rect (job->current_program, UNIFORM_OUTSET_SHADOW_OUTLINE_RECT, 0, &transformed_outline); gsk_gl_render_job_draw_offscreen_with_color (job, &GRAPHENE_RECT_INIT (min_x, min_y, texture_width / scale_x, texture_height / scale_y), &offscreen, color); gsk_gl_render_job_end_draw (job); } return; } /* slicing */ if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, outset_shadow))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, blurred_texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_program_set_uniform_rounded_rect (job->current_program, 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 GskGLTextureNineSlice *slices; float left_width, center_width, right_width; float top_height, center_height, bottom_height; GskGLTexture *texture; texture = gsk_gl_driver_get_texture_by_id (job->driver, blurred_texture_id); slices = gsk_gl_texture_get_nine_slice (texture, &scaled_outline, extra_blur_pixels_x, extra_blur_pixels_y); offscreen.was_offscreen = TRUE; /* Our texture coordinates MUST be scaled, while the actual vertex coords * MUST NOT be scaled. */ left_width = slices[NINE_SLICE_TOP_LEFT].rect.width / scale_x; right_width = slices[NINE_SLICE_TOP_RIGHT].rect.width / scale_x; center_width = (max_x - min_x) - (left_width + right_width); top_height = slices[NINE_SLICE_TOP_LEFT].rect.height / scale_y; bottom_height = slices[NINE_SLICE_BOTTOM_LEFT].rect.height / scale_y; center_height = (max_y - min_y) - (top_height + bottom_height); /* 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_gl_render_job_draw_offscreen_with_color (job, &GRAPHENE_RECT_INIT (min_x, min_y, left_width, top_height), &offscreen, color); } /* Top center */ if (nine_slice_is_visible (&slices[NINE_SLICE_TOP_CENTER])) { memcpy (&offscreen.area, &slices[NINE_SLICE_TOP_CENTER].area, sizeof offscreen.area); gsk_gl_render_job_draw_offscreen_with_color (job, &GRAPHENE_RECT_INIT (min_x + left_width, min_y, center_width, top_height), &offscreen, color); } /* 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_gl_render_job_draw_offscreen_with_color (job, &GRAPHENE_RECT_INIT (max_x - right_width, min_y, right_width, top_height), &offscreen, color); } /* 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_gl_render_job_draw_offscreen_with_color (job, &GRAPHENE_RECT_INIT (max_x - right_width, max_y - bottom_height, right_width, bottom_height), &offscreen, color); } /* 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_gl_render_job_draw_offscreen_with_color (job, &GRAPHENE_RECT_INIT (min_x, max_y - bottom_height, left_width, bottom_height), &offscreen, color); } /* Left side */ if (nine_slice_is_visible (&slices[NINE_SLICE_LEFT_CENTER])) { memcpy (&offscreen.area, &slices[NINE_SLICE_LEFT_CENTER].area, sizeof offscreen.area); gsk_gl_render_job_draw_offscreen_with_color (job, &GRAPHENE_RECT_INIT (min_x, min_y + top_height, left_width, center_height), &offscreen, color); } /* Right side */ if (nine_slice_is_visible (&slices[NINE_SLICE_RIGHT_CENTER])) { memcpy (&offscreen.area, &slices[NINE_SLICE_RIGHT_CENTER].area, sizeof offscreen.area); gsk_gl_render_job_draw_offscreen_with_color (job, &GRAPHENE_RECT_INIT (max_x - right_width, min_y + top_height, right_width, center_height), &offscreen, color); } /* Bottom side */ if (nine_slice_is_visible (&slices[NINE_SLICE_BOTTOM_CENTER])) { memcpy (&offscreen.area, &slices[NINE_SLICE_BOTTOM_CENTER].area, sizeof offscreen.area); gsk_gl_render_job_draw_offscreen_with_color (job, &GRAPHENE_RECT_INIT (min_x + left_width, max_y - bottom_height, center_width, bottom_height), &offscreen, color); } /* Middle */ if (nine_slice_is_visible (&slices[NINE_SLICE_CENTER])) { if (!gsk_rounded_rect_contains_rect (outline, &GRAPHENE_RECT_INIT (min_x + left_width, min_y + top_height, center_width, center_height))) { memcpy (&offscreen.area, &slices[NINE_SLICE_CENTER].area, sizeof offscreen.area); gsk_gl_render_job_draw_offscreen_with_color (job, &GRAPHENE_RECT_INIT (min_x + left_width, min_y + top_height, center_width, center_height), &offscreen, color); } } } gsk_gl_render_job_end_draw (job); } } static inline gboolean G_GNUC_PURE equal_texture_nodes (const GskRenderNode *node1, const GskRenderNode *node2) { if (GSK_RENDER_NODE_TYPE (node1) != GSK_TEXTURE_NODE || GSK_RENDER_NODE_TYPE (node2) != GSK_TEXTURE_NODE) return FALSE; if (gsk_texture_node_get_texture (node1) != gsk_texture_node_get_texture (node2)) return FALSE; return gsk_rect_equal (&node1->bounds, &node2->bounds); } static inline void gsk_gl_render_job_visit_cross_fade_node (GskGLRenderJob *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); GskGLRenderOffscreen offscreen_start = {0}; GskGLRenderOffscreen 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; gsk_gl_render_job_set_modelview (job, gsk_transform_scale (NULL, fabs (job->scale_x), fabs (job->scale_y))); if (!gsk_gl_render_job_visit_node_with_offscreen (job, start_node, &offscreen_start)) { gsk_gl_render_job_pop_modelview (job); gsk_gl_render_job_visit_node (job, end_node); return; } g_assert (offscreen_start.texture_id); if (!gsk_gl_render_job_visit_node_with_offscreen (job, end_node, &offscreen_end)) { float prev_alpha; gsk_gl_render_job_pop_modelview (job); prev_alpha = gsk_gl_render_job_set_alpha (job, job->alpha * progress); gsk_gl_render_job_visit_node (job, start_node); gsk_gl_render_job_set_alpha (job, prev_alpha); return; } gsk_gl_render_job_pop_modelview (job); g_assert (offscreen_end.texture_id); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, cross_fade))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, offscreen_start.texture_id); gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_CROSS_FADE_SOURCE2, 0, GL_TEXTURE_2D, GL_TEXTURE1, offscreen_end.texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_program_set_uniform1f (job->current_program, UNIFORM_CROSS_FADE_PROGRESS, 0, progress); gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen_end); gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_opacity_node (GskGLRenderJob *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_gl_render_job_set_alpha (job, new_alpha); if (!gsk_render_node_use_offscreen_for_opacity (child)) { gsk_gl_render_job_visit_node (job, child); gsk_gl_render_job_set_alpha (job, prev_alpha); } else { GskGLRenderOffscreen offscreen = {0}; offscreen.bounds = &child->bounds; offscreen.force_offscreen = TRUE; offscreen.reset_clip = TRUE; /* Note: offscreen rendering resets alpha to 1.0 */ if (!gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen)) return; g_assert (offscreen.texture_id); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, offscreen.texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen); gsk_gl_render_job_end_draw (job); } } gsk_gl_render_job_set_alpha (job, prev_alpha); } } static inline int compute_phase_and_pos (float value, float *pos) { float v; *pos = floorf (value); v = value - *pos; if (v < 0.125) return 0; else if (v < 0.375) return 1; else if (v < 0.625) return 2; else if (v < 0.875) return 3; else { *pos += 1; return 0; } } static inline void gsk_gl_render_job_visit_text_node (GskGLRenderJob *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 (fabs (job->scale_x), fabs (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; GskGLGlyphLibrary *library = job->driver->glyphs_library; GskGLCommandBatch *batch; int x_position = 0; GskGLGlyphKey lookup; guint last_texture = 0; GskGLDrawVertex *vertices; guint used = 0; guint16 nc[4] = { FP16_MINUS_ONE, FP16_MINUS_ONE, FP16_MINUS_ONE, FP16_MINUS_ONE }; guint16 cc[4]; const guint16 *c; const PangoGlyphInfo *gi; guint i; int yshift; float ypos; if (num_glyphs == 0) return; if ((force_color || !gsk_text_node_has_color_glyphs (node)) && RGBA_IS_CLEAR (color)) return; rgba_to_half (color, cc); lookup.font = (PangoFont *)font; lookup.scale = (guint) (text_scale * 1024); yshift = compute_phase_and_pos (y, &ypos); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, coloring))) { batch = gsk_gl_command_queue_get_batch (job->command_queue); vertices = gsk_gl_command_queue_add_n_vertices (job->command_queue, num_glyphs); /* We use one quad per character */ for (i = 0, gi = glyphs; i < num_glyphs; i++, gi++) { const GskGLGlyphValue *glyph; float glyph_x, glyph_y, glyph_x2, glyph_y2; float tx, ty, tx2, ty2; float cx; float cy; guint texture_id; lookup.glyph = gi->glyph; /* If the glyph has color, we don't need to recolor anything. * We tell the shader by setting the color to vec4(-1). */ if (!force_color && gi->attr.is_color) c = nc; else c = cc; cx = (float)(x_position + gi->geometry.x_offset) / PANGO_SCALE; lookup.xshift = compute_phase_and_pos (x + cx, &cx); if G_UNLIKELY (gi->geometry.y_offset != 0) { cy = (float)(gi->geometry.y_offset) / PANGO_SCALE; lookup.yshift = compute_phase_and_pos (y + cy, &cy); } else { lookup.yshift = yshift; cy = ypos; } x_position += gi->geometry.width; texture_id = gsk_gl_glyph_library_lookup_or_add (library, &lookup, &glyph); if G_UNLIKELY (texture_id == 0) continue; if G_UNLIKELY (last_texture != texture_id || batch->draw.vbo_count + GSK_GL_N_VERTICES > 0xffff) { 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_gl_render_job_split_draw (job); batch = gsk_gl_command_queue_get_batch (job->command_queue); batch->draw.vbo_offset = vbo_offset; } gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, texture_id); job->source_is_glyph_atlas = TRUE; 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 = cx + glyph->ink_rect.x; glyph_y = cy + glyph->ink_rect.y; glyph_x2 = glyph_x + glyph->ink_rect.width; glyph_y2 = glyph_y + glyph->ink_rect.height; *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x, glyph_y }, .uv = { tx, ty }, .color = { c[0], c[1], c[2], c[3] } }; *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x, glyph_y2 }, .uv = { tx, ty2 }, .color = { c[0], c[1], c[2], c[3] } }; *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x2, glyph_y }, .uv = { tx2, ty }, .color = { c[0], c[1], c[2], c[3] } }; *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x2, glyph_y2 }, .uv = { tx2, ty2 }, .color = { c[0], c[1], c[2], c[3] } }; *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x, glyph_y2 }, .uv = { tx, ty2 }, .color = { c[0], c[1], c[2], c[3] } }; *(vertices++) = (GskGLDrawVertex) { .position = { glyph_x2, glyph_y }, .uv = { tx2, ty }, .color = { c[0], c[1], c[2], c[3] } }; batch->draw.vbo_count += GSK_GL_N_VERTICES; used++; } if (used != num_glyphs) gsk_gl_command_queue_retract_n_vertices (job->command_queue, num_glyphs - used); gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_shadow_node (GskGLRenderJob *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_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; GskGLRenderOffscreen offscreen = {0}; graphene_rect_t bounds; guint16 color[4]; if (RGBA_IS_CLEAR (&shadow->color)) continue; if (node_is_invisible (shadow_child)) continue; if (shadow->radius == 0 && GSK_RENDER_NODE_TYPE (shadow_child) == GSK_TEXT_NODE) { if (dx != 0 || dy != 0) { gsk_gl_render_job_offset (job, dx, dy); gsk_gl_render_job_visit_text_node (job, shadow_child, &shadow->color, TRUE); gsk_gl_render_job_offset (job, -dx, -dy); } 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 { offscreen.bounds = &shadow_child->bounds; offscreen.reset_clip = TRUE; offscreen.do_not_cache = TRUE; if (!gsk_gl_render_job_visit_node_with_offscreen (job, shadow_child, &offscreen)) g_assert_not_reached (); bounds = shadow_child->bounds; } gsk_gl_render_job_offset (job, dx, dy); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, coloring))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, offscreen.texture_id); job->source_is_glyph_atlas = FALSE; rgba_to_half (&shadow->color, color); gsk_gl_render_job_draw_offscreen_with_color (job, &bounds, &offscreen, color); gsk_gl_render_job_end_draw (job); } gsk_gl_render_job_offset (job, -dx, -dy); } /* Now draw the child normally */ gsk_gl_render_job_visit_node (job, original_child); } static inline void gsk_gl_render_job_visit_blur_node (GskGLRenderJob *job, const GskRenderNode *node) { const GskRenderNode *child = gsk_blur_node_get_child (node); float blur_radius = gsk_blur_node_get_radius (node); GskGLRenderOffscreen 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; offscreen.texture_id = gsk_gl_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_gl_driver_cache_texture (job->driver, &key, offscreen.texture_id); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, offscreen.texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_coords (job, min_x, min_y, max_x, max_y, 0, 1, 1, 0, (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO } ); gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_blend_node (GskGLRenderJob *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); GskGLRenderOffscreen top_offscreen = {0}; GskGLRenderOffscreen 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; gsk_gl_render_job_set_modelview (job, gsk_transform_scale (NULL, fabs (job->scale_x), fabs (job->scale_y))); /* 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_gl_render_job_visit_node_with_offscreen (job, bottom_child, &bottom_offscreen)) { gsk_gl_render_job_pop_modelview (job); gsk_gl_render_job_visit_node (job, top_child); return; } g_assert (bottom_offscreen.was_offscreen); if (!gsk_gl_render_job_visit_node_with_offscreen (job, top_child, &top_offscreen)) { gsk_gl_render_job_pop_modelview (job); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, bottom_offscreen.texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_offscreen (job, &node->bounds, &bottom_offscreen); gsk_gl_render_job_end_draw (job); } return; } g_assert (top_offscreen.was_offscreen); gsk_gl_render_job_pop_modelview (job); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blend))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, bottom_offscreen.texture_id); gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_BLEND_SOURCE2, 0, GL_TEXTURE_2D, GL_TEXTURE1, top_offscreen.texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_program_set_uniform1i (job->current_program, UNIFORM_BLEND_MODE, 0, gsk_blend_node_get_blend_mode (node)); gsk_gl_render_job_draw_offscreen_rect (job, &node->bounds); gsk_gl_render_job_end_draw (job); } } static gboolean gsk_gl_render_job_texture_mask_for_color (GskGLRenderJob *job, const GskRenderNode *mask, const GskRenderNode *color, const graphene_rect_t *bounds) { int max_texture_size = job->command_queue->max_texture_size; GdkTexture *texture = gsk_texture_node_get_texture (mask); const GdkRGBA *rgba; rgba = gsk_color_node_get_color (color); if (RGBA_IS_CLEAR (rgba)) return TRUE; if G_LIKELY (texture->width <= max_texture_size && texture->height <= max_texture_size && gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, coloring))) { GskGLRenderOffscreen offscreen = {0}; float scale_x = mask->bounds.size.width / texture->width; float scale_y = mask->bounds.size.height / texture->height; gboolean use_mipmap; guint16 cc[4]; use_mipmap = (scale_x * fabs (job->scale_x)) < 0.5 || (scale_y * fabs (job->scale_y)) < 0.5; rgba_to_half (rgba, cc); gsk_gl_render_job_upload_texture (job, texture, use_mipmap, &offscreen); gsk_gl_program_set_uniform_texture_with_sync (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, offscreen.texture_id, offscreen.has_mipmap ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR, GL_LINEAR, offscreen.sync); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_offscreen_with_color (job, bounds, &offscreen, cc); gsk_gl_render_job_end_draw (job); return TRUE; } return FALSE; } static inline void gsk_gl_render_job_visit_mask_node (GskGLRenderJob *job, const GskRenderNode *node) { const GskRenderNode *source = gsk_mask_node_get_source (node); const GskRenderNode *mask = gsk_mask_node_get_mask (node); GskGLRenderOffscreen source_offscreen = {0}; GskGLRenderOffscreen mask_offscreen = {0}; /* If the mask is a texture and the source is a color node * then we can take a shortcut and avoid offscreens. */ if (GSK_RENDER_NODE_TYPE (mask) == GSK_TEXTURE_NODE && GSK_RENDER_NODE_TYPE (source) == GSK_COLOR_NODE && gsk_mask_node_get_mask_mode (node) == GSK_MASK_MODE_ALPHA) { if (gsk_gl_render_job_texture_mask_for_color (job, mask, source, &node->bounds)) return; } source_offscreen.bounds = &node->bounds; source_offscreen.force_offscreen = TRUE; source_offscreen.reset_clip = TRUE; mask_offscreen.bounds = &node->bounds; mask_offscreen.force_offscreen = TRUE; mask_offscreen.reset_clip = TRUE; mask_offscreen.do_not_cache = TRUE; gsk_gl_render_job_set_modelview (job, gsk_transform_scale (NULL, fabs (job->scale_x), fabs (job->scale_y))); /* TODO: We create 2 textures here as big as the mask node, but both * nodes might be a lot smaller than that. */ if (!gsk_gl_render_job_visit_node_with_offscreen (job, source, &source_offscreen)) { gsk_gl_render_job_pop_modelview (job); gsk_gl_render_job_visit_node (job, source); return; } g_assert (source_offscreen.was_offscreen); if (!gsk_gl_render_job_visit_node_with_offscreen (job, mask, &mask_offscreen)) { gsk_gl_render_job_pop_modelview (job); return; } g_assert (mask_offscreen.was_offscreen); gsk_gl_render_job_pop_modelview (job); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, mask))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, source_offscreen.texture_id); gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_MASK_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE1, mask_offscreen.texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_program_set_uniform1i (job->current_program, UNIFORM_MASK_MODE, 0, gsk_mask_node_get_mask_mode (node)); gsk_gl_render_job_draw_offscreen_rect (job, &node->bounds); gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_color_matrix_node (GskGLRenderJob *job, const GskRenderNode *node) { const GskRenderNode *child = gsk_color_matrix_node_get_child (node); GskGLRenderOffscreen offscreen = {0}; float offset[4]; if (node_is_invisible (child)) return; offscreen.bounds = &node->bounds; offscreen.reset_clip = TRUE; if (!gsk_gl_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); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color_matrix))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, offscreen.texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_program_set_uniform_matrix (job->current_program, UNIFORM_COLOR_MATRIX_COLOR_MATRIX, 0, gsk_color_matrix_node_get_color_matrix (node)); gsk_gl_program_set_uniform4fv (job->current_program, UNIFORM_COLOR_MATRIX_COLOR_OFFSET, 0, 1, offset); gsk_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen); gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_gl_shader_node_fallback (GskGLRenderJob *job, const GskRenderNode *node) { guint16 pink[4] = { 15360, 13975, 14758, 15360 }; /* 255 105 180 */ if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color))) { gsk_gl_render_job_draw_rect_with_color (job, &node->bounds, pink); gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_gl_shader_node (GskGLRenderJob *job, const GskRenderNode *node) { GError *error = NULL; GskGLShader *shader; GskGLProgram *program; int n_children; shader = gsk_gl_shader_node_get_shader (node); program = gsk_gl_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_gl_render_job_visit_gl_shader_node_fallback (job, node); g_clear_error (&error); } else { GskGLRenderOffscreen 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_gl_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); if (gsk_gl_render_job_begin_draw (job, program)) { for (guint i = 0; i < n_children; i++) gsk_gl_program_set_uniform_texture (program, UNIFORM_CUSTOM_TEXTURE1 + i, 0, GL_TEXTURE_2D, GL_TEXTURE0 + i, offscreens[i].texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_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; switch (u->type) { default: case GSK_GL_UNIFORM_TYPE_NONE: break; case GSK_GL_UNIFORM_TYPE_FLOAT: gsk_gl_uniform_state_set1fv (job->command_queue->uniforms, program->program_info, UNIFORM_CUSTOM_ARG0 + i, 0, 1, (const float *)data); break; case GSK_GL_UNIFORM_TYPE_INT: gsk_gl_uniform_state_set1i (job->command_queue->uniforms, program->program_info, UNIFORM_CUSTOM_ARG0 + i, 0, *(const gint32 *)data); break; case GSK_GL_UNIFORM_TYPE_UINT: case GSK_GL_UNIFORM_TYPE_BOOL: gsk_gl_uniform_state_set1ui (job->command_queue->uniforms, program->program_info, UNIFORM_CUSTOM_ARG0 + i, 0, *(const guint32 *)data); break; case GSK_GL_UNIFORM_TYPE_VEC2: gsk_gl_uniform_state_set2fv (job->command_queue->uniforms, program->program_info, UNIFORM_CUSTOM_ARG0 + i, 0, 1, (const float *)data); break; case GSK_GL_UNIFORM_TYPE_VEC3: gsk_gl_uniform_state_set3fv (job->command_queue->uniforms, program->program_info, UNIFORM_CUSTOM_ARG0 + i, 0, 1, (const float *)data); break; case GSK_GL_UNIFORM_TYPE_VEC4: gsk_gl_uniform_state_set4fv (job->command_queue->uniforms, program->program_info, UNIFORM_CUSTOM_ARG0 + i, 0, 1, (const float *)data); break; } } gsk_gl_render_job_draw_offscreen_rect (job, &node->bounds); gsk_gl_render_job_end_draw (job); } } } static void gsk_gl_render_job_upload_texture (GskGLRenderJob *job, GdkTexture *texture, gboolean ensure_mipmap, GskGLRenderOffscreen *offscreen) { /* Don't put GL or dmabuf textures into icon caches, they are already on the GPU side */ if (!ensure_mipmap && gsk_gl_texture_library_can_cache ((GskGLTextureLibrary *)job->driver->icons_library, texture->width, texture->height) && !(GDK_IS_GL_TEXTURE (texture) || GDK_IS_DMABUF_TEXTURE (texture))) { const GskGLIconData *icon_data; gsk_gl_icon_library_lookup_or_add (job->driver->icons_library, texture, &icon_data); offscreen->texture_id = GSK_GL_TEXTURE_ATLAS_ENTRY_TEXTURE (icon_data); memcpy (&offscreen->area, &icon_data->entry.area, sizeof offscreen->area); offscreen->has_mipmap = FALSE; } else { /* Only generate a mipmap if it does not make use reupload * a GL texture which we could otherwise use directly. */ if (GDK_IS_GL_TEXTURE (texture) && gdk_gl_context_is_shared (gdk_gl_texture_get_context (GDK_GL_TEXTURE (texture)), job->command_queue->context)) ensure_mipmap = gdk_gl_texture_has_mipmap (GDK_GL_TEXTURE (texture)); else if (GDK_IS_DMABUF_TEXTURE (texture)) ensure_mipmap = FALSE; offscreen->texture_id = gsk_gl_driver_load_texture (job->driver, texture, ensure_mipmap); init_full_texture_region (offscreen); offscreen->has_mipmap = ensure_mipmap; if (GDK_IS_GL_TEXTURE (texture) && offscreen->texture_id == gdk_gl_texture_get_id (GDK_GL_TEXTURE (texture))) offscreen->sync = gdk_gl_texture_get_sync (GDK_GL_TEXTURE (texture)); } } static inline void gsk_gl_render_job_visit_texture (GskGLRenderJob *job, GdkTexture *texture, const graphene_rect_t *bounds) { int max_texture_size = job->command_queue->max_texture_size; float scale_x = bounds->size.width / texture->width; float scale_y = bounds->size.height / texture->height; gboolean use_mipmap; use_mipmap = (scale_x * fabs (job->scale_x)) < 0.5 || (scale_y * fabs (job->scale_y)) < 0.5; if G_LIKELY (texture->width <= max_texture_size && texture->height <= max_texture_size) { GskGLRenderOffscreen offscreen = {0}; gsk_gl_render_job_upload_texture (job, texture, use_mipmap, &offscreen); g_assert (offscreen.texture_id); g_assert (offscreen.was_offscreen == FALSE); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) { gsk_gl_program_set_uniform_texture_with_sync (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, offscreen.texture_id, offscreen.has_mipmap ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR, GL_LINEAR, offscreen.sync); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_offscreen (job, bounds, &offscreen); gsk_gl_render_job_end_draw (job); } } else { float min_x = job->offset_x + bounds->origin.x; float min_y = job->offset_y + bounds->origin.y; GskGLTextureSlice *slices = NULL; guint n_slices = 0; gsk_gl_driver_slice_texture (job->driver, texture, use_mipmap, &slices, &n_slices); g_assert (slices != NULL); g_assert (n_slices > 0); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) { for (unsigned int i = 0; i < n_slices; i++) { const GskGLTextureSlice *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_gl_render_job_split_draw (job); gsk_gl_program_set_uniform_texture_with_filter (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, slice->texture_id, use_mipmap ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR, GL_LINEAR); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_coords (job, x1, y1, x2, y2, slice->area.x, slice->area.y, slice->area.x2, slice->area.y2, (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO }); } gsk_gl_render_job_end_draw (job); } } } static inline void gsk_gl_render_job_visit_texture_node (GskGLRenderJob *job, const GskRenderNode *node) { GdkTexture *texture = gsk_texture_node_get_texture (node); const graphene_rect_t *bounds = &node->bounds; gsk_gl_render_job_visit_texture (job, texture, bounds); } static inline void gsk_gl_render_job_visit_texture_scale_node (GskGLRenderJob *job, const GskRenderNode *node) { GdkTexture *texture = gsk_texture_scale_node_get_texture (node); const graphene_rect_t *bounds = &node->bounds; GskScalingFilter filter = gsk_texture_scale_node_get_filter (node); int min_filters[] = { GL_LINEAR, GL_NEAREST, GL_LINEAR_MIPMAP_LINEAR }; int mag_filters[] = { GL_LINEAR, GL_NEAREST, GL_LINEAR }; int min_filter = min_filters[filter]; int mag_filter = mag_filters[filter]; int max_texture_size = job->command_queue->max_texture_size; graphene_rect_t clip_rect; GskGLRenderTarget *render_target; graphene_rect_t viewport; graphene_rect_t prev_viewport; graphene_matrix_t prev_projection; float prev_alpha; guint prev_fbo; float u0, u1, v0, v1; GskTextureKey key; guint texture_id; gsk_gl_render_job_untransform_bounds (job, &job->current_clip->rect.bounds, &clip_rect); if (!gsk_rect_intersection (bounds, &clip_rect, &clip_rect)) return; key.pointer = node; key.pointer_is_child = TRUE; key.parent_rect = clip_rect; key.scale_x = 1.; key.scale_y = 1.; texture_id = gsk_gl_driver_lookup_texture (job->driver, &key); if (texture_id != 0) goto render_texture; viewport = GRAPHENE_RECT_INIT (0, 0, clip_rect.size.width, clip_rect.size.height); if (!gsk_gl_driver_create_render_target (job->driver, (int) ceilf (clip_rect.size.width), (int) ceilf (clip_rect.size.height), get_target_format (job, node), &render_target)) { gsk_gl_render_job_visit_texture (job, texture, bounds); return; } gsk_gl_render_job_set_viewport (job, &viewport, &prev_viewport); gsk_gl_render_job_set_projection_from_rect (job, &viewport, &prev_projection); gsk_gl_render_job_set_modelview (job, NULL); prev_alpha = gsk_gl_render_job_set_alpha (job, 1.0f); gsk_gl_render_job_push_clip (job, &GSK_ROUNDED_RECT_INIT_FROM_RECT (viewport)); prev_fbo = gsk_gl_command_queue_bind_framebuffer (job->command_queue, render_target->framebuffer_id); gsk_gl_command_queue_clear (job->command_queue, 0, &viewport); if G_LIKELY (texture->width <= max_texture_size && texture->height <= max_texture_size) { gpointer sync; texture_id = gsk_gl_driver_load_texture (job->driver, texture, filter == GSK_SCALING_FILTER_TRILINEAR); if (GDK_IS_GL_TEXTURE (texture) && texture_id == gdk_gl_texture_get_id (GDK_GL_TEXTURE (texture))) sync = gdk_gl_texture_get_sync (GDK_GL_TEXTURE (texture)); else sync = NULL; u0 = (clip_rect.origin.x - bounds->origin.x) / bounds->size.width; v0 = (clip_rect.origin.y - bounds->origin.y) / bounds->size.height; u1 = (clip_rect.origin.x + clip_rect.size.width - bounds->origin.x) / bounds->size.width; v1 = (clip_rect.origin.y + clip_rect.size.height - bounds->origin.y) / bounds->size.height; if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) { gsk_gl_program_set_uniform_texture_with_sync (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, texture_id, min_filter, mag_filter, sync); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_coords (job, 0, 0, clip_rect.size.width, clip_rect.size.height, u0, v0, u1, v1, (guint16[]) { FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO }); gsk_gl_render_job_end_draw (job); } } else { float scale_x = bounds->size.width / texture->width; float scale_y = bounds->size.height / texture->height; GskGLTextureSlice *slices = NULL; guint n_slices = 0; gsk_gl_driver_slice_texture (job->driver, texture, filter == GSK_SCALING_FILTER_TRILINEAR, &slices, &n_slices); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) { for (guint i = 0; i < n_slices; i++) { const GskGLTextureSlice *slice = &slices[i]; graphene_rect_t slice_bounds; slice_bounds.origin.x = bounds->origin.x - clip_rect.origin.x + slice->rect.x * scale_x; slice_bounds.origin.y = bounds->origin.y - clip_rect.origin.y + slice->rect.y * scale_y; slice_bounds.size.width = slice->rect.width * scale_x; slice_bounds.size.height = slice->rect.height * scale_y; if (!gsk_rect_intersects (&slice_bounds, &viewport)) continue; if (i > 0) gsk_gl_render_job_split_draw (job); gsk_gl_program_set_uniform_texture_with_filter (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, slice->texture_id, min_filter, mag_filter); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_coords (job, slice_bounds.origin.x, slice_bounds.origin.y, slice_bounds.origin.x + slice_bounds.size.width, slice_bounds.origin.y + slice_bounds.size.height, slice->area.x, slice->area.y, slice->area.x2, slice->area.y2, (guint16[]){ FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO } ); } gsk_gl_render_job_end_draw (job); } } gsk_gl_render_job_pop_clip (job); gsk_gl_render_job_pop_modelview (job); gsk_gl_render_job_set_viewport (job, &prev_viewport, NULL); gsk_gl_render_job_set_projection (job, &prev_projection); gsk_gl_render_job_set_alpha (job, prev_alpha); gsk_gl_command_queue_bind_framebuffer (job->command_queue, prev_fbo); texture_id = gsk_gl_driver_release_render_target (job->driver, render_target, FALSE); gsk_gl_driver_cache_texture (job->driver, &key, texture_id); render_texture: if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_coords (job, job->offset_x + clip_rect.origin.x, job->offset_y + clip_rect.origin.y, job->offset_x + clip_rect.origin.x + clip_rect.size.width, job->offset_y + clip_rect.origin.y + clip_rect.size.height, 0, clip_rect.size.width / ceilf (clip_rect.size.width), clip_rect.size.height / ceilf (clip_rect.size.height), 0, (guint16[]){ FP16_ZERO, FP16_ZERO, FP16_ZERO, FP16_ZERO } ); gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_repeat_node (GskGLRenderJob *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); GskGLRenderOffscreen offscreen = {0}; if (node_is_invisible (child)) return; if (!gsk_rect_equal (child_bounds, &child->bounds)) { /* TODO: implement these repeat nodes. */ gsk_gl_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 (gsk_rect_contains_rect (child_bounds, &node->bounds)) { gsk_gl_render_job_visit_clipped_child (job, child, &node->bounds); return; } offscreen.bounds = &child->bounds; offscreen.reset_clip = TRUE; if (!gsk_gl_render_job_visit_node_with_offscreen (job, child, &offscreen)) g_assert_not_reached (); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, repeat))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, offscreen.texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_program_set_uniform4f (job->current_program, 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_gl_program_set_uniform4f (job->current_program, 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_gl_render_job_draw_offscreen (job, &node->bounds, &offscreen); gsk_gl_render_job_end_draw (job); } } static inline void gsk_gl_render_job_visit_subsurface_node (GskGLRenderJob *job, const GskRenderNode *node) { GdkSubsurface *subsurface; subsurface = (GdkSubsurface *) gsk_subsurface_node_get_subsurface (node); if (subsurface && gdk_subsurface_get_texture (subsurface) && gdk_subsurface_get_parent (subsurface) == gdk_gl_context_get_surface (job->command_queue->context)) { if (!gdk_subsurface_is_above_parent (subsurface)) { /* Clear the area so we can see through */ if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, color))) { GskGLCommandBatch *batch; guint16 color[4]; rgba_to_half (&(GdkRGBA){0,0,0,0}, color); batch = gsk_gl_command_queue_get_batch (job->command_queue); batch->draw.blend = 0; gsk_gl_render_job_draw_rect_with_color (job, &node->bounds, color); gsk_gl_render_job_end_draw (job); } } } else { gsk_gl_render_job_visit_node (job, gsk_subsurface_node_get_child (node)); } } static void gsk_gl_render_job_visit_node (GskGLRenderJob *job, const GskRenderNode *node) { gboolean has_clip; g_assert (job != NULL); g_assert (node != NULL); g_assert (GSK_IS_GL_DRIVER (job->driver)); g_assert (GSK_IS_GL_COMMAND_QUEUE (job->command_queue)); if (node_is_invisible (node)) return; if (!gsk_gl_render_job_update_clip (job, &node->bounds, &has_clip)) return; switch (GSK_RENDER_NODE_TYPE (node)) { case GSK_BLEND_NODE: gsk_gl_render_job_visit_blend_node (job, node); break; case GSK_BLUR_NODE: if (gsk_blur_node_get_radius (node) > 0) gsk_gl_render_job_visit_blur_node (job, node); else gsk_gl_render_job_visit_node (job, gsk_blur_node_get_child (node)); break; case GSK_BORDER_NODE: if (gsk_border_node_get_uniform_color (node) && gsk_rounded_rect_is_rectilinear (gsk_border_node_get_outline (node))) gsk_gl_render_job_visit_rect_border_node (job, node); else gsk_gl_render_job_visit_border_node (job, node); break; case GSK_CLIP_NODE: gsk_gl_render_job_visit_clip_node (job, node); break; case GSK_COLOR_NODE: gsk_gl_render_job_visit_color_node (job, node); break; case GSK_COLOR_MATRIX_NODE: gsk_gl_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_gl_render_job_visit_conic_gradient_node (job, node); else gsk_gl_render_job_visit_as_fallback (job, node); break; case GSK_CONTAINER_NODE: { GskRenderNode **children; guint n_children; children = gsk_container_node_get_children (node, &n_children); for (guint i = 0; i < n_children; i++) { const GskRenderNode *child = children[i]; if (i + 1 < n_children && job->current_clip->is_fully_contained && GSK_RENDER_NODE_TYPE (child) == GSK_ROUNDED_CLIP_NODE) { const GskRenderNode *grandchild = gsk_rounded_clip_node_get_child (child); const GskRenderNode *child2 = children[i + 1]; if (GSK_RENDER_NODE_TYPE (grandchild) == GSK_COLOR_NODE && GSK_RENDER_NODE_TYPE (child2) == GSK_BORDER_NODE && gsk_border_node_get_uniform_color (child2) && rounded_rect_equal (gsk_rounded_clip_node_get_clip (child), gsk_border_node_get_outline (child2))) { gsk_gl_render_job_visit_css_background (job, child, child2); i++; /* skip the border node */ continue; } } gsk_gl_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_gl_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_gl_render_job_visit_node (job, gsk_cross_fade_node_get_end_child (node)); else gsk_gl_render_job_visit_cross_fade_node (job, node); } break; case GSK_DEBUG_NODE: /* Debug nodes are ignored because draws get reordered anyway */ gsk_gl_render_job_visit_node (job, gsk_debug_node_get_child (node)); break; case GSK_GL_SHADER_NODE: gsk_gl_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_gl_render_job_visit_blurred_inset_shadow_node (job, node); else gsk_gl_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_gl_render_job_visit_linear_gradient_node (job, node); else gsk_gl_render_job_visit_as_fallback (job, node); break; case GSK_MASK_NODE: gsk_gl_render_job_visit_mask_node (job, node); break; case GSK_OPACITY_NODE: gsk_gl_render_job_visit_opacity_node (job, node); break; case GSK_OUTSET_SHADOW_NODE: if (gsk_outset_shadow_node_get_blur_radius (node) > 0) gsk_gl_render_job_visit_blurred_outset_shadow_node (job, node); else gsk_gl_render_job_visit_unblurred_outset_shadow_node (job, node); break; case GSK_RADIAL_GRADIENT_NODE: case GSK_REPEATING_RADIAL_GRADIENT_NODE: if (gsk_radial_gradient_node_get_n_color_stops (node) < MAX_GRADIENT_STOPS) gsk_gl_render_job_visit_radial_gradient_node (job, node); else gsk_gl_render_job_visit_as_fallback (job, node); break; case GSK_REPEAT_NODE: gsk_gl_render_job_visit_repeat_node (job, node); break; case GSK_ROUNDED_CLIP_NODE: gsk_gl_render_job_visit_rounded_clip_node (job, node); break; case GSK_SHADOW_NODE: gsk_gl_render_job_visit_shadow_node (job, node); break; case GSK_TEXT_NODE: gsk_gl_render_job_visit_text_node (job, node, gsk_text_node_get_color (node), FALSE); break; case GSK_TEXTURE_NODE: gsk_gl_render_job_visit_texture_node (job, node); break; case GSK_TEXTURE_SCALE_NODE: gsk_gl_render_job_visit_texture_scale_node (job, node); break; case GSK_TRANSFORM_NODE: gsk_gl_render_job_visit_transform_node (job, node); break; case GSK_CAIRO_NODE: gsk_gl_render_job_visit_as_fallback (job, node); break; case GSK_FILL_NODE: gsk_gl_render_job_visit_as_fallback (job, node); break; case GSK_STROKE_NODE: gsk_gl_render_job_visit_as_fallback (job, node); break; case GSK_SUBSURFACE_NODE: gsk_gl_render_job_visit_subsurface_node (job, node); break; case GSK_NOT_A_RENDER_NODE: default: g_assert_not_reached (); break; } if (has_clip) gsk_gl_render_job_pop_clip (job); } static gboolean gsk_gl_render_job_visit_node_with_offscreen (GskGLRenderJob *job, const GskRenderNode *node, GskGLRenderOffscreen *offscreen) { GskTextureKey key; guint cached_id; 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_TYPE (node) == GSK_TEXTURE_NODE && !offscreen->force_offscreen) { GdkTexture *texture = gsk_texture_node_get_texture (node); gsk_gl_render_job_upload_texture (job, texture, FALSE, offscreen); return TRUE; } 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; float offset_x = job->offset_x; float offset_y = job->offset_y; gboolean flipped_x = job->scale_x < 0; gboolean flipped_y = job->scale_y < 0; graphene_rect_t viewport; gboolean reset_clip = FALSE; if (flipped_x || flipped_y) { GskTransform *transform = gsk_transform_scale (NULL, flipped_x ? -1 : 1, flipped_y ? -1 : 1); gsk_gl_render_job_push_modelview (job, transform); gsk_transform_unref (transform); } gsk_gl_render_job_transform_bounds (job, offscreen->bounds, &viewport); float aligned_x = floorf (viewport.origin.x); float padding_left = viewport.origin.x - aligned_x; float aligned_width = ceilf (viewport.size.width + padding_left); float padding_right = aligned_width - viewport.size.width - padding_left; float aligned_y = floorf (viewport.origin.y); float padding_top = viewport.origin.y - aligned_y; float aligned_height = ceilf (viewport.size.height + padding_top); float padding_bottom = aligned_height - viewport.size.height - padding_top; /* 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. */ g_assert (job->command_queue->max_texture_size > 0); float downscale_x = 1; float downscale_y = 1; int texture_width; int texture_height; int max_texture_size = job->command_queue->max_texture_size; if (aligned_width > max_texture_size) downscale_x = (float)max_texture_size / viewport.size.width; if (aligned_height > max_texture_size) downscale_y = (float)max_texture_size / viewport.size.height; if (downscale_x != 1 || downscale_y != 1) { GskTransform *transform = gsk_transform_scale (NULL, downscale_x, downscale_y); gsk_gl_render_job_push_modelview (job, transform); gsk_transform_unref (transform); gsk_gl_render_job_transform_bounds (job, offscreen->bounds, &viewport); graphene_rect_scale (&viewport, downscale_x, downscale_y, &viewport); } if (downscale_x == 1) { viewport.origin.x = aligned_x; viewport.size.width = aligned_width; offscreen->area.x = padding_left / aligned_width; offscreen->area.x2 = 1.0f - (padding_right / aligned_width); texture_width = aligned_width; } else { offscreen->area.x = 0; offscreen->area.x2 = 1; texture_width = max_texture_size; } if (downscale_y == 1) { viewport.origin.y = aligned_y; viewport.size.height = aligned_height; offscreen->area.y = padding_bottom / aligned_height; offscreen->area.y2 = 1.0f - padding_top / aligned_height; texture_height = aligned_height; } else { offscreen->area.y = 0; offscreen->area.y2 = 1; texture_height = max_texture_size; } /* Check if we've already cached the drawn texture. */ cached_id = gsk_gl_driver_lookup_texture (job->driver, &key); if (cached_id != 0) { if (downscale_x != 1 || downscale_y != 1) gsk_gl_render_job_pop_modelview (job); if (flipped_x || flipped_y) gsk_gl_render_job_pop_modelview (job); offscreen->texture_id = cached_id; /* We didn't render it offscreen, but hand out an offscreen texture id */ offscreen->was_offscreen = TRUE; return TRUE; } GskGLRenderTarget *render_target; graphene_matrix_t prev_projection; graphene_rect_t prev_viewport; float prev_alpha; guint prev_fbo; if (!gsk_gl_driver_create_render_target (job->driver, texture_width, texture_height, get_target_format (job, node), &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_gl_render_job_set_viewport (job, &viewport, &prev_viewport); gsk_gl_render_job_set_projection_from_rect (job, &job->viewport, &prev_projection); prev_alpha = gsk_gl_render_job_set_alpha (job, 1.0f); prev_fbo = gsk_gl_command_queue_bind_framebuffer (job->command_queue, render_target->framebuffer_id); gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport); if (offscreen->reset_clip) { gsk_gl_render_job_push_clip (job, &GSK_ROUNDED_RECT_INIT_FROM_RECT (job->viewport)); reset_clip = TRUE; } else if (flipped_x || flipped_y || downscale_x != 1 || downscale_y != 1) { GskRoundedRect new_clip; float scale_x = flipped_x ? - downscale_x : downscale_x; float scale_y = flipped_y ? - downscale_y : downscale_y; graphene_rect_scale (&job->current_clip->rect.bounds, scale_x, scale_y, &new_clip.bounds); rounded_rect_scale_corners (&job->current_clip->rect, &new_clip, scale_x, scale_y); gsk_gl_render_job_push_clip (job, &new_clip); reset_clip = TRUE; } gsk_gl_render_job_visit_node (job, node); if (reset_clip) gsk_gl_render_job_pop_clip (job); if (downscale_x != 1 || downscale_y != 1) gsk_gl_render_job_pop_modelview (job); if (flipped_x || flipped_y) gsk_gl_render_job_pop_modelview (job); gsk_gl_render_job_set_viewport (job, &prev_viewport, NULL); gsk_gl_render_job_set_projection (job, &prev_projection); gsk_gl_render_job_set_alpha (job, prev_alpha); gsk_gl_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_gl_driver_release_render_target (job->driver, render_target, FALSE); if (!offscreen->do_not_cache) gsk_gl_driver_cache_texture (job->driver, &key, offscreen->texture_id); return TRUE; } void gsk_gl_render_job_render_flipped (GskGLRenderJob *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_GL_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_gl_command_queue_create_render_target (job->command_queue, MAX (1, job->viewport.size.width), MAX (1, job->viewport.size.height), job->target_format, &framebuffer_id, &texture_id)) return; /* Setup drawing to our offscreen texture/framebuffer which is flipped */ gsk_gl_command_queue_bind_framebuffer (job->command_queue, framebuffer_id); gsk_gl_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_gl_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_gl_render_job_set_alpha (job, 1.0f); gsk_gl_command_queue_bind_framebuffer (job->command_queue, job->framebuffer); gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport); if (gsk_gl_render_job_begin_draw (job, CHOOSE_PROGRAM (job, blit))) { gsk_gl_program_set_uniform_texture (job->current_program, UNIFORM_SHARED_SOURCE, 0, GL_TEXTURE_2D, GL_TEXTURE0, texture_id); job->source_is_glyph_atlas = FALSE; gsk_gl_render_job_draw_rect (job, &job->viewport); gsk_gl_render_job_end_draw (job); } gdk_gl_context_push_debug_group (job->command_queue->context, "Executing command queue"); gsk_gl_command_queue_execute (job->command_queue, surface_height, 1, NULL, job->default_framebuffer); gdk_gl_context_pop_debug_group (job->command_queue->context); glDeleteFramebuffers (1, &framebuffer_id); glDeleteTextures (1, &texture_id); } void gsk_gl_render_job_render (GskGLRenderJob *job, GskRenderNode *root) { G_GNUC_UNUSED gint64 start_time; float scale; guint surface_height; g_return_if_fail (job != NULL); g_return_if_fail (root != NULL); g_return_if_fail (GSK_IS_GL_DRIVER (job->driver)); scale = MAX (job->scale_x, job->scale_y); surface_height = job->viewport.size.height; gsk_gl_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_gl_command_queue_bind_framebuffer (job->command_queue, job->framebuffer); if (job->clear_framebuffer) gsk_gl_command_queue_clear (job->command_queue, 0, &job->viewport); gsk_gl_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_gl_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_gl_command_queue_make_current (job->command_queue); gdk_gl_context_push_debug_group (job->command_queue->context, "Executing command queue"); gsk_gl_command_queue_execute (job->command_queue, surface_height, scale, job->region, job->default_framebuffer); 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_gl_render_job_set_debug_fallback (GskGLRenderJob *job, gboolean debug_fallback) { g_return_if_fail (job != NULL); job->debug_fallback = !!debug_fallback; } static int get_framebuffer_format (GdkGLContext *context, guint framebuffer) { int size; if (!gdk_gl_context_check_version (context, NULL, "3.0")) return GL_RGBA8; glBindFramebuffer (GL_FRAMEBUFFER, framebuffer); glGetFramebufferAttachmentParameteriv (GL_FRAMEBUFFER, framebuffer ? GL_COLOR_ATTACHMENT0 : gdk_gl_context_get_use_es (context) ? GL_BACK : GL_BACK_LEFT, GL_FRAMEBUFFER_ATTACHMENT_RED_SIZE, &size); if (size > 16) return GL_RGBA32F; else if (size > 8) return GL_RGBA16F; else return GL_RGBA8; } GskGLRenderJob * gsk_gl_render_job_new (GskGLDriver *driver, const graphene_rect_t *viewport, float scale, const cairo_region_t *region, guint framebuffer, gboolean clear_framebuffer) { const graphene_rect_t *clip_rect = viewport; graphene_rect_t transformed_extents; GskGLRenderJob *job; GdkGLContext *context; GLint default_framebuffer = 0; g_return_val_if_fail (GSK_IS_GL_DRIVER (driver), NULL); g_return_val_if_fail (viewport != NULL, NULL); g_return_val_if_fail (scale > 0, NULL); /* Check for non-standard framebuffer binding as we might not be using * the default framebuffer on systems like macOS where we've bound an * IOSurface to a GL_TEXTURE_RECTANGLE. Otherwise, no scissor clip will * be applied in the command queue causing overdrawing. */ context = driver->command_queue->context; default_framebuffer = GDK_GL_CONTEXT_GET_CLASS (context)->get_default_framebuffer (context); if (framebuffer == 0 && default_framebuffer != 0) framebuffer = default_framebuffer; job = g_new0 (GskGLRenderJob, 1); job->driver = g_object_ref (driver); job->command_queue = job->driver->command_queue; clips_init (&job->clip); modelviews_init (&job->modelview); job->framebuffer = framebuffer; job->clear_framebuffer = !!clear_framebuffer; job->default_framebuffer = default_framebuffer; job->offset_x = 0; job->offset_y = 0; job->scale_x = scale; job->scale_y = scale; job->viewport = *viewport; job->target_format = get_framebuffer_format (job->command_queue->context, framebuffer); gsk_gl_render_job_set_alpha (job, 1.0f); gsk_gl_render_job_set_projection_from_rect (job, viewport, NULL); gsk_gl_render_job_set_modelview (job, gsk_transform_scale (NULL, scale, scale)); /* 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_gl_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_gl_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_gl_render_job_free (GskGLRenderJob *job) { job->current_modelview = NULL; job->current_clip = NULL; while (job->modelview.end > job->modelview.start) { GskGLRenderModelview *modelview = job->modelview.end-1; g_clear_pointer (&modelview->transform, gsk_transform_unref); job->modelview.end--; } g_clear_object (&job->driver); g_clear_pointer (&job->region, cairo_region_destroy); modelviews_clear (&job->modelview); clips_clear (&job->clip); g_free (job); }