From 489c7128fd8c8a73d840b6f5db9bcb8e8c75d09b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sat, 10 Feb 2024 22:40:22 +0100 Subject: [PATCH] Math: introduce Nanoseconds and Seconds types. Like the Deg / Rad classes, these are for strongly-typed representation of time. Because the current way, either with untyped and imprecise Float, or the insanely-hard-to-use and bloated std::chrono::nanoseconds, was just too crappy. This is just the types alone, corresponding typedefs in the root namespace, and conversion from std::chrono. Using these in the Animation library, in Timeline, in DebugTools::FrameProfiler, GL::TimeQuery etc., will eventually and gradually follow. --- doc/changelog.dox | 3 + doc/namespaces.dox | 16 +- doc/snippets/MagnumMath-stl.cpp | 67 ++++ doc/snippets/MagnumMath.cpp | 75 +++++ doc/types.dox | 32 +- src/Magnum/CMakeLists.txt | 1 + src/Magnum/Magnum.h | 12 + src/Magnum/Math/CMakeLists.txt | 2 + src/Magnum/Math/Math.h | 2 + src/Magnum/Math/Test/CMakeLists.txt | 2 + src/Magnum/Math/Test/TimeStlTest.cpp | 246 +++++++++++++++ src/Magnum/Math/Test/TimeTest.cpp | 442 +++++++++++++++++++++++++++ src/Magnum/Math/Time.cpp | 44 +++ src/Magnum/Math/Time.h | 398 ++++++++++++++++++++++++ src/Magnum/Math/TimeStl.h | 87 ++++++ 15 files changed, 1420 insertions(+), 9 deletions(-) create mode 100644 src/Magnum/Math/Test/TimeStlTest.cpp create mode 100644 src/Magnum/Math/Test/TimeTest.cpp create mode 100644 src/Magnum/Math/Time.cpp create mode 100644 src/Magnum/Math/Time.h create mode 100644 src/Magnum/Math/TimeStl.h diff --git a/doc/changelog.dox b/doc/changelog.dox index c682ce24f..d36083fbf 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -76,6 +76,7 @@ See also: typedefs for half-float angles and ranges - New @ref Range1Dui, @ref Range2Dui and @ref Range3Dui typedefs for unsigned integer ranges +- New @ref Nanoseconds and @ref Seconds typedefs for time values - Added MSVC Natvis files and pretty-printers for GDB. See @ref debuggers, [mosra/magnum#589](https://github.com/mosra/magnum/pull/589), [mosra/magnum#595](https://github.com/mosra/magnum/pull/595), @@ -248,6 +249,8 @@ See also: mostly useless in practice - New @ref Magnum/Math/ColorBatch.h header with utilities for performing Y flip of various block-compressed formats +- New @ref Math::Nanoseconds and @ref Math::Seconds classes for strongly + typed representation of time values @subsubsection changelog-latest-new-materialtools MaterialTools library diff --git a/doc/namespaces.dox b/doc/namespaces.dox index 6175557c9..363a62ea9 100644 --- a/doc/namespaces.dox +++ b/doc/namespaces.dox @@ -124,10 +124,11 @@ more information. /** @namespace Magnum::Math::Literals @brief Math literals -Literals for easy construction of angle, color and other values. The namespace -is further split to prevent potential ambiguity and conflicts with literals -defined by other code, but the second namespace level is @cpp inline @ce so to -get for example the color literals you can do either of these two: +Literals for easy construction of angle, color, time and other values. The +namespace is further split to prevent potential ambiguity and conflicts with +literals defined by other code, but the second namespace level is +@cpp inline @ce so to get for example the color literals you can do either of +these two: @snippet MagnumMath.cpp Literals-using @@ -171,6 +172,13 @@ more information. See the @ref Literals namespace and the @ref Half class for more information. */ +/** @namespace Magnum::Math::Literals::TimeLiterals +@brief Math time literals +@m_since_latest + +See the @ref Literals namespace and the @ref Nanoseconds and @ref Seconds +classes for more information. +*/ /** @dir Magnum/Math/Algorithms * @brief Namespace @ref Magnum::Math::Algorithms diff --git a/doc/snippets/MagnumMath-stl.cpp b/doc/snippets/MagnumMath-stl.cpp index 2098ecb19..46d07af36 100644 --- a/doc/snippets/MagnumMath-stl.cpp +++ b/doc/snippets/MagnumMath-stl.cpp @@ -23,10 +23,13 @@ DEALINGS IN THE SOFTWARE. */ +#include +#include #include #include #include "Magnum/Math/StrictWeakOrdering.h" +#include "Magnum/Math/TimeStl.h" #include "Magnum/Math/Vector4.h" #define DOXYGEN_ELLIPSIS(...) __VA_ARGS__ @@ -36,6 +39,25 @@ using namespace Magnum; using namespace Magnum::Math::Literals; int main() { +{ +Nanoseconds previousFrameTime; +/* The (void) is to avoid -Wvexing-parse */ +void stillCanDoSomething(void); +/* The include is already above, so doing it again here should be harmless */ +/* [types-time] */ +#include + +DOXYGEN_ELLIPSIS() + +using namespace Math::Literals; + +Nanoseconds currentFrameTime{std::chrono::steady_clock::now()}; + +if(currentFrameTime - previousFrameTime < 16.667_msec) + stillCanDoSomething(); +/* [types-time] */ +} + { /* [StrictWeakOrdering] */ std::set mySet; @@ -44,4 +66,49 @@ std::map myMap; static_cast(myMap); static_cast(mySet); } + +{ +void usleep(Long); +/* The DOXYGEN_IGNORE() is to avoid -Wvexing-parse */ +/* [Nanoseconds-usage-convert] */ +Float fileCopyDuration(DOXYGEN_IGNORE(void)); + +/* Assuming std::time_t is seconds */ +Nanoseconds a{std::time(nullptr)*1000000000}; +Seconds b{fileCopyDuration()}; + +/* And usleep() takes microseconds */ +usleep(Long(2.0_sec)/1000); +/* [Nanoseconds-usage-convert] */ +} + +{ +/* The include is already above, so doing it again here should be harmless */ +/* [Nanoseconds-usage] */ +#include + +Nanoseconds a{std::chrono::high_resolution_clock::now()}; + +std::chrono::nanoseconds b(16.67_msec); +/* [Nanoseconds-usage] */ +static_cast(b); +} + +/* std::time_t is long long or convertible/aliased to long long only on 64-bit + Linux, apparently. Not on 32-bit, not on Emscripten, not on macOS, not on + Windows. */ +#if defined(__linux__) && !defined(CORRADE_TARGET_32BIT) +{ +/* [Nanoseconds-usage-time] */ +Nanoseconds a1{std::time(nullptr)}; // wrong, the input is seconds +Nanoseconds a2{std::time(nullptr)*1000000000ll}; // correct +std::time_t b1(35.0_sec); // wrong, the input is nanoseconds +std::time_t b2(35.0_sec/1000000000ll); // correct +/* [Nanoseconds-usage-time] */ +static_cast(a1); +static_cast(a2); +static_cast(b1); +static_cast(b2); +} +#endif } diff --git a/doc/snippets/MagnumMath.cpp b/doc/snippets/MagnumMath.cpp index da171dcc0..ed0b61d33 100644 --- a/doc/snippets/MagnumMath.cpp +++ b/doc/snippets/MagnumMath.cpp @@ -35,6 +35,7 @@ #include "Magnum/Math/Half.h" #include "Magnum/Math/Range.h" #include "Magnum/Math/Swizzle.h" +#include "Magnum/Math/Time.h" #include "Magnum/Math/Algorithms/GramSchmidt.h" #define DOXYGEN_ELLIPSIS(...) __VA_ARGS__ @@ -735,6 +736,80 @@ static_cast(tan1); static_cast(tan2); } +{ +/* [Nanoseconds-usage] */ +using namespace Math::Literals; + +Nanoseconds fiveSeconds = 5.0_sec; // 5000000000 +Seconds frameTime = 16.667_msec; // 0.016667 +/* [Nanoseconds-usage] */ +static_cast(fiveSeconds); +static_cast(frameTime); +} + +{ +/* [Nanoseconds-usage-operations] */ +Seconds a = 0.15_sec + 16.67_msec; // 0.16667 +Nanoseconds b = 1000.0_usec*1.25; // 1250000 +//auto c = 10.0_msec*10.0_sec; // error, undefined resulting unit +/* [Nanoseconds-usage-operations] */ +static_cast(a); +static_cast(b); +} + +{ +/* The DOXYGEN_IGNORE() is to avoid -Wvexing-parse */ +void stillCanDoSomething(void); +/* [Nanoseconds-usage-comparison] */ +Nanoseconds frameTime(DOXYGEN_IGNORE(void)); + +if(frameTime() < 15.0_msec) + stillCanDoSomething(); +/* [Nanoseconds-usage-comparison] */ +} + +{ +/* [_nsec] */ +using namespace Math::Literals; + +Nanoseconds twoSeconds = 2000000000_nsec; +/* [_nsec] */ +static_cast(twoSeconds); +} + +{ +/* [_usec] */ +using namespace Math::Literals; + +Nanoseconds a = 2000000.0_usec; +Seconds b = 2000000.0_usec; +/* [_usec] */ +static_cast(a); +static_cast(b); +} + +{ +/* [_msec] */ +using namespace Math::Literals; + +Nanoseconds a = 16.67_msec; +Seconds b = 16.67_msec; +/* [_msec] */ +static_cast(a); +static_cast(b); +} + +{ +/* [_sec] */ +using namespace Math::Literals; + +Nanoseconds a = 45.0_sec; +Seconds b = 45.0_sec; +/* [_sec] */ +static_cast(a); +static_cast(b); +} + { Vector3 epsilon; /* [BitVector-boolean] */ diff --git a/doc/types.dox b/doc/types.dox index 9f0adb326..319612852 100644 --- a/doc/types.dox +++ b/doc/types.dox @@ -155,7 +155,7 @@ Half-precision vector and matrix types such as @ref Vector3h or @ref Matrix3x3h work similarly --- you can construct them and convert them from/to other types, but can't perform any arithmetic. -@section types-special Special types +@section types-angle Angle types Magnum has a special type for strongly-typed representation of angles, namely the @ref Deg and @ref Rad classes (or @ref Degd / @ref Degh and @ref Radd / @@ -190,6 +190,27 @@ any need to care about what input the function expects: @snippet MagnumMath.cpp types-literals-usage +@section types-time Time types + +Similarly to @ref Deg and @ref Rad, there's @ref Nanoseconds and @ref Seconds +for strongly-typed representation of time values. The @ref Nanoseconds is a +64-bit integer type, giving the best possible precision over a range of ±292 +years, while @ref Seconds is a 32-bit floating-point type that should be +sufficient for most practical uses where neither large precision nor a large +range is needed, such as animation keyframe timing. As with the angle types, +they're *not* implicitly constructible from their underlying representation, +instead you can construct them explicitly or use the +@link Math::Literals::TimeLiterals::operator""_sec() _sec @endlink, +@link Math::Literals::TimeLiterals::operator""_msec() _msec @endlink, +@link Math::Literals::TimeLiterals::operator""_usec() _usec @endlink and +@link Math::Literals::TimeLiterals::operator""_nsec() _nsec @endlink +convenience literals that are provided in the @ref Math::Literals namespace. +The time types are similar in spirit to @ref std::chrono type definitions, but +without a dependency on STL. An opt-in conversion is available if you include +@link Magnum/Math/TimeStl.h @endlink. + +@snippet MagnumMath-stl.cpp types-time + @section types-other Other types Other types, which don't have their GLSL equivalent, are: @@ -234,13 +255,14 @@ Example: @snippet MagnumMath.cpp types-literals-init -@section types-thirdparty-integration Integration with types from 3rd party APIs +@section types-thirdparty-integration Integration with types from the STL and 3rd party APIs To simplify the workflow when interacting with 3rd party APIs, all Magnum math -types can be made explicitly convertible to and from types coming from external -libraries. Currently, various Magnum libraries provide these conversion, see -documentation of each `Integration.h` header for details: +types can be made explicitly convertible to and from types coming from the STL +or external libraries. Currently, various Magnum libraries provide these +conversions, see documentation of each header for details: +- @ref std::chrono types --- @ref Magnum/Math/TimeStl.h - Math-related Vulkan structures --- @ref Magnum/Vk/Integration.h, part of the @ref Vk library - All Eigen types --- @ref Magnum/EigenIntegration/Integration.h and diff --git a/src/Magnum/CMakeLists.txt b/src/Magnum/CMakeLists.txt index 81061ae1e..e5cee1a29 100644 --- a/src/Magnum/CMakeLists.txt +++ b/src/Magnum/CMakeLists.txt @@ -150,6 +150,7 @@ set(MagnumMath_SRCS Math/Color.cpp Math/Half.cpp Math/Packing.cpp + Math/Time.cpp Math/instantiation.cpp) set(MagnumMath_GracefulAssert_SRCS diff --git a/src/Magnum/Magnum.h b/src/Magnum/Magnum.h index 71074ee9e..57b556839 100644 --- a/src/Magnum/Magnum.h +++ b/src/Magnum/Magnum.h @@ -922,6 +922,18 @@ typedef Math::Range3D Range3Di; /** @brief Float frustum */ typedef Math::Frustum Frustum; +/** +@brief 64-bit signed integer nanoseconds +@m_since_latest +*/ +typedef Math::Nanoseconds Nanoseconds; + +/** +@brief 32-bit float seconds +@m_since_latest +*/ +typedef Math::Seconds Seconds; + /* Since 1.8.17, the original short-hand group closing doesn't work anymore. FFS. */ /** diff --git a/src/Magnum/Math/CMakeLists.txt b/src/Magnum/Math/CMakeLists.txt index ae0998d07..93f5a1936 100644 --- a/src/Magnum/Math/CMakeLists.txt +++ b/src/Magnum/Math/CMakeLists.txt @@ -59,6 +59,8 @@ set(MagnumMath_HEADERS StrictWeakOrdering.h Swizzle.h Tags.h + Time.h + TimeStl.h Unit.h Vector.h Vector2.h diff --git a/src/Magnum/Math/Math.h b/src/Magnum/Math/Math.h index 687ccde6e..e0b25e988 100644 --- a/src/Magnum/Math/Math.h +++ b/src/Magnum/Math/Math.h @@ -77,6 +77,8 @@ template using Matrix4x3 = RectangularMatrix<4, 3, T>; template class, class> class Unit; template class Deg; template class Rad; +template class Nanoseconds; +template class Seconds; class Half; diff --git a/src/Magnum/Math/Test/CMakeLists.txt b/src/Magnum/Math/Test/CMakeLists.txt index 9453aa38b..c8f9d3197 100644 --- a/src/Magnum/Math/Test/CMakeLists.txt +++ b/src/Magnum/Math/Test/CMakeLists.txt @@ -66,6 +66,8 @@ corrade_add_test(MathMatrix4Test Matrix4Test.cpp LIBRARIES MagnumMathTestLib) corrade_add_test(MathSwizzleTest SwizzleTest.cpp LIBRARIES MagnumMathTestLib) corrade_add_test(MathUnitTest UnitTest.cpp LIBRARIES MagnumMathTestLib) corrade_add_test(MathAngleTest AngleTest.cpp LIBRARIES MagnumMathTestLib) +corrade_add_test(MathTimeTest TimeTest.cpp LIBRARIES MagnumMathTestLib) +corrade_add_test(MathTimeStlTest TimeStlTest.cpp LIBRARIES MagnumMathTestLib) corrade_add_test(MathRangeTest RangeTest.cpp LIBRARIES MagnumMathTestLib) corrade_add_test(MathDualTest DualTest.cpp LIBRARIES MagnumMathTestLib) diff --git a/src/Magnum/Math/Test/TimeStlTest.cpp b/src/Magnum/Math/Test/TimeStlTest.cpp new file mode 100644 index 000000000..63b2b4a04 --- /dev/null +++ b/src/Magnum/Math/Test/TimeStlTest.cpp @@ -0,0 +1,246 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include + +#include "Magnum/Math/TimeStl.h" + +namespace Magnum { namespace Math { namespace Test { namespace { + +struct TimeStlTest: TestSuite::Tester { + explicit TimeStlTest(); + + void chronoDurationTypedefs(); + void chronoDurationFloatingPoint(); + void chronoTimePoint(); +}; + +TimeStlTest::TimeStlTest() { + addTests({&TimeStlTest::chronoDurationTypedefs, + &TimeStlTest::chronoDurationFloatingPoint, + &TimeStlTest::chronoTimePoint}); +} + +using Magnum::Nanoseconds; + +using namespace Math::Literals; + +void TimeStlTest::chronoDurationTypedefs() { + /* Negative values should work as well */ + std::chrono::nanoseconds a1{1234567891234567890ll}; + std::chrono::nanoseconds a2{-1234567891234567890ll}; + /* The rest is implemented in a generic way, so no need to test both + variants of every */ + std::chrono::microseconds b1{4567891234567890ll}; + std::chrono::milliseconds c1{-7891234567890ll}; + std::chrono::seconds d1{1234567890ll}; + std::chrono::minutes e1{-34567890ll}; + std::chrono::hours f1{567890ll}; + Nanoseconds a3{a1}; + Nanoseconds a4{a2}; + CORRADE_COMPARE(a3, 1234567891234567890_nsec); + CORRADE_COMPARE(a4, -1234567891234567890_nsec); + /* Using the _nsec literal to circumvent potential rounding errors when + using the _usec etc literals on platforms without an 80-bit long + double */ + CORRADE_COMPARE(Nanoseconds{b1}, 4567891234567890000_nsec); + CORRADE_COMPARE(Nanoseconds{c1}, -7891234567890000000_nsec); + CORRADE_COMPARE(Nanoseconds{d1}, 1234567890000000000_nsec); + CORRADE_COMPARE(Nanoseconds{e1}, 60*-34567890000000000_nsec); + CORRADE_COMPARE(Nanoseconds{f1}, 60*60*567890000000000_nsec); + + /* Only nanoseconds can be converted back */ + std::chrono::nanoseconds a5(a3); + std::chrono::nanoseconds a6(a4); + CORRADE_COMPARE(a5.count(), 1234567891234567890ll); + CORRADE_COMPARE(a6.count(), -1234567891234567890ll); + + constexpr std::chrono::nanoseconds ca1{1234567891234567890ll}; + constexpr std::chrono::nanoseconds ca2{-1234567891234567890ll}; + constexpr std::chrono::microseconds cb1{4567891234567890ll}; + constexpr std::chrono::milliseconds cc1{-7891234567890ll}; + constexpr std::chrono::seconds cd1{1234567890ll}; + constexpr std::chrono::minutes ce1{-34567890ll}; + constexpr std::chrono::hours cf1{567890ll}; + constexpr Nanoseconds ca3{ca1}; + constexpr Nanoseconds ca4{ca2}; + constexpr Nanoseconds cb2{cb1}; + constexpr Nanoseconds cc2{cc1}; + constexpr Nanoseconds cd2{cd1}; + constexpr Nanoseconds ce2{ce1}; + constexpr Nanoseconds cf2{cf1}; + CORRADE_COMPARE(ca3, 1234567891234567890_nsec); + CORRADE_COMPARE(ca4, -1234567891234567890_nsec); + CORRADE_COMPARE(cb2, 4567891234567890000_nsec); + CORRADE_COMPARE(cc2, -7891234567890000000_nsec); + CORRADE_COMPARE(cd2, 1234567890000000000_nsec); + CORRADE_COMPARE(ce2, 60*-34567890000000000_nsec); + CORRADE_COMPARE(cf2, 60*60*567890000000000_nsec); + + constexpr std::chrono::nanoseconds ca5(ca3); + constexpr std::chrono::nanoseconds ca6(ca4); + CORRADE_COMPARE(ca5.count(), 1234567891234567890ll); + CORRADE_COMPARE(ca6.count(), -1234567891234567890ll); +} + +void TimeStlTest::chronoDurationFloatingPoint() { + /* Same as chronoDurationTypedefs(), except that this is using a + floating-point type, for which the precision shouldn't be lost for the + fractional part. In C++14 with std::chrono_literals this would be + 9087654321987654321.0ns, -9087654321987.654321ms etc. */ + std::chrono::duration a1{9087654321987654321.0l}; + std::chrono::duration a2{-9087654321987654321.0l}; + /* Again, everything except nanoseconds is implemented in a generic way, so + no need to test both variants of every */ + std::chrono::duration b1{9087654321987654.321l}; + std::chrono::duration c1{-9087654321987.654321l}; + std::chrono::duration d1{9087654321.987654321l}; + Nanoseconds a3{a1}; + Nanoseconds a4{a2}; + /* Not sure what Emscripten does here, but it behaves as if long double was + actually the full precision. Similar to the logic in + TimeTest::literals(). */ + #if !defined(CORRADE_LONG_DOUBLE_SAME_AS_DOUBLE) || defined(CORRADE_TARGET_EMSCRIPTEN) + CORRADE_COMPARE(a3, 9087654321987654321_nsec); + CORRADE_COMPARE(a4, -9087654321987654321_nsec); + CORRADE_COMPARE(Nanoseconds{b1}, 9087654321987654321_nsec); + /* The conversion suffers from the same minor precision loss as with + the floating-point Nanosecond literals themselves. Again see + TimeTest::literals() for details. */ + #if (defined(CORRADE_TARGET_ARM) && !defined(CORRADE_TARGET_32BIT)) || defined(CORRADE_TARGET_EMSCRIPTEN) + CORRADE_COMPARE(Nanoseconds{c1}, -9087654321987654321_nsec); + CORRADE_COMPARE(Nanoseconds{d1}, 9087654321987654320_nsec); + #else + CORRADE_COMPARE(Nanoseconds{c1}, -9087654321987654320_nsec); + CORRADE_COMPARE(Nanoseconds{d1}, 9087654321987654321_nsec); + #endif + #else + CORRADE_COMPARE(a3, 9087654321987654656_nsec); + CORRADE_COMPARE(a4, -9087654321987654656_nsec); + CORRADE_COMPARE(Nanoseconds{b1}, 9087654321987653632_nsec); + CORRADE_COMPARE(Nanoseconds{c1}, -9087654321987654656_nsec); + CORRADE_COMPARE(Nanoseconds{d1}, 9087654321987653632_nsec); + #endif + + /* Only nanoseconds can be converted back */ + /** @todo enable conversion in the other direction for all ratios if using + a floating-point representation */ + std::chrono::duration a5(a3); + std::chrono::duration a6(a4); + CORRADE_COMPARE(a5.count(), 9087654321987654321.0l); + CORRADE_COMPARE(a6.count(), -9087654321987654321.0l); + + constexpr std::chrono::duration ca1{9087654321987654321.0l}; + constexpr std::chrono::duration ca2{-9087654321987654321.0l}; + constexpr std::chrono::duration cb1{9087654321987654.321l}; + constexpr std::chrono::duration cc1{-9087654321987.654321l}; + constexpr std::chrono::duration cd1{9087654321.987654321l}; + constexpr Nanoseconds ca3{ca1}; + constexpr Nanoseconds ca4{ca2}; + constexpr Nanoseconds cb2{cb1}; + constexpr Nanoseconds cc2{cc1}; + constexpr Nanoseconds cd2{cd1}; + #if !defined(CORRADE_LONG_DOUBLE_SAME_AS_DOUBLE) || defined(CORRADE_TARGET_EMSCRIPTEN) + CORRADE_COMPARE(ca3, 9087654321987654321_nsec); + CORRADE_COMPARE(ca4, -9087654321987654321_nsec); + CORRADE_COMPARE(cb2, 9087654321987654321_nsec); + #if (defined(CORRADE_TARGET_ARM) && !defined(CORRADE_TARGET_32BIT)) || defined(CORRADE_TARGET_EMSCRIPTEN) + CORRADE_COMPARE(cc2, -9087654321987654321_nsec); + CORRADE_COMPARE(cd2, 9087654321987654320_nsec); + #else + CORRADE_COMPARE(cc2, -9087654321987654320_nsec); + CORRADE_COMPARE(cd2, 9087654321987654321_nsec); + #endif + #else + CORRADE_COMPARE(ca3, 9087654321987654656_nsec); + CORRADE_COMPARE(ca4, -9087654321987654656_nsec); + CORRADE_COMPARE(cb2, 9087654321987653632_nsec); + CORRADE_COMPARE(cc2, -9087654321987654656_nsec); + CORRADE_COMPARE(cd2, 9087654321987653632_nsec); + #endif + + /* Only nanoseconds can be converted back */ + /** @todo enable conversion in the other direction for all ratios if using + a floating-point representation */ + constexpr std::chrono::duration ca5(ca3); + constexpr std::chrono::duration ca6(ca4); + CORRADE_COMPARE(ca5.count(), 9087654321987654321.0l); + CORRADE_COMPARE(ca6.count(), -9087654321987654321.0l); +} + +void TimeStlTest::chronoTimePoint() { + std::chrono::system_clock::time_point systemNow = std::chrono::system_clock::now(); + std::chrono::steady_clock::time_point steadyNow = std::chrono::steady_clock::now(); + std::chrono::high_resolution_clock::time_point highNow = std::chrono::high_resolution_clock::now(); + + Nanoseconds system{systemNow}; + Nanoseconds steady{steadyNow}; + Nanoseconds high{highNow}; + CORRADE_COMPARE(Long(system), std::chrono::duration_cast(systemNow.time_since_epoch()).count()); + CORRADE_COMPARE(Long(steady), std::chrono::duration_cast(steadyNow.time_since_epoch()).count()); + CORRADE_COMPARE(Long(high), std::chrono::duration_cast(highNow.time_since_epoch()).count()); + + /* Conversion back is possible only if the STL clock is in nanoseconds. + That's the case for all three in libstdc++. In libc++ the system_clock + has only a microsecond precision: + https://github.com/llvm/llvm-project/blob/44d85c5b15bbf6226f442126735b764d81cbf6e3/libcxx/include/__chrono/system_clock.h#L28 + In MSVC STL system_clock has a 100-nanosecond resolution: + https://github.com/microsoft/STL/blob/192a84008a59ac4d2e55681e1ffac73535788674/stl/inc/__msvc_chrono.hpp#L636. + + std::ostream operators for std::chrono::duration are only available + since C++20 so the comparison is this insane. */ + #if !defined(CORRADE_TARGET_LIBCXX) && !defined(CORRADE_TARGET_DINKUMWARE) + std::chrono::system_clock::time_point systemAgain(system); + #endif + std::chrono::steady_clock::time_point steadyAgain(steady); + std::chrono::high_resolution_clock::time_point highAgain(high); + #if !defined(CORRADE_TARGET_LIBCXX) && !defined(CORRADE_TARGET_DINKUMWARE) + CORRADE_COMPARE(systemAgain.time_since_epoch().count(), systemNow.time_since_epoch().count()); + #endif + CORRADE_COMPARE(steadyAgain.time_since_epoch().count(), steadyNow.time_since_epoch().count()); + CORRADE_COMPARE(highAgain.time_since_epoch().count(), highNow.time_since_epoch().count()); + + /* Constexpr variants with a custom value. Spec says the time_point + constructor is constexpr since C++14. Libstdc++ and MSVC STL has them + since C++11 already, libc++ explicitly only since C++14. */ + #ifndef CORRADE_TARGET_LIBCXX + constexpr + #endif + std::chrono::high_resolution_clock::time_point ca{std::chrono::nanoseconds{1234567891234567890ll}}; + #ifndef CORRADE_TARGET_LIBCXX + constexpr + #endif + Nanoseconds cb{ca}; + #ifndef CORRADE_TARGET_LIBCXX + constexpr + #endif + std::chrono::high_resolution_clock::time_point cc{cb}; + CORRADE_COMPARE(Long(cb),std::chrono::duration_cast(ca.time_since_epoch()).count()); + CORRADE_COMPARE(cc.time_since_epoch().count(), ca.time_since_epoch().count()); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::Math::Test::TimeStlTest) diff --git a/src/Magnum/Math/Test/TimeTest.cpp b/src/Magnum/Math/Test/TimeTest.cpp new file mode 100644 index 000000000..79a8a115a --- /dev/null +++ b/src/Magnum/Math/Test/TimeTest.cpp @@ -0,0 +1,442 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include +#include /** @todo remove once Debug is STL-free */ +#include +#include /** @todo remove once Debug is STL-free */ + +#include "Magnum/Math/Time.h" + +struct Time { + unsigned secondsSinceEpoch; +}; + +struct Keyframe { + float duration; +}; + +namespace Magnum { namespace Math { + +namespace Implementation { + +template<> struct NanosecondsConverter { + constexpr static Nanoseconds from(Time other) { + return Nanoseconds{Long(other.secondsSinceEpoch)*1000000000ll}; + } + constexpr static Time to(Nanoseconds other) { + return {unsigned(Long(other)/1000000000ll)}; + } +}; +template<> struct SecondsConverter { + constexpr static Seconds from(Keyframe other) { + return Seconds{other.duration}; + } + constexpr static Keyframe to(Seconds other) { + return {Float(other)}; + } +}; + +} + +namespace Test { namespace { + +struct TimeTest: TestSuite::Tester { + explicit TimeTest(); + + void limits(); + + void construct(); + void constructDefault(); + void constructNoInit(); + void constructCopy(); + void constructFromBase(); + void convert(); + + void literals(); + void conversion(); + void nanosecondFloatScaling(); + + void debugNanoseconds(); + void debugNanosecondsPacked(); + void debugSeconds(); + void debugSecondsPacked(); +}; + +using Magnum::Nanoseconds; +using Magnum::Seconds; + +using namespace Math::Literals; + +TimeTest::TimeTest() { + addTests({&TimeTest::limits, + + &TimeTest::construct, + &TimeTest::constructDefault, + &TimeTest::constructNoInit, + &TimeTest::constructCopy, + &TimeTest::constructFromBase, + &TimeTest::convert, + + &TimeTest::literals, + &TimeTest::conversion, + &TimeTest::nanosecondFloatScaling, + + &TimeTest::debugNanoseconds, + &TimeTest::debugNanosecondsPacked, + &TimeTest::debugSeconds, + &TimeTest::debugSecondsPacked}); +} + +void TimeTest::limits() { + /* There's apparently no way to say -0x8000000000000000ll, so there's also + no non-error-prone way to verify the values are correct. */ + + /* It should be all 64 bits (so 16 nibbles) being set */ + CORRADE_COMPARE(UnsignedLong(Long(Nanoseconds::min()))| + UnsignedLong(Long(Nanoseconds::max())), + /* 0123456789abcdef */ + 0xffffffffffffffffull); + + /* Assuming signed integer overflow is defined sanely, which it should */ + CORRADE_COMPARE(Nanoseconds::min() - 1_nsec, Nanoseconds::max()); + CORRADE_COMPARE(Nanoseconds::max() + 1_nsec, Nanoseconds::min()); + + /* This should also hold */ + CORRADE_COMPARE(Nanoseconds::min() + Nanoseconds::max(), -1_nsec); + CORRADE_COMPARE(Nanoseconds::max() - Nanoseconds::min(), -1_nsec); +} + +void TimeTest::construct() { + Nanoseconds a{-123456789123456789ll}; + Seconds b{123.45f}; + CORRADE_COMPARE(Long(a), -123456789123456789ll); + CORRADE_COMPARE(Float(b), 123.45f); + + constexpr Nanoseconds ca{-123456789123456789ll}; + constexpr Seconds cb{123.45f}; + CORRADE_COMPARE(Long(ca), -123456789123456789ll); + CORRADE_COMPARE(Float(cb), 123.45f); + + /* Implicit conversion is not allowed */ + CORRADE_VERIFY(!std::is_convertible::value); + CORRADE_VERIFY(!std::is_convertible::value); + + CORRADE_VERIFY(std::is_nothrow_constructible::value); + CORRADE_VERIFY(std::is_nothrow_constructible::value); +} + +void TimeTest::constructDefault() { + Nanoseconds a1; + Nanoseconds a2{ZeroInit}; + Seconds b1; + Seconds b2{ZeroInit}; + CORRADE_COMPARE(Long(a1), 0ll); + CORRADE_COMPARE(Long(a2), 0ll); + CORRADE_COMPARE(Float(b1), 0.0f); + CORRADE_COMPARE(Float(b2), 0.0f); + + constexpr Nanoseconds ca1; + constexpr Nanoseconds ca2{ZeroInit}; + constexpr Seconds cb1; + constexpr Seconds cb2{ZeroInit}; + CORRADE_COMPARE(Long(ca1), 0ll); + CORRADE_COMPARE(Long(ca2), 0ll); + CORRADE_COMPARE(Float(cb1), 0.0f); + CORRADE_COMPARE(Float(cb2), 0.0f); + + CORRADE_VERIFY(std::is_nothrow_default_constructible::value); + CORRADE_VERIFY(std::is_nothrow_default_constructible::value); + CORRADE_VERIFY(std::is_nothrow_constructible::value); + CORRADE_VERIFY(std::is_nothrow_constructible::value); + + /* Implicit construction is not allowed */ + CORRADE_VERIFY(!std::is_convertible::value); + CORRADE_VERIFY(!std::is_convertible::value); +} + +void TimeTest::constructNoInit() { + Nanoseconds a{123456789123456789ll}; + Seconds b{123.45f}; + new(&a) Nanoseconds{Magnum::NoInit}; + new(&b) Seconds{Magnum::NoInit}; + { + /* Explicitly check we're not on Clang because certain Clang-based IDEs + inherit __GNUC__ if GCC is used instead of leaving it at 4 like + Clang itself does */ + #if defined(CORRADE_TARGET_GCC) && !defined(CORRADE_TARGET_CLANG) && __GNUC__*100 + __GNUC_MINOR__ >= 601 + /* The warning is reported for both debug and release build */ + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" + /* On GCC 13 it's -Wuninitialized now, the compiler is now EVEN MORE + DEFINITELY RIGHT that I'm doing something wrong */ + #if __GNUC__ >= 13 + #pragma GCC diagnostic ignored "-Wuninitialized" + #endif + #ifdef __OPTIMIZE__ + CORRADE_EXPECT_FAIL("GCC 6.1+ misoptimizes and overwrites the value."); + #endif + #endif + CORRADE_COMPARE(Long(a), 123456789123456789ll); + CORRADE_COMPARE(Float(b), 123.45f); + #if defined(CORRADE_TARGET_GCC) && !defined(CORRADE_TARGET_CLANG) && __GNUC__*100 + __GNUC_MINOR__ >= 601 + #pragma GCC diagnostic pop + #endif + } + + CORRADE_VERIFY(std::is_nothrow_constructible::value); + CORRADE_VERIFY(std::is_nothrow_constructible::value); + + /* Implicit construction is not allowed */ + CORRADE_VERIFY(!std::is_convertible::value); + CORRADE_VERIFY(!std::is_convertible::value); +} + +void TimeTest::constructCopy() { + Nanoseconds a{-987654321987654321ll}; + Nanoseconds b{a}; + Seconds c{-543.21f}; + Seconds d{c}; + CORRADE_COMPARE(Long(b), -987654321987654321ll); + CORRADE_COMPARE(Float(d), -543.21f); + + constexpr Nanoseconds ca{-987654321987654321ll}; + constexpr Nanoseconds cb{ca}; + constexpr Seconds cc{-543.21f}; + constexpr Seconds cd{cc}; + CORRADE_COMPARE(Long(cb), -987654321987654321ll); + CORRADE_COMPARE(Float(cd), -543.21f); + + #ifndef CORRADE_NO_STD_IS_TRIVIALLY_TRAITS + CORRADE_VERIFY(std::is_trivially_copy_constructible::value); + CORRADE_VERIFY(std::is_trivially_copy_constructible::value); + CORRADE_VERIFY(std::is_trivially_copy_assignable::value); + CORRADE_VERIFY(std::is_trivially_copy_assignable::value); + #endif + CORRADE_VERIFY(std::is_nothrow_copy_constructible::value); + CORRADE_VERIFY(std::is_nothrow_copy_constructible::value); + CORRADE_VERIFY(std::is_nothrow_copy_assignable::value); + CORRADE_VERIFY(std::is_nothrow_copy_assignable::value); +} + +void TimeTest::constructFromBase() { + /* The operation returns Unit instead of the leaf type, so this can work + only if the base class has a "copy constructor" from the base type */ + Nanoseconds a = 15.0_usec + 3.5_msec; + Seconds b = Seconds{15.0_msec} + Seconds{3.5_sec}; + CORRADE_COMPARE(a, 3.515_msec); + /* Comparing as seconds because precision loss involved */ + CORRADE_COMPARE_AS(b, 3.515_sec, Seconds); +} + +void TimeTest::convert() { + /* From external type */ + Time a0{1707678819}; + Keyframe b0{56.72f}; + Nanoseconds a1{a0}; + Seconds b1{b0}; + CORRADE_COMPARE(a1, 1707678819.0_sec); + CORRADE_COMPARE_AS(b1, 56.72_sec, Seconds); + + constexpr Time ca0{1707678819}; + constexpr Keyframe cb0{56.72f}; + constexpr Nanoseconds ca1{ca0}; + constexpr Seconds cb1{cb0}; + CORRADE_COMPARE(ca1, 1707678819.0_sec); + CORRADE_COMPARE_AS(cb1, 56.72_sec, Seconds); + + /* To external type */ + Nanoseconds c0 = 1707678819.0_sec; + Seconds d0 = 56.72_sec; + Time c1(c0); + Keyframe d1(d0); + CORRADE_COMPARE(c1.secondsSinceEpoch, 1707678819); + CORRADE_COMPARE(d1.duration, 56.72f); + + constexpr Nanoseconds cc0 = 1707678819.0_sec; + constexpr Seconds cd0 = 56.72_sec; + constexpr Time cc1(cc0); + constexpr Keyframe cd1(cd0); + CORRADE_COMPARE(cc1.secondsSinceEpoch, 1707678819); + CORRADE_COMPARE(cd1.duration, 56.72f); + + /* It should not be possible to convert in a direction that may result in a + precision loss, i.e. Seconds with a NanosecondsConverter */ + CORRADE_VERIFY(std::is_constructible::value); + CORRADE_VERIFY(!std::is_constructible::value); + CORRADE_VERIFY(std::is_constructible::value); + CORRADE_VERIFY(!std::is_constructible::value); + CORRADE_VERIFY(std::is_constructible::value); + CORRADE_VERIFY(!std::is_constructible::value); + CORRADE_VERIFY(std::is_constructible::value); + CORRADE_VERIFY(!std::is_constructible::value); + + /* Implicit conversion is not allowed */ + CORRADE_VERIFY(!std::is_convertible::value); + CORRADE_VERIFY(!std::is_convertible::value); + CORRADE_VERIFY(!std::is_convertible::value); + CORRADE_VERIFY(!std::is_convertible::value); +} + +void TimeTest::literals() { + /* Testing the full precision, 19 digits. Max representable 63-bit value is + 9223372036854775807. */ + auto a = 9087654321987654321_nsec; + auto b = 9087654321987654.321_usec; + auto c = 9087654321987.654321_msec; + auto d = 9087654321.987654321_sec; + CORRADE_VERIFY(std::is_same::value); + CORRADE_VERIFY(std::is_same::value); + CORRADE_VERIFY(std::is_same::value); + CORRADE_VERIFY(std::is_same::value); + CORRADE_COMPARE(Long(a), 9087654321987654321ll); + /* Not sure what Emscripten does here, but it behaves as if long double was + actually the full precision */ + #if !defined(CORRADE_LONG_DOUBLE_SAME_AS_DOUBLE) || defined(CORRADE_TARGET_EMSCRIPTEN) + /* 80-bit long double has a 63-bit mantissa, which means this is converted + without any precision loss. Otherwise the precision is just 52 bits. */ + CORRADE_COMPARE(Long(b), 9087654321987654321ll); + /* Well, almost. On x86 this conversion has a slight imprecision in the + lowest bit for the _ms variant, on ARM64 and Emscripten for the _s + variant. */ + #if (defined(CORRADE_TARGET_ARM) && !defined(CORRADE_TARGET_32BIT)) || defined(CORRADE_TARGET_EMSCRIPTEN) + CORRADE_COMPARE(Long(c), 9087654321987654321ll); + CORRADE_COMPARE(Long(d), 9087654321987654320ll); + #else + CORRADE_COMPARE(Long(c), 9087654321987654320ll); + CORRADE_COMPARE(Long(d), 9087654321987654321ll); + #endif + #else + CORRADE_COMPARE(Long(b), 9087654321987653632ll); + CORRADE_COMPARE(Long(c), 9087654321987654656ll); + CORRADE_COMPARE(Long(d), 9087654321987653632ll); + #endif + + constexpr auto ca = 9087654321987654321_nsec; + constexpr auto cb = 9087654321987654.321_usec; + constexpr auto cc = 9087654321987.654321_msec; + constexpr auto cd = 9087654321.987654321_sec; + CORRADE_VERIFY(std::is_same::value); + CORRADE_VERIFY(std::is_same::value); + CORRADE_VERIFY(std::is_same::value); + CORRADE_VERIFY(std::is_same::value); + CORRADE_COMPARE(Long(ca), 9087654321987654321ll); + #if !defined(CORRADE_LONG_DOUBLE_SAME_AS_DOUBLE) || defined(CORRADE_TARGET_EMSCRIPTEN) + CORRADE_COMPARE(Long(cb), 9087654321987654321ll); + #if (defined(CORRADE_TARGET_ARM) && !defined(CORRADE_TARGET_32BIT)) || defined(CORRADE_TARGET_EMSCRIPTEN) + CORRADE_COMPARE(Long(cc), 9087654321987654321ll); + CORRADE_COMPARE(Long(cd), 9087654321987654320ll); + #else + CORRADE_COMPARE(Long(cc), 9087654321987654320ll); + CORRADE_COMPARE(Long(cd), 9087654321987654321ll); + #endif + #else + CORRADE_COMPARE(Long(cb), 9087654321987653632ll); + CORRADE_COMPARE(Long(cc), 9087654321987654656ll); + CORRADE_COMPARE(Long(cd), 9087654321987653632ll); + #endif +} + +void TimeTest::conversion() { + /* Implicit conversion should be allowed. Again testing (almost) the full + nanosecond precision, although not much of it is left when converting + to a 32-bit float. */ + Nanoseconds a = Seconds{-987654321.987654321f}; + Seconds b = 987654321987654321_nsec; + CORRADE_COMPARE(Long(a), -987654336000000000ll); + CORRADE_COMPARE(Float(b), 987654336.0f); + + constexpr Nanoseconds ca = Seconds{987654321.987654321f}; + constexpr Seconds cb = -987654321987654321_nsec; + CORRADE_COMPARE(Long(ca), 987654336000000000ll); + CORRADE_COMPARE(Float(cb), -987654336.0f); + + CORRADE_VERIFY(std::is_nothrow_constructible::value); + CORRADE_VERIFY(std::is_nothrow_constructible::value); +} + +void TimeTest::nanosecondFloatScaling() { + /* Nanoseconds is an integer type, but multiplying it with float should + give a reasonable output. The actual logic is in the Unit class, just + verify that it works from the high level perspective here. */ + + CORRADE_COMPARE(1000000000_nsec*1.25, 1250000000_nsec); + CORRADE_COMPARE(1000000000_nsec*1.25f, 1250000000_nsec); + CORRADE_COMPARE(1000000000_nsec/0.8, 1250000000_nsec); + CORRADE_COMPARE(1000000000_nsec/0.8f, 1250000000_nsec); + + /* Compared to above this looks like it should "obviously work", although + internally both cases are the same, operating on Nanoseconds */ + CORRADE_COMPARE(1.0_sec*1.25, 1.25_sec); + CORRADE_COMPARE(1.0_sec*1.25f, 1.25_sec); + CORRADE_COMPARE(1.0_sec/0.8, 1.25_sec); + CORRADE_COMPARE(1.0_sec/0.8f, 1.25_sec); + + /* This would be nice if it worked, but so far it doesn't, as it's + calculated as an integer value */ + CORRADE_COMPARE(1.0f/0.018f, 55.5556f); + { + CORRADE_EXPECT_FAIL("This doesn't work correctly."); + CORRADE_COMPARE(1.0_sec/18.0_msec, 55.5556f); + } +} + +void TimeTest::debugNanoseconds() { + std::ostringstream out; + + /* Also verify that the second expression compiles (it's the Unit type, + not Nanoseconds) */ + Debug{&out} << 987654321987654321_nsec << 15.0_sec - 7.5_sec; + CORRADE_COMPARE(out.str(), "Nanoseconds(987654321987654321) Nanoseconds(7500000000)\n"); +} + +void TimeTest::debugNanosecondsPacked() { + std::ostringstream out; + + /* Second is not packed, the first should not make any flags persistent */ + Debug{&out} << Debug::packed << 15.0_sec << 45.0_sec; + CORRADE_COMPARE(out.str(), "15000000000 Nanoseconds(45000000000)\n"); +} + +void TimeTest::debugSeconds() { + std::ostringstream out; + + /* Also verify that the second expression compiles (it's the Unit type, + not Nanoseconds) */ + Debug{&out} << Seconds{123.45_sec} << Seconds{15.0_sec} - Seconds{7.5_sec}; + CORRADE_COMPARE(out.str(), "Seconds(123.45) Seconds(7.5)\n"); +} + +void TimeTest::debugSecondsPacked() { + std::ostringstream out; + + /* Second is not packed, the first should not make any flags persistent */ + Debug{&out} << Debug::packed << Seconds{123.45_sec} << Seconds{45.0_sec}; + CORRADE_COMPARE(out.str(), "123.45 Seconds(45)\n"); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::Math::Test::TimeTest) diff --git a/src/Magnum/Math/Time.cpp b/src/Magnum/Math/Time.cpp new file mode 100644 index 000000000..9ab6a1191 --- /dev/null +++ b/src/Magnum/Math/Time.cpp @@ -0,0 +1,44 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include "Time.h" + +#include + +namespace Magnum { namespace Math { + +Utility::Debug& operator<<(Utility::Debug& debug, const Unit& value) { + if(debug.immediateFlags() >= Utility::Debug::Flag::Packed) + return debug << Long(value); + return debug << "Nanoseconds(" << Utility::Debug::nospace << Long(value) << Utility::Debug::nospace << ")"; +} + +Utility::Debug& operator<<(Utility::Debug& debug, const Unit& value) { + if(debug.immediateFlags() >= Utility::Debug::Flag::Packed) + return debug << Float(value); + return debug << "Seconds(" << Utility::Debug::nospace << Float(value) << Utility::Debug::nospace << ")"; +} + +}} diff --git a/src/Magnum/Math/Time.h b/src/Magnum/Math/Time.h new file mode 100644 index 000000000..266e1e3e8 --- /dev/null +++ b/src/Magnum/Math/Time.h @@ -0,0 +1,398 @@ +#ifndef Magnum_Math_Time_h +#define Magnum_Math_Time_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/** @file + * @brief Class @ref Magnum::Math::Nanoseconds, @ref Magnum::Math::Seconds, literal @link Magnum::Math::Literals::TimeLiterals::operator""_nsec() @endlink, @link Magnum::Math::Literals::TimeLiterals::operator""_usec() @endlink, @link Magnum::Math::Literals::TimeLiterals::operator""_msec() @endlink, @link Magnum::Math::Literals::TimeLiterals::operator""_sec() @endlink + * @m_since_latest + */ + +#include + +#include "Magnum/Magnum.h" +#include "Magnum/visibility.h" +#include "Magnum/Math/Unit.h" + +namespace Magnum { namespace Math { + +namespace Implementation { + template struct NanosecondsConverter; + template struct SecondsConverter; +} + +/** +@brief Nanoseconds +@m_since_latest + +Along with @ref Seconds provides convenience classes to make time specification +and conversion less error-prone. As there's little need to represent fractions +of nanoseconds, the @ref Magnum::Nanoseconds typedef uses a 64-bit signed +integer, which covers a span of ±292 years. In scenarios where nanosecond +precision or a large range isn't needed, the @ref Magnum::Seconds typedef, +which is a 32-bit floating-point type, may be sufficient. + +@section Math-Nanoseconds-usage Usage + +You can create the value by using one of the time literals. For all of them the +result type is @ref Nanoseconds for preserving maximum precision, but you can +directly convert the literal value @link Seconds @endlink: + +@snippet MagnumMath.cpp Nanoseconds-usage + +Or by explicitly converting a unitless value (such as an output from some +function) to either nanoseconds or seconds. And same can be done in the other direction: + +@snippet MagnumMath-stl.cpp Nanoseconds-usage-convert + +The classes support all arithmetic operations, such as addition, subtraction +or multiplication/division by a unitless number: + +@snippet MagnumMath.cpp Nanoseconds-usage-operations + +It is also possible to compare time values with all comparison operators. As +the literals are all producing @ref Nanoseconds, it's most convenient to +compare to nanosecond values. Comparison of @ref Nanoseconds and @ref Seconds +is not possible without conversion to a common type first. + +@snippet MagnumMath.cpp Nanoseconds-usage-comparison + +@section Math-Nanoseconds-stl STL compatibility + +Instances of @ref Nanoseconds are explicitly convertible from and to +@ref std::chrono::duration and @ref std::chrono::time_point types if you +include @ref Magnum/Math/TimeStl.h. The conversion is provided in a separate +header to avoid unconditional @cpp #include @ce, which can +significantly affect compile times. The following table lists allowed +conversions, conversions in certain directions aren't allowed as they cause a +precision loss: + +Magnum type | ↭ | STL type +-----------------| - | --------------------- +@ref Nanoseconds | ⇆ | @ref std::chrono::nanoseconds +@ref Nanoseconds | ← | @ref std::chrono::microseconds +@ref Nanoseconds | ← | @ref std::chrono::milliseconds +@ref Nanoseconds | ← | @ref std::chrono::seconds +@ref Nanoseconds | ← | @ref std::chrono::minutes +@ref Nanoseconds | ← | @ref std::chrono::hours +@ref Nanoseconds | ← | @ref std::chrono::duration +@ref Nanoseconds | ⇆ | @ref std::chrono::duration "std::chrono::duration" +@ref Nanoseconds | ← | @ref std::chrono::time_point +@ref Nanoseconds | ⇆ | @ref std::chrono::time_point "std::chrono::time_point>" + +Example: + +@snippet MagnumMath-stl.cpp Nanoseconds-usage + + + +@m_class{m-block m-warning} + +@par Conversion from and to std::time_t + Even though @ref std::time_t may look like an implementation-defined strong + type, it's actually just an alias to an integer type, which in turn means + it's not possible to provide safe conversion for it. Thus a simple + conversion, while it may compile, won't do the right thing: +@par + @snippet MagnumMath-stl.cpp Nanoseconds-usage-time + +@see @link Literals::TimeLiterals::operator""_nsec() @endlink, + @link Literals::TimeLiterals::operator""_usec() @endlink, + @link Literals::TimeLiterals::operator""_msec() @endlink, + @link Literals::TimeLiterals::operator""_sec() @endlink +*/ +template class Nanoseconds: public Unit { + public: + /** + * @brief Minimal representable value + * + * Returns @cpp -0x8000000000000000_nsec @ce. + */ + constexpr static Nanoseconds min(); + + /** + * @brief Maximal representable value + * + * Returns @cpp 0x7fffffffffffffff_nsec @ce. + */ + constexpr static Nanoseconds max(); + + /** + * @brief Default constructor + * + * Equivalent to @ref Nanoseconds(ZeroInitT). + */ + /* Needs to be Math::Nanoseconds here and in all other places because + older Clang and both MSVC 2015 and 2017 treat it as a template + instance Nanoseconds instead of a Nanoseconds template */ + constexpr /*implicit*/ Nanoseconds() noexcept: Unit{ZeroInit} {} + + /** @brief Construct a zero time */ + constexpr explicit Nanoseconds(ZeroInitT) noexcept: Unit{ZeroInit} {} + + /** @brief Construct without initializing the contents */ + explicit Nanoseconds(Magnum::NoInitT) noexcept: Unit{Magnum::NoInit} {} + + /** @brief Explicit constructor from a unitless type */ + constexpr explicit Nanoseconds(T value) noexcept: Unit{value} {} + + /** @brief Copy constructor */ + /* Needed in order to make arithmetic operations (which have a Unit + return type) convertible to Nanoseconds */ + constexpr /*implicit*/ Nanoseconds(Unit other) noexcept: Unit(other) {} + + /** + * @brief Construct nanoseconds from seconds + * + * The floating-point value is multiplied by a billion and rounded. + */ + template constexpr /*implicit*/ Nanoseconds(Unit value) noexcept; + + /** @brief Construct nanoseconds from external representation */ + template::from(std::declval()))> constexpr explicit Nanoseconds(const U& other) noexcept: Nanoseconds{Implementation::NanosecondsConverter::from(other)} {} + + /** @brief Convert nanoseconds to external representation */ + template::to(std::declval>()))> constexpr explicit operator U() const { + return Implementation::NanosecondsConverter::to(*this); + } +}; + +/* Doxygen can't match these to the class, meh */ +#ifndef DOXYGEN_GENERATING_OUTPUT +template<> constexpr Nanoseconds Nanoseconds::min() { + /* There's apparently no way to say -0x8000000000000000ll. C++, LOL. */ + return Nanoseconds{Long(0x8000000000000000ull)}; +} +template<> constexpr Nanoseconds Nanoseconds::max() { + return Nanoseconds{0x7fffffffffffffffll}; +} +#endif + +/** +@brief Seconds +@m_since_latest + +Represents a floating-point second value. Compared to @ref Nanoseconds, the +@ref Magnum::Seconds typedef uses a 32-bit float which offers a +microsecond-level precision and a reasonable range for scenarios where storing +a full 64-bit nanosecond value isn't needed. See @ref Nanoseconds for more +information and usage examples. +*/ +template class Seconds: public Unit { + public: + /** + * @brief Default constructor + * + * Equivalent to @ref Seconds(ZeroInitT). + */ + /* Needs to be Math::Seconds here and in all other places because + older Clang and both MSVC 2015 and 2017 treat it as a template + instance Seconds instead of a Seconds template */ + constexpr /*implicit*/ Seconds() noexcept: Unit{ZeroInit} {} + + /** @brief Construct a zero time */ + constexpr explicit Seconds(ZeroInitT) noexcept: Unit{ZeroInit} {} + + /** @brief Construct without initializing the contents */ + explicit Seconds(Magnum::NoInitT) noexcept: Unit{Magnum::NoInit} {} + + /** @brief Explicit constructor from a unitless type */ + constexpr explicit Seconds(T value) noexcept: Unit{value} {} + + /** @brief Copy constructor */ + /* Needed in order to make arithmetic operations (which have a Unit + return type) convertible to Seconds */ + constexpr /*implicit*/ Seconds(Unit other) noexcept: Unit(other) {} + + /** + * @brief Construct seconds from nanoseconds + * + * A floating-point value can accurately only represent microseconds + * and only in a limited range, so the conversion may result in some + * precision loss. + */ + template constexpr /*implicit*/ Seconds(Unit value) noexcept; + + /** @brief Construct seconds from external representation */ + template::from(std::declval()))> constexpr explicit Seconds(const U& other) noexcept: Seconds{Implementation::SecondsConverter::from(other)} {} + + /** @brief Convert seconds to external representation */ + template::to(std::declval>()))> constexpr explicit operator U() const { + return Implementation::SecondsConverter::to(*this); + } +}; + +/* Unlike STL, where there's e.g. std::literals::string_literals with both + being inline, here's just the second inline because making both would cause + the literals to be implicitly available to all code in Math. Which isn't + great if there are eventually going to be conflicts. In case of STL the + expected use case was that literals are available to anybody who does + `using namespace std;`, that doesn't apply here as most APIs are in + subnamespaces that *should not* be pulled in via `using` as a whole. */ +namespace Literals { + /** @todoc The inline causes "error: non-const getClassDef() called on + aliased member. Please report as a bug." on Doxygen 1.8.18, plus the + fork I have doesn't even mark them as inline in the XML output yet. And + it also duplicates the literal reference to parent namespace, adding + extra noise. Revisit once upgrading to a newer version. */ + #ifndef DOXYGEN_GENERATING_OUTPUT + inline + #endif + namespace TimeLiterals { + +/* Note on literal naming: while the STL in C++14 uses `ns`, `us`, `s` and + https://en.cppreference.com/w/cpp/chrono/operator%22%22s claims that having + `s` for both a string literal, taking const char*, and a second literal, + taking long double, "just works", at least on GCC and Clang it's only true + if both are defined next to each other IN THE SAME NAMESPACE. Which + completely breaks any library composability or encapsulation, as it means I + can not have a StringView _s literal defined in Corrade::Containers and a + Nanosecond _s literal defined in Magnum::Math. The only workaround I found + was to do something along the lines of the following, which was NOT NICE at + all (and no, `using namespace Containers::Literals` wasn't enough). + + namespace Magnum { namespace Math { namespace Literals { + using Containers::Literals::operator""_s; + }}} + + Thus I'm choosing a different name to prevent the conflict from even + happening. On the other hand, that's probably a better solution even without + the above design issue in the C++ spec, because it prevents potential + conflicts with _s / _us being eventually used for Short and UnsignedShort + literals, or _h conflicting between half-float and hour literals (now it'd + be _hr). Even C++14 picked `min` for minutes instead of `m` because it + apparently seemed to become problematic once/if distance literals for meters + and such get introduced. So why not go with the more clear name for + everything already. Seeing 15.0_sec in unfamiliar code doesn't feel + ambiguous, seeing 127_s or 0.5_h definitely does. */ + +/** @relatesalso Magnum::Math::Nanoseconds +@brief Nanosecond value literal +@m_since_latest + +Compared to the microsecond, millisecond and second literals, this literal is +an integer value and not a floating-point, as it's not possible to represent +fractions of nanoseconds. Usage example: + +@snippet MagnumMath.cpp _nsec + +@see @link operator""_usec() @endlink, @link operator""_msec() @endlink, + @link operator""_sec() @endlink +@m_keywords{_nsec nsec} +*/ +constexpr Nanoseconds operator "" _nsec(unsigned long long value) { + return Nanoseconds{Long(value)}; +} + +/** @relatesalso Magnum::Math::Nanoseconds +@brief Microsecond value literal +@m_since_latest + +As the value is converted to whole nanoseconds, everything after thousandths is +truncated. Additionally, up to thousandths the conversion is without precision +loss only on systems with a 80-bit @cpp long double @ce (which has a 63-bit +mantissa). If you need to ensure nanosecond-level precision on systems that +have a 64-bit @cpp long double @ce, use @link operator""_nsec() @endlink +instead. On the other hand, if nanosecond-level precision isn't needed, it's +possible to convert directly to @ref Seconds that offer a microsecond-level +precision on a range of roughly ±8 seconds. For example: + +@snippet MagnumMath.cpp _usec + +@see @link operator""_msec() @endlink, @link operator""_sec() @endlink, + @ref CORRADE_LONG_DOUBLE_SAME_AS_DOUBLE +@m_keywords{_usec usec} +*/ +constexpr Nanoseconds operator "" _usec(long double value) { + return Nanoseconds{Long(value*1000.0l)}; +} + +/** @relatesalso Magnum::Math::Nanoseconds +@brief Millisecond value literal +@m_since_latest + +As the value is converted to whole nanoseconds, everything after millionths is +truncated. Additionally, up to millionths the conversion is without precision +loss only on systems with a 80-bit @cpp long double @ce (which has a 63-bit +mantissa). If you need to ensure nanosecond-level precision on systems that +have a 64-bit @cpp long double @ce, use @link operator""_nsec() @endlink +instead. On the other hand, if nanosecond-level precision isn't needed, it's +possible to convert directly to @ref Seconds that offer a millisecond-level +precision on a range of roughly ±2 hours. For example: + +@snippet MagnumMath.cpp _msec + +@see @link operator""_usec() @endlink, @link operator""_sec() @endlink, + @ref CORRADE_LONG_DOUBLE_SAME_AS_DOUBLE +@m_keywords{_msec msec} +*/ +constexpr Nanoseconds operator "" _msec(long double value) { + return Nanoseconds{Long(value*1000000.0l)}; +} + +/** @relatesalso Magnum::Math::Nanoseconds +@brief Second value literal +@m_since_latest + +As the value is converted to whole nanoseconds, everything after billionths is +truncated. Additionally, up to billionths the conversion is without precision +loss only on systems with a 80-bit @cpp long double @ce (which has a 63-bit +mantissa). If you need to ensure nanosecond-level precision on systems that +have a 64-bit @cpp long double @ce, use @link operator""_nsec() @endlink +instead. On the other hand, if nanosecond-level precision isn't needed, it's +possible to convert directly to @ref Seconds that offer a millisecond-level +precision on a range of roughly ±2 hours. For example: + +@snippet MagnumMath.cpp _sec + +@see @link operator""_usec() @endlink, @link operator""_msec() @endlink, + @ref CORRADE_LONG_DOUBLE_SAME_AS_DOUBLE +@m_keywords{_sec sec} +*/ +constexpr Nanoseconds operator "" _sec(long double value) { + return Nanoseconds{Long(value*1000000000.0l)}; +} + +}} + +template template constexpr Nanoseconds::Nanoseconds(Unit value) noexcept: Unit{T(static_cast(U(value))*1000000000.0l)} {} + +template template constexpr Seconds::Seconds(Unit value) noexcept: Unit{T(static_cast(U(value))/1000000000.0l)} {} + +/** + * @debugoperator{Nanoseconds} + * @m_since_latest + */ +MAGNUM_EXPORT Utility::Debug& operator<<(Utility::Debug& debug, const Unit& value); + +/** + * @debugoperator{Seconds} + * @m_since_latest + */ +MAGNUM_EXPORT Utility::Debug& operator<<(Utility::Debug& debug, const Unit& value); + +}} + +#endif diff --git a/src/Magnum/Math/TimeStl.h b/src/Magnum/Math/TimeStl.h new file mode 100644 index 000000000..5f16109d6 --- /dev/null +++ b/src/Magnum/Math/TimeStl.h @@ -0,0 +1,87 @@ +#ifndef Magnum_Math_TimeStl_h +#define Magnum_Math_TimeStl_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, + 2020, 2021, 2022, 2023 Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +/** @file +@brief STL @ref std::chrono compatibility for @ref Magnum::Math::Nanoseconds +@m_since_latest + +Including this header allows you to convert a +@ref Magnum::Math::Nanoseconds from and to @ref std::chrono::duration and +@ref std::chrono::time_point. See @ref Math-Nanoseconds-stl for more +information. +*/ + +#include + +#include "Magnum/Math/Time.h" + +namespace Magnum { namespace Math { namespace Implementation { + +/* There's no NanosecondsConverter because this is a typedef + to some integral type, which when simply picks the Nanoseconds(Long) + constructor and not a NanosecondsConverter. C types yay. */ + +template struct NanosecondsConverter>> { + constexpr static Nanoseconds from(std::chrono::duration> other) { + /* The Rep can be floating-point, truncate just the integral part but + only after converting to nanoseconds */ + return Nanoseconds{Long(other.count()*num*(1000000000ll/denom))}; + } + /* No to() because it can be an integer type, losing precision */ + /** @todo add a floating-point-only to() once desirable */ +}; +template struct NanosecondsConverter> { + constexpr static Nanoseconds from(std::chrono::duration other) { + /* The Rep can be floating-point, truncate just the integral part -- + we don't have anything for sub-nanosecond precision */ + return Nanoseconds{Long(other.count())}; + } + constexpr static std::chrono::duration to(Nanoseconds other) { + return std::chrono::duration{Long(other)}; + } +}; + +template struct NanosecondsConverter>>> { + constexpr static Nanoseconds from(std::chrono::time_point>> other) { + return Nanoseconds{other.time_since_epoch().count()*num*(1000000000ll/denom)}; + } + /* No to() because it's an integer type, losing precision */ + /** @todo is there even any std::chrono::time_point that would use a FP + duration type? */ +}; +template struct NanosecondsConverter>> { + constexpr static Nanoseconds from(std::chrono::time_point> other) { + return Nanoseconds{other.time_since_epoch().count()}; + } + constexpr static std::chrono::time_point> to(Nanoseconds other) { + return std::chrono::time_point>{std::chrono::duration{Long(other)}}; + } +}; + +}}} + +#endif