From ce3713d8726078f8ea9895654d05a4f48f1903ad Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Fri, 9 Dec 2022 19:37:35 +0100 Subject: [PATCH] Timeline: now able to increase to maximum pixel count There is a built-in limitation, which now is even lowered to 100000 pixels horizontally. With the techniques introduced in this changeset, it seems possible to support more -- yet this would be a case of unnecessary genricity; handling such large numbers will drive more computations into the danger zone, and doing so incurs cost in terms of testing and debugging. Placing that into context, contemporary displays are not even 4K on average, and it does not look likely even for cinema display to go way beyond 8k -- so yes, I want display hardware with 100000 pixels!! The key takeaway of this changeset: - can calculate px = trunc(zoomFactor * duration) step wise, even when the direct calculation would lead to wrap-around - can safely adjust and fix the zoomFactor using Newton approximation --- src/stage/model/zoom-window.hpp | 69 +++++- tests/stage/model/zoom-window-test.cpp | 25 ++ wiki/thinkPad.ichthyo.mm | 312 +++++++++++++++++++++++-- 3 files changed, 386 insertions(+), 20 deletions(-) diff --git a/src/stage/model/zoom-window.hpp b/src/stage/model/zoom-window.hpp index e38d24198..544187266 100644 --- a/src/stage/model/zoom-window.hpp +++ b/src/stage/model/zoom-window.hpp @@ -175,7 +175,7 @@ namespace model { namespace {// initial values (rather arbitrary) const FSecs DEFAULT_CANVAS{23}; const Rat DEFAULT_METRIC{25}; - const uint MAX_PX_WIDTH{1000000}; + const uint MAX_PX_WIDTH{100000}; const FSecs MAX_TIMESPAN{_FSecs(Duration::MAX)}; const FSecs MICRO_TICK{1_r/Time::SCALE}; @@ -259,7 +259,7 @@ namespace model { pxWidth() const { REQUIRE (startWin_ < afterWin_); - return rational_cast (px_per_sec() * FSecs(afterWin_-startWin_)); + return calcPixelsForDurationAtScale (px_per_sec(), afterWin_-startWin_); } @@ -677,6 +677,53 @@ namespace model { : start + Time{DEFAULT_CANVAS}; } + /** Assertion helper: resulting pxWidth matches expectations */ + static void + __assertMatchesExpectedPixWidth (Rat zoomFactor, FSecs duration, uint pxWidth) + { + auto sizeAtRequestedScale = calcPixelsForDurationAtScale (zoomFactor, duration); + ENSURE (abs(pxWidth - sizeAtRequestedScale) <= 1 + ,"ZoomWindow: established size or metric misses expectation " + "by more than 1px. %upx != %1.6f expected pixel." + , pxWidth, sizeAtRequestedScale); + } + + static void + __assertMetricConforms2pxWidth (Rat zoomFactor, FSecs duration, uint pxWidth) + { + if (util::can_represent_Product(zoomFactor, duration)) + { + ENSURE (pxWidth == rational_cast (zoomFactor*duration) + ,"ZoomWindow: established zoom factor misses expected " + "width in pixels: %upx != %upx (resulting)" + , pxWidth, rational_cast (zoomFactor*duration)); + } + else + { + auto sizeAtRequestedScale = approx(zoomFactor) * approx(duration); + ENSURE (pxWidth == uint(sizeAtRequestedScale + 0.5) + ,"ZoomWindow: established zoom factor misses expected " + "width in pixels: %upx != %1.8f px (resulting)" + , pxWidth, sizeAtRequestedScale); + } + } + + /** @remark indirect calculation path to avoid overflow on large durations */ + static int64_t + calcPixelsForDurationAtScale (Rat zoomFactor, FSecs duration) + {// calculate rational_cast (zoomFactor * duration) + auto zn = zoomFactor.numerator(); + auto zd = zoomFactor.denominator(); + auto dn = duration.numerator(); + auto dd = duration.denominator(); + auto [secs,r] = util::iDiv (dn, dd); // split duration in full seconds and rest + auto [px1,r1] = util::iDiv (secs*zn, zd); // calc pixels required for full seconds + auto [px2,r2] = util::iDiv (r*zn, dd*zd); // calc pixels required for rest duration + auto pxr = (r1*dd +r2) /(dd*zd); // and calculate integer div for combined remainders + ENSURE (0 <= px1 and 0 <= px2 and 0<= pxr); + return px1 + px2 + pxr; + } + /** window size beyond that limit would yield * numerically dangerous zoom factors */ static FSecs @@ -706,7 +753,20 @@ namespace model { REQUIRE (afterWin_> startWin_); FSecs dur{afterWin_-startWin_}; Rat adjMetric = detox (Rat(pxWidth) / dur); - ENSURE (pxWidth == rational_cast (adjMetric*dur)); + // check if new metric reproduces expected pxWidth... + for (uint resPx + ;pxWidth != (resPx = calcPixelsForDurationAtScale (adjMetric, dur)) // calculate trunc {adjMetric*dur} + ; // but calculate cleverly to avoid numeric wrap + ) // Problem: detox() introduced too much error + { // need to adjust metric to match expected pxWidth + // Using Newton-Raphson; can't just calculate fix due to numeric-wraparound + auto delta = double(resPx) - pxWidth; // note: approximation calculated in double + int64_t mn{adjMetric.numerator()}, dn{dur.numerator()}, + md{adjMetric.denominator()}, dd{dur.denominator()}; + //assuming: f(xₙ) ≔ Δₙ = resPx-pxWidth = trunc{ xₙ * dn/(md*dd) } - pxWidth + mn -= int64_t(delta * md*dd / dn); // xₙ₁ ≔ xₙ - f(xₙ)/f'(xₙ) + adjMetric = Rat{mn, md}; + } return adjMetric; } @@ -731,8 +791,7 @@ namespace model { // re-check metric to maintain precise pxWidth px_per_sec_ = conformMetricToWindow (pxWidth); ENSURE (_FSecs(afterWin_-startWin_) < MAX_TIMESPAN); - auto sizeAtRequestedScale = approx(changedMetric)*approx(dur); - ENSURE (abs(pxWidth - sizeAtRequestedScale) <= 1); + __assertMatchesExpectedPixWidth (changedMetric, dur, pxWidth); } /** diff --git a/tests/stage/model/zoom-window-test.cpp b/tests/stage/model/zoom-window-test.cpp index 46ba63d13..fb98b4411 100644 --- a/tests/stage/model/zoom-window-test.cpp +++ b/tests/stage/model/zoom-window-test.cpp @@ -680,6 +680,7 @@ namespace test { void safeguard_extremeZoomOut() { + /*--Test-1-----------*/ ZoomWindow win{3, TimeSpan{_t(-1,2), _t(1,2)}}; // setup ZoomWindow to very small pixel size (3px) CHECK (win.overallSpan().duration() == _t(1)); CHECK (win.px_per_sec() == 3_r/1); @@ -690,6 +691,7 @@ namespace test { CHECK (win.px_per_sec() == 3_r/1); CHECK (win.pxWidth() == 3); + /*--Test-2-----------*/ Rat bruteZoom{3_r/(int64_t{1}<<60)}; win.setMetric (bruteZoom); // zoom out beyond what is possible and to a toxic factor @@ -705,11 +707,13 @@ namespace test { CHECK (win.px_per_sec() == 3 / _FSecs(win.visible().duration())); // and this value also conforms with the pixel size and window duration CHECK (win.pxWidth() == 3); + /*--Test-3-----------*/ win.setMetric (5_r/std::numeric_limits::max()); // same limiting applies to even more nasty values CHECK (_raw(win.visible().duration()) == 3298534883328); // still unchanged at limit CHECK (win.px_per_sec() == 15625_r/17179869184); CHECK (win.pxWidth() == 3); + /*--Test-4-----------*/ win.setMetric (1000001_r/LIM_HAZARD); // but zooming in more than that limit will be honored CHECK (_raw(win.visible().duration()) == 3298531584796); // ...window now slightly reduced in size CHECK (_raw(win.visible().duration()) < 3 * LIM_HAZARD); @@ -718,6 +722,27 @@ namespace test { CHECK (win.px_per_sec() > 1000001_r/LIM_HAZARD); // (this is what was requested) CHECK (win.px_per_sec() < 1000002_r/LIM_HAZARD); // (thus result was slightly adjusted upwards) CHECK (win.pxWidth() == 3); + + /*--Test-5-----------*/ + win.calibrateExtension (1'000'000'000); // implicit drastic zoom-out by increasing the number of pixels + CHECK (win.pxWidth() < 1'000'000'000); // however: this number is capped at a fixed maximum + CHECK (win.pxWidth() == MAX_PX_WIDTH); // (which „should be enough“ for the time being...) + CHECK (win.px_per_sec() == 508631_r/559239974093); // the zoom metric has been adapted, but to a sanitised value + + CHECK (_raw(win.overallSpan().duration()) == 614891469123651720); // overall canvas duration not changed + CHECK (_raw(win.visible().duration()) == 109951052826533333); // window duration now drastically increased + CHECK (win.overallSpan().end() == TimeValue{307445734561825860}); // window is somewhere within canvas + CHECK (win.visible().end() == TimeValue{109949403560740935}); // but was expanded to the right, + CHECK (win.visible().start() == TimeValue{ -1649265792398}); // ... retaining the start value + + // Note: these parameters build up to really »poisonous« values.... + CHECK (MAX_PX_WIDTH / _FSecs(win.visible().duration()) == 100000000000_r/109951052826533333); + CHECK (win.px_per_sec() * _FSecs(win.visible().duration()) < 0); // we can't even calculate the resulting pxWidth() naively + CHECK (rational_cast(win.px_per_sec()) // ...while effectively these values are still correct + * rational_cast(_FSecs(win.visible().duration())) == 100000.914f); + CHECK (rational_cast(100000000000_r/109951052826533333) == 9.09495611e-07f); // theoretical value + CHECK (rational_cast(win.px_per_sec()) == 9.09503967e-07f); // value actually chosen + CHECK (win.px_per_sec() == 508631_r/559239974093); } diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 880744bac..87839c7fa 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -39597,11 +39597,43 @@ + + + + + + + + + + + +

+ gelöst durch geschickte schrittweise Berechnung: +

+

+ calcPixelsForDurationAtScale (Rat zoomFactor, FSecs duration) +

+ +
+ + + + + +

+ Wir nutzen explizit aus, daß das Ergebnis durch truncating quantisiert wird (Integer-Division). Daher können wir erst den ersten Dividenden (die Zeitspanne) aufteilen, und zwar erst mal in den Sekunden-Anteil, dann den Rest, und dann den Rest vom Rest +

+ +
+ +
+
- + - + @@ -39614,16 +39646,21 @@ - + + + + + + - + - + @@ -39659,10 +39696,10 @@ - + - + @@ -39765,14 +39802,35 @@ - - - - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -39824,8 +39882,8 @@ - - + + @@ -40122,12 +40180,208 @@ + + + + + + + + + + + + + + + + + + + + + +

+ sofern wir überhaupt relevant große Zeitspannen zulassen wollen +

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

+ latente Gefahr: Rest bei giftiger Duration +

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

+ das unterschlägt den Divisionsrest. Oder anders gesagt, die Integer-Division ist keine lineare Funktion, die man einfach so umkehren kann +

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

+ anders im Nenner;  md * dd ist garantiert berechenbar, sofern die Duration selber aus einem time::Duration gebildet wurde (und md ist durch das detox() limitiert worden) +

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

+ Der Rechenweg ist hier eine »Einbahnstraße« : durch einen Kniff ist es gelungen die Quatisierung zu berechnen Metrik ⟼ pxWidth. Aber die Umkehrfunktion können wir nicht berechnen, weil es in der Berechnung zu einem Überlauf kommt. Daher können wir die Fehler-Korrektur nicht einfach ausrechnen, weil wir nicht einfach von einen Pixel-Δ auf ein Metrik-Δ zurückrechnen können +

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

+ ...das mag unsinnig erscheinen, aber der Punkt ist, wir können diese lineare Beziehung nur eingeschränkt numerisch berechnen +

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
- + + + + + + + + + + + + + @@ -40141,7 +40395,7 @@ - + @@ -62758,6 +63012,34 @@ + + + + + + + + + + + + + + + + + + +

+ Die Methode steht und fällt damit, daß die Tangente näher zur Lösung zeigt, als der aktuelle (Start)Wert. Sonst kann die Iteration in weite Oszillationen münden und sich ggfs sogar in einem neben-Minimum festbeißen. Die Newton-Methode spielt daher ihr Potential einer senationell schnellen Konvergenz nur aus, wenn man anderweitig schon eine sehr gute Näherung als Startwert hat. +

+ +
+ +
+
+
+