Skip to content

perf(IBA): precompute the resample mapping into per-axis tables - #5395

Open
wingfiring wants to merge 1 commit into
AcademySoftwareFoundation:mainfrom
wingfiring:feat/resample-axis-map
Open

perf(IBA): precompute the resample mapping into per-axis tables#5395
wingfiring wants to merge 1 commit into
AcademySoftwareFoundation:mainfrom
wingfiring:feat/resample-axis-map

Conversation

@wingfiring

@wingfiring wingfiring commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Description

resample_scalar() recomputes the destination-to-source mapping for every
pixel. But the mapping is separable and axis aligned: which source column a
destination column reads depends only on x, and which source row it reads
depends only on y. Computing it once per axis makes it O(width + height)
instead of O(width * height), and leaves an inner loop that indexes rather
than computes.

Both sampling modes are covered.

  • Nearest stores one tap per destination coordinate, plus the interval over
    which the source is in range. Inside that interval no per-pixel bounds test
    is needed; outside it the destination is black, which becomes a run rather
    than a branch per pixel.
  • Bilinear stores both taps and the weight. It keeps the two clamped taps
    rather than deriving the second as tap + 1, so the boundary costs no branch
    in the hot loop.

The tables serve any source with resident pixels whose channels and pixels are
packed within a scanline. Note that contiguous_scanline() permits a gap
between rows, so the row table is built from scanline_stride() rather than
from width * nchannels. Bilinear additionally declines sources whose data
window differs from their display window: WrapClamp folds to the display
window and only then goes black for anything still outside the data window,
which does not decompose into two independent per-axis tables. Anything not
served falls back to resample_scalar(), which is unchanged.

Performance

Against resample_scalar(), which is what this replaces. 1920x1080 to
1024x512, RGBA, best of 10 trials, Windows 11 / MSVC 14.50 / x64 Release:

case resample_scalar axis map
u8 -> u8 nearest 681 us 299 us 2.3x
u16 -> u16 nearest 714 us 325 us 2.2x
f -> f nearest 508 us 272 us 1.9x
u8 -> u8 bilinear 1429 us 587 us 2.4x
u16 -> u16 bilinear 1708 us 628 us 2.7x
f -> f bilinear 1770 us 579 us 3.1x

Mixed-type cases such as f -> u8 barely move, but that is not this code: when
Rtype != Atype && Rtype != TypeFloat, OIIO_DISPATCH_COMMON_TYPES2 round
trips the destination through a temporary float ImageBuf — copying in
pixels resample() is about to overwrite — and that copy pair costs roughly
3.5 ms regardless of which kernel runs underneath. Worth fixing, separately.

Relationship to the Highway path

This is orthogonal to the Highway work and does not compete with it. It changes
how the mapping is computed; Highway changes how the arithmetic is
vectorized. The dispatch puts the tables after the Highway check and
before resample_scalar(), so Highway keeps every case it already serves and
the tables pick up the rest — all of nearest, which Highway does not implement,
plus everything Highway declines.

To answer the obvious question of why not simply widen Highway's coverage
instead: the two are complementary, not alternatives. I have a four-channel
SSE2 specialization of these same tables working locally, and against
resample_hwy on the bilinear cases above it runs 1.7x to 2.3x faster —
because the tables remove the address arithmetic that SIMD cannot help with,
not because of anything about the vector width. My plan after this lands is to
extend the tables to the other resample modes and then apply them inside the
Highway path, which should carry that factor to every target Highway supports.
Keeping that out of this PR is what makes this one reviewable.

Note on ordering: because the tables sit behind the Highway check, nearest
must not reach resample_hwy — it ignores its interpolate argument and
always bilerps. That is fixed in #5393, which this PR should land
after (or alongside).

Tests

test_resample_correctness() covers two things.

Against independently evaluated expected values: a magnification and a
reduction where source pixel (x,y) holds x + 10*y, so each destination
pixel names the source pixel it came from; plus a source whose data window is a
2x2 crop of a 4x4 display window, where the outer ring must come out black.

Against resample_scalar(), bit for bit. There is no runtime switch to flip —
dispatch is by capability — so the two paths are selected by input shape
instead: the same pixels are resampled twice, once from a source with padded
scanlines (which the tables accept) and once from a source with padded pixels
(which they decline). memcmp, not a tolerance, because the failure a mistake
in the mapping arithmetic produces is a one-pixel shift, and a tolerance would
hide it. This also gets the fallback selection under test.

testsuite/oiiotool-xform gains a --resample:interp=0 case and its reference
image, generated by resample_scalar() before this change.

Checklist:

  • I have read the guidelines on contributions and code review procedures.
  • I have read the Policy on AI Coding Assistants
    and if I used AI coding assistants, I have an Assisted-by: TOOL / MODEL
    line in the pull request description above.
  • I have updated the documentation if my PR adds features or changes
    behavior.
  • I am sure that this PR's changes are tested in the testsuite.
  • I have run and passed the testsuite in CI before submitting the
    PR, by pushing the changes to my fork and seeing that the automated CI
    passed there. (Exceptions: If most tests pass and you can't figure out why
    the remaining ones fail, it's ok to submit the PR and ask for help. Or if
    any failures seem entirely unrelated to your change; sometimes things break
    on the GitHub runners.)
  • My code follows the prevailing code style of this project and I
    fixed any problems reported by the clang-format CI test.
  • If I added or modified a public C++ API call, I have also amended the
    corresponding Python bindings. If altering ImageBufAlgo functions, I also
    exposed the new functionality as oiiotool options.

@wingfiring
wingfiring force-pushed the feat/resample-axis-map branch 3 times, most recently from c2e62fb to d5b2c74 Compare August 11, 2026 01:42
resample_scalar() recomputes the destination-to-source mapping for every
pixel, but the mapping is separable and axis aligned: the source column a
destination column reads depends only on x, and likewise for rows. Building
one table per axis makes that O(width + height) instead of O(width *
height), and leaves an inner loop that indexes rather than computes.

Both sampling modes are covered. Nearest stores one tap per destination
coordinate plus the interval over which the source is in range, so the
in-range span needs no per-pixel bounds test. Bilinear stores both taps and
the weight; it keeps the two clamped taps rather than deriving the second
from the first so that the boundary needs no branch.

The tables serve any source with resident pixels whose channels and pixels
are packed within a scanline; note that this permits a gap between rows, so
the row table is built from the actual scanline stride rather than from
width * nchannels. Bilinear additionally declines sources whose data window
differs from their display window, because WrapClamp folds to the display
window and only then goes black for anything still outside the data window,
which does not decompose into two independent per-axis tables. Anything not
served falls back to the per-pixel path, which is unchanged.

Measured on a 1920x1080 to 1024x512 RGBA resample, best of 10 trials:

                       scalar   axis map
    u8->u8   nearest     581us      314us   1.9x
    f->f     nearest     612us      284us   2.2x
    u8->u8   bilinear   1854us      646us   2.9x
    f->f     bilinear   2060us      579us   3.6x

Both are bit-identical to the per-pixel path. The new test compares them by
resampling the same pixels twice, once from a source the tables accept and
once from one padded so that they decline it, so the comparison needs no
switch to flip and covers the fallback selection as well.

Signed-off-by: Jackson Sun <jackson.sun@autodesk.com>
@wingfiring
wingfiring force-pushed the feat/resample-axis-map branch from d5b2c74 to fc372fb Compare August 11, 2026 03:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant