From e3b6b87e93e1f87bc1931e5cf81e976239c4831f Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Tue, 21 Jul 2026 23:58:29 +0200 Subject: [PATCH] Parameters: look for ways to properly clamp signed / unsigned Signed and unsigned integral types have a partial overlap of definition domains, yet due to the automatic promotion rules it is impossible to write a domain clamping function directly, since `std::numeric_limits` returns the domain boundaries in the given type, and `int(-1) < uint(0)` evaluates to false, due to int being is promoted to uint. The attempt to circumvent this problem here relies on a check if the comparison crosses the zero point, and otherwise relies explicitly on the promotion to the number with the larger domain. --- src/lib/par/domain.hpp | 50 +++- tests/lib/par/parameter-domain-test.cpp | 15 +- wiki/thinkPad.ichthyo.mm | 377 +++++++++++++++++++++++- 3 files changed, 432 insertions(+), 10 deletions(-) diff --git a/src/lib/par/domain.hpp b/src/lib/par/domain.hpp index 086a8f391..419a24a32 100644 --- a/src/lib/par/domain.hpp +++ b/src/lib/par/domain.hpp @@ -168,11 +168,46 @@ _Pragma("GCC diagnostic ignored \"-Woverloaded-virtual\"") /* ===== type conversion details ===== */ + /** Constraint: type \a SUB can not represent the full domain of type \a BAS + * @todo implicitly also implements a limitation to »numeric« types, + * due to the use of std::numeric_limits + */ template concept sub_domain = std::signed_integral != std::signed_integral or std::numeric_limits::max() < std::numeric_limits::max() or std::numeric_limits::lowest() > std::numeric_limits::lowest() ; + + template + inline constexpr bool + isNeg (NUM const& num) + { + return num < NUM(0); + }; + + +_Pragma("GCC diagnostic push") \ +_Pragma("GCC diagnostic ignored \"-Wsign-compare\"") +// the following functions are specifically crafted to work around +// the ambiguities caused by automatic signed-to-unsigned promotion; +// furthermore, the automatic promotion allows to perform the comparison. + + template + inline constexpr bool + isLower_safe (X const& x, Y const& y) + { + return isNeg(x) == isNeg(y)? x < y + : isNeg(x); + } + + template + inline constexpr bool + isLarger_safe (X const& x, Y const& y) + { + return isNeg(x) == isNeg(y)? x > y + : isNeg(y); + } +_Pragma("GCC diagnostic pop") /** generic helper to conform into a common value domain * @tparam SUB the implied target value domain @@ -187,14 +222,23 @@ _Pragma("GCC diagnostic ignored \"-Woverloaded-virtual\"") return value; } - /** special case: clamp the source value into the target value domain */ - template requires sub_domain + /** special case: clamp the source value into the target value domain. + * @remark tricky due to automatic conversions between signed/unsigned */ + template requires (sub_domain + and not same_as) inline constexpr BAS preClamp (BAS const& rawVal) { + static_assert (std::is_constructible_v + ,"Value domains must overlap at least partially " + "and values must be cross-constructible within overlap"); + auto upperBound = numeric_limits::max(); auto lowerBound = numeric_limits::lowest(); - return util::limited (lowerBound, rawVal, upperBound); + + return isLarger_safe(rawVal, upperBound)? BAS(upperBound) + : isLower_safe (rawVal, lowerBound)? BAS(lowerBound) + : rawVal; } /** special case: trigger a bool at the 0.5 level */ diff --git a/tests/lib/par/parameter-domain-test.cpp b/tests/lib/par/parameter-domain-test.cpp index 97ad9d4b5..26d852f44 100644 --- a/tests/lib/par/parameter-domain-test.cpp +++ b/tests/lib/par/parameter-domain-test.cpp @@ -91,17 +91,24 @@ SHOW_EXPR(preClamp (-55)) SHOW_EXPR((sub_domain)) SHOW_EXPR((sub_domain)) SHOW_EXPR((sub_domain)) +SHOW_EXPR((sub_domain)) SHOW_EXPR(std::numeric_limits::max()) SHOW_EXPR(std::numeric_limits::max()) SHOW_EXPR(std::numeric_limits::lowest()) SHOW_EXPR(std::numeric_limits::lowest()) -// concept sub_domain = std::numeric_limits::max() < std::numeric_limits::max() -// or std::numeric_limits::lowest() > std::numeric_limits::lowest() -//SHOW_EXPR(std::numeric_limits::lowest() > std::numeric_limits::lowest()) -//SHOW_EXPR(abs(std::numeric_limits::lowest()) < (std::numeric_limits::lowest())) SHOW_EXPR(preClamp (INT64_MAX)) SHOW_EXPR(preClamp (INT64_MIN)) + +SHOW_EXPR(preClamp (numeric_limits::max())) +SHOW_EXPR(preClamp (numeric_limits::lowest())) + +SHOW_EXPR(preClamp (numeric_limits::max())) +SHOW_EXPR(preClamp (numeric_limits::lowest())) + +SHOW_EXPR(preClamp (0.5)); +SHOW_EXPR(preClamp (0.5 + numeric_limits::epsilon())) +SHOW_EXPR(preClamp (numeric_limits::min())) } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index a529b4234..94b0d7efd 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -81663,15 +81663,22 @@ - +

- andernfalls muß ich eine VTable mitschleppen, und das würde mir Bauchschmerzen machen, da die Parameter-Auswertund nun doch in die Render-Engine hineinreicht + andernfalls muß ich eine VTable mitschleppen, und das würde mir Bauchschmerzen machen, da die Parameter-Auswertung nun doch in die Render-Engine hineinreicht

+ + + + +
+ +
@@ -82662,7 +82669,8 @@ - + + @@ -83122,6 +83130,275 @@ + + + + + + +

+ #include <type_traits> +

+

+ #include <limits> +

+

+ #include <cmath>    // std::isnan, std::trunc, std::llround (if needed) +

+

+ +

+

+ template<typename X, typename U> +

+

+ constexpr X safe_clamp_cast(U value) noexcept { +

+

+     using Dx = std::numeric_limits<X>; +

+

+     using Du = std::numeric_limits<U>; +

+

+ +

+

+     // If target can represent all values of source, direct cast is safe. +

+

+     if constexpr (std::is_same_v<X, U> || +

+

+                   (std::is_integral_v<X> && std::is_integral_v<U> && +

+

+                    (std::is_signed_v<X> == std::is_signed_v<U>) && +

+

+                    (sizeof(X) >= sizeof(U))) || +

+

+                   (std::is_floating_point_v<X> && std::is_floating_point_v<U> && +

+

+                    (sizeof(X) >= sizeof(U)))) { +

+

+         return static_cast<X>(value); +

+

+     } +

+

+ +

+

+     // Handle floating source separately +

+

+     if constexpr (std::is_floating_point_v<U>) { +

+

+         if (std::isnan(value)) { +

+

+             // policy: map NaN to 0 (could be Dx::lowest() or Dx::min() depending on needs) +

+

+             return static_cast<X>(0); +

+

+         } +

+

+         long double v = static_cast<long double>(value); +

+

+         if (v <= static_cast<long double>(Dx::lowest())) return static_cast<X>(Dx::lowest()); +

+

+         if (v >= static_cast<long double>(Dx::max()))    return static_cast<X>(Dx::max()); +

+

+         // when casting float->int, truncate toward zero (same as static_cast) +

+

+         return static_cast<X>(v); +

+

+     } +

+

+ +

+

+     // Now source is integral (U), target X might be integral or floating +

+

+     if constexpr (std::is_integral_v<U> && std::is_floating_point_v<X>) { +

+

+         // integer -> float: float may not represent huge integers exactly, but no UB; clamp to finite range +

+

+         long double v = static_cast<long double>(value); +

+

+         if (v <= static_cast<long double>(Dx::lowest())) return static_cast<X>(Dx::lowest()); +

+

+         if (v >= static_cast<long double>(Dx::max()))    return static_cast<X>(Dx::max()); +

+

+         return static_cast<X>(v); +

+

+     } +

+

+ +

+

+     // Both integral but possibly differing signedness/range +

+

+     if constexpr (std::is_integral_v<X> && std::is_integral_v<U>) { +

+

+         if constexpr (std::is_signed_v<U> && !std::is_signed_v<X>) { +

+

+             // signed source -> unsigned target +

+

+             if (value < 0) return static_cast<X>(0); +

+

+             using UT_prom = std::make_unsigned_t<U>; +

+

+             UT_prom uv = static_cast<UT_prom>(value); +

+

+             if (uv > static_cast<std::make_unsigned_t<X>>(Dx::max())) return static_cast<X>(Dx::max()); +

+

+             return static_cast<X>(uv); +

+

+         } else if constexpr (!std::is_signed_v<U> && std::is_signed_v<X>) { +

+

+             // unsigned source -> signed target +

+

+             using UX_prom = std::make_unsigned_t<X>; +

+

+             if (value > static_cast<std::make_unsigned_t<U>>(Dx::max())) return static_cast<X>(Dx::max()); +

+

+             return static_cast<X>(value); // safe +

+

+         } else { +

+

+             // same signedness but different sizes +

+

+             if constexpr (sizeof(U) > sizeof(X)) { +

+

+                 if (value > static_cast<U>(Dx::max())) return static_cast<X>(Dx::max()); +

+

+                 if (std::is_signed_v<U> && value < static_cast<U>(Dx::lowest())) return static_cast<X>(Dx::lowest()); +

+

+             } +

+

+             return static_cast<X>(value); +

+

+         } +

+

+     } +

+

+ +

+

+     // Fallback +

+

+     return static_cast<X>(value); +

+

+ } +

+ + +
+ + +
+
+ + + + + +

+ Unter der Annahme, daß lediglich die signed/unsigned Integers das Problem sind; denn für jeden Integer-Typ kann man val < TY(0) bilden +

+ + +
+ + + + + +

+ die Funktion "noneg()" macht den Verlgeich genau falsch herum:  0 < uint(-1) ⟹ true +

+ + +
+ +
+
+ + + + + +

+ hier hilft uns die automatische Promotion in den Typ mit dem höheren Rank: nur deshalb können wir die Vergleichsoperation machen. Zudem kann der Verleich im negativen Segment überhaupt nur stattfinden, wenn beide Typen negativ sind, also mithin signed +

+ + +
+
+ + + + + + + + + + + + + + +
@@ -83167,6 +83444,9 @@
+ + +
@@ -83186,6 +83466,97 @@
+ + + + + + +

+ hier steckt das Thema Speicher, deep-copy, interned-strings, Konstanten, Inline-IDs? Vielleicht genügt guter Support für Ennumerationen? +

+ +
+
+ + + + +

+ gemeint ist: man muß die Ennumeratoren irgendwo in Textform vorhalten, und muß parsen können; einzelne Ennumeratoren könnten dynamisch hinzugefügt werden, oder gar ganze Finite-Domains per Konfiguration oder dynamischer Definition. Hier tut sich ein Abgrund an Komplexität auf — im Grunde verweist das direkt in den Themenkomplex »Domain-Ontology« +

+ +
+
+ + + + +

+ was heißt hier »numerisch«? +

+
    +
  • + sind bool und char mit eingeschlossen? +
  • +
  • + wollen wir spezielle Custom-Types, wie Big-Deciamal varianten hier miterfassen? +
  • +
  • + was ist mit Brüchen (boost::rational) und Komplexen Zahlen? Bitte nicht lachen... +
  • +
  • + wollen wir auf das Konzept einer Algebra abstellen? Also zumindest eine Gruppe sowie eine Ordnungsrelation? +
  • +
+ + +
+
+ + + + +

+ Im ersten Entwurf habe ich die unterliegenden Typen einfach in eine Typelist gepackt. Die Analyse zeigte ganz klar, daß diese Menge der Basiswerte bereits zur Compile-time festgelegt sein muß, sonst tun sich ganz ekelhafte Probleme auf mit Registrierung und Locking im Zusammenhang mit dem Laden von dynamischen Objekten (Plug-ins).... +

+ + +
+
+ + + + +

+ Ein billiger Ansatz wäre, das Thema zu ignorieren, und solche Parameter einfach als eine Gruppe von Einzelparametern zu behandeln. Das wäre angemessen, wenn man das als Randthema betrachtet. Gegen diesen Ansatz spricht, daß man an einen Farbwert auch eine Kalibrierungskurve und einen Farbraum anhängen möchte, sonst wird Arbeiten mit professionellen Medien-Produktionsprozessen (HDR, Grading, Mastering) praktisch unmöglich. Auch an verschiedene Spielarten von »3D« sollte man denken, incl. virtueller Projektionsräume. Ein MIttelweg wäre, daß man Parameter-Tupel unterstützt, denen Metadaten angehängt werden +

+ + +
+
+ + + + +

+ ....zunächst einmal sind das Produkttypen, aber mit einer hohen Kardinalität. Das Konzept der Shape-Keys reduziert das wieder auf eine Achse, auf der man einen Kontrollparameter animieren kann. Bei Masken besteht das zusätzliche (in der Praxis sehr relevante) Problem, daß »unterwegs« weitere Maskenpunkte dazukommen können, und daß einzelne Maskenpunkte noch weitere Parameter tragen können, wie z.B. eine feather-strength, was extrem wichtig ist für Rotoscoping. +

+ + +
+
+ + + + +

+ ...das heißt, die einfachen Parameter wären tatsächlich in der Speichergröße beschränkt (z.B. 4 Slots) und könnten stets inline transportiert werden; nur für diese einfachen Parameter gäbe es den Ansatz eines generischen Parameters. Wohingegen die komplexen Parameter  von Vornherein symbolisch repräsentiert werden, und nur als Funktion zugänglich sind, möglicherweise überhaupt nur als Resultat einer Animations-Auswertung auf Basis einfacher Steuerparameter. Damit würden im »Parameter-System« auch zwei Zonen entstehen; die einfachen Parameter wären im Kernsystem festgelegt, die Basis-Parameter vielleicht sogar zur Compile-Zeit— wohingegen alle komplexen Parameter in eine »Domain-Ontology« gehören und indirekt über ein Plug-in angesprochen werden.... +

+ +
+
+