mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
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:
1 parent
cc070e91a9
commit
4a0d8c8c92
2 files changed
+106
-76
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user