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
169 changes: 167 additions & 2 deletions Tests/Structures/PlanarViewTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<float>) <= 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<float>) <= sizeof(PointerAndThreeInts));
static_assert(std::is_trivially_copyable_v<EA::PlanarView<float>>);
};

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<float>(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<decltype(view), EA::PlanarView<float>>);

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<float>(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<float>(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<float>(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<float>(data, 2, 3, 5);

EA::PlanarView<const float> 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<float>(data, 2, 4);

check(EA::PlanarView<float>().isContiguous());
check(view.isContiguous());
check(EA::PlanarView<float>(EA::Span<float>(data), 2).isContiguous());
check(EA::PlanarView<float>(data, 1, 2, 4).isContiguous());
check(EA::PlanarView<float>(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<float>(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<float>(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<const float>(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);
};
98 changes: 86 additions & 12 deletions ea_data_structures/Structures/PlanarView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> operator*() const noexcept
{
return {ptr + channel * numSamples, numSamples};
return {ptr + channel * stride, numSamples};
}

constexpr PlanarIterator& operator++() noexcept
Expand Down Expand Up @@ -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
Expand All @@ -76,6 +82,9 @@ class PlanarView
using size_type = int;
using Iterator = PlanarIterator<T>;

//Passed as subView's count to mean "everything up to the end"
static constexpr int toEnd = Span<T>::toEnd;

PlanarView() = default;

constexpr PlanarView(T* dataToUse,
Expand All @@ -84,14 +93,30 @@ class PlanarView
: ptr(dataToUse)
, numChannels(numChannelsToUse.get<int>())
, numSamples(numSamplesToUse.get<int>())
, 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<int>())
, numSamples(numSamplesToUse.get<int>())
, channelStride(channelStrideToUse.get<int>())
{
assert(numChannels <= 1 || channelStride >= numSamples);
}

//Splits one flat view evenly between the channels
constexpr PlanarView(Span<T> flatData, SizeType numChannelsToUse) noexcept
: ptr(flatData.data())
, numChannels(numChannelsToUse.get<int>())
, numSamples(numChannels > 0 ? flatData.size() / numChannels : 0)
, channelStride(numSamples)
{
}

Expand All @@ -102,6 +127,7 @@ class PlanarView
: ptr(other.data())
, numChannels(other.getNumChannels())
, numSamples(other.getNumSamples())
, channelStride(other.getChannelStride())
{
}

Expand All @@ -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())
Expand All @@ -124,14 +159,15 @@ class PlanarView
return numChannels <= 0 || numSamples <= 0;
}

//The first sample of channel 0
constexpr T* data() const noexcept { return ptr; }

constexpr Span<T> getChannel(SizeType channel) const noexcept
{
if (empty())
return {};

return {ptr + channel.get<int>() * numSamples, numSamples};
return {ptr + channel.get<int>() * channelStride, numSamples};
}

constexpr Span<T> operator[](SizeType channel) const noexcept
Expand All @@ -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<T> 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<int>());
auto remaining = numSamples - start;
auto length = count.get<int>();

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<T> 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<T>)
{
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 <typename T>
PlanarView(T*, SizeType, SizeType) -> PlanarView<T>;

template <typename T>
PlanarView(T*, SizeType, SizeType, SizeType) -> PlanarView<T>;

template <typename T>
PlanarView(Span<T>, SizeType) -> PlanarView<T>;

Expand Down
Loading