Skip to content

Mandelbrot Set: fix the hang, centre the zoom, add held-key panning - #241

Merged
mishamyte merged 1 commit into
devfrom
fix/mandelbrot-hang-and-controls
Jul 23, 2026
Merged

Mandelbrot Set: fix the hang, centre the zoom, add held-key panning#241
mishamyte merged 1 commit into
devfrom
fix/mandelbrot-hang-and-controls

Conversation

@mishamyte

Copy link
Copy Markdown
Collaborator

Closes #160
Closes #17

The hang (#160)

The fractal was computed inside the draw callback, so all 8192 pixels were iterated on the GUI service thread. Three things compounded:

  • view_port_update() ran on every loop iteration including the 100 ms queue timeout, so the app demanded a fresh frame at least ten times a second whether or not anything had moved.
  • The draw callback held plugin_state->mutex for the entire 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 each press it consumed immediately queued another frame.
  • The input callback pushed with FuriWaitForever into an 8-slot queue.

Once enough pixels reached the 50-iteration cap, frame time exceeded the redraw interval and the GUI thread never went idle. Presses arriving faster than that - key auto-repeat while panning does exactly this - fill the queue, at which point the input callback blocks the GUI service thread itself. That matches the report: zoom a few times, hold a direction, everything stops.

Worth recording since it was my first theory and it was wrong: the 2.0/4.0/128.0 double literals cost nothing. GCC narrows them - the old binary disassembles to pure vmul.f32/vfma.f32 with zero __aeabi_d* calls. The bottleneck was the threading, not the arithmetic.

The fix

  • Compute on the app thread, only when the view actually changed, and hand the draw callback a ready-made XBM bitmap - one canvas_draw_xbm instead of up to 8192 canvas_draw_dot calls.
  • Coarse 4x4 preview published first, then full resolution refined in bands, polling the queue between scanlines. A slow picture is abandoned the moment a key is pressed, so input latency is bounded by one scanline instead of one frame.
  • Main cardioid and period-2 bulb interior tests skip the iteration loop - these are the two large solid regions that always cost the full budget, i.e. exactly the case that used to wedge the app.
  • The input callback no longer waits. Nothing tracks which keys are down, so a dropped event costs one pan step; parking the GUI service thread costs the whole UI.

Controls (#17)

All three requests in the issue, plus zoom-out which the app never had:

Arrows (tap or hold) pan, 1/8 screen per step, scaled to the zoom level
Ok tap zoom in 2x, centred
Ok hold zoom out 2x, repeats while held
Back exit
  • Centred zoom. The centre is now fixed and only the span around it changes. Previously the span shrank while the top-left edge stayed pinned, so everything contracted into the corner.
  • Each step halves or doubles the span. The old step subtracted a fixed amount from it, which drove the span through zero and mirrored the picture after about 23 presses.
  • Zoom stops at 16384x, where float32 runs out of mantissa, and the zoom factor is shown briefly after each step (x2 ... x16384 max) so hitting either limit is visible rather than looking like the app stopped reacting.
  • The window is kept within reach of the set, so panning cannot strand the view in empty space with no way back.

Also: square pixels (ratio = 128/64 was applied on top of an already-wider x range, squashing the picture by about a tenth) and an iteration budget that grows with zoom depth, since a fixed 50 turned deep views into a solid blob.

Metadata

fap_weburl pointed at github.com/Possibly-Matt, which now 404s - the account is gone. Repointed at this repo (following eth_troubleshooter's precedent) and the same dead link dropped from the ReadMe row, keeping the credit as plain text. This was the second half of #160. Adds CHANGELOG.md backfilled from repo history, bumps fap_version to 1.4, raises the stack 1 KB -> 2 KB for the zoom indicator's snprintf.

Verification

Builds clean under standalone ufbt (API 88.0), ufbt format applied, recompiled with the project's exact flags with no diagnostics. Deployed and launched on hardware.

No unit tests in this repo, so I modelled the renderer exactly in Python with float32 rounding and checked it three ways:

  • Fine pass vs an independent reference render: 0 mismatching pixels at zoom levels 0, 5 and 14 - confirms XBM bit order, coarse block masks and the coordinate mapping.
  • Cardioid/bulb shortcut vs pure iteration over 32768 points across four views: 0 disagreements, short-circuiting 67% of points.
  • float32 vs float64 divergence to justify the zoom cap: 0% at level 8, 5% at 14, 9.6% at 16, with adjacent pixels still distinct at 18 - so 16384x is conservative.

img/1.png and img/2.png still show the old border frame and framing; they need real device captures rather than model-rendered stand-ins.

🤖 Generated with Claude Code

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) <noreply@anthropic.com>
@github-actions github-actions Bot added category/games App category: games pack/catalog App in apps_source_code (catalog) labels Jul 23, 2026
@mishamyte mishamyte added this to the unlshd-090 milestone Jul 23, 2026
@mishamyte mishamyte added type/bug Something isn't working type/enhancement New feature or request labels Jul 23, 2026
@mishamyte
mishamyte merged commit 4d6861b into dev Jul 23, 2026
2 checks passed
@mishamyte
mishamyte deleted the fix/mandelbrot-hang-and-controls branch July 23, 2026 21:39
Repository owner locked as resolved and limited conversation to collaborators Jul 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

category/games App category: games pack/catalog App in apps_source_code (catalog) type/bug Something isn't working type/enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"Mandelbrot Set" hangs after a dozen button presses mandelbtot set: better controls

1 participant