diff --git a/include/alpaka/mem/BoundaryIter.hpp b/include/alpaka/mem/BoundaryIter.hpp index fb2987024..1b5483854 100644 --- a/include/alpaka/mem/BoundaryIter.hpp +++ b/include/alpaka/mem/BoundaryIter.hpp @@ -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. */ @@ -84,21 +94,21 @@ 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 @@ -106,7 +116,7 @@ namespace alpaka */ [[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 @@ -114,7 +124,7 @@ namespace alpaka */ [[nodiscard]] constexpr bool isInterior() const { - return boundaryDimensionality() == dim(); + return !isOutOfBounds() && boundaryDimensionality() == dim(); } [[nodiscard]] constexpr auto operator<=>(BoundaryDirection const&) const = default; diff --git a/include/alpaka/mem/IdxRange.hpp b/include/alpaka/mem/IdxRange.hpp index b4d4ffe35..89d134a83 100644 --- a/include/alpaka/mem/IdxRange.hpp +++ b/include/alpaka/mem/IdxRange.hpp @@ -137,8 +137,8 @@ namespace alpaka auto const range, alpaka::BoundaryDirection const& boundaryDir) { - auto m_begin = Vec::fill(0u); - auto m_end = Vec::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]) diff --git a/include/alpaka/mem/MdForwardIter.hpp b/include/alpaka/mem/MdForwardIter.hpp index 66e865b0a..b47fb3689 100644 --- a/include/alpaka/mem/MdForwardIter.hpp +++ b/include/alpaka/mem/MdForwardIter.hpp @@ -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 diff --git a/include/alpaka/mem/MdSpan.hpp b/include/alpaka/mem/MdSpan.hpp index 4cae16299..8ea176586 100644 --- a/include/alpaka/mem/MdSpan.hpp +++ b/include/alpaka/mem/MdSpan.hpp @@ -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}; } diff --git a/include/alpaka/mem/MdSpanArray.hpp b/include/alpaka/mem/MdSpanArray.hpp index 75a418fff..f788cbca5 100644 --- a/include/alpaka/mem/MdSpanArray.hpp +++ b/include/alpaka/mem/MdSpanArray.hpp @@ -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}; } diff --git a/include/alpaka/mem/View.hpp b/include/alpaka/mem/View.hpp index bc00be600..f7dbac63a 100644 --- a/include/alpaka/mem/View.hpp +++ b/include/alpaka/mem/View.hpp @@ -233,11 +233,39 @@ namespace alpaka constexpr auto getSubView( alpaka::BoundaryDirection boundaryDir) const { - constexpr uint32_t dim = View::dim(); - auto offset = alpaka::Vec{}; - auto extents = alpaka::Vec{}; + 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 + constexpr auto getSubView(alpaka::BoundaryDirection 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]) { diff --git a/test/unit/mem/boundaryIter.cpp b/test/unit/mem/boundaryIter.cpp new file mode 100644 index 000000000..25669b05b --- /dev/null +++ b/test/unit/mem/boundaryIter.cpp @@ -0,0 +1,203 @@ +/* Copyright 2026 Simeon Ehrig + * SPDX-License-Identifier: MPL-2.0 + */ + +#include + +#include + +#include +#include +#include + +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; + std::vector 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; + + 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()); + REQUIRE(defaultCore.upperHaloSize == alpaka::fillCVec()); + } + + 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::dim() == 2u); + STATIC_REQUIRE(std::remove_cvref_t::dim() == 3u); + STATIC_REQUIRE(std::remove_cvref_t::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()); + REQUIRE((*defaultDirections.begin()).upperHaloSize == alpaka::fillCVec()); + } + + 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; + 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(), + lowerHalos, + upperHalos}; + + std::ostringstream edgeText; + edgeText << edge; + REQUIRE(edgeText.str() == "v-^ (edge) "); + + std::ostringstream oobText; + oobText << oob; + REQUIRE(oobText.str() == "xxx"); + } +} diff --git a/test/unit/mem/concepts.cpp b/test/unit/mem/concepts.cpp index c243df08c..d4e32f1f3 100644 --- a/test/unit/mem/concepts.cpp +++ b/test/unit/mem/concepts.cpp @@ -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> + // This regression test models element types such as std::atomic: + // 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); STATIC_REQUIRE(std::convertible_to); concepts::Vector auto const extents = Vec{1u}; - concepts::Vector auto const pitches = alpaka::calculatePitchesFromExtents(extents); + concepts::Vector auto const pitches = alpaka::calculatePitchesFromExtents(extents); alpaka::MdSpan mdspan(&mcs, extents, pitches); diff --git a/test/unit/mem/constCorrectness.cpp b/test/unit/mem/constCorrectness.cpp index 915531289..881838376 100644 --- a/test/unit/mem/constCorrectness.cpp +++ b/test/unit/mem/constCorrectness.cpp @@ -465,6 +465,7 @@ TEST_CASE("buffer const correctness MD", "[mem][sharedBuffer][correctness]") auto buffer0 = onHost::allocHost(Vec{10, 10}); requiresMutableBufferMd(buffer0); static_assert(!std::is_const_v>); + auto boundaryDir = alpaka::makeCoreBoundaryDirection<2u>(alpaka::Vec{1u, 2u}, alpaka::Vec{3u, 4u}); SECTION("getSubView/getSubBuffer from mutable DataStorage object") { @@ -491,6 +492,12 @@ TEST_CASE("buffer const correctness MD", "[mem][sharedBuffer][correctness]") [[maybe_unused]] MdSpan mdSpan = buffer0.getMdSpan(); static_assert(!std::is_const_v>); static_assert(!std::is_const_v>); + + // BoundaryDirection subviews from mutable views must preserve writable element access. + [[maybe_unused]] View boundaryView = buffer0.getView(); + [[maybe_unused]] auto boundarySubView = boundaryView.getSubView(boundaryDir); + static_assert(!std::is_const_v>); + static_assert(!std::is_const_v>); } SECTION("getSubView/getSubBuffer from non-mutable DataStorage object (inner const)") @@ -520,6 +527,12 @@ TEST_CASE("buffer const correctness MD", "[mem][sharedBuffer][correctness]") [[maybe_unused]] MdSpan mdSpan = innerConstBuffer0.getMdSpan(); static_assert(std::is_const_v>); static_assert(std::is_const_v>); + + // BoundaryDirection subviews from inner-const views must stay read-only. + [[maybe_unused]] View boundaryView = innerConstBuffer0.getView(); + [[maybe_unused]] auto boundarySubView = boundaryView.getSubView(boundaryDir); + static_assert(std::is_const_v>); + static_assert(std::is_const_v>); } SECTION("getSubView/getSubBuffer from non-mutable DataStorage object (outer const)") @@ -550,5 +563,180 @@ TEST_CASE("buffer const correctness MD", "[mem][sharedBuffer][correctness]") [[maybe_unused]] MdSpan mdSpan = outerConstBuffer0.getMdSpan(); static_assert(std::is_const_v>); static_assert(std::is_const_v>); + + // BoundaryDirection subviews from outer-const views must also stay read-only. + [[maybe_unused]] View boundaryView = outerConstBuffer0.getView(); + [[maybe_unused]] auto boundarySubView = boundaryView.getSubView(boundaryDir); + static_assert(std::is_const_v>); + static_assert(std::is_const_v>); + } +} + +TEST_CASE("View::getConstView keeps host metadata and read-only access", "[mem][view][correctness][constView]") +{ + alignas(32) std::array storage{}; + for(std::size_t i = 0; i < storage.size(); ++i) + { + storage[i] = static_cast(100 + i); + } + + auto mutableView + = alpaka::makeView(alpaka::api::host, storage.data(), alpaka::Vec{2u, 3u, 4u}, alpaka::Alignment<32>{}); + auto const constView = mutableView.getConstView(); + + SECTION("compile-time const access stays locked down") + { + // `getConstView()` must turn a mutable host view into a read-only view without changing its view category. + static_assert(std::same_as< + std::remove_cvref_t, + View, alpaka::Alignment<32>>>); + static_assert(std::is_const_v>); + static_assert(std::is_const_v>); + + auto const subView = constView.getSubView(alpaka::Vec{1u, 2u, 3u}); + static_assert(std::is_const_v>); + static_assert(std::is_const_v>); + static_assert(std::is_same_v>); + } + + SECTION("runtime metadata and aliasing are preserved") + { + // Converting to a const view must preserve shape, layout, aliasing, and the original alignment contract. + REQUIRE(constView.getExtents() == mutableView.getExtents()); + REQUIRE(constView.getPitches() == mutableView.getPitches()); + REQUIRE(constView.data() == mutableView.data()); + + STATIC_REQUIRE(std::is_same_v>); + + auto const sampleIdx = alpaka::Vec{1u, 2u, 3u}; + REQUIRE(&constView[sampleIdx] == &mutableView[sampleIdx]); + REQUIRE(constView[sampleIdx] == mutableView[sampleIdx]); + + mutableView[sampleIdx] = 777; + REQUIRE(constView[sampleIdx] == 777); + } + + SECTION("subviews created from the const view stay read-only and alias the same storage") + { + // Follow-on subviews from the returned const view should keep the parent layout while exposing only const + // access. + auto const offset = alpaka::Vec{1u, 1u, 1u}; + auto const extents = alpaka::Vec{1u, 2u, 3u}; + auto const subView = constView.getSubView(offset, extents); + + REQUIRE(subView.getExtents() == extents); + REQUIRE(subView.getPitches() == constView.getPitches()); + REQUIRE(subView.data() == &constView[offset]); + STATIC_REQUIRE(std::is_same_v>); + static_assert(std::is_const_v>); + static_assert(std::is_const_v>); + + for(auto idx : alpaka::IdxRange{extents}) + { + REQUIRE(subView[idx] == mutableView[offset + idx]); + } + } +} + +TEST_CASE( + "alpaka::makeView(any) preserves host view shape alignment and inner constness", + "[mem][view][correctness][makeView]") +{ + auto mutableBuffer = alpaka::onHost::allocHost(alpaka::Vec{2u, 3u}); + auto mutableView = mutableBuffer.getView(); + auto const& outerConstBuffer = mutableBuffer; + auto const& outerConstView = mutableView; + + alignas(32) std::array innerConstStorage{0, 1, 2, 3, 4, 5}; + auto innerConstView + = alpaka::makeView(alpaka::api::host, innerConstStorage.data(), alpaka::Vec{2u, 3u}, alpaka::Alignment<32>{}); + + auto mutableBufferView = alpaka::makeView(mutableBuffer); + auto outerConstBufferView = alpaka::makeView(outerConstBuffer); + auto mutableRebuiltView = alpaka::makeView(mutableView); + auto outerConstRebuiltView = alpaka::makeView(outerConstView); + auto innerConstRebuiltView = alpaka::makeView(innerConstView); + + SECTION("compile-time rebuilding keeps host API metadata and inner constness") + { + // `makeView(any)` should rebuild host buffers and views without changing their API, shape, alignment, or + // pointed-to constness. + static_assert(std::same_as); + static_assert(std::same_as); + + static_assert(std::same_as< + std::remove_cvref_t, + std::remove_cvref_t>); + static_assert(std::same_as< + std::remove_cvref_t, + std::remove_cvref_t>); + static_assert( + std::same_as); + static_assert(std::same_as); + static_assert(!std::is_const_v>); + static_assert(!std::is_const_v>); + static_assert(!std::is_const_v>); + static_assert(!std::is_const_v>); + + static_assert(std::same_as< + std::remove_cvref_t, + std::remove_cvref_t>); + static_assert(std::same_as< + std::remove_cvref_t, + std::remove_cvref_t>); + static_assert( + std::same_as); + static_assert( + std::same_as); + static_assert(std::is_const_v>); + static_assert(std::is_const_v>); + static_assert(std::is_const_v>); + static_assert(std::is_const_v>); + + static_assert(std::same_as< + std::remove_cvref_t, + std::remove_cvref_t>); + static_assert( + std::same_as); + static_assert(std::is_const_v>); + static_assert(std::is_const_v>); + } +} + +TEST_CASE("View::getMdSpan keeps host mutability boundaries", "[mem][view][mdspan][correctness]") +{ + alignas(32) std::array storage{}; + auto mutableView + = alpaka::makeView(alpaka::api::host, storage.data(), alpaka::Vec{2u, 3u}, alpaka::Alignment<32>{}); + auto mutableMdSpan = mutableView.getMdSpan(); + auto const constMdSpanFromOuterConst = std::as_const(mutableView).getMdSpan(); + auto const constMdSpanFromConstView = mutableView.getConstView().getMdSpan(); + + SECTION("compile-time mutability follows the originating host view") + { + // `getMdSpan()` must keep mutable access only for mutable host views and lock read-only paths otherwise. + static_assert(std::same_as< + std::remove_cvref_t, + MdSpan, alpaka::Vec, alpaka::Alignment<32>>>); + static_assert(std::same_as< + std::remove_cvref_t, + MdSpan, alpaka::Vec, alpaka::Alignment<32>>>); + static_assert(std::same_as< + std::remove_cvref_t, + MdSpan, alpaka::Vec, alpaka::Alignment<32>>>); + + static_assert(!std::is_const_v>); + static_assert(!std::is_const_v>); + static_assert(std::is_const_v>); + static_assert( + std::is_const_v>); + static_assert(std::is_const_v>); + static_assert( + std::is_const_v>); + + auto const readOnlyMdSpan = mutableMdSpan.getConstMdSpan(); + static_assert(std::is_const_v>); + static_assert(std::is_const_v>); + static_assert(std::is_same_v>); } } diff --git a/test/unit/mem/idxRange.cpp b/test/unit/mem/idxRange.cpp index a9d5e3d4b..ccde00187 100644 --- a/test/unit/mem/idxRange.cpp +++ b/test/unit/mem/idxRange.cpp @@ -6,6 +6,8 @@ #include +#include + TEST_CASE("IdxRange::begin() and end()", "[mem][IdxRange][iterator]") { SECTION("only end extents") @@ -59,3 +61,222 @@ TEST_CASE("IdxRange::begin() and end()", "[mem][IdxRange][iterator]") } } } + +TEST_CASE("IdxRange edge cases and transforms", "[mem][IdxRange][iterator]") +{ + auto collectVisited = [](T_Range const& range) + { + using Idx = std::decay_t; + std::vector visited; + for(auto const idx : range) + visited.push_back(idx); + return visited; + }; + + SECTION("empty range yields no elements") + { + // A range with identical begin/end must be empty rather than yielding a sentinel element. + auto const range = alpaka::IdxRange(alpaka::Vec{2, 3}, alpaka::Vec{2, 3}); + + REQUIRE(range.begin() == range.end()); + REQUIRE(collectVisited(range).empty()); + REQUIRE(range.distance() == alpaka::Vec{0, 0}); + REQUIRE(decltype(range)::dim() == 2u); + } + + SECTION("1D stride stops before overshooting end") + { + // Non-divisible spans must stop at the last valid point instead of stepping past the end bound. + auto const range = alpaka::IdxRange(alpaka::Vec{1}, alpaka::Vec{10}, alpaka::Vec{4}); + + REQUIRE(collectVisited(range) == std::vector{alpaka::Vec{1}, alpaka::Vec{5}, alpaka::Vec{9}}); + REQUIRE(range.distance() == alpaka::Vec{9}); + REQUIRE(decltype(range)::dim() == 1u); + } + + SECTION("3D traversal keeps x fastest and carries into y then z") + { + // This locks down the multidimensional visitation order for host-side iteration. + auto const range = alpaka::IdxRange(alpaka::Vec{1, 2, 3}, alpaka::Vec{3, 4, 5}); + + REQUIRE( + collectVisited(range) + == std::vector{ + alpaka::Vec{1, 2, 3}, + alpaka::Vec{1, 2, 4}, + alpaka::Vec{1, 3, 3}, + alpaka::Vec{1, 3, 4}, + alpaka::Vec{2, 2, 3}, + alpaka::Vec{2, 2, 4}, + alpaka::Vec{2, 3, 3}, + alpaka::Vec{2, 3, 4}}); + REQUIRE(range.distance() == alpaka::Vec{2, 2, 2}); + REQUIRE(decltype(range)::dim() == 3u); + } + + SECTION("translations preserve stride and shift every visited point") + { + // Shifting a range should translate begin/end and all visited indices without altering stride. + auto const original = alpaka::IdxRange(alpaka::Vec{2, 1}, alpaka::Vec{8, 7}, alpaka::Vec{3, 2}); + auto const shiftedRight = original >> alpaka::Vec{4, 5}; + auto const shiftedLeft = original << alpaka::Vec{1, 1}; + + REQUIRE(original.m_stride == shiftedRight.m_stride); + REQUIRE(original.m_stride == shiftedLeft.m_stride); + REQUIRE(original.distance() == shiftedRight.distance()); + REQUIRE(original.distance() == shiftedLeft.distance()); + REQUIRE(decltype(original)::dim() == decltype(shiftedRight)::dim()); + REQUIRE(decltype(original)::dim() == decltype(shiftedLeft)::dim()); + REQUIRE( + collectVisited(shiftedRight) + == std::vector{ + alpaka::Vec{6, 6}, + alpaka::Vec{6, 8}, + alpaka::Vec{6, 10}, + alpaka::Vec{9, 6}, + alpaka::Vec{9, 8}, + alpaka::Vec{9, 10}}); + REQUIRE( + collectVisited(shiftedLeft) + == std::vector{ + alpaka::Vec{1, 0}, + alpaka::Vec{1, 2}, + alpaka::Vec{1, 4}, + alpaka::Vec{4, 0}, + alpaka::Vec{4, 2}, + alpaka::Vec{4, 4}}); + } + + SECTION("stride scaling changes visited points but not begin/end") + { + // Scaling the stride must sparsify iteration while keeping the same logical bounds. + auto const original = alpaka::IdxRange(alpaka::Vec{1, 2}, alpaka::Vec{8, 11}, alpaka::Vec{2, 3}); + auto const scaled = original % 2; + + REQUIRE(scaled.m_begin == original.m_begin); + REQUIRE(scaled.m_end == original.m_end); + REQUIRE(scaled.m_stride == alpaka::Vec{4, 6}); + REQUIRE(scaled.distance() == original.distance()); + REQUIRE(decltype(original)::dim() == decltype(scaled)::dim()); + REQUIRE( + collectVisited(original) + == std::vector{ + alpaka::Vec{1, 2}, + alpaka::Vec{1, 5}, + alpaka::Vec{1, 8}, + alpaka::Vec{3, 2}, + alpaka::Vec{3, 5}, + alpaka::Vec{3, 8}, + alpaka::Vec{5, 2}, + alpaka::Vec{5, 5}, + alpaka::Vec{5, 8}, + alpaka::Vec{7, 2}, + alpaka::Vec{7, 5}, + alpaka::Vec{7, 8}}); + REQUIRE( + collectVisited(scaled) + == std::vector{alpaka::Vec{1, 2}, alpaka::Vec{1, 8}, alpaka::Vec{5, 2}, alpaka::Vec{5, 8}}); + } +} + +TEST_CASE("IdxRange boundary slicing keeps bounds, emptiness, and stride", "[mem][IdxRange][boundary]") +{ + auto collectVisited = [](T_Range const& range) + { + using Idx = std::decay_t; + std::vector visited; + for(auto const idx : range) + visited.push_back(idx); + return visited; + }; + + SECTION("lower upper and middle slices keep the expected subrange values") + { + // Boundary slices should map directly onto the parent begin/end coordinates in each selected dimension. + auto const parent = alpaka::IdxRange(alpaka::Vec{2, 5, 7}, alpaka::Vec{11, 17, 21}, alpaka::Vec{3, 4, 5}); + auto const lowerHalos = alpaka::Vec{2u, 3u, 4u}; + auto const upperHalos = alpaka::Vec{1u, 5u, 2u}; + using HaloVec = std::remove_cvref_t; + auto const makeBoundary = [&](auto const& data) + { return alpaka::BoundaryDirection<3u, HaloVec, HaloVec>{data, lowerHalos, upperHalos}; }; + + // LOWER should keep the parent origin in selected dimensions while the core stays cropped by both halos. + auto const lower = alpaka::makeDirectionSubRange( + parent, + makeBoundary( + alpaka::Vec{alpaka::BoundaryType::LOWER, alpaka::BoundaryType::MIDDLE, alpaka::BoundaryType::LOWER})); + REQUIRE(lower.m_begin == alpaka::Vec{2, 8, 7}); + REQUIRE(lower.m_end == alpaka::Vec{4, 12, 11}); + REQUIRE(lower.m_stride == parent.m_stride); + REQUIRE(collectVisited(lower) == std::vector{alpaka::Vec{2, 8, 7}}); + + // UPPER should anchor the selected dimensions against the parent tail without disturbing stride. + auto const upper = alpaka::makeDirectionSubRange( + parent, + makeBoundary( + alpaka::Vec{alpaka::BoundaryType::UPPER, alpaka::BoundaryType::UPPER, alpaka::BoundaryType::MIDDLE})); + REQUIRE(upper.m_begin == alpaka::Vec{10, 12, 11}); + REQUIRE(upper.m_end == alpaka::Vec{11, 17, 19}); + REQUIRE(upper.m_stride == parent.m_stride); + REQUIRE( + collectVisited(upper) + == std::vector{ + alpaka::Vec{10, 12, 11}, + alpaka::Vec{10, 12, 16}, + alpaka::Vec{10, 16, 11}, + alpaka::Vec{10, 16, 16}}); + + // MIDDLE should crop by asymmetric halos in every dimension and preserve the sparse parent walk. + auto const middle + = alpaka::makeDirectionSubRange(parent, alpaka::makeCoreBoundaryDirection<3u>(lowerHalos, upperHalos)); + REQUIRE(middle.m_begin == alpaka::Vec{4, 8, 11}); + REQUIRE(middle.m_end == alpaka::Vec{10, 12, 19}); + REQUIRE(middle.m_stride == parent.m_stride); + REQUIRE( + collectVisited(middle) + == std::vector{ + alpaka::Vec{4, 8, 11}, + alpaka::Vec{4, 8, 16}, + alpaka::Vec{7, 8, 11}, + alpaka::Vec{7, 8, 16}}); + } + + SECTION("empty core stays empty when halos consume a dimension") + { + // If halos eat the full span of one dimension, the middle slice must become empty instead of yielding garbage. + auto const parent = alpaka::IdxRange(alpaka::Vec{1, 3, 5}, alpaka::Vec{7, 9, 11}, alpaka::Vec{2, 3, 2}); + auto const middle = alpaka::makeDirectionSubRange( + parent, + alpaka::makeCoreBoundaryDirection<3u>(alpaka::Vec{1u, 2u, 3u}, alpaka::Vec{1u, 4u, 1u})); + + REQUIRE(middle.m_begin == alpaka::Vec{2, 5, 8}); + REQUIRE(middle.m_end == alpaka::Vec{6, 5, 10}); + REQUIRE(middle.m_stride == parent.m_stride); + REQUIRE(middle.begin() == middle.end()); + REQUIRE(collectVisited(middle).empty()); + } + + SECTION("signed ranges keep their signed coordinate type") + { + // Signed parent coordinates must survive boundary slicing so negative starts stay representable. + auto const parent = alpaka::IdxRange(alpaka::Vec{-5, -4}, alpaka::Vec{6, 9}, alpaka::Vec{4, 3}); + auto const lowerHalos = alpaka::Vec{2u, 3u}; + auto const upperHalos = alpaka::Vec{1u, 2u}; + auto const middle + = alpaka::makeDirectionSubRange(parent, alpaka::makeCoreBoundaryDirection<2u>(lowerHalos, upperHalos)); + + static_assert(std::is_same_v); + REQUIRE(middle.m_begin == alpaka::Vec{-3, -1}); + REQUIRE(middle.m_end == alpaka::Vec{5, 7}); + REQUIRE(middle.m_stride == parent.m_stride); + REQUIRE( + collectVisited(middle) + == std::vector{ + alpaka::Vec{-3, -1}, + alpaka::Vec{-3, 2}, + alpaka::Vec{-3, 5}, + alpaka::Vec{1, -1}, + alpaka::Vec{1, 2}, + alpaka::Vec{1, 5}}); + } +} diff --git a/test/unit/mem/mdIterator.cpp b/test/unit/mem/mdIterator.cpp index cdbc4da93..78ac2564f 100644 --- a/test/unit/mem/mdIterator.cpp +++ b/test/unit/mem/mdIterator.cpp @@ -6,29 +6,157 @@ #include +#include +#include +#include +#include using namespace alpaka; using namespace alpaka::onHost; -TEST_CASE("mdIterator", "") +TEST_CASE("mdIterator host coverage", "[mem][mdIterator][iterator]") { - constexpr auto numElements = CVec{}; - alpaka::concepts::IBuffer auto span = onHost::allocHost(numElements); + auto collectValues = [](T_Range&& range) + { + using Value = std::remove_cvref_t; + std::vector values; + for(auto&& value : range) + values.push_back(value); + return values; + }; + + SECTION("zero-extent MdSpan and View are empty") + { + // Zero extents should terminate immediately instead of yielding a bogus first element. + std::array storage{42u}; + auto const emptyMdSpan = alpaka::makeMdSpan(storage.data(), alpaka::Vec{0u, 3u}); + auto const emptyView = alpaka::makeView(api::host, storage.data(), alpaka::Vec{0u, 3u}); + + REQUIRE(emptyMdSpan.begin() == emptyMdSpan.end()); + REQUIRE(emptyView.begin() == emptyView.end()); + REQUIRE(collectValues(emptyMdSpan).empty()); + REQUIRE(collectValues(emptyView).empty()); + } + + SECTION("1D MdSpan iteration stays linear") + { + // A linear buffer should be visited in storage order on the host path. + std::array storage{3u, 6u, 9u, 12u, 15u}; + auto span = alpaka::makeMdSpan(storage.data(), alpaka::Vec{storage.size()}); + + REQUIRE(collectValues(span) == std::vector{3u, 6u, 9u, 12u, 15u}); + } + + SECTION("3D View iteration follows linearize order") + { + // The iterator must keep the last dimension fastest so traversal matches linearized indexing. + auto const extents = alpaka::Vec{2u, 3u, 4u}; + auto buffer = onHost::allocHost(extents); + auto view = buffer.getView(); + + meta::ndLoopIncIdx( + extents, + [&](alpaka::concepts::Vector auto idx) + { view[idx] = static_cast(linearize(extents, idx)); }); + + auto const visited = collectValues(view); + REQUIRE(visited.size() == extents.product()); + REQUIRE(visited.front() == 0u); + REQUIRE(visited.back() == extents.product() - 1u); + + for(uint32_t linearIdx = 0; linearIdx < visited.size(); ++linearIdx) + CHECK(visited[linearIdx] == linearIdx); + } + + SECTION("mutable View iteration writes back into the underlying buffer") + { + // Writing through a non-const iterator must update the storage the view refers to. + auto const extents = alpaka::Vec{2u, 3u}; + auto buffer = onHost::allocHost(extents); + auto view = buffer.getView(); - size_t counter = 0u; - for(uint32_t& v : span) - v = counter++; + int nextValue = 10; + for(int& value : view) + { + value = nextValue; + nextValue += 5; + } - // validate by using the forward iterator - size_t refence = 0u; - for(uint32_t v : span) + REQUIRE(view[alpaka::Vec{0u, 0u}] == 10); + REQUIRE(view[alpaka::Vec{0u, 2u}] == 20); + REQUIRE(view[alpaka::Vec{1u, 0u}] == 25); + REQUIRE(view[alpaka::Vec{1u, 2u}] == 35); + CHECK(buffer[alpaka::Vec{1u, 1u}] == 30); + } + + SECTION("const View iteration is read-only and preserves order") { - CHECK(v == refence); - ++refence; + // Const iteration should expose const references while traversing the same order as mutable iteration. + std::array storage{2, 4, 6, 8, 10, 12}; + auto view = alpaka::makeView(api::host, storage.data(), alpaka::Vec{2u, 3u}); + auto const constView = view.getConstView(); + + static_assert(!std::is_const_v>); + static_assert(std::is_const_v>); + static_assert(std::is_const_v>); + + REQUIRE(collectValues(view) == std::vector{2, 4, 6, 8, 10, 12}); + REQUIRE(collectValues(constView) == collectValues(view)); } - // validate without using the forward iterator - meta::ndLoopIncIdx( - numElements, - [&](alpaka::concepts::Vector auto idx) { CHECK(span[idx] == linearize(numElements, idx)); }); + SECTION("View::getMdSpan preserves layout, write-through, and const iteration") + { + // The MdSpan returned from a host view should alias the same storage and iterate in the same linear order. + alignas(32) std::array storage{}; + for(std::size_t i = 0; i < storage.size(); ++i) + storage[i] = static_cast(i); + + auto view = alpaka::makeView(api::host, storage.data(), alpaka::Vec{2u, 3u, 4u}, alpaka::Alignment<32>{}); + auto mdSpan = view.getMdSpan(); + auto const constMdSpan = view.getConstView().getMdSpan(); + + REQUIRE(mdSpan.getExtents() == view.getExtents()); + REQUIRE(mdSpan.getPitches() == view.getPitches()); + REQUIRE(mdSpan.data() == view.data()); + STATIC_REQUIRE(std::is_same_v>); + STATIC_REQUIRE(std::is_same_v>); + static_assert(!std::is_const_v>); + static_assert(std::is_const_v>); + + auto const sampleIdx = alpaka::Vec{1u, 2u, 3u}; + mdSpan[sampleIdx] = 777; + REQUIRE(view[sampleIdx] == 777); + REQUIRE(&mdSpan[sampleIdx] == &view[sampleIdx]); + REQUIRE(&constMdSpan[sampleIdx] == &view[sampleIdx]); + + auto const visited = collectValues(mdSpan); + REQUIRE(visited.size() == storage.size()); + REQUIRE(visited.front() == 0); + REQUIRE(visited.back() == 777); + REQUIRE(collectValues(constMdSpan) == visited); + } + + SECTION("pre-increment and post-increment advance one element at a time") + { + // Forward-iterator increments need to preserve the old value for post-increment and return self for + // pre-increment. + std::array storage{7, 11, 13, 17}; + auto span = alpaka::makeMdSpan(storage.data(), alpaka::Vec{storage.size()}); + + auto iter = span.begin(); + REQUIRE(*iter == 7); + + auto& preIncrement = ++iter; + REQUIRE(&preIncrement == &iter); + REQUIRE(*iter == 11); + + auto postIncrement = iter++; + REQUIRE(*postIncrement == 11); + REQUIRE(*iter == 13); + + ++iter; + REQUIRE(*iter == 17); + ++iter; + REQUIRE(iter == span.end()); + } } diff --git a/test/unit/mem/subDataStorage.cpp b/test/unit/mem/subDataStorage.cpp index 858388ac0..9bbc21e33 100644 --- a/test/unit/mem/subDataStorage.cpp +++ b/test/unit/mem/subDataStorage.cpp @@ -7,6 +7,9 @@ #include #include +#include +#include + TEST_CASE("1D alpaka::View::getSubView function tests", "[mem][view][SubDataStorage]") { constexpr int x = 10; @@ -41,6 +44,22 @@ TEST_CASE("1D alpaka::View::getSubView function tests", "[mem][view][SubDataStor REQUIRE_MESSAGE(sub_view0[i] == i + offset, "i=" << i); } } + + SECTION("getSubView, zero extent in 1D stays empty and does not iterate") + { + // Zero-sized 1D subviews should preserve the requested extent and produce an empty host iteration range. + auto sub_view0 = view0.getSubView(0); + + REQUIRE(sub_view0.getExtents() == alpaka::Vec{0}); + + auto count = 0; + for([[maybe_unused]] auto&& value : sub_view0) + { + alpaka::unused(value); + ++count; + } + REQUIRE(count == 0); + } } TEST_CASE("3D alpaka::View::getSubView function tests", "[mem][view][SubDataStorage]") @@ -117,4 +136,298 @@ TEST_CASE("3D alpaka::View::getSubView function tests", "[mem][view][SubDataStor } } } + + SECTION("getSubView, zero extent in 3D stays empty and does not iterate") + { + // Zero in any dimension should still create a valid empty subview for host iteration. + alpaka::Vec extents_subview0{z, 0, x}; + auto sub_view0 = view0.getSubView(extents_subview0); + + REQUIRE(sub_view0.getExtents() == extents_subview0); + + auto count = 0; + for([[maybe_unused]] auto&& value : sub_view0) + { + alpaka::unused(value); + ++count; + } + REQUIRE(count == 0); + } + + SECTION("getSubView with offset writes through to the parent storage") + { + // Offset subviews must alias the parent data so writes land at shifted coordinates in the original view. + alpaka::Vec offset_subview0{1, 2, 1}; + alpaka::Vec extents_subview0{1, 2, 2}; + auto sub_view0 = view0.getSubView(offset_subview0, extents_subview0); + + for(auto vec : alpaka::IdxRange{extents_subview0}) + { + sub_view0[vec] = 900 + static_cast(alpaka::linearize(extents_subview0, vec)); + } + + for(auto vec : alpaka::IdxRange{extents_subview0}) + { + auto const parent_idx = offset_subview0 + vec; + REQUIRE(view0[parent_idx] == 900 + static_cast(alpaka::linearize(extents_subview0, vec))); + } + } + + SECTION("const getSubView with offset stays read-only and reads shifted values") + { + // `getSubView() const` should propagate constness to the returned view while still reading the shifted region. + auto const& const_view0 = view0; + alpaka::Vec offset_subview0{1, 2, 1}; + alpaka::Vec extents_subview0{2, 2, 3}; + auto sub_view0 = const_view0.getSubView(offset_subview0, extents_subview0); + + static_assert(std::is_const_v>); + static_assert(std::is_const_v>); + + for(auto vec : alpaka::IdxRange{extents_subview0}) + { + auto const parent_idx = offset_subview0 + vec; + REQUIRE(sub_view0[vec] == view0[parent_idx]); + } + } +} + +TEST_CASE("alpaka::View::getSubView keeps pitches, pointer, and alignment contracts", "[mem][view][SubDataStorage]") +{ + alignas(32) std::array storage{}; + for(std::size_t i = 0; i < storage.size(); ++i) + { + storage[i] = static_cast(i); + } + + auto view0 = alpaka::makeView(alpaka::api::host, storage.data(), alpaka::Vec{2, 3, 4}, alpaka::Alignment<32>{}); + + SECTION("offset subviews preserve pitches and drop to plain alignment") + { + // A shifted origin can break stronger alignment guarantees, but the pitch layout must still match the parent. + auto const offset_subview0 = view0.getSubView(alpaka::Vec{1, 1, 1}, alpaka::Vec{1, 2, 3}); + + REQUIRE(offset_subview0.getPitches() == view0.getPitches()); + REQUIRE(offset_subview0.data() == &view0[alpaka::Vec{1, 1, 1}]); + + static_assert(std::is_same_v>); + } + + SECTION("extent-only subviews keep the original pointer and alignment") + { + // Cropping only by extent must not move the pointer or weaken the parent alignment contract. + auto const sub_view0 = view0.getSubView(alpaka::Vec{2, 2, 3}); + + REQUIRE(sub_view0.data() == view0.data()); + REQUIRE(sub_view0.getPitches() == view0.getPitches()); + + static_assert(std::is_same_v>); + } +} + +TEST_CASE( + "alpaka::makeView(any) keeps host metadata and rebuilt subviews alias the source", + "[mem][view][SubDataStorage]") +{ + auto const extents = alpaka::Vec{2u, 3u, 4u}; + auto buffer = alpaka::onHost::allocHost(extents); + for(auto idx : alpaka::IdxRange{extents}) + { + buffer[idx] = static_cast(alpaka::linearize(extents, idx)); + } + + auto rebuiltFromBuffer = alpaka::makeView(buffer); + + alignas(32) std::array storage{}; + for(std::size_t i = 0; i < storage.size(); ++i) + { + storage[i] = static_cast(100 + i); + } + + auto sourceView = alpaka::makeView(alpaka::api::host, storage.data(), extents, alpaka::Alignment<32>{}); + auto rebuiltFromView = alpaka::makeView(sourceView); + + SECTION("rebuilding from a host buffer preserves the parent metadata and alias") + { + // `makeView(buffer)` should hand back a plain host view with the same layout contract as the owning buffer. + REQUIRE(rebuiltFromBuffer.getApi() == buffer.getApi()); + REQUIRE(rebuiltFromBuffer.getExtents() == buffer.getExtents()); + REQUIRE(rebuiltFromBuffer.getPitches() == buffer.getPitches()); + REQUIRE(rebuiltFromBuffer.data() == buffer.data()); + STATIC_REQUIRE(std::is_same_v); + + auto const sampleIdx = alpaka::Vec{1u, 2u, 3u}; + rebuiltFromBuffer[sampleIdx] = 777; + REQUIRE(buffer[sampleIdx] == 777); + REQUIRE(&rebuiltFromBuffer[sampleIdx] == &buffer[sampleIdx]); + } + + SECTION("rebuilding from a host view preserves layout and narrowed subviews still alias") + { + // `makeView(view)` must keep the original view metadata, and follow-on subviews should still reference the + // same shifted region. + REQUIRE(rebuiltFromView.getApi() == sourceView.getApi()); + REQUIRE(rebuiltFromView.getExtents() == sourceView.getExtents()); + REQUIRE(rebuiltFromView.getPitches() == sourceView.getPitches()); + REQUIRE(rebuiltFromView.data() == sourceView.data()); + STATIC_REQUIRE(std::is_same_v>); + + auto const offset = alpaka::Vec{1u, 1u, 1u}; + auto const subExtents = alpaka::Vec{1u, 2u, 3u}; + auto subView = rebuiltFromView.getSubView(offset, subExtents); + + REQUIRE(subView.getExtents() == subExtents); + REQUIRE(subView.getPitches() == rebuiltFromView.getPitches()); + REQUIRE(subView.data() == &rebuiltFromView[offset]); + STATIC_REQUIRE(std::is_same_v>); + + auto const localIdx = alpaka::Vec{0u, 1u, 2u}; + subView[localIdx] = 2026; + REQUIRE(rebuiltFromView[offset + localIdx] == 2026); + REQUIRE(sourceView[offset + localIdx] == 2026); + } +} + +TEST_CASE( + "alpaka::View::getSubView(BoundaryDirection) covers host lower upper core and const aliasing", + "[mem][view][SubDataStorage]") +{ + alpaka::Vec totalExtents{4, 5, 6}; + auto buffer0 = alpaka::onHost::allocHost(totalExtents); + + for(auto idx : alpaka::IdxRange{totalExtents}) + { + buffer0[idx] = idx.z() * 100 + idx.y() * 10 + idx.x(); + } + + auto view0 = buffer0.getView(); + auto const lowerHalos = alpaka::Vec{1u, 2u, 1u}; + auto const upperHalos = alpaka::Vec{2u, 1u, 3u}; + using HaloVec = std::remove_cvref_t; + auto const makeBoundary = [&](auto const& boundaries) + { return alpaka::BoundaryDirection<3, HaloVec, HaloVec>{boundaries, lowerHalos, upperHalos}; }; + + SECTION("lower boundary keeps the origin region") + { + // LOWER should select the leading halo without shifting the origin in any chosen dimension. + auto subView = view0.getSubView(makeBoundary( + alpaka::Vec{alpaka::BoundaryType::LOWER, alpaka::BoundaryType::MIDDLE, alpaka::BoundaryType::LOWER})); + + auto const expectedExtents = alpaka::Vec{1, 2, 1}; + auto const expectedOffset = alpaka::Vec{0, static_cast(lowerHalos.y()), 0}; + + REQUIRE(subView.getExtents() == expectedExtents); + + for(auto idx : alpaka::IdxRange{expectedExtents}) + { + REQUIRE(subView[idx] == view0[expectedOffset + idx]); + } + } + + SECTION("upper boundary maps to the shifted tail region") + { + // UPPER should start from the tail extents minus the upper halo sizes. + auto subView = view0.getSubView(makeBoundary( + alpaka::Vec{alpaka::BoundaryType::UPPER, alpaka::BoundaryType::UPPER, alpaka::BoundaryType::MIDDLE})); + + auto const expectedExtents = alpaka::Vec{2, 1, 2}; + auto const expectedOffset = alpaka::Vec{ + totalExtents.z() - static_cast(upperHalos.z()), + totalExtents.y() - static_cast(upperHalos.y()), + static_cast(lowerHalos.x())}; + + REQUIRE(subView.getExtents() == expectedExtents); + + for(auto idx : alpaka::IdxRange{expectedExtents}) + { + REQUIRE(subView[idx] == view0[expectedOffset + idx]); + } + } + + SECTION("middle boundary returns the asymmetric interior") + { + // MIDDLE should crop by the lower and upper halos independently in each dimension. + auto subView = view0.getSubView(alpaka::makeCoreBoundaryDirection<3>(lowerHalos, upperHalos)); + + auto const expectedOffset = alpaka::Vec{ + static_cast(lowerHalos.z()), + static_cast(lowerHalos.y()), + static_cast(lowerHalos.x())}; + auto const expectedExtents = alpaka::Vec{ + totalExtents.z() - static_cast(lowerHalos.z()) - static_cast(upperHalos.z()), + totalExtents.y() - static_cast(lowerHalos.y()) - static_cast(upperHalos.y()), + totalExtents.x() - static_cast(lowerHalos.x()) - static_cast(upperHalos.x())}; + + REQUIRE(subView.getExtents() == expectedExtents); + + for(auto idx : alpaka::IdxRange{expectedExtents}) + { + REQUIRE(subView[idx] == view0[expectedOffset + idx]); + } + } + + SECTION("degenerate middle boundary with a consumed dimension stays empty") + { + // If halos consume a dimension completely, the middle region should be a valid zero-extent subview. + auto subView + = view0.getSubView(alpaka::makeCoreBoundaryDirection<3>(alpaka::Vec{1u, 2u, 3u}, alpaka::Vec{2u, 3u, 3u})); + + REQUIRE(subView.getExtents() == alpaka::Vec{1, 0, 0}); + + auto count = 0; + for([[maybe_unused]] auto&& value : subView) + { + alpaka::unused(value); + ++count; + } + REQUIRE(count == 0); + } + + SECTION("const boundary subviews stay read-only and still read shifted values") + { + // The boundary-direction overload must propagate constness just like the offset overload does. + auto const& constView0 = view0; + auto subView = constView0.getSubView(makeBoundary( + alpaka::Vec{alpaka::BoundaryType::UPPER, alpaka::BoundaryType::MIDDLE, alpaka::BoundaryType::UPPER})); + + auto const expectedOffset = alpaka::Vec{ + totalExtents.z() - static_cast(upperHalos.z()), + static_cast(lowerHalos.y()), + totalExtents.x() - static_cast(upperHalos.x())}; + auto const expectedExtents = alpaka::Vec{2, 2, 3}; + + REQUIRE(subView.getExtents() == expectedExtents); + static_assert(std::is_const_v>); + static_assert(std::is_const_v>); + + for(auto idx : alpaka::IdxRange{expectedExtents}) + { + REQUIRE(subView[idx] == view0[expectedOffset + idx]); + } + } + + SECTION("non-const boundary subviews preserve mutability and alias the parent") + { + // Non-const parents should keep writable element access so boundary views can update the original storage. + auto subView = view0.getSubView(makeBoundary( + alpaka::Vec{alpaka::BoundaryType::LOWER, alpaka::BoundaryType::UPPER, alpaka::BoundaryType::MIDDLE})); + + auto const expectedOffset + = alpaka::Vec{0, totalExtents.y() - static_cast(upperHalos.y()), static_cast(lowerHalos.x())}; + auto const expectedExtents = alpaka::Vec{1, 1, 2}; + + REQUIRE(subView.getExtents() == expectedExtents); + static_assert(!std::is_const_v>); + static_assert(!std::is_const_v>); + + for(auto idx : alpaka::IdxRange{expectedExtents}) + { + subView[idx] = 700 + static_cast(alpaka::linearize(expectedExtents, idx)); + } + + for(auto idx : alpaka::IdxRange{expectedExtents}) + { + REQUIRE(view0[expectedOffset + idx] == 700 + static_cast(alpaka::linearize(expectedExtents, idx))); + } + } }