Created
December 12, 2024 23:05
-
-
Save mrbid/3ffc6984d0dbdb35fea27ae16faa8bab to your computer and use it in GitHub Desktop.
Multi-threaded CPU Ray-Trace 3D Renderer for X11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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