From 7760f83ce1c9c27a0683539a641dda46660cbdf7 Mon Sep 17 00:00:00 2001 From: Dominic Szablewski Date: Wed, 16 Aug 2023 22:55:04 +0200 Subject: [PATCH] Add software rendering backend as a demo --- Makefile | 3 + README.md | 9 ++ src/platform.h | 4 + src/platform_sdl.c | 67 +++++++- src/render_software.c | 369 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 445 insertions(+), 7 deletions(-) create mode 100644 src/render_software.c diff --git a/Makefile b/Makefile index 28beb38..e3ca12c 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,9 @@ endif ifeq ($(RENDERER), GL) RENDERER_SRC = src/render_gl.c C_FLAGS := $(C_FLAGS) -DRENDERER_GL +else ifeq ($(RENDERER), SOFTWARE) + RENDERER_SRC = src/render_software.c + C_FLAGS := $(C_FLAGS) -DRENDERER_SOFTWARE else $(error Unknown RENDERER) endif diff --git a/README.md b/README.md index 6a828e8..16953d5 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,15 @@ make wasm This builds the minimal version (no music, no intro) as well as the full version. +### Flags + +The makefile accepts several flags. You can specify them with `make FLAG=VALUE` + +- `DEBUG` – `true` or `fals`, default is `false`. Whether to include debug symbols in the build. +- `RENDERER` – `GL` or `SOFTWARE`, default is `GL` (the `SOFTWARE` renderer is very much unfinished and only works with SDL) +- `USE_GLX` – `true` or `false`, default is `false` and uses `GLVND` over `GLX`. Only used for the linux build. + + ## Running This repository does not contain the assets (textures, 3d models etc.) required to run the game. This code mostly assumes to have the PSX NTSC data, but some menu models from the PC version are loaded as well. Both of these can be easily found on archive.org and similar sites. The music (optional) needs to be provided in [QOA format](https://github.com/phoboslab/qoa). The intro video as MPEG1. diff --git a/src/platform.h b/src/platform.h index dd0c9a4..bcb8b0e 100644 --- a/src/platform.h +++ b/src/platform.h @@ -9,4 +9,8 @@ double platform_now(); void platform_set_fullscreen(bool fullscreen); void platform_set_audio_mix_cb(void (*cb)(float *buffer, uint32_t len)); +#if defined(RENDERER_SOFTWARE) + rgba_t *platform_get_screenbuffer(int32_t *pitch); +#endif + #endif diff --git a/src/platform_sdl.c b/src/platform_sdl.c index e68ff76..f1a3eaa 100755 --- a/src/platform_sdl.c +++ b/src/platform_sdl.c @@ -174,12 +174,6 @@ void platform_pump_events() { } } -vec2i_t platform_screen_size() { - int width, height; - SDL_GL_GetDrawableSize(window, &width, &height); - return vec2i(width, height); -} - double platform_now() { uint64_t perf_counter = SDL_GetPerformanceCounter(); return (double)perf_counter / (double)perf_freq; @@ -216,7 +210,7 @@ void platform_set_audio_mix_cb(void (*cb)(float *buffer, uint32_t len)) { } -#if defined(RENDERER_GL) +#if defined(RENDERER_GL) // ---------------------------------------------------- #define PLATFORM_WINDOW_FLAGS SDL_WINDOW_OPENGL SDL_GLContext platform_gl; @@ -242,6 +236,65 @@ void platform_set_audio_mix_cb(void (*cb)(float *buffer, uint32_t len)) { void platform_end_frame() { SDL_GL_SwapWindow(window); } + + vec2i_t platform_screen_size() { + int width, height; + SDL_GL_GetDrawableSize(window, &width, &height); + return vec2i(width, height); + } + + +#elif defined(RENDERER_SOFTWARE) // ---------------------------------------------- + #define PLATFORM_WINDOW_FLAGS 0 + static SDL_Renderer *renderer; + static SDL_Texture *screenbuffer = NULL; + static void *screenbuffer_pixels = NULL; + static int screenbuffer_pitch; + static vec2i_t screenbuffer_size = vec2i(0, 0); + static vec2i_t screen_size = vec2i(0, 0); + + + void platform_video_init() { + renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); + } + + void platform_video_cleanup() { + + } + + void platform_prepare_frame() { + if (screen_size.x != screenbuffer_size.x || screen_size.y != screenbuffer_size.y) { + if (screenbuffer) { + SDL_DestroyTexture(screenbuffer); + } + screenbuffer = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, screen_size.x, screen_size.y); + screenbuffer_size = screen_size; + } + SDL_LockTexture(screenbuffer, NULL, &screenbuffer_pixels, &screenbuffer_pitch); + } + + void platform_end_frame() { + screenbuffer_pixels = NULL; + SDL_UnlockTexture(screenbuffer); + SDL_RenderCopy(renderer, screenbuffer, NULL, NULL); + SDL_RenderPresent(renderer); + } + + rgba_t *platform_get_screenbuffer(int32_t *pitch) { + *pitch = screenbuffer_pitch; + return screenbuffer_pixels; + } + + vec2i_t platform_screen_size() { + int width, height; + SDL_GetWindowSize(window, &width, &height); + + // float aspect = (float)width / (float)height; + // screen_size = vec2i(240 * aspect, 240); + screen_size = vec2i(width, height); + return screen_size; + } + #else #error "Unsupported renderer for platform SDL" #endif diff --git a/src/render_software.c b/src/render_software.c new file mode 100644 index 0000000..5b12549 --- /dev/null +++ b/src/render_software.c @@ -0,0 +1,369 @@ +#include "system.h" +#include "render.h" +#include "mem.h" +#include "utils.h" +#include "platform.h" + +#define NEAR_PLANE 16.0 +#define FAR_PLANE (RENDER_FADEOUT_FAR) +#define TEXTURES_MAX 1024 + + +typedef struct { + vec2i_t size; + rgba_t *pixels; +} render_texture_t; + +static void line(vec2i_t p0, vec2i_t p1, rgba_t color); + +static rgba_t *screen_buffer; +static int32_t screen_pitch; +static int32_t screen_ppr; +static vec2i_t screen_size; + +static mat4_t view_mat = mat4_identity(); +static mat4_t mvp_mat = mat4_identity(); +static mat4_t projection_mat = mat4_identity(); +static mat4_t sprite_mat = mat4_identity(); + +static render_texture_t textures[TEXTURES_MAX]; +static uint32_t textures_len; + +uint16_t RENDER_NO_TEXTURE; + + +void render_init(vec2i_t screen_size) { + render_set_screen_size(screen_size); + textures_len = 0; + + rgba_t white_pixels[4] = { + rgba(128,128,128,255), rgba(128,128,128,255), + rgba(128,128,128,255), rgba(128,128,128,255) + }; + RENDER_NO_TEXTURE = render_texture_create(2, 2, white_pixels); +} + +void render_cleanup() {} + +void render_set_screen_size(vec2i_t size) { + screen_size = size; + + float aspect = (float)size.x / (float)size.y; + float fov = (73.75 / 180.0) * 3.14159265358; + float f = 1.0 / tan(fov / 2); + float nf = 1.0 / (NEAR_PLANE - FAR_PLANE); + projection_mat = mat4( + f / aspect, 0, 0, 0, + 0, f, 0, 0, + 0, 0, (FAR_PLANE + NEAR_PLANE) * nf, -1, + 0, 0, 2 * FAR_PLANE * NEAR_PLANE * nf, 0 + ); +} + +void render_set_resolution(render_resolution_t res) {} +void render_set_post_effect(render_post_effect_t post) {} + +vec2i_t render_size() { + return screen_size; +} + + +void render_frame_prepare() { + screen_buffer = platform_get_screenbuffer(&screen_pitch); + screen_ppr = screen_pitch / sizeof(rgba_t); + + rgba_t color = rgba(0, 0, 0, 255); + for (uint32_t y = 0; y < screen_size.y; y++) { + for (uint32_t x = 0; x < screen_size.x; x++) { + screen_buffer[y * screen_ppr + x] = color; + } + } +} + +void render_frame_end() {} + +void render_set_view(vec3_t pos, vec3_t angles) { + view_mat = mat4_identity(); + mat4_set_translation(&view_mat, vec3(0, 0, 0)); + mat4_set_roll_pitch_yaw(&view_mat, vec3(angles.x, -angles.y + M_PI, angles.z + M_PI)); + mat4_translate(&view_mat, vec3_inv(pos)); + mat4_set_yaw_pitch_roll(&sprite_mat, vec3(-angles.x, angles.y - M_PI, 0)); + + render_set_model_mat(&mat4_identity()); +} + +void render_set_view_2d() { + float near = -1; + float far = 1; + float left = 0; + float right = screen_size.x; + float bottom = screen_size.y; + float top = 0; + float lr = 1 / (left - right); + float bt = 1 / (bottom - top); + float nf = 1 / (near - far); + mvp_mat = mat4( + -2 * lr, 0, 0, 0, + 0, -2 * bt, 0, 0, + 0, 0, 2 * nf, 0, + (left + right) * lr, (top + bottom) * bt, (far + near) * nf, 1 + ); +} + +void render_set_model_mat(mat4_t *m) { + mat4_t vm_mat; + mat4_mul(&vm_mat, &view_mat, m); + mat4_mul(&mvp_mat, &projection_mat, &vm_mat); +} + +void render_set_depth_write(bool enabled) {} +void render_set_depth_test(bool enabled) {} +void render_set_depth_offset(float offset) {} +void render_set_screen_position(vec2_t pos) {} +void render_set_blend_mode(render_blend_mode_t mode) {} +void render_set_cull_backface(bool enabled) {} + +vec3_t render_transform(vec3_t pos) { + return vec3_transform(vec3_transform(pos, &view_mat), &projection_mat); +} + +void render_push_tris(tris_t tris, uint16_t texture_index) { + float w2 = screen_size.x * 0.5; + float h2 = screen_size.y * 0.5; + + vec3_t p0 = vec3_transform(tris.vertices[0].pos, &mvp_mat); + vec3_t p1 = vec3_transform(tris.vertices[1].pos, &mvp_mat); + vec3_t p2 = vec3_transform(tris.vertices[2].pos, &mvp_mat); + if (p0.z >= 1.0 || p1.z >= 1.0 || p2.z >= 1.0) { + return; + } + + vec2i_t sc0 = vec2i(p0.x * w2 + w2, h2 - p0.y * h2); + vec2i_t sc1 = vec2i(p1.x * w2 + w2, h2 - p1.y * h2); + vec2i_t sc2 = vec2i(p2.x * w2 + w2, h2 - p2.y * h2); + + rgba_t color = tris.vertices[0].color; + color.as_rgba.r = min(color.as_rgba.r * 2, 255); + color.as_rgba.g = min(color.as_rgba.g * 2, 255); + color.as_rgba.b = min(color.as_rgba.b * 2, 255); + color.as_rgba.a = clamp(color.as_rgba.a * (1.0-p0.z) * FAR_PLANE * (2.0/255.0), 0, 255); + + line(sc0, sc1, color); + line(sc1, sc2, color); + line(sc2, sc0, color); +} + +void render_push_sprite(vec3_t pos, vec2i_t size, rgba_t color, uint16_t texture_index) { + error_if(texture_index >= textures_len, "Invalid texture %d", texture_index); + + vec3_t p0 = vec3_add(pos, vec3_transform(vec3(-size.x * 0.5, -size.y * 0.5, 0), &sprite_mat)); + vec3_t p1 = vec3_add(pos, vec3_transform(vec3( size.x * 0.5, -size.y * 0.5, 0), &sprite_mat)); + vec3_t p2 = vec3_add(pos, vec3_transform(vec3(-size.x * 0.5, size.y * 0.5, 0), &sprite_mat)); + vec3_t p3 = vec3_add(pos, vec3_transform(vec3( size.x * 0.5, size.y * 0.5, 0), &sprite_mat)); + + render_texture_t *t = &textures[texture_index]; + render_push_tris((tris_t){ + .vertices = { + {.pos = p0, .uv = {0, 0}, .color = color}, + {.pos = p1, .uv = {0 + t->size.x ,0}, .color = color}, + {.pos = p2, .uv = {0, 0 + t->size.y}, .color = color}, + } + }, texture_index); + render_push_tris((tris_t){ + .vertices = { + {.pos = p2, .uv = {0, 0 + t->size.y}, .color = color}, + {.pos = p1, .uv = {0 + t->size.x, 0}, .color = color}, + {.pos = p3, .uv = {0 + t->size.x, 0 + t->size.y}, .color = color}, + } + }, texture_index); +} + +void render_push_2d(vec2i_t pos, vec2i_t size, rgba_t color, uint16_t texture_index) { + render_push_2d_tile(pos, vec2i(0, 0), render_texture_size(texture_index), size, color, texture_index); +} + +void render_push_2d_tile(vec2i_t pos, vec2i_t uv_offset, vec2i_t uv_size, vec2i_t size, rgba_t color, uint16_t texture_index) { + error_if(texture_index >= textures_len, "Invalid texture %d", texture_index); + render_push_tris((tris_t){ + .vertices = { + {.pos = {pos.x, pos.y + size.y, 0}, .uv = {uv_offset.x , uv_offset.y + uv_size.y}, .color = color}, + {.pos = {pos.x + size.x, pos.y, 0}, .uv = {uv_offset.x + uv_size.x, uv_offset.y}, .color = color}, + {.pos = {pos.x, pos.y, 0}, .uv = {uv_offset.x , uv_offset.y}, .color = color}, + } + }, texture_index); + + render_push_tris((tris_t){ + .vertices = { + {.pos = {pos.x + size.x, pos.y + size.y, 0}, .uv = {uv_offset.x + uv_size.x, uv_offset.y + uv_size.y}, .color = color}, + {.pos = {pos.x + size.x, pos.y, 0}, .uv = {uv_offset.x + uv_size.x, uv_offset.y}, .color = color}, + {.pos = {pos.x, pos.y + size.y, 0}, .uv = {uv_offset.x , uv_offset.y + uv_size.y}, .color = color}, + } + }, texture_index); +} + + +uint16_t render_texture_create(uint32_t width, uint32_t height, rgba_t *pixels) { + error_if(textures_len >= TEXTURES_MAX, "TEXTURES_MAX reached"); + + uint32_t byte_size = width * height * sizeof(rgba_t); + uint16_t texture_index = textures_len; + + textures[texture_index] = (render_texture_t){{width, height}, NULL}; + // textures[texture_index] = (render_texture_t){{width, height}, mem_bump(byte_size)}; + // memcpy(textures[texture_index].pixels, pixels, byte_size); + + textures_len++; + return texture_index; +} + +vec2i_t render_texture_size(uint16_t texture_index) { + error_if(texture_index >= textures_len, "Invalid texture %d", texture_index); + return textures[texture_index].size; +} + +void render_texture_replace_pixels(int16_t texture_index, rgba_t *pixels) { + error_if(texture_index >= textures_len, "Invalid texture %d", texture_index); + render_texture_t *t = &textures[texture_index]; + // memcpy(t->pixels, pixels, t->size.x * t->size.y * sizeof(rgba_t)); +} + +uint16_t render_textures_len() { + return textures_len; +} + +void render_textures_reset(uint16_t len) { + error_if(len > textures_len, "Invalid texture reset len %d >= %d", len, textures_len); + textures_len = len; +} + +void render_textures_dump(const char *path) {} + + + +// ----------------------------------------------------------------------------- + +static inline rgba_t color_mix(rgba_t in, rgba_t out) { + return rgba( + lerp(in.as_rgba.r, out.as_rgba.r, out.as_rgba.a/255.0), + lerp(in.as_rgba.g, out.as_rgba.g, out.as_rgba.a/255.0), + lerp(in.as_rgba.b, out.as_rgba.b, out.as_rgba.a/255.0), + 1 + ); +} + +typedef enum { + CLIP_INSIDE = 0, + CLIP_LEFT = (1<<0), + CLIP_RIGHT = (1<<1), + CLIP_BOTTOM = (1<<2), + CLIP_TOP = (1<<3), +} clip_code_t; + +static inline clip_code_t clip_code(vec2i_t p) { + clip_code_t cc = CLIP_INSIDE; + if (p.x < 0) { + flags_add(cc, CLIP_LEFT); + } + else if (p.x >= screen_size.x) { + flags_add(cc, CLIP_RIGHT); + } + if (p.y < 0) { + flags_add(cc, CLIP_BOTTOM); + } + else if (p.y >= screen_size.y) { + flags_add(cc, CLIP_TOP); + } + return cc; +} + +static void line(vec2i_t p0, vec2i_t p1, rgba_t color) { + // Cohen Sutherland Line Clipping + clip_code_t cc0 = clip_code(p0); + clip_code_t cc1 = clip_code(p1); + bool accept = false; + + vec2i_t ss = vec2i(screen_size.x-1, screen_size.y-1); + while (true) { + if (!(cc0 | cc1)) { + accept = true; + break; + } + else if (cc0 & cc1) { + break; + } + else { + vec2i_t r = p0; + clip_code_t cc_out = cc0 ? cc0 : cc1; + + if (flags_is(cc_out, CLIP_TOP)) { + r.x = p0.x + (p1.x - p0.x) * (ss.y - p0.y) / (p1.y - p0.y); + r.y = ss.y; + } + else if (flags_is(cc_out, CLIP_BOTTOM)) { + r.x = p0.x + (p1.x - p0.x) * (-p0.y) / (p1.y - p0.y); + r.y = 0; + } + else if (flags_is(cc_out, CLIP_RIGHT)) { + r.y = p0.y + (p1.y - p0.y) * (ss.x - p0.x) / (p1.x - p0.x); + r.x = ss.x; + } + else if (flags_is(cc_out, CLIP_LEFT)) { + r.y = p0.y + (p1.y - p0.y) * (-p0.x) / (p1.x - p0.x); + r.x = 0; + } + + if (cc_out == cc0) { + p0.x = r.x; + p0.y = r.y; + cc0 = clip_code(p0); + } + else { + p1.x = r.x; + p1.y = r.y; + cc1 = clip_code(p1); + } + } + } + if (!accept) { + return; + } + + // Bresenham's line algorithm + bool steep = false; + if (abs(p0.x - p1.x) < abs(p0.y - p1.y)) { + swap(p0.x, p0.y); + swap(p1.x, p1.y); + steep = true; + } + if (p0.x > p1.x) { + swap(p0.x, p1.x); + swap(p0.y, p1.y); + } + int32_t dx = p1.x - p0.x; + int32_t dy = p1.y - p0.y; + int32_t derror2 = abs(dy) * 2; + int32_t error2 = 0; + int32_t y = p0.y; + int32_t ydir = (p1.y > p0.y ? 1 : -1); + + if (steep) { + for (int32_t x = p0.x; x <= p1.x; x++) { + screen_buffer[x * screen_ppr + y] = color_mix(screen_buffer[x * screen_ppr + y], color); + error2 += derror2; + if (error2 > dx) { + y += ydir; + error2 -= dx * 2; + } + } + } + else { + for (int32_t x = p0.x; x <= p1.x; x++) { + screen_buffer[y * screen_ppr + x] = color_mix(screen_buffer[y * screen_ppr + x], color); + error2 += derror2; + if (error2 > dx) { + y += ydir; + error2 -= dx * 2; + } + } + } +}