// runtime_test — exercises full gamedev runtime (issue 0072b). // // --self-test : runs N frames offscreen-friendly and exits 0 if no crash. // : interactive mode (gradient + 3 colored sprites + ImGui HUD). #include #include #include #include // sokol implementation lives in sokol_impl.cpp (one TU only). #include "sokol_gfx.h" #include "sokol_log.h" #if defined(__EMSCRIPTEN__) #include #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; }