Merge branch 'development' of https://github.com/o3de/o3de into cgalvan/RemovedLegacyEditorConfigSpec

This commit is contained in:
Chris Galvan
2022-01-26 15:48:03 -06:00
21 changed files with 1264 additions and 121 deletions
@@ -309,78 +309,87 @@ namespace AZStd
}
static constexpr bool eq(char_type left, char_type right) noexcept { return left == right; }
static constexpr bool lt(char_type left, char_type right) noexcept { return left < right; }
static constexpr int compare(const char_type* s1, const char_type* s2, size_t count) noexcept
{
// In GCC versions prior to major version 10, __builtin_memcmp fails in valid checks in constexpr evaluation
#if !defined(AZ_COMPILER_GCC) || AZ_COMPILER_GCC >= 100000
if constexpr (AZStd::is_same_v<char_type, char>)
{
return __builtin_memcmp(s1, s2, count);
}
else if constexpr (AZStd::is_same_v<char_type, wchar_t>)
{
return __builtin_wmemcmp(s1, s2, count);
} else
#endif
{
if (az_builtin_is_constant_evaluated())
{
for (; count; --count, ++s1, ++s2)
static constexpr int compare(const char_type* s1, const char_type* s2, size_t count) noexcept
{
// In GCC versions , __builtin_memcmp fails in valid checks in constexpr evaluation
#if !defined(AZ_COMPILER_GCC)
if constexpr (AZStd::is_same_v<char_type, char>)
{
return __builtin_memcmp(s1, s2, count);
}
else if constexpr (AZStd::is_same_v<char_type, wchar_t>)
{
return __builtin_wmemcmp(s1, s2, count);
}
else
#endif
{
if (az_builtin_is_constant_evaluated())
{
for (; count; --count, ++s1, ++s2)
{
if (lt(*s1, *s2))
{
return -1;
}
else if (lt(*s2, *s1))
{
return 1;
}
}
return 0;
}
else
{
return ::memcmp(s1, s2, count * sizeof(char_type));
}
}
if (lt(*s1, *s2))
{
return -1;
}
else if (lt(*s2, *s1))
{
return 1;
}
}
return 0;
}
else
{
return ::memcmp(s1, s2, count * sizeof(char_type));
}
}
}
static constexpr size_t length(const char_type* s) noexcept
{
// For GCC versions less than 10, __builtin_strlen and __builtin_wcslen is not supported as const expressions
// so for that case it will need to manually count the characters (at compile time) instead
#if defined(AZ_COMPILER_GCC) && AZ_COMPILER_GCC < 100000
if constexpr (AZStd::is_same_v<char_type, char>)
{
#if defined(AZ_COMPILER_GCC) && AZ_COMPILER_GCC < 100000
if (!az_builtin_is_constant_evaluated())
{
return strlen(s);
}
else
{
size_t strLength{};
for (; *s; ++s, ++strLength)
{
;
}
return strLength;
}
#else
return __builtin_strlen(s);
#endif
}
else if constexpr (AZStd::is_same_v<char_type, wchar_t>)
{
#if defined(AZ_COMPILER_GCC)
if (!az_builtin_is_constant_evaluated())
{
return wcslen(s);
}
}
size_t strLength{};
for (; *s; ++s, ++strLength)
{
;
}
return strLength;
else
{
size_t strLength{};
for (; *s; ++s, ++strLength)
{
;
}
return strLength;
}
#else
if constexpr (AZStd::is_same_v<char_type, char>)
{
return __builtin_strlen(s);
}
else if constexpr (AZStd::is_same_v<char_type, wchar_t>)
{
return __builtin_wcslen(s);
#endif
}
else
{
@@ -391,46 +400,59 @@ namespace AZStd
}
return strLength;
}
#endif // defined(AZ_COMPILER_GCC) && AZ_COMPILER_GCC < 100000
}
static constexpr const char_type* find(const char_type* s, size_t count, const char_type& ch) noexcept
{
// For GCC versions less than 10, __builtin_char_memchr and __builtin_wmemchr is not supported, and
// __builtin_memchr is not supported as const expressions. In those cases we will manually locate and
// For GCC versions less than 10, __builtin_char_memchr and __builtin_wmemchr is not supported, and
// __builtin_memchr is not supported as const expressions. In those cases we will manually locate and
// return the pointer to 's' (at compile time)
#if defined(AZ_COMPILER_GCC) && AZ_COMPILER_GCC < 100000
if constexpr (AZStd::is_same_v<char_type, char>)
{
#if defined(AZ_COMPILER_GCC)
if (!az_builtin_is_constant_evaluated())
{
return static_cast<const char_type*>(__builtin_memchr(s, ch, count));
}
else
{
for (; count; --count, ++s)
{
if (eq(*s, ch))
{
return s;
}
}
return nullptr;
}
#else
return __builtin_char_memchr(s, ch, count);
#endif // defined(AZ_COMPILER_GCC)AZ_COMPILER_GCC < 100000
}
else if constexpr (AZStd::is_same_v<char_type, wchar_t>)
{
#if defined(AZ_COMPILER_GCC)
if (!az_builtin_is_constant_evaluated())
{
return wmemchr(s, ch, count);
}
}
for (; count; --count, ++s)
{
if (eq(*s, ch))
else
{
return s;
for (; count; --count, ++s)
{
if (eq(*s, ch))
{
return s;
}
}
return nullptr;
}
}
return nullptr;
#else
if constexpr (AZStd::is_same_v<char_type, char>)
{
return __builtin_char_memchr(s, ch, count);
}
else if constexpr (AZStd::is_same_v<char_type, wchar_t>)
{
return __builtin_wmemchr(s, ch, count);
#endif
}
else
{
@@ -441,9 +463,9 @@ namespace AZStd
return s;
}
}
return nullptr;
}
#endif
}
static constexpr char_type* move(char_type* dest, const char_type* src, size_t count) noexcept
{
@@ -453,7 +475,7 @@ namespace AZStd
return dest;
}
#if az_has_builtin_memmove
#if !defined(AZ_COMPILER_GCC) && az_has_builtin_memmove
__builtin_memmove(dest, src, count * sizeof(char_type));
#else
auto NonBuiltinMove = [](char_type* dest1, const char_type* src1, size_t count1) constexpr
@@ -506,7 +528,7 @@ namespace AZStd
}
static constexpr char_type* copy(char_type* dest, const char_type* src, size_t count) noexcept
{
#if az_has_builtin_memcpy
#if !defined(AZ_COMPILER_GCC) && az_has_builtin_memcpy
__builtin_memcpy(dest, src, count * sizeof(char_type));
#else
auto NonBuiltinCopy = [](char_type* dest1, const char_type* src1, size_t count1) constexpr
@@ -536,7 +558,7 @@ namespace AZStd
static constexpr char_type* copy_backward(char_type* dest, const char_type* src, size_t count) noexcept
{
char_type* result = dest;
#if az_has_builtin_memmove
#if !defined(AZ_COMPILER_GCC) && az_has_builtin_memmove
__builtin_memmove(dest, src, count * sizeof(char_type));
#else
if (az_builtin_is_constant_evaluated())
@@ -412,7 +412,7 @@ void ApplicationManager::PopulateApplicationDependencies()
// Note that its not necessary for any of these files to actually exist. It is considered a "change" if they
// change their file modtime, or if they go from existing to not existing, or if they go from not existing, to existing.
// any of those should cause AP to drop.
for (const QString& pathName : { "CrySystem",
for (QString pathName : { "CrySystem",
"SceneCore", "SceneData",
"SceneBuilder", "AzQtComponents"
})
@@ -19,7 +19,7 @@ namespace AZ
{
/**
* This class is a container of thread local storage. It allows for multiple instances
* of thread local storage to exist simultaneously (a property not possible with the
* of thread local storage to exist simultaneously (a property not possible with the
* thread_local modifier, which is really a thread global). The context tracks AZ thread
* lifetime through a bus in order to clean up storage for exiting threads. The context
* allows thread-safe iteration of all thread contexts, which is also a property not possible
@@ -36,7 +36,11 @@ namespace AZ
public:
using InitFunction = AZStd::function<void(Storage&)>;
ThreadLocalContext(InitFunction initFunction = [] (Storage&) {});
static void DefaultFunction(Storage&)
{
}
ThreadLocalContext(InitFunction initFunction = &DefaultFunction);
~ThreadLocalContext();
// No copying or moving allowed.
@@ -18,12 +18,40 @@ namespace Terrain
Aabb2i Aabb2i::operator+(const Vector2i& rhs) const
{
return { m_min + rhs, m_max + rhs };
Aabb2i returnValue = *this;
returnValue += rhs;
return returnValue;
}
Aabb2i& Aabb2i::operator+=(const Vector2i& rhs)
{
m_min += rhs;
m_max += rhs;
return *this;
}
Aabb2i Aabb2i::operator-(const Vector2i& rhs) const
{
return *this + -rhs;
Aabb2i returnValue = *this;
returnValue -= rhs;
return returnValue;
}
Aabb2i& Aabb2i::operator-=(const Vector2i& rhs)
{
m_min -= rhs;
m_max -= rhs;
return *this;
}
bool Aabb2i::operator==(const Aabb2i& other) const
{
return m_min == other.m_min && m_max == other.m_max;
}
bool Aabb2i::operator!=(const Aabb2i& other) const
{
return !(*this == other);
}
Aabb2i Aabb2i::GetClamped(Aabb2i rhs) const
@@ -21,7 +21,11 @@ namespace Terrain
Aabb2i(const Vector2i& min, const Vector2i& max);
Aabb2i operator+(const Vector2i& offset) const;
Aabb2i& operator+=(const Vector2i& offset);
Aabb2i operator-(const Vector2i& offset) const;
Aabb2i& operator-=(const Vector2i& offset);
bool operator==(const Aabb2i& other) const;
bool operator!=(const Aabb2i& other) const;
Aabb2i GetClamped(Aabb2i rhs) const;
bool IsValid() const;
@@ -0,0 +1,278 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <TerrainRenderer/ClipmapBounds.h>
#include <AzCore/Casting/numeric_cast.h>
#include <AzCore/Math/MathUtils.h>
namespace Terrain
{
bool ClipmapBoundsRegion::operator==(const ClipmapBoundsRegion& other) const
{
return m_localAabb == other.m_localAabb && m_worldAabb.IsClose(other.m_worldAabb);
}
bool ClipmapBoundsRegion::operator!=(const ClipmapBoundsRegion& other) const
{
return !(*this == other);
}
ClipmapBounds::ClipmapBounds(const ClipmapBoundsDescriptor& desc)
: m_size(desc.m_size)
, m_halfSize(desc.m_size >> 1)
, m_clipmapUpdateMultiple(AZ::GetMax<uint32_t>(desc.m_clipmapUpdateMultiple, 1))
, m_scale(desc.m_clipToWorldScale)
, m_rcpScale(1.0f / desc.m_clipToWorldScale)
{
AZ_Error("ClipmapBounds", m_scale > 0.0f, "ClipmapBounds should have a scale that is greater than 0.0f.");
m_scale = AZ::GetMax(m_scale, AZ::Constants::FloatEpsilon);
// recalculate m_center
m_center = GetSnappedCenter(GetClipSpaceVector(desc.m_worldSpaceCenter));
}
auto ClipmapBounds::UpdateCenter(const AZ::Vector2& newCenter, AZ::Aabb* untouchedRegion) -> ClipmapBoundsRegionList
{
return UpdateCenter(GetClipSpaceVector(newCenter), untouchedRegion);
}
auto ClipmapBounds::UpdateCenter(const Vector2i& newCenter, AZ::Aabb* untouchedRegion) -> ClipmapBoundsRegionList
{
AZStd::vector<Aabb2i> updateRegions;
// If the new snapped center isn't the same as the old, then generate update regions in clipmap space
Vector2i updatedCenter = GetSnappedCenter(newCenter);
int32_t xDiff = updatedCenter.m_x - m_center.m_x;
int32_t updateWidth = AZStd::GetMin<uint32_t>(abs(xDiff), m_size);
/*
Calculate the update regions. In the common case, there will be two update regions that form either
an L or inverted L shape. To avoid double-counting the corner, it is always put in the vertical box:
_
| |
| |____
|_|____|
*/
// Calculate the vertical box
if (updatedCenter.m_x != m_center.m_x)
{
updateRegions.push_back();
Aabb2i& updateRegion = updateRegions.back();
if (updatedCenter.m_x < m_center.m_x)
{
updateRegion.m_min.m_x = updatedCenter.m_x - m_halfSize;
updateRegion.m_max.m_x = updateRegion.m_min.m_x + updateWidth;
}
else
{
updateRegion.m_max.m_x = updatedCenter.m_x + m_halfSize;
updateRegion.m_min.m_x = updateRegion.m_max.m_x - updateWidth;
}
updateRegion.m_min.m_y = updatedCenter.m_y - m_halfSize;
updateRegion.m_max.m_y = updatedCenter.m_y + m_halfSize;
}
// Calculate the horizontal box
if (updatedCenter.m_y != m_center.m_y && updateWidth < m_size)
{
updateRegions.push_back();
Aabb2i& updateRegion = updateRegions.back();
uint32_t updateHeight = AZStd::GetMin<uint32_t>(abs(updatedCenter.m_y - m_center.m_y), m_size);
if (updatedCenter.m_y < m_center.m_y)
{
updateRegion.m_min.m_y = updatedCenter.m_y - m_halfSize;
updateRegion.m_max.m_y = updateRegion.m_min.m_y + updateHeight;
}
else
{
updateRegion.m_max.m_y = updatedCenter.m_y + m_halfSize;
updateRegion.m_min.m_y = updateRegion.m_max.m_y - updateHeight;
}
// If there was a vertical box, then don't double-count the corner of the update.
if (xDiff < 0)
{
updateRegion.m_min.m_x = updatedCenter.m_x - m_halfSize + updateWidth;
updateRegion.m_max.m_x = updatedCenter.m_x + m_halfSize;
}
else if (xDiff > 0)
{
updateRegion.m_min.m_x = updatedCenter.m_x - m_halfSize;
updateRegion.m_max.m_x = updatedCenter.m_x + m_halfSize - updateWidth;
}
}
if (untouchedRegion)
{
// Default to the entire area being untouched.
AZ::Aabb worldBounds = GetWorldBounds();
float maxX = worldBounds.GetMax().GetX();
float minX = worldBounds.GetMin().GetX();
float maxY = worldBounds.GetMax().GetY();
float minY = worldBounds.GetMin().GetY();
if (updatedCenter.m_x < m_center.m_x)
{
maxX = (updatedCenter.m_x + m_halfSize) * m_rcpScale;
}
else if (updatedCenter.m_x > m_center.m_x)
{
minX = (updatedCenter.m_x - m_halfSize) * m_rcpScale;
}
if (updatedCenter.m_y < m_center.m_y)
{
maxY = (updatedCenter.m_y + m_halfSize) * m_rcpScale;
}
else if (updatedCenter.m_y > m_center.m_y)
{
minY = (updatedCenter.m_y - m_halfSize) * m_rcpScale;
}
untouchedRegion->Set(AZ::Vector3(minX, minY, 0.0f), AZ::Vector3(maxX, maxY, 0.0f));
}
m_center = updatedCenter;
m_modCenter.m_x = (m_size + (m_center.m_x % m_size)) % m_size;
m_modCenter.m_y = (m_size + (m_center.m_y % m_size)) % m_size;
ClipmapBoundsRegionList boundsUpdate;
for (Aabb2i& updateRegion : updateRegions)
{
ClipmapBoundsRegionList update = TransformRegion(updateRegion);
boundsUpdate.insert(boundsUpdate.end(), update.begin(), update.end());
}
return boundsUpdate;
}
auto ClipmapBounds::TransformRegion(AZ::Aabb worldSpaceRegion) -> ClipmapBoundsRegionList
{
AZ::Vector2 worldMin = AZ::Vector2(worldSpaceRegion.GetMin().GetX(), worldSpaceRegion.GetMin().GetY());
AZ::Vector2 worldMax = AZ::Vector2(worldSpaceRegion.GetMax().GetX(), worldSpaceRegion.GetMax().GetY());
Aabb2i clipSpaceRegion;
clipSpaceRegion.m_min = GetClipSpaceVector(worldMin);
clipSpaceRegion.m_max = GetClipSpaceVector(worldMax);
return TransformRegion(clipSpaceRegion);
}
auto ClipmapBounds::TransformRegion(Aabb2i region) -> ClipmapBoundsRegionList
{
ClipmapBoundsRegionList transformedRegions;
Aabb2i clampedRegion = region.GetClamped(GetLocalBounds());
if (!clampedRegion.IsValid())
{
// Early out if the region is outside the bounds
return transformedRegions;
}
Vector2i minCorner = m_center - m_halfSize;
Vector2i minBoundary;
minBoundary.m_x = (minCorner.m_x / m_size - (minCorner.m_x < 0 ? 1 : 0)) * m_size;
minBoundary.m_y = (minCorner.m_y / m_size - (minCorner.m_y < 0 ? 1 : 0)) * m_size;
Aabb2i bottomLeftTile = Aabb2i(minBoundary, minBoundary + m_size);
// For each of the 4 quadrants:
auto calculateQuadrant = [&](Aabb2i tile)
{
Aabb2i regionClampedToTile = clampedRegion.GetClamped(tile);
if (regionClampedToTile.IsValid())
{
transformedRegions.push_back(
ClipmapBoundsRegion({
GetWorldSpaceAabb(regionClampedToTile),
regionClampedToTile - tile.m_min
})
);
}
};
calculateQuadrant(bottomLeftTile);
calculateQuadrant(bottomLeftTile + Vector2i(m_size, 0));
calculateQuadrant(bottomLeftTile + Vector2i(0, m_size));
calculateQuadrant(bottomLeftTile + Vector2i(m_size, m_size));
return transformedRegions;
}
AZ::Aabb ClipmapBounds::GetWorldBounds() const
{
Aabb2i localBounds = GetLocalBounds();
return AZ::Aabb::CreateFromMinMaxValues(
localBounds.m_min.m_x * m_scale, localBounds.m_min.m_y * m_scale, 0.0f,
localBounds.m_max.m_x * m_scale, localBounds.m_max.m_y * m_scale, 0.0f);
}
float ClipmapBounds::GetWorldSpaceSafeDistance() const
{
return (m_halfSize - m_clipmapUpdateMultiple) * m_scale;
}
Vector2i ClipmapBounds::GetSnappedCenter(const Vector2i& center)
{
Vector2i updatedCenter = m_center;
// Update the snapped center if the new center has drifted beyond the margin
auto UpdateDim = [&](int32_t centerDim, int32_t& snappedCenterDim) -> void
{
int32_t diff = centerDim - snappedCenterDim;
int32_t scaledCenterDim = (centerDim / m_clipmapUpdateMultiple);
if (centerDim < 0)
{
// Force rounding down for negatives
scaledCenterDim--;
}
if (diff >= m_clipmapUpdateMultiple)
{
snappedCenterDim = scaledCenterDim * m_clipmapUpdateMultiple;
}
if (diff < -m_clipmapUpdateMultiple)
{
snappedCenterDim = (scaledCenterDim + 1) * m_clipmapUpdateMultiple;
}
};
UpdateDim(center.m_x, updatedCenter.m_x);
UpdateDim(center.m_y, updatedCenter.m_y);
return updatedCenter;
}
Aabb2i ClipmapBounds::GetLocalBounds() const
{
return Aabb2i(m_center - m_halfSize, m_center + m_halfSize);
}
Vector2i ClipmapBounds::GetClipSpaceVector(const AZ::Vector2& worldSpaceVector) const
{
// Get rounded integer x/y coords in clipmap space.
int32_t x = AZStd::lround(worldSpaceVector.GetX() * m_rcpScale);
int32_t y = AZStd::lround(worldSpaceVector.GetY() * m_rcpScale);
return Vector2i(x, y);
}
AZ::Aabb ClipmapBounds::GetWorldSpaceAabb(const Aabb2i& clipSpaceAabb) const
{
return AZ::Aabb::CreateFromMinMaxValues(
clipSpaceAabb.m_min.m_x * m_scale, clipSpaceAabb.m_min.m_y * m_scale, 0.0f,
clipSpaceAabb.m_max.m_x * m_scale, clipSpaceAabb.m_max.m_y * m_scale, 0.0f
);
}
}
@@ -0,0 +1,159 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/base.h>
#include <AzCore/Math/Aabb.h>
#include <AzCore/Math/Vector2.h>
#include <AzCore/std/containers/vector.h>
#include <TerrainRenderer/Vector2i.h>
#include <TerrainRenderer/Aabb2i.h>
namespace Terrain
{
struct ClipmapBoundsDescriptor
{
//! Width and height of the clipmap in texels.
uint32_t m_size = 1024;
//! Current center location of the clipmap in world space
AZ::Vector2 m_worldSpaceCenter = AZ::Vector2::CreateZero();
// Updates to the clipmap will be produced in multiples of this value. This
// allows for larger but less frequent updates, and gives some wiggle room
// for each movement before an update is triggered.
// Note: This also means that whatever uses this clipmap should only ever
// display m_size - (2 * m_clipmapUpdateMultiple) pixels from the clipmap.
// Use GetWorldSpaceSafeDistance() to get the safe distance from center.
uint32_t m_clipmapUpdateMultiple = 4;
//! Scale of the clip map compared to the world. A scale of 0.5 means that
//! a clipmap of size 1024 would cover 512 meters.
float m_clipToWorldScale = 1.0f;
};
struct ClipmapBoundsRegion
{
//! The world bounds of the updated region. Z is ignored.
AZ::Aabb m_worldAabb;
//! The clipmaps bounds of the updated region. Will always be between 0 and size.
//! Min inclusive, max exclusive.
Aabb2i m_localAabb;
bool operator==(const ClipmapBoundsRegion& other) const;
bool operator!=(const ClipmapBoundsRegion& other) const;
};
// This class manages a single clipmap region. A clipmap is a virtual view into a much larger
// region, where the clipmap view is centered around a point like the current camera position.
// The clipmap texture wraps to form a repeating grid and never moves, but only data within
// the clipmap bounds is actually valid. This makes looking up data in the clipmap trivial
// since it's just the world coordinate scaled by some amount. This technique also allows for
// only the edge areas of the clipmap to be updated as the center point moves around the world.
//
// The edges of the clipmap bounds will typically run through the texture, dividing it into 4
// regions, except in cases where the clipmap bounds happen to be aligned with the underlying
// grid. This means whenever some bounding box needs to be updated in the clipmap, it may actually
// translate to 4 different areas of the underlying texture - one for each quadrant.
//
// This class aids in figuring out which areas of a clipmap need to be updated as its center point
// moves around in the world, and can map a single region that needs to be updated into several
// separate regions for each quadrant.
/*
___________________________
| | | | | Clipmap Clipmap
| | | | | Bounds Texture (Tiled)
|______|______|______|______| ______ ______
| | _|____ | | | | | |____|_|
| | | | | | | |_|_*__| | | |
|______|____|_|_*__|_|______| |_|____| |_*__|_|
| | |_|____| | |
| | | | |
|______|______|______|______|
| | | | |
| | | | |
|______|______|______|______|
*/
class ClipmapBounds
{
public:
explicit ClipmapBounds(const ClipmapBoundsDescriptor& desc);
~ClipmapBounds() = default;
using ClipmapBoundsRegionList = AZStd::vector<ClipmapBoundsRegion>;
//! Updates the clipmap bounds using a world coordinate center position and returns
//! 0-2 regions that need to be updated due to moving beyond the margins. These update
//! regions will always be at least the size of the margin, and will represent horizontal
//! and/or vertical strips along the edges of the clipmap. An optional untouched region
//! aabb can be passed to this function to get an aabb of areas inside the bounds of the
//! clipmap but not updated by the center moving. This can be useful in cases where part
//! of the bounds of the clipmap is dirty, but areas that will already be updated due
//! to the center moving shouldn't be updated twice.
ClipmapBoundsRegionList UpdateCenter(const AZ::Vector2& newCenter, AZ::Aabb* untouchedRegion = nullptr);
//! Updates the clipmap bounds using a position in clipmap space (no scaling) and returns
//! 0-2 regions that need to be updated due to moving beyond the margins. These update
//! regions will always be at least the size of the margin, and will represent horizontal
//! and/or vertical strips along the edges of the clipmap. An optional untouched region
//! aabb can be passed to this function to get an aabb of areas inside the bounds of the
//! clipmap but not updated by the center moving. This can be useful in cases where part
//! of the bounds of the clipmap is dirty, but areas that will already be updated due
//! to the center moving shouldn't be updated twice.
ClipmapBoundsRegionList UpdateCenter(const Vector2i& newCenter, AZ::Aabb* untouchedRegion = nullptr);
//! Takes in a single world space region and transforms it into 0-4 regions in the clipmap clamped
//! to the bounds of the clipmap.
ClipmapBoundsRegionList TransformRegion(AZ::Aabb worldSpaceRegion);
//! Takes in a single unscaled clipmap space region and transforms it into 0-4 regions in the clipmap clamped
//! to the bounds of the clipmap.
ClipmapBoundsRegionList TransformRegion(Aabb2i clipSpaceRegion);
//! Returns the bounds covered by this clipmap in world space. Z component is always 0.
AZ::Aabb GetWorldBounds() const;
//! Returns the safe x and y distance from the center in world space. This is based on the scale,
//! clipmap size, and m_clipmapUpdateMultiple. For example, a clipmap size 1024 with scale
//! 0.25 and margin of 4 would have a safe distance of (1024 * 0.5 - 4) * 0.25 = 127.0f.
float GetWorldSpaceSafeDistance() const;
private:
//! Returns the center point snapped to a multiple of m_clipmapUpdateMultiple. This isn't
//! a simple rounding operation. The value returned will only be different from the curernt
//! center if the value passed in is greater than m_clipmapUpdateMultiple away from the center.
Vector2i GetSnappedCenter(const Vector2i& center);
//! Returns the bounds covered by the clipmap in local space
Aabb2i GetLocalBounds() const;
//! Applies scale and averages a world space vector to get a clip space vector.
Vector2i GetClipSpaceVector(const AZ::Vector2& worldSpaceVector) const;
//! Applies inverse scale to get a world aabb from clip space aabb.
AZ::Aabb GetWorldSpaceAabb(const Aabb2i& clipSpaceAabb) const;
Vector2i m_center;
Vector2i m_modCenter;
int32_t m_size;
int32_t m_halfSize;
int32_t m_clipmapUpdateMultiple;
float m_scale;
float m_rcpScale;
};
}
@@ -7,35 +7,90 @@
*/
#include <TerrainRenderer/Vector2i.h>
#include <AzCore/Casting/numeric_cast.h>
namespace Terrain
{
auto Vector2i::operator+(const Vector2i& rhs) const -> Vector2i
Vector2i::Vector2i(int32_t x, int32_t y)
: m_x(x)
, m_y(y)
{}
Vector2i::Vector2i(uint32_t value)
: m_x(aznumeric_cast<int32_t>(value))
, m_y(aznumeric_cast<int32_t>(value))
{}
Vector2i::Vector2i(int32_t value)
: m_x(value)
, m_y(value)
{}
Vector2i Vector2i::operator+(const Vector2i& rhs) const
{
Vector2i offsetPoint = *this;
offsetPoint += rhs;
return offsetPoint;
Vector2i returnPoint = *this;
returnPoint += rhs;
return returnPoint;
}
auto Vector2i::operator+=(const Vector2i& rhs) -> Vector2i&
Vector2i& Vector2i::operator+=(const Vector2i& rhs)
{
m_x += rhs.m_x;
m_y += rhs.m_y;
return *this;
}
auto Vector2i::operator-(const Vector2i& rhs) const -> Vector2i
Vector2i Vector2i::operator-(const Vector2i& rhs) const
{
return *this + -rhs;
}
auto Vector2i::operator-=(const Vector2i& rhs) -> Vector2i&
Vector2i& Vector2i::operator-=(const Vector2i& rhs)
{
return *this += -rhs;
}
auto Vector2i::operator-() const -> Vector2i
Vector2i Vector2i::operator-() const
{
return {-m_x, -m_y};
}
Vector2i Vector2i::operator*(const Vector2i& rhs) const
{
Vector2i returnPoint = *this;
returnPoint *= rhs;
return returnPoint;
}
Vector2i& Vector2i::operator*=(const Vector2i& rhs)
{
m_x *= rhs.m_x;
m_y *= rhs.m_y;
return *this;
}
Vector2i Vector2i::operator/(const Vector2i& rhs) const
{
Vector2i returnPoint = *this;
returnPoint /= rhs;
return returnPoint;
}
Vector2i& Vector2i::operator/=(const Vector2i& rhs)
{
m_x /= rhs.m_x;
m_y /= rhs.m_y;
return *this;
}
bool Vector2i::operator==(const Vector2i& rhs) const
{
return rhs.m_x == m_x && rhs.m_y == m_y;
}
bool Vector2i::operator!=(const Vector2i& rhs) const
{
return !(*this == rhs);
}
}
@@ -16,14 +16,26 @@ namespace Terrain
{
public:
Vector2i() = default;
Vector2i(int32_t x, int32_t y);
Vector2i(uint32_t value);
Vector2i(int32_t value);
Vector2i operator+(const Vector2i& rhs) const;
Vector2i& operator+=(const Vector2i& rhs);
Vector2i operator-(const Vector2i& rhs) const;
Vector2i& operator-=(const Vector2i& rhs);
Vector2i operator-() const;
Vector2i operator*(const Vector2i& rhs) const;
Vector2i& operator*=(const Vector2i& rhs);
Vector2i operator/(const Vector2i& rhs) const;
Vector2i& operator/=(const Vector2i& rhs);
bool operator==(const Vector2i& rhs) const;
bool operator!=(const Vector2i& rhs) const;
int32_t m_x{ 0 };
int32_t m_y{ 0 };
};
}
@@ -0,0 +1,336 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/UnitTest/TestTypes.h>
#include <gmock/gmock.h>
#include <TerrainRenderer/ClipmapBounds.h>
#include <TerrainRenderer/Aabb2i.h>
#include <AzCore/std/containers/span.h>
#include <AzCore/std/containers/vector.h>
namespace UnitTest
{
class ClipmapBoundsTests
: public UnitTest::AllocatorsTestFixture
{
public:
void CheckTransformRegionFullBounds(const Terrain::ClipmapBoundsDescriptor& desc);
};
void ClipmapBoundsTests::CheckTransformRegionFullBounds(const Terrain::ClipmapBoundsDescriptor& desc)
{
Terrain::ClipmapBounds bounds(desc);
AZ::Aabb worldBounds = bounds.GetWorldBounds();
float worldBoundsSize = worldBounds.GetXExtent();
auto output = bounds.TransformRegion(worldBounds);
ASSERT_EQ(output.size(), 4);
AZ::Vector2 boundary = AZ::Vector2(
floorf(worldBounds.GetMax().GetX() / worldBoundsSize),
floorf(worldBounds.GetMax().GetY() / worldBoundsSize)
) * worldBoundsSize;
Terrain::Vector2i localMax = {
aznumeric_cast<int32_t>(AZStd::lround(desc.m_worldSpaceCenter.GetX() / desc.m_clipToWorldScale)),
aznumeric_cast<int32_t>(AZStd::lround(desc.m_worldSpaceCenter.GetY() / desc.m_clipToWorldScale))
};
localMax += aznumeric_cast<int32_t>(desc.m_size / 2ul);
int32_t intSize = int32_t(desc.m_size);
Terrain::Vector2i localBoundary = {
((localMax.m_x % intSize) + intSize) % intSize,
((localMax.m_y % intSize) + intSize) % intSize
};
// Check each quadrant returned
AZStd::vector<Terrain::ClipmapBoundsRegion> expected;
expected.resize(4);
expected.at(0).m_localAabb = Terrain::Aabb2i({localBoundary.m_x, localBoundary.m_y}, {intSize, intSize});
expected.at(0).m_worldAabb = AZ::Aabb::CreateFromMinMaxValues(
worldBounds.GetMin().GetX(), worldBounds.GetMin().GetY(), 0.0f,
boundary.GetX(), boundary.GetY(), 0.0f);
expected.at(1).m_localAabb = Terrain::Aabb2i({0, localBoundary.m_y}, {localBoundary.m_x, intSize});
expected.at(1).m_worldAabb = AZ::Aabb::CreateFromMinMaxValues(
boundary.GetX(), worldBounds.GetMin().GetY(), 0.0f,
worldBounds.GetMax().GetX(), boundary.GetY(), 0.0f);
expected.at(2).m_localAabb = Terrain::Aabb2i({localBoundary.m_x, 0}, {intSize, localBoundary.m_y});
expected.at(2).m_worldAabb = AZ::Aabb::CreateFromMinMaxValues(
worldBounds.GetMin().GetX(), boundary.GetY(), 0.0f,
boundary.GetX(), worldBounds.GetMax().GetY(), 0.0f);
expected.at(3).m_localAabb = Terrain::Aabb2i({ 0, 0 }, { localBoundary.m_x, localBoundary.m_y });
expected.at(3).m_worldAabb = AZ::Aabb::CreateFromMinMaxValues(
boundary.GetX(), boundary.GetY(), 0.0f,
worldBounds.GetMax().GetX(), worldBounds.GetMax().GetY(), 0.0f);
EXPECT_THAT(output, ::testing::UnorderedElementsAreArray(expected));
}
TEST_F(ClipmapBoundsTests, Construction)
{
Terrain::ClipmapBoundsDescriptor desc;
Terrain::ClipmapBounds bounds(desc);
}
TEST_F(ClipmapBoundsTests, BasicTransform)
{
// Create clipmap around 0.0, so it's perfectly divided into 4 quadrants
Terrain::ClipmapBoundsDescriptor desc;
desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f);
desc.m_clipmapUpdateMultiple = 0;
desc.m_clipToWorldScale = 1.0f;
desc.m_size = 1024;
Terrain::ClipmapBounds bounds(desc);
auto output = bounds.TransformRegion(AZ::Aabb::CreateFromMinMaxValues(-512.0f, -512.0f, 0.0f, 512.0f, 512.0f, 0.0f));
ASSERT_EQ(output.size(), 4);
// Check each quadrant returned
EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({512, 512}, {1024, 1024}));
EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-512.0f, -512.0f, 0.0f, 0.0f, 0.0f, 0.0f)));
EXPECT_EQ(output.at(1).m_localAabb, Terrain::Aabb2i({0, 512}, {512, 1024}));
EXPECT_TRUE(output.at(1).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(0.0f, -512.0f, 0.0f, 512.0f, 0.0f, 0.0f)));
EXPECT_EQ(output.at(2).m_localAabb, Terrain::Aabb2i({512, 0}, {1024, 512}));
EXPECT_TRUE(output.at(2).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-512.0f, 0.0f, 0.0f, 0.0f, 512.0f, 0.0f)));
EXPECT_EQ(output.at(3).m_localAabb, Terrain::Aabb2i({0, 0}, {512, 512}));
EXPECT_TRUE(output.at(3).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 0.0f, 512.0f, 512.0f, 0.0f)));
}
TEST_F(ClipmapBoundsTests, ScaledTransform)
{
// Create clipmap around 0.0, so it's perfectly divided into 4 quadrants, but half-scale
Terrain::ClipmapBoundsDescriptor desc;
desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f);
desc.m_clipmapUpdateMultiple = 0;
desc.m_clipToWorldScale = 0.5f;
desc.m_size = 1024;
Terrain::ClipmapBounds bounds(desc);
auto output = bounds.TransformRegion(AZ::Aabb::CreateFromMinMaxValues(-256.0f, -256.0f, 0.0f, 256.0f, 256.0f, 0.0f));
ASSERT_EQ(output.size(), 4);
// Check each quadrant returned
EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({512, 512}, {1024, 1024}));
EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-256.0f, -256.0f, 0.0f, 0.0f, 0.0f, 0.0f)));
EXPECT_EQ(output.at(1).m_localAabb, Terrain::Aabb2i({0, 512}, {512, 1024}));
EXPECT_TRUE(output.at(1).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(0.0f, -256.0f, 0.0f, 256.0f, 0.0f, 0.0f)));
EXPECT_EQ(output.at(2).m_localAabb, Terrain::Aabb2i({512, 0}, {1024, 512}));
EXPECT_TRUE(output.at(2).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-256.0f, 0.0f, 0.0f, 0.0f, 256.0f, 0.0f)));
EXPECT_EQ(output.at(3).m_localAabb, Terrain::Aabb2i({0, 0}, {512, 512}));
EXPECT_TRUE(output.at(3).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 0.0f, 256.0f, 256.0f, 0.0f)));
}
TEST_F(ClipmapBoundsTests, ComplexTransformsFullBounds)
{
// Check 4 different clipmaps - one in completely positive space, one in negative space, and two straddling the axis
// Clipmap in negative space
{
Terrain::ClipmapBoundsDescriptor desc;
desc.m_worldSpaceCenter = AZ::Vector2(-1234.0f, -5432.0f);
desc.m_clipmapUpdateMultiple = 0;
desc.m_clipToWorldScale = 0.75f;
desc.m_size = 512;
CheckTransformRegionFullBounds(desc);
}
// Clipmap in positive space
{
Terrain::ClipmapBoundsDescriptor desc;
desc.m_worldSpaceCenter = AZ::Vector2(1234.0f, 5432.0f);
desc.m_clipmapUpdateMultiple = 0;
desc.m_clipToWorldScale = 1.25f;
desc.m_size = 1024;
CheckTransformRegionFullBounds(desc);
}
// Clipmap on x axis
{
Terrain::ClipmapBoundsDescriptor desc;
desc.m_worldSpaceCenter = AZ::Vector2(1234.0f, -100.0f);
desc.m_clipmapUpdateMultiple = 0;
desc.m_clipToWorldScale = 1.5f;
desc.m_size = 256;
CheckTransformRegionFullBounds(desc);
}
// Clipmap on y axis
{
Terrain::ClipmapBoundsDescriptor desc;
desc.m_worldSpaceCenter = AZ::Vector2(-100.0f, 5432.0f);
desc.m_clipmapUpdateMultiple = 0;
desc.m_clipToWorldScale = 1.0f;
desc.m_size = 2048;
CheckTransformRegionFullBounds(desc);
}
}
TEST_F(ClipmapBoundsTests, TransformSmallBounds)
{
// Create clipmap around 0.0, so it's perfectly divided into 4 quadrants
Terrain::ClipmapBoundsDescriptor desc;
desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f);
desc.m_clipmapUpdateMultiple = 0;
desc.m_clipToWorldScale = 1.0f;
desc.m_size = 1024;
Terrain::ClipmapBounds bounds(desc);
{
// Single quadrant positive
AZ::Aabb smallArea = AZ::Aabb::CreateFromMinMaxValues(
10.0f, 10.0f, 0.0f, 50.0f, 50.0f, 0.0f
);
auto output = bounds.TransformRegion(smallArea);
ASSERT_EQ(output.size(), 1);
EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({10, 10}, {50, 50}));
EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(10.0f, 10.0f, 0.0f, 50.0f, 50.0f, 0.0f)));
}
{
// Single quadrant negative
AZ::Aabb smallArea = AZ::Aabb::CreateFromMinMaxValues(
-50.0f, -50.0f, 0.0f, -10.0f, -10.0f, 0.0f
);
auto output = bounds.TransformRegion(smallArea);
ASSERT_EQ(output.size(), 1);
EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({974, 974}, {1014, 1014}));
EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(-50.0f, -50.0f, 0.0f, -10.0f, -10.0f, 0.0f)));
}
{
// 2 quadrant positive
AZ::Aabb smallArea = AZ::Aabb::CreateFromMinMaxValues(
10.0f, -10.0f, 0.0f, 50.0f, 50.0f, 0.0f
);
auto output = bounds.TransformRegion(smallArea);
ASSERT_EQ(output.size(), 2);
EXPECT_EQ(output.at(0).m_localAabb, Terrain::Aabb2i({10, 1014}, {50, 1024}));
EXPECT_TRUE(output.at(0).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(10.0f, -10.0f, 0.0f, 50.0f, 0.0f, 0.0f)));
EXPECT_EQ(output.at(1).m_localAabb, Terrain::Aabb2i({10, 0}, {50, 50}));
EXPECT_TRUE(output.at(1).m_worldAabb.IsClose(AZ::Aabb::CreateFromMinMaxValues(10.0f, 0.0f, 0.0f, 50.0f, 50.0f, 0.0f)));
}
}
TEST_F(ClipmapBoundsTests, MarginReducesUpdates)
{
// With a margin defined, the bounds should only trigger updates when the camera moves outside the margins
// Create clipmap around 0.0, so it's perfectly divided into 4 quadrants
Terrain::ClipmapBoundsDescriptor desc;
desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f);
desc.m_clipmapUpdateMultiple = 16;
desc.m_clipToWorldScale = 1.0f;
desc.m_size = 1024;
Terrain::ClipmapBounds bounds(desc);
// center moved forward to 10, still within margin
auto output1 = bounds.UpdateCenter(AZ::Vector2(10.0f, 10.0f));
EXPECT_EQ(output1.size(), 0);
// center moved forwrd to 20, beyond margin, triggers update
auto output2 = bounds.UpdateCenter(AZ::Vector2(20.0f, 20.0f));
EXPECT_GT(output2.size(), 0);
// center moved back to 10, still within margin
auto output3 = bounds.UpdateCenter(AZ::Vector2(10.0f, 10.0f));
EXPECT_EQ(output3.size(), 0);
// center moved back to 0, still within margin (on edge)
auto output4 = bounds.UpdateCenter(AZ::Vector2(0.0f, 0.0f));
EXPECT_EQ(output4.size(), 0);
// center moved back to -10, beyond margin, triggers update
auto output5 = bounds.UpdateCenter(AZ::Vector2(-10.0f, -10.0f));
EXPECT_GT(output5.size(), 0);
}
TEST_F(ClipmapBoundsTests, CenterMovementUpdates)
{
// Create clipmap around 0.0, so it's perfectly divided into 4 quadrants
Terrain::ClipmapBoundsDescriptor desc;
desc.m_worldSpaceCenter = AZ::Vector2(0.0f, 0.0f);
desc.m_clipmapUpdateMultiple = 16;
desc.m_clipToWorldScale = 1.0f;
desc.m_size = 1024;
Terrain::ClipmapBounds bounds(desc);
{
AZ::Aabb untouchedRegion = AZ::Aabb::CreateNull();
auto output = bounds.UpdateCenter(AZ::Vector2(20.0f, 20.0f), &untouchedRegion);
ASSERT_EQ(output.size(), 4);
// Instead of checking bounds directly, do several checks to make sure the bounds are appropriate. Since
// the center moved just outside the margin along the diagonal, we should expect two edges to be updated
// that are the width of the margin.
// 1. The number of pixels updated in the bounds should be two sides of margin width
float pixelsCovered = 0;
for (auto& region : output)
{
// Note: GetSurfaceArea() returns the area of all 6 sides of the aabb. With a Z extent of 0, that
// means that only the top and bottom will be counted, so we need to multiply by 0.5.
pixelsCovered += region.m_worldAabb.GetSurfaceArea() * 0.5f;
}
// Two edges of margin * size, minus the overlap in the corner.
const uint32_t updateMultiple = desc.m_clipmapUpdateMultiple;
float expectedCoverage = updateMultiple * desc.m_size * 2.0f - updateMultiple * updateMultiple;
EXPECT_NEAR(pixelsCovered, expectedCoverage, 0.0001f);
// 2. The untouched region area should match what's expected
float untouchedRegionArea = untouchedRegion.GetSurfaceArea() * 0.5f;
float expectedUntouchedRegionSide = aznumeric_cast<float>(desc.m_size - desc.m_clipmapUpdateMultiple);
float expectedUntouchedRegionArea = expectedUntouchedRegionSide * expectedUntouchedRegionSide;
EXPECT_NEAR(untouchedRegionArea, expectedUntouchedRegionArea, 0.0001f);
// 3. All of the update regions should be inside the world bounds of the clipmap
AZ::Aabb worldBounds = bounds.GetWorldBounds();
for (auto& region : output)
{
EXPECT_EQ(region.m_worldAabb.GetClamped(worldBounds), region.m_worldAabb);
}
// 4. The untouched region should also be inside the world bounds of the clipmap;
EXPECT_EQ(untouchedRegion.GetClamped(worldBounds), untouchedRegion);
// 5. None of the update regions should overlap each other or the untouched region
// push the untouched region on the vector to make comparisons easier
output.push_back(Terrain::ClipmapBoundsRegion({untouchedRegion, Terrain::Aabb2i({}) }));
for (uint32_t i = 0; i < output.size(); ++i)
{
const AZ::Aabb boundsToCheck = output.at(i).m_worldAabb;
for (uint32_t j = i + 1; j < output.size(); ++j)
{
// AZ::Aabb::Overlaps() counts touching edges as overlapping, so we need a strict version
auto strictOverlaps = [](const AZ::Aabb& aabb1, const AZ::Aabb& aabb2) -> bool
{
return aabb1.GetMin().IsLessThan(aabb2.GetMax()) &&
aabb1.GetMax().IsGreaterThan(aabb2.GetMin());
};
EXPECT_FALSE(strictOverlaps(boundsToCheck, output.at(j).m_worldAabb));
}
}
}
}
}
+4 -2
View File
@@ -35,6 +35,10 @@ set(FILES
Source/TerrainRenderer/Components/TerrainMacroMaterialComponent.h
Source/TerrainRenderer/Aabb2i.cpp
Source/TerrainRenderer/Aabb2i.h
Source/TerrainRenderer/BindlessImageArrayHandler.cpp
Source/TerrainRenderer/BindlessImageArrayHandler.h
Source/TerrainRenderer/ClipmapBounds.cpp
Source/TerrainRenderer/ClipmapBounds.h
Source/TerrainRenderer/TerrainFeatureProcessor.cpp
Source/TerrainRenderer/TerrainFeatureProcessor.h
Source/TerrainRenderer/TerrainDetailMaterialManager.cpp
@@ -43,8 +47,6 @@ set(FILES
Source/TerrainRenderer/TerrainMacroMaterialManager.h
Source/TerrainRenderer/TerrainMeshManager.cpp
Source/TerrainRenderer/TerrainMeshManager.h
Source/TerrainRenderer/BindlessImageArrayHandler.cpp
Source/TerrainRenderer/BindlessImageArrayHandler.h
Source/TerrainRenderer/TerrainAreaMaterialRequestBus.h
Source/TerrainRenderer/TerrainMacroMaterialBus.cpp
Source/TerrainRenderer/TerrainMacroMaterialBus.h
+5 -4
View File
@@ -7,14 +7,15 @@
#
set(FILES
Tests/TerrainTest.cpp
Tests/TerrainSystemTest.cpp
Tests/ClipmapBoundsTests.cpp
Tests/LayerSpawnerTests.cpp
Tests/TerrainPhysicsColliderTests.cpp
Tests/SurfaceMaterialsListTest.cpp
Tests/MockAxisAlignedBoxShapeComponent.h
Tests/TerrainHeightGradientListTests.cpp
Tests/TerrainMacroMaterialTests.cpp
Tests/SurfaceMaterialsListTest.cpp
Tests/TerrainPhysicsColliderTests.cpp
Tests/TerrainSurfaceGradientListTests.cpp
Tests/TerrainSystemBenchmarks.cpp
Tests/TerrainSystemTest.cpp
Tests/TerrainTest.cpp
)
+30 -2
View File
@@ -9,6 +9,7 @@
import groovy.json.JsonOutput
PIPELINE_CONFIG_FILE = 'scripts/build/Jenkins/lumberyard.json'
INCREMENTAL_BUILD_SCRIPT_PATH = 'scripts/build/bootstrap/incremental_build_util.py'
EBS_SNAPSHOT_SCRIPT_PATH = 'scripts/build/tools/ebs_snapshot.py'
PIPELINE_RETRY_ATTEMPTS = 3
EMPTY_JSON = readJSON text: '{}'
@@ -206,7 +207,8 @@ def CheckoutBootstrapScripts(String branchName) {
[$class: 'SparseCheckoutPaths', sparseCheckoutPaths: [
[ $class: 'SparseCheckoutPath', path: 'scripts/build/Jenkins/' ],
[ $class: 'SparseCheckoutPath', path: 'scripts/build/bootstrap/' ],
[ $class: 'SparseCheckoutPath', path: 'scripts/build/Platform' ]
[ $class: 'SparseCheckoutPath', path: 'scripts/build/Platform' ],
[ $class: 'SparseCheckoutPath', path: 'scripts/build/tools/' ]
]],
// Shallow checkouts break changelog computation. Do not enable.
[$class: 'CloneOption', noTags: false, reference: '', shallow: false]
@@ -495,6 +497,19 @@ def PostBuildCommonSteps(String workspace, Map params, boolean mount = true) {
}
}
def HandleDriveSnapshots(String repositoryName, String projectName, String pipeline, String branchName, String platform, String buildType) {
unstash name: 'ebs_snapshot_script'
catchError(message: "Error snapshotting volume (this won't fail the build)", buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
def pythonCmd = 'python3 -u '
mountName = "Name:${repositoryName}_${projectName}_${pipeline}_${branchName}_${platform}_${buildType}"
mountName = mountName.replace('/', '_').replace('\\', '_')
palSh("${pythonCmd} ${EBS_SNAPSHOT_SCRIPT_PATH} --action create --tags ${mountName} --execute", "Starting volume snapshots", true)
palSh("${pythonCmd} ${EBS_SNAPSHOT_SCRIPT_PATH} --action delete --tags ${mountName} --retention ${env.SNAP_RETENTION} --execute", "Cleaning up old snapshots", true)
}
}
def CreateSetupStage(Map pipelineConfig, String snapshot, String repositoryName, String projectName, String pipelineName, String branchName, String platformName, String jobName, Map environmentVars, boolean onlyMountEBSVolume = false) {
return {
stage('Setup') {
@@ -564,6 +579,14 @@ def CreateTeardownStage(Map environmentVars, Map params) {
}
}
def CreateSnapshotStage(String repositoryName, String projectName, String pipelineName, String branchName, String platformName, String buildType, String jobName) {
return{
stage("${jobName}_snapshot_ebs_volume") {
HandleDriveSnapshots(repositoryName, projectName, pipelineName, branchName, platformName, buildType)
}
}
}
def CreateSingleNode(Map pipelineConfig, def platform, def build_job, Map envVars, String branchName, String pipelineName, String repositoryName, String projectName, boolean onlyMountEBSVolume = false) {
def nodeLabel = envVars['NODE_LABEL']
return {
@@ -632,6 +655,9 @@ def CreateSingleNode(Map pipelineConfig, def platform, def build_job, Map envVar
CreateExportTestScreenshotsStage(pipelineConfig, branchName, platform.key, build_job_name, envVars, params).call()
}
CreateTeardownStage(envVars, params).call()
if (envVars['CREATE_SNAPSHOT']?.toBoolean()) {
CreateSnapshotStage(repositoryName, projectName, pipelineName, branchName, platform.key, build_job.key, build_job_name).call()
}
}
}
}
@@ -816,9 +842,11 @@ try {
pipelineProperties.add(parameters(pipelineParameters.unique()))
properties(pipelineProperties)
// Stash the INCREMENTAL_BUILD_SCRIPT_PATH since all nodes will use it
// Stash the INCREMENTAL_BUILD_SCRIPT_PATH and EBS_SNAPSHOT_SCRIPT_PATH since all nodes will use it
stash name: 'incremental_build_script',
includes: INCREMENTAL_BUILD_SCRIPT_PATH
stash name: 'ebs_snapshot_script',
includes: EBS_SNAPSHOT_SCRIPT_PATH
}
}
}
@@ -9,7 +9,8 @@
},
"profile_pipe": {
"TAGS": [
"default"
"default",
"snapshot"
],
"steps": [
"profile"
@@ -77,7 +78,8 @@
"default",
"weekly-build-metrics",
"nightly-incremental",
"nightly-clean"
"nightly-clean",
"snapshot"
],
"COMMAND":"../Windows/build_asset_windows.cmd",
"PARAMETERS": {
@@ -127,7 +129,8 @@
"gradle": {
"TAGS":[
"default",
"weekly-build-metrics"
"weekly-build-metrics",
"snapshot"
],
"COMMAND":"gradle_windows.cmd",
"PARAMETERS": {
+5 -1
View File
@@ -14,6 +14,10 @@
},
"nightly-clean": {
"CLEAN_WORKSPACE": true
},
"snapshot": {
"CLEAN_WORKSPACE": true,
"CREATE_SNAPSHOT": true
}
}
}
}
@@ -9,7 +9,8 @@
},
"profile_nounity_pipe": {
"TAGS": [
"default"
"default",
"snapshot"
],
"steps": [
"profile_nounity",
@@ -12,6 +12,10 @@
},
"nightly-clean": {
"CLEAN_WORKSPACE": true
},
"snapshot": {
"CLEAN_WORKSPACE": true,
"CREATE_SNAPSHOT": true
}
},
"PIPELINE_JENKINS_PARAMETERS": {
@@ -9,7 +9,8 @@
},
"validation_pipe": {
"TAGS": [
"default"
"default",
"snapshot"
],
"steps": [
"validation"
@@ -27,7 +28,8 @@
},
"profile_pipe": {
"TAGS": [
"default"
"default",
"snapshot"
],
"steps": [
"profile",
@@ -288,7 +290,8 @@
"default",
"nightly-incremental",
"nightly-clean",
"weekly-build-metrics"
"weekly-build-metrics",
"snapshot"
],
"COMMAND": "build_windows.cmd",
"PARAMETERS": {
+5 -1
View File
@@ -12,6 +12,10 @@
},
"nightly-clean": {
"CLEAN_WORKSPACE": true
},
"snapshot": {
"CLEAN_WORKSPACE": true,
"CREATE_SNAPSHOT": true
}
},
"PIPELINE_JENKINS_PARAMETERS": {
@@ -62,4 +66,4 @@
}
]
}
}
}
@@ -40,9 +40,9 @@ idna==2.10 \
--hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \
# via requests
jinja2==2.11.2 \
--hash=sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0 \
--hash=sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035 \
jinja2==2.11.3 \
--hash=sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419 \
--hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6 \
# via -r requirements.txt
jmespath==0.10.0 \
--hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \
@@ -131,19 +131,17 @@ pytz==2020.4 \
--hash=sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268 \
--hash=sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd \
# via -r requirements.txt
pywin32==228 \
--hash=sha256:00eaf43dbd05ba6a9b0080c77e161e0b7a601f9a3f660727a952e40140537de7 \
--hash=sha256:11cb6610efc2f078c9e6d8f5d0f957620c333f4b23466931a247fb945ed35e89 \
--hash=sha256:1f45db18af5d36195447b2cffacd182fe2d296849ba0aecdab24d3852fbf3f80 \
--hash=sha256:37dc9935f6a383cc744315ae0c2882ba1768d9b06700a70f35dc1ce73cd4ba9c \
--hash=sha256:6e38c44097a834a4707c1b63efa9c2435f5a42afabff634a17f563bc478dfcc8 \
--hash=sha256:8319bafdcd90b7202c50d6014efdfe4fde9311b3ff15fd6f893a45c0868de203 \
--hash=sha256:9b3466083f8271e1a5eb0329f4e0d61925d46b40b195a33413e0905dccb285e8 \
--hash=sha256:a60d795c6590a5b6baeacd16c583d91cce8038f959bd80c53bd9a68f40130f2d \
--hash=sha256:af40887b6fc200eafe4d7742c48417529a8702dcc1a60bf89eee152d1d11209f \
--hash=sha256:ec16d44b49b5f34e99eb97cf270806fdc560dff6f84d281eb2fcb89a014a56a9 \
--hash=sha256:ed74b72d8059a6606f64842e7917aeee99159ebd6b8d6261c518d002837be298 \
--hash=sha256:fa6ba028909cfc64ce9e24bcf22f588b14871980d9787f1e2002c99af8f1850c \
pywin32==301 \
--hash=sha256:93367c96e3a76dfe5003d8291ae16454ca7d84bb24d721e0b74a07610b7be4a7 \
--hash=sha256:9635df6998a70282bd36e7ac2a5cef9ead1627b0a63b17c731312c7a0daebb72 \
--hash=sha256:c866f04a182a8cb9b7855de065113bbd2e40524f570db73ef1ee99ff0a5cc2f0 \
--hash=sha256:dafa18e95bf2a92f298fe9c582b0e205aca45c55f989937c52c454ce65b93c78 \
--hash=sha256:98f62a3f60aa64894a290fb7494bfa0bfa0a199e9e052e1ac293b2ad3cd2818b \
--hash=sha256:fb3b4933e0382ba49305cc6cd3fb18525df7fd96aa434de19ce0878133bf8e4a \
--hash=sha256:88981dd3cfb07432625b180f49bf4e179fb8cbb5704cd512e38dd63636af7a17 \
--hash=sha256:8c9d33968aa7fcddf44e47750e18f3d034c3e443a707688a008a2e52bbef7e96 \
--hash=sha256:595d397df65f1b2e0beaca63a883ae6d8b6df1cdea85c16ae85f6d2e648133fe \
--hash=sha256:87604a4087434cd814ad8973bd47d6524bd1fa9e971ce428e76b62a5e0860fdf \
# via -r requirements.txt
pyxb==1.2.6 \
--hash=sha256:2a00f38dd1d87b88f92d79bc5a09718d730419b88e814545f472bbd5a3bf27b4 \
@@ -175,9 +173,9 @@ requests==2.25.0 \
--hash=sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8 \
--hash=sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998 \
# via -r requirements.txt
rsa==4.5 \
--hash=sha256:35c5b5f6675ac02120036d97cf96f1fde4d49670543db2822ba5015e21a18032 \
--hash=sha256:4d409f5a7d78530a4a2062574c7bd80311bc3af29b364e293aa9b03eea77714f \
rsa==4.7 \
--hash=sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b \
--hash=sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4 \
# via -r requirements.txt
s3transfer==0.3.3 \
--hash=sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13 \
+197
View File
@@ -0,0 +1,197 @@
#
# Copyright (c) Contributors to the Open 3D Engine Project.
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
import argparse
import boto3
import logging
import sys
from botocore.config import Config
log = logging.getLogger(__name__)
log.setLevel(logging.INFO)
log.addHandler(logging.StreamHandler())
DEFAULT_REGION = 'us-west-2'
DEFAULT_SNAPSHOT_RETAIN = 2
DEFAULT_SNAPSHOT_DESCRIPTION = 'Created for Build Artifact Snapshots'
DEFAULT_DRYRUN = True
def _kv_to_dict(kv_string):
"""
Simple splitting of a key value string to dictionary in "Name: <Key>, Values: [<value>]" form
:param kv_string: String in the form of "key:value"
:return Dictionary of values
"""
dict = {}
if ":" not in kv_string:
log.error(f'Keyvalue parameter not in the form of "key:value"')
raise ValueError
kv = kv_string.split(':')
dict['Name'] = f'tag:{kv[0]}'
dict['Values'] = [kv[1]]
return dict
def _format_tags(tag_keyvalue):
"""
Format tags in list form
:param tag_keyvalue: String of comma separated key value pairs
:return List of dictionary values
"""
tag_filter = []
for keyvalue in tag_keyvalue:
tag_filter.append(_kv_to_dict(keyvalue))
return tag_filter
def get_ec2_resource():
"""
Get the AWS EC2 resource object, with appropriate region
:return The EC2 resource object
"""
session = boto3.session.Session()
region = session.region_name
if region is None:
region = DEFAULT_REGION
resource_config = Config(
region_name=region,
retries={
'mode': 'standard'
}
)
resource = boto3.resource('ec2', config=resource_config)
return resource
def create_snapshot(ec2_resource, tag_keyvalue, snap_description, snap_dryrun=DEFAULT_DRYRUN):
"""
Find and snapshot all EBS volumes that have a matching tag value. Injects all volume tags into the snapshot,
including the name and adds a description
:param ec2_resource: The EC2 resource object
:param tag_keyvalue: List of Strings with tag keyvalues in the form of "key:value"
:param snap_description: String with the snapshot description to write
:param snap_dryrun: Boolean to dryrun the action. Set to true by default (always dryrun)
:return: Number of EBS volumes that are snapshotted successfully, number of EBS volumes that failed to be snapshotted
"""
success = 0
failure = 0
tags = _format_tags(tag_keyvalue)
for tag in tags:
response = ec2_resource.volumes.filter(Filters=[tag])
log.info(f'Snapshotting EBS volumes with tags that match {tag}...')
for volume in response:
try:
log.info(f'Snapshotting volume {volume.volume_id}')
volume.create_snapshot(Description=snap_description, TagSpecifications=[{'ResourceType': 'snapshot', 'Tags': volume.tags}], DryRun=snap_dryrun)
success += 1
except Exception as e:
log.error(f'Failed to snapshot volume {volume.volume_id}.')
log.error(e)
failure += 1
return success, failure
def delete_snapshot(ec2_resource, tag_keyvalue, snap_description, snap_retention, snap_dryrun=DEFAULT_DRYRUN):
"""
Find all EBS snapshots that have a matching tag value AND description. If the number of snapshots exceeds a retention amount,
delete the oldest snapshot until retention is achived.
:param ec2_resource: The EC2 resource object
:param tag_keyvalue: List of Strings with tag keyvalues in the form of "key:value"
:param snap_description: String with the snapshot description to search
:param snap_retention: Integer with the number of snapshots to retain
:param snap_dryrun: Boolean to dryrun the action. Set to true by default (always dryrun)
:return: Number of EBS snapshots deleted successfully, number of EBS snapshots that failed to be deleted
"""
success = 0
failure = 0
description_filter = {"Name": "description", "Values": [snap_description]}
tags = _format_tags(tag_keyvalue)
for tag in tags:
response = list(ec2_resource.snapshots.filter(Filters=[tag,description_filter]))
log.info(f'Getting snapshots with tags that match {tag}...')
num_snaps = len(response)
log.info(f'Tag {tag} has {num_snaps} snapshots')
if num_snaps > snap_retention:
log.info(f'Deleting oldest snapshots to keep retention of {snap_retention}')
snap_list = sorted(response, key=lambda k: k.start_time) # Get a sorted list of snapshots by start time in descending order
diff_snap = num_snaps - snap_retention
for n in range(diff_snap):
try:
log.info(f'Deleting snapshot {snap_list[n].snapshot_id}')
snap_list[n].delete(DryRun=snap_dryrun)
success += 1
except Exception as e:
log.error(f'Failed to delete snapshot {snap_list[n].snapshot_id}.')
log.error(e)
failure += 1
return success, failure
def list_snapshot(ec2_resource, tag_keyvalue, snap_description):
"""
Find all EBS snapshots that have a matching tag value AND description. Prints snap id, description, tags, and start time.
:param ec2_resource: The EC2 resource object
:param tag_keyvalue: List of Strings with tag keyvalues in the form of "key:value"
:param snap_description: String with the snapshot description to search
:return: None
"""
description_filter = {"Name": "description", "Values": [snap_description]}
tags = _format_tags(tag_keyvalue)
for tag in tags:
response = ec2_resource.snapshots.filter(Filters=[tag,description_filter])
log.info(f'Getting snapshots with tags that match {tag}...')
num_snaps = len(list(response))
log.info(f'Tag {tag} has {num_snaps} snapshots')
snap_list = sorted(response, key=lambda k: k.start_time)
for n in range(num_snaps):
print(f'Snap ID: {snap_list[n].snapshot_id} \n Description: {snap_list[n].description} \n Tags: {snap_list[n].tags} \n Start Time: {snap_list[n].start_time}')
return None
def parse_args():
parser = argparse.ArgumentParser(description='Script to manage EBS snapshots for build artifacts')
parser.add_argument('--action', '-a', type=str, help='(create|delete|list) Creates, deletes, or lists EBS snapshots based on tag. Requires --tags argument')
parser.add_argument('--tags', '-t', type=str, required=True, help='Comma separated key value tags to search for in the form of "key:value", for example, "PipelineAndBranch:default_development","PipelineAndBranch:default_development"')
parser.add_argument('--description', '-d', default=DEFAULT_SNAPSHOT_DESCRIPTION, help=f'Snapshot description to write or search for. Defaults to "{DEFAULT_SNAPSHOT_DESCRIPTION}"')
parser.add_argument('--retention', '-r', nargs="?", const=DEFAULT_SNAPSHOT_RETAIN, type=int, help=f'Integer with the number of snapshots to retain. Defaults to {DEFAULT_SNAPSHOT_RETAIN}')
parser.add_argument('--execute', '-e', action='store_false', help=f'Execute the snapshot commands. This needs to be set, otherwise it will always dryrun')
return parser.parse_args()
def main():
args = parse_args()
tag_list = args.tags.split(",")
ec2_resource = get_ec2_resource()
if 'create' in args.action:
ret = create_snapshot(ec2_resource, tag_list, args.description, args.execute)
log.info(f'{ret[0]} snapshots created, {ret[1]} snapshots failed')
elif 'delete' in args.action:
ret = delete_snapshot(ec2_resource, tag_list, args.description, args.retention, args.execute)
log.info(f'{ret[0]} snapshots deleted, {ret[1]} snapshot deletions failed')
elif 'list' in args.action:
ret = list_snapshot(ec2_resource, tag_list, args.description)
if __name__ == "__main__":
sys.exit(main())