This is an automated email from the git hooks/post-receive script.
git pushed a commit to reference refs/pull/114/head
in repository efl.
View the commit online.
commit 6c20138a9154e3525ea2520616d86799937e6522
Author: Cedric BAIL <[email protected]>
AuthorDate: Thu Aug 13 22:23:30 2026 -0600
perf(evas_ector_gl): reuse span textures across height changes
Profiling the VG expedite tests on Intel UHD 620 (Kaby Lake-R, Gen9
GT2, Mesa 26.1.6/iris) showed the span path beating master on eight of
nine VG tests but losing 40% on test 125 "VG Scaled" - 38.2 FPS on
master against 23.1 FPS on the span path. VG Scaled is the one test
whose objects change size every frame.
span_collector_upload_textures() destroyed and recreated the GPU
texture whenever the active height changed:
if (existing->h != (unsigned int)sc->height)
evas_gl_common_texture_free(existing, EINA_TRUE);
Content that resizes every frame therefore never reached the in-place
glTexSubImage2D path. Every collector took evas_gl_common_texture_new()
instead, which reallocates an atlas slot and re-uploads the whole rect
plus its border rows. Measured with an LD_PRELOAD GL counter, test 125
issued 2269 glTexSubImage2D calls totalling 9.65 MB per frame.
The CPU-side buffers already follow a high-water mark (alloc_height
grows, never shrinks). Apply the same policy to the GPU texture:
allocate at alloc_height and recreate only when the active height grows
past the allocation. A shorter active height just leaves unused rows at
the bottom, which the shader never samples since py < sc->height. Those
rows are zeroed on creation so the texture never carries uninitialised
memory.
Second, the upload width was hardcoded to max_spans (64 columns) even
though the shader's scan loop is bounded by actual_max_spans - a
rounded rect needs 2 to 15. Entries past actual_max_spans are never
sampled, so uploading them is pure bandwidth waste. Upload
actual_max_spans + 1 columns; the texture stays max_spans wide so
nothing has to be reallocated when the span count fluctuates.
Test 125, best of 2 over 300-frame runs:
branch as-is 23.1 FPS 2269 uploads/frame 9.65 MB/frame
+ texture reuse 58.3 FPS
+ narrow upload 60.1 FPS 255 uploads/frame 1.58 MB/frame
That turns a 40% regression against master into a 57% gain. Test 121
(Batman) also improves, 102 to 109 FPS. All ten VG expedite tests
render byte-identical frames before and after; ector-suite and
evas-suite pass.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
.../engines/gl_generic/evas_ector_gl_span_shader.c | 53 ++++++++++++++++------
1 file changed, 40 insertions(+), 13 deletions(-)
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 05a6ec5721..fc87ce106c 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
@@ -1026,17 +1026,36 @@ span_collector_upload_textures(Span_Collector *sc, void *gc_ptr)
tex_width = sc->max_spans;
+ /* Upload only the columns the shader will actually read. The scan loop
+ * is bounded by actual_max_spans, so entries past that are never sampled
+ * and uploading the full max_spans width is pure bandwidth waste (64
+ * columns instead of the 2-15 a typical shape needs). The texture stays
+ * max_spans wide so nothing has to be reallocated when the span count
+ * fluctuates between frames. */
+ int up_width = sc->actual_max_spans + 1;
+ if (up_width > sc->max_spans) up_width = sc->max_spans;
+ if (up_width < 1) up_width = 1;
+
for (i = 0; i < sc->texture_count; i++)
{
Span_Texture *tex = &sc->textures[i];
Evas_GL_Texture *evas_t;
- /* Recreate the GPU texture if the active height changed
- * (the sub-region within the pool was allocated for a different height). */
+ /* Keep the GPU texture across height changes. It is allocated at
+ * the high-water alloc_height, so a shorter active height simply
+ * leaves unused rows at the bottom that the shader never samples
+ * (px/py are clamped to the surface, and py < sc->height). Only a
+ * genuine growth past the allocation forces a recreate.
+ *
+ * Freeing and recreating on every height change was extremely
+ * expensive: it takes the evas_gl_common_texture_new() path, which
+ * reallocates an atlas slot and re-uploads the whole rect plus its
+ * border rows. For content that resizes every frame that was
+ * ~2200 glTexSubImage2D calls and ~10 MB of upload per frame. */
if (tex->evas_tex)
{
Evas_GL_Texture *existing = (Evas_GL_Texture *)tex->evas_tex;
- if (existing->h != (unsigned int)sc->height)
+ if (existing->h < (unsigned int)sc->height)
{
evas_gl_common_texture_free(existing, EINA_TRUE);
tex->evas_tex = NULL;
@@ -1096,20 +1115,26 @@ span_collector_upload_textures(Span_Collector *sc, void *gc_ptr)
* without a staging copy. */
if (!tex->evas_tex)
{
- /* Initial creation via Evas texture pool. */
+ /* Initial creation via Evas texture pool. Allocate at the
+ * high-water alloc_height so later frames with a different
+ * active height can reuse this texture in place. */
int row_bytes = tex_width * 4;
+ int tex_h = sc->alloc_height;
RGBA_Image *im;
Image_Entry *ie;
+ if (tex_h < sc->height) tex_h = sc->height;
ie = evas_cache_image_copied_data(evas_common_image_cache_get(),
- tex_width, sc->height,
+ tex_width, tex_h,
NULL, EINA_TRUE,
EVAS_COLORSPACE_ARGB8888);
if (!ie) continue;
ie->flags.preload_done = 0;
im = (RGBA_Image *)ie;
- /* Copy pre-swapped buffer into the RGBA_Image. */
+ /* Copy pre-swapped buffer into the RGBA_Image. Rows past the
+ * active height are never sampled but are zeroed so the texture
+ * never carries uninitialised memory. */
{
int row;
uint8_t *dst = (uint8_t *)im->image.data;
@@ -1119,6 +1144,8 @@ span_collector_upload_textures(Span_Collector *sc, void *gc_ptr)
row_bytes);
dst += row_bytes;
}
+ if (tex_h > sc->height)
+ memset(dst, 0, (size_t)(tex_h - sc->height) * row_bytes);
}
evas_t = evas_gl_common_texture_new(gc, im, EINA_FALSE);
@@ -1128,9 +1155,9 @@ span_collector_upload_textures(Span_Collector *sc, void *gc_ptr)
}
else
{
- /* Update existing texture in-place. Upload the full span
- * buffer via glTexSubImage2D — avoids creating a new
- * texture object and pool allocation each frame. */
+ /* Update existing texture in-place via glTexSubImage2D —
+ * avoids creating a new texture object and pool allocation
+ * each frame. */
evas_t = (Evas_GL_Texture *)tex->evas_tex;
glBindTexture(GL_TEXTURE_2D, evas_t->pt->texture);
@@ -1146,7 +1173,7 @@ span_collector_upload_textures(Span_Collector *sc, void *gc_ptr)
glPixelStorei(GL_UNPACK_ROW_LENGTH, stride_pixels);
glTexSubImage2D(GL_TEXTURE_2D, 0,
evas_t->x, evas_t->y,
- tex_width, sc->height,
+ up_width, sc->height,
evas_t->pt->format,
GL_UNSIGNED_BYTE, tex->buffer);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
@@ -1158,7 +1185,7 @@ span_collector_upload_textures(Span_Collector *sc, void *gc_ptr)
* the rows cannot be handed to GL as-is. Pack them into a
* contiguous scratch buffer and upload once, rather than
* issuing one call per scanline per shape per frame. */
- size_t row_bytes = (size_t)tex_width * 4;
+ size_t row_bytes = (size_t)up_width * 4;
size_t need = row_bytes * (size_t)sc->height;
uint8_t *packed = _span_pack_buf_get(need);
@@ -1171,7 +1198,7 @@ span_collector_upload_textures(Span_Collector *sc, void *gc_ptr)
row_bytes);
glTexSubImage2D(GL_TEXTURE_2D, 0,
evas_t->x, evas_t->y,
- tex_width, sc->height,
+ up_width, sc->height,
evas_t->pt->format,
GL_UNSIGNED_BYTE, packed);
}
@@ -1181,7 +1208,7 @@ span_collector_upload_textures(Span_Collector *sc, void *gc_ptr)
for (row = 0; row < sc->height; row++)
glTexSubImage2D(GL_TEXTURE_2D, 0,
evas_t->x, evas_t->y + row,
- tex_width, 1,
+ up_width, 1,
evas_t->pt->format,
GL_UNSIGNED_BYTE,
tex->buffer + (size_t)row * sc->stride);
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.