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).
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-11-07 15:22:15 +01:00
1 parent cc070e91a9
commit 4a0d8c8c92
2 files changed
+106 -76

No files matched your search

+91 -62
View File
@@ -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;
@@ -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);
}