Open edyoung opened 4 years ago
We've started to work in this feature. @josebailo has created a branch in which Gnofract4D allows the user to hold down left/right mouse button to continuously zoom in/out the fractal. Under the hood, Gnofract4d still does just the same calculations as usual, and the result is not the smooth effect we're looking for. In order to get the desired result we need to change the way every "frame" is calculated. Here Thomas Marsh and Jan Hubicka (original authors of the XaoS project) explain how they got there. In the mentioned article, there's a subtle explanation of the many algorithms they use. We don't know yet if it's possible to bring them to Gnofract4d, due to the special features we have here. Nevertheless, in order to find out we're going to need some reverse engineering to understand their specifics.
I think the easiest way to get somewhere near the desired result is to follow an iterative process, starting from the most basic approach (maybe some kind of interpolation). To do that, we need to change the way Gnofract4d calculates the pixels and also add new data structures to reuse calculations from the previous frame (which is the foundation of the Xaos algorithms).
At this moment I'm working in the idea of creating a new IFractWorker
implementation (not sure yet if I will need a new fractFunc
as well) to be the place to implement the new algorithms. Hopefully I'll manage to keep the interface to ease switching between normal and XaoS zooming.
1st approach created by @josebailo is based on a Timer that zooms and calculates the frame every 300ms while the user holds the button. Problem with this is you don't know exactly how much time you need to calculate the previous frame, but you need the previous frame to be calculated. We need then to sync the animation frequency with the previous frame calculation ending.
On the other hand, while this is just a proof of concept for now, we need to define some requirements of what the final result should look like. As some of the current features could clash with it (the selection rect for example), I see this new feature as a different "working mode", like the user has to toggle some sort of switch to enable it (and disable other features)... or maybe the user needs to hold a key. What would be the better, or more consistent, approach?
We've been doing some progress these days:
XaosFractWorker
class, which is a lightweight version STFractWorker
. This class would be the responsible to perform the calculations when the continuous zoom mode is activated. By the time of writing this comment, the class has the ability to produce a new image based on a previous one with a wider value range (zooming in). To do so, it reuses the pixels from the old image that are within its value range. Of course this is yet incorrect, as it's only valid to go from the original image to the 1st zoom in level. Subsequent zoom levels shouldn't reuse all the pixels already reused by their previous image as they are already approximations. In summary, there's still a long way ahead and many pitfalls to discover (I have a feeling the 4D rotations will complicate things), but this 1st has been very insightful about how to bring Xaos algorithms into Gnofract.A last note about XaosFractWorker
: the reason to create a lightweight version of STFractWorker
is to avoid dealing with more complexity (stats, qbox drawing strategy, etc.). We are also not using MTFractWorker
yet, which enables having multiple workers and multiple threads. In the future we might even dispose XaosFractWorker
and add their abilities to STFractWorker
.
I think I've managed to make the new worker handle both zooming in and out. There's probably an easier approach for the maths in there and also I have to confirm everything (reused pixels) is put in the right place.
Next steps:
I think if we manage to complete these 2 steps we are good to merge and have a new "experimental feature". In the meantime, if you are curious, just checkout that branch, compile and launch, but try with 320x240 resolution, unless you have a very powerful CPU you'll get an error (not enough time to calculate previous frame).
On the other hand, we've been having some trouble with the tests. They got stuck at "test_director". We have to figure out how the new setting "enable/disable continuous zooming" is affecting them.
We made gtkfractal to syncrhonize the frame rate to avoid unfinished frames. We had to create a custom Thread class to have a setinterval-like behavior.
We also had to deal with a problem regarding button press/release: when the user double clicks on the image, the onButtonPress
event is called twice while the onButtonRelease
is called only once, that made the continuous zoom effect to stay hanged.
On the other hand we also managed to avoid reusing pixels from ancient frames, preventing carrying errors on subsequent frames.
The problem with the tests is already solved as well.
Next steps:
STFractWorker
and MTFractWorker
to XaosFractWorker
: multithread, rectangle guessing...This is what we've done since the las update:
All in all it seems the weird "flickering" effect is gone and performance has improved quite a bit: I'm able to run it for a 640x480 drawing area with a bearable framerate (I'm using my laptop, equipped with i5-8259U CPU @ 2.30GHz).
Next steps:
Actually, the second point would go 1st, as it would allow us to control the framerate and thus decide if we calculate all the pixels for the current frame, as the dynamic resolution algorithm states.
Updates since the last comment:
GLib.timeout_add
instead of python threads.fract4dc.calc
interface for every frame (that involved creating several objects), we have an infinite loop in C++ waiting for updates.
fract4dc.calcxaos
: this calculates the 1st frame and starts the infinite loopfract4dc.updatexaos
: this sends an update (new location params after recentering the image) to the C++ infinite loopfract4dc.interruptxaos
: this stops the infinite C++ loopCurrent status and plans:
I tried it out - very cool effect!
One UX suggestion - perhaps we could make this feature a bit more discoverable? I actually had to read the source to find there was a checkbox in the preferences to enable it. Maybe something that can be toggled in the toolbar (like Explorer Mode) to enable it?
By using a high resolution (2560x1600) I was able to trigger a seg fault in malloc - haven't looked more deeply yet but it presumably indicates a memory management issue somewhere.
Definitely we need to tell the user this feature is there, therefore user manual update should be included in the PR.
We thought about different approaches to enable the feature, including a mouse command (like holding "alt" + pointer button). However, since this is an experimental feature so far maybe it'd be a good idea to keep it a little bit "hidden" for now.
I've been investigating this seg fault error for a while. I've the hunch either it's related with Gtk or the image buffer python interface, but unfortunately my knowledge in both is very limited:
I've added a draft markdown document to try to explain everything we are doing in the CZ implementation:
https://github.com/HyveInnovate/gnofract4d/blob/continuous-zoom/doc/continuous_zooming.md
Source code comments should also help to understand fine grain detail.
Being trying several things to solve the segfault errors but no luck so far.
I've tried to use a copy of the image buffer
so in case the problem was related to simultaneous read/write (Gtk reading from one end and C++ extension updating the buffer from the other end). Didn't solve the problem.
I had also another suspect: "box guessing algorithm" works with sharing edges in between boxes, so I thought different threads could be trying to update the same pixel at once.
I'm out of ideas at this moment.
This is a sample trace log in MacOS 10.15.7 . To reproduce the problem just, madly, click left and right mouse button to zoom in/out the image:
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 libsystem_kernel.dylib 0x00007fff71df133a __pthread_kill + 10
1 libsystem_pthread.dylib 0x00007fff71eade60 pthread_kill + 430
2 libsystem_c.dylib 0x00007fff71d78808 abort + 120
3 libsystem_c.dylib 0x00007fff71d77ac6 __assert_rtn + 314
4 com.apple.CoreGraphics 0x00007fff3847ff84 CGBitmapFreeData.cold.1 + 35
5 com.apple.CoreGraphics 0x00007fff380730c4 CGBitmapFreeData + 34
6 com.apple.CoreGraphics 0x00007fff3805ee14 CGBitmapContextInfoRelease + 76
7 com.apple.CoreGraphics 0x00007fff3809b1e4 CGBitmapContextSetData + 184
8 com.apple.QuartzCore 0x00007fff4373584c CAGetCachedCGBitmapContext_(void*, unsigned int, unsigned int, unsigned int, unsigned long, CGColorSpace*) + 513
9 com.apple.QuartzCore 0x00007fff43735263 CABackingStoreBeginUpdate_(CABackingStore*, unsigned int, unsigned int, unsigned int, unsigned int, unsigned int, unsigned long long, unsigned int, UpdateState*) + 653
10 com.apple.QuartzCore 0x00007fff43733821 CABackingStoreUpdate_ + 519
11 com.apple.QuartzCore 0x00007fff437934ad invocation function for block in CA::Layer::display_() + 53
12 com.apple.QuartzCore 0x00007fff43732d86 -[CALayer _display] + 2103
13 com.apple.AppKit 0x00007fff3503a69a -[_NSBackingLayer display] + 537
14 com.apple.AppKit 0x00007fff34f9c187 -[_NSViewBackingLayer display] + 800
15 com.apple.QuartzCore 0x00007fff43731e09 CA::Layer::display_if_needed(CA::Transaction*) + 757
16 com.apple.QuartzCore 0x00007fff43710106 CA::Context::commit_transaction(CA::Transaction*, double) + 334
17 com.apple.QuartzCore 0x00007fff4370ecf0 CA::Transaction::commit() + 644
18 com.apple.AppKit 0x00007fff35050da1 __62+[CATransaction(NSCATransaction) NS_setFlushesWithDisplayLink]_block_invoke + 266
19 com.apple.AppKit 0x00007fff35770080 ___NSRunLoopObserverCreateWithHandler_block_invoke + 41
20 com.apple.CoreFoundation 0x00007fff37c40335 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
21 com.apple.CoreFoundation 0x00007fff37c40267 __CFRunLoopDoObservers + 457
22 com.apple.CoreFoundation 0x00007fff37c3f805 __CFRunLoopRun + 874
23 com.apple.CoreFoundation 0x00007fff37c3ee3e CFRunLoopRunSpecific + 462
24 com.apple.HIToolbox 0x00007fff3686babd RunCurrentEventLoopInMode + 292
25 com.apple.HIToolbox 0x00007fff3686b7d5 ReceiveNextEventCommon + 584
26 com.apple.HIToolbox 0x00007fff3686b579 _BlockUntilNextEventMatchingListInModeWithFilter + 64
27 com.apple.AppKit 0x00007fff34eb1039 _DPSNextEvent + 883
28 com.apple.AppKit 0x00007fff34eaf880 -[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 1352
29 libgdk-3.0.dylib 0x000000010c2f13ac poll_func + 172
30 libglib-2.0.0.dylib 0x000000010b733ff1 g_main_context_iterate + 433
31 libglib-2.0.0.dylib 0x000000010b73437f g_main_loop_run + 239
32 libgtk-3.0.dylib 0x00000001103037aa gtk_main + 74
33 libffi.7.dylib 0x000000010b8bef2d ffi_call_unix64 + 85
34 libffi.7.dylib 0x000000010b8be7fe ffi_call_int + 721
35 _gi.cpython-39-darwin.so 0x000000010b6be9d2 pygi_invoke_c_callable + 2338
36 _gi.cpython-39-darwin.so 0x000000010b6bf9e7 pygi_function_cache_invoke + 55
37 org.python.python 0x000000010a922fc9 _PyObject_Call + 138
38 org.python.python 0x000000010a9c8064 _PyEval_EvalFrameDefault + 28268
39 org.python.python 0x000000010a9cb6eb _PyEval_EvalCode + 1998
40 org.python.python 0x000000010a92317c _PyFunction_Vectorcall + 248
41 org.python.python 0x000000010a9caba3 call_function + 403
42 org.python.python 0x000000010a9c7d34 _PyEval_EvalFrameDefault + 27452
43 org.python.python 0x000000010a9231ec function_code_fastcall + 97
44 org.python.python 0x000000010a9caba3 call_function + 403
45 org.python.python 0x000000010a9c7de0 _PyEval_EvalFrameDefault + 27624
46 org.python.python 0x000000010a9231ec function_code_fastcall + 97
47 org.python.python 0x000000010a9caba3 call_function + 403
48 org.python.python 0x000000010a9c7de0 _PyEval_EvalFrameDefault + 27624
49 org.python.python 0x000000010a9cb6eb _PyEval_EvalCode + 1998
50 org.python.python 0x000000010a9c111d PyEval_EvalCode + 79
51 org.python.python 0x000000010a9fc185 run_eval_code_obj + 110
52 org.python.python 0x000000010a9fb57d run_mod + 103
53 org.python.python 0x000000010a9fa441 PyRun_FileExFlags + 241
54 org.python.python 0x000000010a9f9a31 PyRun_SimpleFileExFlags + 271
55 org.python.python 0x000000010aa1194d Py_RunMain + 1839
56 org.python.python 0x000000010aa11c86 pymain_main + 306
57 org.python.python 0x000000010aa11cd4 Py_BytesMain + 42
58 libdyld.dylib 0x00007fff71ca9cc9 start + 1
Thread 1:
0 libsystem_pthread.dylib 0x00007fff71ea9b68 start_wqthread + 0
Thread 2:
0 libsystem_kernel.dylib 0x00007fff71df13d6 poll + 10
1 libgdk-3.0.dylib 0x000000010c2f1e54 select_thread_func + 212
2 libsystem_pthread.dylib 0x00007fff71eae109 _pthread_start + 148
3 libsystem_pthread.dylib 0x00007fff71ea9b8b thread_start + 15
Thread 3:
0 libsystem_pthread.dylib 0x00007fff71ea9b68 start_wqthread + 0
Thread 4:
0 libsystem_pthread.dylib 0x00007fff71ea9b68 start_wqthread + 0
Thread 5:
0 fract4dc.cpython-39-darwin.so 0x000000010b58e7f0 image::get(int, int) const + 0 (image.cpp:120)
1 fract4dc.cpython-39-darwin.so 0x000000010b58765d XaosFractWorker::reuse_pixels() + 2861 (XaosFractWorker.cpp:133)
2 fract4dc.cpython-39-darwin.so 0x000000010b58d462 fractFunc::draw_all_xaos() + 1042 (fractfunc.cpp:208)
3 fract4dc.cpython-39-darwin.so 0x000000010b58a3ea calc_xaos + 266 (calcfunc.cpp:70)
4 fract4dc.cpython-39-darwin.so 0x000000010b57cfc8 calculation_thread_xaos(calc_args*) + 104 (calcs.cpp:168)
5 fract4dc.cpython-39-darwin.so 0x000000010b57d2cc void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, void* (*)(calc_args*), calc_args*> >(void*) + 44 (thread:289)
6 libsystem_pthread.dylib 0x00007fff71eae109 _pthread_start + 148
7 libsystem_pthread.dylib 0x00007fff71ea9b8b thread_start + 15
Thread 0 crashed with X86 Thread State (64-bit):
rax: 0x0000000000000000 rbx: 0x000000011574cdc0 rcx: 0x00007ffee53183a8 rdx: 0x0000000000000000
rdi: 0x0000000000000307 rsi: 0x0000000000000006 rbp: 0x00007ffee53183d0 rsp: 0x00007ffee53183a8
r8: 0x00000000000000e8 r9: 0xcccccccccccccccd r10: 0x000000011574cdc0 r11: 0x0000000000000246
r12: 0x0000000000000307 r13: 0x000000011265c000 r14: 0x0000000000000006 r15: 0x0000000000000016
rip: 0x00007fff71df133a rfl: 0x0000000000000246 cr2: 0x000000011265c000
Programs like Xaos provide the ability to hold down a mouse button and zoom continuously into the fractal, as opposed to the click-by-click operation Gnofract 4D currently supports.