Estou desenvolvendo um explorador fractal em C++ usando SFML e std::thread
renderizando o conjunto de Mandelbrot na CPU com exibição progressiva. O objetivo é aproveitar múltiplos núcleos, dividindo a imagem em faixas horizontais, com cada faixa renderizada por uma thread separada. A responsividade do usuário é importante, o que significa que interações como zoom ou panorâmica devem, idealmente, interromper a renderização atual e iniciar uma nova.
Ao implementar a lógica de renderização progressiva, encontrei um artefato visual que é consistentemente reproduzível em um exemplo mínimo.
O Artefato Observado:
Durante o processo de renderização, que visa preencher a imagem faixa por faixa, a saída não mostra faixas sólidas de pixels computados. Em vez disso, dentro da região horizontal atribuída a cada thread, a estrutura fractal aparece como linhas horizontais finas e desconexas, separadas por espaços pretos.
Este não é o preenchimento suave esperado da área de trabalho horizontal. Uma captura de tela ilustrando esse artefato específico e consistente pode ser vista aqui:
https://isstatic.askoverflow.dev/82LBOVUT.png
O que considero particularmente intrigante é a consistência com que esse artefato se manifesta. Não parece ser um "rasgo" transitório ou uma condição de corrida típica; o padrão está presente de forma confiável sempre que executo o código com os mesmos parâmetros.
#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;
}
CMakeLists.txt associado:
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
)
Detalhes do ambiente:
- SO: Arch Linux
- Compilador C++: G++
- Versão SFML: 3
- Versão do CMake: 3.20+
Perguntas específicas:
Por que esse artefato fragmentado específico e consistente aparece durante a renderização progressiva neste código de CPU multithread?
Como esse artefato pode ser corrigido de forma confiável?