diff --git a/apps/Makefile b/apps/Makefile index ff6c78f78..bd7195041 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -84,7 +84,7 @@ $(eval $(call depends_on_image,apps/title_bar_view.cpp,apps/exam_icon.png)) $(call object_for,$(apps_src) $(tests_src)): $(BUILD_DIR)/apps/i18n.h $(call object_for,$(apps_src) $(tests_src)): $(BUILD_DIR)/python/port/genhdr/qstrdefs.generated.h -apps_tests_src = $(app_calculation_test_src) $(app_code_test_src) $(app_probability_test_src) $(app_regression_test_src) $(app_sequence_test_src) $(app_shared_test_src) $(app_statistics_test_src) $(app_settings_test_src) $(app_solver_test_src) +apps_tests_src = $(app_calculation_test_src) $(app_code_test_src) $(app_graph_test_src) $(app_probability_test_src) $(app_regression_test_src) $(app_sequence_test_src) $(app_shared_test_src) $(app_statistics_test_src) $(app_settings_test_src) $(app_solver_test_src) apps_tests_src += $(addprefix apps/,\ alternate_empty_nested_menu_controller.cpp \ diff --git a/apps/graph/Makefile b/apps/graph/Makefile index 4f440fea8..1a3fbddcc 100644 --- a/apps/graph/Makefile +++ b/apps/graph/Makefile @@ -1,9 +1,12 @@ apps += Graph::App app_headers += apps/graph/app.h +app_graph_test_src = $(addprefix apps/graph/,\ + continuous_function_store.cpp \ +) + app_graph_src = $(addprefix apps/graph/,\ app.cpp \ - continuous_function_store.cpp \ graph/banner_view.cpp \ graph/calculation_graph_controller.cpp \ graph/calculation_parameter_controller.cpp \ @@ -31,8 +34,13 @@ app_graph_src = $(addprefix apps/graph/,\ values/values_controller.cpp \ ) +app_graph_src += $(app_graph_test_src) apps_src += $(app_graph_src) i18n_files += $(call i18n_without_universal_for,graph/base) +tests_src += $(addprefix apps/graph/test/,\ + caching.cpp\ +) + $(eval $(call depends_on_image,apps/graph/app.cpp,apps/graph/graph_icon.png)) diff --git a/apps/graph/test/caching.cpp b/apps/graph/test/caching.cpp new file mode 100644 index 000000000..70ee68631 --- /dev/null +++ b/apps/graph/test/caching.cpp @@ -0,0 +1,132 @@ +#include +#include "../app.h" +#include + +using namespace Poincare; +using namespace Shared; + +namespace Graph { + +bool floatEquals(float a, float b, float tolerance = 1.f/static_cast(Ion::Display::Height)) { + /* The default value for the tolerance is chosen so that the error introduced + * by caching would not typically be visible on screen. */ + return a == b || std::abs(a - b) <= tolerance * std::abs(a + b) / 2.f || (std::isnan(a) && std::isnan(b)); +} + +ContinuousFunction * addFunction(ContinuousFunctionStore * store, ContinuousFunction::PlotType type, const char * definition, Context * context) { + Ion::Storage::Record::ErrorStatus err = store->addEmptyModel(); + assert(err == Ion::Storage::Record::ErrorStatus::None); + (void) err; // Silence compilation warning about unused variable. + Ion::Storage::Record record = store->recordAtIndex(store->numberOfModels() - 1); + ContinuousFunction * f = static_cast(store->modelForRecord(record).operator->()); + f->setPlotType(type, Poincare::Preferences::AngleUnit::Degree, context); + f->setContent(definition, context); + return f; +} + +void assert_check_cartesian_cache_against_function(ContinuousFunction * function, ContinuousFunctionCache * cache, Context * context, float tMin) { + /* We set the cache to nullptr to force the evaluation (otherwise we would be + * comparing the cache against itself). */ + function->setCache(nullptr); + + float t; + for (int i = 0; i < Ion::Display::Width; i++) { + t = tMin + i*cache->step(); + Coordinate2D cacheValues = cache->valueForParameter(function, context, t); + Coordinate2D functionValues = function->evaluateXYAtParameter(t, context); + quiz_assert(floatEquals(t, cacheValues.x1())); + quiz_assert(floatEquals(t, functionValues.x1())); + quiz_assert(floatEquals(cacheValues.x2(), functionValues.x2())); + } + /* We set back the cache, so that it will not be invalidated in + * PrepareForCaching later. */ + function->setCache(cache); +} + +void assert_cartesian_cache_stays_valid_while_panning(ContinuousFunction * function, Context * context, InteractiveCurveViewRange * range, CurveViewCursor * cursor, ContinuousFunctionStore * store, float step) { + ContinuousFunctionCache * cache = store->cacheAtIndex(0); + assert(cache); + + float tMin, tStep; + constexpr float margin = 0.04f; + constexpr int numberOfMoves = 30; + for (int i = 0; i < numberOfMoves; i++) { + cursor->moveTo(cursor->t() + step, cursor->x() + step, function->evaluateXYAtParameter(cursor->x() + step, context).x2()); + range->panToMakePointVisible(cursor->x(), cursor->y(), margin, margin, margin, margin, (range->xMax() - range->xMin()) / (Ion::Display::Width - 1)); + tMin = range->xMin(); + tStep = (range->xMax() - range->xMin()) / (Ion::Display::Width - 1); + ContinuousFunctionCache::PrepareForCaching(function, cache, tMin, tStep); + assert_check_cartesian_cache_against_function(function, cache, context, tMin); + } +} + +void assert_check_polar_cache_against_function(ContinuousFunction * function, Context * context, InteractiveCurveViewRange * range, ContinuousFunctionStore * store) { + ContinuousFunctionCache * cache = store->cacheAtIndex(0); + assert(cache); + + float tMin = range->xMin(); + float tMax = range->xMax(); + float tStep = ((tMax - tMin) / Graph::GraphView::k_graphStepDenominator) / ContinuousFunctionCache::k_parametricStepFactor; + ContinuousFunctionCache::PrepareForCaching(function, cache, tMin, tStep); + + // Fill the cache + float t; + for (int i = 0; i < Ion::Display::Width / 2; i++) { + t = tMin + i*cache->step(); + function->evaluateXYAtParameter(t, context); + } + + function->setCache(nullptr); + for (int i = 0; i < Ion::Display::Width / 2; i++) { + t = tMin + i*cache->step(); + Coordinate2D cacheValues = cache->valueForParameter(function, context, t); + Coordinate2D functionValues = function->evaluateXYAtParameter(t, context); + quiz_assert(floatEquals(cacheValues.x1(), functionValues.x1())); + quiz_assert(floatEquals(cacheValues.x2(), functionValues.x2())); + } +} + +void assert_cache_stays_valid(ContinuousFunction::PlotType type, const char * definition, float rangeXMin = -5, float rangeXMax = 5) { + GlobalContext globalContext; + ContinuousFunctionStore functionStore; + + InteractiveCurveViewRange graphRange; + graphRange.setXMin(rangeXMin); + graphRange.setXMax(rangeXMax); + graphRange.setYMin(-3.f); + graphRange.setYMax(3.f); + + CurveViewCursor cursor; + ContinuousFunction * function = addFunction(&functionStore, type, definition, &globalContext); + Coordinate2D origin = function->evaluateXYAtParameter(0.f, &globalContext); + cursor.moveTo(0.f, origin.x1(), origin.x2()); + + if (type == ContinuousFunction::PlotType::Cartesian) { + assert_cartesian_cache_stays_valid_while_panning(function, &globalContext, &graphRange, &cursor, &functionStore, 2.f); + assert_cartesian_cache_stays_valid_while_panning(function, &globalContext, &graphRange, &cursor, &functionStore, -0.4f); + } else { + assert_check_polar_cache_against_function(function, &globalContext, &graphRange, &functionStore); + } + + functionStore.removeAll(); +} + +QUIZ_CASE(graph_caching) { + assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "x"); + assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "x^2"); + assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "sin(x)"); + assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "sin(x)", -1e6f, 2e8f); + assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "sin(x^2)"); + assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "1/x"); + assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "1/x", -5e-5f, 5e-5f); + assert_cache_stays_valid(ContinuousFunction::PlotType::Cartesian, "-ℯ^x"); + + assert_cache_stays_valid(ContinuousFunction::PlotType::Polar, "1", 0.f, 360.f); + assert_cache_stays_valid(ContinuousFunction::PlotType::Polar, "θ", 0.f, 360.f); + assert_cache_stays_valid(ContinuousFunction::PlotType::Polar, "sin(θ)", 0.f, 360.f); + assert_cache_stays_valid(ContinuousFunction::PlotType::Polar, "sin(θ)", 2e-4f, 1e-3f); + assert_cache_stays_valid(ContinuousFunction::PlotType::Polar, "cos(5θ)", 0.f, 360.f); + assert_cache_stays_valid(ContinuousFunction::PlotType::Polar, "cos(5θ)", -1e8f, 1e8f); +} + +} diff --git a/apps/shared/Makefile b/apps/shared/Makefile index 08c51ef13..b525a9033 100644 --- a/apps/shared/Makefile +++ b/apps/shared/Makefile @@ -1,5 +1,7 @@ app_shared_test_src = $(addprefix apps/shared/,\ continuous_function.cpp\ + continuous_function_cache.cpp \ + curve_view_cursor.cpp \ curve_view_range.cpp \ curve_view.cpp \ dots.cpp \ @@ -24,9 +26,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 \ expression_field_delegate_app.cpp \ expression_model_list_controller.cpp \