Skip to content
Open
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
20 changes: 15 additions & 5 deletions include/alpaka/mem/BoundaryIter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ namespace alpaka
return T_dim;
}

[[nodiscard]] constexpr bool isOutOfBounds() const
{
for(uint32_t i = 0; i < T_dim; ++i)
{
if(data[i] == BoundaryType::OOB)
return true;
}
return false;
}

/** @brief The dimensionality of the boundary direction. For example, a vertex (corner) of a 3D-volume (cube)
* is 0-dimensional. See also the functions isVertex(), isEdge(), etc.
*/
Expand All @@ -84,37 +94,37 @@ namespace alpaka
*/
[[nodiscard]] constexpr bool isVertex() const
{
return boundaryDimensionality() == 0;
return !isOutOfBounds() && boundaryDimensionality() == 0;
}

/** @brief Return true if this boundary direction describes an edge, for example any of the 12 edges of a cube.
*/
[[nodiscard]] constexpr bool isEdge() const
{
return boundaryDimensionality() == 1;
return !isOutOfBounds() && boundaryDimensionality() == 1;
}

/** @brief Return true if this boundary direction describes a face, for example any of the 6 sides of a cube.
*/
[[nodiscard]] constexpr bool isFace() const
{
return boundaryDimensionality() == 2;
return !isOutOfBounds() && boundaryDimensionality() == 2;
}

/** @brief Return true if this boundary direction describes a cell, for example the interior of a cube or one
* of the 8 cells in a tesseract.
*/
[[nodiscard]] constexpr bool isCell() const
{
return boundaryDimensionality() == 3;
return !isOutOfBounds() && boundaryDimensionality() == 3;
}

/** @brief Return true if this boundary direction describes the interior of a volume, like the 2D interior of a
* plane or the 3D interior of a cube.
*/
[[nodiscard]] constexpr bool isInterior() const
{
return boundaryDimensionality() == dim();
return !isOutOfBounds() && boundaryDimensionality() == dim();
}

[[nodiscard]] constexpr auto operator<=>(BoundaryDirection const&) const = default;
Expand Down
4 changes: 2 additions & 2 deletions include/alpaka/mem/IdxRange.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ namespace alpaka
auto const range,
alpaka::BoundaryDirection<T_dim, T_LowHaloVec, T_UpHaloVec> const& boundaryDir)
{
auto m_begin = Vec<uint32_t, T_dim>::fill(0u);
auto m_end = Vec<uint32_t, T_dim>::fill(0u);
auto m_begin = range.m_begin;
auto m_end = range.m_end;
for(uint32_t i = 0; i < T_dim; ++i)
{
switch(boundaryDir.data[i])
Expand Down
9 changes: 9 additions & 0 deletions include/alpaka/mem/MdForwardIter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ namespace alpaka
public:
constexpr MdForwardIter(T_MdSpan const& mdSpan) : m_mdSpan(mdSpan), m_current{IterIdxVecType::fill(0u)}
{
// Any zero extent makes the span empty, so start directly at end().
for(uint32_t idx = 0; idx < iterDim; ++idx)
{
if(m_mdSpan.getExtents()[idx] == index_type{0u})
{
m_current[0] = m_mdSpan.getExtents()[0];
break;
}
}
}

ALPAKA_FN_ACC constexpr index_type slowCurrent() const
Expand Down
10 changes: 10 additions & 0 deletions include/alpaka/mem/MdSpan.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,21 @@ namespace alpaka
}

constexpr auto begin() const
{
return MdForwardIter{this->getConstMdSpan()};
}

constexpr auto begin()
{
return MdForwardIter{*this};
}

constexpr auto end() const
{
return MdForwardIterEnd{this->getConstMdSpan()};
}

constexpr auto end()
{
return MdForwardIterEnd{*this};
}
Expand Down
10 changes: 10 additions & 0 deletions include/alpaka/mem/MdSpanArray.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,21 @@ namespace alpaka
}

constexpr auto begin() const
{
return MdForwardIter{this->getConstMdSpan()};
}

constexpr auto begin()
{
return MdForwardIter{*this};
}

constexpr auto end() const
{
return MdForwardIterEnd{this->getConstMdSpan()};
}

constexpr auto end()
{
return MdForwardIterEnd{*this};
}
Expand Down
36 changes: 32 additions & 4 deletions include/alpaka/mem/View.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,39 @@ namespace alpaka
constexpr auto getSubView(
alpaka::BoundaryDirection<View::dim(), LowHaloVecType, UpHaloVecType> boundaryDir) const
{
constexpr uint32_t dim = View::dim();
auto offset = alpaka::Vec<uint32_t, dim>{};
auto extents = alpaka::Vec<uint32_t, dim>{};
auto offset = typename T_Extents::UniVec{};
auto extents = typename T_Extents::UniVec{};

for(uint32_t i = 0; i < dim; ++i)
for(uint32_t i = 0; i < View::dim(); ++i)
{
switch(boundaryDir.data[i])
{
case BoundaryType::LOWER:
offset[i] = 0;
extents[i] = boundaryDir.lowerHaloSize[i];
break;
case BoundaryType::UPPER:
offset[i] = this->getExtents()[i] - boundaryDir.upperHaloSize[i];
extents[i] = boundaryDir.upperHaloSize[i];
break;
case BoundaryType::MIDDLE:
offset[i] = boundaryDir.lowerHaloSize[i];
extents[i] = this->getExtents()[i] - boundaryDir.lowerHaloSize[i] - boundaryDir.upperHaloSize[i];
break;
default:
throw std::invalid_argument("invalid direction");
}
}
return getSubView(offset, extents);
}

template<alpaka::concepts::Vector LowHaloVecType, alpaka::concepts::Vector UpHaloVecType>
constexpr auto getSubView(alpaka::BoundaryDirection<View::dim(), LowHaloVecType, UpHaloVecType> boundaryDir)
{
auto offset = typename T_Extents::UniVec{};
auto extents = typename T_Extents::UniVec{};

for(uint32_t i = 0; i < View::dim(); ++i)
{
switch(boundaryDir.data[i])
{
Expand Down
203 changes: 203 additions & 0 deletions test/unit/mem/boundaryIter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/* Copyright 2026 Simeon Ehrig
* SPDX-License-Identifier: MPL-2.0
*/

#include <alpaka/alpaka.hpp>

#include <catch2/catch_test_macros.hpp>

#include <sstream>
#include <type_traits>
#include <vector>

using namespace alpaka;

TEST_CASE("BoundaryIter host coverage", "[mem][BoundaryIter][iterator]")
{
SECTION("containers enumerate every direction once and stop at the OOB sentinel")
{
// The iterator should walk all 3^dim boundary states in lexicographic order without duplicates.
auto const lowerHalos = alpaka::Vec{1u, 2u, 3u};
auto const upperHalos = alpaka::Vec{4u, 5u, 6u};
auto const directions = alpaka::BoundaryDirectionsContainer{lowerHalos, upperHalos};

using Direction = std::remove_cvref_t<decltype(*directions.begin())>;
std::vector<Direction> visited;
for(auto it = directions.begin(); it != directions.end(); ++it)
{
auto const direction = *it;
REQUIRE(direction.lowerHaloSize == lowerHalos);
REQUIRE(direction.upperHaloSize == upperHalos);

bool duplicate = false;
for(auto const& previous : visited)
{
if(previous == direction)
{
duplicate = true;
break;
}
}
REQUIRE_FALSE(duplicate);
visited.push_back(direction);
}

REQUIRE(visited.size() == directions.length());
REQUIRE(directions.length() == 27u);
REQUIRE(visited.front().data[0] == alpaka::BoundaryType::LOWER);
REQUIRE(visited.front().data[1] == alpaka::BoundaryType::LOWER);
REQUIRE(visited.front().data[2] == alpaka::BoundaryType::LOWER);
REQUIRE(directions.begin() != directions.end());

auto iter = directions.begin();
for(uint32_t i = 0; i < directions.length(); ++i)
++iter;
REQUIRE(iter == directions.end());
auto const endIter = directions.end();
REQUIRE((*endIter).data[0] == alpaka::BoundaryType::OOB);
REQUIRE((*endIter).data[1] == alpaka::BoundaryType::OOB);
REQUIRE((*endIter).data[2] == alpaka::BoundaryType::OOB);
}

SECTION("classification helpers match explicit directions")
{
// Explicit boundary patterns should map to the expected geometric classification.
auto const lowerHalos = alpaka::Vec{1u, 1u, 1u};
auto const upperHalos = alpaka::Vec{2u, 2u, 2u};
using HaloVec = std::remove_cvref_t<decltype(lowerHalos)>;

auto const vertex = alpaka::BoundaryDirection<3u, HaloVec, HaloVec>{
alpaka::Vec{alpaka::BoundaryType::LOWER, alpaka::BoundaryType::UPPER, alpaka::BoundaryType::LOWER},
lowerHalos,
upperHalos};
auto const edge = alpaka::BoundaryDirection<3u, HaloVec, HaloVec>{
alpaka::Vec{alpaka::BoundaryType::LOWER, alpaka::BoundaryType::MIDDLE, alpaka::BoundaryType::UPPER},
lowerHalos,
upperHalos};
auto const face = alpaka::BoundaryDirection<3u, HaloVec, HaloVec>{
alpaka::Vec{alpaka::BoundaryType::MIDDLE, alpaka::BoundaryType::MIDDLE, alpaka::BoundaryType::LOWER},
lowerHalos,
upperHalos};
auto const cell = alpaka::makeCoreBoundaryDirection<3u>(lowerHalos, upperHalos);

REQUIRE(vertex.boundaryDimensionality() == 0u);
REQUIRE(vertex.isVertex());
REQUIRE_FALSE(vertex.isEdge());
REQUIRE_FALSE(vertex.isFace());
REQUIRE_FALSE(vertex.isCell());
REQUIRE_FALSE(vertex.isInterior());

REQUIRE(edge.boundaryDimensionality() == 1u);
REQUIRE(edge.isEdge());
REQUIRE_FALSE(edge.isVertex());
REQUIRE_FALSE(edge.isFace());
REQUIRE_FALSE(edge.isCell());
REQUIRE_FALSE(edge.isInterior());

REQUIRE(face.boundaryDimensionality() == 2u);
REQUIRE(face.isFace());
REQUIRE_FALSE(face.isVertex());
REQUIRE_FALSE(face.isEdge());
REQUIRE_FALSE(face.isCell());
REQUIRE_FALSE(face.isInterior());

REQUIRE(cell.boundaryDimensionality() == 3u);
REQUIRE(cell.isCell());
REQUIRE(cell.isInterior());
REQUIRE_FALSE(cell.isVertex());
REQUIRE_FALSE(cell.isEdge());
REQUIRE_FALSE(cell.isFace());
}

SECTION("the OOB sentinel stays unclassified")
{
// The end sentinel is not a real boundary and should not masquerade as a vertex or interior cell.
auto const directions = alpaka::makeBoundaryDirIterator<2u>();
auto const endIter = directions.end();
auto const oob = *endIter;

REQUIRE(oob.isOutOfBounds());
REQUIRE(oob.boundaryDimensionality() == 0u);
REQUIRE_FALSE(oob.isVertex());
REQUIRE_FALSE(oob.isEdge());
REQUIRE_FALSE(oob.isFace());
REQUIRE_FALSE(oob.isCell());
REQUIRE_FALSE(oob.isInterior());
}

SECTION("core direction helpers always produce a middle direction")
{
// All makeCoreBoundaryDirection overloads should agree on the middle state and requested halo sizes.
auto const lowerHalos = alpaka::Vec{2u, 3u};
auto const upperHalos = alpaka::Vec{4u, 5u};
auto const symmetricHalos = alpaka::Vec{7u, 8u};

auto const explicitCore = alpaka::makeCoreBoundaryDirection<2u>(lowerHalos, upperHalos);
auto const symmetricCore = alpaka::makeCoreBoundaryDirection<2u>(symmetricHalos);
constexpr auto defaultCore = alpaka::makeCoreBoundaryDirection<2u>();

REQUIRE(explicitCore.data[0] == alpaka::BoundaryType::MIDDLE);
REQUIRE(explicitCore.data[1] == alpaka::BoundaryType::MIDDLE);
REQUIRE(explicitCore.lowerHaloSize == lowerHalos);
REQUIRE(explicitCore.upperHaloSize == upperHalos);

REQUIRE(symmetricCore.data[0] == alpaka::BoundaryType::MIDDLE);
REQUIRE(symmetricCore.data[1] == alpaka::BoundaryType::MIDDLE);
REQUIRE(symmetricCore.lowerHaloSize == symmetricHalos);
REQUIRE(symmetricCore.upperHaloSize == symmetricHalos);

REQUIRE(defaultCore.data[0] == alpaka::BoundaryType::MIDDLE);
REQUIRE(defaultCore.data[1] == alpaka::BoundaryType::MIDDLE);
REQUIRE(defaultCore.lowerHaloSize == alpaka::fillCVec<uint32_t, 2u, 1u>());
REQUIRE(defaultCore.upperHaloSize == alpaka::fillCVec<uint32_t, 2u, 1u>());
}

SECTION("factory overloads preserve halos and infer dimensions from inputs")
{
// The convenience factories should keep the supplied halo values whether they come from vectors or a view.
auto const symmetricHalos = alpaka::Vec{3u, 4u};
auto const asymmetricLowerHalos = alpaka::Vec{1u, 2u, 3u};
auto const asymmetricUpperHalos = alpaka::Vec{4u, 5u, 6u};
int storage[2 * 3 * 4]{};
auto const view = alpaka::makeView(alpaka::api::host, storage, alpaka::Vec{2u, 3u, 4u});

auto const symmetricDirections = alpaka::makeBoundaryDirIterator(symmetricHalos);
auto const asymmetricDirections = alpaka::makeBoundaryDirIterator(asymmetricLowerHalos, asymmetricUpperHalos);
auto const defaultDirections = alpaka::makeBoundaryDirIterator(view);

STATIC_REQUIRE(std::remove_cvref_t<decltype(symmetricDirections)>::dim() == 2u);
STATIC_REQUIRE(std::remove_cvref_t<decltype(asymmetricDirections)>::dim() == 3u);
STATIC_REQUIRE(std::remove_cvref_t<decltype(defaultDirections)>::dim() == 3u);

REQUIRE((*symmetricDirections.begin()).lowerHaloSize == symmetricHalos);
REQUIRE((*symmetricDirections.begin()).upperHaloSize == symmetricHalos);
REQUIRE((*asymmetricDirections.begin()).lowerHaloSize == asymmetricLowerHalos);
REQUIRE((*asymmetricDirections.begin()).upperHaloSize == asymmetricUpperHalos);
REQUIRE((*defaultDirections.begin()).lowerHaloSize == alpaka::fillCVec<uint32_t, 3u, 1u>());
REQUIRE((*defaultDirections.begin()).upperHaloSize == alpaka::fillCVec<uint32_t, 3u, 1u>());
}

SECTION("stream output stays human-readable for representative directions")
{
// Stable text output keeps host-side diagnostics and examples easy to understand.
auto const lowerHalos = alpaka::Vec{1u, 1u, 1u};
auto const upperHalos = alpaka::Vec{2u, 2u, 2u};
using HaloVec = std::remove_cvref_t<decltype(lowerHalos)>;
auto const edge = alpaka::BoundaryDirection<3u, HaloVec, HaloVec>{
alpaka::Vec{alpaka::BoundaryType::LOWER, alpaka::BoundaryType::MIDDLE, alpaka::BoundaryType::UPPER},
lowerHalos,
upperHalos};
auto const oob = alpaka::BoundaryDirection<3u, HaloVec, HaloVec>{
alpaka::fillCVec<alpaka::BoundaryType, 3u, alpaka::BoundaryType::OOB>(),
lowerHalos,
upperHalos};

std::ostringstream edgeText;
edgeText << edge;
REQUIRE(edgeText.str() == "v-^ (edge) ");

std::ostringstream oobText;
oobText << oob;
REQUIRE(oobText.str() == "xxx");
}
}
6 changes: 4 additions & 2 deletions test/unit/mem/concepts.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,16 @@ TEST_CASE(
"test return value type of the access operator for alpaka::concepts::IDataSource and IMdSpan",
"[mem][concept]")
{
// the test is motivate by creating a MdSpan<std::atomic<int>>
// This regression test models element types such as std::atomic<int>:
// access must still work by reference even when the element type is not copyable.
// The pitch vector has to be computed from the same element type used by MdSpan.
NonCopyStruct mcs;

STATIC_REQUIRE_FALSE(std::convertible_to<NonCopyStruct, NonCopyStruct>);
STATIC_REQUIRE(std::convertible_to<NonCopyStruct&, NonCopyStruct&>);

concepts::Vector auto const extents = Vec{1u};
concepts::Vector auto const pitches = alpaka::calculatePitchesFromExtents<int>(extents);
concepts::Vector auto const pitches = alpaka::calculatePitchesFromExtents<NonCopyStruct>(extents);

alpaka::MdSpan mdspan(&mcs, extents, pitches);

Expand Down
Loading