Files
2026-05-11 16:28:46 +02:00

209 lines
6.0 KiB
C++

// runtime_test — exercises full gamedev runtime (issue 0072b).
//
// --self-test : runs N frames offscreen-friendly and exits 0 if no crash.
// <no args> : interactive mode (gradient + 3 colored sprites + ImGui HUD).
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <SDL3/SDL.h>
// sokol implementation lives in sokol_impl.cpp (one TU only).
#include "sokol_gfx.h"
#include "sokol_log.h"
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#endif
#include "math2d.h"
#include "sokol_setup.h"
#include "sprite_batch.h"
#include "audio_engine.h"
#include "audio_play.h"
#include "input_unified.h"
#include "camera_2d.h"
#include "game_loop.h"
using fn::math2d::Vec2;
using fn::math2d::Rect;
using fn::math2d::Color;
namespace {
struct Runtime {
SDL_Window* win = nullptr;
SDL_GLContext gl = nullptr;
fn::audio::Engine audio{};
fn::gfx::SpriteBatch batch{};
fn::input::InputState input{};
fn::cam::Camera2D cam{};
sg_image white_tex{};
sg_view white_view{};
sg_pass_action pass_action{};
int self_test_frames = -1; // -1 = interactive
int frame = 0;
bool quit = false;
};
Runtime g;
// Make a 2x2 white texture so sprite_batch has something to bind without
// loading PNGs from disk (keeps runtime_test asset-free).
sg_image make_white_tex() {
static const uint32_t px[4] = { 0xFFFFFFFFu, 0xFFFFFFFFu, 0xFFFFFFFFu, 0xFFFFFFFFu };
sg_image_desc d{};
d.width = 2; d.height = 2;
d.pixel_format = SG_PIXELFORMAT_RGBA8;
d.data.mip_levels[0] = sg_range{ px, sizeof(px) };
return sg_make_image(&d);
}
void on_fixed_update(void* /*user*/, float /*dt*/) {
// No physics yet. Just exercises the callback path.
}
void on_render(void* /*user*/, float /*interp*/) {
int w, h;
SDL_GetWindowSizeInPixels(g.win, &w, &h);
g.cam.viewport_w = w;
g.cam.viewport_h = h;
sg_pass p{};
p.action = g.pass_action;
p.swapchain = fn::gfx::make_swapchain(w, h);
sg_begin_pass(&p);
float vp[16];
fn::cam::view_proj_matrix(g.cam, vp);
fn::gfx::sprite_batch_begin(g.batch, vp);
// Three colored squares moving slightly with frame.
float t = (float)g.frame * 0.02f;
Color cs[3] = {
Color::rgba(255, 80, 80),
Color::rgba(80, 255, 80),
Color::rgba(80, 80, 255),
};
for (int i = 0; i < 3; ++i) {
float x = -200.0f + i * 200.0f + 30.0f * SDL_sinf(t + i);
float y = 30.0f * SDL_cosf(t + i);
fn::gfx::sprite_batch_draw(g.batch, g.white_view, 2, 2,
Rect{0, 0, 2, 2}, Rect{x - 50, y - 50, 100, 100}, cs[i]);
}
fn::gfx::sprite_batch_end(g.batch);
sg_end_pass();
sg_commit();
SDL_GL_SwapWindow(g.win);
g.frame++;
}
bool should_quit(void* /*user*/) {
SDL_Event e;
fn::input::input_begin_frame(g.input);
while (SDL_PollEvent(&e)) {
fn::input::input_process_event(g.input, &e);
if (e.type == SDL_EVENT_QUIT) g.quit = true;
if (e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) g.quit = true;
}
if (g.input.back || g.input.back_pressed) g.quit = true;
if (g.self_test_frames >= 0 && g.frame >= g.self_test_frames) g.quit = true;
return g.quit;
}
bool init(int argc, char** argv) {
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--self-test") == 0) {
g.self_test_frames = 60;
}
}
if (!SDL_Init(SDL_INIT_VIDEO)) {
std::fprintf(stderr, "SDL_Init: %s\n", SDL_GetError());
return false;
}
#if defined(__EMSCRIPTEN__)
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#else
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
#endif
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
g.win = SDL_CreateWindow("runtime_test", 1280, 720,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (!g.win) { std::fprintf(stderr, "SDL_CreateWindow: %s\n", SDL_GetError()); return false; }
g.gl = SDL_GL_CreateContext(g.win);
if (!g.gl) { std::fprintf(stderr, "SDL_GL_CreateContext: %s\n", SDL_GetError()); return false; }
SDL_GL_MakeCurrent(g.win, g.gl);
SDL_GL_SetSwapInterval(1);
sg_desc d{};
d.environment = fn::gfx::make_environment();
d.logger.func = slog_func;
sg_setup(&d);
g.white_tex = make_white_tex();
sg_view_desc vd{};
vd.texture.image = g.white_tex;
g.white_view = sg_make_view(&vd);
g.batch = fn::gfx::sprite_batch_create(1024);
if (!g.batch.ok) { std::fprintf(stderr, "sprite_batch_create failed\n"); return false; }
// Audio: non-fatal in self-test (CI may have no audio device).
g.audio = fn::audio::engine_init();
if (!g.audio.ok) {
std::fprintf(stderr, "audio_engine: init failed (continuing)\n");
}
g.cam.viewport_w = 1280;
g.cam.viewport_h = 720;
g.cam.zoom = 1.0f;
g.pass_action.colors[0].load_action = SG_LOADACTION_CLEAR;
g.pass_action.colors[0].clear_value = { 0.05f, 0.05f, 0.08f, 1.0f };
return true;
}
void shutdown() {
if (g.audio.ok) fn::audio::engine_shutdown(g.audio);
fn::gfx::sprite_batch_destroy(g.batch);
if (g.white_view.id) sg_destroy_view(g.white_view);
if (g.white_tex.id) sg_destroy_image(g.white_tex);
sg_shutdown();
if (g.gl) SDL_GL_DestroyContext(g.gl);
if (g.win) SDL_DestroyWindow(g.win);
SDL_Quit();
}
} // namespace
int main(int argc, char** argv) {
if (!init(argc, argv)) {
shutdown();
return 1;
}
fn::game::LoopCfg cfg{};
cfg.fixed_dt = 1.0f / 60.0f;
cfg.max_steps_per_frame = 5;
cfg.on_fixed_update = on_fixed_update;
cfg.on_render = on_render;
cfg.should_quit = should_quit;
fn::game::loop_run(g.win, cfg);
#if !defined(__EMSCRIPTEN__)
shutdown();
std::fprintf(stderr, "runtime_test: %d frames, exit 0\n", g.frame);
#endif
return 0;
}