From 1500c54857444e2f900d8ff604901e6543009039 Mon Sep 17 00:00:00 2001 From: Richard Date: Sat, 8 Feb 2025 19:26:49 -0700 Subject: [PATCH] Convert IFS 2D to floating-point implementation Use fractint-float as the reference implementation. Add an image test with reference image from fractint. Disk video OK for type=ifs, no need to warn. Fixes #249 --- libid/fractals/fractalp.cpp | 1 - libid/fractals/lorenz.cpp | 103 +++++------------------------ tests/images/CMakeLists.txt | 1 + tests/images/gold-ifs-dragon-1.gif | Bin 0 -> 15282 bytes 4 files changed, 18 insertions(+), 87 deletions(-) create mode 100644 tests/images/gold-ifs-dragon-1.gif diff --git a/libid/fractals/fractalp.cpp b/libid/fractals/fractalp.cpp index 4124cab67..ef1d9ba6b 100644 --- a/libid/fractals/fractalp.cpp +++ b/libid/fractals/fractalp.cpp @@ -355,7 +355,6 @@ FractalSpecific g_fractal_specific[] = HelpLabels::HT_IFS, HelpLabels::SPECIAL_IFS, // FractalFlags::NO_GUESS | FractalFlags::NO_TRACE | FractalFlags::NO_RESUME | FractalFlags::INF_CALC, // -8.0F, 8.0F, -1.0F, 11.0F, // - // 16, TODO: convert to floating-point // FractalType::NO_FRACTAL, FractalType::NO_FRACTAL, // SymmetryType::NONE, // nullptr, nullptr, standalone_setup, ifs, // diff --git a/libid/fractals/lorenz.cpp b/libid/fractals/lorenz.cpp index 78bcd8794..15e8d6c80 100644 --- a/libid/fractals/lorenz.cpp +++ b/libid/fractals/lorenz.cpp @@ -23,11 +23,9 @@ #include "io/encoder.h" #include "io/save_file.h" #include "math/cmplx.h" -#include "math/fixed_pt.h" #include "math/mpmath.h" #include "math/rand15.h" #include "math/sign.h" -#include "misc/debug_flags.h" #include "misc/Driver.h" #include "ui/cmdfiles.h" #include "ui/not_disk_msg.h" @@ -38,7 +36,6 @@ #include #include #include -#include #include template @@ -78,17 +75,6 @@ enum namespace { -struct LAffine -{ - // weird order so a,b,e and c,d,f are vectors - long a; - long b; - long e; - long c; - long d; - long f; -}; - // data used by 3d view transform subroutine struct ViewTransform3D { @@ -111,7 +97,6 @@ struct ViewTransform3D static int ifs2d(); static int ifs3d(); static int ifs3d_float(); -static bool l_setup_convert_to_screen(LAffine *l_cvt); static void setup_matrix(Matrix double_mat); static bool float_view_transf3d(ViewTransform3D *inf); static std::FILE *open_orbit_save(); @@ -279,25 +264,6 @@ bool setup_convert_to_screen(Affine *scrn_cnvt) return false; } -static bool l_setup_convert_to_screen(LAffine *l_cvt) -{ - Affine cvt; - - // This function should return a something! - if (setup_convert_to_screen(&cvt)) - { - return true; - } - l_cvt->a = (long)(cvt.a*g_fudge_factor); - l_cvt->b = (long)(cvt.b*g_fudge_factor); - l_cvt->c = (long)(cvt.c*g_fudge_factor); - l_cvt->d = (long)(cvt.d*g_fudge_factor); - l_cvt->e = (long)(cvt.e*g_fudge_factor); - l_cvt->f = (long)(cvt.f*g_fudge_factor); - - return false; -} - //**************************************************************** // setup functions - put in fractalspecific[fractype].per_image //**************************************************************** @@ -1806,57 +1772,25 @@ int ifs() // front-end for ifs2d and ifs3d { return -1; } - if (driver_is_disk()) // this would KILL a disk drive! - { - not_disk_msg(); - } return !g_ifs_type ? ifs2d() : ifs3d(); } -// IFS logic shamelessly converted to integer math static int ifs2d() { int color; - std::vector local_ifs; - LAffine cvt; + Affine cvt; // setup affine screen coord conversion - l_setup_convert_to_screen(&cvt); + setup_convert_to_screen(&cvt); std::srand(1); int color_method = (int) g_params[0]; - try - { - local_ifs.resize(g_num_affine_transforms*NUM_IFS_2D_PARAMS); - } - catch (const std::bad_alloc &) - { - stop_msg("Insufficient memory for IFS"); - return -1; - } - - for (int i = 0; i < g_num_affine_transforms; i++) // fill in the local IFS array - { - for (int j = 0; j < NUM_IFS_2D_PARAMS; j++) - { - local_ifs[i*NUM_IFS_2D_PARAMS+j] = (long)(g_ifs_definition[i*NUM_IFS_2D_PARAMS+j] * g_fudge_factor); - } - } - - long temp_r = g_fudge_factor / 32767; // find the proper rand() fudge std::FILE *fp = open_orbit_save(); - long x = 0; - long y = 0; + double x = 0; + double y = 0; int ret = 0; - if (g_max_iterations > 0x1fffffL) - { - g_max_count = 0x7fffffffL; - } - else - { - g_max_count = g_max_iterations*1024L; - } + g_max_count = g_max_iterations > 0x1fffffL ? 0x7fffffffL : g_max_iterations * 1024L; g_color_iter = 0L; while (g_color_iter++ <= g_max_count) // loop until keypress or maxit { @@ -1865,42 +1799,39 @@ static int ifs2d() ret = -1; break; } - long r = RAND15(); // generate fudged random number between 0 and 1 - r *= temp_r; + double r = static_cast(RAND15())/32767.0; // generate random number between 0 and 1 // pick which iterated function to execute, weighted by probability - long sum = local_ifs[6]; // [0][6] + double sum = g_ifs_definition[6]; // [0][6] int k = 0; while (sum < r && k < g_num_affine_transforms-1) // fixed bug of error if sum < 1 { - sum += local_ifs[++k*NUM_IFS_2D_PARAMS+6]; + sum += g_ifs_definition[++k * NUM_IFS_2D_PARAMS + 6]; } // calculate image of last point under selected iterated function - long *l_f_ptr = local_ifs.data() + k * NUM_IFS_2D_PARAMS; // point to first parm in row - long new_x = multiply(l_f_ptr[0], x, g_bit_shift) + multiply(l_f_ptr[1], y, g_bit_shift) + l_f_ptr[4]; - long new_y = multiply(l_f_ptr[2], x, g_bit_shift) + multiply(l_f_ptr[3], y, g_bit_shift) + l_f_ptr[5]; + float *f_f_ptr = g_ifs_definition.data() + k * NUM_IFS_2D_PARAMS; // point to first parm in row + double new_x = *(f_f_ptr + 0) * x + *(f_f_ptr + 1) * y + *(f_f_ptr + 4); + double new_y = *(f_f_ptr + 2) * x + *(f_f_ptr + 3) * y + *(f_f_ptr + 5); x = new_x; y = new_y; if (fp) { - std::fprintf(fp, "%g %g %g 15\n", (double)new_x/g_fudge_factor, (double)new_y/g_fudge_factor, 0.0); + std::fprintf(fp, "%g %g %g 15\n", new_x, new_y, 0.0); } // plot if inside window - int col = (int) ((multiply(cvt.a, x, g_bit_shift) + multiply(cvt.b, y, g_bit_shift) + cvt.e) >> - g_bit_shift); - int row = (int) ((multiply(cvt.c, x, g_bit_shift) + multiply(cvt.d, y, g_bit_shift) + cvt.f) >> - g_bit_shift); + const int col = (int) (cvt.a * x + cvt.b * y + cvt.e); + const int row = (int) (cvt.c * x + cvt.d * y + cvt.f); if (col >= 0 && col < g_logical_screen_x_dots && row >= 0 && row < g_logical_screen_y_dots) { - // color is count of hits on this pixel if (color_method) { - color = (k%g_colors)+1; + color = (k % g_colors) + 1; } else { - color = get_color(col, row)+1; + // color is count of hits on this pixel + color = get_color(col, row) + 1; } if (color < g_colors) // color sticks on last value { diff --git a/tests/images/CMakeLists.txt b/tests/images/CMakeLists.txt index c7751ca7d..019374f2e 100644 --- a/tests/images/CMakeLists.txt +++ b/tests/images/CMakeLists.txt @@ -85,3 +85,4 @@ add_image_test(fractint-formula PARAMETERS "type=formula" "formulafile=example2. foreach(bailout and imag manh manr or real) add_image_test(bailout-${bailout} PARAMETERS "type=mandel" "bailoutest=${bailout}") endforeach() +add_image_test(ifs-dragon-1 PARAMETERS "type=ifs" "ifs=dragon" "params=1" "maxiter=500") diff --git a/tests/images/gold-ifs-dragon-1.gif b/tests/images/gold-ifs-dragon-1.gif new file mode 100644 index 0000000000000000000000000000000000000000..afaa3e16705beb1655266a25997438ae7a6b39ec GIT binary patch literal 15282 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 ah;MiX{$AN%Q`1uaNQuCI?f