From 4a0d8c8c9225eca8b024dd7fcfaedfbc3f22294b Mon Sep 17 00:00:00 2001 From: LibretroAdmin Date: Sat, 7 Nov 2026 15:22:16 +0100 Subject: [PATCH] image_texture_scale: the box stage reduces the image in its own buffer Capping an oversized image box-averaged it by a power of two into a second buffer of the reduced size, then either kept that (when it was the target) or ran the sinc stage from it, freeing the original in both cases. A 4K screenshot capped to 1920 held an 8.3 MB copy it did not need: every averaged pixel lands below every source pixel a later block still reads (the next block starts at y*f*sw + (x+1)*f, the store is at y*dw + x, with f >= 2), so the reduction can be made in place. Now it is. When the box result is the target the buffer is shrunk to fit with realloc and kept - no allocation at all - except on GEKKO, whose decoders memalign their output. When the sinc stage follows, its output buffer and filter are made before the box runs, so a failure still leaves the image exactly as it was; the box stage itself cannot fail once its geometry has been checked. Two things made the in-place loop slower until handled: the compiler must assume the output aliases the source it is still reading, so a row's averages are built in a 64-wide local run and copied out; and the box being a power of two, the four divisions per pixel become shifts, which give the same result for unsigned sums. Output is bit-identical to before - pixels, dimensions and return value - over 352 cases: 11 shapes from 2047x2 to 16385x3, eight caps, 8- and 10-bit, with and without the upscale stage; ASan/UBSan clean, no leaks. The image_scale_transfer test now asserts the new ownership: an exact box makes no allocation (it runs with an allocation budget of 0), a box + sinc makes one, and a failed sinc set-up leaves the source untouched; image_loader passes too. Builds C89, as C++, for aarch64 and with GEKKO. 4K -> 1920x1080: additional image storage 8.3 MB -> 0, 17.9 ms instead of 20.1 ms (8-bit) and 14.5 instead of 17.2 (10-bit). --- libretro-common/formats/image_texture_scale.c | 153 +++++++++++------- .../image_scale_transfer_test.c | 29 ++-- 2 files changed, 106 insertions(+), 76 deletions(-) diff --git a/libretro-common/formats/image_texture_scale.c b/libretro-common/formats/image_texture_scale.c index 3f95255ddc..f33432b5d5 100644 --- a/libretro-common/formats/image_texture_scale.c +++ b/libretro-common/formats/image_texture_scale.c @@ -170,21 +170,23 @@ static uint32_t box_average_wide(const uint32_t *src, | ((uint32_t)(g / n) << 8) | (uint32_t)(b / n); } -static uint32_t *downscale_box(const uint32_t *src, +/* Box-average by f (2 or more) in place: each block is read whole + * before its average is stored, and the store, at y*dw + x, lies below + * every source pixel a later block reads - the next block in the row + * starts at y*f*sw + (x+1)*f, and y*dw <= y*f*sw, x < (x+1)*f - so the + * image is reduced in its own buffer, with no second one and no copy. */ +static bool downscale_box(uint32_t *d, unsigned sw, unsigned sh, unsigned f, bool pix10, unsigned *out_w, unsigned *out_h) { - unsigned x, y, i, j; + unsigned x, y, i, j, shift; unsigned n = f * f; unsigned dw = sw / f; unsigned dh = sh / f; - uint32_t *d; + const uint32_t *src = d; - if ((dw < 1) || (dh < 1)) - return NULL; - - if (!(d = (uint32_t*)malloc((size_t)dw * dh * sizeof(uint32_t)))) - return NULL; + if ((dw < 1) || (dh < 1) || f < 2 || (f & (f - 1))) + return false; /* Keep 32-bit sums for safe power-of-two box sizes. */ if (f > (pix10 ? 2048u : 4096u)) @@ -195,55 +197,72 @@ static uint32_t *downscale_box(const uint32_t *src, src + (size_t)y * f * sw + x * f, sw, f, pix10); *out_w = dw; *out_h = dh; - return d; + return true; } + /* f is a power of two (downscale_image doubles it), so the block + * holds 2^sh pixels and the averages are shifts. A row's outputs + * are made a run at a time in a small local array and then copied + * out: built straight into d, which the compiler must assume can + * alias the source it is still reading, the loop could not be + * vectorised or have its loads hoisted, as it could when the output + * was a buffer of its own. */ + for (shift = 0; (1u << shift) < n; shift++) ; for (y = 0; y < dh; y++) { - for (x = 0; x < dw; x++) + uint32_t *drow = d + (size_t)y * dw; + unsigned x0; + for (x0 = 0; x0 < dw; x0 += 64) { - unsigned a = 0, r = 0, g = 0, b = 0; - - for (j = 0; j < f; j++) + uint32_t tmp[64]; + unsigned xn = dw - x0 < 64 ? dw - x0 : 64; + for (x = 0; x < xn; x++) { - const uint32_t *row = src + (size_t)(y * f + j) * sw + x * f; + unsigned a = 0, r = 0, g = 0, b = 0; + + for (j = 0; j < f; j++) + { + const uint32_t *row = src + (size_t)(y * f + j) * sw + + (size_t)(x0 + x) * f; + + if (pix10) + { + for (i = 0; i < f; i++) + { + uint32_t p = row[i]; + r += (p >> 20) & 0x3ff; + g += (p >> 10) & 0x3ff; + b += p & 0x3ff; + } + } + else + { + for (i = 0; i < f; i++) + { + uint32_t p = row[i]; + a += (p >> 24) & 0xff; + r += (p >> 16) & 0xff; + g += (p >> 8) & 0xff; + b += p & 0xff; + } + } + } if (pix10) - { - for (i = 0; i < f; i++) - { - uint32_t p = row[i]; - r += (p >> 20) & 0x3ff; - g += (p >> 10) & 0x3ff; - b += p & 0x3ff; - } - } + tmp[x] = 0xc0000000u | ((r >> shift) << 20) + | ((g >> shift) << 10) | (b >> shift); else - { - for (i = 0; i < f; i++) - { - uint32_t p = row[i]; - a += (p >> 24) & 0xff; - r += (p >> 16) & 0xff; - g += (p >> 8) & 0xff; - b += p & 0xff; - } - } + tmp[x] = ((a >> shift) << 24) | ((r >> shift) << 16) + | ((g >> shift) << 8) | (b >> shift); } - - if (pix10) - d[(size_t)y * dw + x] = 0xc0000000u - | ((r / n) << 20) | ((g / n) << 10) | (b / n); - else - d[(size_t)y * dw + x] = - ((a / n) << 24) | ((r / n) << 16) - | ((g / n) << 8) | (b / n); + /* Below every source pixel still to be read: see above. */ + memcpy(drow + x0, tmp, xn * sizeof(uint32_t)); } } *out_w = dw; *out_h = dh; - return d; + return true; } /* Cap a decoded image to 'cap' pixels on its longest side, preserving @@ -263,9 +282,8 @@ static bool downscale_image(unsigned cap, struct texture_image *img) { struct scaler_ctx ctx; unsigned sw, sh, dw, dh, f; - uint32_t *mid = NULL; uint32_t *out = NULL; - const uint32_t *src; + bool boxed = false; double ratio; if (!cap || !img || !img->pixels) @@ -285,7 +303,6 @@ static bool downscale_image(unsigned cap, struct texture_image *img) if ((dw < 1) || (dh < 1)) return false; - src = img->pixels; sw = img->width; sh = img->height; @@ -297,21 +314,34 @@ static bool downscale_image(unsigned cap, struct texture_image *img) * tap. */ for (f = 1; (sw / (f * 2)) >= dw; f *= 2) ; - if (f > 1) + /* The box stage runs in place, so nothing that can fail may follow + * it: the sinc stage's buffer and filter are made first, sized for + * what the box will leave, and if either fails the image is left as + * it was - as when the box had a buffer of its own. */ + if (f > 1 && (sw / f) && (sh / f)) { - unsigned bw, bh; - if ((mid = downscale_box(src, sw, sh, f, img->pix10, &bw, &bh))) - { - src = mid; - sw = bw; - sh = bh; - } + sw /= f; + sh /= f; + boxed = true; } + else + f = 1; - if (mid && sw == dw && sh == dh) + if (boxed && sw == dw && sh == dh) { - free(img->pixels); - img->pixels = mid; + downscale_box(img->pixels, img->width, img->height, f, img->pix10, + &sw, &sh); +#if !defined(GEKKO) + /* Hand back the tail the reduction left unused. Not on GEKKO, + * whose decoders memalign their output: realloc there need not + * keep the alignment. */ + { + uint32_t *shrunk = (uint32_t*)realloc(img->pixels, + (size_t)dw * dh * sizeof(uint32_t)); + if (shrunk) + img->pixels = shrunk; + } +#endif img->width = dw; img->height = dh; return true; @@ -319,10 +349,7 @@ static bool downscale_image(unsigned cap, struct texture_image *img) /* Stage 2: sinc to the exact target. */ if (!(out = (uint32_t*)malloc((size_t)dw * dh * sizeof(uint32_t)))) - { - free(mid); return false; - } memset(&ctx, 0, sizeof(ctx)); ctx.in_width = sw; @@ -339,14 +366,16 @@ static bool downscale_image(unsigned cap, struct texture_image *img) if (!scaler_ctx_gen_filter(&ctx)) { free(out); - free(mid); return false; } - scaler_ctx_scale(&ctx, out, src); + if (boxed) + downscale_box(img->pixels, img->width, img->height, f, img->pix10, + &sw, &sh); + + scaler_ctx_scale(&ctx, out, img->pixels); scaler_ctx_gen_reset(&ctx); - free(mid); free(img->pixels); img->pixels = out; diff --git a/libretro-common/samples/gfx/image_scale_transfer/image_scale_transfer_test.c b/libretro-common/samples/gfx/image_scale_transfer/image_scale_transfer_test.c index fa61bd5603..bfb976375f 100644 --- a/libretro-common/samples/gfx/image_scale_transfer/image_scale_transfer_test.c +++ b/libretro-common/samples/gfx/image_scale_transfer/image_scale_transfer_test.c @@ -115,12 +115,13 @@ static void exact(unsigned w, unsigned h, unsigned cap, int pix10) for (c = 0; c < (pix10 ? 3u : 4u); c++) expected[y * dw + x] |= (sums[c] / n) << (c * (pix10 ? 10 : 8)); } - budget = 1; + /* An exact box reduction is made in the image's own buffer: no + * allocation at all, so none is allowed. */ + budget = 0; assert(image_texture_scale(&img, 0, cap)); - assert(calls == 1 && source_frees == 1); + assert(calls == 0 && source_frees == 0 && !live); assert(img.width == dw && img.height == dh); assert(img.supports_rgba && img.pix10 == (pix10 != 0)); - assert(img.pixels == buffers[0].ptr && live == (size_t)dw * dh * 4); assert(!memcmp(img.pixels, expected, (size_t)dw * dh * 4)); image_free(img.pixels); assert(!live); @@ -130,26 +131,26 @@ static void exact(unsigned w, unsigned h, unsigned cap, int pix10) static void failures(void) { struct texture_image img; - init(&img, 16, 8, 0); + /* A reduction that needs the sinc stage: its buffer comes first, and + * the box stage, which works in place, only runs once that and the + * filter are in hand - so a failure leaves the image untouched. */ + init(&img, 39, 29, 0); budget = 0; - assert(!image_texture_scale(&img, 0, 8)); + assert(!image_texture_scale(&img, 0, 16)); assert(!source_frees && img.pixels == source_pixels); - assert(img.width == 16 && img.height == 8 && !live); + assert(img.width == 39 && img.height == 29 && !live); budget = -1; - assert(image_texture_scale(&img, 0, 8)); + calls = 0; + assert(image_texture_scale(&img, 0, 16)); + assert(img.width == 16 && img.height == 11 && calls == 1); assert(source_frees == 1); image_free(img.pixels); assert(!live); init(&img, 19, 13, 0); assert(!image_texture_scale(&img, 0, 8)); - assert(img.width == 19 && img.height == 13 && calls == 2); + assert(img.width == 19 && img.height == 13 && calls == 1); assert(!source_frees && !live); image_free(img.pixels); - init(&img, 39, 29, 0); - assert(image_texture_scale(&img, 0, 16)); - assert(img.width == 16 && img.height == 11 && calls == 2); - image_free(img.pixels); - assert(!live); } static void benchmark(int pix10) @@ -256,7 +257,7 @@ static void large_box(unsigned factor, int pix10, uint32_t pixel) assert(image_texture_scale(&img, 0, 1)); assert(img.width == 1 && img.height == 1); assert(img.pixels[0] == expected); - assert(source_frees == 1 && calls == 1); + assert(source_frees == 0 && calls == 0); image_free(img.pixels); assert(!live); }