From c737a3925e0b3aa1cfa11c1d0126c67df2674ee7 Mon Sep 17 00:00:00 2001 From: Mykhailo Shevchuk Date: Fri, 24 Jul 2026 00:35:53 +0300 Subject: [PATCH] Mandelbrot Set: fix the hang, centre the zoom, add held-key panning Fixes #160 Closes #17 The fractal was computed inside the draw callback, so all 8192 pixels were iterated on the GUI service thread, and the event loop asked for a fresh frame at least ten times a second whether or not the view had moved (view_port_update ran on the 100 ms queue timeout path too). The draw callback held the state mutex for the whole computation and the event loop needed that same mutex before it could act on an event, so the app could consume at most one button press per frame - and every press it consumed immediately queued another frame. Once enough pixels reached the 50-iteration cap, presses arriving faster than the frame rate (key auto-repeat while panning does exactly this) filled the 8-slot queue, at which point the input callback's FuriWaitForever put blocked the GUI service thread itself. - Compute on the app thread, only when the view actually changed, and publish as an XBM bitmap drawn with a single canvas_draw_xbm instead of up to 8192 canvas_draw_dot calls - Show a coarse 4x4 preview first, then refine at full resolution in bands, polling the queue between scanlines so a slow picture is abandoned as soon as a key is pressed - Skip the iteration loop for the main cardioid and the period-2 bulb, the two large solid regions that always cost the full budget - Never wait in the input callback: dropping a pan step is cheaper than parking the GUI service thread on a full queue - Zoom around the middle of the screen and halve or double the span per step; the old step subtracted a fixed amount from the span, which drove it through zero and mirrored the picture after ~23 presses - Hold Ok to zoom out (previously impossible), capped at 16384x where float32 runs out of mantissa, with the zoom factor shown briefly so reaching either limit is visible - Arrows react to press, long and repeat, panning by a fixed fraction of the screen; the window is kept within reach of the set so panning cannot strand the view in empty space - Square pixels (the picture was squashed horizontally by about a tenth) and an iteration budget that grows with zoom depth - Point fap_weburl at this repo and drop the same dead link from ReadMe.md, keeping the credit: github.com/Possibly-Matt now 404s - Add CHANGELOG.md backfilled from repo history; bump to 1.4, and raise the stack to 2 KB for the zoom indicator's snprintf Co-Authored-By: Claude Opus 4.8 (1M context) --- ReadMe.md | 2 +- apps_source_code/mandelbrot/CHANGELOG.md | 31 ++ apps_source_code/mandelbrot/application.fam | 6 +- apps_source_code/mandelbrot/mandelbrot.c | 439 ++++++++++++++------ 4 files changed, 347 insertions(+), 131 deletions(-) create mode 100644 apps_source_code/mandelbrot/CHANGELOG.md diff --git a/ReadMe.md b/ReadMe.md index 0ff08ec6..67017823 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -141,7 +141,7 @@ The Flipper and its community wouldn't be as rich as it is without your contribu | Etch-A-Sketch | ![Games Badge] | [by SimplyMinimal](https://github.com/SimplyMinimal/FlipperZero-Etch-A-Sketch) | | ![None Badge] | | Pong | ![Games Badge] | [by nmrr](https://github.com/nmrr/flipperzero-pong) | Modified by [SimplyMinimal](https://github.com/SimplyMinimal/FlipperZero-Pong) | [![UFW Badge]](https://lab.flipper.net/apps/flipper_pong) | | Game of Life (Updated to work by tgxn) | ![Games Badge] | [by itsyourbedtime](https://github.com/tgxn/flipperzero-firmware/blob/dev/applications/game_of_life/game_of_life.c) | | [![UFW Badge]](https://lab.flipper.net/apps/gameoflife) | -| Mandelbrot Set | ![Games Badge] | [by Possibly-Matt](https://github.com/Possibly-Matt/flipperzero-firmware-wPlugins) | | [![UFW Badge]](https://lab.flipper.net/apps/mandelbrotset) | +| Mandelbrot Set | ![Games Badge] | by Possibly-Matt | | [![UFW Badge]](https://lab.flipper.net/apps/mandelbrotset) | | Monty Hall | ![Games Badge] | [by DevMilanIan](https://github.com/RogueMaster/flipperzero-firmware-wPlugins/pull/203) | | [![UFW Badge]](https://lab.flipper.net/apps/montyhall) | | Nu Pogodi! (Ну, погоди!) | ![Games Badge] | [by sionyx](https://github.com/sionyx/flipper_nupogodi) | | [![UFW Badge]](https://lab.flipper.net/apps/nupogodi) | | Paint | ![Games Badge] | [by iinsaane](https://github.com/iinsaane) | | [![UFW Badge]](https://lab.flipper.net/apps/paint) | diff --git a/apps_source_code/mandelbrot/CHANGELOG.md b/apps_source_code/mandelbrot/CHANGELOG.md new file mode 100644 index 00000000..24334ca2 --- /dev/null +++ b/apps_source_code/mandelbrot/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +## 1.4 + +- Compute the picture on the app thread, only when the view actually changed, and hand it to the draw callback as a ready-made bitmap. It used to be computed inside the draw callback, on the GUI service thread, and requested afresh at least ten times a second whether or not anything had moved; once enough pixels reached the iteration limit the app could no longer keep up with its own redraws and the device stopped responding to buttons +- Show a coarse preview first and refine it band by band, checking for key presses between scanlines, so a slow picture at high zoom can be interrupted instead of holding up the event loop +- Skip the iteration loop for points in the main cardioid and the period-2 bulb, the two large solid regions that always cost the full iteration budget +- Zoom around the middle of the screen; zooming used to shrink the picture towards its top left corner +- Halve or double the visible span per zoom step. The old step subtracted a fixed amount from the span, which drove it through zero and mirrored the picture after about two dozen presses +- Hold Ok to zoom back out, which the app previously could not do at all, and stop zooming in at 16384x, where single precision floats run out of mantissa +- Show the zoom factor for a moment after each step, so reaching either limit is visible instead of looking like the app stopped reacting +- Hold a direction key to keep panning, and pan by a fixed fraction of the screen so the step stays usable at every zoom level +- Keep the visible window within reach of the set, so panning cannot strand the view in empty space with no way back +- Use square pixels and raise the iteration budget with the zoom level: the picture used to be squashed horizontally by about a tenth, and deep views collapsed into a solid blob +- Point the app link at this repository, since the original author's GitHub account no longer exists + +## 1.3 + +- Version bump for catalog compatibility (no functional changes) + +## 1.2 + +- Version bump for catalog compatibility (no functional changes) + +## 1.1 + +- Release the state mutex before asking for a redraw, instead of holding it across the request + +## 1.0 + +- Initial release: browse the Mandelbrot set with the arrow keys, Ok to zoom in diff --git a/apps_source_code/mandelbrot/application.fam b/apps_source_code/mandelbrot/application.fam index 4f5fff01..fd5678a3 100644 --- a/apps_source_code/mandelbrot/application.fam +++ b/apps_source_code/mandelbrot/application.fam @@ -5,12 +5,12 @@ App( entry_point="mandelbrot_app", cdefines=["APP_MANDELBROT_GAME"], requires=["gui"], - stack_size=1 * 1024, + stack_size=2 * 1024, order=130, fap_icon="Mandelbrot.png", fap_category="Games", fap_author="@Possibly-Matt", - fap_weburl="https://github.com/Possibly-Matt", - fap_version="1.3", + fap_weburl="https://github.com/xMasterX/all-the-plugins", + fap_version="1.4", fap_description="The Mandelbrot set is the set of all so-called (complex) numbers that meet Mandelbrots simple arithmetic criterion.", ) diff --git a/apps_source_code/mandelbrot/mandelbrot.c b/apps_source_code/mandelbrot/mandelbrot.c index c3ef2523..1d953e4f 100644 --- a/apps_source_code/mandelbrot/mandelbrot.c +++ b/apps_source_code/mandelbrot/mandelbrot.c @@ -1,173 +1,358 @@ #include -#include #include #include +#include #include +#include -typedef enum { - EventTypeTick, - EventTypeKey, -} EventType; +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +#define IMAGE_STRIDE (SCREEN_WIDTH / 8) +#define IMAGE_SIZE (IMAGE_STRIDE * SCREEN_HEIGHT) -typedef struct { - EventType type; - InputEvent input; -} PluginEvent; +// The picture is computed in two passes: one sample per COARSE_STEP x COARSE_STEP block +// first, so something appears almost immediately, then at full resolution, published +// every REFINE_ROWS scanlines so the refinement is visible as it happens. +#define COARSE_STEP 4 +#define REFINE_ROWS 16 + +// Home view: the complete set, filling the height of the 2:1 screen. +#define HOME_CENTER_RE (-0.7f) +#define HOME_CENTER_IM (0.0f) +#define HOME_SPAN (4.2f) + +// One zoom step halves the visible span. Past 2^14 the float mantissa runs out: +// neighbouring pixels start landing on the same complex number and the picture breaks +// up into blocks instead of showing more detail. +#define ZOOM_LEVEL_MAX 14 + +// Deeper views need a bigger iteration budget, otherwise everything inside the set +// merges into one solid blob. +#define ITERATIONS_BASE 40 +#define ITERATIONS_PER_LEVEL 12 + +// An eighth of the screen per step, so panning stays proportional at every zoom level. +#define PAN_STEP_X (SCREEN_WIDTH / 8) +#define PAN_STEP_Y (SCREEN_HEIGHT / 8) + +// Keep the view within reach of the set, so panning cannot strand the user in an +// endless empty plane with no way back. +#define CENTER_RE_MIN (-2.5f) +#define CENTER_RE_MAX (1.5f) +#define CENTER_IM_MIN (-1.5f) +#define CENTER_IM_MAX (1.5f) + +#define HINT_DURATION_MS 2000 +#define HINT_STARTUP "Hold OK to zoom out" typedef struct { + FuriMessageQueue* queue; + ViewPort* view_port; FuriMutex* mutex; - float xZoom; - float yZoom; - float xOffset; - float yOffset; - float zoom; -} PluginState; - -bool mandelbrot_pixel(int x, int y, float xZoom, float yZoom, float xOffset, float yOffset) { - float ratio = 128.0 / 64.0; - //x0 := scaled x coordinate of pixel (scaled to lie in the Mandelbrot X scale (-2.00, 0.47)) - float x0 = (((x / 128.0) * ratio * xZoom)) - xOffset; - //y0 := scaled y coordinate of pixel (scaled to lie in the Mandelbrot Y scale (-1.12, 1.12)) - float y0 = ((y / 64.0) * yZoom) - yOffset; - float x1 = 0.0; - float y1 = 0.0; - float x2 = 0.0; - float y2 = 0.0; - - int iteration = 0; - int max_iteration = 50; - - while(x2 + y2 <= 4.0 && iteration < max_iteration) { - y1 = 2.0 * x1 * y1 + y0; - x1 = x2 - y2 + x0; - x2 = x1 * x1; - y2 = y1 * y1; + + // Owned by the app thread + float center_re; + float center_im; + uint8_t zoom_level; + bool redraw; + bool running; + bool hint_shown; + uint32_t hint_expiry; + uint8_t work[IMAGE_SIZE]; + + // Read by the draw callback on the GUI thread, guarded by mutex + uint8_t image[IMAGE_SIZE]; + char hint[24]; +} Mandelbrot; + +static uint16_t mandelbrot_iterations(const Mandelbrot* app) { + return ITERATIONS_BASE + app->zoom_level * ITERATIONS_PER_LEVEL; +} + +/** Size of one pixel in the complex plane. Equal on both axes, so the picture keeps its + * shape instead of being stretched to the screen aspect ratio. */ +static float mandelbrot_scale(const Mandelbrot* app) { + return HOME_SPAN / (float)((uint32_t)SCREEN_WIDTH << app->zoom_level); +} + +/** Escape time test for a single point: true when the orbit is still bounded after the + * whole iteration budget, which is what gets painted black. */ +static bool mandelbrot_inside(float re, float im, uint16_t max_iterations) { + // The main cardioid and the period-2 bulb are the two largest solid regions and + // always cost the full budget, so test for them directly instead of iterating. + const float dre = re - 0.25f; + const float q = dre * dre + im * im; + if(q * (q + dre) <= 0.25f * im * im) return true; + const float bulb = re + 1.0f; + if(bulb * bulb + im * im <= 0.0625f) return true; + + float zr = 0.0f; + float zi = 0.0f; + float zr2 = 0.0f; + float zi2 = 0.0f; + uint16_t iteration = 0; + + while(zr2 + zi2 <= 4.0f && iteration < max_iterations) { + zi = 2.0f * zr * zi + im; + zr = zr2 - zi2 + re; + zr2 = zr * zr; + zi2 = zi * zi; iteration++; } - if(iteration > 49) { - return true; + return iteration == max_iterations; +} + +/** Set or clear a COARSE_STEP wide square of the coarse pass. Blocks are aligned to + * their own width, so a row of one never straddles a byte boundary. */ +static void mandelbrot_fill_block(uint8_t* image, uint8_t x, uint8_t y, bool inside) { + const uint8_t mask = (uint8_t)(((1u << COARSE_STEP) - 1u) << (x % 8)); + + for(uint8_t row = 0; row < COARSE_STEP; row++) { + uint8_t* byte = image + (size_t)(y + row) * IMAGE_STRIDE + x / 8; + if(inside) { + *byte |= mask; + } else { + *byte &= (uint8_t)~mask; + } } +} - return false; +/** Hand the work buffer to the draw callback and ask for a repaint. */ +static void mandelbrot_publish(Mandelbrot* app) { + furi_mutex_acquire(app->mutex, FuriWaitForever); + memcpy(app->image, app->work, IMAGE_SIZE); + furi_mutex_release(app->mutex); + view_port_update(app->view_port); } -static void render_callback(Canvas* const canvas, void* ctx) { - furi_assert(ctx); - const PluginState* plugin_state = ctx; - furi_mutex_acquire(plugin_state->mutex, FuriWaitForever); - // border around the edge of the screen - canvas_draw_frame(canvas, 0, 0, 128, 64); - - for(int y = 0; y < 64; y++) { - for(int x = 0; x < 128; x++) { - // did you know if you just pass the indivdiual bits of plugin_state instead of plugin_state - // you dont get any compiler warnings :) - if(mandelbrot_pixel( - x, - y, - plugin_state->xZoom, - plugin_state->yZoom, - plugin_state->xOffset, - plugin_state->yOffset)) { - canvas_draw_dot(canvas, x, y); - } +/** Show a message at the bottom of the screen for a moment. */ +static void mandelbrot_hint(Mandelbrot* app, const char* text) { + furi_mutex_acquire(app->mutex, FuriWaitForever); + snprintf(app->hint, sizeof(app->hint), "%s", text); + furi_mutex_release(app->mutex); + + app->hint_shown = true; + app->hint_expiry = furi_get_tick() + furi_ms_to_ticks(HINT_DURATION_MS); + view_port_update(app->view_port); +} + +static void mandelbrot_hint_hide(Mandelbrot* app) { + furi_mutex_acquire(app->mutex, FuriWaitForever); + app->hint[0] = '\0'; + furi_mutex_release(app->mutex); + + app->hint_shown = false; + view_port_update(app->view_port); +} + +static void mandelbrot_pan(Mandelbrot* app, int8_t steps_re, int8_t steps_im) { + const float scale = mandelbrot_scale(app); + const float re = + CLAMP(app->center_re + steps_re * (PAN_STEP_X * scale), CENTER_RE_MAX, CENTER_RE_MIN); + const float im = + CLAMP(app->center_im + steps_im * (PAN_STEP_Y * scale), CENTER_IM_MAX, CENTER_IM_MIN); + + if(re != app->center_re || im != app->center_im) { + app->center_re = re; + app->center_im = im; + app->redraw = true; + } +} + +/** Zoom one step around the middle of the screen, which is the point the user is + * looking at. The centre never moves, only the span around it changes. */ +static void mandelbrot_zoom(Mandelbrot* app, int8_t direction) { + if(direction > 0 && app->zoom_level < ZOOM_LEVEL_MAX) { + app->zoom_level++; + app->redraw = true; + } else if(direction < 0 && app->zoom_level > 0) { + app->zoom_level--; + app->redraw = true; + } + + // Also reported when the step was refused, so that hitting either limit is visible + // instead of looking like the app stopped reacting. + char text[sizeof(app->hint)]; + snprintf( + text, + sizeof(text), + "x%lu%s", + (unsigned long)(1UL << app->zoom_level), + app->zoom_level == ZOOM_LEVEL_MAX ? " max" : ""); + mandelbrot_hint(app, text); +} + +static void mandelbrot_handle_input(Mandelbrot* app, const InputEvent* event) { + // Arrows react to press, long and repeat, so holding a direction keeps panning. + const bool held = event->type == InputTypePress || event->type == InputTypeLong || + event->type == InputTypeRepeat; + + switch(event->key) { + case InputKeyUp: + if(held) mandelbrot_pan(app, 0, 1); + break; + case InputKeyDown: + if(held) mandelbrot_pan(app, 0, -1); + break; + case InputKeyLeft: + if(held) mandelbrot_pan(app, -1, 0); + break; + case InputKeyRight: + if(held) mandelbrot_pan(app, 1, 0); + break; + case InputKeyOk: + // A tap zooms in, holding Ok zooms back out. + if(event->type == InputTypeShort) { + mandelbrot_zoom(app, 1); + } else if(event->type == InputTypeLong || event->type == InputTypeRepeat) { + mandelbrot_zoom(app, -1); + } + break; + case InputKeyBack: + if(event->type == InputTypePress) app->running = false; + break; + default: + break; + } +} + +/** Apply everything the user has pressed so far. Returns true once the picture being + * computed is out of date, which is the signal to abandon it and start over. */ +static bool mandelbrot_poll_input(Mandelbrot* app) { + InputEvent event; + + while(furi_message_queue_get(app->queue, &event, 0) == FuriStatusOk) { + mandelbrot_handle_input(app, &event); + } + + return app->redraw || !app->running; +} + +/** Compute the visible window on the app thread, checking for input between scanlines + * so that a slow picture can never hold up the GUI service or the event queue. */ +static void mandelbrot_render(Mandelbrot* app) { + const uint16_t max_iterations = mandelbrot_iterations(app); + const float scale = mandelbrot_scale(app); + const float re_origin = app->center_re - (SCREEN_WIDTH / 2 - 0.5f) * scale; + const float im_origin = app->center_im + (SCREEN_HEIGHT / 2 - 0.5f) * scale; + + for(uint8_t y = 0; y < SCREEN_HEIGHT; y += COARSE_STEP) { + const float im = im_origin - (y + COARSE_STEP / 2) * scale; + for(uint8_t x = 0; x < SCREEN_WIDTH; x += COARSE_STEP) { + const float re = re_origin + (x + COARSE_STEP / 2) * scale; + mandelbrot_fill_block(app->work, x, y, mandelbrot_inside(re, im, max_iterations)); } + if(mandelbrot_poll_input(app)) return; } + mandelbrot_publish(app); + + for(uint8_t y = 0; y < SCREEN_HEIGHT; y++) { + uint8_t* row = app->work + (size_t)y * IMAGE_STRIDE; + const float im = im_origin - y * scale; - furi_mutex_release(plugin_state->mutex); + for(uint8_t byte = 0; byte < IMAGE_STRIDE; byte++) { + uint8_t bits = 0; + for(uint8_t bit = 0; bit < 8; bit++) { + const float re = re_origin + (byte * 8 + bit) * scale; + if(mandelbrot_inside(re, im, max_iterations)) bits |= 1u << bit; + } + row[byte] = bits; + } + + if(mandelbrot_poll_input(app)) return; + if((y + 1) % REFINE_ROWS == 0 || y + 1 == SCREEN_HEIGHT) mandelbrot_publish(app); + } } -static void input_callback(InputEvent* input_event, void* ctx) { - furi_assert(ctx); - FuriMessageQueue* event_queue = ctx; +static void mandelbrot_draw_callback(Canvas* canvas, void* context) { + furi_assert(context); + Mandelbrot* app = context; + char hint[sizeof(app->hint)]; - PluginEvent event = {.type = EventTypeKey, .input = *input_event}; - furi_message_queue_put(event_queue, &event, FuriWaitForever); + furi_mutex_acquire(app->mutex, FuriWaitForever); + canvas_draw_xbm(canvas, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, app->image); + memcpy(hint, app->hint, sizeof(hint)); + furi_mutex_release(app->mutex); + + if(hint[0] != '\0') { + canvas_set_font(canvas, FontSecondary); + const size_t box_height = canvas_current_font_height(canvas) + 3; + canvas_set_color(canvas, ColorWhite); + canvas_draw_box( + canvas, + 0, + SCREEN_HEIGHT - box_height, + canvas_string_width(canvas, hint) + 4, + box_height); + canvas_set_color(canvas, ColorBlack); + canvas_draw_str(canvas, 2, SCREEN_HEIGHT - 3, hint); + } } -static void mandelbrot_state_init(PluginState* const plugin_state) { - plugin_state->xOffset = 3.0; - plugin_state->yOffset = 1.12; - plugin_state->xZoom = 2.47; - plugin_state->yZoom = 2.24; - plugin_state->zoom = 1; // this controls the camera when +static void mandelbrot_input_callback(InputEvent* input_event, void* context) { + furi_assert(context); + FuriMessageQueue* queue = context; + + // Runs on the GUI service thread, so it must never wait: a backlog of pan steps is + // worth far less than keeping the rest of the system responsive. Nothing here tracks + // which keys are down, so a dropped event only costs one step. + furi_message_queue_put(queue, input_event, 0); } int32_t mandelbrot_app(void* p) { UNUSED(p); - FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent)); - - PluginState* plugin_state = malloc(sizeof(PluginState)); - mandelbrot_state_init(plugin_state); - plugin_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal); - if(!plugin_state->mutex) { - FURI_LOG_E("mandelbrot", "cannot create mutex\r\n"); - furi_message_queue_free(event_queue); - free(plugin_state); - return 255; - } + Mandelbrot* app = malloc(sizeof(Mandelbrot)); + memset(app, 0, sizeof(Mandelbrot)); + app->center_re = HOME_CENTER_RE; + app->center_im = HOME_CENTER_IM; + app->running = true; + app->redraw = true; + app->queue = furi_message_queue_alloc(8, sizeof(InputEvent)); + app->mutex = furi_mutex_alloc(FuriMutexTypeNormal); - // Set system callbacks ViewPort* view_port = view_port_alloc(); - view_port_draw_callback_set(view_port, render_callback, plugin_state); - view_port_input_callback_set(view_port, input_callback, event_queue); + app->view_port = view_port; + view_port_draw_callback_set(view_port, mandelbrot_draw_callback, app); + view_port_input_callback_set(view_port, mandelbrot_input_callback, app->queue); - // Open GUI and register view_port Gui* gui = furi_record_open(RECORD_GUI); gui_add_view_port(gui, view_port, GuiLayerFullscreen); - PluginEvent event; - for(bool processing = true; processing;) { - FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100); - furi_mutex_acquire(plugin_state->mutex, FuriWaitForever); - - if(event_status == FuriStatusOk) { - // press events - if(event.type == EventTypeKey) { - if(event.input.type == InputTypePress) { - switch(event.input.key) { - case InputKeyUp: - plugin_state->yOffset += 0.1 / plugin_state->zoom; - break; - case InputKeyDown: - plugin_state->yOffset += -0.1 / plugin_state->zoom; - break; - case InputKeyRight: - plugin_state->xOffset += -0.1 / plugin_state->zoom; - break; - case InputKeyLeft: - plugin_state->xOffset += 0.1 / plugin_state->zoom; - break; - case InputKeyOk: - plugin_state->xZoom -= (2.47 / 10) / plugin_state->zoom; - plugin_state->yZoom -= (2.24 / 10) / plugin_state->zoom; - // used to make camera control finer the more zoomed you are - // this needs to be some sort of curve - plugin_state->zoom += 0.15; - break; - case InputKeyBack: - processing = false; - break; - default: - break; - } - } - } + mandelbrot_hint(app, HINT_STARTUP); + + while(app->running) { + if(app->redraw) { + app->redraw = false; + mandelbrot_render(app); + continue; + } + + // Sleep until the next key press, or until the hint on screen has had its time. + uint32_t timeout = FuriWaitForever; + if(app->hint_shown) { + const int32_t remaining = (int32_t)(app->hint_expiry - furi_get_tick()); + timeout = remaining > 0 ? (uint32_t)remaining : 0; + } + + InputEvent event; + if(furi_message_queue_get(app->queue, &event, timeout) == FuriStatusOk) { + mandelbrot_handle_input(app, &event); } - furi_mutex_release(plugin_state->mutex); - view_port_update(view_port); + if(app->hint_shown && (int32_t)(furi_get_tick() - app->hint_expiry) >= 0) { + mandelbrot_hint_hide(app); + } } view_port_enabled_set(view_port, false); gui_remove_view_port(gui, view_port); furi_record_close(RECORD_GUI); view_port_free(view_port); - furi_message_queue_free(event_queue); - furi_mutex_free(plugin_state->mutex); - free(plugin_state); + furi_message_queue_free(app->queue); + furi_mutex_free(app->mutex); + free(app); return 0; }