Skip to content

Instantly share code, notes, and snippets.

@mrbid
Created December 12, 2024 23:05
Show Gist options
  • Save mrbid/3ffc6984d0dbdb35fea27ae16faa8bab to your computer and use it in GitHub Desktop.
Save mrbid/3ffc6984d0dbdb35fea27ae16faa8bab to your computer and use it in GitHub Desktop.
Multi-threaded CPU Ray-Trace 3D Renderer for X11
/*
Test_User (notabug.org/Test_User)
October 2024
Notes: -DNOSSE because it doesn't work on other arches otherwise.
Requires: vec.h: https://gist.github.com/mrbid/77a92019e1ab8b86109bf103166bd04e
Compile: $(CC) $(CFLAGS) cpurender.c -lX11 -lXext -lm -DNOSSE -o CPURender
*/
#include <sys/shm.h>
#include <X11/Xutil.h>
#include <X11/extensions/XShm.h>
#include <math.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define WIDTH 1920
#define HEIGHT 1080
size_t NUM_THREADS = 1;
#include <stdio.h>
#include <time.h>
#include <sys/time.h>
#include "vec.h"
size_t microtime(void) {
struct timeval tv;
struct timezone tz = {0};
gettimeofday(&tv, &tz);
return ((size_t)tv.tv_sec * 1000000) + (size_t)tv.tv_usec;
}
struct color {
unsigned char r;
unsigned char g;
unsigned char b;
unsigned char a;
};
struct sphere {
vec pos;
float radius;
struct color color;
// animation stuff
float rotation_offset;
};
volatile struct sphere volatile_spheres[] = { // inhibit compiler optimizations
{
{0.f, 0.f, 0.f}, // pos
0.8f, // radius
{255, 0, 0}, // color
},
{
{0.f, 0.f, 0.f}, // pos, (x, y) overwritten by rotation
0.1f, // radius
{0, 255, 0}, // color
0, // rotation offset
},
{
{0.f, 0.f, 0.f}, // pos, (x, y) overwritten by rotation
0.1f, // radius
{0, 127, 127}, // color
M_PI/2, // rotation offset
},
{
{0.f, 0.f, 0.f}, // pos, (x, y) overwritten by rotation
0.1f, // radius
{127, 0, 127}, // color
M_PI, // rotation offset
},
{
{0.f, 0.f, 0.f}, // pos, (x, y) overwritten by rotation
0.1f, // radius
{0, 0, 255}, // color
M_PI*3/2, // rotation offset
},
};
#define NUM_SPHERES (sizeof(volatile_spheres)/sizeof(volatile_spheres[0]))
struct sphere spheres[NUM_SPHERES];
volatile vec volatile_viewpos = {0.f, 1.f, -10.f};
vec viewpos;
volatile vec volatile_viewdir[3] = {
{0.f, 0.f, 1.f}, // forward
{0.f, 1.f, 0.f}, // up
{1.f, 0.f, 0.f}, // right
};
volatile vec viewdir[3];
sem_t threads_started; // yay
pthread_rwlock_t render_start; // main to threads: start rendering
pthread_rwlock_t render_complete; // threads to main: unlocked when rendering has been completed, locked before render_start
pthread_rwlock_t render_next; // main to threads: prepare for next render
pthread_rwlock_t render_ready; // threads to main: threads are ready to begin rendering
pthread_attr_t pthread_attr;
XImage *ximage;
int ray(vec pos, vec direction, struct color *color) {
float a = vMag(direction);
char found = 0;
float dist;
size_t sphere_index;
for (size_t i = 0; i < NUM_SPHERES; i++) {
float radius_sq = spheres[i].radius * spheres[i].radius;
vec Q = {pos.x - spheres[i].pos.x, pos.y - spheres[i].pos.y, pos.z - spheres[i].pos.z};
float Qmag = vMag(Q);
float b = vDot(direction, Q);
if (b > 0.f)
continue;
float c = Qmag - radius_sq;
if (c < 0)
continue;
float d = b*b - a*c;
if (d < 0.f)
continue;
float this_dist = (-b - sqrtf(d))/a;
if (!found || this_dist < dist) {
found = 1;
dist = this_dist;
sphere_index = i;
}
}
if (found) {
// TODO: Mix properly
if (color->a == 0) {
color->a = 255;
vec hit_point = {
pos.x + direction.x * dist,
pos.y + direction.y * dist,
pos.z + direction.z * dist,
};
vec forward = {
(hit_point.x - spheres[sphere_index].pos.x) / spheres[sphere_index].radius,
(hit_point.y - spheres[sphere_index].pos.y) / spheres[sphere_index].radius,
(hit_point.z - spheres[sphere_index].pos.z) / spheres[sphere_index].radius,
};
float diff = 2*vDot(direction, forward);
vec new_dir = {direction.x - diff*forward.x, direction.y - diff*forward.y, direction.z - diff*forward.z};
ray(hit_point, new_dir, color);
color->r /= 3;
color->g /= 3;
color->b /= 3;
color->r += spheres[sphere_index].color.r * 2 / 3;
color->g += spheres[sphere_index].color.g * 2 / 3;
color->b += spheres[sphere_index].color.b * 2 / 3;
} else {
*color = spheres[sphere_index].color;
}
} else {
float largest = direction.x;
if (direction.y > largest)
largest = direction.y;
if (direction.z > largest)
largest = direction.z;
*color = (struct color){255.f * direction.x / largest, 255.f * direction.y / largest, 255.f * direction.z / largest};
}
return found;
}
void * thread(void *arg) {
size_t index = *((size_t*)arg);
free(arg);
pthread_rwlock_rdlock(&render_ready);
sem_post(&threads_started);
while (1) {
pthread_rwlock_rdlock(&render_complete);
pthread_rwlock_unlock(&render_ready);
pthread_rwlock_rdlock(&render_start);
pthread_rwlock_unlock(&render_start);
pthread_rwlock_rdlock(&render_ready);
size_t overflow = (HEIGHT * WIDTH) % NUM_THREADS;
size_t start;
if (overflow < index)
start = (((HEIGHT * WIDTH) / NUM_THREADS) * index) + overflow;
else
start = (((HEIGHT * WIDTH) / NUM_THREADS) * index) + index;
size_t end;
if (overflow <= index)
end = start + ((HEIGHT * WIDTH) / NUM_THREADS);
else
end = start + ((HEIGHT * WIDTH) / NUM_THREADS) + 1;
for (size_t i = start; i < end; i++) {
long x = (i % WIDTH) - WIDTH/2;
long y = (i / WIDTH) - HEIGHT/2;
vec direction_offset = {(float)x / WIDTH, (float)-y / WIDTH, 1};
vec direction;
direction.x = direction_offset.x * volatile_viewdir[2].x; // right
direction.x += direction_offset.y * volatile_viewdir[1].x; // up
direction.x += direction_offset.z * volatile_viewdir[0].x; // forward
direction.y = direction_offset.x * volatile_viewdir[2].y; // right
direction.y += direction_offset.y * volatile_viewdir[1].y; // up
direction.y += direction_offset.z * volatile_viewdir[0].y; // forward
direction.z = direction_offset.x * volatile_viewdir[2].z; // right
direction.z += direction_offset.y * volatile_viewdir[1].z; // up
direction.z += direction_offset.z * volatile_viewdir[0].z; // forward
struct color color = {0, 0, 0};
ray(viewpos, direction, &color);
// XPutPixel(ximage, x + WIDTH/2, y + HEIGHT/2, (color.r << 16) + (color.g << 8) + color.b);
ximage->data[((x + WIDTH/2) * 4) + ((y + HEIGHT/2) * ximage->bytes_per_line)] = color.b;
ximage->data[((x + WIDTH/2) * 4) + ((y + HEIGHT/2) * ximage->bytes_per_line) + 1] = color.g;
ximage->data[((x + WIDTH/2) * 4) + ((y + HEIGHT/2) * ximage->bytes_per_line) + 2] = color.r;
}
pthread_rwlock_unlock(&render_complete);
pthread_rwlock_rdlock(&render_next);
pthread_rwlock_unlock(&render_next);
}
return 0;
}
int main(int argc, char **argv) {
if (argc > 1)
sscanf(argv[1], "%zu", &NUM_THREADS);
Display *display = XOpenDisplay(NULL);
if (!display)
return 0x01;
XShmSegmentInfo shminfo = {0};
int screen = XDefaultScreen(display);
ximage = XShmCreateImage(display, XDefaultVisual(display, screen), DefaultDepth(display, screen), ZPixmap, 0, &shminfo, WIDTH, HEIGHT);
if (!ximage)
return 0x02;
shminfo.shmid = shmget(IPC_PRIVATE, ximage->bytes_per_line * ximage->height, IPC_CREAT | 0600);
if (shminfo.shmid == -1)
return 0x03;
shminfo.shmaddr = shmat(shminfo.shmid, 0, SHM_RND);
if (shminfo.shmaddr == (void*)-1)
return 0x04;
shmctl(shminfo.shmid, IPC_RMID, 0);
ximage->data = shminfo.shmaddr;
if (XShmAttach(display, &shminfo) == 0)
return 0x05;
GC gc = XDefaultGC(display, screen);
Window window = XCreateWindow(display, RootWindow(display, screen), 0, 0, WIDTH, HEIGHT, 0,
DefaultDepth(display, screen), InputOutput, XDefaultVisual(display, screen), 0, 0);
XMapWindow(display, window);
memset(ximage->data, 0, ximage->bytes_per_line*ximage->height);
XShmPutImage(display, window, gc, ximage, 0, 0, 0, 0, WIDTH, HEIGHT, 1);
XSync(display, 0);
pthread_rwlock_init(&render_start, 0);
pthread_rwlock_init(&render_complete, 0);
pthread_rwlock_init(&render_next, 0);
pthread_rwlock_init(&render_ready, 0);
sem_init(&threads_started, 0, 0);
pthread_attr_init(&pthread_attr);
pthread_attr_setdetachstate(&pthread_attr, PTHREAD_CREATE_DETACHED);
pthread_rwlock_wrlock(&render_start);
for (size_t i = 0; i < NUM_THREADS; i++) {
pthread_t trash;
size_t *tmp;
tmp = malloc(sizeof(*tmp));
*tmp = i;
pthread_create(&trash, &pthread_attr, thread, tmp);
}
for (size_t i = 0; i < NUM_THREADS; i++)
sem_wait(&threads_started);
float angle = 0;
while (1) {
// compiler optimization inhibitation
for (int i = 0; i < NUM_SPHERES; i++) // ugh memcpy+volatile=bad
spheres[i] = volatile_spheres[i];
viewpos = volatile_viewpos;
for (int i = 0; i < 3; i++)
viewdir[i], volatile_viewdir[i];
// end of compiler optimization inhibitations
size_t start = microtime();
angle += 0.01;
if (angle > M_PI*2)
angle -= M_PI*2;
for (size_t i = 1; i < NUM_SPHERES; i++) {
spheres[i].pos.x = sinf(angle+spheres[i].rotation_offset);
spheres[i].pos.z = cosf(angle+spheres[i].rotation_offset);
}
pthread_rwlock_wrlock(&render_ready); // rendering threads must be ready
pthread_rwlock_unlock(&render_ready); // no need fo this anymore
pthread_rwlock_wrlock(&render_next); // don't skip to the next cycle
pthread_rwlock_unlock(&render_start); // go do this stuff
pthread_rwlock_wrlock(&render_complete); // yay done doing stuff
pthread_rwlock_unlock(&render_complete); // no need for this anymore
pthread_rwlock_wrlock(&render_start); // don't go on to the next yet
pthread_rwlock_unlock(&render_next); // prepare for next
printf("Frame complete, took %luus.\n", microtime() - start);
// TODO: early stuff might be dropped for some currently-unidentified reason, find out why and fix it
XEvent event;
while (XCheckTypedEvent(display, XShmGetEventBase(display)+ShmCompletion, &event) == 0)
;
XShmPutImage(display, window, gc, ximage, 0, 0, 0, 0, WIDTH, HEIGHT, 1);
}
XDestroyWindow(display, window);
XShmDetach(display, &shminfo);
XDestroyImage(ximage);
XCloseDisplay(display);
return 0x00;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment