[dasher: 33/38] Rewrite dynamics!



commit 8ff98264e2f25fc40c66786fe480b18dcc5a468e
Author: Alan Lawrence <acl33 inf phy cam ac uk>
Date:   Tue Nov 22 18:59:24 2011 +0000

    Rewrite dynamics!
    
    Continuous movement works by stepping root node bounds, rather than viewport
     (avoids computing center-of-expansion, which is undefined for translation);
     new approximation using (very) approx. sqrt, or BP_EXACT_DYNAMICS to use pow()
    
    Static (click-mode) works by interpolating growth in log space, with linear
     deceleration (i.e. a quadratic) => moves quickly at first then slows down, in
     all directions.

 Src/DasherCore/DasherModel.cpp   |  275 ++++++++++++++++++++++----------------
 Src/DasherCore/DasherModel.h     |   35 +++---
 Src/DasherCore/DefaultFilter.cpp |    6 +-
 Src/DasherCore/DynamicFilter.cpp |   30 ++---
 Src/DasherCore/FrameRate.cpp     |   41 ++----
 Src/DasherCore/FrameRate.h       |   21 +---
 Src/DasherCore/Parameters.cpp    |    2 +
 Src/DasherCore/Parameters.h      |    4 +-
 8 files changed, 217 insertions(+), 197 deletions(-)
---
diff --git a/Src/DasherCore/DasherModel.cpp b/Src/DasherCore/DasherModel.cpp
index 44d4a9e..174f48a 100644
--- a/Src/DasherCore/DasherModel.cpp
+++ b/Src/DasherCore/DasherModel.cpp
@@ -46,9 +46,10 @@ static char THIS_FILE[] = __FILE__;
 #endif
 #endif
 
-// FIXME - need to get node deletion working properly and implement reference counting
 
-// CDasherModel
+//If preprocessor variable DEBUG_DYNAMICS is defined, will display the difference
+// between computed (approximate) one-step motion, and ideal/exact motion (using pow()).
+//#define DEBUG_DYNAMICS
 
 CDasherModel::CDasherModel() {
   
@@ -303,94 +304,147 @@ bool CDasherModel::NextScheduledStep()
   return false;
 }
 
-void CDasherModel::ScheduleOneStep(myint X, myint Y, int iSteps, dasherint iMinSize) {
-  myint r1, r2;
-  // works out next viewpoint
-
-  DASHER_ASSERT(m_Root != NULL);
-  // Avoid X == 0, as this corresponds to infinite zoom
-  if (X <= 0) X = 1;
+///A very approximate square root. Finds the square root of (just) the
+/// most significant bit, then two iterations of Newton.
+inline dasherint mysqrt(dasherint in) {
+  //1. Find greatest i satisfying 1<<(i<<1) < in; let rt = 1<<i be first approx
+  // but find by binary chop: at first double each time..
+  dasherint i=1;
+  while (dasherint(1)<<4*i < in) i*=2;
+  //then try successively smaller bits.
+  for (dasherint test=i; test/=2;)
+    if (dasherint(1)<<2*(i+test) < in) i+=test;
+  //so, first approx:
+  dasherint rt = 1<<i;
+  rt = (rt+in/rt)/2;//better
+  return (rt+in/rt)/2;//better still
   
-  // If X is too large we risk overflow errors, so limit it
-  dasherint iMaxX = (1 << 29) / iSteps;
-  if (X > iMaxX) X = iMaxX;
+  //Some empirical results (from DEBUG_DYNAMICS, at about 40fps with XLimit=400)
+  // with one iteration, error in rate of data entry is ~~10% near xhair, falls
+  // as we get further away, then abruptly jumps up to 30% near the x limit
+  // (and beyond it, but also before reaching it).
+  //With two iterations, error is 0-1% near xhair, gradually rising to 10%
+  // near/at the x limit.
+  //However, reversing is less good - it can go twice as fast at extreme x...
+}
+
+void CDasherModel::ScheduleOneStep(dasherint y1, dasherint y2, int nSteps, int limX, bool bExact) {
   
-  // Mouse coords X, Y
-  // const dasherint Y1 = 0;
-  const dasherint Y2(MAX_Y);
+  m_deGotoQueue.clear();
   
-  // Calculate what the extremes of the viewport will be when the
-  // point under the cursor is at the cross-hair. This is where
-  // we want to be in iSteps updates
+  // Rename for readability.
+  const dasherint R1 = m_Rootmin;
+  const dasherint R2 = m_Rootmax;
   
-  dasherint y1(Y - (Y2 * X) / (2 * ORIGIN_X));
-  dasherint y2(Y + (Y2 * X) / (2 * ORIGIN_Y));
-  dasherint oy1(y1),oy2(y2); //back these up to use later
+  // Calculate the bounds of the root node when the target range y1-y2
+  // fills the viewport.
+  // This is where we want to be in iSteps updates
+  dasherint targetRange=y2-y1;
+
+  const dasherint r1 = MAX_Y*(R1-y1)/targetRange;
+  const dasherint r2 = MAX_Y*(R2-y1)/targetRange;
   
-  // iSteps is the number of update steps we need to get the point
-  // under the cursor over to the cross hair. Calculated in order to
-  // keep a constant bit-rate.
-  DASHER_ASSERT(iSteps > 0);
+  dasherint m1=(r1-R1),m2=(r2-R2);
   
-  // Calculate the new values of y1 and y2 required to perform a single update
-  // step.
-  {
-    const dasherint denom = Y2 + (iSteps - 1) * (y2 - y1),
-    newy1 = y1 * Y2 / denom,
-    newy2 = ((y2 * iSteps - y1 * (iSteps - 1)) * Y2) / denom;
-    
-    y1 = newy1;
-    y2 = newy2;
-  }
+  //Any interpolation (R1,R2) + alpha*(m1,m2) moves along the correct path.
+  // Just have to decide how far, i.e. what alpha.
   
-  // Calculate the minimum size of the viewport corresponding to the
-  // maximum zoom.
+  //Possible schemes (using rw=r2-r1, Rw=R2-R1)
+  // (Note: if y2-y1 == MAX_Y, alpha=1/nSteps is correct, and in some schemes must be a special case)
+  // alpha = (pow(rw/Rw,1/nSteps)-1)*rW / (rw-Rw) : correct/ideal, but uses pow
+  // alpha = 1/nSteps : moves forwards too fast, reverses too slow (correct for translation)
+  // alpha = MAX_Y / (MAX_Y + (nSteps-1)*(y2-y1)) : (same eqn as old Dasher) more so! reversing ~~ 1/3 ideal speed, and maxes out at moderate dasherX.
+  // alpha = (y2-y1) / (MAX_Y*(nSteps-1) + y2-y1) : too slow forwards, reverses too quick
+  //We are using:
+  // alpha = sqrt(y2-y1) / (sqrt(MAX_Y)*(nSteps-1) + sqrt(y2-y1))
+  //      with approx sqrt on y2-y1
+  //this is pretty good going forwards, but reverses faster than the ideal, on the order of 2*
   
-  if((y2 - y1) < iMinSize) {
-    const dasherint newy1 = y1 * (Y2 - iMinSize) / (Y2 - (y2 - y1)),
-    newy2 = newy1 + iMinSize;
+  if (targetRange < 2*limX) {
+#ifdef DEBUG_DYNAMICS
+    {
+      const dasherint Rw=R2-R1, rw=r2-r1;
+      dasherint apsq = mysqrt(y2-y1);
+      dasherint denom = 64*(nSteps-1) + apsq;
+      dasherint nw = (rw*apsq + Rw*64*(nSteps-1))/denom;
+      double bits = (log(nw) - log(Rw))/log(2);
+      std::cout << "Too fast at X " << (y2-y1)/2 << ": would enter " << bits << "b = " << (bits*nSteps) << " in " << nSteps << "steps; will now enter ";
+    }
+#endif
+    //atm we have Rw=R2-R1, rw=r2-r1 = Rw*MAX_Y/targetRange, (m1,m2) to take us there
     
-    y1 = newy1;
-    y2 = newy2;
+    //if targetRange were = 2*limX, we'd have rw' = Rw*MAX_Y/2*limX < rw
+    //the movement necessary to take us to rw', rather than rw, is thus:
+    // (m1',m2') = (m1,m2) * (rw' - Rw) / (rw-Rw) => scale m1,m2 by (rw'-Rw)/(rw-Rw)
+    // = (Rw*MAX_Y/(2*limX) - Rw)/(Rw*MAX_Y/targetRange-Rw)
+    // = (MAX_Y/(2*limX)-1) / (MAX_Y/targetRange-1)
+    // = (MAX_Y-(2*limX))/(2*limX) / ((MAX_Y-targetRange)/targetRange)
+    // = (MAX_Y-(2*limX)) / (2*limX) * targetRange / (MAX_Y-targetRange)
+    {
+      const dasherint n=targetRange*(MAX_Y-2*limX), d=(MAX_Y-targetRange)*2*limX;
+      bool bOver=max(abs(m1),abs(m2))>std::numeric_limits<dasherint>::max()/n;
+      if (bOver) {
+        //std::cout << "Overflow in max-speed-limit " << m1 << "," << m2 << " =wd> " << ((m1*n)/d) << "," << ((m2*n)/d);
+        //so do it a harder way, but which uses smaller intermediates:
+        // (Yes, this is valid even if !bOver. Could use it all the time?)
+        m1 = (m1/d)*n + ((m1 % d) * n) / d;
+        m2 = (m2/d)*n + ((m2 % d) * n) / d;
+        //std::cout << " => " << m1 << "," << m2 << std::endl;
+      } else {
+        m1 = (m1*n)/d;
+        m2 = (m2*n)/d;
+      } 
+    }
+    //then make the stepping function, which follows, behave as if we were at limX:
+    targetRange=2*limX;
   }
   
-  //okay, we now have target bounds for the viewport, after allowing for framerate etc.
-  // we now go there in one step...
-  
-  // new root{min,max} r1,r2, old root{min,max} R1,R2
-  const dasherint R1 = m_Rootmin;
-  const dasherint R2 = m_Rootmax;  
-  
-  // If |(0,Y2)| = |(y1,y2)|, the "zoom factor" is 1, so we just translate.
-  if (Y2 == y2 - y1) {
-    r1 = R1 - y1;
-    r2 = R2 - y1;
-  } else {
-    // There is a point C on the y-axis such the ratios (y1-C):(Y1-C) and
-    // (y2-C):(Y2-C) are equal - iow that divides the "target" region y1-y2
-    // into the same proportions as it divides the screen (0-Y2). I.e., this
-    // is the center of expansion - the point on the y-axis which everything
-    // moves away from (or towards, if reversing).
-    
-    //We prefer to compute C from the _original_ (y1,y2) pair, as this is more
-    // accurate (and avoids drifting up/down when heading straight along the
-    // x-axis in dynamic button modes). However...
-    if (((y2-y1) < Y2) ^ ((oy2-oy1) < Y2)) {
-      //Sometimes (very occasionally), the calculation of a single-step above
-      // can turn a zoom-in into a zoom-out, or vice versa, when the movement
-      // is mostly translation. In which case, must compute C consistently with
-      // the (scaled, single-step) movement we are going to perform, or else we
-      // will end up suddenly going the wrong way along the y-axis (i.e., the
-      // sense of translation will be reversed) !
-      oy1=y1; oy2=y2;
+#ifndef DEBUG_DYNAMICS
+  if (bExact) {
+    //#else, for DEBUG_DYNAMICS, we compute the exact movement either way, to compare.
+#endif
+    double frac;
+    if (targetRange == MAX_Y) {
+      frac=1.0/nSteps;
+    } else {
+      double tr(targetRange);
+      //expansion factor (of root node) for one step, post-speed-limit
+      double eFac = pow(MAX_Y/tr,1.0/nSteps);
+      //fraction of way along linear interpolation Rw->rw that yields that width:
+      // = (Rw*eFac - Rw) / (rw-Rw)
+      // = Rw * (eFac-1.0) / (Rw*MAX_Y/tr-Rw)
+      // = (eFac - 1.0) / (MAX_Y/tr - 1.0)
+      frac = (eFac-1.0) /  (MAX_Y/tr - 1.0);
     }
-    const dasherint C = (oy1 * Y2) / (oy1 + Y2 - oy2);
+#ifdef DEBUG_DYNAMICS
+    const dasherint m1t=m1*frac, m2t=m2*frac; //keep original m1,m2 to compare
+#else
+    m1*=frac; m2*=frac;
+  } else //conditional - only do one of exact/approx
+#endif
+  { //begin block A (regardless of #ifdef)
     
-    r1 = ((R1 - C) * Y2) / (y2 - y1) + C;
-    r2 = ((R2 - C) * Y2) / (y2 - y1) + C;
-  }
-  m_deGotoQueue.clear();
-  m_deGotoQueue.push_back(pair<myint,myint>(r1,r2));
+    //approximate dynamics: interpolate
+    // apsq parts rw to 64*(nSteps-1) parts Rw
+    // (no need to compute target width)
+    dasherint apsq = mysqrt(targetRange);
+    dasherint denom = 64*(nSteps-1) + apsq;
+    
+    // so new width nw = (64*(nSteps-1)*Rw + apsq*rw)/denom
+    // = Rw*(64*(nSteps-1) + apsq*MAX_Y/targetRange)/denom
+    m1 = (m1*apsq)/denom, m2=(m2*apsq)/denom;
+#ifdef DEBUG_DYNAMICS
+    std::cout << "Move " << m1 << "," << m2 << " should be " << m1t << "," << m2t;
+    double dActualBits = (log((R2+m2)-(R1+m1))-log(R2-R1))/log(2);
+    double dDesiredBits = (log((R2+m2t)-(R1+m1t))-log(R2-R1))/log(2);
+    std::cout << " enters " << dActualBits << "b = " << (dActualBits*nSteps) << " in " << nSteps << "steps, should be "
+    << dDesiredBits << "=>" << (dDesiredBits*nSteps) << ", error " << int(abs(dDesiredBits-dActualBits)*100/dDesiredBits) << "%" << std::endl;
+    if (bExact)
+      m1=m1t, m2=m2t; //overwrite approx values (we needed them somewhere!)
+#endif
+  } //end block A (regardless of #ifdef)
+  
+  m_deGotoQueue.push_back(pair<myint,myint>(R1+m1, R2+m2));
 }
 
 void CDasherModel::OutputTo(CDasherNode *pNewNode) {
@@ -481,49 +535,44 @@ void CDasherModel::RenderToView(CDasherView *pView, CExpansionPolicy &policy) {
 }
 
 void CDasherModel::ScheduleZoom(dasherint y1, dasherint y2, int nsteps) {
-  DASHER_ASSERT(y2>y1);
-
+  
+  m_deGotoQueue.clear();
+  
   // Rename for readability.
-  const dasherint Y1 = 0;
-  const dasherint Y2 = MAX_Y;
   const dasherint R1 = m_Rootmin;
   const dasherint R2 = m_Rootmax;
 
-  dasherint C, r1, r2;
-
-  // If |(y1,y2)| = |(Y1,Y2)|, the "zoom factor" is 1, so we just translate.
-  // y2 - y1 == Y2 - Y1 => y1 - Y1 == y2 - Y2
-  C = y1 - Y1;
-  if (C == y2 - Y2) {
-      r1 = R1 + C;
-      r2 = R2 + C;
-  } else {
-  // There is a point C on the y-axis such the ratios (y1-C):(Y1-C) and
-  // (y2-C):(Y2-C) are equal. (Obvious when drawn on separate parallel axes.)
-      C = (y1 * Y2 - y2 * Y1) / (y1 + Y2 - y2 - Y1);
-
-  // So another point r's zoomed y coordinate R, has the same ratio (r-C):(R-C)
-      if (y1 != C) {
-          r1 = ((R1 - C) * (Y1 - C)) / (y1 - C) + C;
-          r2 = ((R2 - C) * (Y1 - C)) / (y1 - C) + C;
-      } else if (y2 != C) {
-          r1 = ((R1 - C) * (Y2 - C)) / (y2 - C) + C;
-          r2 = ((R2 - C) * (Y2 - C)) / (y2 - C) + C;
-      } else { // implies y1 = y2
-          std::cerr << "Impossible geometry in CDasherModel::ScheduleZoom\n";
-      }
-  }
-
-  // sNewItem seems to contain a list of root{min,max} for the frames of the
-  // zoom, so split r -> R into n steps, with accurate R
-  m_deGotoQueue.clear();
-  for (int s = nsteps - 1; s >= 0; --s) {
-    m_deGotoQueue.push_back(pair<myint,myint>(
-        r1 - (s * (r1 - R1)) / nsteps,
-        r2 - (s * (r2 - R2)) / nsteps));
+  const dasherint r1 = MAX_Y*(m_Rootmin-y1)/(y2-y1);
+  const dasherint r2 = MAX_Y*(m_Rootmax-y1)/(y2-y1);
+
+  //We're going to interpolate in steps whose size starts at nsteps
+  // and decreases by one each time - so cumulatively: 
+  // <nsteps> <2*nsteps-1> <3*nsteps-3> <4*nsteps-6>
+  // (until the next value is the same as the previous)
+  //These will sum to / reach (triangular number formula):
+  const int max((nsteps*(nsteps+1))/2);
+  //heights:
+  const myint oh(R2-R1), nh(r2-r1);
+  //log(the amount by which we wish to multiply the height):
+  const double logHeightMul(nh==oh ? 0 : log(nh/static_cast<double>(oh)));
+  for (int s = nsteps; nsteps>1; s+=(--nsteps)) {
+    double dFrac; //(linear) fraction of way from oh to nh...
+    if (nh==oh)
+      dFrac = s/static_cast<double>(max);
+    else {
+      //interpolate expansion logarithmically to get new height:
+      const double h(oh*exp((logHeightMul*s)/max));
+      //then treat that as a fraction of the way between oh to nh linearly
+      dFrac = (h-oh)/(nh-oh);
+    }
+    //and use that fraction to interpolate from R to r
+    m_deGotoQueue.push_back(pair<myint,myint>(R1+dFrac*(r1-R1), R2+dFrac*(r2-R2)));
   }
+  //final point, done accurately/simply:
+  m_deGotoQueue.push_back(pair<myint,myint>(r1,r2));
 }
 
+
 void CDasherModel::ClearScheduledSteps() {
   m_deGotoQueue.clear();
 }
diff --git a/Src/DasherCore/DasherModel.h b/Src/DasherCore/DasherModel.h
index 60e1961..3e28920 100644
--- a/Src/DasherCore/DasherModel.h
+++ b/Src/DasherCore/DasherModel.h
@@ -70,24 +70,10 @@ class Dasher::CDasherModel: public Observable<CDasherNode*>, private NoClones
   CDasherModel();
   ~CDasherModel();
 
-  /// @name Dymanic evolution
-  /// Routines detailing the timer dependent evolution of the model
+  /// @name Offset routines
+  /// For "bouncing" the display up and down (dynamic button modes)
   /// @{
 
-  ///
-  /// Schedules *one step*  of movement towards the specified
-  /// co-ordinates - used by timer callbacks for non-button modes.
-  /// Interpolates movement according to iSteps and iMinSize, and calculates
-  /// new co-ordinates for the root node (after *one step*) into m_deGotoQueue
-  /// just as ScheduleZoom. For further information, see Doc/geometry.tex.
-  ///
-  /// \param mousex dasherx co-ordinate towards which to move (e.g. mouse pos)
-  /// \param mousey dashery co-ordinate towards which to move (e.g. mouse pos)
-  /// \param iSteps number of frames which should get us all the way to (mousex,mousey)
-  /// \param iMinSize limit on rate of expansion due to bitrate (as moving
-  /// all the way to the mouse at mousex==1 would be an absurd rate of data entry,
-  /// becoming infinite at mousex==0).
-  void ScheduleOneStep(myint mousex, myint mousey, int iSteps, dasherint iMinSize);
 
   ///
   /// Apply an offset to the 'target' coordinates - implements the jumps in
@@ -134,8 +120,8 @@ class Dasher::CDasherModel: public Observable<CDasherNode*>, private NoClones
   /// @}
 
   ///
-  /// @name Scheduled operation
-  /// E.g. response to button mode
+  /// @name Dynamics
+  /// For controlling movement of the model
   /// @{
 
   ///
@@ -147,6 +133,19 @@ class Dasher::CDasherModel: public Observable<CDasherNode*>, private NoClones
   /// \param y1,y2 - target range of y axis, i.e. to move to 0,MAXY
   /// \param nSteps number of steps to schedule to take us all the way there
   void ScheduleZoom(dasherint y1, dasherint y2, int nSteps);
+  
+  /// Schedule one frame of movement, with the property that
+  /// <nsteps> calls with the same parameter, should bring
+  /// the given range of Dasher Y-space to fill the axis.
+  /// (Roughly - we use approximations! - but more accurate
+  /// than the first step of a zoom).
+  /// \param y1,y2 - target range of y axis, i.e. to move to 0,MAXY
+  /// \param nSteps number of steps that would take us all the way there
+  /// \param limX X coord at which max speed achieved (any X coord lower than
+  /// this, will be slowed down to that speed).
+  /// \param bExact whether to do "exact" calculations (slower, using floating-point
+  /// pow), or approximate with integers (will move at not-ideal rate in some directions)
+  void ScheduleOneStep(dasherint y1, dasherint y2, int nSteps, int limX, bool bExact);
 
   ///Cancel any steps previously scheduled (most likely by ScheduleZoom)
   void ClearScheduledSteps();
diff --git a/Src/DasherCore/DefaultFilter.cpp b/Src/DasherCore/DefaultFilter.cpp
index 4c1affe..6617b3e 100644
--- a/Src/DasherCore/DefaultFilter.cpp
+++ b/Src/DasherCore/DefaultFilter.cpp
@@ -16,7 +16,9 @@ static SModuleSettings sSettings[] = {
   {BP_REMAP_XTREME, T_BOOL, -1, -1, -1, -1, _("At top and bottom, scroll more and translate less (makes error-correcting easier)")},
   {LP_GEOMETRY, T_LONG, 0, 3, 1, 1, _("Screen geometry (mostly for tall thin screens) - 0=old-style, 1=square no-xhair, 2=squish, 3=squish+log")},
   {LP_SHAPE_TYPE, T_LONG, 0, 5, 1, 1, _("Shape type: 0=disjoint rects, 1=overlapping, 2=triangles, 3=trunc-tris, 4=quadrics, 5=circles")},
+  {LP_X_LIMIT_SPEED, T_LONG, 1, 800, 1536, 1, _("Distance from right-hand-side Y-axis, at which maximum speed is reached. (2048=xhair)")},
   {BP_TURBO_MODE, T_BOOL, -1, -1, -1, -1, _("Hold right mouse button / key 1 to go 3/4 faster")},
+  {BP_EXACT_DYNAMICS, T_BOOL, -1, -1, -1, -1, _("Use exact computation of per-frame movement (slower)")},
 };
 
 bool CDefaultFilter::GetSettings(SModuleSettings **sets, int *iCount) {
@@ -62,7 +64,9 @@ bool CDefaultFilter::DecorateView(CDasherView *pView, CDasherInput *pInput) {
     x[0] = CDasherModel::ORIGIN_X;
     y[0] = CDasherModel::ORIGIN_Y;
 
-    x[1] = m_iLastX;
+    //If the user's finger/mouse is in the margin, draw the line to the closest
+    // point we'll actually head to.
+    x[1] = max(myint(1),m_iLastX);
     y[1] = m_iLastY;
 
     // Actually plot the line
diff --git a/Src/DasherCore/DynamicFilter.cpp b/Src/DasherCore/DynamicFilter.cpp
index eb402f7..d86960f 100644
--- a/Src/DasherCore/DynamicFilter.cpp
+++ b/Src/DasherCore/DynamicFilter.cpp
@@ -32,27 +32,17 @@ bool CDynamicFilter::OneStepTowards(CDasherModel *pModel, myint X, myint Y, unsi
   if (dSpeedMul<=0.0) return false; //going nowhere
   m_pFramerate->RecordFrame(iTime); //Hmmm, even if we don't do anything else?
 
-  //The maximum number of bits we should allow to be entered in this frame:
-  // (after adjusting for slow start, turbo mode, control node slowdown, etc.)
-  double dBits = m_pFramerate->GetMaxBitsPerFrame()*dSpeedMul;
-
-  //Compute max expansion, i.e. the minimum size we should allow the range 0..MAX_Y
-  // to be shrunk down to, for this frame. We cache the most-recent result to
-  // avoid an exp() (and a division): in the majority of cases this doesn't change
-  // between frames, but only does so when the maxbitrate changes, or dspeedmul
-  // changes (e.g. continuously during slow start, or when entering/leaving turbo
-  // mode or a control node).
-  if (dBits != m_dLastBits) m_iLastMinSize = static_cast<myint>(CDasherModel::MAX_Y / exp(m_dLastBits = dBits));
-  //However, note measurements on iPhone suggest even one exp() per frame is not
-  // a significant overhead; so the caching may be unnecessary, but it's easy.
+  // iSteps is the number of update steps we need to get the point
+  // under the cursor over to the cross hair. Calculated in order to
+  // keep a constant bit-rate.
+  const int iSteps(static_cast<int>(m_pFramerate->Steps() / dSpeedMul));
+  DASHER_ASSERT(iSteps > 0);
+  
+  // If X is too large we risk overflow errors, so limit it
+  // Not rescaling Y in this case: at that X, all Y's are nearly equivalent!
+  X = max(myint(1),min(X, myint(1<<29)/iSteps));
   
-  //If we wanted to take things further we could generalize this cache to cover
-  // exp()s done in the dynamic button modes too, and thus to allow them to adjust
-  // lag, guide markers, etc., according to the dSpeedMul in use. (And/or
-  // to do slow-start more efficiently by interpolating cache values.)
-  pModel->ScheduleOneStep(X, Y,
-                          static_cast<int>(m_pFramerate->Steps() / dSpeedMul),
-                          m_iLastMinSize);
+  pModel->ScheduleOneStep(Y-X, Y+X, iSteps, GetLongParameter(LP_X_LIMIT_SPEED), GetBoolParameter(BP_EXACT_DYNAMICS));
   return true;
 }
 
diff --git a/Src/DasherCore/FrameRate.cpp b/Src/DasherCore/FrameRate.cpp
index 49242bd..9677a8a 100644
--- a/Src/DasherCore/FrameRate.cpp
+++ b/Src/DasherCore/FrameRate.cpp
@@ -11,8 +11,8 @@ CFrameRate::CFrameRate(CSettingsUser *pCreator) :
   m_iTime = 0;
 
   //try and carry on from where we left off at last run
-  HandleEvent(LP_MAX_BITRATE);
-  //calls UpdateSteps(), which sets m_dRXMax and m_iSteps
+  HandleEvent(LP_X_LIMIT_SPEED);
+  //Sets m_dBitsAtLimX and m_iSteps
 }
 
 void CFrameRate::RecordFrame(unsigned long Time)
@@ -36,43 +36,30 @@ void CFrameRate::RecordFrame(unsigned long Time)
 
     // Calculate the framerate and reset framerate statistics for next
     // sampling period
-    double dFrNow;
     if(m_iTime2 - m_iTime > 0) {
-      dFrNow = m_iFrames * 1000.0 / (m_iTime2 - m_iTime);
+      double dFrNow = m_iFrames * 1000.0 / (m_iTime2 - m_iTime);
       //LP_FRAMERATE records a decaying average, smoothed 50:50 with previous value
       SetLongParameter(LP_FRAMERATE, long(GetLongParameter(LP_FRAMERATE) + (dFrNow*100))/2);
       m_iTime = m_iTime2;
       m_iFrames = 0;
-    } else //best guess: use decaying average
-      dFrNow = GetLongParameter(LP_FRAMERATE) / 100.0;
 
-    UpdateSteps(dFrNow);
+    DASHER_TRACEOUTPUT("Fr %f Steps %d Samples %d Time2 %d\n", dFrNow, m_iSteps, m_iSamples, m_iTime2);
 
-    DASHER_TRACEOUTPUT("Fr %f Steps %d Samples %d Time2 %d maxbits %f\n", dFrNow, m_iSteps, m_iSamples, m_iTime2, m_dFrameBits);
+    }
 
   }
 }
 
 void CFrameRate::HandleEvent(int iParameter) {
-
   switch (iParameter) {
-  case LP_MAX_BITRATE:
-    UpdateSteps(GetLongParameter(LP_FRAMERATE) / 100.0); //use the decaying average as current
-    break;
+    case LP_X_LIMIT_SPEED:
+      m_dBitsAtLimX = (log(CDasherModel::MAX_Y) - log (2*GetLongParameter(LP_X_LIMIT_SPEED)))/log(2);
+      //fallthrough
+    case LP_MAX_BITRATE:
+    case LP_FRAMERATE:
+    //Calculate m_iSteps from the decaying-average framerate, as the number
+    // of steps that, at the X limit, will cause LP_MAX_BITRATE bits to be
+    // entered per second
+    m_iSteps = std::max(1,(int)(GetLongParameter(LP_FRAMERATE)*m_dBitsAtLimX/GetLongParameter(LP_MAX_BITRATE)));
   }
 }
-
-const double LN2 = log(2.0);
-const double STEPS_COEFF = -log(0.2) / LN2;
-
-void CFrameRate::UpdateSteps(double dFrNow) {
-  const double dMaxbitrate = GetLongParameter(LP_MAX_BITRATE) / 100.0;
-    // Update auxiliary variables - even if we didn't recalc the framerate
-    //   (means we reach sensible values more quickly after first loading)
-    m_dFrameBits = dMaxbitrate * LN2 / dFrNow;
-    
-    //Calculate m_iSteps from the decaying-average framerate, and ensure
-    // it is at least 1 (else, if framerate slows to <4, we get 0 steps!)
-    m_iSteps = std::max(1,(int)(STEPS_COEFF * (GetLongParameter(LP_FRAMERATE)/100.0) / dMaxbitrate));
-
-}
diff --git a/Src/DasherCore/FrameRate.h b/Src/DasherCore/FrameRate.h
index 31ed87b..6e44b83 100644
--- a/Src/DasherCore/FrameRate.h
+++ b/Src/DasherCore/FrameRate.h
@@ -24,16 +24,10 @@ namespace Dasher {
 class CFrameRate : public CSettingsUserObserver  {
 public:
   CFrameRate(CSettingsUser *pCreator);
-  
-  virtual void HandleEvent(int iParameter);
 
-  ///The maximum amount by which one frame may zoom in. Used as a hard
-  /// upper-bound on the speed of movement (however far to RHS the cursor is),
-  /// calculated from the most-recent (instantaneous) framerate rather than
-  /// the decaying average used for the Steps() parameter.
-  double GetMaxBitsPerFrame() {
-    return m_dFrameBits;
-  }
+  //Responds to a change to LP_FRAMERATE or LP_MAX_BITRATE
+  // by recomputing the Steps() parameter.
+  virtual void HandleEvent(int iParameter);
 
   ///The number of frames, in which we will attempt to bring
   /// the target location (under the cursor, or in dynamic button
@@ -53,11 +47,8 @@ public:
   }
 
   void RecordFrame(unsigned long Time);
-
-  bool OneStepTowards(CDasherModel *pModel, myint y1, myint y2, unsigned long iTime, double dSpeedMul);
   
 private:
-  double m_dFrameBits;              // the maximum zoomin per frame
   ///number of frames that have been sampled
   int m_iFrames;
   ///time at which first sampled frame was rendered
@@ -66,10 +57,8 @@ private:
   int m_iSamples;
 
   int m_iSteps;
-
-  ///updates m_dRXMax and m_iSteps
-  /// \param dFrNow current (non-decaying-average) framerate (if available!)
-  void UpdateSteps(double dFrNow);
+  
+  double m_dBitsAtLimX;
 };
 /// \}
 }
diff --git a/Src/DasherCore/Parameters.cpp b/Src/DasherCore/Parameters.cpp
index bd4c10f..bebe518 100644
--- a/Src/DasherCore/Parameters.cpp
+++ b/Src/DasherCore/Parameters.cpp
@@ -22,6 +22,7 @@ const bp_table boolparamtable[] = {
   {BP_MOUSEPOS_MODE, "StartOnMousePosition", false, "StartOnMousePosition"},
   {BP_PALETTE_CHANGE, "PaletteChange", true, "PaletteChange"},
   {BP_TURBO_MODE, "TurboMode", true, "Boost speed when holding key1 or right mouse button"},
+  {BP_EXACT_DYNAMICS, "ExactDynamics", false, "Use exact computation of per-frame movement (slower)"},
   {BP_AUTOCALIBRATE, "Autocalibrate", false, "Automatically learn TargetOffset e.g. gazetracking"},
   {BP_REMAP_XTREME, "RemapXtreme", false, "Pointer at extreme Y translates more and zooms less"},
   {BP_LM_DICTIONARY, "Dictionary", true, "Whether the word-based language model uses a dictionary"},
@@ -143,6 +144,7 @@ const lp_table longparamtable[] = {
   {LP_MARGIN_WIDTH, "MarginWidth", 300, "Width of RHS margin (in Dasher co-ords)"},
 #endif
   {LP_TARGET_OFFSET, "TargetOffset", 0, "Vertical distance between mouse pointer and target (400=screen height)"},
+  {LP_X_LIMIT_SPEED, "XLimitSpeed", 800, "X Co-ordinate at which maximum speed is reached (<2048=xhair)"},
   {LP_GAME_HELP_DIST, "GameHelpDistance", 1920, "Distance of sentence from center to decide user needs help"},
   {LP_GAME_HELP_TIME, "GameHelpTime", 0, "Time for which user must need help before help drawn"},
 };
diff --git a/Src/DasherCore/Parameters.h b/Src/DasherCore/Parameters.h
index fd4be5b..7114a48 100644
--- a/Src/DasherCore/Parameters.h
+++ b/Src/DasherCore/Parameters.h
@@ -32,7 +32,7 @@ enum {
   BP_SHOW_SLIDER, BP_START_MOUSE,
   BP_START_SPACE, BP_STOP_IDLE, BP_CONTROL_MODE, 
   BP_COLOUR_MODE, BP_MOUSEPOS_MODE,
-  BP_PALETTE_CHANGE, BP_TURBO_MODE,
+  BP_PALETTE_CHANGE, BP_TURBO_MODE, BP_EXACT_DYNAMICS,
   BP_AUTOCALIBRATE, BP_REMAP_XTREME,
   BP_LM_DICTIONARY, 
   BP_LM_LETTER_EXCLUSION, BP_AUTO_SPEEDCONTROL,
@@ -64,7 +64,7 @@ enum {
   LP_DYNAMIC_BUTTON_LAG, LP_STATIC1B_TIME, LP_STATIC1B_ZOOM,
   LP_DEMO_SPRING, LP_DEMO_NOISE_MEM, LP_DEMO_NOISE_MAG, LP_MAXZOOM, 
   LP_DYNAMIC_SPEED_INC, LP_DYNAMIC_SPEED_FREQ, LP_DYNAMIC_SPEED_DEC,
-  LP_TAP_TIME, LP_MARGIN_WIDTH, LP_TARGET_OFFSET,
+  LP_TAP_TIME, LP_MARGIN_WIDTH, LP_TARGET_OFFSET, LP_X_LIMIT_SPEED,
   LP_GAME_HELP_DIST, LP_GAME_HELP_TIME,
   END_OF_LPS
 };



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]