chore: sync from fn-registry agent
This commit is contained in:
@@ -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()
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
|
// <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;
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user