I'm developing a fractal explorer in C++ using SFML and std::thread to render the Mandelbrot set on the CPU with progressive display. The goal is to leverage multiple cores by dividing the image into horizontal strips, with each strip rendered by a separate thread. User responsiveness is important, meaning interactions like zoom or pan should ideally interrupt the current render and start a new one.
While implementing the progressive rendering logic, I've encountered a visual artifact that is consistently reproducible in a minimal example.
The Observed Artifact:
During the rendering process, which is intended to fill the image strip by strip, the output doesn't show solid bands of computed pixels. Instead, within each thread's assigned horizontal region, the fractal structure appears as thin, disconnected horizontal lines, separated by black gaps.
This is not the expected smooth filling of the horizontal work area. A screenshot illustrating this specific, consistent artifact can be seen here:
https://i.sstatic.net/82LBOVUT.png
What I find particularly perplexing is how consistently this artifact manifests. It doesn't seem to be a transient "tearing" or a typical race condition; the pattern is reliably present every time I run the code with the same parameters.
#include <SFML/Graphics.hpp>
#include <iostream>
#include <vector>
#include <thread>
#include <chrono>
#include <cstring>
struct render_target {
unsigned int x_start, y_start, x_end, y_end;
render_target(unsigned int xs, unsigned int ys, unsigned int xe, unsigned int ye)
: x_start(xs), y_start(ys), x_end(xe), y_end(ye) {}
};
unsigned char* pixels = nullptr;
const unsigned int render_width = 800;
const unsigned int render_height = 600;
const unsigned int buffer_width = render_width;
const unsigned int buffer_height = render_height;
const double zoom_x = 240.0;
const double zoom_y = 240.0;
const double x_offset = 2.25;
const double y_offset = 1.25;
const unsigned int max_iterations = 300;
std::vector<unsigned char> thread_stop_flags; // 0: running, 1: stop requested, 2: stopped
sf::Texture texture;
sf::Sprite sprite(texture);
sf::Image image;
void cpu_render_minimal(render_target target, unsigned char* pixels, unsigned int width_param, unsigned int height_param,
double zoom_x, double zoom_y, double x_offset, double y_offset,
unsigned int max_iterations,
unsigned char& finish_flag)
{
finish_flag = 0;
for(unsigned int y = target.y_start; y < target.y_end; ++y){
for(unsigned int x = target.x_start; x < target.x_end; ++x){
double zr = 0.0;
double zi = 0.0;
double cr = x / zoom_x - x_offset;
double ci = y / zoom_y - y_offset;
unsigned int curr_iter = 0;
while (curr_iter < max_iterations && zr * zr + zi * zi < 4.0) {
double tmp_zr = zr;
zr = zr * zr - zi * zi + cr;
zi = 2.0 * tmp_zr * zi + ci;
++curr_iter;
if(finish_flag == 1) {
finish_flag = 2;
return;
}
}
unsigned char color_val;
if (curr_iter == max_iterations) {
color_val = 255;
} else {
color_val = static_cast<unsigned char>((curr_iter % 255) + 1);
}
const unsigned int index = (y * width_param + x) * 4;
if (index + 3 < buffer_width * buffer_height * 4) {
pixels[index] = color_val;
pixels[index + 1] = color_val;
pixels[index + 2] = color_val;
pixels[index + 3] = 255;
}
}
}
finish_flag = 1;
}
void post_processing_minimal() {
if (!pixels) return;
image = sf::Image({render_width, render_height}, pixels);
texture = sf::Texture(image, true);
sprite = sf::Sprite(texture);
}
void start_render_job()
{
if (pixels != nullptr) {
delete[] pixels;
pixels = nullptr;
}
pixels = new unsigned char[buffer_width * buffer_height * 4];
if (!pixels) {
std::cerr << "Error: Could not allocate pixel buffer!" << std::endl;
return;
}
memset(pixels, 0, buffer_width * buffer_height * 4);
unsigned int max_threads = std::thread::hardware_concurrency();
if (max_threads == 0) max_threads = 1;
thread_stop_flags.assign(max_threads, 0);
std::vector<render_target> render_targets;
unsigned int strip_height = render_height / max_threads;
for(unsigned int i = 0; i < max_threads; ++i) {
unsigned int x_start = 0;
unsigned int x_end = render_width;
unsigned int y_start = strip_height * i;
unsigned int y_end = (i == max_threads - 1) ? render_height : strip_height * (i + 1);
if (y_start >= y_end) continue;
render_targets.emplace_back(x_start, y_start, x_end, y_end);
}
for(size_t i = 0; i < render_targets.size(); ++i) {
std::thread t(cpu_render_minimal, render_targets[i], pixels,
render_width, render_height,
zoom_x, zoom_y, x_offset, y_offset,
max_iterations, std::ref(thread_stop_flags[i]));
t.detach();
}
std::cout << "Started render job with " << render_targets.size() << " threads." << std::endl;
std::cout << "Buffer dimensions: " << buffer_width << "x" << buffer_height << std::endl;
std::cout << "Render dimensions passed to threads: " << render_width << "x" << render_height << std::endl;
}
int main() {
sf::RenderWindow window(sf::VideoMode({render_width, render_height}), "Mandelbrot MRE");
window.setFramerateLimit(60);
image = sf::Image({render_width, render_height}, sf::Color::Black);
texture = sf::Texture(image);
sprite.setTexture(texture);
start_render_job();
while(window.isOpen()){
while(const auto event = window.pollEvent()) {
if(event->is<sf::Event::Closed>())
window.close();
if (event->is<sf::Event::KeyPressed>() && event->getIf<sf::Event::KeyPressed>()->scancode == sf::Keyboard::Scancode::Space) {
std::cout << "Space pressed. Simulating render restart attempt..." << std::endl;
start_render_job();
if (pixels) memset(pixels, 0, buffer_width * buffer_height * 4);
}
}
post_processing_minimal();
window.clear(sf::Color::Black);
window.draw(sprite);
window.display();
static bool render_finished = false;
if (!render_finished && !thread_stop_flags.empty() &&
std::all_of(thread_stop_flags.begin(), thread_stop_flags.end(),
[](unsigned char state){ return state == 1 || state == 2; }))
{
std::cout << "All threads finished (or stopped)." << std::endl;
render_finished = true;
}
}
if (pixels) {
delete[] pixels;
pixels = nullptr;
}
return 0;
}
Associated CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
project(MandelbrotMRE LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_TOOLCHAIN_FILE "$ENV{HOME}/vcpkg/scripts/buildsystems/vcpkg.cmake" CACHE STRING "Vcpkg toolchain file")
find_package(SFML 3 COMPONENTS Graphics Window System REQUIRED)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
add_compile_options(-Wall -Wextra -pedantic -fPIE)
elseif(MSVC)
add_compile_options(/W4 /MP /std:c++latest)
endif()
set(CPP_SOURCES main.cpp)
add_executable(${PROJECT_NAME} ${CPP_SOURCES})
target_link_libraries(${PROJECT_NAME} PRIVATE
SFML::Graphics
SFML::Window
SFML::System
)
Environment Details:
- OS: Arch Linux
- C++ Compiler: G++
- SFML Version: 3
- CMake Version: 3.20+
Specific Questions:
Why does this specific, consistent fragmented artifact appear during progressive rendering in this multithreaded CPU code?
How can this artifact be reliably fixed?
memsetin yourmainfunctionif (pixels != nullptr) { delete[] pixels;-- Possibly off-topic, but there is no need to check fornullptrwhen issuing adelete[]call. Callingdelete[]on anullptris perfectly OK, as that will result in a no-op.memsetinmainand you're fine for a while, so long as the user doesn't hold down space for a long time.