This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch span-gl-clean
in repository efl.

View the commit online.

commit 942b914f1988327397dc52edb08fb0d76fa8c079
Author: [email protected] <[email protected]>
AuthorDate: Tue Apr 7 10:01:55 2026 -0600

    refactor(evas_ector_gl): consolidate span-buffer GLSL and gradient logic
    
    Eliminates ~279 lines of code duplication across the span-buffer rendering
    pipeline by extracting common patterns into reusable helpers and shared GLSL
    fragments. This improves maintainability and reduces the risk of logic
    skew between fill/stroke or solid/gradient/mask shader variants.
    
    1. GLSL shader deduplication (evas_ector_gl_span_shader.c):
       - Replaced 4 monolithic shader strings (~520 lines) with 14 shared GLSL
         fragments assembled via multi-part glShaderSource arrays
       - Introduced _glsl_precision, _glsl_uniforms_common, _glsl_uniforms_solid,
         _glsl_uniforms_gradient, _glsl_uniforms_mask, _glsl_scan_spans,
         _glsl_grad_spread, _glsl_main_solid_body, _glsl_main_gradient_body,
         etc. — each covers one logical concern
       - Four shader arrays (_solid_shader_parts, _solid_mask_shader_parts,
         _gradient_shader_parts, _gradient_mask_shader_parts) compose these
         fragments to produce the final shader programs
       - Renamed _compile_shader → _compile_shader_parts to reflect the new
         multi-part interface; _link_program now accepts arrays instead of
         single strings
       - Benefit: Common code (uniforms, scan_spans logic) is defined once;
         shader variants reuse fragments instead of duplicating full programs
    
    2. Gradient coefficient helper (evas_engine.c):
       - Extracted _compute_gradient_coeffs() to replace duplicated 41-line
         fill/stroke gradient coefficient blocks in eng_ector_end
       - Handles both linear and radial gradients, uploads ramp textures,
         and downgrades degenerate radial to solid with fallback color
       - Benefit: Coefficient computation and degenerate checks are now defined
         once; fill and stroke paths call the same function
    
    3. Row-tail memset helper (evas_ector_gl_span.c):
       - Extracted _flush_row_tail(sc, ti, y) to replace duplicated row-tail
         memset logic in _collect_spans_solid (appeared at 2 sites)
       - Memsets from span_counts[y] to max_spans+1, writing the len=0
         sentinel and clearing stale data in one cache-friendly operation
       - Benefit: Single definition avoids skew; maintainers only need to
         update one place if the memset pattern changes
    
    4. Composite method predicate (efl_canvas_vg_container.c):
       - Introduced _comp_method_needs_mask() to replace two 4-method OR chains
         checking if composite method requires a mask texture
       - Used at lines 283 and 301 to guard GL composite setup
       - Benefit: Predicate logic is defined once; adding/removing mask methods
         requires only one code change
    
    5. GL composite helper (evas_vg_private.h):
       - Introduced _maybe_set_gl_comp() inline helper to guard and call
         ector_software_surface_gl_comp_set with proper NULL checks
       - Replaces 3 identical guard+call patterns in efl_canvas_vg_container.c
         and efl_canvas_vg_object.c
       - Benefit: Common guard pattern is factored out; easier to adjust
         NULL-safety logic or composite setup in one place
    
    No functional changes. All shader variants produce identical output.
    The refactoring reduces visual regression risk by centralizing common
    logic and reducing copy-paste errors.
    
    Commits that introduced these patterns:
      - GLSL shaders: introduced as separate monolithic strings during Task 2-4
      - Gradient logic: added in Task 3 (per-pixel gradients) with fill/stroke
        duplication in eng_ector_end
      - Row-tail memset: added in Task 2 span collection, appeared twice by
        Task 4 during mask integration work
      - Composite checks: introduced during Task 4 mask integration in both
        render_pre and the draw path
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---
 src/lib/evas/canvas/efl_canvas_vg_container.c      |  35 +-
 src/lib/evas/canvas/efl_canvas_vg_object.c         |   5 +-
 src/lib/evas/canvas/evas_vg_private.h              |  19 +
 .../evas/engines/gl_generic/evas_ector_gl_span.c   |  70 ++-
 .../evas/engines/gl_generic/evas_ector_gl_span.h   |   6 +
 .../engines/gl_generic/evas_ector_gl_span_shader.c | 628 +++++++++------------
 src/modules/evas/engines/gl_generic/evas_engine.c  | 158 +++---
 7 files changed, 434 insertions(+), 487 deletions(-)

diff --git a/src/lib/evas/canvas/efl_canvas_vg_container.c b/src/lib/evas/canvas/efl_canvas_vg_container.c
index d1450ac901..2a2de947a7 100644
--- a/src/lib/evas/canvas/efl_canvas_vg_container.c
+++ b/src/lib/evas/canvas/efl_canvas_vg_container.c
@@ -5,6 +5,19 @@
 
 #define MY_CLASS EFL_CANVAS_VG_CONTAINER_CLASS
 
+/**
+ * Return EINA_TRUE if the composite method requires a mask texture
+ * (i.e., it belongs to the matte/mask family handled by the GL span path).
+ */
+static inline Eina_Bool
+_comp_method_needs_mask(Efl_Gfx_Vg_Composite_Method m)
+{
+   return (m == EFL_GFX_VG_COMPOSITE_METHOD_MATTE_ALPHA ||
+           m == EFL_GFX_VG_COMPOSITE_METHOD_MATTE_ALPHA_INVERSE ||
+           m == EFL_GFX_VG_COMPOSITE_METHOD_MASK_INTERSECT ||
+           m == EFL_GFX_VG_COMPOSITE_METHOD_MASK_SUBSTRACT);
+}
+
 static void
 _invalidate_cb(void *data EINA_UNUSED, const Efl_Event *event)
 {
@@ -267,18 +280,11 @@ _efl_canvas_vg_container_render_pre(Evas_Object_Protected_Data *vg_pd,
    /* Even when the container's flags are NONE (nothing changed), we must
     * still propagate the GL composite mask reference to the ector surface
     * every frame — eng_ector_end clears it after use. */
-   if (pd->comp_target &&
-       (pd->comp.method == EFL_GFX_VG_COMPOSITE_METHOD_MATTE_ALPHA ||
-        pd->comp.method == EFL_GFX_VG_COMPOSITE_METHOD_MATTE_ALPHA_INVERSE ||
-        pd->comp.method == EFL_GFX_VG_COMPOSITE_METHOD_MASK_INTERSECT ||
-        pd->comp.method == EFL_GFX_VG_COMPOSITE_METHOD_MASK_SUBSTRACT))
+   if (pd->comp_target && _comp_method_needs_mask(pd->comp.method))
      {
         Efl_Canvas_Vg_Container_Data *cpd =
            efl_data_scope_get(pd->comp_target, MY_CLASS);
-        if (cpd && cpd->comp.gl_surface)
-          ector_software_surface_gl_comp_set(surface,
-                                             cpd->comp.gl_surface,
-                                             (int)pd->comp.method);
+        _maybe_set_gl_comp(surface, cpd, pd->comp.method);
      }
 
    if (nd->flags == EFL_GFX_CHANGE_FLAG_NONE) return;
@@ -292,11 +298,7 @@ _efl_canvas_vg_container_render_pre(Evas_Object_Protected_Data *vg_pd,
    //Container may have composite target.
    //FIXME : _prepare_comp() should only work in cases with matte or masking.
    // This condition is valid because the masking use same type as matte.
-   if (pd->comp_target &&
-       (pd->comp.method == EFL_GFX_VG_COMPOSITE_METHOD_MATTE_ALPHA ||
-        pd->comp.method == EFL_GFX_VG_COMPOSITE_METHOD_MATTE_ALPHA_INVERSE ||
-        pd->comp.method == EFL_GFX_VG_COMPOSITE_METHOD_MASK_INTERSECT ||
-        pd->comp.method == EFL_GFX_VG_COMPOSITE_METHOD_MASK_SUBSTRACT))
+   if (pd->comp_target && _comp_method_needs_mask(pd->comp.method))
      {
         comp_method = pd->comp.method;
         comp = _prepare_comp(vg_pd, pd->comp_target,
@@ -310,10 +312,7 @@ _efl_canvas_vg_container_render_pre(Evas_Object_Protected_Data *vg_pd,
           {
              Efl_Canvas_Vg_Container_Data *cpd =
                 efl_data_scope_get(pd->comp_target, MY_CLASS);
-             if (cpd && cpd->comp.gl_surface)
-               ector_software_surface_gl_comp_set(surface,
-                                                  cpd->comp.gl_surface,
-                                                  (int)pd->comp.method);
+             _maybe_set_gl_comp(surface, cpd, pd->comp.method);
           }
      }
 
diff --git a/src/lib/evas/canvas/efl_canvas_vg_object.c b/src/lib/evas/canvas/efl_canvas_vg_object.c
index f4001fdcc8..6e4da011e5 100644
--- a/src/lib/evas/canvas/efl_canvas_vg_object.c
+++ b/src/lib/evas/canvas/efl_canvas_vg_object.c
@@ -438,10 +438,7 @@ _evas_vg_render(Evas_Object_Protected_Data *obj, Efl_Canvas_Vg_Object_Data *pd,
           {
              Efl_Canvas_Vg_Container_Data *cpd =
                 efl_data_scope_get(cd->comp_target, EFL_CANVAS_VG_CONTAINER_CLASS);
-             if (cpd && cpd->comp.gl_surface)
-               ector_software_surface_gl_comp_set(ector,
-                                                  cpd->comp.gl_surface,
-                                                  (int)cd->comp.method);
+             _maybe_set_gl_comp(ector, cpd, cd->comp.method);
           }
 
         int alpha = 255;
diff --git a/src/lib/evas/canvas/evas_vg_private.h b/src/lib/evas/canvas/evas_vg_private.h
index e68f82b155..b2f011d6b3 100644
--- a/src/lib/evas/canvas/evas_vg_private.h
+++ b/src/lib/evas/canvas/evas_vg_private.h
@@ -156,6 +156,25 @@ void *                      evas_cache_vg_surface_key_get(Efl_Canvas_Vg_Node *ro
 ECTOR_API void ector_software_surface_gl_comp_set(Ector_Surface *obj, void *gl_surface, int comp_method);
 ECTOR_API void ector_software_surface_gl_comp_get(Ector_Surface *obj, void **gl_surface_out, int *comp_method_out);
 
+/**
+ * Set the GL composite surface on @p ector if @p target_cd holds a valid
+ * gl_surface reference.  No-op when @p target_cd is NULL or has no
+ * gl_surface (software render path, or mask not yet rendered).
+ *
+ * @param ector     Shared ector surface for this VG render pass.
+ * @param target_cd Container data of the composite target node.
+ * @param method    Composite method to propagate.
+ */
+static inline void
+_maybe_set_gl_comp(Ector_Surface *ector,
+                   Efl_Canvas_Vg_Container_Data *target_cd,
+                   Efl_Gfx_Vg_Composite_Method method)
+{
+   if (target_cd && target_cd->comp.gl_surface)
+     ector_software_surface_gl_comp_set(ector, target_cd->comp.gl_surface,
+                                        (int)method);
+}
+
 void                        efl_canvas_vg_node_vg_obj_set(Efl_VG *node, Efl_VG *vg_obj, Efl_Canvas_Vg_Object_Data *vd);
 void                        efl_canvas_vg_node_change(Efl_VG *node);
 void                        efl_canvas_vg_container_vg_obj_update(Efl_VG *obj, Efl_Canvas_Vg_Node_Data *nd);
diff --git a/src/modules/evas/engines/gl_generic/evas_ector_gl_span.c b/src/modules/evas/engines/gl_generic/evas_ector_gl_span.c
index 8ee56ecee4..9be566715a 100644
--- a/src/modules/evas/engines/gl_generic/evas_ector_gl_span.c
+++ b/src/modules/evas/engines/gl_generic/evas_ector_gl_span.c
@@ -272,6 +272,10 @@ span_collector_clear(Span_Collector *sc)
    sc->color         = 0;
    sc->mask_surface  = NULL;
    sc->comp_method   = 0;
+
+   /* Reset row-tail flush state for the new frame. */
+   sc->flush_prev_y  = -1;
+   sc->flush_prev_ti = -1;
 }
 
 /* ------------------------------------------------------------------ */
@@ -628,6 +632,38 @@ _find_texture_for_x(Span_Collector *sc, int x)
    return 0;
 }
 
+/* ------------------------------------------------------------------ */
+/* Row-tail sentinel helper                                            */
+/* ------------------------------------------------------------------ */
+
+/**
+ * Zero the tail of a span texture row from the last written entry to the
+ * end of the stride.
+ *
+ * This writes the zero-length sentinel implicitly (the entry at
+ * span_counts[y] has len=0 after zeroing) AND clears any stale data from
+ * prior frames beyond the current frame's last span.
+ *
+ * The memset covers exactly (max_spans + 1 - idx) entries starting at
+ * index idx — only the unused tail, not the full row.  When idx == 0 the
+ * entire row is zeroed; when idx == max_spans nothing is done (row full,
+ * sentinel already provided by the spatial split path).
+ *
+ * @param sc  Span collector owning the texture.
+ * @param ti  Texture index within sc->textures[].
+ * @param y   Row index.
+ */
+static inline void
+_flush_row_tail(Span_Collector *sc, int ti, int y)
+{
+   Span_Texture *tex = &sc->textures[ti];
+   int           idx = tex->span_counts[y];
+
+   if (idx < sc->max_spans)
+     memset(tex->buffer + ((size_t)y * sc->stride) + ((size_t)idx * 4),
+            0, (size_t)(sc->max_spans + 1 - idx) * 4);
+}
+
 /* ------------------------------------------------------------------ */
 /* Solid span collector callback                                       */
 /* ------------------------------------------------------------------ */
@@ -662,8 +698,6 @@ _collect_spans_solid(int count, const SW_FT_Span *spans, void *user_data)
    Span_Data      *sd  = (Span_Data *)user_data;
    Span_Collector *sc  = (Span_Collector *)sd->span_collector;
    int             ti, idx, y, sx;
-   int             prev_y  = -1;
-   int             prev_ti = -1;
    Span_Texture   *tex;
    uint8_t        *entry;
 
@@ -702,17 +736,8 @@ _collect_spans_solid(int count, const SW_FT_Span *spans, void *user_data)
          * memset is nearly free.
          *
          * Only fires when y actually changes — not once per span. */
-        if (y != prev_y && prev_y >= 0 && prev_ti >= 0)
-          {
-             Span_Texture *prev_tex = &sc->textures[prev_ti];
-             int           prev_idx = prev_tex->span_counts[prev_y];
-
-             if (prev_idx < sc->max_spans)
-               memset(prev_tex->buffer +
-                      ((size_t)prev_y * sc->stride) + ((size_t)prev_idx * 4),
-                      0,
-                      (size_t)(sc->max_spans + 1 - prev_idx) * 4);
-          }
+        if (y != sc->flush_prev_y && sc->flush_prev_y >= 0 && sc->flush_prev_ti >= 0)
+          _flush_row_tail(sc, sc->flush_prev_ti, sc->flush_prev_y);
 
         ti  = (sc->texture_count == 1) ? 0 : _find_texture_for_x(sc, sx);
         tex = &sc->textures[ti];
@@ -801,28 +826,19 @@ _collect_spans_solid(int count, const SW_FT_Span *spans, void *user_data)
         if (idx > sc->actual_max_spans)
           sc->actual_max_spans = idx;
 
-        prev_y  = y;
-        prev_ti = ti;
+        sc->flush_prev_y  = y;
+        sc->flush_prev_ti = ti;
 
         spans++;
         count--;
      }
 
-   /* Memset the tail of the last row after the loop ends.
+   /* Flush the tail of the last row after the loop ends.
     * The row-change path above fires only when y changes, so the final
     * row (or the only row when the shape spans a single scanline) is
     * handled here. */
-   if (prev_y >= 0 && prev_ti >= 0)
-     {
-        Span_Texture *prev_tex = &sc->textures[prev_ti];
-        int           prev_idx = prev_tex->span_counts[prev_y];
-
-        if (prev_idx < sc->max_spans)
-          memset(prev_tex->buffer +
-                 ((size_t)prev_y * sc->stride) + ((size_t)prev_idx * 4),
-                 0,
-                 (size_t)(sc->max_spans + 1 - prev_idx) * 4);
-     }
+   if (sc->flush_prev_y >= 0 && sc->flush_prev_ti >= 0)
+     _flush_row_tail(sc, sc->flush_prev_ti, sc->flush_prev_y);
 }
 
 /* ------------------------------------------------------------------ */
diff --git a/src/modules/evas/engines/gl_generic/evas_ector_gl_span.h b/src/modules/evas/engines/gl_generic/evas_ector_gl_span.h
index e9d12ea34d..5c2e0fba2d 100644
--- a/src/modules/evas/engines/gl_generic/evas_ector_gl_span.h
+++ b/src/modules/evas/engines/gl_generic/evas_ector_gl_span.h
@@ -199,6 +199,12 @@ struct _Span_Collector
                                     * The +1 reserves a dedicated sentinel slot. */
    int            actual_max_spans; /* max span_counts[y] seen during collection this frame */
 
+   /* Row-tail flush state — tracked across multiple _collect_spans_solid
+    * invocations (e.g., when _span_fill_clipRect calls the callback in
+    * chunks).  Reset in span_collector_clear. */
+   int            flush_prev_y;    /* last row flushed (-1 = none) */
+   int            flush_prev_ti;   /* texture index of last flushed row */
+
    /* Fill parameters captured at span_collector_new() time or during collection */
    Span_Data_Type type;            /* Solid, LinearGradient, or RadialGradient */
    uint32_t       color;           /* premultiplied ARGB (0xAARRGGBB) for Solid fills */
diff --git a/src/modules/evas/engines/gl_generic/evas_ector_gl_span_shader.c b/src/modules/evas/engines/gl_generic/evas_ector_gl_span_shader.c
index 95d1e28ad8..7417ecffb1 100644
--- a/src/modules/evas/engines/gl_generic/evas_ector_gl_span_shader.c
+++ b/src/modules/evas/engines/gl_generic/evas_ector_gl_span_shader.c
@@ -37,201 +37,17 @@ static const char _span_vertex_glsl[] =
    "   gl_Position = a_position;\n"
    "}\n";
 
-/* --- Solid fragment shader ---
- *
- * Each span entry is 1 texel (4 bytes) in the span texture:
- *   byte0 (B): coverage — AA coverage 0-255
- *   byte1 (G): len      — span length (max 255; longer spans are split)
- *   byte2 (R): gap      — distance from end of previous span on this row
- *   byte3 (A): reserved — zero
- *
- * The shader iterates over up to u_max_spans span entries on the current
- * scanline (identified by gl_FragCoord.y), checks whether the current pixel
- * falls inside each span's x range, and accumulates a coverage-weighted
- * premultiplied-alpha result.  A zero-length sentinel terminates the search.
- *
- * u_inv_tw / u_inv_th are 1/pool_w and 1/pool_h (pool-space reciprocals).
- * u_offset.x / u_offset.y are the texel offsets of the span sub-region
- * within the pool.  The shader adds these to convert from logical
- * span-buffer coordinates to pool UV coordinates.
- * #define MAX_SPANS must match SPAN_COLLECTOR_DEFAULT_MAX_SPANS (64).
- */
-static const char _span_solid_fragment_glsl[] =
-   "precision highp float;\n"
-   "uniform sampler2D u_fill_spans;\n"
-   "uniform sampler2D u_stroke_spans;\n"
-   "uniform float u_inv_tw;\n"
-   "uniform float u_inv_th;\n"
-   "uniform int   u_max_spans;\n"
-   "uniform vec4  u_mul_col;\n"
-   "uniform vec2  u_fill_offset;\n"
-   "uniform vec2  u_stroke_offset;\n"
-   "uniform vec2  u_fbo_offset;\n"
-   "uniform vec4  u_fill_col;\n"
-   "uniform vec4  u_stroke_col;\n"
-   "uniform int   u_has_fill;\n"
-   "uniform int   u_has_stroke;\n"
-   "uniform int   u_fill_x_min;\n"
-   "uniform int   u_stroke_x_min;\n"
-   "#define MAX_SPANS 64\n"
-   "\n"
-   "/* Scan one span texture row, accumulating coverage-weighted base_col\n"
-   " * via premultiplied-alpha src-over into result. */\n"
-   "vec4 scan_spans(sampler2D tex, vec2 off, vec4 base_col, float px,\n"
-   "                float fy, float inv_tw, int max_s, int x_min, vec4 res) {\n"
-   "   int sx = x_min;\n"
-   "   for (int i = 0; i < MAX_SPANS; i++) {\n"
-   "      if (i >= max_s) break;\n"
-   "      float fx = (off.x + float(i) + 0.5) * inv_tw;\n"
-   "      vec4 t = texture2D(tex, vec2(fx, fy));\n"
-   "      int gap = int(t.r * 255.0 + 0.5);\n"
-   "      int len = int(t.g * 255.0 + 0.5);\n"
-   "      float cov = t.b;\n"
-   "      if (len == 0) break;\n"
-   "      sx += gap;\n"
-   "      if (int(px) >= sx && int(px) < sx + len) {\n"
-   "         vec4 col = base_col * cov;\n"
-   "         res.rgb = col.rgb + res.rgb * (1.0 - col.a);\n"
-   "         res.a   = col.a  + res.a   * (1.0 - col.a);\n"
-   "      }\n"
-   "      sx += len;\n"
-   "   }\n"
-   "   return res;\n"
-   "}\n"
-   "\n"
-   "void main() {\n"
-   "   float px = gl_FragCoord.x - u_fbo_offset.x;\n"
-   "   float py = gl_FragCoord.y - u_fbo_offset.y;\n"
-   "   vec4 result = vec4(0.0);\n"
-   "\n"
-   "   if (u_has_fill == 1) {\n"
-   "      float fy = (u_fill_offset.y + py) * u_inv_th;\n"
-   "      result = scan_spans(u_fill_spans, u_fill_offset, u_fill_col,\n"
-   "                          px, fy, u_inv_tw, u_max_spans, u_fill_x_min, result);\n"
-   "   }\n"
-   "   if (u_has_stroke == 1) {\n"
-   "      float fy = (u_stroke_offset.y + py) * u_inv_th;\n"
-   "      result = scan_spans(u_stroke_spans, u_stroke_offset, u_stroke_col,\n"
-   "                          px, fy, u_inv_tw, u_max_spans, u_stroke_x_min, result);\n"
-   "   }\n"
-   "\n"
-   "   gl_FragColor = result * u_mul_col;\n"
-   "}\n";
+/* ------------------------------------------------------------------ */
+/* Shared GLSL fragment shader source fragments                        */
+/* ------------------------------------------------------------------ */
 
-/* --- Solid mask fragment shader ---
- *
- * Identical to the solid shader but with unconditional composite mask sampling.
- * Used when span_mask_tex != 0.  No u_has_mask / u_comp_method uniforms —
- * the mask alpha is always multiplied into the result.
- */
-static const char _span_solid_mask_fragment_glsl[] =
-   "precision highp float;\n"
-   "uniform sampler2D u_fill_spans;\n"
-   "uniform sampler2D u_stroke_spans;\n"
-   "uniform float u_inv_tw;\n"
-   "uniform float u_inv_th;\n"
-   "uniform int   u_max_spans;\n"
-   "uniform vec4  u_mul_col;\n"
-   "uniform vec2  u_fill_offset;\n"
-   "uniform vec2  u_stroke_offset;\n"
-   "uniform vec2  u_fbo_offset;\n"
-   "uniform vec4  u_fill_col;\n"
-   "uniform vec4  u_stroke_col;\n"
-   "uniform int   u_has_fill;\n"
-   "uniform int   u_has_stroke;\n"
-   "uniform int   u_fill_x_min;\n"
-   "uniform int   u_stroke_x_min;\n"
-   "uniform sampler2D u_mask_tex;\n"
-   "uniform vec2  u_mask_size;\n"
-   "uniform vec2  u_mask_offset;\n"
-   "uniform float u_mask_inv;\n"
-   "uniform float u_mask_op;\n"
-   "#define MAX_SPANS 64\n"
-   "\n"
-   "vec4 scan_spans(sampler2D tex, vec2 off, vec4 base_col, float px,\n"
-   "                float fy, float inv_tw, int max_s, int x_min, vec4 res) {\n"
-   "   int sx = x_min;\n"
-   "   for (int i = 0; i < MAX_SPANS; i++) {\n"
-   "      if (i >= max_s) break;\n"
-   "      float fx = (off.x + float(i) + 0.5) * inv_tw;\n"
-   "      vec4 t = texture2D(tex, vec2(fx, fy));\n"
-   "      int gap = int(t.r * 255.0 + 0.5);\n"
-   "      int len = int(t.g * 255.0 + 0.5);\n"
-   "      float cov = t.b;\n"
-   "      if (len == 0) break;\n"
-   "      sx += gap;\n"
-   "      if (int(px) >= sx && int(px) < sx + len) {\n"
-   "         vec4 col = base_col * cov;\n"
-   "         res.rgb = col.rgb + res.rgb * (1.0 - col.a);\n"
-   "         res.a   = col.a  + res.a   * (1.0 - col.a);\n"
-   "      }\n"
-   "      sx += len;\n"
-   "   }\n"
-   "   return res;\n"
-   "}\n"
-   "\n"
-   "void main() {\n"
-   "   float px = gl_FragCoord.x - u_fbo_offset.x;\n"
-   "   float py = gl_FragCoord.y - u_fbo_offset.y;\n"
-   "   vec4 result = vec4(0.0);\n"
-   "\n"
-   "   if (u_has_fill == 1) {\n"
-   "      float fy = (u_fill_offset.y + py) * u_inv_th;\n"
-   "      result = scan_spans(u_fill_spans, u_fill_offset, u_fill_col,\n"
-   "                          px, fy, u_inv_tw, u_max_spans, u_fill_x_min, result);\n"
-   "   }\n"
-   "   if (u_has_stroke == 1) {\n"
-   "      float fy = (u_stroke_offset.y + py) * u_inv_th;\n"
-   "      result = scan_spans(u_stroke_spans, u_stroke_offset, u_stroke_col,\n"
-   "                          px, fy, u_inv_tw, u_max_spans, u_stroke_x_min, result);\n"
-   "   }\n"
-   "\n"
-   "   /* Force unconditional sampler references (driver workaround). */\n"
-   "   result += (texture2D(u_fill_spans, vec2(0.0)) +\n"
-   "             texture2D(u_stroke_spans, vec2(0.0))) * 0.0;\n"
-   "\n"
-   "   vec2 mask_uv = vec2((px + u_mask_offset.x + 0.5) / u_mask_size.x,\n"
-   "                       (py + u_mask_offset.y + 0.5) / u_mask_size.y);\n"
-   "   float mask_a = texture2D(u_mask_tex, mask_uv).a;\n"
-   "   /* u_mask_op: 0=multiply, 1=add, 2=difference\n"
-   "    * u_mask_inv: 0=normal, 1=invert (for multiply path) */\n"
-   "   if (u_mask_op < 0.5)\n"
-   "      result *= mix(mask_a, 1.0 - mask_a, u_mask_inv);\n"
-   "   else if (u_mask_op < 1.5)\n"
-   "      result = vec4(result.rgb, min(result.a + mask_a, 1.0));\n"
-   "   else\n"
-   "      result *= abs(result.a - mask_a);\n"
-   "\n"
-   "   gl_FragColor = result * u_mul_col;\n"
-   "}\n";
+/* Shared by all four shaders. */
+static const char _glsl_precision[] =
+   "precision highp float;\n";
 
-/* --- Gradient fragment shader ---
- *
- * Per-pixel gradient evaluation using a 1024×1 RGBA8 ramp texture.
- *
- * Span buffer format: identical to the solid shader — 1 texel per span
- * (gap, len, coverage).  The shader uses the same scan_spans() helper as
- * the solid shader to find which span covers the current pixel.  On hit it
- * computes the gradient parameter t per-pixel instead of using a fixed color:
- *
- *   t = u_grad_a * gl_FragCoord.x + u_grad_b * gl_FragCoord.y + u_grad_c
- *
- * The three coefficients encode the full inverse-transform + gradient
- * direction in a single dot-product, pre-computed on the CPU in eng_ector_end.
- *
- * Spread modes:
- *   u_grad_spread == 0 (PAD):     t = clamp(t, 0.0, 1.0)
- *   u_grad_spread == 1 (REFLECT): t = 1.0 - abs(fract(t*0.5)*2.0 - 1.0)
- *   u_grad_spread == 2 (REPEAT):  t = fract(t)
- *
- * The ramp texture is on unit 2 (units 0 and 1 are fill/stroke span textures).
- *
- * For fill and stroke each channel carries independent gradient parameters
- * (u_fill_grad_* vs u_stroke_grad_*) so mixed gradient+gradient shapes
- * render correctly with different gradients per channel.
- */
-static const char _span_gradient_fragment_glsl[] =
-   "precision highp float;\n"
+/* 15 base uniforms + MAX_SPANS macro shared by all four shaders.
+ * #define MAX_SPANS must match SPAN_COLLECTOR_DEFAULT_MAX_SPANS (64). */
+static const char _glsl_uniforms_common[] =
    "uniform sampler2D u_fill_spans;\n"
    "uniform sampler2D u_stroke_spans;\n"
    "uniform float u_inv_tw;\n"
@@ -245,6 +61,15 @@ static const char _span_gradient_fragment_glsl[] =
    "uniform int   u_has_stroke;\n"
    "uniform int   u_fill_x_min;\n"
    "uniform int   u_stroke_x_min;\n"
+   "#define MAX_SPANS 64\n";
+
+/* Solid-only color uniforms (absent in gradient shaders → loc returns -1). */
+static const char _glsl_uniforms_solid[] =
+   "uniform vec4  u_fill_col;\n"
+   "uniform vec4  u_stroke_col;\n";
+
+/* 18 gradient coefficient uniforms (fill + stroke). */
+static const char _glsl_uniforms_gradient[] =
    "uniform sampler2D u_fill_grad_ramp;\n"
    "uniform float u_fill_grad_a;\n"
    "uniform float u_fill_grad_b;\n"
@@ -268,10 +93,60 @@ static const char _span_gradient_fragment_glsl[] =
    "uniform float u_stroke_grad_f;\n"
    "uniform float u_stroke_grad_ra;\n"
    "uniform float u_stroke_grad_rdx;\n"
-   "uniform float u_stroke_grad_rdy;\n"
-   "#define MAX_SPANS 64\n"
+   "uniform float u_stroke_grad_rdy;\n";
+
+/* Composite mask uniforms (present only in *_mask variants). */
+static const char _glsl_uniforms_mask[] =
+   "uniform sampler2D u_mask_tex;\n"
+   "uniform vec2  u_mask_size;\n"
+   "uniform vec2  u_mask_offset;\n"
+   "uniform float u_mask_inv;\n"
+   "uniform float u_mask_op;\n";
+
+/* scan_spans() — shared by solid and solid_mask shaders.
+ *
+ * Each span entry is 1 texel (4 bytes) in the span texture:
+ *   byte0 (B): coverage — AA coverage 0-255
+ *   byte1 (G): len      — span length (max 255; longer spans are split)
+ *   byte2 (R): gap      — distance from end of previous span on this row
+ *   byte3 (A): reserved — zero
+ */
+static const char _glsl_scan_spans[] =
    "\n"
-   "/* Apply gradient spread mode to t in [−∞, +∞] → [0, 1]. */\n"
+   "/* Scan one span texture row, accumulating coverage-weighted base_col\n"
+   " * via premultiplied-alpha src-over into result. */\n"
+   "vec4 scan_spans(sampler2D tex, vec2 off, vec4 base_col, float px,\n"
+   "                float fy, float inv_tw, int max_s, int x_min, vec4 res) {\n"
+   "   int sx = x_min;\n"
+   "   for (int i = 0; i < MAX_SPANS; i++) {\n"
+   "      if (i >= max_s) break;\n"
+   "      float fx = (off.x + float(i) + 0.5) * inv_tw;\n"
+   "      vec4 t = texture2D(tex, vec2(fx, fy));\n"
+   "      int gap = int(t.r * 255.0 + 0.5);\n"
+   "      int len = int(t.g * 255.0 + 0.5);\n"
+   "      float cov = t.b;\n"
+   "      if (len == 0) break;\n"
+   "      sx += gap;\n"
+   "      if (int(px) >= sx && int(px) < sx + len) {\n"
+   "         vec4 col = base_col * cov;\n"
+   "         res.rgb = col.rgb + res.rgb * (1.0 - col.a);\n"
+   "         res.a   = col.a  + res.a   * (1.0 - col.a);\n"
+   "      }\n"
+   "      sx += len;\n"
+   "   }\n"
+   "   return res;\n"
+   "}\n";
+
+/* grad_spread() — shared by gradient and gradient_mask shaders.
+ *
+ * Spread modes:
+ *   spread == 0 (PAD):     t = clamp(t, 0.0, 1.0)
+ *   spread == 1 (REFLECT): t = 1.0 - abs(fract(t*0.5)*2.0 - 1.0)
+ *   spread == 2 (REPEAT):  t = fract(t)
+ */
+static const char _glsl_grad_spread[] =
+   "\n"
+   "/* Apply gradient spread mode to t in [-inf, +inf] -> [0, 1]. */\n"
    "float grad_spread(float t, int spread) {\n"
    "   if (spread == 1) {\n"
    "      /* REFLECT: mirror at 0 and 1 */\n"
@@ -284,7 +159,15 @@ static const char _span_gradient_fragment_glsl[] =
    "      t = clamp(t, 0.0, 1.0);\n"
    "   }\n"
    "   return t;\n"
-   "}\n"
+   "}\n";
+
+/* scan_gradient_spans() — shared by gradient and gradient_mask shaders.
+ *
+ * Span buffer format identical to the solid shader (gap, len, coverage).
+ * On hit, computes gradient parameter t per-pixel (linear or radial) and
+ * samples the gradient ramp texture.
+ */
+static const char _glsl_scan_gradient_spans[] =
    "\n"
    "/* Scan one gradient span texture row.  On hit, compute t per-pixel\n"
    " * (linear or radial) and sample the gradient ramp, then src-over\n"
@@ -332,7 +215,30 @@ static const char _span_gradient_fragment_glsl[] =
    "      sx += len;\n"
    "   }\n"
    "   return res;\n"
-   "}\n"
+   "}\n";
+
+/* main() body for solid shaders: px/py setup, fill/stroke dispatch.
+ * Everything up to but not including gl_FragColor. */
+static const char _glsl_main_solid_body[] =
+   "\n"
+   "void main() {\n"
+   "   float px = gl_FragCoord.x - u_fbo_offset.x;\n"
+   "   float py = gl_FragCoord.y - u_fbo_offset.y;\n"
+   "   vec4 result = vec4(0.0);\n"
+   "\n"
+   "   if (u_has_fill == 1) {\n"
+   "      float fy = (u_fill_offset.y + py) * u_inv_th;\n"
+   "      result = scan_spans(u_fill_spans, u_fill_offset, u_fill_col,\n"
+   "                          px, fy, u_inv_tw, u_max_spans, u_fill_x_min, result);\n"
+   "   }\n"
+   "   if (u_has_stroke == 1) {\n"
+   "      float fy = (u_stroke_offset.y + py) * u_inv_th;\n"
+   "      result = scan_spans(u_stroke_spans, u_stroke_offset, u_stroke_col,\n"
+   "                          px, fy, u_inv_tw, u_max_spans, u_stroke_x_min, result);\n"
+   "   }\n";
+
+/* main() body for gradient shaders. */
+static const char _glsl_main_gradient_body[] =
    "\n"
    "void main() {\n"
    "   float px = gl_FragCoord.x - u_fbo_offset.x;\n"
@@ -360,141 +266,25 @@ static const char _span_gradient_fragment_glsl[] =
    "                  u_stroke_grad_d, u_stroke_grad_e, u_stroke_grad_f,\n"
    "                  u_stroke_grad_ra, u_stroke_grad_rdx, u_stroke_grad_rdy,\n"
    "                  px, py, fy, u_inv_tw, u_max_spans, u_stroke_x_min, result);\n"
-   "   }\n"
-   "\n"
+   "   }\n";
+
+/* Common main() ending: multiply by color and close. */
+static const char _glsl_main_end[] =
    "   gl_FragColor = result * u_mul_col;\n"
    "}\n";
 
-/* --- Gradient mask fragment shader ---
- *
- * Identical to the gradient shader but with unconditional composite mask sampling.
- * Used when span_mask_tex != 0.  No u_has_mask / u_comp_method uniforms —
- * the mask alpha is always multiplied into the result.
- */
-static const char _span_gradient_mask_fragment_glsl[] =
-   "precision highp float;\n"
-   "uniform sampler2D u_fill_spans;\n"
-   "uniform sampler2D u_stroke_spans;\n"
-   "uniform float u_inv_tw;\n"
-   "uniform float u_inv_th;\n"
-   "uniform int   u_max_spans;\n"
-   "uniform vec4  u_mul_col;\n"
-   "uniform vec2  u_fill_offset;\n"
-   "uniform vec2  u_stroke_offset;\n"
-   "uniform vec2  u_fbo_offset;\n"
-   "uniform int   u_has_fill;\n"
-   "uniform int   u_has_stroke;\n"
-   "uniform int   u_fill_x_min;\n"
-   "uniform int   u_stroke_x_min;\n"
-   "uniform sampler2D u_mask_tex;\n"
-   "uniform vec2  u_mask_size;\n"
-   "uniform vec2  u_mask_offset;\n"
-   "uniform float u_mask_inv;\n"
-   "uniform float u_mask_op;\n"
-   "uniform sampler2D u_fill_grad_ramp;\n"
-   "uniform float u_fill_grad_a;\n"
-   "uniform float u_fill_grad_b;\n"
-   "uniform float u_fill_grad_c;\n"
-   "uniform int   u_fill_grad_spread;\n"
-   "uniform sampler2D u_stroke_grad_ramp;\n"
-   "uniform float u_stroke_grad_a;\n"
-   "uniform float u_stroke_grad_b;\n"
-   "uniform float u_stroke_grad_c;\n"
-   "uniform int   u_stroke_grad_spread;\n"
-   "uniform int   u_fill_grad_type;\n"
-   "uniform float u_fill_grad_d;\n"
-   "uniform float u_fill_grad_e;\n"
-   "uniform float u_fill_grad_f;\n"
-   "uniform float u_fill_grad_ra;\n"
-   "uniform float u_fill_grad_rdx;\n"
-   "uniform float u_fill_grad_rdy;\n"
-   "uniform int   u_stroke_grad_type;\n"
-   "uniform float u_stroke_grad_d;\n"
-   "uniform float u_stroke_grad_e;\n"
-   "uniform float u_stroke_grad_f;\n"
-   "uniform float u_stroke_grad_ra;\n"
-   "uniform float u_stroke_grad_rdx;\n"
-   "uniform float u_stroke_grad_rdy;\n"
-   "#define MAX_SPANS 64\n"
+/* Unconditional sampler-keep lines for solid mask shaders.
+ * Forces the driver to preserve fill/stroke span samplers that only
+ * appear inside conditional branches. */
+static const char _glsl_mask_keep_solid[] =
    "\n"
-   "float grad_spread(float t, int spread) {\n"
-   "   if (spread == 1) {\n"
-   "      t = 1.0 - abs(fract(t * 0.5) * 2.0 - 1.0);\n"
-   "   } else if (spread == 2) {\n"
-   "      t = fract(t);\n"
-   "   } else {\n"
-   "      t = clamp(t, 0.0, 1.0);\n"
-   "   }\n"
-   "   return t;\n"
-   "}\n"
-   "\n"
-   "vec4 scan_gradient_spans(sampler2D span_tex, vec2 off,\n"
-   "                         sampler2D ramp, float ga, float gb, float gc,\n"
-   "                         int gspread, int gtype,\n"
-   "                         float gd, float ge, float gf,\n"
-   "                         float gra, float grdx, float grdy,\n"
-   "                         float px, float py,\n"
-   "                         float fy, float inv_tw, int max_s, int x_min, vec4 res) {\n"
-   "   int sx = x_min;\n"
-   "   for (int i = 0; i < MAX_SPANS; i++) {\n"
-   "      if (i >= max_s) break;\n"
-   "      float fx = (off.x + float(i) + 0.5) * inv_tw;\n"
-   "      vec4 s = texture2D(span_tex, vec2(fx, fy));\n"
-   "      int gap = int(s.r * 255.0 + 0.5);\n"
-   "      int len = int(s.g * 255.0 + 0.5);\n"
-   "      float cov = s.b;\n"
-   "      if (len == 0) break;\n"
-   "      sx += gap;\n"
-   "      if (int(px) >= sx && int(px) < sx + len) {\n"
-   "         float t;\n"
-   "         if (gtype == 1) {\n"
-   "            float rx = ga * px + gb * py + gc;\n"
-   "            float ry = gd * px + ge * py + gf;\n"
-   "            float b_val = 2.0 * (rx * grdx + ry * grdy);\n"
-   "            float b_s = b_val * gra;\n"
-   "            float det = b_s * b_s + (rx * rx + ry * ry) * 2.0 * gra;\n"
-   "            t = sqrt(max(det, 0.0)) - b_s;\n"
-   "         } else {\n"
-   "            t = ga * px + gb * py + gc;\n"
-   "         }\n"
-   "         t = grad_spread(t, gspread);\n"
-   "         vec4 grad_col = texture2D(ramp, vec2(t, 0.5));\n"
-   "         vec4 col = grad_col * cov;\n"
-   "         res.rgb = col.rgb + res.rgb * (1.0 - col.a);\n"
-   "         res.a   = col.a  + res.a   * (1.0 - col.a);\n"
-   "      }\n"
-   "      sx += len;\n"
-   "   }\n"
-   "   return res;\n"
-   "}\n"
-   "\n"
-   "void main() {\n"
-   "   float px = gl_FragCoord.x - u_fbo_offset.x;\n"
-   "   float py = gl_FragCoord.y - u_fbo_offset.y;\n"
-   "   vec4 result = vec4(0.0);\n"
-   "\n"
-   "   if (u_has_fill == 1) {\n"
-   "      float fy = (u_fill_offset.y + py) * u_inv_th;\n"
-   "      result = scan_gradient_spans(\n"
-   "                  u_fill_spans, u_fill_offset,\n"
-   "                  u_fill_grad_ramp,\n"
-   "                  u_fill_grad_a, u_fill_grad_b, u_fill_grad_c,\n"
-   "                  u_fill_grad_spread, u_fill_grad_type,\n"
-   "                  u_fill_grad_d, u_fill_grad_e, u_fill_grad_f,\n"
-   "                  u_fill_grad_ra, u_fill_grad_rdx, u_fill_grad_rdy,\n"
-   "                  px, py, fy, u_inv_tw, u_max_spans, u_fill_x_min, result);\n"
-   "   }\n"
-   "   if (u_has_stroke == 1) {\n"
-   "      float fy = (u_stroke_offset.y + py) * u_inv_th;\n"
-   "      result = scan_gradient_spans(\n"
-   "                  u_stroke_spans, u_stroke_offset,\n"
-   "                  u_stroke_grad_ramp,\n"
-   "                  u_stroke_grad_a, u_stroke_grad_b, u_stroke_grad_c,\n"
-   "                  u_stroke_grad_spread, u_stroke_grad_type,\n"
-   "                  u_stroke_grad_d, u_stroke_grad_e, u_stroke_grad_f,\n"
-   "                  u_stroke_grad_ra, u_stroke_grad_rdx, u_stroke_grad_rdy,\n"
-   "                  px, py, fy, u_inv_tw, u_max_spans, u_stroke_x_min, result);\n"
-   "   }\n"
+   "   /* Force unconditional sampler references (driver workaround). */\n"
+   "   result += (texture2D(u_fill_spans, vec2(0.0)) +\n"
+   "             texture2D(u_stroke_spans, vec2(0.0))) * 0.0;\n";
+
+/* Unconditional sampler-keep lines for gradient mask shaders.
+ * Keeps all four samplers: fill/stroke span and fill/stroke ramp. */
+static const char _glsl_mask_keep_gradient[] =
    "\n"
    "   /* Force all sampler references unconditionally to prevent the GLSL\n"
    "    * compiler from stripping samplers that only appear inside if-branches.\n"
@@ -503,7 +293,12 @@ static const char _span_gradient_mask_fragment_glsl[] =
    "   vec4 _keep_stroke = texture2D(u_stroke_spans, vec2(0.0));\n"
    "   vec4 _keep_framp  = texture2D(u_fill_grad_ramp, vec2(0.0));\n"
    "   vec4 _keep_sramp  = texture2D(u_stroke_grad_ramp, vec2(0.0));\n"
-   "   result += (_keep_fill + _keep_stroke + _keep_framp + _keep_sramp) * 0.0;\n"
+   "   result += (_keep_fill + _keep_stroke + _keep_framp + _keep_sramp) * 0.0;\n";
+
+/* Mask epilogue: sample the composite mask texture and apply it.
+ * u_mask_op: 0=multiply, 1=add, 2=difference
+ * u_mask_inv: 0=normal, 1=invert (multiply path only) */
+static const char _glsl_mask_epilogue[] =
    "\n"
    "   vec2 mask_uv = vec2((px + u_mask_offset.x + 0.5) / u_mask_size.x,\n"
    "                       (py + u_mask_offset.y + 0.5) / u_mask_size.y);\n"
@@ -515,9 +310,103 @@ static const char _span_gradient_mask_fragment_glsl[] =
    "   else if (u_mask_op < 1.5)\n"
    "      result = vec4(result.rgb, min(result.a + mask_a, 1.0));\n"
    "   else\n"
-   "      result *= abs(result.a - mask_a);\n"
-   "   gl_FragColor = result * u_mul_col;\n"
-   "}\n";
+   "      result *= abs(result.a - mask_a);\n";
+
+/* ------------------------------------------------------------------ */
+/* Per-shader fragment arrays                                          */
+/* ------------------------------------------------------------------ */
+
+/* --- Solid fragment shader ---
+ *
+ * Each span entry is 1 texel (4 bytes) in the span texture:
+ *   byte0 (B): coverage — AA coverage 0-255
+ *   byte1 (G): len      — span length (max 255; longer spans are split)
+ *   byte2 (R): gap      — distance from end of previous span on this row
+ *   byte3 (A): reserved — zero
+ *
+ * The shader iterates over up to u_max_spans span entries on the current
+ * scanline, checks whether the current pixel falls inside each span's x
+ * range, and accumulates a coverage-weighted premultiplied-alpha result.
+ * A zero-length sentinel terminates the search.
+ *
+ * u_inv_tw / u_inv_th are 1/pool_w and 1/pool_h (pool-space reciprocals).
+ * u_fill_offset / u_stroke_offset are texel offsets within the pool.
+ * #define MAX_SPANS must match SPAN_COLLECTOR_DEFAULT_MAX_SPANS (64).
+ */
+static const char * const _solid_shader_parts[] = {
+   _glsl_precision,
+   _glsl_uniforms_common,
+   _glsl_uniforms_solid,
+   _glsl_scan_spans,
+   _glsl_main_solid_body,
+   _glsl_main_end
+};
+#define _SOLID_SHADER_PARTS \
+   ((int)(sizeof(_solid_shader_parts) / sizeof(_solid_shader_parts[0])))
+
+/* --- Solid mask fragment shader ---
+ *
+ * Identical to the solid shader but with unconditional composite mask
+ * sampling.  Used when span_mask_tex != 0.  No u_has_mask / u_comp_method
+ * uniforms — the mask alpha is always applied inside the fragment shader.
+ */
+static const char * const _solid_mask_shader_parts[] = {
+   _glsl_precision,
+   _glsl_uniforms_common,
+   _glsl_uniforms_solid,
+   _glsl_uniforms_mask,
+   _glsl_scan_spans,
+   _glsl_main_solid_body,
+   _glsl_mask_keep_solid,
+   _glsl_mask_epilogue,
+   _glsl_main_end
+};
+#define _SOLID_MASK_SHADER_PARTS \
+   ((int)(sizeof(_solid_mask_shader_parts) / sizeof(_solid_mask_shader_parts[0])))
+
+/* --- Gradient fragment shader ---
+ *
+ * Per-pixel gradient evaluation using a 1024×1 RGBA8 ramp texture.
+ *
+ * Span buffer format: identical to the solid shader — 1 texel per span
+ * (gap, len, coverage).  On hit it computes the gradient parameter t
+ * per-pixel instead of using a fixed color:
+ *   t = u_grad_a * gl_FragCoord.x + u_grad_b * gl_FragCoord.y + u_grad_c
+ *
+ * The ramp texture is on unit 2 (units 0 and 1 are fill/stroke span
+ * textures).  Fill and stroke carry independent gradient parameters.
+ */
+static const char * const _gradient_shader_parts[] = {
+   _glsl_precision,
+   _glsl_uniforms_common,
+   _glsl_uniforms_gradient,
+   _glsl_grad_spread,
+   _glsl_scan_gradient_spans,
+   _glsl_main_gradient_body,
+   _glsl_main_end
+};
+#define _GRADIENT_SHADER_PARTS \
+   ((int)(sizeof(_gradient_shader_parts) / sizeof(_gradient_shader_parts[0])))
+
+/* --- Gradient mask fragment shader ---
+ *
+ * Identical to the gradient shader but with unconditional composite mask
+ * sampling.  Used when span_mask_tex != 0.
+ */
+static const char * const _gradient_mask_shader_parts[] = {
+   _glsl_precision,
+   _glsl_uniforms_common,
+   _glsl_uniforms_gradient,
+   _glsl_uniforms_mask,
+   _glsl_grad_spread,
+   _glsl_scan_gradient_spans,
+   _glsl_main_gradient_body,
+   _glsl_mask_keep_gradient,
+   _glsl_mask_epilogue,
+   _glsl_main_end
+};
+#define _GRADIENT_MASK_SHADER_PARTS \
+   ((int)(sizeof(_gradient_mask_shader_parts) / sizeof(_gradient_mask_shader_parts[0])))
 
 /* ------------------------------------------------------------------ */
 /* Internal shader state                                               */
@@ -590,14 +479,15 @@ static GLuint _white_mask_tex = 0;
 /* ------------------------------------------------------------------ */
 
 /**
- * Compile a single shader stage.
+ * Compile a single shader stage from an array of source fragments.
  *
- * @param type  GL_VERTEX_SHADER or GL_FRAGMENT_SHADER.
- * @param src   Null-terminated GLSL source string.
- * @return      GL shader object name, or 0 on failure.
+ * @param type    GL_VERTEX_SHADER or GL_FRAGMENT_SHADER.
+ * @param parts   Array of null-terminated GLSL source strings.
+ * @param count   Number of strings in @p parts.
+ * @return        GL shader object name, or 0 on failure.
  */
 static unsigned int
-_compile_shader(unsigned int type, const char *src)
+_compile_shader_parts(unsigned int type, const char **parts, int count)
 {
    unsigned int shd;
    int          ok = 0;
@@ -605,7 +495,7 @@ _compile_shader(unsigned int type, const char *src)
    shd = glCreateShader(type);
    if (!shd) return 0;
 
-   glShaderSource(shd, 1, &src, NULL);
+   glShaderSource(shd, count, parts, NULL);
    glCompileShader(shd);
    glGetShaderiv(shd, GL_COMPILE_STATUS, &ok);
    if (!ok)
@@ -620,25 +510,29 @@ _compile_shader(unsigned int type, const char *src)
 }
 
 /**
- * Compile and link a span shader program.
+ * Compile and link a span shader program from source fragment arrays.
  *
  * Idempotent: returns EINA_TRUE immediately if the program is already
  * compiled (ss->program != 0).
  *
- * @param ss        Shader state to populate.
- * @param frag_src  Fragment shader GLSL source.
- * @return          EINA_TRUE on success, EINA_FALSE on compile/link error.
+ * @param ss          Shader state to populate.
+ * @param frag_parts  Array of fragment shader GLSL source strings.
+ * @param frag_count  Number of strings in @p frag_parts.
+ * @return            EINA_TRUE on success, EINA_FALSE on compile/link error.
  */
 static Eina_Bool
-_link_program(Span_Shader *ss, const char *frag_src)
+_link_program(Span_Shader *ss, const char **frag_parts, int frag_count)
 {
    unsigned int vs, fs;
    int          ok = 0;
 
    if (ss->program) return EINA_TRUE; /* already compiled */
 
-   vs = _compile_shader(GL_VERTEX_SHADER, _span_vertex_glsl);
-   fs = _compile_shader(GL_FRAGMENT_SHADER, frag_src);
+   {
+      const char *vert_parts[1] = { _span_vertex_glsl };
+      vs = _compile_shader_parts(GL_VERTEX_SHADER, vert_parts, 1);
+   }
+   fs = _compile_shader_parts(GL_FRAGMENT_SHADER, frag_parts, frag_count);
    if (!vs || !fs)
      {
         if (vs) glDeleteShader(vs);
@@ -765,22 +659,26 @@ span_debug_readback(const char *label, GLuint tex_id, int px_x, int px_y)
 Eina_Bool
 span_shader_init(void)
 {
-   if (!_link_program(&_solid_shader, _span_solid_fragment_glsl))
+   if (!_link_program(&_solid_shader,
+                      (const char **)_solid_shader_parts, _SOLID_SHADER_PARTS))
      {
         ERR("span solid shader link failed");
         return EINA_FALSE;
      }
-   if (!_link_program(&_gradient_shader, _span_gradient_fragment_glsl))
+   if (!_link_program(&_gradient_shader,
+                      (const char **)_gradient_shader_parts, _GRADIENT_SHADER_PARTS))
      {
         ERR("span gradient shader link failed");
         return EINA_FALSE;
      }
-   if (!_link_program(&_solid_mask_shader, _span_solid_mask_fragment_glsl))
+   if (!_link_program(&_solid_mask_shader,
+                      (const char **)_solid_mask_shader_parts, _SOLID_MASK_SHADER_PARTS))
      {
         ERR("span solid mask shader link failed");
         return EINA_FALSE;
      }
-   if (!_link_program(&_gradient_mask_shader, _span_gradient_mask_fragment_glsl))
+   if (!_link_program(&_gradient_mask_shader,
+                      (const char **)_gradient_mask_shader_parts, _GRADIENT_MASK_SHADER_PARTS))
      {
         ERR("span gradient mask shader link failed");
         return EINA_FALSE;
@@ -883,16 +781,30 @@ span_collector_upload_textures(Span_Collector *sc, void *gc_ptr)
              }
           }
 
-        /* Sentinel write is no longer needed here.
+        /* Write sentinels for rows that have spans.
          *
-         * _collect_spans_solid memsets the full tail of each row it touches
-         * (from span_counts[y] to max_spans+1) during collection, which
-         * implicitly writes the len=0 sentinel AND clears stale data from
-         * previous frames in a single L1-hot memset.
+         * The collection phase (_flush_row_tail) memsets the tail of each
+         * row it touches, but edge cases in chunked callbacks can leave
+         * rows without a clean sentinel.  This per-row single-byte write
+         * is the correctness backstop: it ensures byte[1] (len) at the
+         * span_counts[y] position is 0 for every active row.
          *
-         * For rows that receive NO spans this frame, span_collector_clear
-         * zeroes byte[1] (len) of entry 0 on every row, so the shader
-         * sees len=0 at the very first entry and terminates immediately. */
+         * For rows with NO spans, span_collector_clear already zeroed
+         * byte[1] of entry 0, so only rows with idx > 0 need attention. */
+        {
+           int y;
+           for (y = 0; y < sc->height; y++)
+             {
+                int idx = tex->span_counts[y];
+                if (idx > 0 && idx < sc->max_spans)
+                  {
+                     uint8_t *sentinel = tex->buffer +
+                                         ((size_t)y * sc->stride) +
+                                         ((size_t)idx * 4);
+                     sentinel[1] = 0;
+                  }
+             }
+        }
 
         /* First frame: create the Evas texture via the standard path.
          * Subsequent dirty frames: update in-place via glTexSubImage2D.
diff --git a/src/modules/evas/engines/gl_generic/evas_engine.c b/src/modules/evas/engines/gl_generic/evas_engine.c
index 097db361e2..dd09d12a65 100644
--- a/src/modules/evas/engines/gl_generic/evas_engine.c
+++ b/src/modules/evas/engines/gl_generic/evas_engine.c
@@ -3057,6 +3057,71 @@ _span_gradient_radial_coeffs(Ector_Renderer_Software_Gradient_Data *gd,
    *out_rdy = (float)gd->radial.dy;
 }
 
+/**
+ * Compute per-channel gradient coefficients for the span shader.
+ *
+ * Inspects sc->type and sc->gradient_data.  When sc is non-NULL and holds
+ * gradient data this function fills all out parameters and may downgrade
+ * *inout_shader_type from LinearGradient/RadialGradient to Solid when the
+ * radial geometry degenerates (fradius != 0 or a ≈ 0).  When sc is NULL
+ * or has no gradient data all out values are left at their zero defaults.
+ *
+ * @param sc               Span collector for this channel (fill or stroke).
+ * @param inout_shader_type  On entry: LinearGradient or RadialGradient.
+ *                           On exit: may be downgraded to Solid.
+ * @param inout_col        Solid color — updated when downgraded to Solid.
+ * @param out_ga..out_grdy Output gradient coefficients.
+ * @param out_gs           Gradient spread mode (EFL enum → int).
+ * @param out_gramp        Uploaded ramp texture GL name.
+ * @param out_gtype        0=linear, 1=radial.
+ */
+static void
+_compute_gradient_coeffs(Span_Collector *sc,
+                          int *inout_shader_type, uint32_t *inout_col,
+                          float *out_ga, float *out_gb, float *out_gc,
+                          int *out_gs, GLuint *out_gramp, int *out_gtype,
+                          float *out_gd, float *out_ge, float *out_gf,
+                          float *out_gra, float *out_grdx, float *out_grdy)
+{
+   Ector_Renderer_Software_Gradient_Data *gd;
+   int shader_type = *inout_shader_type;
+
+   if (!sc || !sc->gradient_data) return;
+   if (shader_type != (int)LinearGradient && shader_type != (int)RadialGradient) return;
+
+   gd = (Ector_Renderer_Software_Gradient_Data *)sc->gradient_data;
+   *out_gramp = _span_gradient_upload_ramp(sc);
+
+   if (shader_type == (int)LinearGradient)
+     {
+        _span_gradient_linear_coeffs(gd, &sc->inv,
+                                     sc->grad_offx, sc->grad_offy,
+                                     0.0f, 0.0f,
+                                     out_ga, out_gb, out_gc);
+        *out_gtype = 0;
+     }
+   else /* RadialGradient */
+     {
+        if (gd->radial.fradius >= 0.00001f || fabsf(gd->radial.a) <= 0.00001f)
+          {
+             /* Degenerate radial — fall back to solid using first stop color. */
+             *inout_shader_type = (int)Solid;
+             if (gd->color_table)
+               *inout_col = gd->color_table[0];
+             return;
+          }
+        _span_gradient_radial_coeffs(gd, &sc->inv,
+                                     sc->grad_offx, sc->grad_offy,
+                                     0.0f, 0.0f,
+                                     out_ga, out_gb, out_gc,
+                                     out_gd, out_ge, out_gf,
+                                     out_gra, out_grdx, out_grdy);
+        *out_gtype = 1;
+     }
+
+   *out_gs = (int)gd->gd->s;
+}
+
 static void
 eng_ector_end(void *engine,
               void *surface,
@@ -3192,87 +3257,20 @@ eng_ector_end(void *engine,
                      float stroke_gd = 0.0f, stroke_ge = 0.0f, stroke_gf = 0.0f;
                      float stroke_gra = 0.0f, stroke_grdx = 0.0f, stroke_grdy = 0.0f;
 
-                     if ((fill_shader_type == (int)LinearGradient ||
-                          fill_shader_type == (int)RadialGradient) &&
-                         sc_fill && sc_fill->gradient_data && _rsd)
+                     if (_rsd)
                        {
-                          Ector_Renderer_Software_Gradient_Data *gd =
-                             (Ector_Renderer_Software_Gradient_Data *)sc_fill->gradient_data;
-
-                          fill_gramp = _span_gradient_upload_ramp(sc_fill);
-
-                          if (fill_shader_type == (int)LinearGradient)
-                            {
-                                _span_gradient_linear_coeffs(gd, &sc_fill->inv,
-                                                              sc_fill->grad_offx, sc_fill->grad_offy,
-                                                              0.0f, 0.0f,
-                                                              &fill_ga, &fill_gb, &fill_gc_coef);
-                                fill_gtype = 0;
-                            }
-                          else if (fill_shader_type == (int)RadialGradient)
-                            {
-                               if (gd->radial.fradius >= 0.00001f ||
-                                   fabsf(gd->radial.a) <= 0.00001f)
-                                 {
-                                    fill_shader_type = (int)Solid;
-                                    if (gd->color_table)
-                                      fill_col = gd->color_table[0];
-                                 }
-                               else
-                                 {
-                                     _span_gradient_radial_coeffs(gd, &sc_fill->inv,
-                                                                   sc_fill->grad_offx, sc_fill->grad_offy,
-                                                                   0.0f, 0.0f,
-                                                                   &fill_ga, &fill_gb, &fill_gc_coef,
-                                                                   &fill_gd, &fill_ge, &fill_gf,
-                                                                   &fill_gra, &fill_grdx, &fill_grdy);
-                                    fill_gtype = 1;
-                                 }
-                            }
-
-                          /* Map EFL spread enum to shader int (PAD=0, REFLECT=1, REPEAT=2). */
-                          fill_gs = (int)gd->gd->s;
-                       }
-
-                     if ((stroke_shader_type == (int)LinearGradient ||
-                          stroke_shader_type == (int)RadialGradient) &&
-                         sc_stroke && sc_stroke->gradient_data && _rsd)
-                       {
-                            Ector_Renderer_Software_Gradient_Data *gd =
-                               (Ector_Renderer_Software_Gradient_Data *)sc_stroke->gradient_data;
-
-                           stroke_gramp = _span_gradient_upload_ramp(sc_stroke);
-
-                            if (stroke_shader_type == (int)LinearGradient)
-                              {
-                                  _span_gradient_linear_coeffs(gd, &sc_stroke->inv,
-                                                                sc_stroke->grad_offx, sc_stroke->grad_offy,
-                                                                0.0f, 0.0f,
-                                                                &stroke_ga, &stroke_gb, &stroke_gc_coef);
-                                 stroke_gtype = 0;
-                              }
-                           else if (stroke_shader_type == (int)RadialGradient)
-                             {
-                                if (gd->radial.fradius >= 0.00001f ||
-                                    fabsf(gd->radial.a) <= 0.00001f)
-                                  {
-                                     stroke_shader_type = (int)Solid;
-                                     if (gd->color_table)
-                                       stroke_col = gd->color_table[0];
-                                  }
-                                 else
-                                   {
-                                       _span_gradient_radial_coeffs(gd, &sc_stroke->inv,
-                                                                     sc_stroke->grad_offx, sc_stroke->grad_offy,
-                                                                     0.0f, 0.0f,
-                                                                     &stroke_ga, &stroke_gb, &stroke_gc_coef,
-                                                                     &stroke_gd, &stroke_ge, &stroke_gf,
-                                                                     &stroke_gra, &stroke_grdx, &stroke_grdy);
-                                      stroke_gtype = 1;
-                                   }
-                             }
-
-                          stroke_gs = (int)gd->gd->s;
+                          _compute_gradient_coeffs(sc_fill,
+                                                   &fill_shader_type, &fill_col,
+                                                   &fill_ga, &fill_gb, &fill_gc_coef,
+                                                   &fill_gs, &fill_gramp, &fill_gtype,
+                                                   &fill_gd, &fill_ge, &fill_gf,
+                                                   &fill_gra, &fill_grdx, &fill_grdy);
+                          _compute_gradient_coeffs(sc_stroke,
+                                                   &stroke_shader_type, &stroke_col,
+                                                   &stroke_ga, &stroke_gb, &stroke_gc_coef,
+                                                   &stroke_gs, &stroke_gramp, &stroke_gtype,
+                                                   &stroke_gd, &stroke_ge, &stroke_gf,
+                                                   &stroke_gra, &stroke_grdx, &stroke_grdy);
                        }
 
                      /* Draw each spatial-split texture within this shape.

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to