Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions include/tap/dsp/fir_kernels.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,55 @@ namespace tap::dsp {
}
// ANCHOR_END: rs_dot_row

// ANCHOR: rs_dot_row_reversed
/// Dot product with the coefficient row read tap-reversed:
/// y = sum_t hist[t] * row[taps - 1 - t], history walked forward.
///
/// This is the kernel a linear-phase polyphase table halved by symmetry
/// needs: branch p of a symmetric prototype is branch L-1-p read in
/// reverse, so the mirrored branches dot the STORED row backward and the
/// halving costs storage only. Bit-exactness contract: the t-th product
/// equals dot_row's t-th product against the materialized mirrored row,
/// and accumulation runs in the same ascending-t order with the same
/// single finalize — so outputs are bit-identical to keeping the full
/// table, for every sample type (float's fixed accumulation order
/// included). On DSP-extension Arm cores the Q15 path pairs taps with the
/// swapped-lane dual MAC (SMLALDX): each 16x16 product is exact in int32
/// and the int64 accumulation is associative, so pairing changes no
/// output bit (the same argument as dot_row's SMLALD gate above).
template <sample_type S>
inline S dot_row_reversed(const typename sample_traits<S>::coeff* TAP_DSP_RESTRICT row,
const S* TAP_DSP_RESTRICT hist, std::size_t taps) noexcept {
using tr = sample_traits<S>;
#if TAP_DSP_Q15_SMLALD
if constexpr (std::is_same_v<S, std::int16_t>) {
std::int64_t acc = 0;
std::size_t t = 0;
for (; t + 1 < taps; t += 2) {
// One 32-bit load per pair on each side; the row load at
// taps-2-t packs (row[taps-2-t] lo, row[taps-1-t] hi), and
// SMLALDX's cross pairing multiplies hist.lo * row.hi +
// hist.hi * row.lo = hist[t]*row[taps-1-t] +
// hist[t+1]*row[taps-2-t] — exactly the reversed walk.
std::uint32_t h;
std::uint32_t r;
std::memcpy(&h, hist + t, sizeof h);
std::memcpy(&r, row + (taps - 2 - t), sizeof r);
acc = __smlaldx(static_cast<int16x2_t>(h), static_cast<int16x2_t>(r), acc);
}
for (; t < taps; ++t) // odd-tap tail
acc = tr::mac(acc, hist[t], row[taps - 1 - t]);
return tr::finalize(acc);
}
#endif
typename tr::accum acc{};
for (std::size_t t = 0; t < taps; ++t) {
acc = tr::mac(acc, hist[t], row[taps - 1 - t]);
}
return tr::finalize(acc);
}
// ANCHOR_END: rs_dot_row_reversed

// ANCHOR: opt_dot_tile
/// One K-channel tile of the channel-parallel dot (hypothesis C6): K
/// accumulators live in a constexpr-size local array — registers, not
Expand Down
24 changes: 24 additions & 0 deletions tests/test_fir_kernels.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
namespace {

using tap::dsp::dot_row;
using tap::dsp::dot_row_reversed;
using tap::dsp::dot_rows_frame_major;
using tap::dsp::sample_traits;

Expand Down Expand Up @@ -107,6 +108,29 @@ namespace {
}
}

// dot_row_reversed's contract: bit-identical to dot_row against the
// materialized mirrored row — the promise that lets a symmetric
// polyphase table drop half its rows and dot the stored half backward
// (see the kernel's doc comment). Even taps exercise the SMLALDX
// dual-MAC pairing on DSP-extension targets; odd taps its scalar tail.
TYPED_TEST(fir_kernels_test, ReversedMatchesMaterializedMirrorBitExact) {
using sample = TypeParam;
using tr = sample_traits<sample>;
for (const std::size_t taps : {44u, 48u, 33u, 1u, 2u}) {
std::uint32_t seed = 0x2545f491u + static_cast<std::uint32_t>(taps);
std::vector<sample> hist(taps);
std::vector<typename tr::coeff> row(taps);
for (std::size_t t = 0; t < taps; ++t) {
hist[t] = sample_from<sample>(next(seed));
row[t] = coeff_from<sample>(next(seed));
}
std::vector<typename tr::coeff> mirrored(row.rbegin(), row.rend());
EXPECT_EQ(dot_row_reversed<sample>(row.data(), hist.data(), taps),
dot_row<sample>(mirrored.data(), hist.data(), taps))
<< "taps=" << taps;
}
}

// Odd tap counts exercise dot_row's scalar tail on SMLALD targets; on
// hosts this just pins the same contract at a second geometry.
TYPED_TEST(fir_kernels_test, OddTapCountMatchesReference) {
Expand Down
Loading