diff --git a/apps/shared/Makefile b/apps/shared/Makefile index c10dece7c..08c51ef13 100644 --- a/apps/shared/Makefile +++ b/apps/shared/Makefile @@ -24,6 +24,7 @@ app_shared_src = $(addprefix apps/shared/,\ buffer_function_title_cell.cpp \ buffer_text_view_with_text_field.cpp \ button_with_separator.cpp \ + continuous_function_cache.cpp \ cursor_view.cpp \ curve_view_cursor.cpp \ editable_cell_table_view_controller.cpp \ diff --git a/apps/shared/continuous_function.h b/apps/shared/continuous_function.h index 612651231..096d6ee61 100644 --- a/apps/shared/continuous_function.h +++ b/apps/shared/continuous_function.h @@ -10,6 +10,7 @@ */ #include "global_context.h" +#include "continuous_function_cache.h" #include "function.h" #include "range_1D.h" #include @@ -18,6 +19,9 @@ namespace Shared { class ContinuousFunction : public Function { + /* We want the cache to be able to call privateEvaluateXYAtParameter to + * bypass cache lookup when memoizing the function's values. */ + friend class ContinuousFunctionCache; public: static void DefaultName(char buffer[], size_t bufferSize); static ContinuousFunction NewModel(Ion::Storage::Record::ErrorStatus * error, const char * baseName = nullptr); @@ -73,6 +77,9 @@ public: Poincare::Coordinate2D nextIntersectionFrom(double start, double step, double max, Poincare::Context * context, Poincare::Expression e, double eDomainMin = -INFINITY, double eDomainMax = INFINITY) const; // Integral Poincare::Expression sumBetweenBounds(double start, double end, Poincare::Context * context) const override; + + // Cache + ContinuousFunctionCache * cache() const { return const_cast(&m_cache); } private: constexpr static float k_polarParamRangeSearchNumberOfPoints = 100.0f; // This is ad hoc, no special justification typedef Poincare::Coordinate2D (*ComputePointOfInterest)(Poincare::Expression e, char * symbol, double start, double step, double max, Poincare::Context * context); @@ -115,6 +122,7 @@ private: RecordDataBuffer * recordData() const; template Poincare::Coordinate2D templatedApproximateAtParameter(T t, Poincare::Context * context) const; Model m_model; + ContinuousFunctionCache m_cache; }; } diff --git a/apps/shared/continuous_function_cache.cpp b/apps/shared/continuous_function_cache.cpp new file mode 100644 index 000000000..567452f3c --- /dev/null +++ b/apps/shared/continuous_function_cache.cpp @@ -0,0 +1,146 @@ +#include "continuous_function_cache.h" +#include "continuous_function.h" + +namespace Shared { + +constexpr int ContinuousFunctionCache::k_sizeOfCache; +constexpr float ContinuousFunctionCache::k_cacheHitTolerance; + +// public +void ContinuousFunctionCache::PrepareCache(void * f, void * c, float tMin, float tStep) { + ContinuousFunction * function = (ContinuousFunction *)f; + Poincare::Context * context = (Poincare::Context *)c; + ContinuousFunctionCache * functionCache = function->cache(); + if (functionCache->filled() && tStep / StepFactor(function) == functionCache->step()) { + if (function->plotType() == ContinuousFunction::PlotType::Cartesian) { + function->cache()->pan(function, context, tMin); + } + return; + } + functionCache->setRange(function, tMin, tStep); + functionCache->memoize(function, context); +} + +void ContinuousFunctionCache::clear() { + m_filled = false; + m_startOfCache = 0; +} + +Poincare::Coordinate2D ContinuousFunctionCache::valueForParameter(const ContinuousFunction * function, float t) const { + int iRes = indexForParameter(function, t); + /* If t does not map to an index, iRes is -1 */ + if (iRes < 0) { + return Poincare::Coordinate2D(NAN, NAN); + } + if (function->plotType() == ContinuousFunction::PlotType::Cartesian) { + return Poincare::Coordinate2D(t, m_cache[iRes]); + } + assert(m_startOfCache == 0); + return Poincare::Coordinate2D(m_cache[2*iRes], m_cache[2*iRes+1]); +} + +// private +float ContinuousFunctionCache::StepFactor(ContinuousFunction * function) { + /* When drawing a parametric or polar curve, the range is first divided by + * ~10,9, creating 11 intervals which are filled by dichotomy. + * We memoize 16 values for each of the 10 big intervals. */ + return (function->plotType() == ContinuousFunction::PlotType::Cartesian) ? 1.f : 16.f; +} + +void ContinuousFunctionCache::setRange(ContinuousFunction * function, float tMin, float tStep) { + m_tMin = tMin; + m_tStep = tStep / StepFactor(function); +} + +void ContinuousFunctionCache::memoize(ContinuousFunction * function, Poincare::Context * context) { + m_filled = true; + m_startOfCache = 0; + if (function->plotType() == ContinuousFunction::PlotType::Cartesian) { + memoizeYForX(function, context); + return; + } + memoizeXYForT(function, context); +} + +void ContinuousFunctionCache::memoizeYForX(ContinuousFunction * function, Poincare::Context * context) { + memoizeYForXBetweenIndices(function, context, 0, k_sizeOfCache); +} + +void ContinuousFunctionCache::memoizeYForXBetweenIndices(ContinuousFunction * function, Poincare::Context * context, int iInf, int iSup) { + assert(function->plotType() == ContinuousFunction::PlotType::Cartesian); + for (int i = iInf; i < iSup; i++) { + m_cache[i] = function->privateEvaluateXYAtParameter(parameterForIndex(i), context).x2(); + } +} + +void ContinuousFunctionCache::memoizeXYForT(ContinuousFunction * function, Poincare::Context * context) { + assert(function->plotType() != ContinuousFunction::PlotType::Cartesian); + for (int i = 1; i < k_sizeOfCache; i += 2) { + Poincare::Coordinate2D res = function->privateEvaluateXYAtParameter(parameterForIndex(i/2), context); + m_cache[i - 1] = res.x1(); + m_cache[i] = res.x2(); + } +} + +float ContinuousFunctionCache::parameterForIndex(int i) const { + if (i < m_startOfCache) { + i += k_sizeOfCache; + } + return m_tMin + m_tStep * (i - m_startOfCache); +} + +int ContinuousFunctionCache::indexForParameter(const ContinuousFunction * function, float t) const { + float delta = (t - m_tMin) / m_tStep; + if (delta < 0 || delta > INT_MAX) { + return -1; + } + int res = std::round(delta); + assert(res >= 0); + if (res >= k_sizeOfCache || std::abs(res - delta) > k_cacheHitTolerance) { + return -1; + } + assert(function->plotType() == ContinuousFunction::PlotType::Cartesian || m_startOfCache == 0); + return (res + m_startOfCache) % k_sizeOfCache; +} + +void ContinuousFunctionCache::pan(ContinuousFunction * function, Poincare::Context * context, float newTMin) { + assert(function->plotType() == ContinuousFunction::PlotType::Cartesian); + if (newTMin == m_tMin) { + return; + } + + float dT = (newTMin - m_tMin) / m_tStep; + m_tMin = newTMin; + if (std::abs(dT) > INT_MAX) { + memoize(function, context); + return; + } + int dI = std::round(dT); + if (dI >= k_sizeOfCache || dI <= -k_sizeOfCache || std::abs(dT - dI) > k_cacheHitTolerance) { + memoize(function, context); + return; + } + + int oldStart = m_startOfCache; + m_startOfCache = (m_startOfCache + dI) % k_sizeOfCache; + if (m_startOfCache < 0) { + m_startOfCache += k_sizeOfCache; + } + if (dI > 0) { + if (m_startOfCache > oldStart) { + memoizeYForXBetweenIndices(function, context, oldStart, m_startOfCache); + } else { + memoizeYForXBetweenIndices(function, context, oldStart, k_sizeOfCache); + memoizeYForXBetweenIndices(function, context, 0, m_startOfCache); + } + } else { + if (m_startOfCache > oldStart) { + memoizeYForXBetweenIndices(function, context, m_startOfCache, k_sizeOfCache); + memoizeYForXBetweenIndices(function, context, 0, oldStart); + } else { + memoizeYForXBetweenIndices(function, context, m_startOfCache, oldStart); + } + } +} + +} diff --git a/apps/shared/continuous_function_cache.h b/apps/shared/continuous_function_cache.h new file mode 100644 index 000000000..0752f75b2 --- /dev/null +++ b/apps/shared/continuous_function_cache.h @@ -0,0 +1,48 @@ +#ifndef SHARED_CONTINUOUS_FUNCTION_CACHE_H +#define SHARED_CONTINUOUS_FUNCTION_CACHE_H + +#include +#include +#include + +namespace Shared { + +class ContinuousFunction; + +class ContinuousFunctionCache { +public: + static void PrepareCache(void * f, void * c, float tMin, float tStep); + + float step() const { return m_tStep; } + bool filled() const { return m_filled; } + void clear(); + Poincare::Coordinate2D valueForParameter(const ContinuousFunction * function, float t) const; +private: + /* The size of the cache is chosen to optimize the display of cartesian + * function */ + static constexpr int k_sizeOfCache = Ion::Display::Width; + static constexpr float k_cacheHitTolerance = 1e-3; + + static float StepFactor(ContinuousFunction * function); + + void setRange(ContinuousFunction * function, float tMin, float tStep); + void memoize(ContinuousFunction * function, Poincare::Context * context); + void memoizeYForX(ContinuousFunction * function, Poincare::Context * context); + void memoizeYForXBetweenIndices(ContinuousFunction * function, Poincare::Context * context, int iInf, int iSup); + void memoizeXYForT(ContinuousFunction * function, Poincare::Context * context); + float parameterForIndex(int i) const; + int indexForParameter(const ContinuousFunction * function, float t) const; + void pan(ContinuousFunction * function, Poincare::Context * context, float newTMin); + + float m_tMin, m_tStep; + float m_cache[k_sizeOfCache]; + /* m_startOfCache is used to implement a circular buffer for easy panning + * with cartesian functions. When dealing with parametric or polar functions, + * m_startOfCache should be zero.*/ + int m_startOfCache; + bool m_filled; +}; + +} + +#endif