Mandelbrot

Source: src/mandelbrot.c · 823 lines · 35 functions.

Screen captures

Views of this part running on the Mini Arcade.

Mandelbrot viewer on the Mini Arcade
Mandelbrot showing the fractal viewer.

Module entry points

These functions are exposed to the rest of the project. Open one to follow its decisions and the helpers it calls.

How to read these diagrams

Each function has its own expandable diagram using the actual expressions and statements from this file. Decisions split into True and False; loop bodies are indented. Returns end the function immediately; break and continue follow their C meanings. Arrows between statements show their order when execution reaches them.

These are structural source diagrams before preprocessing, not runtime traces. Conditional compilation and macros remain source expressions. Ternary choices remain inside their statement. Calls are linked separately; callback execution and task scheduling are not inferred.

Functions in this file

refresh_maths_text() · source line 122
static void refresh_maths_text(void)
  1. Executesnprintf(maths_lines[0], sizeof(maths_lines[0]), "z=z*z+c MID %u,%u", (unsigned)(LCD_WIDTH / 2), (unsigned)(LCD_HEIGHT / 2));
  2. Executesnprintf(maths_lines[1], sizeof(maths_lines[1]), "VR %+.7f", maths_view.centre_real);
  3. Executesnprintf(maths_lines[2], sizeof(maths_lines[2]), "VI %+.7f", maths_view.centre_imaginary);
  4. Executesnprintf(maths_lines[3], sizeof(maths_lines[3]), "W %.5g", maths_view.width);
  5. Executesnprintf(maths_lines[4], sizeof(maths_lines[4]), "ZOOM %.4gx", views[0].width / maths_view.width);
  6. Executesnprintf(maths_lines[5], sizeof(maths_lines[5]), "CR %+.7f", maths_c_real);
  7. Executesnprintf(maths_lines[6], sizeof(maths_lines[6]), "CI %+.7f", maths_c_imaginary);
  8. Executesnprintf(maths_lines[7], sizeof(maths_lines[7]), "ZR %+.6f", maths_z_real);
  9. Executesnprintf(maths_lines[8], sizeof(maths_lines[8]), "ZI %+.6f", maths_z_imaginary);
  10. Executesnprintf(maths_lines[9], sizeof(maths_lines[9]), "N %u/%u %s", (unsigned)maths_iteration, (unsigned)maths_limit, maths_finished ? (maths_magnitude_squared > 4.0 ? "ESC" : "LIMIT") : "");
  11. Executesnprintf(maths_lines[10], sizeof(maths_lines[10]), "|z|2 %.6g", maths_magnitude_squared);
Read original function source
static void refresh_maths_text(void)
{
    snprintf(maths_lines[0], sizeof(maths_lines[0]), "z=z*z+c  MID %u,%u",
             (unsigned)(LCD_WIDTH / 2), (unsigned)(LCD_HEIGHT / 2));
    snprintf(maths_lines[1], sizeof(maths_lines[1]), "VR %+.7f", maths_view.centre_real);
    snprintf(maths_lines[2], sizeof(maths_lines[2]), "VI %+.7f", maths_view.centre_imaginary);
    snprintf(maths_lines[3], sizeof(maths_lines[3]), "W %.5g", maths_view.width);
    snprintf(maths_lines[4], sizeof(maths_lines[4]), "ZOOM %.4gx",
             views[0].width / maths_view.width);
    snprintf(maths_lines[5], sizeof(maths_lines[5]), "CR %+.7f", maths_c_real);
    snprintf(maths_lines[6], sizeof(maths_lines[6]), "CI %+.7f", maths_c_imaginary);
    snprintf(maths_lines[7], sizeof(maths_lines[7]), "ZR %+.6f", maths_z_real);
    snprintf(maths_lines[8], sizeof(maths_lines[8]), "ZI %+.6f", maths_z_imaginary);
    snprintf(maths_lines[9], sizeof(maths_lines[9]), "N %u/%u %s", (unsigned)maths_iteration,
             (unsigned)maths_limit, maths_finished ?
             (maths_magnitude_squared > 4.0 ? "ESC" : "LIMIT") : "");
    snprintf(maths_lines[10], sizeof(maths_lines[10]), "|z|2 %.6g",
             maths_magnitude_squared);
}
set_maths_sample() · source line 142
static void set_maths_sample(mandelbrot_view_t view, double real, double imaginary,
                             bool automatic)

Calls into this documentation:

  1. Executemaths_view = view;
  2. Executemaths_c_real = real;
  3. Executemaths_c_imaginary = imaginary;
  4. Executemaths_z_real = maths_z_imaginary = 0.0;
  5. Executemaths_magnitude_squared = 0.0;
  6. Executemaths_iteration = 0U;
  7. Executemaths_limit = automatic ? MANDELBROT_AUTO_MAX_ITERATIONS : MAX_ITERATIONS;
  8. Executemaths_auto = automatic;
  9. Executemaths_finished = false;
  10. Executemaths_valid = true;
  11. Executerefresh_maths_text();
Read original function source
static void set_maths_sample(mandelbrot_view_t view, double real, double imaginary,
                             bool automatic)
{
    maths_view = view;
    maths_c_real = real;
    maths_c_imaginary = imaginary;
    maths_z_real = maths_z_imaginary = 0.0;
    maths_magnitude_squared = 0.0;
    maths_iteration = 0U;
    maths_limit = automatic ? MANDELBROT_AUTO_MAX_ITERATIONS : MAX_ITERATIONS;
    maths_auto = automatic;
    maths_finished = false;
    maths_valid = true;
    refresh_maths_text();
}
set_manual_maths_sample() · source line 158
static void set_manual_maths_sample(void)

Calls into this documentation:

  1. Executeconst double height = view_width * (double)LCD_HEIGHT / (double)LCD_WIDTH;
  2. Executeconst double real = centre_real - view_width * 0.5 + view_width * (double)(LCD_WIDTH / 2) / (double)(LCD_WIDTH - 1U);
  3. Executeconst double imaginary = centre_imaginary - height * 0.5 + height * (double)(LCD_HEIGHT / 2) / (double)(LCD_HEIGHT - 1U);
  4. Executeset_maths_sample((mandelbrot_view_t){views[selected_view].name, centre_real, centre_imaginary, view_width}, real, imaginary, false);
Read original function source
static void set_manual_maths_sample(void)
{
    const double height = view_width * (double)LCD_HEIGHT / (double)LCD_WIDTH;
    const double real = centre_real - view_width * 0.5 +
                        view_width * (double)(LCD_WIDTH / 2) / (double)(LCD_WIDTH - 1U);
    const double imaginary = centre_imaginary - height * 0.5 +
                             height * (double)(LCD_HEIGHT / 2) / (double)(LCD_HEIGHT - 1U);
    set_maths_sample((mandelbrot_view_t){views[selected_view].name, centre_real,
                                        centre_imaginary, view_width}, real, imaginary, false);
}
update_maths_sample() · source line 169
static bool update_maths_sample(uint32_t now_ms)

Calls into this documentation:

  1. Decision!maths_valid || now_ms - maths_last_update < MANDELBROT_MATHS_INTERVAL_MS
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Executemaths_last_update = now_ms;
  3. Decisionmaths_finished
    True
      1. Decisionnow_ms - maths_finished_at < MANDELBROT_MATHS_HOLD_MS
        True
        1. Exit this functionreturn false;
        False

        Continue after this decision.

      2. Executemaths_z_real = maths_z_imaginary = 0.0;
      3. Executemaths_magnitude_squared = 0.0;
      4. Executemaths_iteration = 0U;
      5. Executemaths_finished = false;
    False
      1. Decisionmaths_auto
        True
          1. Executeconst float real = (float)maths_z_real, imaginary = (float)maths_z_imaginary;
          2. Executemaths_z_imaginary = 2.0f * real * imaginary + (float)maths_c_imaginary;
          3. Executemaths_z_real = real * real - imaginary * imaginary + (float)maths_c_real;
        False
          1. Executeconst double real = maths_z_real, imaginary = maths_z_imaginary;
          2. Executemaths_z_imaginary = 2.0 * real * imaginary + maths_c_imaginary;
          3. Executemaths_z_real = real * real - imaginary * imaginary + maths_c_real;
      2. Execute++maths_iteration;
      3. Decisionmaths_auto
        True
          1. Executeconst float real = (float)maths_z_real, imaginary = (float)maths_z_imaginary;
          2. Executemaths_magnitude_squared = real * real + imaginary * imaginary;
        False
          1. Executemaths_magnitude_squared = maths_z_real * maths_z_real + maths_z_imaginary * maths_z_imaginary;
      4. Executemaths_finished = maths_magnitude_squared > 4.0 || maths_iteration >= maths_limit;
      5. Decisionmaths_finished
        True
        1. Executemaths_finished_at = now_ms;
        False

        Continue after this decision.

  4. Executerefresh_maths_text();
  5. Exit this functionreturn true;
Read original function source
static bool update_maths_sample(uint32_t now_ms)
{
    if (!maths_valid || now_ms - maths_last_update < MANDELBROT_MATHS_INTERVAL_MS) return false;
    maths_last_update = now_ms;
    if (maths_finished) {
        if (now_ms - maths_finished_at < MANDELBROT_MATHS_HOLD_MS) return false;
        maths_z_real = maths_z_imaginary = 0.0;
        maths_magnitude_squared = 0.0;
        maths_iteration = 0U;
        maths_finished = false;
    } else {
        // Match AUTO's float arithmetic and manual rendering's double arithmetic.
        // Keep old real/imaginary values until both components have been computed.
        if (maths_auto) {
            const float real = (float)maths_z_real, imaginary = (float)maths_z_imaginary;
            maths_z_imaginary = 2.0f * real * imaginary + (float)maths_c_imaginary;
            maths_z_real = real * real - imaginary * imaginary + (float)maths_c_real;
        } else {
            const double real = maths_z_real, imaginary = maths_z_imaginary;
            maths_z_imaginary = 2.0 * real * imaginary + maths_c_imaginary;
            maths_z_real = real * real - imaginary * imaginary + maths_c_real;
        }
        ++maths_iteration;
        if (maths_auto) {
            const float real = (float)maths_z_real, imaginary = (float)maths_z_imaginary;
            maths_magnitude_squared = real * real + imaginary * imaginary;
        } else {
            maths_magnitude_squared = maths_z_real * maths_z_real + maths_z_imaginary * maths_z_imaginary;
        }
        maths_finished = maths_magnitude_squared > 4.0 || maths_iteration >= maths_limit;
        if (maths_finished) maths_finished_at = now_ms;
    }
    refresh_maths_text();
    return true;
}
draw_maths_panel() · source line 205
static void draw_maths_panel(uint16_t *frame)

Calls into this documentation:

  1. Executeconst int x = LCD_WIDTH - MANDELBROT_MATHS_WIDTH - 2;
  2. Executefill_rect(frame, x, 2, MANDELBROT_MATHS_WIDTH, MANDELBROT_MATHS_HEIGHT, COLOR_BLACK);
  3. Executedraw_rect(frame, x, 2, MANDELBROT_MATHS_WIDTH, MANDELBROT_MATHS_HEIGHT, COLOR_CYAN);
  4. Decision!maths_valid
    True
      1. Executedraw_text(frame, x + 4, 7, "WAITING FOR FRAME", 1, COLOR_WHITE);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. FOR loopunsigned line = 0U; line < MANDELBROT_MATHS_LINES; ++line

    Repeat body while the loop condition permits; then continue below.

      1. Executedraw_text(frame, x + 4, 6 + (int)line * 9, maths_lines[line], 1, line == 0U ? COLOR_CYAN : COLOR_WHITE);
Read original function source
static void draw_maths_panel(uint16_t *frame)
{
    const int x = LCD_WIDTH - MANDELBROT_MATHS_WIDTH - 2;
    fill_rect(frame, x, 2, MANDELBROT_MATHS_WIDTH, MANDELBROT_MATHS_HEIGHT, COLOR_BLACK);
    draw_rect(frame, x, 2, MANDELBROT_MATHS_WIDTH, MANDELBROT_MATHS_HEIGHT, COLOR_CYAN);
    if (!maths_valid) {
        draw_text(frame, x + 4, 7, "WAITING FOR FRAME", 1, COLOR_WHITE);
        return;
    }
    for (unsigned line = 0U; line < MANDELBROT_MATHS_LINES; ++line) {
        draw_text(frame, x + 4, 6 + (int)line * 9, maths_lines[line], 1,
                  line == 0U ? COLOR_CYAN : COLOR_WHITE);
    }
}
colour_wheel() · source line 221
static uint16_t colour_wheel(uint8_t position)
  1. Executeposition = 255U - position;
  2. Decisionposition < 85U
    True
    1. Exit this functionreturn RGB565(255U - position * 3U, 0U, position * 3U);
    False

    Continue after this decision.

  3. Decisionposition < 170U
    True
      1. Executeposition -= 85U;
      2. Exit this functionreturn RGB565(0U, position * 3U, 255U - position * 3U);
    False

    Continue after this decision.

  4. Executeposition -= 170U;
  5. Exit this functionreturn RGB565(position * 3U, 255U - position * 3U, 0U);
Read original function source
static uint16_t colour_wheel(uint8_t position)
{
    position = 255U - position;
    if (position < 85U) return RGB565(255U - position * 3U, 0U, position * 3U);
    if (position < 170U) {
        position -= 85U;
        return RGB565(0U, position * 3U, 255U - position * 3U);
    }
    position -= 170U;
    return RGB565(position * 3U, 255U - position * 3U, 0U);
}
iteration_colour() · source line 233
static uint16_t iteration_colour(uint16_t iteration)

Calls into this documentation:

  1. Decisioniteration >= MAX_ITERATIONS
    True
    1. Exit this functionreturn COLOR_BLACK;
    False

    Continue after this decision.

  2. Executeconst uint8_t intensity = (uint8_t)((iteration * 255U) / MAX_ITERATIONS);
  3. Exit this functionreturn colour_wheel((uint8_t)(intensity + 32U));
Read original function source
static uint16_t iteration_colour(uint16_t iteration)
{
    if (iteration >= MAX_ITERATIONS) return COLOR_BLACK;
    const uint8_t intensity = (uint8_t)((iteration * 255U) / MAX_ITERATIONS);
    return colour_wheel((uint8_t)(intensity + 32U));
}
calculate_pixel() · source line 240
static uint16_t calculate_pixel(double real, double imaginary, uint16_t *calculation_count)

Calls into this documentation:

  1. Executedouble z_real = 0.0, z_imaginary = 0.0;
  2. Executedouble z_real_squared = 0.0, z_imaginary_squared = 0.0;
  3. Executeuint16_t iteration = 0U;
  4. WHILE loopz_real_squared + z_imaginary_squared <= 4.0 && iteration < MAX_ITERATIONS

    Repeat body while the loop condition permits; then continue below.

      1. Executez_imaginary = 2.0 * z_real * z_imaginary + imaginary;
      2. Executez_real = z_real_squared - z_imaginary_squared + real;
      3. Executez_real_squared = z_real * z_real;
      4. Executez_imaginary_squared = z_imaginary * z_imaginary;
      5. Execute++iteration;
  5. Decisioncalculation_count != NULL
    True
    1. Execute*calculation_count = iteration;
    False

    Continue after this decision.

  6. Exit this functionreturn iteration_colour(iteration);
Read original function source
static uint16_t calculate_pixel(double real, double imaginary, uint16_t *calculation_count)
{
    double z_real = 0.0, z_imaginary = 0.0;
    double z_real_squared = 0.0, z_imaginary_squared = 0.0;
    uint16_t iteration = 0U;
    while (z_real_squared + z_imaginary_squared <= 4.0 && iteration < MAX_ITERATIONS) {
        z_imaginary = 2.0 * z_real * z_imaginary + imaginary;
        z_real = z_real_squared - z_imaginary_squared + real;
        z_real_squared = z_real * z_real;
        z_imaginary_squared = z_imaginary * z_imaginary;
        ++iteration;
    }
    if (calculation_count != NULL) *calculation_count = iteration;
    return iteration_colour(iteration);
}
calculate_iteration_index() · source line 256
static uint8_t calculate_iteration_index(float real, float imaginary)
  1. Executefloat z_real = 0.0f, z_imaginary = 0.0f;
  2. Executefloat z_real_squared = 0.0f, z_imaginary_squared = 0.0f;
  3. Executeuint16_t iteration = 0U;
  4. WHILE loopz_real_squared + z_imaginary_squared <= 4.0f && iteration < MANDELBROT_AUTO_MAX_ITERATIONS

    Repeat body while the loop condition permits; then continue below.

      1. Executez_imaginary = 2.0f * z_real * z_imaginary + imaginary;
      2. Executez_real = z_real_squared - z_imaginary_squared + real;
      3. Executez_real_squared = z_real * z_real;
      4. Executez_imaginary_squared = z_imaginary * z_imaginary;
      5. Execute++iteration;
  5. Decisioniteration >= MANDELBROT_AUTO_MAX_ITERATIONS
    True
    1. Exit this functionreturn 0U;
    False

    Continue after this decision.

  6. Exit this functionreturn (uint8_t)(1U + ((iteration - 1U) * 254U) / (MANDELBROT_AUTO_MAX_ITERATIONS - 1U));
Read original function source
static uint8_t calculate_iteration_index(float real, float imaginary)
{
    float z_real = 0.0f, z_imaginary = 0.0f;
    float z_real_squared = 0.0f, z_imaginary_squared = 0.0f;
    uint16_t iteration = 0U;
    while (z_real_squared + z_imaginary_squared <= 4.0f &&
           iteration < MANDELBROT_AUTO_MAX_ITERATIONS) {
        z_imaginary = 2.0f * z_real * z_imaginary + imaginary;
        z_real = z_real_squared - z_imaginary_squared + real;
        z_real_squared = z_real * z_real;
        z_imaginary_squared = z_imaginary * z_imaginary;
        ++iteration;
    }
    if (iteration >= MANDELBROT_AUTO_MAX_ITERATIONS) return 0U;
    return (uint8_t)(1U + ((iteration - 1U) * 254U) /
                     (MANDELBROT_AUTO_MAX_ITERATIONS - 1U));
}
build_auto_palette() · source line 274
static void build_auto_palette(void)

Calls into this documentation:

  1. Executeauto_palette[0] = COLOR_BLACK;
  2. FOR loopunsigned index = 1U; index < 256U; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executeauto_palette[index] = colour_wheel((uint8_t)(index + 32U));
Read original function source
static void build_auto_palette(void)
{
    auto_palette[0] = COLOR_BLACK;
    for (unsigned index = 1U; index < 256U; ++index) {
        auto_palette[index] = colour_wheel((uint8_t)(index + 32U));
    }
}
automaticZoomActive() · source line 282
static bool automaticZoomActive(void)
  1. Executebool active;
  2. ExecuteportENTER_CRITICAL(&auto_frame_lock);
  3. Executeactive = automatic_zoom;
  4. ExecuteportEXIT_CRITICAL(&auto_frame_lock);
  5. Exit this functionreturn active;
Read original function source
static bool automaticZoomActive(void)
{
    bool active;
    portENTER_CRITICAL(&auto_frame_lock);
    active = automatic_zoom;
    portEXIT_CRITICAL(&auto_frame_lock);
    return active;
}
begin_render() · source line 291
static void begin_render(void)

Calls into this documentation:

  1. Executenext_row = 0U;
  2. Executerow_count = 0U;
  3. Executemanual_iteration_count = 0U;
  4. Executerendering = true;
  5. Executeclear_requested = true;
  6. Executeframe_ready = true;
  7. Executecursor_drawn = false;
  8. Executemanual_reveal_pending = false;
  9. Preprocessor directive#if 0
  10. Executeset_manual_maths_sample();
  11. Preprocessor directive#endif
Read original function source
static void begin_render(void)
{
    next_row = 0U;
    row_count = 0U;
    manual_iteration_count = 0U;
    rendering = true;
    clear_requested = true;
    frame_ready = true;
    cursor_drawn = false;
    manual_reveal_pending = false;
#if 0 // Maths panel experiment: disabled; see the retained implementation above.
    set_manual_maths_sample();
#endif
}
select_view() · source line 306
static void select_view(uint8_t view_index)
  1. Executeselected_view = view_index;
  2. Executecentre_real = views[selected_view].centre_real;
  3. Executecentre_imaginary = views[selected_view].centre_imaginary;
  4. Executeview_width = views[selected_view].width;
  5. Executezoom_level = 0U;
  6. Executecursor_x = (LCD_WIDTH - ZOOM_BOX_WIDTH) / 2;
  7. Executecursor_y = (LCD_HEIGHT - ZOOM_BOX_HEIGHT) / 2;
Read original function source
static void select_view(uint8_t view_index)
{
    selected_view = view_index;
    centre_real = views[selected_view].centre_real;
    centre_imaginary = views[selected_view].centre_imaginary;
    view_width = views[selected_view].width;
    zoom_level = 0U;
    cursor_x = (LCD_WIDTH - ZOOM_BOX_WIDTH) / 2;
    cursor_y = (LCD_HEIGHT - ZOOM_BOX_HEIGHT) / 2;
}
render_row() · source line 317
static uint32_t render_row(uint16_t y, uint16_t *destination)

Calls into this documentation:

  1. Executeconst double height = view_width * (double)LCD_HEIGHT / (double)LCD_WIDTH;
  2. Executeconst double minimum_real = centre_real - view_width * 0.5;
  3. Executeconst double minimum_imaginary = centre_imaginary - height * 0.5;
  4. Executeconst double imaginary = minimum_imaginary + height * (double)y / (double)(LCD_HEIGHT - 1U);
  5. Executeuint32_t row_iterations = 0U;
  6. FOR loopuint16_t x = 0U; x < LCD_WIDTH; ++x

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst double real = minimum_real + view_width * (double)x / (double)(LCD_WIDTH - 1U);
      2. Executeuint16_t pixel_iterations;
      3. Executedestination[x] = calculate_pixel(real, imaginary, &pixel_iterations);
      4. Executerow_iterations += pixel_iterations;
  7. Exit this functionreturn row_iterations;
Read original function source
static uint32_t render_row(uint16_t y, uint16_t *destination)
{
    const double height = view_width * (double)LCD_HEIGHT / (double)LCD_WIDTH;
    const double minimum_real = centre_real - view_width * 0.5;
    const double minimum_imaginary = centre_imaginary - height * 0.5;
    const double imaginary = minimum_imaginary + height * (double)y / (double)(LCD_HEIGHT - 1U);
    uint32_t row_iterations = 0U;
    for (uint16_t x = 0U; x < LCD_WIDTH; ++x) {
        const double real = minimum_real + view_width * (double)x / (double)(LCD_WIDTH - 1U);
        uint16_t pixel_iterations;
        destination[x] = calculate_pixel(real, imaginary, &pixel_iterations);
        row_iterations += pixel_iterations;
    }
    return row_iterations;
}
screen_pixel_colour() · source line 333
static uint16_t screen_pixel_colour(int x, int y)

Calls into this documentation:

  1. Executeconst double height = view_width * (double)LCD_HEIGHT / (double)LCD_WIDTH;
  2. Executeconst double minimum_real = centre_real - view_width * 0.5;
  3. Executeconst double minimum_imaginary = centre_imaginary - height * 0.5;
  4. Executeconst double real = minimum_real + view_width * (double)x / (double)(LCD_WIDTH - 1U);
  5. Executeconst double imaginary = minimum_imaginary + height * (double)y / (double)(LCD_HEIGHT - 1U);
  6. Exit this functionreturn calculate_pixel(real, imaginary, NULL);
Read original function source
static uint16_t screen_pixel_colour(int x, int y)
{
    const double height = view_width * (double)LCD_HEIGHT / (double)LCD_WIDTH;
    const double minimum_real = centre_real - view_width * 0.5;
    const double minimum_imaginary = centre_imaginary - height * 0.5;
    const double real = minimum_real + view_width * (double)x / (double)(LCD_WIDTH - 1U);
    const double imaginary = minimum_imaginary + height * (double)y / (double)(LCD_HEIGHT - 1U);
    return calculate_pixel(real, imaginary, NULL);
}
restore_cursor_border() · source line 343
static void restore_cursor_border(uint16_t *frame, int x, int y)

Calls into this documentation:

  1. FOR loopint column = x; column < x + ZOOM_BOX_WIDTH; ++column

    Repeat body while the loop condition permits; then continue below.

      1. Executeframe[y * LCD_WIDTH + column] = screen_pixel_colour(column, y);
      2. Executeframe[(y + ZOOM_BOX_HEIGHT - 1) * LCD_WIDTH + column] = screen_pixel_colour(column, y + ZOOM_BOX_HEIGHT - 1);
  2. FOR loopint row = y + 1; row < y + ZOOM_BOX_HEIGHT - 1; ++row

    Repeat body while the loop condition permits; then continue below.

      1. Executeframe[row * LCD_WIDTH + x] = screen_pixel_colour(x, row);
      2. Executeframe[row * LCD_WIDTH + x + ZOOM_BOX_WIDTH - 1] = screen_pixel_colour(x + ZOOM_BOX_WIDTH - 1, row);
Read original function source
static void restore_cursor_border(uint16_t *frame, int x, int y)
{
    for (int column = x; column < x + ZOOM_BOX_WIDTH; ++column) {
        frame[y * LCD_WIDTH + column] = screen_pixel_colour(column, y);
        frame[(y + ZOOM_BOX_HEIGHT - 1) * LCD_WIDTH + column] =
            screen_pixel_colour(column, y + ZOOM_BOX_HEIGHT - 1);
    }
    for (int row = y + 1; row < y + ZOOM_BOX_HEIGHT - 1; ++row) {
        frame[row * LCD_WIDTH + x] = screen_pixel_colour(x, row);
        frame[row * LCD_WIDTH + x + ZOOM_BOX_WIDTH - 1] =
            screen_pixel_colour(x + ZOOM_BOX_WIDTH - 1, row);
    }
}
allocate_manual_staging_frame() · source line 357
static bool allocate_manual_staging_frame(void)
  1. Decisionmanual_staging_frame != NULL
    True
    1. Exit this functionreturn true;
    False

    Continue after this decision.

  2. Executemanual_staging_frame = heap_caps_malloc(MANDELBROT_MANUAL_FRAME_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
  3. Decisionmanual_staging_frame == NULL
    True
      1. Executeauto_status_text = "MANUAL PSRAM ALLOCATION FAILED";
      2. ExecuteESP_LOGE(TAG, "Manual render needs %u PSRAM bytes", (unsigned)MANDELBROT_MANUAL_FRAME_BYTES);
      3. Exit this functionreturn false;
    False

    Continue after this decision.

  4. ExecuteESP_LOGI(TAG, "Manual render staging frame: %u PSRAM bytes", (unsigned)MANDELBROT_MANUAL_FRAME_BYTES);
  5. Exit this functionreturn true;
Read original function source
static bool allocate_manual_staging_frame(void)
{
    if (manual_staging_frame != NULL) return true;
    manual_staging_frame = heap_caps_malloc(MANDELBROT_MANUAL_FRAME_BYTES,
                                            MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    if (manual_staging_frame == NULL) {
        auto_status_text = "MANUAL PSRAM ALLOCATION FAILED";
        ESP_LOGE(TAG, "Manual render needs %u PSRAM bytes",
                 (unsigned)MANDELBROT_MANUAL_FRAME_BYTES);
        return false;
    }
    ESP_LOGI(TAG, "Manual render staging frame: %u PSRAM bytes",
             (unsigned)MANDELBROT_MANUAL_FRAME_BYTES);
    return true;
}
release_manual_staging_frame() · source line 373
static void release_manual_staging_frame(void)
  1. Decisionmanual_staging_frame != NULL
    True
      1. Executeheap_caps_free(manual_staging_frame);
      2. Executemanual_staging_frame = NULL;
    False

    Continue after this decision.

Read original function source
static void release_manual_staging_frame(void)
{
    if (manual_staging_frame != NULL) {
        heap_caps_free(manual_staging_frame);
        manual_staging_frame = NULL;
    }
}
allocate_auto_buffers() · source line 381
static bool allocate_auto_buffers(void)
  1. Decisionauto_buffers_ready
    True
    1. Exit this functionreturn true;
    False

    Continue after this decision.

  2. FOR loopunsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executeauto_frames[index].pixels = heap_caps_malloc((size_t)LCD_WIDTH * LCD_HEIGHT, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
      2. Decisionauto_frames[index].pixels == NULL
        True
          1. Executeauto_status_text = "AUTO PSRAM ALLOCATION FAILED";
          2. ExecuteESP_LOGE(TAG, "Automatic zoom needs %u PSRAM bytes per frame", (unsigned)(LCD_WIDTH * LCD_HEIGHT));
          3. Exit this functionreturn false;
        False

        Continue after this decision.

      3. Executeauto_frames[index].state = MANDELBROT_FRAME_FREE;
  3. Executeauto_buffers_ready = true;
  4. ExecuteESP_LOGI(TAG, "Automatic zoom PSRAM queue: %u frames, %u bytes", MANDELBROT_AUTO_FRAME_COUNT, (unsigned)(MANDELBROT_AUTO_FRAME_COUNT * LCD_WIDTH * LCD_HEIGHT));
  5. Exit this functionreturn true;
Read original function source
static bool allocate_auto_buffers(void)
{
    if (auto_buffers_ready) return true;
    for (unsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index) {
        auto_frames[index].pixels = heap_caps_malloc((size_t)LCD_WIDTH * LCD_HEIGHT,
                                                      MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
        if (auto_frames[index].pixels == NULL) {
            auto_status_text = "AUTO PSRAM ALLOCATION FAILED";
            ESP_LOGE(TAG, "Automatic zoom needs %u PSRAM bytes per frame",
                     (unsigned)(LCD_WIDTH * LCD_HEIGHT));
            return false;
        }
        auto_frames[index].state = MANDELBROT_FRAME_FREE;
    }
    auto_buffers_ready = true;
    ESP_LOGI(TAG, "Automatic zoom PSRAM queue: %u frames, %u bytes",
             MANDELBROT_AUTO_FRAME_COUNT,
             (unsigned)(MANDELBROT_AUTO_FRAME_COUNT * LCD_WIDTH * LCD_HEIGHT));
    return true;
}
reset_auto_queue() · source line 402
static void reset_auto_queue(void)
  1. ExecuteportENTER_CRITICAL(&auto_frame_lock);
  2. Execute++auto_generation;
  3. Executeauto_next_sequence = 0U;
  4. FOR loopunsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Decisionauto_frames[index].state != MANDELBROT_FRAME_RENDERING
        True
          1. Executeauto_frames[index].state = MANDELBROT_FRAME_FREE;
        False

        Continue after this decision.

  5. ExecuteportEXIT_CRITICAL(&auto_frame_lock);
Read original function source
static void reset_auto_queue(void)
{
    portENTER_CRITICAL(&auto_frame_lock);
    ++auto_generation;
    auto_next_sequence = 0U;
    for (unsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index) {
        if (auto_frames[index].state != MANDELBROT_FRAME_RENDERING) {
            auto_frames[index].state = MANDELBROT_FRAME_FREE;
        }
    }
    portEXIT_CRITICAL(&auto_frame_lock);
}
take_free_auto_frame() · source line 415
static int take_free_auto_frame(uint32_t *sequence, uint32_t *generation)
  1. Executeint slot = -1;
  2. ExecuteportENTER_CRITICAL(&auto_frame_lock);
  3. Decisionautomatic_zoom
    True
      1. FOR loopunsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index

        Repeat body while the loop condition permits; then continue below.

          1. Decisionauto_frames[index].state != MANDELBROT_FRAME_FREE
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          2. Executeauto_frames[index].state = MANDELBROT_FRAME_RENDERING;
          3. Executeauto_frames[index].sequence = auto_next_sequence++;
          4. Executeauto_frames[index].generation = auto_generation;
          5. Execute*sequence = auto_frames[index].sequence;
          6. Execute*generation = auto_frames[index].generation;
          7. Executeslot = (int)index;
          8. Exit enclosing loop or switchbreak;
    False

    Continue after this decision.

  4. ExecuteportEXIT_CRITICAL(&auto_frame_lock);
  5. Exit this functionreturn slot;
Read original function source
static int take_free_auto_frame(uint32_t *sequence, uint32_t *generation)
{
    int slot = -1;
    portENTER_CRITICAL(&auto_frame_lock);
    if (automatic_zoom) {
        for (unsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index) {
            if (auto_frames[index].state != MANDELBROT_FRAME_FREE) continue;
            auto_frames[index].state = MANDELBROT_FRAME_RENDERING;
            auto_frames[index].sequence = auto_next_sequence++;
            auto_frames[index].generation = auto_generation;
            *sequence = auto_frames[index].sequence;
            *generation = auto_frames[index].generation;
            slot = (int)index;
            break;
        }
    }
    portEXIT_CRITICAL(&auto_frame_lock);
    return slot;
}
render_auto_frame() · source line 435
static void render_auto_frame(mandelbrot_frame_t *frame, uint32_t sequence)

Calls into this documentation:

  1. Executeuint8_t *destination = frame->pixels;
  2. Executeconst float width = MANDELBROT_AUTO_START_WIDTH * powf(MANDELBROT_AUTO_ZOOM_PER_FRAME, (float)sequence);
  3. Executeconst float height = width * (float)LCD_HEIGHT / (float)LCD_WIDTH;
  4. Executeconst float progress = 1.0f - width / MANDELBROT_AUTO_START_WIDTH;
  5. Executeconst float centre_real = MANDELBROT_AUTO_START_REAL + (MANDELBROT_AUTO_TARGET_REAL - MANDELBROT_AUTO_START_REAL) * progress;
  6. Executeconst float centre_imaginary = MANDELBROT_AUTO_START_IMAGINARY + (MANDELBROT_AUTO_TARGET_IMAGINARY - MANDELBROT_AUTO_START_IMAGINARY) * progress;
  7. Executeconst float minimum_real = centre_real - width * 0.5f;
  8. Executeconst float minimum_imaginary = centre_imaginary - height * 0.5f;
  9. Executeconst float real_step = width / (float)(LCD_WIDTH - 1U);
  10. Executeconst float imaginary_step = height / (float)(LCD_HEIGHT - 1U);
  11. Preprocessor directive#if 0
  12. Executeframe->view = (mandelbrot_view_t){"AUTO", centre_real, centre_imaginary, width};
  13. Executefloat sample_real = minimum_real;
  14. FOR loopunsigned x = 0U; x < LCD_WIDTH / 2U; ++x

    Repeat body while the loop condition permits; then continue below.

    1. Executesample_real += real_step;
  15. Executeframe->sample_real = sample_real;
  16. Executeframe->sample_imaginary = minimum_imaginary + (float)(LCD_HEIGHT / 2U) * imaginary_step;
  17. Preprocessor directive#endif
  18. FOR loopunsigned y = 0U; y < LCD_HEIGHT; ++y

    Repeat body while the loop condition permits; then continue below.

      1. Executefloat real = minimum_real;
      2. Executeconst float imaginary = minimum_imaginary + (float)y * imaginary_step;
      3. FOR loopunsigned x = 0U; x < LCD_WIDTH; ++x

        Repeat body while the loop condition permits; then continue below.

          1. Executedestination[(size_t)y * LCD_WIDTH + x] = calculate_iteration_index(real, imaginary);
          2. Executereal += real_step;
      4. Decision(y + 1U) % MANDELBROT_AUTO_ROWS_PER_YIELD == 0U
        True
          1. ExecutevTaskDelay(pdMS_TO_TICKS(1U));
        False

        Continue after this decision.

Read original function source
static void render_auto_frame(mandelbrot_frame_t *frame, uint32_t sequence)
{
    uint8_t *destination = frame->pixels;
    const float width = MANDELBROT_AUTO_START_WIDTH *
                        powf(MANDELBROT_AUTO_ZOOM_PER_FRAME, (float)sequence);
    const float height = width * (float)LCD_HEIGHT / (float)LCD_WIDTH;
    // Move the view centre toward the target at the same exponential rate as width.
    // Frame zero is the recognizable full view; later frames approach the target.
    const float progress = 1.0f - width / MANDELBROT_AUTO_START_WIDTH;
    const float centre_real = MANDELBROT_AUTO_START_REAL +
                              (MANDELBROT_AUTO_TARGET_REAL - MANDELBROT_AUTO_START_REAL) * progress;
    const float centre_imaginary = MANDELBROT_AUTO_START_IMAGINARY +
                                   (MANDELBROT_AUTO_TARGET_IMAGINARY -
                                    MANDELBROT_AUTO_START_IMAGINARY) * progress;
    const float minimum_real = centre_real - width * 0.5f;
    const float minimum_imaginary = centre_imaginary - height * 0.5f;
    const float real_step = width / (float)(LCD_WIDTH - 1U);
    const float imaginary_step = height / (float)(LCD_HEIGHT - 1U);
#if 0 // Maths panel experiment: disabled.
    frame->view = (mandelbrot_view_t){"AUTO", centre_real, centre_imaginary, width};
    // Reproduce the renderer's accumulated X steps at the actual centre pixel.
    float sample_real = minimum_real;
    for (unsigned x = 0U; x < LCD_WIDTH / 2U; ++x) sample_real += real_step;
    frame->sample_real = sample_real;
    frame->sample_imaginary = minimum_imaginary + (float)(LCD_HEIGHT / 2U) * imaginary_step;
#endif
    for (unsigned y = 0U; y < LCD_HEIGHT; ++y) {
        float real = minimum_real;
        const float imaginary = minimum_imaginary + (float)y * imaginary_step;
        for (unsigned x = 0U; x < LCD_WIDTH; ++x) {
            destination[(size_t)y * LCD_WIDTH + x] = calculate_iteration_index(real, imaginary);
            real += real_step;
        }
        // This task owns CPU 1 while rendering; yield regularly so IDLE1 can reset
        // the task watchdog without exposing a partially rendered frame to display.
        if ((y + 1U) % MANDELBROT_AUTO_ROWS_PER_YIELD == 0U) {
            vTaskDelay(pdMS_TO_TICKS(1U));
        }
    }
}
automatic_renderer_task() · source line 476
static void automatic_renderer_task(void *unused)

Calls into this documentation:

  1. Execute(void)unused;
  2. FOR loop;;

    Repeat body while the loop condition permits; then continue below.

      1. Decision!automaticZoomActive()
        True
          1. ExecuteulTaskNotifyTake(pdTRUE, portMAX_DELAY);
          2. Next loop iterationcontinue;
        False

        Continue after this decision.

      2. Executeuint32_t sequence = 0U;
      3. Executeuint32_t generation = 0U;
      4. Executeconst int slot = take_free_auto_frame(&sequence, &generation);
      5. Decisionslot < 0
        True
          1. ExecutevTaskDelay(pdMS_TO_TICKS(1U));
          2. Next loop iterationcontinue;
        False

        Continue after this decision.

      6. Executeconst uint32_t render_started = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
      7. Executerender_auto_frame(&auto_frames[slot], sequence);
      8. Executeconst uint32_t render_finished = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
      9. ExecuteportENTER_CRITICAL(&auto_frame_lock);
      10. Decisionautomatic_zoom && auto_frames[slot].generation == generation && auto_generation == generation
        True
          1. Executeauto_frames[slot].state = MANDELBROT_FRAME_READY;
          2. Executeauto_last_render_ms = render_finished - render_started;
          3. Execute++auto_rendered_frames;
        False
          1. Executeauto_frames[slot].state = MANDELBROT_FRAME_FREE;
      11. ExecuteportEXIT_CRITICAL(&auto_frame_lock);
Read original function source
static void automatic_renderer_task(void *unused)
{
    (void)unused;
    for (;;) {
        if (!automaticZoomActive()) {
            // B and Back leave the renderer dormant with no periodic CPU work.
            ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
            continue;
        }
        uint32_t sequence = 0U;
        uint32_t generation = 0U;
        const int slot = take_free_auto_frame(&sequence, &generation);
        if (slot < 0) {
            vTaskDelay(pdMS_TO_TICKS(1U));
            continue;
        }
        const uint32_t render_started = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
        render_auto_frame(&auto_frames[slot], sequence);
        const uint32_t render_finished = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
        portENTER_CRITICAL(&auto_frame_lock);
        if (automatic_zoom && auto_frames[slot].generation == generation &&
            auto_generation == generation) {
            auto_frames[slot].state = MANDELBROT_FRAME_READY;
            auto_last_render_ms = render_finished - render_started;
            ++auto_rendered_frames;
        } else {
            auto_frames[slot].state = MANDELBROT_FRAME_FREE;
        }
        portEXIT_CRITICAL(&auto_frame_lock);
    }
}
start_automatic_zoom() · source line 508
static bool start_automatic_zoom(void)

Calls into this documentation:

  1. Decision!allocate_auto_buffers()
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Executebuild_auto_palette();
  3. Executereset_auto_queue();
  4. ExecuteportENTER_CRITICAL(&auto_frame_lock);
  5. Executeautomatic_zoom = true;
  6. ExecuteportEXIT_CRITICAL(&auto_frame_lock);
  7. Executeauto_rendered_frames = 0U;
  8. Executeauto_displayed_frames = 0U;
  9. Executeauto_log_started_ms = 0U;
  10. Executeframe_ready = true;
  11. Decisionauto_renderer_task == NULL
    True
      1. Executeconst BaseType_t result = xTaskCreatePinnedToCore(automatic_renderer_task, "mandel_render", 8192U, NULL, 2U, &auto_renderer_task, xPortGetCoreID() == 0 ? 1 : 0);
      2. Decisionresult != pdPASS
        True
          1. ExecuteportENTER_CRITICAL(&auto_frame_lock);
          2. Executeautomatic_zoom = false;
          3. ExecuteportEXIT_CRITICAL(&auto_frame_lock);
          4. Executeauto_renderer_task = NULL;
          5. Executeauto_status_text = "AUTO RENDER TASK FAILED";
          6. ExecuteESP_LOGE(TAG, "Unable to start automatic zoom renderer task");
          7. Exit this functionreturn false;
        False

        Continue after this decision.

    False
      1. ExecutexTaskNotifyGive(auto_renderer_task);
  12. Executeauto_status_text = "AUTO RENDERING - B MANUAL";
  13. Preprocessor directive#if 0
  14. Executemaths_valid = false;
  15. Preprocessor directive#endif
  16. ExecuteESP_LOGI(TAG, "Automatic zoom started from the full Mandelbrot view");
  17. Exit this functionreturn true;
Read original function source
static bool start_automatic_zoom(void)
{
    if (!allocate_auto_buffers()) return false;
    build_auto_palette();
    reset_auto_queue();
    portENTER_CRITICAL(&auto_frame_lock);
    automatic_zoom = true;
    portEXIT_CRITICAL(&auto_frame_lock);
    auto_rendered_frames = 0U;
    auto_displayed_frames = 0U;
    auto_log_started_ms = 0U;
    frame_ready = true;
    if (auto_renderer_task == NULL) {
        const BaseType_t result = xTaskCreatePinnedToCore(automatic_renderer_task,
                                                           "mandel_render", 8192U, NULL, 2U,
                                                           &auto_renderer_task,
                                                           xPortGetCoreID() == 0 ? 1 : 0);
        if (result != pdPASS) {
            portENTER_CRITICAL(&auto_frame_lock);
            automatic_zoom = false;
            portEXIT_CRITICAL(&auto_frame_lock);
            auto_renderer_task = NULL;
            auto_status_text = "AUTO RENDER TASK FAILED";
            ESP_LOGE(TAG, "Unable to start automatic zoom renderer task");
            return false;
        }
    } else {
        // Wake the dormant renderer after B returned the module to manual mode.
        xTaskNotifyGive(auto_renderer_task);
    }
    auto_status_text = "AUTO RENDERING - B MANUAL";
#if 0 // Maths panel experiment: disabled.
    maths_valid = false;
#endif
    ESP_LOGI(TAG, "Automatic zoom started from the full Mandelbrot view");
    return true;
}
return_to_manual_view() · source line 546
static void return_to_manual_view(void)

Calls into this documentation:

  1. ExecuteportENTER_CRITICAL(&auto_frame_lock);
  2. Executeautomatic_zoom = false;
  3. FOR loopunsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Decisionauto_frames[index].state == MANDELBROT_FRAME_READY
        True
          1. Executeauto_frames[index].state = MANDELBROT_FRAME_FREE;
        False

        Continue after this decision.

  4. ExecuteportEXIT_CRITICAL(&auto_frame_lock);
  5. Executedisplayed_frame_slot = -1;
  6. Executeauto_status_text = "B RECEIVED - MANUAL MODE";
  7. Executebegin_render();
  8. ExecuteESP_LOGI(TAG, "Automatic zoom stopped; manual viewer restored");
Read original function source
static void return_to_manual_view(void)
{
    portENTER_CRITICAL(&auto_frame_lock);
    automatic_zoom = false;
    for (unsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index) {
        if (auto_frames[index].state == MANDELBROT_FRAME_READY) {
            auto_frames[index].state = MANDELBROT_FRAME_FREE;
        }
    }
    portEXIT_CRITICAL(&auto_frame_lock);
    displayed_frame_slot = -1;
    auto_status_text = "B RECEIVED - MANUAL MODE";
    begin_render();
    ESP_LOGI(TAG, "Automatic zoom stopped; manual viewer restored");
}
take_ready_auto_frame() · source line 562
static int take_ready_auto_frame(void)
  1. Executeint slot = -1;
  2. Executeuint32_t oldest_sequence = UINT32_MAX;
  3. ExecuteportENTER_CRITICAL(&auto_frame_lock);
  4. Decisionautomatic_zoom
    True
      1. FOR loopunsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index

        Repeat body while the loop condition permits; then continue below.

          1. Decisionauto_frames[index].state == MANDELBROT_FRAME_READY && auto_frames[index].sequence < oldest_sequence
            True
              1. Executeoldest_sequence = auto_frames[index].sequence;
              2. Executeslot = (int)index;
            False

            Continue after this decision.

      2. Decisionslot >= 0
        True
        1. Executeauto_frames[slot].state = MANDELBROT_FRAME_DISPLAYING;
        False

        Continue after this decision.

    False

    Continue after this decision.

  5. ExecuteportEXIT_CRITICAL(&auto_frame_lock);
  6. Exit this functionreturn slot;
Read original function source
static int take_ready_auto_frame(void)
{
    int slot = -1;
    uint32_t oldest_sequence = UINT32_MAX;
    portENTER_CRITICAL(&auto_frame_lock);
    if (automatic_zoom) {
        for (unsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index) {
            if (auto_frames[index].state == MANDELBROT_FRAME_READY &&
                auto_frames[index].sequence < oldest_sequence) {
                oldest_sequence = auto_frames[index].sequence;
                slot = (int)index;
            }
        }
        if (slot >= 0) auto_frames[slot].state = MANDELBROT_FRAME_DISPLAYING;
    }
    portEXIT_CRITICAL(&auto_frame_lock);
    return slot;
}
ready_auto_frame_count() · source line 581
static unsigned ready_auto_frame_count(void)
  1. Executeunsigned count = 0U;
  2. ExecuteportENTER_CRITICAL(&auto_frame_lock);
  3. FOR loopunsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Decisionauto_frames[index].state == MANDELBROT_FRAME_READY
        True
        1. Execute++count;
        False

        Continue after this decision.

  4. ExecuteportEXIT_CRITICAL(&auto_frame_lock);
  5. Exit this functionreturn count;
Read original function source
static unsigned ready_auto_frame_count(void)
{
    unsigned count = 0U;
    portENTER_CRITICAL(&auto_frame_lock);
    for (unsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index) {
        if (auto_frames[index].state == MANDELBROT_FRAME_READY) ++count;
    }
    portEXIT_CRITICAL(&auto_frame_lock);
    return count;
}
log_auto_status() · source line 592
static void log_auto_status(uint32_t now_ms)

Calls into this documentation:

  1. Decisionauto_log_started_ms == 0U
    True
    1. Executeauto_log_started_ms = now_ms;
    False

    Continue after this decision.

  2. Decisionnow_ms - auto_log_started_ms < MANDELBROT_AUTO_LOG_INTERVAL_MS
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  3. ExecuteESP_LOGI(TAG, "AUTO fps=%u render_fps=%u render=%ums ready=%u psram=%u bytes", (unsigned)auto_displayed_frames, (unsigned)auto_rendered_frames, (unsigned)auto_last_render_ms, ready_auto_frame_count(), (unsigned)(MANDELBROT_AUTO_FRAME_COUNT * LCD_WIDTH * LCD_HEIGHT));
  4. Executeauto_displayed_frames = 0U;
  5. Executeauto_rendered_frames = 0U;
  6. Executeauto_log_started_ms = now_ms;
Read original function source
static void log_auto_status(uint32_t now_ms)
{
    if (auto_log_started_ms == 0U) auto_log_started_ms = now_ms;
    if (now_ms - auto_log_started_ms < MANDELBROT_AUTO_LOG_INTERVAL_MS) return;
    ESP_LOGI(TAG, "AUTO fps=%u render_fps=%u render=%ums ready=%u psram=%u bytes",
             (unsigned)auto_displayed_frames, (unsigned)auto_rendered_frames,
             (unsigned)auto_last_render_ms, ready_auto_frame_count(),
             (unsigned)(MANDELBROT_AUTO_FRAME_COUNT * LCD_WIDTH * LCD_HEIGHT));
    auto_displayed_frames = 0U;
    auto_rendered_frames = 0U;
    auto_log_started_ms = now_ms;
}
startMandelbrot() · source line 605
void startMandelbrot(uint32_t now_ms)

Calls into this documentation:

  1. Executerunning = true;
  2. Executeselect_view(0U);
  3. Executelast_cursor_move = now_ms;
  4. Preprocessor directive#if 0
  5. Executemaths_last_update = now_ms;
  6. Preprocessor directive#endif
  7. Executeautomatic_zoom = false;
  8. Executedisplayed_frame_slot = -1;
  9. Execute(void)allocate_manual_staging_frame();
  10. Executebegin_render();
Read original function source
void startMandelbrot(uint32_t now_ms)
{
    running = true;
    select_view(0U);
    last_cursor_move = now_ms;
#if 0 // Maths panel experiment: disabled.
    maths_last_update = now_ms;
#endif
    automatic_zoom = false;
    displayed_frame_slot = -1;
    (void)allocate_manual_staging_frame();
    begin_render();
}
stopMandelbrot() · source line 619
void stopMandelbrot(void)

Calls into this documentation:

  1. Executerunning = false;
  2. Executerendering = false;
  3. Executeframe_ready = false;
  4. ExecuteportENTER_CRITICAL(&auto_frame_lock);
  5. Executeautomatic_zoom = false;
  6. ExecuteportEXIT_CRITICAL(&auto_frame_lock);
  7. Executerelease_manual_staging_frame();
Read original function source
void stopMandelbrot(void)
{
    running = false;
    rendering = false;
    frame_ready = false;
    portENTER_CRITICAL(&auto_frame_lock);
    automatic_zoom = false;
    portEXIT_CRITICAL(&auto_frame_lock);
    release_manual_staging_frame();
}
mandelbrotAction() · source line 630
void mandelbrotAction(bool zoom, bool start_automatic, bool return_to_manual,
                      bool next_view, uint32_t now_ms)

Calls into this documentation:

  1. Decision!running
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Decisionreturn_to_manual && automaticZoomActive()
    True
      1. Executereturn_to_manual_view();
      2. Exit this functionreturn;
    False

    Continue after this decision.

  3. Decisionstart_automatic
    True
      1. Executeauto_status_text = "C RECEIVED - STARTING AUTO";
      2. Executeframe_ready = true;
      3. Executestart_automatic_zoom();
      4. Exit this functionreturn;
    False

    Continue after this decision.

  4. DecisionautomaticZoomActive() || (!zoom && !next_view)
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  5. Decisionnext_view
    True
      1. Executeselect_view((uint8_t)((selected_view + 1U) % (sizeof(views) / sizeof(views[0]))));
    False
      1. Executeconst double height = view_width * (double)LCD_HEIGHT / (double)LCD_WIDTH;
      2. Executeconst double minimum_real = centre_real - view_width * 0.5;
      3. Executeconst double minimum_imaginary = centre_imaginary - height * 0.5;
      4. Executecentre_real = minimum_real + view_width * (double)(cursor_x + ZOOM_BOX_WIDTH / 2) / (double)(LCD_WIDTH - 1U);
      5. Executecentre_imaginary = minimum_imaginary + height * (double)(cursor_y + ZOOM_BOX_HEIGHT / 2) / (double)(LCD_HEIGHT - 1U);
      6. Executeview_width *= (double)ZOOM_BOX_WIDTH / (double)LCD_WIDTH;
      7. Execute++zoom_level;
  6. Executelast_cursor_move = now_ms;
  7. Executebegin_render();
Read original function source
void mandelbrotAction(bool zoom, bool start_automatic, bool return_to_manual,
                      bool next_view, uint32_t now_ms)
{
    if (!running) return;
    if (return_to_manual && automaticZoomActive()) {
        return_to_manual_view();
        return;
    }
    if (start_automatic) {
        auto_status_text = "C RECEIVED - STARTING AUTO";
        frame_ready = true;
        start_automatic_zoom();
        return;
    }
    if (automaticZoomActive() || (!zoom && !next_view)) return;
    if (next_view) {
        select_view((uint8_t)((selected_view + 1U) % (sizeof(views) / sizeof(views[0]))));
    } else {
        const double height = view_width * (double)LCD_HEIGHT / (double)LCD_WIDTH;
        const double minimum_real = centre_real - view_width * 0.5;
        const double minimum_imaginary = centre_imaginary - height * 0.5;
        centre_real = minimum_real + view_width * (double)(cursor_x + ZOOM_BOX_WIDTH / 2) /
                      (double)(LCD_WIDTH - 1U);
        centre_imaginary = minimum_imaginary + height * (double)(cursor_y + ZOOM_BOX_HEIGHT / 2) /
                           (double)(LCD_HEIGHT - 1U);
        view_width *= (double)ZOOM_BOX_WIDTH / (double)LCD_WIDTH;
        ++zoom_level;
    }
    last_cursor_move = now_ms;
    begin_render();
}
updateMandelbrot() · source line 662
void updateMandelbrot(int x_offset, int y_offset, bool joystick_valid, uint32_t now_ms)

Calls into this documentation:

  1. Decision!running
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Preprocessor directive#if 0
  3. Executerow_count = 0U;
  4. Executeconst bool maths_changed = update_maths_sample(now_ms);
  5. Decisionmaths_changed
    True
    1. Executeframe_ready = true;
    False

    Continue after this decision.

  6. Preprocessor directive#endif
  7. DecisionautomaticZoomActive()
    True
      1. Executelog_auto_status(now_ms);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  8. Decisionjoystick_valid && now_ms - last_cursor_move >= CURSOR_REPEAT_MS
    True
      1. Decisionx_offset >= JOYSTICK_ENTER
        True
        1. Executecursor_x += CURSOR_STEP;
        False
        1. Decisionx_offset <= -JOYSTICK_ENTER
          True
          1. Executecursor_x -= CURSOR_STEP;
          False

          Continue after this decision.

      2. Decisiony_offset >= JOYSTICK_ENTER
        True
        1. Executecursor_y -= CURSOR_STEP;
        False
        1. Decisiony_offset <= -JOYSTICK_ENTER
          True
          1. Executecursor_y += CURSOR_STEP;
          False

          Continue after this decision.

      3. Decisioncursor_x < 0
        True
        1. Executecursor_x = 0;
        False

        Continue after this decision.

      4. Decisioncursor_x > LCD_WIDTH - ZOOM_BOX_WIDTH
        True
        1. Executecursor_x = LCD_WIDTH - ZOOM_BOX_WIDTH;
        False

        Continue after this decision.

      5. Decisioncursor_y < 0
        True
        1. Executecursor_y = 0;
        False

        Continue after this decision.

      6. Decisioncursor_y > LCD_HEIGHT - ZOOM_BOX_HEIGHT
        True
        1. Executecursor_y = LCD_HEIGHT - ZOOM_BOX_HEIGHT;
        False

        Continue after this decision.

      7. Executelast_cursor_move = now_ms;
      8. Executeframe_ready = true;
    False

    Continue after this decision.

  9. Decision!rendering
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  10. Executerendered_start_row = next_row;
  11. Executerow_count = 0U;
  12. WHILE looprow_count < ROWS_PER_UPDATE && next_row < LCD_HEIGHT

    Repeat body while the loop condition permits; then continue below.

      1. Executeuint16_t *destination = rendered_rows[row_count];
      2. Decisionmanual_staging_frame != NULL
        True
          1. Executedestination = manual_staging_frame + (size_t)next_row * LCD_WIDTH;
        False

        Continue after this decision.

      3. Executemanual_iteration_count += render_row(next_row, destination);
      4. Execute++row_count;
      5. Execute++next_row;
  13. Executeframe_ready = frame_ready || row_count != 0U || clear_requested;
  14. Decisionnext_row >= LCD_HEIGHT
    True
      1. Executerendering = false;
      2. Executemanual_reveal_pending = manual_staging_frame != NULL;
    False

    Continue after this decision.

Read original function source
void updateMandelbrot(int x_offset, int y_offset, bool joystick_valid, uint32_t now_ms)
{
    if (!running) return;
#if 0 // Maths panel experiment: disabled.
    // Overlay-only redraws must not copy an old pair of manual render rows.
    row_count = 0U;
    const bool maths_changed = update_maths_sample(now_ms);
    if (maths_changed) frame_ready = true;
#endif
    if (automaticZoomActive()) {
        log_auto_status(now_ms);
        return;
    }
    if (joystick_valid && now_ms - last_cursor_move >= CURSOR_REPEAT_MS) {
        if (x_offset >= JOYSTICK_ENTER) cursor_x += CURSOR_STEP;
        else if (x_offset <= -JOYSTICK_ENTER) cursor_x -= CURSOR_STEP;
        if (y_offset >= JOYSTICK_ENTER) cursor_y -= CURSOR_STEP;
        else if (y_offset <= -JOYSTICK_ENTER) cursor_y += CURSOR_STEP;
        if (cursor_x < 0) cursor_x = 0;
        if (cursor_x > LCD_WIDTH - ZOOM_BOX_WIDTH) cursor_x = LCD_WIDTH - ZOOM_BOX_WIDTH;
        if (cursor_y < 0) cursor_y = 0;
        if (cursor_y > LCD_HEIGHT - ZOOM_BOX_HEIGHT) cursor_y = LCD_HEIGHT - ZOOM_BOX_HEIGHT;
        last_cursor_move = now_ms;
        frame_ready = true;
    }
    if (!rendering) return;
    rendered_start_row = next_row;
    row_count = 0U;
    while (row_count < ROWS_PER_UPDATE && next_row < LCD_HEIGHT) {
        uint16_t *destination = rendered_rows[row_count];
        if (manual_staging_frame != NULL) {
            destination = manual_staging_frame + (size_t)next_row * LCD_WIDTH;
        }
        manual_iteration_count += render_row(next_row, destination);
        ++row_count;
        ++next_row;
    }
    frame_ready = frame_ready || row_count != 0U || clear_requested;
    if (next_row >= LCD_HEIGHT) {
        rendering = false;
        // Keep the 100% screen visible for one frame before showing the fractal.
        manual_reveal_pending = manual_staging_frame != NULL;
    }
}
mandelbrotTakeFrame() · source line 707
bool mandelbrotTakeFrame(void)

Calls into this documentation:

  1. DecisionautomaticZoomActive()
    True
      1. Executedisplayed_frame_slot = take_ready_auto_frame();
      2. Decisiondisplayed_frame_slot >= 0
        True
          1. Exit this functionreturn true;
        False

        Continue after this decision.

      3. Executeconst bool ready = frame_ready;
      4. Executeframe_ready = false;
      5. Exit this functionreturn ready;
    False

    Continue after this decision.

  2. Executeconst bool ready = frame_ready;
  3. Executeframe_ready = false;
  4. Exit this functionreturn ready;
Read original function source
bool mandelbrotTakeFrame(void)
{
    if (automaticZoomActive()) {
        displayed_frame_slot = take_ready_auto_frame();
        if (displayed_frame_slot >= 0) {
            return true;
        }
        const bool ready = frame_ready;
        frame_ready = false;
        return ready;
    }
    const bool ready = frame_ready;
    frame_ready = false;
    return ready;
}
draw_manual_progress() · source line 723
static void draw_manual_progress(uint16_t *frame)

Calls into this documentation:

  1. Executeconst unsigned percent = ((unsigned)next_row * 100U) / LCD_HEIGHT;
  2. Executeconst int bar_x = (LCD_WIDTH - MANDELBROT_PROGRESS_BAR_WIDTH) / 2;
  3. Executeconst int bar_y = 102;
  4. Executeconst int inner_width = MANDELBROT_PROGRESS_BAR_WIDTH - 2;
  5. Executeconst int filled_width = (inner_width * (int)percent) / 100;
  6. Executechar text[32];
  7. Executeclear(frame, COLOR_BLACK);
  8. ExecutedrawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 20, "MANDELBROT", COLOR_LIME, 3);
  9. ExecutedrawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 51, "Z=Z*Z+C", COLOR_LIGHTGREEN, 2);
  10. ExecutedrawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 76, "CALCULATING", COLOR_GREENYELLOW, 2);
  11. Executedraw_rect(frame, bar_x, bar_y, MANDELBROT_PROGRESS_BAR_WIDTH, MANDELBROT_PROGRESS_BAR_HEIGHT, COLOR_CRIMSON);
  12. Decisionfilled_width > 0
    True
      1. Executefill_rect(frame, bar_x + 1, bar_y + 1, filled_width, MANDELBROT_PROGRESS_BAR_HEIGHT - 2, COLOR_RED);
    False

    Continue after this decision.

  13. Executesnprintf(text, sizeof(text), "%u%%", percent);
  14. ExecutedrawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 119, text, COLOR_LIME, 2);
  15. Executesnprintf(text, sizeof(text), "ITERATIONS %lu", (unsigned long)manual_iteration_count);
  16. ExecutedrawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 146, text, COLOR_LIGHTGREEN, 1);
Read original function source
static void draw_manual_progress(uint16_t *frame)
{
    const unsigned percent = ((unsigned)next_row * 100U) / LCD_HEIGHT;
    const int bar_x = (LCD_WIDTH - MANDELBROT_PROGRESS_BAR_WIDTH) / 2;
    const int bar_y = 102;
    const int inner_width = MANDELBROT_PROGRESS_BAR_WIDTH - 2;
    const int filled_width = (inner_width * (int)percent) / 100;
    char text[32];

    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 20, "MANDELBROT", COLOR_LIME, 3);
    drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 51, "Z=Z*Z+C", COLOR_LIGHTGREEN, 2);
    drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 76, "CALCULATING", COLOR_GREENYELLOW, 2);
    draw_rect(frame, bar_x, bar_y, MANDELBROT_PROGRESS_BAR_WIDTH,
              MANDELBROT_PROGRESS_BAR_HEIGHT, COLOR_CRIMSON);
    if (filled_width > 0) {
        fill_rect(frame, bar_x + 1, bar_y + 1, filled_width,
                  MANDELBROT_PROGRESS_BAR_HEIGHT - 2, COLOR_RED);
    }
    snprintf(text, sizeof(text), "%u%%", percent);
    drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 119, text, COLOR_LIME, 2);
    snprintf(text, sizeof(text), "ITERATIONS %lu", (unsigned long)manual_iteration_count);
    drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 146, text, COLOR_LIGHTGREEN, 1);
}
drawMandelbrot() · source line 748
void drawMandelbrot(uint16_t *frame)

Calls into this documentation:

  1. Decision!running || frame == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. DecisionautomaticZoomActive()
    True
      1. Decisiondisplayed_frame_slot < 0
        True
          1. Executefill_rect(frame, 0, 0, LCD_WIDTH, 12, COLOR_BLACK);
          2. ExecutedrawPet2001TextTransparent(frame, 2, 2, auto_status_text, COLOR_WHITE, 1);
          3. Exit this functionreturn;
        False

        Continue after this decision.

      2. Executeconst mandelbrot_frame_t *displayed = &auto_frames[displayed_frame_slot];
      3. Executeconst uint8_t *source = displayed->pixels;
      4. FOR loopsize_t pixel = 0U; pixel < (size_t)LCD_WIDTH * LCD_HEIGHT; ++pixel

        Repeat body while the loop condition permits; then continue below.

          1. Executeframe[pixel] = auto_palette[source[pixel]];
      5. Preprocessor directive#if 0
      6. Executeset_maths_sample(displayed->view, displayed->sample_real, displayed->sample_imaginary, true);
      7. Preprocessor directive#endif
      8. ExecuteportENTER_CRITICAL(&auto_frame_lock);
      9. Executeauto_frames[displayed_frame_slot].state = MANDELBROT_FRAME_FREE;
      10. ExecuteportEXIT_CRITICAL(&auto_frame_lock);
      11. Executedisplayed_frame_slot = -1;
      12. Execute++auto_displayed_frames;
      13. Exit this functionreturn;
    False

    Continue after this decision.

  3. Preprocessor directive#if 0
  4. Decision!clear_requested && row_count == 0U && cursor_drawn && cursor_x == drawn_cursor_x && cursor_y == drawn_cursor_y
    True
      1. Executedraw_maths_panel(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Preprocessor directive#endif
  6. Decisionmanual_staging_frame != NULL
    True
      1. Decisionrendering || manual_reveal_pending
        True
          1. Executedraw_manual_progress(frame);
          2. Decisionmanual_reveal_pending
            True
              1. Executemanual_reveal_pending = false;
              2. Executeframe_ready = true;
            False

            Continue after this decision.

          3. Exit this functionreturn;
        False

        Continue after this decision.

      2. Executememcpy(frame, manual_staging_frame, MANDELBROT_MANUAL_FRAME_BYTES);
      3. Executeclear_requested = false;
    False
      1. Decisionclear_requested
        True
          1. Executeclear(frame, COLOR_BLACK);
          2. Executeclear_requested = false;
        False

        Continue after this decision.

      2. FOR loopuint8_t row = 0U; row < row_count; ++row

        Repeat body while the loop condition permits; then continue below.

          1. Executememcpy(frame + (size_t)(rendered_start_row + row) * LCD_WIDTH, rendered_rows[row], sizeof(rendered_rows[row]));
  7. Decisioncursor_drawn
    True
    1. Executerestore_cursor_border(frame, drawn_cursor_x, drawn_cursor_y);
    False

    Continue after this decision.

  8. Executechar status[40];
  9. Executesnprintf(status, sizeof(status), "%s Z%u", views[selected_view].name, (unsigned)zoom_level);
  10. Executefill_rect(frame, 0, 0, 128, 10, COLOR_BLACK);
  11. ExecutedrawPet2001TextTransparent(frame, 2, 1, status, COLOR_WHITE, 1);
  12. Executedraw_rect(frame, cursor_x, cursor_y, ZOOM_BOX_WIDTH, ZOOM_BOX_HEIGHT, COLOR_WHITE);
  13. Executedrawn_cursor_x = cursor_x;
  14. Executedrawn_cursor_y = cursor_y;
  15. Executecursor_drawn = true;
  16. ExecutedrawPet2001TextTransparent(frame, 2, 148, "A/K ZOOM C AUTO E VIEW", COLOR_WHITE, 1);
  17. ExecutedrawPet2001TextTransparent(frame, 2, 158, "B MANUAL BOOT BACK", COLOR_WHITE, 1);
  18. Decisionauto_status_text != NULL
    True
      1. Executefill_rect(frame, 0, 12, LCD_WIDTH, 10, COLOR_BLACK);
      2. ExecutedrawPet2001TextTransparent(frame, 2, 12, auto_status_text, COLOR_WHITE, 1);
    False

    Continue after this decision.

  19. Preprocessor directive#if 0
  20. Executedraw_maths_panel(frame);
  21. Preprocessor directive#endif
Read original function source
void drawMandelbrot(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (automaticZoomActive()) {
        if (displayed_frame_slot < 0) {
            fill_rect(frame, 0, 0, LCD_WIDTH, 12, COLOR_BLACK);
            drawPet2001TextTransparent(frame, 2, 2, auto_status_text, COLOR_WHITE, 1);
            return;
        }
        const mandelbrot_frame_t *displayed = &auto_frames[displayed_frame_slot];
        const uint8_t *source = displayed->pixels;
        for (size_t pixel = 0U; pixel < (size_t)LCD_WIDTH * LCD_HEIGHT; ++pixel) {
            frame[pixel] = auto_palette[source[pixel]];
        }
#if 0 // Maths panel experiment: disabled.
        // Copy metadata before releasing the slot for the background renderer.
        set_maths_sample(displayed->view, displayed->sample_real,
                         displayed->sample_imaginary, true);
#endif
        portENTER_CRITICAL(&auto_frame_lock);
        auto_frames[displayed_frame_slot].state = MANDELBROT_FRAME_FREE;
        portEXIT_CRITICAL(&auto_frame_lock);
        displayed_frame_slot = -1;
        ++auto_displayed_frames;
        return;
    }
#if 0 // Maths panel experiment: disabled.
    if (!clear_requested && row_count == 0U && cursor_drawn &&
        cursor_x == drawn_cursor_x && cursor_y == drawn_cursor_y) {
        // A maths-only tick does not need to recalculate the cursor border pixels.
        draw_maths_panel(frame);
        return;
    }
#endif
    if (manual_staging_frame != NULL) {
        if (rendering || manual_reveal_pending) {
            draw_manual_progress(frame);
            if (manual_reveal_pending) {
                manual_reveal_pending = false;
                frame_ready = true;
            }
            return;
        }
        memcpy(frame, manual_staging_frame, MANDELBROT_MANUAL_FRAME_BYTES);
        clear_requested = false;
    } else {
        if (clear_requested) {
            clear(frame, COLOR_BLACK);
            clear_requested = false;
        }
        for (uint8_t row = 0U; row < row_count; ++row) {
            memcpy(frame + (size_t)(rendered_start_row + row) * LCD_WIDTH,
                   rendered_rows[row], sizeof(rendered_rows[row]));
        }
    }
    if (cursor_drawn) restore_cursor_border(frame, drawn_cursor_x, drawn_cursor_y);
    char status[40];
    snprintf(status, sizeof(status), "%s Z%u", views[selected_view].name,
             (unsigned)zoom_level);
    fill_rect(frame, 0, 0, 128, 10, COLOR_BLACK);
    drawPet2001TextTransparent(frame, 2, 1, status, COLOR_WHITE, 1);
    draw_rect(frame, cursor_x, cursor_y, ZOOM_BOX_WIDTH, ZOOM_BOX_HEIGHT, COLOR_WHITE);
    drawn_cursor_x = cursor_x;
    drawn_cursor_y = cursor_y;
    cursor_drawn = true;
    drawPet2001TextTransparent(frame, 2, 148, "A/K ZOOM  C AUTO  E VIEW", COLOR_WHITE, 1);
    drawPet2001TextTransparent(frame, 2, 158, "B MANUAL  BOOT BACK", COLOR_WHITE, 1);
    if (auto_status_text != NULL) {
        fill_rect(frame, 0, 12, LCD_WIDTH, 10, COLOR_BLACK);
        drawPet2001TextTransparent(frame, 2, 12, auto_status_text, COLOR_WHITE, 1);
    }
#if 0 // Maths panel experiment: disabled.
    // Compose last so manual rows, status text and cursor restoration stay below it.
    draw_maths_panel(frame);
#endif
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
// Mandelbrot viewer with an optional PSRAM-backed automatic zoom renderer.
#include "mandelbrot.h"

#include "game_graphics.h"
#include "PET2001_font.h"

#include "esp_heap_caps.h"
#include "esp_log.h"

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include <math.h>
#include <stdio.h>
#include <string.h>

#define MAX_ITERATIONS 100U
#define ROWS_PER_UPDATE 2U
#define ZOOM_BOX_WIDTH 80
#define ZOOM_BOX_HEIGHT 42
#define JOYSTICK_ENTER 600
#define CURSOR_STEP 8
#define CURSOR_REPEAT_MS 70U

// Maths panel: one sample iteration per display tick, never a render-time wait.
#define MANDELBROT_MATHS_INTERVAL_MS 16U
#define MANDELBROT_MATHS_HOLD_MS 1000U // Hold escape/limit result before replaying.
#define MANDELBROT_MATHS_WIDTH 154
#define MANDELBROT_MATHS_HEIGHT 106
#define MANDELBROT_MATHS_LINES 11U

// Automatic zoom tuning. These can be changed without altering the queue.
#define MANDELBROT_AUTO_FRAME_COUNT 4U
#define MANDELBROT_AUTO_START_REAL -0.50f
#define MANDELBROT_AUTO_START_IMAGINARY 0.00f
#define MANDELBROT_AUTO_START_WIDTH 3.76f
// This fixed target produces the familiar Seahorse Valley detail.
#define MANDELBROT_AUTO_TARGET_REAL -0.7436439f
#define MANDELBROT_AUTO_TARGET_IMAGINARY 0.1318259f
// Every rendered frame is this fraction as wide as the preceding frame.
// Values nearer 1.0 zoom more slowly; it must remain greater than zero.
#define MANDELBROT_AUTO_ZOOM_PER_FRAME 0.95f
#define MANDELBROT_AUTO_LOG_INTERVAL_MS 1000U
// Automatic frames trade a little edge detail for regular animation updates.
// Keep this below the manual viewer's MAX_ITERATIONS value.
#define MANDELBROT_AUTO_MAX_ITERATIONS 64U
// Yielding after this many rows lets CPU 1's idle task run and prevents a watchdog reset.
#define MANDELBROT_AUTO_ROWS_PER_YIELD 4U

// Manual rendering is staged off-screen so no partially calculated rows reach the LCD.
#define MANDELBROT_MANUAL_FRAME_BYTES (LCD_WIDTH * LCD_HEIGHT * sizeof(uint16_t))
// Visual tuning controls for the centred manual-render progress screen.
#define MANDELBROT_PROGRESS_BAR_WIDTH 200
// Taller than the PET text strokes so progress is clear at a glance.
#define MANDELBROT_PROGRESS_BAR_HEIGHT 16

typedef struct {
    const char *name;
    double centre_real;
    double centre_imaginary;
    double width;
} mandelbrot_view_t;

typedef enum {
    MANDELBROT_FRAME_FREE,
    MANDELBROT_FRAME_RENDERING,
    MANDELBROT_FRAME_READY,
    MANDELBROT_FRAME_DISPLAYING,
} mandelbrot_frame_state_t;

typedef struct {
    uint8_t *pixels;
    mandelbrot_frame_state_t state;
    uint32_t sequence;
    uint32_t generation;
    // Published with the pixels so the panel describes the displayed frame.
    mandelbrot_view_t view;
    double sample_real, sample_imaginary;
} mandelbrot_frame_t;

static const char *const TAG = "mandelbrot";
static const mandelbrot_view_t views[] = {
    {"FULL",  -0.50,     0.00,     3.76},
    {"HORSE", -0.74364,  0.13183,  0.030},
    {"ELEPH",  0.285,    0.010,    0.180},
    {"SPIRAL",-0.761574,-0.084760, 0.018},
};

static uint16_t rendered_rows[ROWS_PER_UPDATE][LCD_WIDTH];
static uint16_t auto_palette[256];
static uint16_t *manual_staging_frame;
static mandelbrot_frame_t auto_frames[MANDELBROT_AUTO_FRAME_COUNT];
static portMUX_TYPE auto_frame_lock = portMUX_INITIALIZER_UNLOCKED;
static TaskHandle_t auto_renderer_task;
static int displayed_frame_slot = -1;
static uint8_t selected_view, row_count, zoom_level;
static uint16_t next_row, rendered_start_row;
static int cursor_x, cursor_y, drawn_cursor_x, drawn_cursor_y;
static double centre_real, centre_imaginary, view_width;
static uint32_t last_cursor_move, auto_generation, auto_next_sequence;
static uint32_t manual_iteration_count;
static uint32_t auto_rendered_frames, auto_displayed_frames, auto_last_render_ms;
static uint32_t auto_log_started_ms;
static const char *auto_status_text;
static bool running, rendering, clear_requested, frame_ready, cursor_drawn;
static bool manual_reveal_pending;
static bool automatic_zoom, auto_buffers_ready;

// The maths-panel experiment is retained below, but is disabled because the
// panel occupied too much of the fractal display. Keep it here for a future,
// smaller presentation rather than removing the working calculations.
#if 0
// Owned only by the main/display task. The worker publishes frame metadata above.
static mandelbrot_view_t maths_view;
static double maths_c_real, maths_c_imaginary, maths_z_real, maths_z_imaginary;
static double maths_magnitude_squared;
static uint16_t maths_iteration, maths_limit;
static uint32_t maths_last_update, maths_finished_at;
static bool maths_valid, maths_auto, maths_finished;
static char maths_lines[MANDELBROT_MATHS_LINES][24];

static void refresh_maths_text(void)
{
    snprintf(maths_lines[0], sizeof(maths_lines[0]), "z=z*z+c  MID %u,%u",
             (unsigned)(LCD_WIDTH / 2), (unsigned)(LCD_HEIGHT / 2));
    snprintf(maths_lines[1], sizeof(maths_lines[1]), "VR %+.7f", maths_view.centre_real);
    snprintf(maths_lines[2], sizeof(maths_lines[2]), "VI %+.7f", maths_view.centre_imaginary);
    snprintf(maths_lines[3], sizeof(maths_lines[3]), "W %.5g", maths_view.width);
    snprintf(maths_lines[4], sizeof(maths_lines[4]), "ZOOM %.4gx",
             views[0].width / maths_view.width);
    snprintf(maths_lines[5], sizeof(maths_lines[5]), "CR %+.7f", maths_c_real);
    snprintf(maths_lines[6], sizeof(maths_lines[6]), "CI %+.7f", maths_c_imaginary);
    snprintf(maths_lines[7], sizeof(maths_lines[7]), "ZR %+.6f", maths_z_real);
    snprintf(maths_lines[8], sizeof(maths_lines[8]), "ZI %+.6f", maths_z_imaginary);
    snprintf(maths_lines[9], sizeof(maths_lines[9]), "N %u/%u %s", (unsigned)maths_iteration,
             (unsigned)maths_limit, maths_finished ?
             (maths_magnitude_squared > 4.0 ? "ESC" : "LIMIT") : "");
    snprintf(maths_lines[10], sizeof(maths_lines[10]), "|z|2 %.6g",
             maths_magnitude_squared);
}

static void set_maths_sample(mandelbrot_view_t view, double real, double imaginary,
                             bool automatic)
{
    maths_view = view;
    maths_c_real = real;
    maths_c_imaginary = imaginary;
    maths_z_real = maths_z_imaginary = 0.0;
    maths_magnitude_squared = 0.0;
    maths_iteration = 0U;
    maths_limit = automatic ? MANDELBROT_AUTO_MAX_ITERATIONS : MAX_ITERATIONS;
    maths_auto = automatic;
    maths_finished = false;
    maths_valid = true;
    refresh_maths_text();
}

static void set_manual_maths_sample(void)
{
    const double height = view_width * (double)LCD_HEIGHT / (double)LCD_WIDTH;
    const double real = centre_real - view_width * 0.5 +
                        view_width * (double)(LCD_WIDTH / 2) / (double)(LCD_WIDTH - 1U);
    const double imaginary = centre_imaginary - height * 0.5 +
                             height * (double)(LCD_HEIGHT / 2) / (double)(LCD_HEIGHT - 1U);
    set_maths_sample((mandelbrot_view_t){views[selected_view].name, centre_real,
                                        centre_imaginary, view_width}, real, imaginary, false);
}

static bool update_maths_sample(uint32_t now_ms)
{
    if (!maths_valid || now_ms - maths_last_update < MANDELBROT_MATHS_INTERVAL_MS) return false;
    maths_last_update = now_ms;
    if (maths_finished) {
        if (now_ms - maths_finished_at < MANDELBROT_MATHS_HOLD_MS) return false;
        maths_z_real = maths_z_imaginary = 0.0;
        maths_magnitude_squared = 0.0;
        maths_iteration = 0U;
        maths_finished = false;
    } else {
        // Match AUTO's float arithmetic and manual rendering's double arithmetic.
        // Keep old real/imaginary values until both components have been computed.
        if (maths_auto) {
            const float real = (float)maths_z_real, imaginary = (float)maths_z_imaginary;
            maths_z_imaginary = 2.0f * real * imaginary + (float)maths_c_imaginary;
            maths_z_real = real * real - imaginary * imaginary + (float)maths_c_real;
        } else {
            const double real = maths_z_real, imaginary = maths_z_imaginary;
            maths_z_imaginary = 2.0 * real * imaginary + maths_c_imaginary;
            maths_z_real = real * real - imaginary * imaginary + maths_c_real;
        }
        ++maths_iteration;
        if (maths_auto) {
            const float real = (float)maths_z_real, imaginary = (float)maths_z_imaginary;
            maths_magnitude_squared = real * real + imaginary * imaginary;
        } else {
            maths_magnitude_squared = maths_z_real * maths_z_real + maths_z_imaginary * maths_z_imaginary;
        }
        maths_finished = maths_magnitude_squared > 4.0 || maths_iteration >= maths_limit;
        if (maths_finished) maths_finished_at = now_ms;
    }
    refresh_maths_text();
    return true;
}

static void draw_maths_panel(uint16_t *frame)
{
    const int x = LCD_WIDTH - MANDELBROT_MATHS_WIDTH - 2;
    fill_rect(frame, x, 2, MANDELBROT_MATHS_WIDTH, MANDELBROT_MATHS_HEIGHT, COLOR_BLACK);
    draw_rect(frame, x, 2, MANDELBROT_MATHS_WIDTH, MANDELBROT_MATHS_HEIGHT, COLOR_CYAN);
    if (!maths_valid) {
        draw_text(frame, x + 4, 7, "WAITING FOR FRAME", 1, COLOR_WHITE);
        return;
    }
    for (unsigned line = 0U; line < MANDELBROT_MATHS_LINES; ++line) {
        draw_text(frame, x + 4, 6 + (int)line * 9, maths_lines[line], 1,
                  line == 0U ? COLOR_CYAN : COLOR_WHITE);
    }
}
#endif

static uint16_t colour_wheel(uint8_t position)
{
    position = 255U - position;
    if (position < 85U) return RGB565(255U - position * 3U, 0U, position * 3U);
    if (position < 170U) {
        position -= 85U;
        return RGB565(0U, position * 3U, 255U - position * 3U);
    }
    position -= 170U;
    return RGB565(position * 3U, 255U - position * 3U, 0U);
}

static uint16_t iteration_colour(uint16_t iteration)
{
    if (iteration >= MAX_ITERATIONS) return COLOR_BLACK;
    const uint8_t intensity = (uint8_t)((iteration * 255U) / MAX_ITERATIONS);
    return colour_wheel((uint8_t)(intensity + 32U));
}

static uint16_t calculate_pixel(double real, double imaginary, uint16_t *calculation_count)
{
    double z_real = 0.0, z_imaginary = 0.0;
    double z_real_squared = 0.0, z_imaginary_squared = 0.0;
    uint16_t iteration = 0U;
    while (z_real_squared + z_imaginary_squared <= 4.0 && iteration < MAX_ITERATIONS) {
        z_imaginary = 2.0 * z_real * z_imaginary + imaginary;
        z_real = z_real_squared - z_imaginary_squared + real;
        z_real_squared = z_real * z_real;
        z_imaginary_squared = z_imaginary * z_imaginary;
        ++iteration;
    }
    if (calculation_count != NULL) *calculation_count = iteration;
    return iteration_colour(iteration);
}

static uint8_t calculate_iteration_index(float real, float imaginary)
{
    float z_real = 0.0f, z_imaginary = 0.0f;
    float z_real_squared = 0.0f, z_imaginary_squared = 0.0f;
    uint16_t iteration = 0U;
    while (z_real_squared + z_imaginary_squared <= 4.0f &&
           iteration < MANDELBROT_AUTO_MAX_ITERATIONS) {
        z_imaginary = 2.0f * z_real * z_imaginary + imaginary;
        z_real = z_real_squared - z_imaginary_squared + real;
        z_real_squared = z_real * z_real;
        z_imaginary_squared = z_imaginary * z_imaginary;
        ++iteration;
    }
    if (iteration >= MANDELBROT_AUTO_MAX_ITERATIONS) return 0U;
    return (uint8_t)(1U + ((iteration - 1U) * 254U) /
                     (MANDELBROT_AUTO_MAX_ITERATIONS - 1U));
}

static void build_auto_palette(void)
{
    auto_palette[0] = COLOR_BLACK;
    for (unsigned index = 1U; index < 256U; ++index) {
        auto_palette[index] = colour_wheel((uint8_t)(index + 32U));
    }
}

static bool automaticZoomActive(void)
{
    bool active;
    portENTER_CRITICAL(&auto_frame_lock);
    active = automatic_zoom;
    portEXIT_CRITICAL(&auto_frame_lock);
    return active;
}

static void begin_render(void)
{
    next_row = 0U;
    row_count = 0U;
    manual_iteration_count = 0U;
    rendering = true;
    clear_requested = true;
    frame_ready = true;
    cursor_drawn = false;
    manual_reveal_pending = false;
#if 0 // Maths panel experiment: disabled; see the retained implementation above.
    set_manual_maths_sample();
#endif
}

static void select_view(uint8_t view_index)
{
    selected_view = view_index;
    centre_real = views[selected_view].centre_real;
    centre_imaginary = views[selected_view].centre_imaginary;
    view_width = views[selected_view].width;
    zoom_level = 0U;
    cursor_x = (LCD_WIDTH - ZOOM_BOX_WIDTH) / 2;
    cursor_y = (LCD_HEIGHT - ZOOM_BOX_HEIGHT) / 2;
}

static uint32_t render_row(uint16_t y, uint16_t *destination)
{
    const double height = view_width * (double)LCD_HEIGHT / (double)LCD_WIDTH;
    const double minimum_real = centre_real - view_width * 0.5;
    const double minimum_imaginary = centre_imaginary - height * 0.5;
    const double imaginary = minimum_imaginary + height * (double)y / (double)(LCD_HEIGHT - 1U);
    uint32_t row_iterations = 0U;
    for (uint16_t x = 0U; x < LCD_WIDTH; ++x) {
        const double real = minimum_real + view_width * (double)x / (double)(LCD_WIDTH - 1U);
        uint16_t pixel_iterations;
        destination[x] = calculate_pixel(real, imaginary, &pixel_iterations);
        row_iterations += pixel_iterations;
    }
    return row_iterations;
}

static uint16_t screen_pixel_colour(int x, int y)
{
    const double height = view_width * (double)LCD_HEIGHT / (double)LCD_WIDTH;
    const double minimum_real = centre_real - view_width * 0.5;
    const double minimum_imaginary = centre_imaginary - height * 0.5;
    const double real = minimum_real + view_width * (double)x / (double)(LCD_WIDTH - 1U);
    const double imaginary = minimum_imaginary + height * (double)y / (double)(LCD_HEIGHT - 1U);
    return calculate_pixel(real, imaginary, NULL);
}

static void restore_cursor_border(uint16_t *frame, int x, int y)
{
    for (int column = x; column < x + ZOOM_BOX_WIDTH; ++column) {
        frame[y * LCD_WIDTH + column] = screen_pixel_colour(column, y);
        frame[(y + ZOOM_BOX_HEIGHT - 1) * LCD_WIDTH + column] =
            screen_pixel_colour(column, y + ZOOM_BOX_HEIGHT - 1);
    }
    for (int row = y + 1; row < y + ZOOM_BOX_HEIGHT - 1; ++row) {
        frame[row * LCD_WIDTH + x] = screen_pixel_colour(x, row);
        frame[row * LCD_WIDTH + x + ZOOM_BOX_WIDTH - 1] =
            screen_pixel_colour(x + ZOOM_BOX_WIDTH - 1, row);
    }
}

static bool allocate_manual_staging_frame(void)
{
    if (manual_staging_frame != NULL) return true;
    manual_staging_frame = heap_caps_malloc(MANDELBROT_MANUAL_FRAME_BYTES,
                                            MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    if (manual_staging_frame == NULL) {
        auto_status_text = "MANUAL PSRAM ALLOCATION FAILED";
        ESP_LOGE(TAG, "Manual render needs %u PSRAM bytes",
                 (unsigned)MANDELBROT_MANUAL_FRAME_BYTES);
        return false;
    }
    ESP_LOGI(TAG, "Manual render staging frame: %u PSRAM bytes",
             (unsigned)MANDELBROT_MANUAL_FRAME_BYTES);
    return true;
}

static void release_manual_staging_frame(void)
{
    if (manual_staging_frame != NULL) {
        heap_caps_free(manual_staging_frame);
        manual_staging_frame = NULL;
    }
}

static bool allocate_auto_buffers(void)
{
    if (auto_buffers_ready) return true;
    for (unsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index) {
        auto_frames[index].pixels = heap_caps_malloc((size_t)LCD_WIDTH * LCD_HEIGHT,
                                                      MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
        if (auto_frames[index].pixels == NULL) {
            auto_status_text = "AUTO PSRAM ALLOCATION FAILED";
            ESP_LOGE(TAG, "Automatic zoom needs %u PSRAM bytes per frame",
                     (unsigned)(LCD_WIDTH * LCD_HEIGHT));
            return false;
        }
        auto_frames[index].state = MANDELBROT_FRAME_FREE;
    }
    auto_buffers_ready = true;
    ESP_LOGI(TAG, "Automatic zoom PSRAM queue: %u frames, %u bytes",
             MANDELBROT_AUTO_FRAME_COUNT,
             (unsigned)(MANDELBROT_AUTO_FRAME_COUNT * LCD_WIDTH * LCD_HEIGHT));
    return true;
}

static void reset_auto_queue(void)
{
    portENTER_CRITICAL(&auto_frame_lock);
    ++auto_generation;
    auto_next_sequence = 0U;
    for (unsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index) {
        if (auto_frames[index].state != MANDELBROT_FRAME_RENDERING) {
            auto_frames[index].state = MANDELBROT_FRAME_FREE;
        }
    }
    portEXIT_CRITICAL(&auto_frame_lock);
}

static int take_free_auto_frame(uint32_t *sequence, uint32_t *generation)
{
    int slot = -1;
    portENTER_CRITICAL(&auto_frame_lock);
    if (automatic_zoom) {
        for (unsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index) {
            if (auto_frames[index].state != MANDELBROT_FRAME_FREE) continue;
            auto_frames[index].state = MANDELBROT_FRAME_RENDERING;
            auto_frames[index].sequence = auto_next_sequence++;
            auto_frames[index].generation = auto_generation;
            *sequence = auto_frames[index].sequence;
            *generation = auto_frames[index].generation;
            slot = (int)index;
            break;
        }
    }
    portEXIT_CRITICAL(&auto_frame_lock);
    return slot;
}

static void render_auto_frame(mandelbrot_frame_t *frame, uint32_t sequence)
{
    uint8_t *destination = frame->pixels;
    const float width = MANDELBROT_AUTO_START_WIDTH *
                        powf(MANDELBROT_AUTO_ZOOM_PER_FRAME, (float)sequence);
    const float height = width * (float)LCD_HEIGHT / (float)LCD_WIDTH;
    // Move the view centre toward the target at the same exponential rate as width.
    // Frame zero is the recognizable full view; later frames approach the target.
    const float progress = 1.0f - width / MANDELBROT_AUTO_START_WIDTH;
    const float centre_real = MANDELBROT_AUTO_START_REAL +
                              (MANDELBROT_AUTO_TARGET_REAL - MANDELBROT_AUTO_START_REAL) * progress;
    const float centre_imaginary = MANDELBROT_AUTO_START_IMAGINARY +
                                   (MANDELBROT_AUTO_TARGET_IMAGINARY -
                                    MANDELBROT_AUTO_START_IMAGINARY) * progress;
    const float minimum_real = centre_real - width * 0.5f;
    const float minimum_imaginary = centre_imaginary - height * 0.5f;
    const float real_step = width / (float)(LCD_WIDTH - 1U);
    const float imaginary_step = height / (float)(LCD_HEIGHT - 1U);
#if 0 // Maths panel experiment: disabled.
    frame->view = (mandelbrot_view_t){"AUTO", centre_real, centre_imaginary, width};
    // Reproduce the renderer's accumulated X steps at the actual centre pixel.
    float sample_real = minimum_real;
    for (unsigned x = 0U; x < LCD_WIDTH / 2U; ++x) sample_real += real_step;
    frame->sample_real = sample_real;
    frame->sample_imaginary = minimum_imaginary + (float)(LCD_HEIGHT / 2U) * imaginary_step;
#endif
    for (unsigned y = 0U; y < LCD_HEIGHT; ++y) {
        float real = minimum_real;
        const float imaginary = minimum_imaginary + (float)y * imaginary_step;
        for (unsigned x = 0U; x < LCD_WIDTH; ++x) {
            destination[(size_t)y * LCD_WIDTH + x] = calculate_iteration_index(real, imaginary);
            real += real_step;
        }
        // This task owns CPU 1 while rendering; yield regularly so IDLE1 can reset
        // the task watchdog without exposing a partially rendered frame to display.
        if ((y + 1U) % MANDELBROT_AUTO_ROWS_PER_YIELD == 0U) {
            vTaskDelay(pdMS_TO_TICKS(1U));
        }
    }
}

static void automatic_renderer_task(void *unused)
{
    (void)unused;
    for (;;) {
        if (!automaticZoomActive()) {
            // B and Back leave the renderer dormant with no periodic CPU work.
            ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
            continue;
        }
        uint32_t sequence = 0U;
        uint32_t generation = 0U;
        const int slot = take_free_auto_frame(&sequence, &generation);
        if (slot < 0) {
            vTaskDelay(pdMS_TO_TICKS(1U));
            continue;
        }
        const uint32_t render_started = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
        render_auto_frame(&auto_frames[slot], sequence);
        const uint32_t render_finished = (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
        portENTER_CRITICAL(&auto_frame_lock);
        if (automatic_zoom && auto_frames[slot].generation == generation &&
            auto_generation == generation) {
            auto_frames[slot].state = MANDELBROT_FRAME_READY;
            auto_last_render_ms = render_finished - render_started;
            ++auto_rendered_frames;
        } else {
            auto_frames[slot].state = MANDELBROT_FRAME_FREE;
        }
        portEXIT_CRITICAL(&auto_frame_lock);
    }
}

static bool start_automatic_zoom(void)
{
    if (!allocate_auto_buffers()) return false;
    build_auto_palette();
    reset_auto_queue();
    portENTER_CRITICAL(&auto_frame_lock);
    automatic_zoom = true;
    portEXIT_CRITICAL(&auto_frame_lock);
    auto_rendered_frames = 0U;
    auto_displayed_frames = 0U;
    auto_log_started_ms = 0U;
    frame_ready = true;
    if (auto_renderer_task == NULL) {
        const BaseType_t result = xTaskCreatePinnedToCore(automatic_renderer_task,
                                                           "mandel_render", 8192U, NULL, 2U,
                                                           &auto_renderer_task,
                                                           xPortGetCoreID() == 0 ? 1 : 0);
        if (result != pdPASS) {
            portENTER_CRITICAL(&auto_frame_lock);
            automatic_zoom = false;
            portEXIT_CRITICAL(&auto_frame_lock);
            auto_renderer_task = NULL;
            auto_status_text = "AUTO RENDER TASK FAILED";
            ESP_LOGE(TAG, "Unable to start automatic zoom renderer task");
            return false;
        }
    } else {
        // Wake the dormant renderer after B returned the module to manual mode.
        xTaskNotifyGive(auto_renderer_task);
    }
    auto_status_text = "AUTO RENDERING - B MANUAL";
#if 0 // Maths panel experiment: disabled.
    maths_valid = false;
#endif
    ESP_LOGI(TAG, "Automatic zoom started from the full Mandelbrot view");
    return true;
}

static void return_to_manual_view(void)
{
    portENTER_CRITICAL(&auto_frame_lock);
    automatic_zoom = false;
    for (unsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index) {
        if (auto_frames[index].state == MANDELBROT_FRAME_READY) {
            auto_frames[index].state = MANDELBROT_FRAME_FREE;
        }
    }
    portEXIT_CRITICAL(&auto_frame_lock);
    displayed_frame_slot = -1;
    auto_status_text = "B RECEIVED - MANUAL MODE";
    begin_render();
    ESP_LOGI(TAG, "Automatic zoom stopped; manual viewer restored");
}

static int take_ready_auto_frame(void)
{
    int slot = -1;
    uint32_t oldest_sequence = UINT32_MAX;
    portENTER_CRITICAL(&auto_frame_lock);
    if (automatic_zoom) {
        for (unsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index) {
            if (auto_frames[index].state == MANDELBROT_FRAME_READY &&
                auto_frames[index].sequence < oldest_sequence) {
                oldest_sequence = auto_frames[index].sequence;
                slot = (int)index;
            }
        }
        if (slot >= 0) auto_frames[slot].state = MANDELBROT_FRAME_DISPLAYING;
    }
    portEXIT_CRITICAL(&auto_frame_lock);
    return slot;
}

static unsigned ready_auto_frame_count(void)
{
    unsigned count = 0U;
    portENTER_CRITICAL(&auto_frame_lock);
    for (unsigned index = 0U; index < MANDELBROT_AUTO_FRAME_COUNT; ++index) {
        if (auto_frames[index].state == MANDELBROT_FRAME_READY) ++count;
    }
    portEXIT_CRITICAL(&auto_frame_lock);
    return count;
}

static void log_auto_status(uint32_t now_ms)
{
    if (auto_log_started_ms == 0U) auto_log_started_ms = now_ms;
    if (now_ms - auto_log_started_ms < MANDELBROT_AUTO_LOG_INTERVAL_MS) return;
    ESP_LOGI(TAG, "AUTO fps=%u render_fps=%u render=%ums ready=%u psram=%u bytes",
             (unsigned)auto_displayed_frames, (unsigned)auto_rendered_frames,
             (unsigned)auto_last_render_ms, ready_auto_frame_count(),
             (unsigned)(MANDELBROT_AUTO_FRAME_COUNT * LCD_WIDTH * LCD_HEIGHT));
    auto_displayed_frames = 0U;
    auto_rendered_frames = 0U;
    auto_log_started_ms = now_ms;
}

void startMandelbrot(uint32_t now_ms)
{
    running = true;
    select_view(0U);
    last_cursor_move = now_ms;
#if 0 // Maths panel experiment: disabled.
    maths_last_update = now_ms;
#endif
    automatic_zoom = false;
    displayed_frame_slot = -1;
    (void)allocate_manual_staging_frame();
    begin_render();
}

void stopMandelbrot(void)
{
    running = false;
    rendering = false;
    frame_ready = false;
    portENTER_CRITICAL(&auto_frame_lock);
    automatic_zoom = false;
    portEXIT_CRITICAL(&auto_frame_lock);
    release_manual_staging_frame();
}

void mandelbrotAction(bool zoom, bool start_automatic, bool return_to_manual,
                      bool next_view, uint32_t now_ms)
{
    if (!running) return;
    if (return_to_manual && automaticZoomActive()) {
        return_to_manual_view();
        return;
    }
    if (start_automatic) {
        auto_status_text = "C RECEIVED - STARTING AUTO";
        frame_ready = true;
        start_automatic_zoom();
        return;
    }
    if (automaticZoomActive() || (!zoom && !next_view)) return;
    if (next_view) {
        select_view((uint8_t)((selected_view + 1U) % (sizeof(views) / sizeof(views[0]))));
    } else {
        const double height = view_width * (double)LCD_HEIGHT / (double)LCD_WIDTH;
        const double minimum_real = centre_real - view_width * 0.5;
        const double minimum_imaginary = centre_imaginary - height * 0.5;
        centre_real = minimum_real + view_width * (double)(cursor_x + ZOOM_BOX_WIDTH / 2) /
                      (double)(LCD_WIDTH - 1U);
        centre_imaginary = minimum_imaginary + height * (double)(cursor_y + ZOOM_BOX_HEIGHT / 2) /
                           (double)(LCD_HEIGHT - 1U);
        view_width *= (double)ZOOM_BOX_WIDTH / (double)LCD_WIDTH;
        ++zoom_level;
    }
    last_cursor_move = now_ms;
    begin_render();
}

void updateMandelbrot(int x_offset, int y_offset, bool joystick_valid, uint32_t now_ms)
{
    if (!running) return;
#if 0 // Maths panel experiment: disabled.
    // Overlay-only redraws must not copy an old pair of manual render rows.
    row_count = 0U;
    const bool maths_changed = update_maths_sample(now_ms);
    if (maths_changed) frame_ready = true;
#endif
    if (automaticZoomActive()) {
        log_auto_status(now_ms);
        return;
    }
    if (joystick_valid && now_ms - last_cursor_move >= CURSOR_REPEAT_MS) {
        if (x_offset >= JOYSTICK_ENTER) cursor_x += CURSOR_STEP;
        else if (x_offset <= -JOYSTICK_ENTER) cursor_x -= CURSOR_STEP;
        if (y_offset >= JOYSTICK_ENTER) cursor_y -= CURSOR_STEP;
        else if (y_offset <= -JOYSTICK_ENTER) cursor_y += CURSOR_STEP;
        if (cursor_x < 0) cursor_x = 0;
        if (cursor_x > LCD_WIDTH - ZOOM_BOX_WIDTH) cursor_x = LCD_WIDTH - ZOOM_BOX_WIDTH;
        if (cursor_y < 0) cursor_y = 0;
        if (cursor_y > LCD_HEIGHT - ZOOM_BOX_HEIGHT) cursor_y = LCD_HEIGHT - ZOOM_BOX_HEIGHT;
        last_cursor_move = now_ms;
        frame_ready = true;
    }
    if (!rendering) return;
    rendered_start_row = next_row;
    row_count = 0U;
    while (row_count < ROWS_PER_UPDATE && next_row < LCD_HEIGHT) {
        uint16_t *destination = rendered_rows[row_count];
        if (manual_staging_frame != NULL) {
            destination = manual_staging_frame + (size_t)next_row * LCD_WIDTH;
        }
        manual_iteration_count += render_row(next_row, destination);
        ++row_count;
        ++next_row;
    }
    frame_ready = frame_ready || row_count != 0U || clear_requested;
    if (next_row >= LCD_HEIGHT) {
        rendering = false;
        // Keep the 100% screen visible for one frame before showing the fractal.
        manual_reveal_pending = manual_staging_frame != NULL;
    }
}

bool mandelbrotTakeFrame(void)
{
    if (automaticZoomActive()) {
        displayed_frame_slot = take_ready_auto_frame();
        if (displayed_frame_slot >= 0) {
            return true;
        }
        const bool ready = frame_ready;
        frame_ready = false;
        return ready;
    }
    const bool ready = frame_ready;
    frame_ready = false;
    return ready;
}

static void draw_manual_progress(uint16_t *frame)
{
    const unsigned percent = ((unsigned)next_row * 100U) / LCD_HEIGHT;
    const int bar_x = (LCD_WIDTH - MANDELBROT_PROGRESS_BAR_WIDTH) / 2;
    const int bar_y = 102;
    const int inner_width = MANDELBROT_PROGRESS_BAR_WIDTH - 2;
    const int filled_width = (inner_width * (int)percent) / 100;
    char text[32];

    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 20, "MANDELBROT", COLOR_LIME, 3);
    drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 51, "Z=Z*Z+C", COLOR_LIGHTGREEN, 2);
    drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 76, "CALCULATING", COLOR_GREENYELLOW, 2);
    draw_rect(frame, bar_x, bar_y, MANDELBROT_PROGRESS_BAR_WIDTH,
              MANDELBROT_PROGRESS_BAR_HEIGHT, COLOR_CRIMSON);
    if (filled_width > 0) {
        fill_rect(frame, bar_x + 1, bar_y + 1, filled_width,
                  MANDELBROT_PROGRESS_BAR_HEIGHT - 2, COLOR_RED);
    }
    snprintf(text, sizeof(text), "%u%%", percent);
    drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 119, text, COLOR_LIME, 2);
    snprintf(text, sizeof(text), "ITERATIONS %lu", (unsigned long)manual_iteration_count);
    drawPet2001TextCentredTransparent(frame, LCD_WIDTH / 2, 146, text, COLOR_LIGHTGREEN, 1);
}

void drawMandelbrot(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (automaticZoomActive()) {
        if (displayed_frame_slot < 0) {
            fill_rect(frame, 0, 0, LCD_WIDTH, 12, COLOR_BLACK);
            drawPet2001TextTransparent(frame, 2, 2, auto_status_text, COLOR_WHITE, 1);
            return;
        }
        const mandelbrot_frame_t *displayed = &auto_frames[displayed_frame_slot];
        const uint8_t *source = displayed->pixels;
        for (size_t pixel = 0U; pixel < (size_t)LCD_WIDTH * LCD_HEIGHT; ++pixel) {
            frame[pixel] = auto_palette[source[pixel]];
        }
#if 0 // Maths panel experiment: disabled.
        // Copy metadata before releasing the slot for the background renderer.
        set_maths_sample(displayed->view, displayed->sample_real,
                         displayed->sample_imaginary, true);
#endif
        portENTER_CRITICAL(&auto_frame_lock);
        auto_frames[displayed_frame_slot].state = MANDELBROT_FRAME_FREE;
        portEXIT_CRITICAL(&auto_frame_lock);
        displayed_frame_slot = -1;
        ++auto_displayed_frames;
        return;
    }
#if 0 // Maths panel experiment: disabled.
    if (!clear_requested && row_count == 0U && cursor_drawn &&
        cursor_x == drawn_cursor_x && cursor_y == drawn_cursor_y) {
        // A maths-only tick does not need to recalculate the cursor border pixels.
        draw_maths_panel(frame);
        return;
    }
#endif
    if (manual_staging_frame != NULL) {
        if (rendering || manual_reveal_pending) {
            draw_manual_progress(frame);
            if (manual_reveal_pending) {
                manual_reveal_pending = false;
                frame_ready = true;
            }
            return;
        }
        memcpy(frame, manual_staging_frame, MANDELBROT_MANUAL_FRAME_BYTES);
        clear_requested = false;
    } else {
        if (clear_requested) {
            clear(frame, COLOR_BLACK);
            clear_requested = false;
        }
        for (uint8_t row = 0U; row < row_count; ++row) {
            memcpy(frame + (size_t)(rendered_start_row + row) * LCD_WIDTH,
                   rendered_rows[row], sizeof(rendered_rows[row]));
        }
    }
    if (cursor_drawn) restore_cursor_border(frame, drawn_cursor_x, drawn_cursor_y);
    char status[40];
    snprintf(status, sizeof(status), "%s Z%u", views[selected_view].name,
             (unsigned)zoom_level);
    fill_rect(frame, 0, 0, 128, 10, COLOR_BLACK);
    drawPet2001TextTransparent(frame, 2, 1, status, COLOR_WHITE, 1);
    draw_rect(frame, cursor_x, cursor_y, ZOOM_BOX_WIDTH, ZOOM_BOX_HEIGHT, COLOR_WHITE);
    drawn_cursor_x = cursor_x;
    drawn_cursor_y = cursor_y;
    cursor_drawn = true;
    drawPet2001TextTransparent(frame, 2, 148, "A/K ZOOM  C AUTO  E VIEW", COLOR_WHITE, 1);
    drawPet2001TextTransparent(frame, 2, 158, "B MANUAL  BOOT BACK", COLOR_WHITE, 1);
    if (auto_status_text != NULL) {
        fill_rect(frame, 0, 12, LCD_WIDTH, 10, COLOR_BLACK);
        drawPet2001TextTransparent(frame, 2, 12, auto_status_text, COLOR_WHITE, 1);
    }
#if 0 // Maths panel experiment: disabled.
    // Compose last so manual rows, status text and cursor restoration stay below it.
    draw_maths_panel(frame);
#endif
}