I'm refactoring a raytracer to have a window pop up that shows each scanline produced instead of having to wait for the ppm image to be rendered and open it with some ppm viewer.
I've been advised to use SDL3. How do I make it render each scanline that is produced (e.g. implemented as a std::vector<Color>
)? What functionalities should I look for?
In practice i' ve got the rendering part like
void Camera::render() {
std::cout << "P3\n" << _img_width << ' ' << _img_height << "\n255\n";
for(uint32_t j = 0; j < _img_height; ++j) {
for (uint32_t i = 0; i < _img_width; ++i) {
Color pixel_color;
Ray r = get_ray(i, j);
pixel_color += ray_color(r);
write_color(pixel_color);
}
}
}
pixel_color
is a vec3
and so i wanted to print them as soon as i complete one line.
I' m editing and the question because i think the answer provided might be useful for others.
In the answer a possible std::vector<Color>
to represent the row pixels is replaced by a uint32_t*
which will store the colors in rgba format but that's just a matter of codifying the results, it is a general approach that would work in multiple cases.
you normally need 2 threads, one to do the raytracing and one to handle window events, i will give this answer to show one way threading can be done without crashing because SDL_Renderer
functions can only be called from the main thread.
#include "SDL3/SDL.h"
#define SDL_MAIN_USE_CALLBACKS
#include "SDL3/SDL_main.h"
#include <queue>
#include <mutex>
#include <optional>
#include <random>
#include <thread>
std::atomic<bool> quit_app = false;
constexpr int width = 800;
constexpr int height = 600;
template <typename T>
class SimpleQueue
{
public:
void push(T val)
{
std::lock_guard g{ m_mutex };
m_queue.push(std::move(val));
}
std::optional<T> try_pop()
{
std::optional<T> result;
{
std::lock_guard g{ m_mutex };
if (m_queue.size())
{
result.emplace(std::move(m_queue.front()));
m_queue.pop();
}
}
return result;
}
private:
std::queue<T> m_queue;
std::mutex m_mutex;
};
struct ScanLine
{
int y_pos;
std::vector<uint32_t> values;
};
// compute scanlines and push them on the queue
void worker_task(SimpleQueue<ScanLine>& queue)
{
int y = 0;
std::mt19937 engine;
std::uniform_int_distribution dist{};
uint32_t value = 0xFFFFFFFF;
while (!quit_app)
{
// sleep for few millisecond
std::this_thread::sleep_for(std::chrono::milliseconds{ 5 });
// do some work
if (y >= height)
{
y = 0;
value = dist(engine);
}
std::vector<uint32_t> vec(width, value);
// push result to queue
queue.push({ y, std::move(vec) });
y++;
}
}
struct AppContext
{
SDL_Window* window = nullptr;
SDL_Renderer* renderer = nullptr;
std::thread worker;
SimpleQueue<ScanLine> queue;
SDL_Surface* screen_surface = nullptr;
};
// replaces main, called on startup
SDL_AppResult SDL_AppInit(void** appstate, int argc, char** argv)
{
AppContext* context = new AppContext{};
*appstate = context;
bool success = SDL_Init(SDL_INIT_VIDEO);
if (!success)
{
SDL_Log("Init failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
success = SDL_CreateWindowAndRenderer("My Window!", width, height, SDL_WINDOW_RESIZABLE, &context->window, &context->renderer);
if (!success)
{
SDL_Log("Create Window Failed!: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
// set VSYNC
success = SDL_SetRenderVSync(context->renderer, 1);
if (!success)
{
SDL_Log("Failed to set VSync: %s", SDL_GetError());
}
// create worker
context->worker = std::thread{ [context]() { worker_task(context->queue); } };
// create a dark surface
context->screen_surface = SDL_CreateSurface(width, height, SDL_PIXELFORMAT_RGBA32);
return SDL_APP_CONTINUE;
}
// called every frame
SDL_AppResult SDL_AppIterate(void* appstate)
{
AppContext* context = (AppContext*)appstate;
SDL_RenderClear(context->renderer);
// update surface from other thread data
while (auto line = context->queue.try_pop())
{
uint32_t* pixels = static_cast<uint32_t*>(context->screen_surface->pixels);
size_t begin_pixel = context->screen_surface->pitch / sizeof(uint32_t) * line->y_pos;
for (size_t i = 0; i < line->values.size(); i++)
{
pixels[begin_pixel + i] = line->values[i];
}
}
// render surface to screen
SDL_Texture* tex = SDL_CreateTextureFromSurface(context->renderer, context->screen_surface);
SDL_RenderTexture(context->renderer, tex, nullptr, nullptr);
SDL_DestroyTexture(tex);
SDL_RenderPresent(context->renderer);
return SDL_APP_CONTINUE;
}
// event handling here
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{
if (event->type == SDL_EVENT_QUIT)
{
quit_app = true;
return SDL_APP_SUCCESS;
}
return SDL_APP_CONTINUE;
}
// called when app ends, cleanup
void SDL_AppQuit(void* appstate, SDL_AppResult result)
{
AppContext* context = (AppContext*)appstate;
// clean-up
if (context->worker.joinable())
{
// this should be join not detach ... but it is up to you
context->worker.detach();
}
if (context->screen_surface)
{
SDL_DestroySurface(context->screen_surface);
}
if (context->renderer)
{
SDL_DestroyRenderer(context->renderer);
}
if (context->window)
{
SDL_DestroyWindow(context->window);
}
delete context;
}
some people would use SDL_Texture
with SDL_LockTexture instead of SDL_Surface
, which could be faster, but it breaks under a few configurations. (for example windows, on DirectX driver, with resizing, need a lot of code to handle context recreation), and you don't really need that much performance for this anyway, this already uses < 1% CPU.