[OS X] Multithreading rendering

Hello all,

maybe this question has been already addressed, but I am not able to find an answer.
I am trying to plot as many pixels as possible on a single window to create a simple 3D star field in real time.
I am using SDL_RenderDrawPoint(), but I quickly find out a CPU limitation (only one core was used). So I started to use threads in order to use several CPU cores to do the rendering, and he is my issue :

  • when 2 threads are trying to concurrently use the same renderer, I have this error :
Thread X : signal SIGABRT
**malloc: double free for ptr 0x1180bfc00**

Here is the part of my code rendering the pixels :

// Render part of the stars
void* renderStars(void* range) {
	struct Range r = *(struct Range *)range;
	for(int i = r.start; i < r.end; i++) {
		int x = stars_coord[i][0];
		int y = stars_coord[i][1];
		int z = stars_coord[i][2];
		// Choose rendering color
		int col = 255 - (255 * z) / 512;
		SDL_SetRenderDrawColor(r.renderer, col, col, col, 255);
		// Apply rotation
		int xa = x * cos(a) - y  * sin(a);
		int ya = x * sin(a) + y * cos(a);
		int px = windowWidth/2 + (128 * xa / z);
		int py = windowHeight/2 + (128 * ya / z);
		SDL_RenderDrawPoint(r.renderer, px, py);

		// Move stars
		z = z - 1;
		if(z == 0)
			z = 512;
		stars_coord[i][2] = z;
	}
	pthread_exit(NULL);
}

The program is crashing at the SDL_RenderDrawPoint(r.renderer, px, py); line with the double free error.

I am not able to find out where I made a mistake ? Is it possible that SDL does not allow for two threads to access the same renderer concurrently to draw pixels ? How could I manage to have two CPU cores rendering two parts of the window for exemple ?

Thanks for your help !

1 Like

trying to use SDL_Renderer with multiple threads is probably the wrong way to try and increase performance here. Assuming you are using hardware acceleration, the bottle next is going to be pushing the data to the GPU.

Here are a couple of ideas to get better performance:

  1. draw all the stars of the same color at once using SDL_RenderDrawPoints (you could use multiple threads to coalesce each group as long as you lock access to the renderer)
  2. use SDL_RenderGeometryRaw to coalesce all stars into a single vertex buffer with their colors
1 Like

Hello Brada,

thanks for your answer, so you are confirming that accessing the same renderer in two separate threads could not work ? So I have to do the calculation in multiple threads, but the final rendering in the main one ?
This not obvious to me, because does it mean that :

for(int i=0; i < 100000; i++) {
SDL_RenderDrawPoint(r.renderer, px[i], py[i]);
}

is no slower than two threads doing half of the pixels each :
Thread 1

for(int i=0; i < 50000; i++) {
SDL_RenderDrawPoint(r.renderer, px[i], py[i]);
}

Thread 2

for(int i=50000; i < 100000; i++) {
SDL_RenderDrawPoint(r.renderer, px[i], py[i]);
}

I thought that two threads should be faster ?

you could presumably render from multiple threads by adding a lock for the renderer. However, its probably obvious that doing so would yield no benefit as all your threads would be constantly stalled waiting for the one with the lock. You would just end up with a lot of lock thrashing that would actually hurt your performance.

As I said before, trying to do this from multiple threads is likely to be more time and hair pulling for not much if any gains. If you insist on it, you need to split up the work in a way that doesnt require locking such as building collections of SDL_Point and then having the main thread take those and call SDL_RenderDrawPoint**s** for each collection.

I personally think using a single thread with SDL_RenderDrawPoints would be the best approach if the drawing is changing every frame. If this is just a background you are drawing that never changes then just render to a texture one time.

1 Like

Using SDL_Render in multiple or non-main threads isn’t supported at all (as per the header documentation https://github.com/libsdl-org/SDL/blob/main/include/SDL_render.h#L44-L45 ) - adding locks won’t allow it to work on all backends, because some graphics APIs – for example OpenGL and OpenGL ES – have global thread-local instances. SDL_Render’s internals may also be called from inside SDL event handler code, which is also restricted to the main thread on some platforms.

2 Likes

That’s exactly what I do (well, in fact, I use SDL_RenderFillRects() because my stars are bigger than one pixel). To maximise the benefit of this approach I use relatively few different colors (just 8 in my case); the difference is hardly noticeable compared with every star having a different color or brightness.

1 Like

Thanks all for your help, I am working on this solution and will keep you informed !

How many stars are you able to show in real time in one frame ? At the moment I am stuck around 10.000 3D stars with one axis rotation and one translation on the Z axis. This figure seems very loo to me.

OMG !! I used SDL_RenderDrawPointS and now I am able to render up to 300.000 stars in real time !!!
Now my CPU is at 97% and my GPU at 6%, so I guess that the limit is the CPU, I will try to use multithreading now for the computation to see if can break this limit…

1 Like

Thanks all for your help, now with two threads I can render 400.000 stars in real time.
Here is the code (which I compiled successfully on Windows and OS X) :

//
//  main.c
//  3D Stars
//
//  Created by Fabrice on 22/10/2021.
//

#include "SDL.h"
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <pthread.h>
#include <math.h>

// Window size
static const int windowWidth = 1920;
static const int windowHeight = 1080;

// Nb of threads
#define nbThreads 2

// Stars
#define nbStars 300000
#define nbColors 8
int stars_3Dcoord[nbStars][3];
SDL_Point stars_2Dcoord[nbThreads][nbColors][nbStars];
int nbStarsByPlayfield[nbThreads][nbColors];

// Rotation angle
double a = 0;

// Args for rendering thread
struct Range{
	int start;
	int end;
	int threadNb;
};

// Return a random number between 0 and limit inclusive.
int rand_lim(int limit) {
	int divisor = RAND_MAX/(limit+1);
	int retval;

	do {
		retval = rand() / divisor;
	} while (retval > limit);

	return retval;
}

// Return a random number in the range [minVal, maxVal]
int randomRange(int minVal, int maxVal) {
	return rand_lim(maxVal - minVal -1) + minVal;
}

// Render part of the stars
void* renderStars(void* range) {
	struct Range r = *(struct Range *)range;
	for(int i = r.start; i < r.end; i++) {
		int x = stars_3Dcoord[i][0];
		int y = stars_3Dcoord[i][1];
		int z = stars_3Dcoord[i][2];
		// Star color
		int col = (nbColors-1)  - ((nbColors-1)* z) / 512;

		// Apply rotation
		int xa = x * cos(a) - y  * sin(a);
		int ya = x * sin(a) + y * cos(a);

		// Apply 2D projection
		int px = windowWidth/2 + (128 * xa / z);
		int py = windowHeight/2 + (128 * ya / z);
		stars_2Dcoord[r.threadNb][col][nbStarsByPlayfield[r.threadNb][col]] = (SDL_Point) {px , py};
		nbStarsByPlayfield[r.threadNb][col]++;

		// Move stars along Z axis
		z = z - 1;
		if(z == 0)
			z = 512;
		stars_3Dcoord[i][2] = z;
	}
	pthread_exit(NULL);
	return(NULL);
}

int main(int argc, char *argv[]) {
	// Init random generator
	srand((unsigned int)time(NULL));

	// Generate random stars coordinates
	for(int i=0; i < nbStars; i++) {
		stars_3Dcoord[i][0] = randomRange((-1 * windowWidth/2), windowWidth/2);
		stars_3Dcoord[i][1] = randomRange((-1 * windowHeight/2), windowHeight/2);
		stars_3Dcoord[i][2] = randomRange(1, 512);
	}

	// Init SDL
	SDL_Init(SDL_INIT_VIDEO);
	SDL_Window *window = SDL_CreateWindow("SDL Test", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, windowWidth, windowHeight, SDL_WINDOW_SHOWN);
	SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED + SDL_RENDERER_PRESENTVSYNC);

	// Prepare threads
	pthread_t id[nbThreads];
	const int rangeSize = nbStars / nbThreads;
	struct Range range[nbThreads];
	for(int i=0; i < nbThreads; i++) {
		range[i].start = i * rangeSize;
		range[i].end = range[i].start + rangeSize - 1;
		range[i].threadNb = i;
	}

	// Show window and wait for quit
	SDL_Event event;
	bool quit = false;
	while (quit == false) {
		// Clear screen
		SDL_SetRenderDrawColor(renderer, 0, 0, 30, 0);
		SDL_RenderClear(renderer);
	
		// Reset nbStarsByPlayfield
		for(int i=0; i < nbThreads; i++) {
			for(int j=0; j < nbColors; j++) {
				nbStarsByPlayfield[i][j]=0;
			}
		}

		// Create threads to plot stars
		for(int i=0; i < nbThreads; i++) {
			pthread_create(&id[i], NULL, renderStars, &range[i]);
		}
	
		// Wait for all threads to finish
		for(int i=0; i < nbThreads; i++) {
			pthread_join(id[i], NULL);
		}
	
		// Draw stars
		for(int i=0; i < nbThreads; i++) {
			for(int j=0; j < nbColors; j++) {
				int col = (256 / nbColors) * j;
				SDL_SetRenderDrawColor(renderer, col, col, col, 255);
				SDL_RenderDrawPoints(renderer, stars_2Dcoord[i][j], nbStarsByPlayfield[i][j]);
			}
		}
		// Show renderer
		SDL_RenderPresent(renderer);

		// Rotate
		//a = a + 0.008;
		if(a >= 2 * M_PI)
			a = 2 * M_PI - a;
	
		while(SDL_PollEvent(&event)) {
			if(event.type == SDL_QUIT) {
				quit = true;
			}
		}
		SDL_Delay(2);
	}

	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);
	SDL_Quit();
	return 0;
}

There is still one issue left, but it has nothing to do with SDL : there is a graphical glitch on the X-axis and Y-axis. The stars which are shown on the X-axis and Y-axis (so when x=0 or y=0 for a star), then their position seems to be inaccurate (we can see a cross on the screen). It has to do with the 3D projection I think, but I can not figure out where the issue is yet…