From cba21119fffe83ffad58bb738920bf4b07ff6558 Mon Sep 17 00:00:00 2001 From: Eyal Amir Date: Thu, 24 Sep 2026 21:02:33 -0700 Subject: [PATCH] PlanarView: Added channel stride and subView A PlanarView can now carry a channel stride larger than its channel length, so it can describe a range of samples inside a larger planar block. Added a stride constructor, getChannelStride, isContiguous and subView(offset, count). The default stride equals the sample count, so existing views behave exactly as before. --- Tests/Structures/PlanarViewTests.cpp | 169 ++++++++++++++++++++- ea_data_structures/Structures/PlanarView.h | 98 ++++++++++-- 2 files changed, 253 insertions(+), 14 deletions(-) diff --git a/Tests/Structures/PlanarViewTests.cpp b/Tests/Structures/PlanarViewTests.cpp index 26f2a6b..70a6fe3 100644 --- a/Tests/Structures/PlanarViewTests.cpp +++ b/Tests/Structures/PlanarViewTests.cpp @@ -249,8 +249,173 @@ auto planarConstexpr = test("PlanarView.works_at_compile_time") = [] check(view.getNumChannels() == 2); }; -auto planarIsSmall = test("PlanarView.is_a_pointer_and_two_ints") = [] +auto planarIsSmall = test("PlanarView.is_a_pointer_and_three_ints") = [] { - static_assert(sizeof(EA::PlanarView) <= sizeof(void*) + sizeof(int) * 2); + //Three ints after a pointer round up to the pointer's alignment + struct PointerAndThreeInts + { + void* pointer; + int ints[3]; + }; + + static_assert(sizeof(EA::PlanarView) <= sizeof(PointerAndThreeInts)); static_assert(std::is_trivially_copyable_v>); }; + +auto planarStrided = test("PlanarView.strided_view_skips_the_gaps") = [] +{ + //Two channels of five samples, viewed as two channels of three + float data[] = {0, 1, 2, 3, 4, 10, 11, 12, 13, 14}; + auto view = EA::PlanarView(data, 2, 3, 5); + + check(view.getNumChannels() == 2); + check(view.getNumSamples() == 3); + check(view.getChannelStride() == 5); + check(view.getNumElements() == 6); + check(view.data() == data); + check(!view.isContiguous()); + + check(view.getChannelPointer(0) == data); + check(view.getChannelPointer(1) == data + 5); + check(view[0].size() == 3); + check(view[1].size() == 3); + check(view[1][0] == 10.f); + check(view[1][2] == 12.f); +}; + +auto planarStridedDeduction = + test("PlanarView.deduces_element_type_with_a_stride") = [] +{ + float data[] = {1, 2, 3, 4, 5, 6}; + + auto view = EA::PlanarView(data, 2, 2, 3); + static_assert(std::is_same_v>); + + check(view.getChannelStride() == 3); +}; + +auto planarSubView = test("PlanarView.sub_view_offsets_every_channel") = [] +{ + float data[] = {0, 1, 2, 3, 10, 11, 12, 13}; + auto view = EA::PlanarView(data, 2, 4); + + auto sub = view.subView(1, 2); + + check(sub.getNumChannels() == 2); + check(sub.getNumSamples() == 2); + check(sub.getChannelStride() == 4); + check(!sub.isContiguous()); + check(sub.getChannelPointer(0) == data + 1); + check(sub.getChannelPointer(1) == data + 5); + check(sub[0][0] == 1.f); + check(sub[0][1] == 2.f); + check(sub[1][0] == 11.f); + check(sub[1][1] == 12.f); +}; + +auto planarStridedIteration = + test("PlanarView.iterating_a_strided_view_honours_the_stride") = [] +{ + float data[] = {0, 1, 2, 3, 10, 11, 12, 13, 20, 21, 22, 23}; + auto view = EA::PlanarView(data, 3, 4).subView(2); + + auto numChannelsSeen = 0; + + for (auto channel: view) + { + check(channel.size() == 2); + check(channel.data() == data + numChannelsSeen * 4 + 2); + check(channel[0] == (float) (numChannelsSeen * 10 + 2)); + ++numChannelsSeen; + } + + check(numChannelsSeen == 3); +}; + +auto planarSubViewFill = test("PlanarView.fill_on_a_sub_view_leaves_gaps_alone") = [] +{ + float data[] = {0, 1, 2, 3, 10, 11, 12, 13}; + + EA::PlanarView(data, 2, 4).subView(1, 2).fill(-1.f); + + float expected[] = {0, -1, -1, 3, 10, -1, -1, 13}; + + for (auto index = 0; index < 8; ++index) + check(data[index] == expected[index]); +}; + +auto planarConstConversionStride = + test("PlanarView.const_conversion_keeps_the_stride") = [] +{ + float data[] = {0, 1, 2, 3, 4, 10, 11, 12, 13, 14}; + auto view = EA::PlanarView(data, 2, 3, 5); + + EA::PlanarView constView = view; + + check(constView.getChannelStride() == 5); + check(constView.getChannelPointer(1) == data + 5); + check(constView[1][0] == 10.f); +}; + +auto planarContiguity = test("PlanarView.is_contiguous_without_gaps") = [] +{ + float data[] = {0, 1, 2, 3, 10, 11, 12, 13}; + auto view = EA::PlanarView(data, 2, 4); + + check(EA::PlanarView().isContiguous()); + check(view.isContiguous()); + check(EA::PlanarView(EA::Span(data), 2).isContiguous()); + check(EA::PlanarView(data, 1, 2, 4).isContiguous()); + check(EA::PlanarView(data, 1, 4).subView(1, 2).isContiguous()); + check(view.subView(0, 4).isContiguous()); + check(view.subView(0).isContiguous()); + check(!view.subView(0, 3).isContiguous()); +}; + +auto planarSubViewClamps = test("PlanarView.sub_view_clamps_to_the_channel") = [] +{ + float data[] = {0, 1, 2, 3, 10, 11, 12, 13}; + auto view = EA::PlanarView(data, 2, 4); + + auto tail = view.subView(3, 10); + check(tail.getNumSamples() == 1); + check(tail[1][0] == 13.f); + + auto rest = view.subView(1); + check(rest.getNumSamples() == 3); + check(rest[1][2] == 13.f); + + auto pastTheEnd = view.subView(9, 2); + check(pastTheEnd.getNumSamples() == 0); + check(pastTheEnd.empty()); + + auto atTheEnd = view.subView(4, 0); + check(atTheEnd.empty()); + check(atTheEnd.getNumElements() == 0); + check(atTheEnd.getChannel(0).empty()); +}; + +auto planarEmptySubViewFlat = + test("PlanarView.flat_on_an_empty_strided_sub_view") = [] +{ + float data[] = {0, 1, 2, 3, 10, 11, 12, 13}; + auto empty = EA::PlanarView(data, 2, 4).subView(2, 0); + + check(empty.getChannelStride() == 4); + check(empty.isContiguous()); + check(empty.flat().empty()); +}; + +auto planarConstexprSubView = test("PlanarView.sub_view_works_at_compile_time") = [] +{ + static constexpr float data[] = {0, 1, 2, 3, 10, 11, 12, 13}; + constexpr auto view = EA::PlanarView(data, 2, 4); + constexpr auto sub = view.subView(1, 2); + + static_assert(sub.getNumSamples() == 2); + static_assert(sub.getChannelStride() == 4); + static_assert(!sub.isContiguous()); + static_assert(sub[1][0] == 11.f); + + check(sub[0][1] == 2.f); +}; diff --git a/ea_data_structures/Structures/PlanarView.h b/ea_data_structures/Structures/PlanarView.h index cdd66ad..f50c42e 100644 --- a/ea_data_structures/Structures/PlanarView.h +++ b/ea_data_structures/Structures/PlanarView.h @@ -16,16 +16,18 @@ class PlanarIterator public: constexpr PlanarIterator(T* dataToUse, int numSamplesToUse, + int strideToUse, int channelToUse) noexcept : ptr(dataToUse) , numSamples(numSamplesToUse) + , stride(strideToUse) , channel(channelToUse) { } constexpr Span operator*() const noexcept { - return {ptr + channel * numSamples, numSamples}; + return {ptr + channel * stride, numSamples}; } constexpr PlanarIterator& operator++() noexcept @@ -53,13 +55,17 @@ class PlanarIterator private: T* ptr = nullptr; int numSamples = 0; + int stride = 0; int channel = 0; }; -//A non-owning view over a planar (channel-major) block held in one contiguous -//allocation: every sample of channel 0, then every sample of channel 1, and so -//on. Slicing yields a Span per channel, so callers never write -//channel * numSamples by hand. +//A non-owning view over a planar (channel-major) block: every sample of +//channel 0, then every sample of channel 1, and so on. Channel c starts +//c * channelStride elements after channel 0. The stride defaults to the +//channel length, making the block one contiguous allocation; a larger stride +//leaves gaps between channels, which is what a sub-view over a range of +//samples looks like. Slicing yields a Span per channel, so callers never write +//channel * stride by hand. // //For the other common multichannel layout - an array of per-channel pointers - //see TwoDimensionalBufferView in BufferView.h @@ -76,6 +82,9 @@ class PlanarView using size_type = int; using Iterator = PlanarIterator; + //Passed as subView's count to mean "everything up to the end" + static constexpr int toEnd = Span::toEnd; + PlanarView() = default; constexpr PlanarView(T* dataToUse, @@ -84,14 +93,30 @@ class PlanarView : ptr(dataToUse) , numChannels(numChannelsToUse.get()) , numSamples(numSamplesToUse.get()) + , channelStride(numSamples) { } + //Channel c starts channelStride elements after channel c - 1, which must + //be at least the channel length so channels never overlap + constexpr PlanarView(T* dataToUse, + SizeType numChannelsToUse, + SizeType numSamplesToUse, + SizeType channelStrideToUse) noexcept + : ptr(dataToUse) + , numChannels(numChannelsToUse.get()) + , numSamples(numSamplesToUse.get()) + , channelStride(channelStrideToUse.get()) + { + assert(numChannels <= 1 || channelStride >= numSamples); + } + //Splits one flat view evenly between the channels constexpr PlanarView(Span flatData, SizeType numChannelsToUse) noexcept : ptr(flatData.data()) , numChannels(numChannelsToUse.get()) , numSamples(numChannels > 0 ? flatData.size() / numChannels : 0) + , channelStride(numSamples) { } @@ -102,6 +127,7 @@ class PlanarView : ptr(other.data()) , numChannels(other.getNumChannels()) , numSamples(other.getNumSamples()) + , channelStride(other.getChannelStride()) { } @@ -110,7 +136,16 @@ class PlanarView //Samples in a single channel constexpr int getNumSamples() const noexcept { return numSamples; } - //Samples across every channel + //Elements from the start of one channel to the start of the next + constexpr int getChannelStride() const noexcept { return channelStride; } + + //True when the channels sit back to back with no gaps between them + constexpr bool isContiguous() const noexcept + { + return empty() || numChannels <= 1 || channelStride == numSamples; + } + + //Samples across every channel - the logical count, excluding any gaps constexpr int getNumElements() const noexcept { if (empty()) @@ -124,6 +159,7 @@ class PlanarView return numChannels <= 0 || numSamples <= 0; } + //The first sample of channel 0 constexpr T* data() const noexcept { return ptr; } constexpr Span getChannel(SizeType channel) const noexcept @@ -131,7 +167,7 @@ class PlanarView if (empty()) return {}; - return {ptr + channel.get() * numSamples, numSamples}; + return {ptr + channel.get() * channelStride, numSamples}; } constexpr Span operator[](SizeType channel) const noexcept @@ -144,31 +180,69 @@ class PlanarView return getChannel(channel).data(); } - //The whole block as a single flat view, still in channel-major order - constexpr Span flat() const noexcept { return {ptr, getNumElements()}; } + //The samples [offset, offset + count) of every channel, keeping the + //stride. Both are clamped to the channel length, and a count of toEnd + //takes everything from offset onwards + constexpr PlanarView subView(SizeType offset, + SizeType count = toEnd) const noexcept + { + auto start = clampToLength(offset.get()); + auto remaining = numSamples - start; + auto length = count.get(); - constexpr Iterator begin() const noexcept { return {ptr, numSamples, 0}; } + if (length < 0 || length > remaining) + length = remaining; + + return {ptr + start, numChannels, length, channelStride}; + } + + //The whole block as a single flat view, still in channel-major order. + //Only meaningful when the channels are contiguous + constexpr Span flat() const noexcept + { + assert(isContiguous()); + return {ptr, getNumElements()}; + } + + constexpr Iterator begin() const noexcept + { + return {ptr, numSamples, channelStride, 0}; + } constexpr Iterator end() const noexcept { - return {ptr, numSamples, numChannels}; + return {ptr, numSamples, channelStride, numChannels}; } + //Writes every channel, leaving any gaps between them untouched void fill(const T& value) const requires(!std::is_const_v) { - flat().fill(value); + for (auto channel: *this) + channel.fill(value); } private: + constexpr int clampToLength(int index) const noexcept + { + if (index < 0) + return 0; + + return index > numSamples ? numSamples : index; + } + T* ptr = nullptr; int numChannels = 0; int numSamples = 0; + int channelStride = 0; }; template PlanarView(T*, SizeType, SizeType) -> PlanarView; +template +PlanarView(T*, SizeType, SizeType, SizeType) -> PlanarView; + template PlanarView(Span, SizeType) -> PlanarView;