diff --git a/src/lib/rational.hpp b/src/lib/rational.hpp index 9fbe260e6..03b6d2a12 100644 --- a/src/lib/rational.hpp +++ b/src/lib/rational.hpp @@ -139,6 +139,13 @@ namespace util { < 63; } + inline bool + can_represent_Product (Rat a, Rat b) + { + return can_represent_Product(a.numerator(), b.numerator()) + and can_represent_Product(a.denominator(), b.denominator()); + } + inline bool can_represent_Sum (Rat a, Rat b) { diff --git a/src/lib/time/time.cpp b/src/lib/time/time.cpp index 4991a454b..c25ba27c7 100644 --- a/src/lib/time/time.cpp +++ b/src/lib/time/time.cpp @@ -283,8 +283,11 @@ namespace time { /** constant to indicate "no duration" */ - const Duration Duration::NIL (Time::ZERO); + const Duration Duration::NIL {Time::ZERO}; + /** maximum possible temporal extension */ + const Duration Duration::MAX {Offset{Time::MIN, Time::MAX}}; + }} // namespace lib::Time diff --git a/src/lib/time/timevalue.hpp b/src/lib/time/timevalue.hpp index f56983f2e..85946fcfb 100644 --- a/src/lib/time/timevalue.hpp +++ b/src/lib/time/timevalue.hpp @@ -480,6 +480,7 @@ namespace time { { } static const Duration NIL; + static const Duration MAX ; void accept (Mutation const&); diff --git a/src/stage/model/zoom-window.hpp b/src/stage/model/zoom-window.hpp index 42f39cc1e..5d59ff262 100644 --- a/src/stage/model/zoom-window.hpp +++ b/src/stage/model/zoom-window.hpp @@ -111,6 +111,8 @@ namespace model { using util::Rat; using util::rational_cast; + using util::can_represent_Product; + using util::reQuant; using util::min; using util::max; @@ -166,7 +168,7 @@ namespace model { const FSecs DEFAULT_CANVAS{23}; const Rat DEFAULT_METRIC{25}; const uint MAX_PX_WIDTH{1000000}; - const FSecs MAX_TIMESPAN{_FSecs(Time::MAX-Time::MIN)}; + const FSecs MAX_TIMESPAN{_FSecs(Duration::MAX)}; const FSecs MICRO_TICK{1_r/Time::SCALE}; /** Maximum quantiser to be handled in fractional arithmetics without hazard. @@ -174,15 +176,15 @@ namespace model { * DENOMINATOR * Time::Scale has to stay below INT_MAX, with some safety margin */ const int64_t LIM_HAZARD{int64_t{1} << 40 }; + const int64_t HAZARD_DEGREE{util::ilog2(LIM_HAZARD)}; inline int - toxicDegree (Rat poison) + toxicDegree (Rat poison, const int64_t THRESHOLD =HAZARD_DEGREE) { - const int64_t HAZARD_DEGREE{util::ilog2(LIM_HAZARD)}; int64_t magNum = util::ilog2(abs(poison.numerator())); int64_t magDen = util::ilog2(abs(poison.denominator())); int64_t degree = max (magNum, magDen); - return max (0, degree - HAZARD_DEGREE); + return max (0, degree - THRESHOLD); } } @@ -426,7 +428,7 @@ namespace model { setVisiblePos (Rat percentage) { FSecs canvasDuration{afterAll_-startAll_}; - anchorWindowAtPosition (canvasDuration*percentage); + anchorWindowAtPosition (scaleSafe (canvasDuration, percentage)); fireChangeNotification(); } @@ -495,18 +497,48 @@ namespace model { * is thus specific to the ZoomWindow implementation. To sanitise, the denominator * is reduced logarithmically (bit-shift) sufficiently and then used as new quantiser, * thus ensuring that both denominator (=quantiser) and numerator are below limit. + * @warning the rational number must not be to large overall; this heuristic will fail + * on fractions with very large numerator and small denominator — however, for + * the ZoomWindow, this case is not relevant, since the zoom factor is limited, + * and other usages of rational numbers can be range checked explicitly. * @note the check is based on the 2-logarithm of numerator and denominator, which is * pretty much the fastest possibility (even a simple comparison would have - * to do the same). Values below threshold are simply passed-through. + * to do the same). Values below threshold are simply passed-through. */ static Rat detox (Rat poison) { int toxicity = toxicDegree (poison); - return toxicity ? util::reQuant(poison, poison.denominator() >> toxicity) + return toxicity ? reQuant (poison, max (poison.denominator() >> toxicity, 64)) : poison; } + /** + * Scale a possibly large time duration by a rational factor, while attempting to avoid + * integer wrap-around. Obviously this is only a heuristic, yet adequate within the + * framework of ZoomWindow, where the end result is pixel aligned anyway. + */ + static FSecs + scaleSafe (FSecs duration, Rat factor) + { + auto approx = [](Rat r){ return rational_cast (r); }; + + if (not util::can_represent_Product(duration, factor)) + { + if (approx(MAX_TIMESPAN) < approx(duration) * approx (factor)) + return MAX_TIMESPAN; // exceeds limits of time representation => cap the result + + // slightly adjust the factor so that the time-base denominator cancels out, + // allowing to calculate the product without dangerous multiplication of large numbers + factor = 1_r / reQuant (1_r/factor, duration.denominator()); + return detox (duration * factor); + } + else + // just calculate ordinary numbers... + return duration * factor; + } + + static Rat establishMetric (uint pxWidth, Time startWin, Time afterWin) { @@ -732,7 +764,7 @@ namespace model { FSecs duration{afterWin_-startWin_}; Rat posFactor = canvasOffset / FSecs{afterAll_-startAll_}; posFactor = parabolicAnchorRule (posFactor); // also limited 0...1 - FSecs partBeforeAnchor = posFactor * duration; + FSecs partBeforeAnchor = scaleSafe (duration, posFactor); startWin_ = startAll_ + (canvasOffset - partBeforeAnchor); establishWindowDuration (duration); startAll_ = min (startAll_, startWin_); @@ -815,6 +847,8 @@ namespace model { parabolicAnchorRule (Rat posFactor) { posFactor = util::limited (0, posFactor, 1); + if (toxicDegree(posFactor, 20)) // prevent integer wrap + posFactor = util::reQuant(posFactor, 1 << 20); posFactor = (2*posFactor - 1); // -1 ... +1 posFactor = posFactor*posFactor*posFactor; // -1 ... +1 but accelerating towards boundaries posFactor = (posFactor + 1) / 2; // 0 ... 1 diff --git a/tests/stage/model/zoom-window-test.cpp b/tests/stage/model/zoom-window-test.cpp index 623f88c53..08323ad11 100644 --- a/tests/stage/model/zoom-window-test.cpp +++ b/tests/stage/model/zoom-window-test.cpp @@ -399,9 +399,11 @@ namespace test { win.setVisiblePos(0.50); CHECK (win.visible() == TimeSpan(_t((40/2-16) -8), FSecs(16))); // window positioned to centre of canvas + CHECK (win.visible().start() == _t(-4)); // (canvas was already positioned asymmetrically) win.setVisiblePos(-0.50); CHECK (win.visible() == TimeSpan(_t(-16-40/2), FSecs(16))); // relative positioning not limited at lower bound + CHECK (win.visible().start() == _t(-36)); // (causing also further expansion of canvas) win.setVisiblePos(_t(200)); // absolute positioning likewise not limited CHECK (win.visible() == TimeSpan(_t(200-16), FSecs(16))); // but anchored according to relative anchor pos CHECK (win.px_per_sec() == 80); // metric retained @@ -580,13 +582,16 @@ namespace test { } - /** @test verify ZoomWindow code can handle "poisonous" Zoom-Factor parameters + /** @test verify ZoomWindow code can handle "poisonous" fractional number parameters + * - toxic zoom factor passed through ZoomWindow::setMetric(Rat) + * - toxic proportion factor passed to ZoomWindow::setVisiblePos(Rat) + * - indirectly cause toxic posFactor in ZoomWindow::anchorWindowAtPosition(FSecs) + * by providing a target position very far off current window location */ void safeguard_poisonousMetric() { - - ZoomWindow win{}; + ZoomWindow win{}; CHECK (win.visible() == win.overallSpan()); // by default window spans complete canvas CHECK (win.visible().duration() == _t(23)); // ...and has just some handsome extension CHECK (win.px_per_sec() == 25); @@ -624,6 +629,26 @@ namespace test { CHECK(856.350708f == rational_cast (_FSecs(win.visible().duration()))); CHECK (win.px_per_sec() == 575000000_r/856350691); // the new metric however is comprised of sanitised fractional numbers CHECK (win.pxWidth() == 575); // and the existing pixel width was not changed + + SHOW_EXPR(win.overallSpan()); + SHOW_EXPR(_raw(win.visible().duration())); + SHOW_EXPR(win.px_per_sec()); + SHOW_EXPR(win.pxWidth()); + win.setVisiblePos (poison); // Yet another way to sneak in our toxic value... + SHOW_EXPR(win.overallSpan()); + SHOW_EXPR(_raw(win.overallSpan())); + SHOW_EXPR(_raw(win.overallSpan().duration())); + SHOW_EXPR(_raw(win.visible().duration())); + SHOW_EXPR(win.px_per_sec()); + SHOW_EXPR(win.pxWidth()); + SHOW_EXPR(_raw(win.overallSpan().duration()) * rational_cast (poison)) + Time targetPos{TimeValue{gavl_time_t(_raw(win.overallSpan().duration()) * rational_cast (poison))}}; + SHOW_EXPR(targetPos); + SHOW_EXPR(_raw(targetPos)); + SHOW_EXPR(_raw(win.visible().start())) + SHOW_EXPR(_raw(win.visible().end())) + SHOW_EXPR(bool(win.visible().start() < targetPos)) + SHOW_EXPR(bool(win.visible().end() > targetPos)) } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 84d681183..cf66a25cd 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -39764,9 +39764,48 @@ + + + + + + + + + + + + + + + + + +

+ und zwar dann, wenn auch noch die Window-Parameter extrem sind — dann sieht die Lage ziemlich hoffnungslos aus +

+ +
+ +
+ + + + + + +

+ ...denn dadurch würde man die kreuzweise Multiplikation verhindern +

+ +
+ + +
+
@@ -39883,8 +39922,12 @@ - + + + + + @@ -39904,7 +39947,7 @@ - + @@ -40275,12 +40318,37 @@ + + + + + + + + + + + + + + + + + +

+ Und zwar, wenn der Nenner viel kleiner ist als der Zähler, und der Zähler extrem groß. Dann würde nämlich die Ganzzahl-Division keine signifikante Verringerung der Dimension bewirken, und die anschließende re-Quantisierung das Ergebnis (bedingt durch die Normierung auf einen gemeinsamen Nenner) sogar noch vergrößern +

+ +
+
+ +
- - + + @@ -40300,6 +40368,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -40307,6 +40395,47 @@ + + + + + + + + + + + + + + + +

+ #--◆--# _raw(win.overallSpan().duration()) ? = 307445734561825860 +

+

+ #--◆--# _raw(targetPos) ?                    = 206435633551724864 +

+

+ #--◆--# _raw(win.visible().start()) ?        =   2248731193323487 +

+

+ #--◆--# _raw(win.visible().end()) ?          =   2248732049674178 +

+

+ #--◆--# bool(win.visible().start() < targetPos) ? = 1 +

+

+ #--◆--# bool(win.visible().end() > targetPos) ? = 0 +

+ +
+ + + + +
+