commit bed33856e7e64a16344573a07a2a68fac3b4aa4d Author: fn-registry agent Date: Mon May 11 16:28:43 2026 +0200 chore: sync from fn-registry agent diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..4e88813 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,98 @@ +# engine_smoke — gamedev stack smoke test. Issue 0072a. +# Standalone — does NOT use add_imgui_app() / fn_framework. +# +# Two build modes: +# 1. As subdir of cpp/ (cmake -S cpp -B build): cpp/CMakeLists.txt already +# adds vendor/sdl3 and exposes SDL3::SDL3-static. We just link. +# 2. As top-level (cmake -S cpp/apps/engine_smoke -B build) — used by the +# WASM pipeline and any standalone build. We do project() + manually +# add_subdirectory the vendored SDL3. + +cmake_minimum_required(VERSION 3.16) + +# Detect mode: top-level vs subdir. +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(engine_smoke 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) + +# Resolve the repo's cpp/vendor regardless of build mode. +get_filename_component(_REPO_CPP_ROOT ${CMAKE_CURRENT_LIST_DIR}/../.. ABSOLUTE) +set(_VENDOR ${_REPO_CPP_ROOT}/vendor) + +if(_TOP_LEVEL) + # Bring in SDL3 ourselves. + if(NOT EXISTS ${_VENDOR}/sdl3/CMakeLists.txt) + message(FATAL_ERROR "SDL3 vendoring missing: ${_VENDOR}/sdl3 — see cpp/vendor/sdl3.VENDORING.md") + 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() + +# --- ImGui (sources we need) --- +set(_IMGUI_SRCS + ${_VENDOR}/imgui/imgui.cpp + ${_VENDOR}/imgui/imgui_demo.cpp + ${_VENDOR}/imgui/imgui_draw.cpp + ${_VENDOR}/imgui/imgui_tables.cpp + ${_VENDOR}/imgui/imgui_widgets.cpp + ${_VENDOR}/imgui/backends/imgui_impl_sdl3.cpp + ${_VENDOR}/imgui/backends/imgui_impl_opengl3.cpp +) + +add_executable(engine_smoke + main.cpp + ${_IMGUI_SRCS} +) + +target_include_directories(engine_smoke PRIVATE + ${_VENDOR}/imgui + ${_VENDOR}/sokol +) + +target_link_libraries(engine_smoke PRIVATE SDL3::SDL3-static) + +if(NOT EMSCRIPTEN) + find_package(OpenGL REQUIRED) + target_link_libraries(engine_smoke PRIVATE OpenGL::GL) +endif() + +if(EMSCRIPTEN) + set_target_properties(engine_smoke PROPERTIES SUFFIX ".html") + target_compile_options(engine_smoke PRIVATE + -Os + -fno-exceptions + -fno-rtti + -ffunction-sections + -fdata-sections + ) + target_link_options(engine_smoke PRIVATE + -Os + -sUSE_WEBGL2=1 + -sFULL_ES3=1 + -sALLOW_MEMORY_GROWTH=1 + -sINITIAL_MEMORY=33554432 + -sASSERTIONS=0 + -sENVIRONMENT=web + -Wl,--gc-sections + # NOTE: -sFILESYSTEM=0 + --closure=1 broke under SDL3 (closure + # references undeclared FS). Saves ~30KB JS + ~30KB wasm if + # we ever stub SDL3 filesystem refs out. Re-enable per app. + ) +endif() + +if(WIN32) + set_target_properties(engine_smoke PROPERTIES WIN32_EXECUTABLE TRUE) +endif() diff --git a/app.md b/app.md new file mode 100644 index 0000000..1958737 --- /dev/null +++ b/app.md @@ -0,0 +1,84 @@ +--- +name: engine_smoke +lang: cpp +domain: gamedev +description: "Smoke test del stack gamedev: SDL3 + sokol_gfx + Dear ImGui. Valida que el stack compila y corre en PC (Linux/Windows desktop GL) y WASM (WebGL2 via emscripten) antes de invertir tiempo en runtime real (issue 0072a)." +tags: [imgui, sdl3, sokol, gamedev, smoke, wasm] +uses_functions: [] +uses_types: [] +framework: "imgui" +entry_point: "main.cpp" +dir_path: "cpp/apps/engine_smoke" +repo_url: "" +e2e_checks: + - id: build_pc + cmd: "cmake --build build --target engine_smoke -j" + timeout_s: 300 + - id: smoke_self + cmd: "timeout 5 ./build/cpp/apps/engine_smoke/engine_smoke || test $? = 124" + timeout_s: 10 + severity: warning # requires display in CI; warning, not critical + - id: build_wasm + cmd: "bash bash/functions/pipelines/build_wasm_cpp_app.sh engine_smoke" + timeout_s: 600 + severity: warning # requires emsdk + - id: wasm_size_budget + cmd: "test -f build/wasm/engine_smoke/engine_smoke.wasm.gz && test $(stat -c%s build/wasm/engine_smoke/engine_smoke.wasm.gz) -lt 1572864" + severity: warning +--- + +# engine_smoke + +Primer experimento del stack gamedev (issue [0072a](../../../dev/issues/0072a-gamedev-smoke-sdl3-sokol-imgui.md)). + +## Que hace + +1. Abre ventana SDL3 1280x720 con context OpenGL. +2. Inicializa `sokol_gfx` sobre ese context (GL 3.3 desktop, GLES3 en WASM). +3. Pinta un fullscreen triangle con un fragment shader animado (gradient time-based). +4. Pinta sobre eso un panel ImGui con FPS, tamaño de ventana y boton Quit. + +Sin `fn_framework`, sin `add_imgui_app()`. CMakeLists standalone para evitar contaminar el shell desktop hasta validar el stack. + +## Build PC (Linux) + +Desde la raiz del fn_registry: + +```bash +cmake -S cpp -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --target engine_smoke -j +./build/cpp/apps/engine_smoke/engine_smoke +``` + +## Build WASM + +Requiere `emsdk` instalado y activado en el shell. Ver `cpp/vendor/sokol.VENDORING.md` y `bash/functions/pipelines/build_wasm_cpp_app.sh`. + +```bash +bash bash/functions/pipelines/build_wasm_cpp_app.sh engine_smoke +# Sirve build/wasm/engine_smoke/engine_smoke.html en navegador +``` + +Budget objetivo: gzip ≤ 1.5 MB. + +## Stack vendoreado + +- `cpp/vendor/sdl3/` — SDL3 release-3.4.8 +- `cpp/vendor/sokol/` — sokol_gfx single-header pinned commit +- `cpp/vendor/imgui/` — ImGui 1.92.7 con backends `imgui_impl_sdl3` + `imgui_impl_opengl3` + +## Notas de implementacion + +- Fullscreen tri via `gl_VertexID` (3 vertices, no VBO). Cubre todo el viewport sin atributos. +- Uniform `u_time` actualizado por frame. +- Backend ImGui = `imgui_impl_opengl3` (oficial, 0 LoC custom). Compatible WebGL2 emscripten. +- `SOKOL_GLCORE` desktop / `SOKOL_GLES3` web — selccion compile-time. +- `setup_gfx` se llama UNA VEZ tras crear el GL context. `sg_setup` toma `sglue_environment()` que lee el contexto activo. + +## No-objetivos (smoke) + +- No texturas reales (eso entra en 0072b). +- No audio. +- No input game-style. +- No fn_framework integration. +- No mobile (Android/iOS son issues 0072g/h). diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..505713c --- /dev/null +++ b/main.cpp @@ -0,0 +1,242 @@ +// engine_smoke — gamedev stack smoke test. +// Validates SDL3 + sokol_gfx + Dear ImGui (imgui_impl_sdl3 + imgui_impl_opengl3) +// builds and runs on PC (desktop GL 3.3) and WASM (WebGL2 via -sFULL_ES3=1). +// +// Issue 0072a. Standalone — does NOT use fn_framework / add_imgui_app. + +#include +#include +#include + +#include + +// sokol_gfx — single TU includes the implementation. +#define SOKOL_IMPL +#if defined(__EMSCRIPTEN__) + #define SOKOL_GLES3 +#else + #define SOKOL_GLCORE +#endif +#include "sokol_gfx.h" +#include "sokol_log.h" +// sokol_glue.h is NOT used: it pulls in sokol_app symbols. We use SDL3 for +// windowing, so we build sg_environment / sg_swapchain manually. + +#include "imgui.h" +#include "backends/imgui_impl_sdl3.h" +#include "backends/imgui_impl_opengl3.h" + +#if defined(__EMSCRIPTEN__) + #include +#endif + +namespace { + +struct App { + SDL_Window* win = nullptr; + SDL_GLContext gl = nullptr; + sg_pipeline pip{}; + sg_bindings bind{}; + sg_pass_action pass_action{}; + bool running = true; + uint64_t frame = 0; +}; + +App g_app; + +// Fullscreen-quad vertex shader (no inputs, gl_VertexID trick). +const char* VS_SRC = +#if defined(__EMSCRIPTEN__) + "#version 300 es\n" +#else + "#version 330 core\n" +#endif + "out vec2 v_uv;\n" + "void main() {\n" + " vec2 p = vec2((gl_VertexID == 1) ? 3.0 : -1.0, (gl_VertexID == 2) ? 3.0 : -1.0);\n" + " v_uv = p * 0.5 + 0.5;\n" + " gl_Position = vec4(p, 0.0, 1.0);\n" + "}\n"; + +const char* FS_SRC = +#if defined(__EMSCRIPTEN__) + "#version 300 es\n" + "precision mediump float;\n" +#else + "#version 330 core\n" +#endif + "in vec2 v_uv;\n" + "out vec4 frag;\n" + "uniform float u_time;\n" + "void main() {\n" + " float r = 0.5 + 0.5 * sin(u_time + v_uv.x * 6.2831);\n" + " float g = 0.5 + 0.5 * sin(u_time * 1.3 + v_uv.y * 6.2831);\n" + " float b = 0.5 + 0.5 * sin(u_time * 0.7 + (v_uv.x + v_uv.y) * 6.2831);\n" + " frag = vec4(r, g, b, 1.0);\n" + "}\n"; + +sg_environment make_environment() { + sg_environment env{}; + env.defaults.color_format = SG_PIXELFORMAT_RGBA8; + env.defaults.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + env.defaults.sample_count = 1; + return env; +} + +sg_swapchain make_swapchain(int w, int h) { + sg_swapchain sw{}; + sw.width = w; + sw.height = h; + sw.sample_count = 1; + sw.color_format = SG_PIXELFORMAT_RGBA8; + sw.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + sw.gl.framebuffer = 0; // default framebuffer + return sw; +} + +void setup_gfx() { + sg_desc d{}; + d.environment = make_environment(); + d.logger.func = slog_func; + sg_setup(&d); + + sg_shader_desc sd{}; + sd.vertex_func.source = VS_SRC; + sd.fragment_func.source = FS_SRC; + sd.uniform_blocks[0].stage = SG_SHADERSTAGE_FRAGMENT; + sd.uniform_blocks[0].size = sizeof(float); + sd.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_NATIVE; + sd.uniform_blocks[0].glsl_uniforms[0].type = SG_UNIFORMTYPE_FLOAT; + sd.uniform_blocks[0].glsl_uniforms[0].glsl_name = "u_time"; + sg_shader shd = sg_make_shader(&sd); + + sg_pipeline_desc pd{}; + pd.shader = shd; + pd.primitive_type = SG_PRIMITIVETYPE_TRIANGLES; + g_app.pip = sg_make_pipeline(&pd); + + g_app.pass_action.colors[0].load_action = SG_LOADACTION_CLEAR; + g_app.pass_action.colors[0].clear_value = {0.05f, 0.05f, 0.08f, 1.0f}; +} + +void frame() { + SDL_Event ev; + while (SDL_PollEvent(&ev)) { + ImGui_ImplSDL3_ProcessEvent(&ev); + if (ev.type == SDL_EVENT_QUIT) g_app.running = false; + if (ev.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) g_app.running = false; + } + + int w, h; + SDL_GetWindowSizeInPixels(g_app.win, &w, &h); + + sg_pass p{}; + p.action = g_app.pass_action; + p.swapchain = make_swapchain(w, h); + sg_begin_pass(&p); + sg_apply_pipeline(g_app.pip); + + float t = (float)g_app.frame * (1.0f / 60.0f); + sg_range time_range{ &t, sizeof(t) }; + sg_apply_uniforms(0, &time_range); + + sg_draw(0, 3, 1); // 3 vertices, fullscreen tri via gl_VertexID + + // ImGui on top + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplSDL3_NewFrame(); + ImGui::NewFrame(); + + ImGui::SetNextWindowPos(ImVec2(10, 10)); + ImGui::Begin("engine_smoke", nullptr, + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove); + ImGui::Text("SDL3 + sokol_gfx + ImGui"); + ImGui::Text("FPS: %.1f", ImGui::GetIO().Framerate); + ImGui::Text("Frame: %llu Size: %dx%d", (unsigned long long)g_app.frame, w, h); + if (ImGui::Button("Quit")) g_app.running = false; + ImGui::End(); + + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + sg_end_pass(); + sg_commit(); + SDL_GL_SwapWindow(g_app.win); + g_app.frame++; +} + +#if defined(__EMSCRIPTEN__) +void emscripten_loop() { + if (!g_app.running) { + emscripten_cancel_main_loop(); + return; + } + frame(); +} +#endif + +} // namespace + +int main(int /*argc*/, char** /*argv*/) { + if (!SDL_Init(SDL_INIT_VIDEO)) { + std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); + return 1; + } + +#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); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + + g_app.win = SDL_CreateWindow("engine_smoke", 1280, 720, + SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY); + if (!g_app.win) { + std::fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError()); + SDL_Quit(); + return 1; + } + g_app.gl = SDL_GL_CreateContext(g_app.win); + if (!g_app.gl) { + std::fprintf(stderr, "SDL_GL_CreateContext failed: %s\n", SDL_GetError()); + SDL_DestroyWindow(g_app.win); + SDL_Quit(); + return 1; + } + SDL_GL_MakeCurrent(g_app.win, g_app.gl); + SDL_GL_SetSwapInterval(1); + + setup_gfx(); + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::StyleColorsDark(); + ImGui_ImplSDL3_InitForOpenGL(g_app.win, g_app.gl); +#if defined(__EMSCRIPTEN__) + ImGui_ImplOpenGL3_Init("#version 300 es"); +#else + ImGui_ImplOpenGL3_Init("#version 330 core"); +#endif + +#if defined(__EMSCRIPTEN__) + emscripten_set_main_loop(emscripten_loop, 0, 1); +#else + while (g_app.running) frame(); +#endif + + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplSDL3_Shutdown(); + ImGui::DestroyContext(); + sg_shutdown(); + SDL_GL_DestroyContext(g_app.gl); + SDL_DestroyWindow(g_app.win); + SDL_Quit(); + return 0; +}