From 49a9f3273d3e8fa39576497da1469476c5a9d9d4 Mon Sep 17 00:00:00 2001 From: fn-registry agent Date: Mon, 11 May 2026 16:28:46 +0200 Subject: [PATCH] chore: sync from fn-registry agent --- CMakeLists.txt | 79 +++++++++++++++++++ app.md | 74 ++++++++++++++++++ main.cpp | 208 +++++++++++++++++++++++++++++++++++++++++++++++++ sokol_impl.cpp | 14 ++++ 4 files changed, 375 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 app.md create mode 100644 main.cpp create mode 100644 sokol_impl.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5811a51 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,79 @@ +# runtime_test — full runtime exerciser (issue 0072b). +# Standalone, dual-mode CMakeLists (top-level + subdir). + +cmake_minimum_required(VERSION 3.16) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(runtime_test CXX) + set(_TOP_LEVEL TRUE) +else() + set(_TOP_LEVEL FALSE) +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +get_filename_component(_REPO_CPP_ROOT ${CMAKE_CURRENT_LIST_DIR}/../.. ABSOLUTE) +set(_VENDOR ${_REPO_CPP_ROOT}/vendor) +set(_FUNCS ${_REPO_CPP_ROOT}/functions) + +if(_TOP_LEVEL) + if(NOT EXISTS ${_VENDOR}/sdl3/CMakeLists.txt) + message(FATAL_ERROR "SDL3 vendoring missing") + endif() + set(SDL_SHARED OFF CACHE BOOL "" FORCE) + set(SDL_STATIC ON CACHE BOOL "" FORCE) + set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE) + set(SDL_TESTS OFF CACHE BOOL "" FORCE) + set(SDL_EXAMPLES OFF CACHE BOOL "" FORCE) + set(SDL_INSTALL OFF CACHE BOOL "" FORCE) + set(SDL_X11_XSCRNSAVER OFF CACHE BOOL "" FORCE) + add_subdirectory(${_VENDOR}/sdl3 ${CMAKE_BINARY_DIR}/_sdl3 EXCLUDE_FROM_ALL) +endif() + +add_executable(runtime_test + main.cpp + sokol_impl.cpp + ${_FUNCS}/gfx/sokol_setup.cpp + ${_FUNCS}/gfx/sprite_batch.cpp + ${_FUNCS}/gamedev/audio_engine.cpp + ${_FUNCS}/gamedev/audio_play.cpp + ${_FUNCS}/gamedev/input_unified.cpp + ${_FUNCS}/gamedev/game_loop.cpp + ${_FUNCS}/gamedev/camera_2d.cpp +) + +target_include_directories(runtime_test PRIVATE + ${_VENDOR}/sokol + ${_FUNCS}/core # math2d.h + ${_FUNCS}/gfx # sokol_setup.h, sprite_batch.h + ${_FUNCS}/gamedev # audio/input/loop/camera headers +) + +target_link_libraries(runtime_test PRIVATE SDL3::SDL3-static) + +if(NOT EMSCRIPTEN) + find_package(OpenGL REQUIRED) + target_link_libraries(runtime_test PRIVATE OpenGL::GL) + if(UNIX AND NOT APPLE) + target_link_libraries(runtime_test PRIVATE m pthread dl) + endif() +endif() + +if(EMSCRIPTEN) + set_target_properties(runtime_test PROPERTIES SUFFIX ".html") + target_compile_options(runtime_test PRIVATE + -Os -fno-exceptions -fno-rtti -ffunction-sections -fdata-sections + ) + target_link_options(runtime_test PRIVATE + -Os + -sUSE_WEBGL2=1 + -sFULL_ES3=1 + -sALLOW_MEMORY_GROWTH=1 + -sINITIAL_MEMORY=33554432 + -sASSERTIONS=0 + -sENVIRONMENT=web + -Wl,--gc-sections + ) +endif() diff --git a/app.md b/app.md new file mode 100644 index 0000000..bb64846 --- /dev/null +++ b/app.md @@ -0,0 +1,74 @@ +--- +name: runtime_test +lang: cpp +domain: gamedev +description: "Exerciser end-to-end del runtime nucleo gamedev (issue 0072b). Inicializa sokol_gfx + audio (miniaudio) + input unificado + sprite_batch + camera 2D + game loop. Modo `--self-test` corre 60 frames y sale exit 0; sin args entra modo interactivo (3 sprites en gradient)." +tags: [imgui, sdl3, sokol, gamedev, smoke, runtime] +uses_functions: + - sokol_setup_cpp_gfx + - sprite_batch_cpp_gfx + - audio_engine_cpp_gamedev + - audio_play_cpp_gamedev + - input_unified_cpp_gamedev + - game_loop_cpp_gamedev + - camera_2d_cpp_gamedev +uses_types: + - Vec2_cpp_core + - Rect_cpp_core + - Color_cpp_core +framework: "imgui" +entry_point: "main.cpp" +dir_path: "cpp/apps/runtime_test" +repo_url: "" +e2e_checks: + - id: build_pc + cmd: "cmake --build build --target runtime_test -j" + timeout_s: 300 + - id: self_test_pc + cmd: "./build/apps/runtime_test/runtime_test --self-test" + timeout_s: 30 + severity: warning # requires display in CI + - id: build_wasm + cmd: "bash bash/functions/infra/build_wasm_cpp_app.sh runtime_test" + timeout_s: 600 + severity: warning + - id: wasm_size_budget + cmd: "test -f build/wasm/runtime_test/runtime_test.wasm.gz && test $(stat -c%s build/wasm/runtime_test/runtime_test.wasm.gz) -lt 2097152" + severity: warning +--- + +# runtime_test + +Test integrado del runtime nucleo gamedev. Si compila y corre, el stack 0072b esta verde. + +## Modo --self-test + +Corre 60 frames con audio init no-fatal (CI sin device de audio sigue pasando), 3 sprites animados, input procesando eventos vacios, salida exit 0. + +```bash +./build/apps/runtime_test/runtime_test --self-test +``` + +## Modo interactivo + +```bash +./build/apps/runtime_test/runtime_test +``` + +Pulsa Esc o Back (gamepad) para salir. + +## Que ejercita + +- `sokol_setup_cpp_gfx` — make_environment + make_swapchain. +- `sprite_batch_cpp_gfx` — 3 sprites por frame con tint distinto. +- `audio_engine_cpp_gamedev` — engine_init / engine_shutdown. +- `input_unified_cpp_gamedev` — input_begin_frame / input_process_event con SDL_Events. +- `camera_2d_cpp_gamedev` — view_proj_matrix. +- `game_loop_cpp_gamedev` — loop_run con fixed timestep + emscripten branch. +- Tipos `Vec2`, `Rect`, `Color` de `cpp/functions/core/math2d.h`. + +## Notas + +- NO carga assets de disco. Crea textura 2x2 blanca en GPU para alimentar `sprite_batch`. Asi el self-test es asset-free. +- Audio init es no-fatal: si no hay device, registra error y sigue. Permite correr en CI / WSL sin audio. +- Sin ImGui en este test (a diferencia de `engine_smoke`). Reduce binary size y aisla la validacion del runtime puro. diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..0885d02 --- /dev/null +++ b/main.cpp @@ -0,0 +1,208 @@ +// 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; +} diff --git a/sokol_impl.cpp b/sokol_impl.cpp new file mode 100644 index 0000000..b22eb96 --- /dev/null +++ b/sokol_impl.cpp @@ -0,0 +1,14 @@ +// Dedicated TU for sokol_gfx implementation. Including sokol_gfx.h with +// SOKOL_IMPL elsewhere (e.g. main.cpp) plus then including any other header +// that re-includes sokol_gfx.h re-emits the impl block — link/compile errors. +// +// Convention: this is the ONLY file in this app that defines SOKOL_IMPL. + +#define SOKOL_IMPL +#if defined(__EMSCRIPTEN__) + #define SOKOL_GLES3 +#else + #define SOKOL_GLCORE +#endif +#include "sokol_gfx.h" +#include "sokol_log.h"