chore: sync from fn-registry agent

This commit is contained in:
fn-registry agent
2026-05-11 16:28:43 +02:00
commit bed33856e7
3 changed files with 424 additions and 0 deletions
+98
View File
@@ -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()
+84
View File
@@ -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).
+242
View File
@@ -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 <cstdio>
#include <cstdint>
#include <cmath>
#include <SDL3/SDL.h>
// 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 <emscripten/emscripten.h>
#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;
}