From 5a079a2d114f96d4847d1ee305d5b7c16eeec50e Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 27 Dec 2025 12:03:39 -0800 Subject: Initial commit --- src/main.c | 420 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 src/main.c (limited to 'src/main.c') diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..1c017f0 --- /dev/null +++ b/src/main.c @@ -0,0 +1,420 @@ +#include + +#include +// TODO: Update math/camera to explicitly expose ortho/perspective camera parameters, +// not just a baked matrix. +//#include +#include +#include +#include +#include +#include + +#include +#include +#include + +static constexpr int BufferWidth = 160; +static constexpr int BufferHeight = 120; +static constexpr sgVec2i BufferDims = (sgVec2i){.x = BufferWidth, .y = BufferHeight}; +static constexpr R Aspect = (R)BufferWidth / (R)BufferHeight; + +static const char* WindowTitle = "GAME"; +// Window dimensions must be an integer scaling of buffer dimensions. +// TODO: Make window dimensions a function of DPI. +static constexpr int WindowWidth = 640; +static constexpr int WindowHeight = 480; +static constexpr sgVec2i WindowDims = (sgVec2i){.x = WindowWidth, .y = WindowHeight}; + +static const R Fovy = (R)(90 * TO_RAD); + +#define DEBUG_EVENT_LOOP 1 + +#ifdef DEBUG_EVENT_LOOP +#define EVENT_LOOP_PRINT printf +#else +#define EVENT_LOOP_PRINT(...) +#endif // DEBUG_EVENT_LOOP + +typedef struct CameraCommand { + bool CameraMoveLeft : 1; + bool CameraMoveRight : 1; + bool CameraMoveForward : 1; + bool CameraMoveBackward : 1; + bool CameraSlow : 1; // When true, move more slowly. + bool CameraRotate : 1; // When true, subsequent mouse movements cause + // the camera to rotate. +} CameraCommand; + +typedef struct CameraController { + R speed; // Camera movement speed. + R rotation_speed; // Controls the degree with which mouse movements rotate + // the camera. + vec2 prev_mouse_position; // Mouse position in the previous frame. +} CameraController; + +typedef struct Camera { + Spatial3 spatial; + R fovy; + R aspect; + R near; + R far; +} Camera; + +typedef struct State { + SDL_Window* window; + swgfx* gfx; + sgPixel* colour; + Model* model; + Camera camera; + CameraController camera_controller; + Uint64 last_tick; +} State; + +static sgVec3 SgVec3FromMathVec3(vec3 v) { + return (sgVec3){v.x, v.y, v.z}; +} + +static CameraCommand CameraCommandFromInput( + const bool* keyboard_state, const SDL_MouseButtonFlags mouse_flags) { + assert(keyboard_state); + if (keyboard_state[SDL_SCANCODE_W]) { + printf("W: %d\n", keyboard_state[SDL_SCANCODE_W]); + } + return (CameraCommand){ + .CameraMoveLeft = keyboard_state[SDL_SCANCODE_A], + .CameraMoveRight = keyboard_state[SDL_SCANCODE_D], + .CameraMoveForward = keyboard_state[SDL_SCANCODE_W], + .CameraMoveBackward = keyboard_state[SDL_SCANCODE_S], + .CameraSlow = keyboard_state[SDL_SCANCODE_LSHIFT], + .CameraRotate = mouse_flags & SDL_BUTTON_MASK(SDL_BUTTON_LEFT), + }; +} + +static void UpdateCamera( + CameraController* controller, R dt, vec2 mouse_position, + CameraCommand command, Camera* camera) { + assert(controller); + assert(camera); + + Spatial3* cam = &camera->spatial; + + // Translation. + const R move_x = (R)(command.CameraMoveLeft ? -1 : 0) + + (R)(command.CameraMoveRight ? 1 : 0); + const R move_y = (R)(command.CameraMoveForward ? 1 : 0) + + (R)(command.CameraMoveBackward ? -1 : 0); + const R speed_factor = command.CameraSlow ? 0.3f : 1.f; + const vec2 translation = vec2_scale( + vec2_normalize(vec2_make(move_x, move_y)), + controller->speed * speed_factor * dt); + spatial3_move_right(cam, translation.x); + spatial3_move_forwards(cam, translation.y); + + // Rotation. + if (command.CameraRotate) { + const vec2 mouse_delta = + vec2_sub(mouse_position, controller->prev_mouse_position); + + const vec2 rotation = + vec2_scale(mouse_delta, controller->rotation_speed * dt); + + spatial3_global_yaw(cam, -rotation.x); + spatial3_pitch(cam, -rotation.y); + } + + // Update controller state. + controller->prev_mouse_position = mouse_position; +} + +static bool Update(State* state, R dt) { + assert(state); + + int num_keys = 0; + const bool* keyboard_state = SDL_GetKeyboardState(&num_keys); + + vec2 mouse = {0}; + const SDL_MouseButtonFlags mouse_flags = SDL_GetMouseState(&mouse.x, &mouse.y); + + const CameraCommand cmd = CameraCommandFromInput(keyboard_state, mouse_flags); + UpdateCamera(&state->camera_controller, dt, mouse, cmd, &state->camera); + + return true; +} + +static void RenderIndexedModel(swgfx* gfx, const IndexedModel* model) { + assert(gfx); + assert(model); + const sgTriIdx* tris = (const sgTriIdx*)(model->data + model->offsetTris); + const sgVec3* positions = (const sgVec3*)(model->data + model->offsetPositions); + sgTrianglesIndexedNonUniform(gfx, model->numTris, tris, positions); +} + +static void RenderModel(swgfx* gfx, const Model* model) { + assert(gfx); + assert(model); + switch (model->type) { + case ModelTypeIndexed: RenderIndexedModel(gfx, &model->indexed); break; + case ModelTypeFlat: /* TODO: Render flat models. */ break; + default: assert(false); break; + } +} + +static void RenderTriangle2d(swgfx* gfx) { + assert(gfx); + const sgVec2 p0 = (sgVec2){20, 20}; + const sgVec2 p1 = (sgVec2){80, 20}; + const sgVec2 p2 = (sgVec2){50, 50}; + const sgTri2 tri = (sgTri2){p0, p1, p2}; + sgTriangles2(gfx, 1, &tri); +} + +static void Checkerboard(swgfx* gfx, int width, int height) { + assert(gfx); + const sgPixel colour = (sgPixel){255, 0, 255, 255}; + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + if (((x ^ y) & 1) == 1) { + const sgVec2i position = (sgVec2i){x, y}; + sgPixels(gfx, 1, &position, colour); + } + } + } +} + +static bool Render(State* state) { + assert(state); + assert(state->window); + assert(state->gfx); + + // Locking/unlocking SDL software surfaces is not necessary. + // Probably also best to avoid SDL_BlitSurface(); it does pixel format + // conversion while blitting one pixel at a time. Instead, make the UI pixel + // format match the SDL window's and write to SDL's back buffer directly. + SDL_Surface* window_surface = SDL_GetWindowSurface(state->window); + assert(window_surface); + + // Until we make the window resizable, assert dimensions for safety. + assert(window_surface->w == WindowWidth); + assert(window_surface->h == WindowHeight); + +#ifdef DEBUG_EVENT_LOOP + EVENT_LOOP_PRINT( + "Render: window surface: %dx%d\n", + window_surface->w, window_surface->h); +#endif + + const Camera* cam = &state->camera; + + sgColourBuffer(state->gfx, BufferDims, state->colour); + sgClear(state->gfx); + sgViewport(state->gfx, 0, 0, BufferWidth, BufferHeight); + sgCheck(state->gfx); + // TODO: For easier debugging, overlay the checkerboard on top of the + // other rendered items with alpha blending. + //Checkerboard(state->gfx, BufferWidth, BufferHeight); + //RenderTriangle2d(state->gfx); + sgModelId(state->gfx); + sgView(state->gfx, SgVec3FromMathVec3(cam->spatial.p), SgVec3FromMathVec3(cam->spatial.f)); + sgPerspective(state->gfx, cam->fovy, cam->aspect, cam->near, cam->far); + RenderModel(state->gfx, state->model); + /*sgIdx indices[3] = {0, 1, 2}; + sgVec3 positions[3] = { + (sgVec3){0, 0, 0}, + (sgVec3){5, 2, 0}, + (sgVec3){8, 8, 0}, + }; + sgTrianglesIndexed(state->gfx, 3, indices, positions);*/ + sgPresent(state->gfx, WindowDims, window_surface->pixels); + + if (!SDL_UpdateWindowSurface(state->window)) { + return false; + } + + return true; +} + +static bool Resize(State* state) { + assert(state); + + // int width, height; + // SDL_GetWindowSize(state->window, &width, &height); + + const SDL_Surface* window_surface = SDL_GetWindowSurface(state->window); + if (!window_surface) { + return false; + } + const int width = window_surface->w; + const int height = window_surface->h; + + EVENT_LOOP_PRINT("Resize: %dx%d\n", width, height); + + return true; +} + +static bool Initialize(State* state) { + assert(state); + + if ((state->window = SDL_CreateWindow( + WindowTitle, + WindowWidth, + WindowHeight, + 0)) == NULL) { + fprintf(stderr, "SDL_CreateWindow failed\n"); + return false; + } + + if (!(state->gfx = sgNew())) { + fprintf(stderr, "sgNew failed\n"); + return false; + } + + if (!(state->colour = SG_ALIGN_ALLOC(BufferWidth * BufferHeight, sgPixel))) { + fprintf(stderr, "Failed to allocate colour buffer\n"); + return false; + } + + sgColourBuffer(state->gfx, BufferDims, state->colour); + + const char* model_path = "/home/jeanne/blender/box.mdl"; + if (!(state->model = read_file(model_path))) { + fprintf(stderr, "Failed to load model: [%s]\n", model_path); + return false; + } + + Camera* camera = &state->camera; + camera->fovy = Fovy; + camera->aspect = Aspect; + camera->near = 0.1f; + camera->far = 1000.f; + camera->spatial = spatial3_make(); + camera->spatial.p = vec3_make(0, 1, 10); + + state->camera_controller = (CameraController){ + .speed = 7.f, + .rotation_speed = (R)(90 * TO_RAD), + }; + + state->last_tick = SDL_GetPerformanceCounter(); + + return true; +} + +static void Shutdown(State* state) { + assert(state); + + if (state->model) { + free(state->model); + state->model = nullptr; + } + + if (state->colour) { + SG_FREE(&state->colour); + } + + if (state->gfx) { + sgDel(&state->gfx); + } + + if (state->window) { + SDL_DestroyWindow(state->window); + state->window = nullptr; + } +} + +static R GetDeltaTime(State* state) { + assert(state); + constexpr Uint64 NS_IN_SEC = 1'000'000'000; + const Uint64 this_tick = SDL_GetPerformanceCounter(); + const Uint64 freq = SDL_GetPerformanceFrequency(); + const Uint64 elapsed_ns = (this_tick - state->last_tick) * NS_IN_SEC / freq; + const R elapsed_sec = (R)elapsed_ns / (R)NS_IN_SEC; + state->last_tick = this_tick; + return elapsed_sec; +} + +int main() { + bool success = false; + // Controls whether we should keep running. + bool running = true; + + State state = {0}; + + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) { + fprintf(stderr, "SDL_Init failed\n"); + goto cleanup; + } + + if (!Initialize(&state)) { + fprintf(stderr, "Initialization failed\n"); + goto cleanup; + } + + if (!Resize(&state)) { + goto cleanup; + } + + success = true; + + while (success && running) { + EVENT_LOOP_PRINT("loop\n"); + + // Handle events. + SDL_Event event = {0}; + while (SDL_PollEvent(&event)) { + if ((event.type == SDL_EVENT_QUIT) || + (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED)) { + running = false; + break; + } else if ((event.window.type == SDL_EVENT_WINDOW_DISPLAY_CHANGED) || + (event.window.type == SDL_EVENT_WINDOW_RESIZED) || + (event.window.type == SDL_EVENT_WINDOW_MOVED)) { + // When the window is maximized, an SDL_WINDOWEVENT_MOVED comes in + // before an SDL_WINDOWEVENT_SIZE_CHANGED with the window already + // resized. This is unfortunate because we cannot rely on the latter + // event alone to handle resizing. + if (!Resize(&state)) { + success = false; + break; + } + } else if (event.type == SDL_EVENT_KEY_DOWN) { + if (event.key.mod & SDL_KMOD_LCTRL) { + switch (event.key.key) { + // Exit. + case SDLK_C: + case SDLK_D: + running = false; + break; + default: + break; + } + } + } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { + // + } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) { + // + } else if (event.type == SDL_EVENT_MOUSE_WHEEL) { + // + } else { + EVENT_LOOP_PRINT("event.window.type = %d\n", event.window.type); + } + } // events + + // Draw and update if needed. + if (success && running) { + const R dt = GetDeltaTime(&state); + success = Update(&state, dt); + } + if (success && running) { + success = Render(&state); + } + } // loop + +cleanup: + if (!success) { + fprintf(stderr, "%s\n", SDL_GetError()); + } + Shutdown(&state); + SDL_Quit(); + return success ? 0 : 1; +} -- cgit v1.2.3