feat(kotlin-compose): design system + 33 components + gallery_kt + e2e android emulator + scaffolder fixes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 16:28:50 +02:00
parent 0bdb8454e1
commit cb6d9e61d1
152 changed files with 148262 additions and 25 deletions
+28
View File
@@ -289,6 +289,13 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/CMakeLists.txt)
add_subdirectory(apps/primitives_gallery)
endif()
# --- Tables playground (vive dentro de primitives_gallery/playground/tables/) ---
# No es un app del registry; sirve para iterar mejoras sobre table_view_cpp_viz
# antes de promover una API v2 y migrar las apps C++ que hoy usan ImGui::BeginTable raw.
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/playground/tables/CMakeLists.txt)
add_subdirectory(apps/primitives_gallery/playground/tables)
endif()
# --- text_editor + file_watcher smoke test (issue 0025) ---
# Build gate para validar que text_editor.cpp + file_watcher.cpp + vendor enlazan.
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/text_editor_smoke/CMakeLists.txt)
@@ -303,6 +310,27 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/altsnap_jitter_test/CMakeLists.txt)
add_subdirectory(apps/altsnap_jitter_test)
endif()
# --- gamedev stack (SDL3 + sokol_gfx + miniaudio, issue 0072) ---
# Apps standalone, no usan fn_framework. Vendor SDL3 se compila una vez aqui;
# las apps solo linkan SDL3::SDL3-static.
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sdl3/CMakeLists.txt
AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/vendor/sokol/sokol_gfx.h)
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 EXCLUDE_FROM_ALL)
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/engine_smoke/CMakeLists.txt)
add_subdirectory(apps/engine_smoke)
endif()
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/runtime_test/CMakeLists.txt)
add_subdirectory(apps/runtime_test)
endif()
endif()
# --- Registry Dashboard (lives in projects/fn_monitoring/apps/) ---
# _DASH_DIR puede sobreescribirse via -D_DASH_DIR=<path> para apuntar a un
# worktree (parallel-fix-issues u otros flujos que aislen builds).
+27 -5
View File
@@ -32,6 +32,15 @@ Antes de mergear una app, verificar uno por uno:
- [ ] **GL loader** (si la app usa OpenGL >= 2.0 directamente). Pasar
`AppConfig::init_gl_loader = true` para que `fn::run_app()` llame
`fn::gfx::gl_loader_init()` tras crear el contexto.
- [ ] **Auto-dockspace** (default `true`). El framework llama
`ImGui::DockSpaceOverViewport(0, GetMainViewport(), PassthruCentralNode)`
antes de `render_fn()` cada frame. **NO** llamar `DockSpaceOverViewport`
manual en `render()` — duplica nodes y causa flicker. Apps que usan
layout custom con `ImGui::DockSpace` propio o `fullscreen_window` deben
poner `cfg.auto_dockspace = false`.
- [ ] **No `fn_ui::app_menubar(...)` manual**. El framework ya lo dibuja en
cada frame leyendo `cfg.panels`/`cfg.layouts_cb`/`cfg.view_extras`.
Llamarlo manualmente provoca barra duplicada o pisada.
- [ ] **Tokens en lugar de hex literales**. Usar `fn_tokens::colors`,
`fn_tokens::spacing`, `fn_tokens::radius`. Nunca `IM_COL32(0x12,0x34,...)`,
nunca `ImVec4(0.5f, 0.5f, 0.5f, 1.0f)` ad-hoc.
@@ -46,14 +55,23 @@ Antes de mergear una app, verificar uno por uno:
- [ ] **Build incremental**. La app aparece en `cpp/CMakeLists.txt` con su
`add_subdirectory(apps/<nombre>)`. Sin warnings nuevos.
## Crear app nueva — usar el scaffolder
```bash
# App suelta en cpp/apps/<name>/
fn run init_cpp_app my_tool --desc "Herramienta para X"
# App dentro de un proyecto
fn run init_cpp_app finance_panel --project budget --desc "Panel de finanzas"
```
`init_cpp_app_bash_pipelines` genera la estructura canonica (main.cpp + CMakeLists.txt + app.md) cumpliendo este documento, registra la app en `cpp/CMakeLists.txt`, crea repo Gitea `dataforge/<name>` y ejecuta `fn index`. Despues solo se completa `uses_functions` cuando se importan funciones del registry.
## Esqueleto minimo
```cpp
#include "framework/app_base.h"
#include "core/icons_tabler.h"
#include "core/panel_menu.h"
#include "core/app_settings.h"
#include "core/tokens.h"
#include "imgui.h"
namespace {
@@ -67,7 +85,7 @@ constexpr fn_ui::PanelToggle k_panels[] = {
} // namespace
static void render_my_app() {
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
// Sin DockSpaceOverViewport ni app_menubar manual — los da el framework.
if (show_inspector) {
ImGui::Begin(TI_INFO_CIRCLE " Inspector", &show_inspector);
ImGui::TextUnformatted("Inspector contents");
@@ -84,9 +102,11 @@ int main() {
fn::AppConfig cfg;
cfg.title = "My App";
cfg.about = {"My App", "0.1.0", "Demo de app shell canonica"};
cfg.log = {"my_app.log", 1};
cfg.panels = k_panels;
cfg.panel_count = sizeof(k_panels) / sizeof(k_panels[0]);
cfg.init_gl_loader = false; // ponerlo en true si usas OpenGL directo
cfg.init_gl_loader = false; // true si usas OpenGL directo
// cfg.auto_dockspace = false; // solo si gestionas DockSpace propio (ej. shaders_lab)
return fn::run_app(cfg, render_my_app);
}
```
@@ -104,6 +124,8 @@ escribir una linea de codigo.
| `glfwInit()` en `main` | `fn::run_app()` |
| `ImVec4(0.5,0.5,0.5,1)` ad-hoc | `fn_tokens::colors::text_dim` |
| Crear menubar a mano en cada frame | `AppConfig::panels` + `AppConfig::layouts_cb` |
| `fn_ui::app_menubar(nullptr,0,nullptr)` en render | El framework ya lo dibuja |
| `ImGui::DockSpaceOverViewport(...)` en render | `auto_dockspace=true` por defecto |
| `ImGui::Begin(u8"\xEF\xA0\x83 ...")` | `ImGui::Begin(TI_HOME " ...")` |
| Settings dispersos por la app | `settings_window_add_section()` |
| About hardcoded en un `Begin/End` | `AppConfig::about` o `about_window_set_info()` |
Submodule cpp/apps/altsnap_jitter_test updated: 64a01defbc...181c4f3dd6
-6
View File
@@ -45,12 +45,6 @@ static void init_data() {
void render() {
init_data();
// MainMenuBar (solo Settings — chart_demo no tiene paneles toggleables)
fn_ui::app_menubar(nullptr, 0, nullptr);
// Full-window dockspace
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport());
if (ImGui::Begin("fn_registry — Chart Demo")) {
if (ImGui::BeginTabBar("##charts")) {
if (ImGui::BeginTabItem("Line Plot")) {
Submodule cpp/apps/engine_smoke added at bed33856e7
+37
View File
@@ -0,0 +1,37 @@
---
name: primitives_gallery
lang: cpp
domain: gfx
description: "Visual catalog de primitivas C++ UI del fn_registry. Demos por categoria (charts, controls, layout, gl_info). Soporta modo --capture para regresion visual."
tags: [imgui, gallery, gfx, demo, capture]
uses_functions: []
uses_types: []
framework: "imgui"
entry_point: "main.cpp"
dir_path: "cpp/apps/primitives_gallery"
repo_url: ""
---
# primitives_gallery
Catalogo visual de las primitivas y componentes ImGui del registry. Cada demo se carga al hacer click en su entrada del sidebar.
## Build & run
```bash
cd cpp && cmake --build build --target primitives_gallery -j
./build/primitives_gallery
```
## Modo capture (regresion visual)
```bash
./build/primitives_gallery --capture <out_dir>
```
Renderiza cada demo offscreen y guarda PNGs en `<out_dir>/`. Permite gate visual via golden images.
## Notas
- `auto_dockspace = false` — usa `fullscreen_window` que ocupa todo el viewport.
- `init_gl_loader = true` — necesario para demos de OpenGL 4.3 core (compute, SSBOs).
+5 -5
View File
@@ -132,10 +132,8 @@ static void draw_sidebar() {
static void render() {
// Theme y gl_loader gestionados por fn::run_app (theme=FnDark por defecto,
// init_gl_loader=true en AppConfig).
// MainMenuBar (solo Settings — la gallery no tiene paneles toggleables ni layouts)
fn_ui::app_menubar(nullptr, 0, nullptr);
// init_gl_loader=true en AppConfig). Menubar via run_app.
// auto_dockspace=false porque usamos fullscreen_window que ocupa todo.
fullscreen_window_begin("##gallery");
@@ -224,7 +222,9 @@ int main(int argc, char** argv) {
.about = {.name = "Primitives Gallery",
.version = "0.4.0",
.description = "Visual catalog of fn_registry C++ UI primitives. Now on OpenGL 4.3 core (compute, SSBOs, image load/store) — ver demo gl_info."},
.init_gl_loader = true},
.init_gl_loader = true,
.auto_dockspace = false,
.log = {"primitives_gallery.log", 1}},
render
);
}
@@ -0,0 +1,6 @@
# Tables playground - vive dentro de primitives_gallery/ (playgrounds.md).
# No es un app del registry: no tiene app.md, no se indexa.
add_imgui_app(tables_playground
main.cpp
${CMAKE_SOURCE_DIR}/functions/viz/table_view.cpp
)
@@ -0,0 +1,115 @@
// Playground tables: visor de la funcion table_view_cpp_viz tal cual existe
// hoy en el registry. Iteraremos mejoras encima hasta promover una API v2
// que sustituya a los `ImGui::BeginTable` raw de las apps C++.
#include "app_base.h"
#include "imgui.h"
#include "viz/table_view.h"
#include "core/logger.h"
#include <cstdio>
#include <string>
#include <vector>
namespace {
struct Row {
const char* name;
const char* lang;
const char* domain;
const char* purity;
const char* description;
};
// Dataset de muestra inspirado en el registry. Filas reales-ish para
// hacer obvias las limitaciones actuales (sin sort, sin filter, sin
// per-cell render, alto fijo, etc.).
const std::vector<Row>& sample_rows() {
static const std::vector<Row> rows = {
{"filter_slice", "go", "core", "pure", "Filtra slice con predicado"},
{"map_slice", "go", "core", "pure", "Aplica f a cada elemento"},
{"reduce_slice", "go", "core", "pure", "Fold con acumulador"},
{"sma", "py", "finance", "pure", "Simple moving average"},
{"ema", "py", "finance", "pure", "Exponential moving average"},
{"rsi", "py", "finance", "pure", "Relative strength index"},
{"table_view", "cpp", "viz", "pure", "Tabla ImGui actual del registry"},
{"line_plot", "cpp", "viz", "pure", "ImPlot line wrapper"},
{"scatter_plot", "cpp", "viz", "pure", "ImPlot scatter wrapper"},
{"bar_chart", "cpp", "viz", "pure", "ImPlot bar wrapper"},
{"heatmap", "cpp", "viz", "pure", "ImPlot heatmap wrapper"},
{"sqlite_open", "go", "infra", "impure", "Open SQLite con WAL+FK"},
{"http_json_response", "go", "infra", "impure", "Helper JSON response"},
{"http_parse_body", "go", "infra", "impure", "Parse JSON body"},
{"rsync_deploy", "bash", "infra", "impure", "rsync local -> remoto"},
{"systemd_install", "go", "infra", "impure", "Sube unit + enable + start"},
{"systemd_restart", "go", "infra", "impure", "Restart servicio remoto"},
{"jupyter_discover", "py", "notebook", "impure", "Descubre instancias Jupyter"},
{"jupyter_exec", "py", "notebook", "impure", "Ejecuta celda y vuelca output"},
{"docker_pull_image", "go", "infra", "impure", "docker pull con timeout"},
{"graph_force_layout", "cpp", "viz", "pure", "Force-directed CPU"},
{"graph_force_layout_gpu","cpp", "viz", "pure", "Force-directed GPU (compute)"},
{"sql_workbench", "cpp", "core", "impure", "Workbench SQL embebido"},
{"text_editor", "cpp", "core", "impure", "Editor de texto con highlighting"},
{"icon_font", "cpp", "core", "impure", "Carga tabler-icons.ttf"},
};
return rows;
}
// Aplanado row-major para alimentar table_view_cpp_viz (firma `const char* const*`).
const char* const* flatten_cells(int& out_rows, int& out_cols) {
static std::vector<const char*> flat;
static bool built = false;
if (!built) {
const auto& rows = sample_rows();
flat.reserve(rows.size() * 5);
for (const auto& r : rows) {
flat.push_back(r.name);
flat.push_back(r.lang);
flat.push_back(r.domain);
flat.push_back(r.purity);
flat.push_back(r.description);
}
built = true;
}
out_rows = static_cast<int>(sample_rows().size());
out_cols = 5;
return flat.data();
}
} // namespace
void render() {
if (ImGui::Begin("Tables Playground - table_view actual")) {
ImGui::TextWrapped(
"Esta es la funcion `table_view_cpp_viz` del registry hoy. "
"Capacidades: borders, sortable (solo indicador, no sort real), "
"rowBg, resizable, scrollY (alto fijo 300px), reorderable. "
"Sin filter, sin selection, sin per-cell render, sin export. "
"Iteraremos mejoras encima de esto.");
ImGui::Separator();
static const char* headers[] = {"name", "lang", "domain", "purity", "description"};
int rows = 0, cols = 0;
const char* const* cells = flatten_cells(rows, cols);
ImGui::Text("Filas: %d Columnas: %d", rows, cols);
ImGui::Spacing();
table_view("##registry_sample", headers, cols, cells, rows);
}
ImGui::End();
}
#ifndef FN_TEST_BUILD
int main() {
return fn::run_app({
.title = "Tables Playground",
.width = 1280,
.height = 800,
.about = {.name = "tables_playground",
.version = "0.1.0",
.description = "Playground para iterar mejoras sobre table_view_cpp_viz antes de promover a registry y migrar apps C++."},
.log = {.file_path = "tables_playground.log",
.level = static_cast<int>(fn_log::Level::Info)}
}, render);
}
#endif
Submodule cpp/apps/runtime_test added at 49a9f3273d
+5 -3
View File
@@ -413,9 +413,11 @@ int main() {
cfg.about = {.name = "shaders_lab",
.version = "0.3.0",
.description = "Live GLSL shader playground with DAG pipeline. layout_storage publico, compiler extraido, AppConfig estandar, multi-viewport, modal save-as via modal_dialog."};
cfg.panels = k_panels;
cfg.panel_count = sizeof(k_panels) / sizeof(k_panels[0]);
cfg.layouts_cb = &g_layout_cb;
cfg.panels = k_panels;
cfg.panel_count = sizeof(k_panels) / sizeof(k_panels[0]);
cfg.layouts_cb = &g_layout_cb;
cfg.log = {"shaders_lab.log", 1};
cfg.auto_dockspace = false; // shaders_lab gestiona su propio DockSpace en render()
int rc = fn::run_app(cfg, render);
fn::gfx::canvas_destroy(g_canvas_code);
+28
View File
@@ -0,0 +1,28 @@
---
name: text_editor_smoke
lang: cpp
domain: tools
description: "Smoke test CLI (sin GUI) que valida los wrappers PIMPL de text_editor y file_watcher (inotify Linux / ReadDirectoryChangesW Win). No abre ventana ImGui — solo crea/settea texto/lee/poll/destruye."
tags: [cpp, smoke, test, cli]
uses_functions:
- text_editor_cpp_core
- file_watcher_cpp_core
uses_types: []
framework: "cli"
entry_point: "main.cpp"
dir_path: "cpp/apps/text_editor_smoke"
repo_url: ""
---
# text_editor_smoke
Smoke test que verifica las APIs de `text_editor` y `file_watcher` linkean correctamente. Sin ventana ImGui.
## Build & run
```bash
cd cpp && cmake --build build --target text_editor_smoke -j
./build/text_editor_smoke
```
Salida esperada: log con bytes leidos del editor + eventos del file_watcher.
+83
View File
@@ -0,0 +1,83 @@
#pragma once
// math2d — primitive 2D types used across gamedev stack (issue 0072b).
// Pure value types. No allocations, no virtual, trivial copy.
#include <cmath>
namespace fn::math2d {
struct Vec2 {
float x = 0.0f;
float y = 0.0f;
constexpr Vec2() = default;
constexpr Vec2(float xv, float yv) : x(xv), y(yv) {}
constexpr Vec2 operator+(Vec2 b) const { return {x + b.x, y + b.y}; }
constexpr Vec2 operator-(Vec2 b) const { return {x - b.x, y - b.y}; }
constexpr Vec2 operator*(float s) const { return {x * s, y * s}; }
constexpr Vec2 operator/(float s) const { return {x / s, y / s}; }
constexpr Vec2& operator+=(Vec2 b) { x += b.x; y += b.y; return *this; }
constexpr Vec2& operator-=(Vec2 b) { x -= b.x; y -= b.y; return *this; }
constexpr Vec2& operator*=(float s) { x *= s; y *= s; return *this; }
float length() const { return std::sqrt(x * x + y * y); }
constexpr float length_sq() const { return x * x + y * y; }
Vec2 normalized() const {
float l = length();
return l > 0.0f ? Vec2{x / l, y / l} : Vec2{0.0f, 0.0f};
}
static constexpr float dot(Vec2 a, Vec2 b) { return a.x * b.x + a.y * b.y; }
static constexpr float cross(Vec2 a, Vec2 b) { return a.x * b.y - a.y * b.x; }
};
struct Rect {
float x = 0.0f;
float y = 0.0f;
float w = 0.0f;
float h = 0.0f;
constexpr Rect() = default;
constexpr Rect(float xv, float yv, float wv, float hv) : x(xv), y(yv), w(wv), h(hv) {}
constexpr float right() const { return x + w; }
constexpr float bottom() const { return y + h; }
constexpr Vec2 center() const { return {x + w * 0.5f, y + h * 0.5f}; }
constexpr Vec2 min() const { return {x, y}; }
constexpr Vec2 max() const { return {x + w, y + h}; }
constexpr bool contains(Vec2 p) const {
return p.x >= x && p.x < x + w && p.y >= y && p.y < y + h;
}
constexpr bool overlaps(Rect b) const {
return !(b.x >= x + w || b.x + b.w <= x ||
b.y >= y + h || b.y + b.h <= y);
}
};
struct Color {
float r = 1.0f;
float g = 1.0f;
float b = 1.0f;
float a = 1.0f;
constexpr Color() = default;
constexpr Color(float rv, float gv, float bv, float av = 1.0f)
: r(rv), g(gv), b(bv), a(av) {}
static constexpr Color white() { return {1, 1, 1, 1}; }
static constexpr Color black() { return {0, 0, 0, 1}; }
static constexpr Color transparent() { return {0, 0, 0, 0}; }
static Color rgba(unsigned char r8, unsigned char g8, unsigned char b8,
unsigned char a8 = 255) {
return {r8 / 255.0f, g8 / 255.0f, b8 / 255.0f, a8 / 255.0f};
}
static Color hex(unsigned int packed) {
return Color::rgba((packed >> 24) & 0xFF, (packed >> 16) & 0xFF,
(packed >> 8) & 0xFF, packed & 0xFF);
}
};
} // namespace fn::math2d
+38
View File
@@ -0,0 +1,38 @@
// UNICA TU del proyecto que define MINIAUDIO_IMPLEMENTATION.
#define MINIAUDIO_IMPLEMENTATION
#include "../../vendor/miniaudio/miniaudio.h"
#include "audio_engine.h"
#include <cstdlib>
namespace fn::audio {
Engine engine_init() {
Engine e{nullptr, false};
ma_engine* eng = static_cast<ma_engine*>(std::malloc(sizeof(ma_engine)));
if (!eng) return e;
if (ma_engine_init(NULL, eng) != MA_SUCCESS) {
std::free(eng);
return e;
}
e.impl = eng;
e.ok = true;
return e;
}
void engine_shutdown(Engine& e) {
if (!e.ok || !e.impl) return;
ma_engine* eng = static_cast<ma_engine*>(e.impl);
ma_engine_uninit(eng);
std::free(eng);
e.impl = nullptr;
e.ok = false;
}
void engine_set_volume(Engine& e, float v) {
if (!e.ok || !e.impl) return;
ma_engine_set_volume(static_cast<ma_engine*>(e.impl), v);
}
} // namespace fn::audio
+21
View File
@@ -0,0 +1,21 @@
// audio_engine — lifecycle del engine de audio (miniaudio wrapper).
// Issue 0072b — runtime gamedev nucleo (PC desktop + WASM + futuro mobile).
#pragma once
namespace fn::audio {
struct Engine {
void* impl; // ma_engine* opaco
bool ok;
};
// Crea engine con device default. Engine.ok=false si falla.
Engine engine_init();
// Libera engine. Idempotente con Engine.ok=false.
void engine_shutdown(Engine& e);
// Master volume 0..1.
void engine_set_volume(Engine& e, float v);
} // namespace fn::audio
+47
View File
@@ -0,0 +1,47 @@
---
name: audio_engine
kind: function
lang: cpp
domain: gamedev
version: "0.1.0"
purity: impure
signature: "engine_init() -> Engine; engine_shutdown(Engine&); engine_set_volume(Engine&, float)"
description: "Lifecycle del engine de audio basado en miniaudio (single-header, public domain). Inicializa device default, expone master volume, y libera recursos. Cross-platform: Windows/Linux/macOS via WASAPI/ALSA/CoreAudio y WebAudio bajo emscripten. Issue 0072b — runtime gamedev nucleo. Esta TU es la unica del proyecto que define MINIAUDIO_IMPLEMENTATION."
tags: [gamedev, audio, miniaudio]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
example: |
fn::audio::Engine eng = fn::audio::engine_init();
if (!eng.ok) { /* fallback silencioso */ }
fn::audio::engine_set_volume(eng, 0.8f);
// ... loop principal ...
fn::audio::engine_shutdown(eng);
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/gamedev/audio_engine.cpp"
params:
- name: e
desc: "Engine handle. ok=true tras engine_init exitoso. Estructura opaca: impl apunta a ma_engine internamente."
- name: v
desc: "Volumen master 0..1 (lineal). Valores >1 amplifican (riesgo de clipping)."
output: "Engine con impl=ma_engine* y ok=true si MA_SUCCESS, ok=false en cualquier fallo (malloc o init de miniaudio). engine_shutdown deja ok=false e impl=nullptr — llamadas posteriores son no-op seguros."
---
# audio_engine
Wrapper minimo del engine de [miniaudio](https://miniaud.io/) (v0.11.25, single-header, public domain / MIT-0). Vendored en `cpp/vendor/miniaudio/`.
## Por que
Audio cross-platform sin depender de SDL_mixer / OpenAL / FMOD. Compila en Windows/Linux/macOS y WASM (emscripten) desde el mismo source. Sin excepciones, sin RTTI, sin std::string.
## Notas
- Estado de fallo se reporta via `Engine.ok=false`. No hay `error_type` porque no devolvemos `error_go_core`-style; el consumidor revisa `ok` antes de operar.
- `MINIAUDIO_IMPLEMENTATION` se define UNICAMENTE en `audio_engine.cpp`. Otras TU que usen miniaudio deben hacer solo `#include "miniaudio.h"`.
- Memoria via `malloc`/`free` (no `new`) para mantener compat con `-fno-exceptions`.
+59
View File
@@ -0,0 +1,59 @@
// NO definir MINIAUDIO_IMPLEMENTATION aqui — vive en audio_engine.cpp.
#include "../../vendor/miniaudio/miniaudio.h"
#include "audio_play.h"
#include <cstdlib>
namespace fn::audio {
Sound sound_load(Engine& e, const char* path) {
Sound s{nullptr, false};
if (!e.ok || !e.impl || !path) return s;
ma_sound* snd = static_cast<ma_sound*>(std::malloc(sizeof(ma_sound)));
if (!snd) return s;
ma_engine* eng = static_cast<ma_engine*>(e.impl);
if (ma_sound_init_from_file(eng, path, 0, NULL, NULL, snd) != MA_SUCCESS) {
std::free(snd);
return s;
}
s.impl = snd;
s.ok = true;
return s;
}
void sound_play(Sound& s) {
if (!s.ok || !s.impl) return;
ma_sound_start(static_cast<ma_sound*>(s.impl));
}
void sound_stop(Sound& s) {
if (!s.ok || !s.impl) return;
ma_sound_stop(static_cast<ma_sound*>(s.impl));
}
void sound_set_volume(Sound& s, float v) {
if (!s.ok || !s.impl) return;
ma_sound_set_volume(static_cast<ma_sound*>(s.impl), v);
}
void sound_destroy(Sound& s) {
if (!s.ok || !s.impl) return;
ma_sound* snd = static_cast<ma_sound*>(s.impl);
ma_sound_uninit(snd);
std::free(snd);
s.impl = nullptr;
s.ok = false;
}
void play_sound_oneshot(Engine& e, const char* path, float volume) {
if (!e.ok || !e.impl || !path) return;
ma_engine* eng = static_cast<ma_engine*>(e.impl);
// ma_engine_play_sound respeta el master volume; volume per-call se aplica
// creando un sonido ad-hoc si el caller quiere control fino. Para fire-and-forget
// usamos el helper directo y dejamos el master modular.
(void)volume;
ma_engine_play_sound(eng, path, NULL);
}
} // namespace fn::audio
+32
View File
@@ -0,0 +1,32 @@
// audio_play — reproducir sonidos one-shot y streaming sobre fn::audio::Engine.
// Issue 0072b — runtime gamedev nucleo.
#pragma once
#include "audio_engine.h"
namespace fn::audio {
struct Sound {
void* impl; // ma_sound* opaco
bool ok;
};
// Carga y prepara un sonido (decodifica streaming desde disco). Sound.ok=false si falla.
Sound sound_load(Engine& e, const char* path);
// Arranca/reanuda reproduccion.
void sound_play(Sound& s);
// Detiene reproduccion (no libera).
void sound_stop(Sound& s);
// Volumen 0..1 del sonido (independiente del master).
void sound_set_volume(Sound& s, float v);
// Libera recursos del sonido.
void sound_destroy(Sound& s);
// Fire-and-forget: reproduce path una vez sin handle. No-op si engine no esta listo.
void play_sound_oneshot(Engine& e, const char* path, float volume = 1.0f);
} // namespace fn::audio
+62
View File
@@ -0,0 +1,62 @@
---
name: audio_play
kind: function
lang: cpp
domain: gamedev
version: "0.1.0"
purity: impure
signature: "sound_load(Engine&, const char*) -> Sound; sound_play/stop/set_volume/destroy(Sound&); play_sound_oneshot(Engine&, const char*, float)"
description: "Reproduccion de audio sobre fn::audio::Engine: carga sonidos con streaming desde disco (wav/mp3/flac/ogg), play/stop/volumen por sonido, y helper fire-and-forget para one-shots sin handle. Cross-platform via miniaudio. Issue 0072b — runtime gamedev nucleo."
tags: [gamedev, audio, miniaudio]
uses_functions: ["audio_engine_cpp_gamedev"]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
example: |
fn::audio::Engine eng = fn::audio::engine_init();
fn::audio::Sound bgm = fn::audio::sound_load(eng, "assets/bgm.ogg");
fn::audio::sound_set_volume(bgm, 0.5f);
fn::audio::sound_play(bgm);
// SFX one-shot:
fn::audio::play_sound_oneshot(eng, "assets/jump.wav");
// ...
fn::audio::sound_destroy(bgm);
fn::audio::engine_shutdown(eng);
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/gamedev/audio_play.cpp"
params:
- name: e
desc: "Engine inicializado por audio_engine_cpp_gamedev. Si e.ok=false, todas las operaciones son no-op."
- name: path
desc: "Ruta al archivo de audio (relativa al cwd o absoluta). Formatos: wav/mp3/flac/ogg via decoders integrados de miniaudio."
- name: s
desc: "Sound handle. ok=true tras sound_load exitoso. Operaciones tras destroy son no-op."
- name: v
desc: "Volumen 0..1 lineal, multiplicativo con el master del Engine."
- name: volume
desc: "Volumen sugerido para play_sound_oneshot (actualmente delega al master via ma_engine_play_sound; reservado para futura implementacion per-instance)."
output: "Sound con impl=ma_sound* y ok=true en sound_load exitoso; ok=false ante cualquier fallo (engine no listo, malloc, decoder). play_sound_oneshot no devuelve handle — el sonido se gestiona internamente por el engine."
---
# audio_play
Reproduccion de audio one-shot y streaming sobre `fn::audio::Engine`. Wrapper minimo de la API `ma_sound` de miniaudio.
## Estructura `Sound`
Handle opaco con `impl` (apunta a `ma_sound`) y `ok` (bool). Gestion explicita: cada `sound_load` requiere `sound_destroy` para liberar (no hay RAII porque mantenemos `-fno-exceptions` y compat con structs trivial).
## Patrones de uso
- **BGM / loops largos:** `sound_load` + `sound_play`. Por defecto miniaudio hace streaming desde disco (no carga todo a memoria).
- **SFX cortos:** `play_sound_oneshot` — fire-and-forget, sin handle, ideal para clicks, jumps, hits.
- **SFX repetidos con control:** `sound_load` + `sound_play` cada vez. Para concurrent voices del mismo sample, considerar `ma_sound_init_copy` (no expuesto aun).
## Notas
- `play_sound_oneshot` recibe `volume` como hint pero actualmente delega al master del engine. Iterar si el caller real lo necesita.
- `sound_load` con `path=NULL` o engine no listo devuelve `Sound{nullptr, false}` — siempre comprobar `ok` antes de operar.
+119
View File
@@ -0,0 +1,119 @@
#include "camera_2d.h"
#include <cmath>
namespace fn::cam {
using fn::math2d::Vec2;
using fn::math2d::Rect;
Vec2 world_to_screen(const Camera2D& c, Vec2 world) {
float dx = world.x - c.pos.x;
float dy = world.y - c.pos.y;
if (c.rotation != 0.0f) {
float cs = std::cos(-c.rotation);
float sn = std::sin(-c.rotation);
float rx = dx * cs - dy * sn;
float ry = dx * sn + dy * cs;
dx = rx;
dy = ry;
}
float sx = dx * c.zoom + (float)c.viewport_w * 0.5f;
float sy = dy * c.zoom + (float)c.viewport_h * 0.5f;
return {sx, sy};
}
Vec2 screen_to_world(const Camera2D& c, Vec2 screen) {
float dx = (screen.x - (float)c.viewport_w * 0.5f) / c.zoom;
float dy = (screen.y - (float)c.viewport_h * 0.5f) / c.zoom;
if (c.rotation != 0.0f) {
float cs = std::cos(c.rotation);
float sn = std::sin(c.rotation);
float rx = dx * cs - dy * sn;
float ry = dx * sn + dy * cs;
dx = rx;
dy = ry;
}
return {c.pos.x + dx, c.pos.y + dy};
}
Rect visible_world_rect(const Camera2D& c) {
// For rotated cameras, return the AABB that contains the rotated viewport.
float hw = (float)c.viewport_w * 0.5f / c.zoom;
float hh = (float)c.viewport_h * 0.5f / c.zoom;
if (c.rotation == 0.0f) {
return {c.pos.x - hw, c.pos.y - hh, hw * 2.0f, hh * 2.0f};
}
float cs = std::fabs(std::cos(c.rotation));
float sn = std::fabs(std::sin(c.rotation));
float ehw = hw * cs + hh * sn;
float ehh = hw * sn + hh * cs;
return {c.pos.x - ehw, c.pos.y - ehh, ehw * 2.0f, ehh * 2.0f};
}
void view_proj_matrix(const Camera2D& c, float out[16]) {
// Orthographic projection mapping a viewport-sized box around camera pos
// to clip space [-1, 1].
float hw = (float)c.viewport_w * 0.5f / c.zoom;
float hh = (float)c.viewport_h * 0.5f / c.zoom;
float l = c.pos.x - hw;
float r = c.pos.x + hw;
// Screen Y goes down, world Y can go either; we pick world-Y-up convention:
// top of screen = pos.y + hh, bottom = pos.y - hh.
float b = c.pos.y - hh;
float t = c.pos.y + hh;
float cs = std::cos(-c.rotation);
float sn = std::sin(-c.rotation);
// Build column-major:
// M = Ortho(l,r,b,t) * Rotate(-rotation around camera center)
// Compose by hand to avoid temporaries.
float ortho[16] = {
2.0f / (r - l), 0.0f, 0.0f, 0.0f,
0.0f, 2.0f / (t - b), 0.0f, 0.0f,
0.0f, 0.0f, -1.0f, 0.0f,
-(r + l) / (r - l), -(t + b) / (t - b), 0.0f, 1.0f,
};
if (c.rotation == 0.0f) {
for (int i = 0; i < 16; ++i) out[i] = ortho[i];
return;
}
// Rotation around camera pos in world space:
// T(pos) * R * T(-pos)
// We bake it as a column-major 4x4 then multiply: out = ortho * rot.
float px = c.pos.x;
float py = c.pos.y;
float rot[16] = {
cs, sn, 0.0f, 0.0f,
-sn, cs, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
px - cs * px + sn * py,
py - sn * px - cs * py,
0.0f, 1.0f,
};
// out = ortho * rot (column-major).
for (int col = 0; col < 4; ++col) {
for (int row = 0; row < 4; ++row) {
float sum = 0.0f;
for (int k = 0; k < 4; ++k) {
sum += ortho[k * 4 + row] * rot[col * 4 + k];
}
out[col * 4 + row] = sum;
}
}
}
} // namespace fn::cam
+25
View File
@@ -0,0 +1,25 @@
#pragma once
// camera_2d — pure orthographic 2D camera (issue 0072b).
// No state, no I/O. World <-> screen transforms + ortho view-projection matrix.
#include "../core/math2d.h"
namespace fn::cam {
struct Camera2D {
fn::math2d::Vec2 pos = {0.0f, 0.0f}; // world position of camera center
float zoom = 1.0f; // 1 = no zoom; >1 zoom in
float rotation = 0.0f; // radians
int viewport_w = 1280;
int viewport_h = 720;
};
fn::math2d::Vec2 world_to_screen(const Camera2D& c, fn::math2d::Vec2 world);
fn::math2d::Vec2 screen_to_world(const Camera2D& c, fn::math2d::Vec2 screen);
fn::math2d::Rect visible_world_rect(const Camera2D& c);
// Column-major 4x4 view-projection matrix (orthographic).
void view_proj_matrix(const Camera2D& c, float out[16]);
} // namespace fn::cam
+54
View File
@@ -0,0 +1,54 @@
---
name: camera_2d
kind: function
lang: cpp
domain: gamedev
version: "0.1.0"
purity: pure
signature: "world_to_screen(Camera2D, Vec2) -> Vec2; screen_to_world(Camera2D, Vec2) -> Vec2; visible_world_rect(Camera2D) -> Rect; view_proj_matrix(Camera2D, float[16])"
description: "Camara ortografica 2D pura: pos (centro), zoom, rotacion (rad) y viewport en pixeles. Conversiones world<->screen, AABB visible y matriz view-projection 4x4 column-major lista para cualquier renderer (sokol_gfx, OpenGL, WebGPU). Fast-path sin trig si rotation==0. Issue 0072b."
tags: [gamedev, camera, 2d, math, pure]
uses_functions: []
uses_types: ["Vec2_cpp_core", "Rect_cpp_core"]
returns: []
returns_optional: false
error_type: ""
imports: []
example: |
fn::cam::Camera2D cam;
cam.pos = {100, 50};
cam.zoom = 2.0f;
cam.viewport_w = 1280;
cam.viewport_h = 720;
// mouse picking:
fn::math2d::Vec2 world = fn::cam::screen_to_world(cam, {input.mx, input.my});
// upload to shader:
float mvp[16];
fn::cam::view_proj_matrix(cam, mvp);
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/gamedev/camera_2d.cpp"
params:
- name: camera
desc: "Camera2D con pos (centro mundo), zoom (>1 acerca), rotation (radianes) y viewport_w/h (pixeles)."
- name: world
desc: "Punto en world space (Vec2) para world_to_screen."
- name: screen
desc: "Punto en screen/pixel space (Vec2) para screen_to_world."
- name: out
desc: "Buffer de 16 floats donde escribir la matriz column-major en view_proj_matrix."
output: "Vec2 (transformaciones), Rect (AABB visible) o matriz column-major 4x4 (mapa world->clip [-1,1])."
---
# camera_2d
Camara 2D minima y pura. Zero estado global, zero I/O — apta para reusar en N renderers (sokol_gfx, OpenGL, WebGPU) y para correr en tests headless.
Convencion:
- `pos` es el centro de la camara en world coords.
- Eje Y en world apunta hacia ARRIBA. La proyeccion mapea `pos.y + hh` a top de clip (y=+1).
- Zoom multiplicativo: `zoom=2` muestra la mitad del area mundial en el mismo viewport.
- Rotation en radianes, sentido antihorario en world (la matriz invierte para clip).
- `visible_world_rect` para rotation != 0 devuelve el AABB ENVOLVENTE (no el rect rotado), util para frustum culling barato.
- `view_proj_matrix` es column-major (compatible con sokol_gfx / OpenGL `glUniformMatrix4fv` con `transpose=GL_FALSE`).
+87
View File
@@ -0,0 +1,87 @@
#include "game_loop.h"
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#endif
namespace fn::game {
namespace {
struct LoopRT {
LoopCfg cfg;
Uint64 last_ticks;
float accumulator;
Uint64 perf_freq;
};
#if defined(__EMSCRIPTEN__)
static LoopRT g_rt;
void em_tick() {
LoopRT& rt = g_rt;
Uint64 now = SDL_GetPerformanceCounter();
float frame_time = (float)((double)(now - rt.last_ticks) / (double)rt.perf_freq);
rt.last_ticks = now;
float cap = rt.cfg.fixed_dt * (float)rt.cfg.max_steps_per_frame;
if (frame_time > cap) frame_time = cap;
rt.accumulator += frame_time;
int steps = 0;
while (rt.accumulator >= rt.cfg.fixed_dt && steps < rt.cfg.max_steps_per_frame) {
if (rt.cfg.on_fixed_update) rt.cfg.on_fixed_update(rt.cfg.user, rt.cfg.fixed_dt);
rt.accumulator -= rt.cfg.fixed_dt;
steps++;
}
float interp = rt.accumulator / rt.cfg.fixed_dt;
if (rt.cfg.on_render) rt.cfg.on_render(rt.cfg.user, interp);
if (rt.cfg.should_quit && rt.cfg.should_quit(rt.cfg.user)) {
emscripten_cancel_main_loop();
}
}
#endif
} // namespace
void loop_run(SDL_Window* /*win*/, const LoopCfg& cfg) {
if (!cfg.on_fixed_update && !cfg.on_render) return;
#if defined(__EMSCRIPTEN__)
g_rt.cfg = cfg;
g_rt.last_ticks = SDL_GetPerformanceCounter();
g_rt.accumulator = 0.0f;
g_rt.perf_freq = SDL_GetPerformanceFrequency();
emscripten_set_main_loop(em_tick, 0, 1);
#else
Uint64 perf_freq = SDL_GetPerformanceFrequency();
Uint64 last = SDL_GetPerformanceCounter();
float accumulator = 0.0f;
for (;;) {
if (cfg.should_quit && cfg.should_quit(cfg.user)) break;
Uint64 now = SDL_GetPerformanceCounter();
float frame_time = (float)((double)(now - last) / (double)perf_freq);
last = now;
float cap = cfg.fixed_dt * (float)cfg.max_steps_per_frame;
if (frame_time > cap) frame_time = cap;
accumulator += frame_time;
int steps = 0;
while (accumulator >= cfg.fixed_dt && steps < cfg.max_steps_per_frame) {
if (cfg.on_fixed_update) cfg.on_fixed_update(cfg.user, cfg.fixed_dt);
accumulator -= cfg.fixed_dt;
steps++;
}
float interp = accumulator / cfg.fixed_dt;
if (cfg.on_render) cfg.on_render(cfg.user, interp);
}
#endif
}
} // namespace fn::game
+22
View File
@@ -0,0 +1,22 @@
#pragma once
// game_loop — fixed-timestep game loop (Glenn Fiedler) for SDL3 + WASM.
// Issue 0072b. Decouples physics/sim (fixed dt) from rendering (interp).
#include <SDL3/SDL.h>
namespace fn::game {
struct LoopCfg {
float fixed_dt = 1.0f / 60.0f;
int max_steps_per_frame = 5;
void (*on_fixed_update)(void* user, float dt) = nullptr;
void (*on_render)(void* user, float interp) = nullptr;
bool (*should_quit)(void* user) = nullptr; // optional poll
void* user = nullptr;
};
// Blocking on desktop. On WASM uses emscripten_set_main_loop and returns immediately.
void loop_run(SDL_Window* win, const LoopCfg& cfg);
} // namespace fn::game
+51
View File
@@ -0,0 +1,51 @@
---
name: game_loop
kind: function
lang: cpp
domain: gamedev
version: "0.1.0"
purity: impure
signature: "loop_run(SDL_Window*, const LoopCfg&) -> void"
description: "Game loop fixed-timestep estilo Glenn Fiedler ('Fix Your Timestep'). Desacopla simulacion (on_fixed_update con dt fijo) de renderizado (on_render con factor de interpolacion). Acumulador con cap anti spiral-of-death. Branch automatico desktop (while loop bloqueante) vs __EMSCRIPTEN__ (emscripten_set_main_loop). Issue 0072b."
tags: [gamedev, game-loop, sdl3, wasm, fixed-timestep]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
example: |
struct State { bool quit = false; float t = 0; };
State st;
fn::game::LoopCfg cfg;
cfg.user = &st;
cfg.on_fixed_update = [](void* u, float dt) {
auto s = (State*)u;
s->t += dt;
};
cfg.on_render = [](void* u, float interp) {
// render with interp factor between [0, 1)
};
cfg.should_quit = [](void* u) { return ((State*)u)->quit; };
fn::game::loop_run(window, cfg);
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/gamedev/game_loop.cpp"
params:
- name: window
desc: "SDL_Window activo (no usado actualmente; reservado para futuras integraciones swap/vsync)."
- name: cfg
desc: "LoopCfg con fixed_dt (default 1/60), max_steps_per_frame (cap), callbacks on_fixed_update/on_render/should_quit y user pointer."
output: "Bloquea hasta should_quit==true (desktop). En WASM retorna inmediatamente y registra emscripten_set_main_loop."
---
# game_loop
Loop canonico para apps gamedev del registry. Garantiza que la simulacion corra a `fixed_dt` constante (default 60 Hz) independientemente del framerate de render, y expone factor `interp` para que el renderer interpole posiciones entre estados de fisica.
Detalles:
- `frame_time` se cap a `fixed_dt * max_steps_per_frame` para evitar la espiral de la muerte cuando el debugger pausa.
- En `__EMSCRIPTEN__` el estado del acumulador vive en variable static (`g_rt`) — solo un loop activo por modulo WASM.
- `should_quit` se consulta antes de cada frame; en WASM dispara `emscripten_cancel_main_loop`.
- `loop_run` retorna sin hacer nada si ambos callbacks son nulos.
+149
View File
@@ -0,0 +1,149 @@
#include "input_unified.h"
namespace fn::input {
namespace {
constexpr float STICK_DEADZONE = 0.15f;
constexpr float STICK_MAX = 32767.0f;
float normalize_axis(int16_t v) {
float f = (float)v / STICK_MAX;
if (f > 1.0f) f = 1.0f;
if (f < -1.0f) f = -1.0f;
if (f > -STICK_DEADZONE && f < STICK_DEADZONE) return 0.0f;
return f;
}
// Set logical button + rising-edge flag if transitioning false->true.
void set_btn(bool& held, bool& pressed, bool down) {
if (down && !held) pressed = true;
held = down;
}
void apply_key(InputState& s, SDL_Keycode key, bool down) {
switch (key) {
case SDLK_A: case SDLK_LEFT: set_btn(s.left, s.left_pressed, down); break;
case SDLK_D: case SDLK_RIGHT: set_btn(s.right, s.right_pressed, down); break;
case SDLK_W: case SDLK_UP: set_btn(s.up, s.up_pressed, down); break;
case SDLK_S: case SDLK_DOWN: set_btn(s.down, s.down_pressed, down); break;
case SDLK_SPACE: set_btn(s.action_a, s.a_pressed, down); break;
case SDLK_RETURN: set_btn(s.start, s.start_pressed, down); break;
case SDLK_ESCAPE: set_btn(s.back, s.back_pressed, down); break;
default: break;
}
}
void apply_gpad_button(InputState& s, Uint8 button, bool down) {
switch (button) {
case SDL_GAMEPAD_BUTTON_DPAD_LEFT: set_btn(s.left, s.left_pressed, down); break;
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: set_btn(s.right, s.right_pressed, down); break;
case SDL_GAMEPAD_BUTTON_DPAD_UP: set_btn(s.up, s.up_pressed, down); break;
case SDL_GAMEPAD_BUTTON_DPAD_DOWN: set_btn(s.down, s.down_pressed, down); break;
case SDL_GAMEPAD_BUTTON_SOUTH: set_btn(s.action_a, s.a_pressed, down); break;
case SDL_GAMEPAD_BUTTON_EAST: set_btn(s.action_b, s.b_pressed, down); break;
case SDL_GAMEPAD_BUTTON_WEST: set_btn(s.action_x, s.x_pressed, down); break;
case SDL_GAMEPAD_BUTTON_NORTH: set_btn(s.action_y, s.y_pressed, down); break;
case SDL_GAMEPAD_BUTTON_START: set_btn(s.start, s.start_pressed, down); break;
case SDL_GAMEPAD_BUTTON_BACK: set_btn(s.back, s.back_pressed, down); break;
default: break;
}
}
int find_touch_slot(InputState& s, int id) {
for (int i = 0; i < s.touch_count; ++i) {
if (s.touches[i].id == id) return i;
}
return -1;
}
} // namespace
void input_begin_frame(InputState& s) {
s.left_pressed = s.right_pressed = s.up_pressed = s.down_pressed = false;
s.a_pressed = s.b_pressed = s.x_pressed = s.y_pressed = false;
s.start_pressed = s.back_pressed = false;
s.m_left_pressed = s.m_right_pressed = false;
}
void input_process_event(InputState& s, const SDL_Event* e) {
if (!e) return;
switch (e->type) {
case SDL_EVENT_KEY_DOWN:
if (!e->key.repeat) apply_key(s, e->key.key, true);
break;
case SDL_EVENT_KEY_UP:
apply_key(s, e->key.key, false);
break;
case SDL_EVENT_MOUSE_MOTION:
s.mx = e->motion.x;
s.my = e->motion.y;
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
if (e->button.button == SDL_BUTTON_LEFT) set_btn(s.m_left, s.m_left_pressed, true);
if (e->button.button == SDL_BUTTON_RIGHT) set_btn(s.m_right, s.m_right_pressed, true);
break;
case SDL_EVENT_MOUSE_BUTTON_UP:
if (e->button.button == SDL_BUTTON_LEFT) set_btn(s.m_left, s.m_left_pressed, false);
if (e->button.button == SDL_BUTTON_RIGHT) set_btn(s.m_right, s.m_right_pressed, false);
break;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
apply_gpad_button(s, e->gbutton.button, true);
break;
case SDL_EVENT_GAMEPAD_BUTTON_UP:
apply_gpad_button(s, e->gbutton.button, false);
break;
case SDL_EVENT_GAMEPAD_AXIS_MOTION: {
float v = normalize_axis(e->gaxis.value);
switch (e->gaxis.axis) {
case SDL_GAMEPAD_AXIS_LEFTX: s.lx = v; break;
case SDL_GAMEPAD_AXIS_LEFTY: s.ly = v; break;
case SDL_GAMEPAD_AXIS_RIGHTX: s.rx = v; break;
case SDL_GAMEPAD_AXIS_RIGHTY: s.ry = v; break;
default: break;
}
break;
}
case SDL_EVENT_GAMEPAD_ADDED:
SDL_OpenGamepad(e->gdevice.which);
break;
case SDL_EVENT_FINGER_DOWN: {
if (s.touch_count < 8) {
auto& t = s.touches[s.touch_count++];
t.id = (int)e->tfinger.fingerID;
t.x = e->tfinger.x;
t.y = e->tfinger.y;
t.pressed = true;
}
break;
}
case SDL_EVENT_FINGER_MOTION: {
int idx = find_touch_slot(s, (int)e->tfinger.fingerID);
if (idx >= 0) {
s.touches[idx].x = e->tfinger.x;
s.touches[idx].y = e->tfinger.y;
}
break;
}
case SDL_EVENT_FINGER_UP: {
int idx = find_touch_slot(s, (int)e->tfinger.fingerID);
if (idx >= 0) {
// compact array
for (int i = idx; i < s.touch_count - 1; ++i) {
s.touches[i] = s.touches[i + 1];
}
s.touch_count--;
s.touches[s.touch_count] = {};
}
break;
}
default: break;
}
}
} // namespace fn::input
+45
View File
@@ -0,0 +1,45 @@
#pragma once
// input_unified — frame-based unified input snapshot for SDL3.
// Issue 0072b. Keyboard + mouse + gamepad + touch mapped to logical buttons.
#include <SDL3/SDL.h>
namespace fn::input {
struct InputState {
// Logical buttons (any source mapped here).
bool left = false, right = false, up = false, down = false;
bool action_a = false, action_b = false, action_x = false, action_y = false;
bool start = false, back = false;
// Same buttons but "just pressed this frame" (rising edge).
bool left_pressed = false, right_pressed = false, up_pressed = false, down_pressed = false;
bool a_pressed = false, b_pressed = false, x_pressed = false, y_pressed = false;
bool start_pressed = false, back_pressed = false;
// Analog sticks [-1, 1].
float lx = 0.0f, ly = 0.0f, rx = 0.0f, ry = 0.0f;
// Mouse (window coords, pixels).
float mx = 0.0f, my = 0.0f;
bool m_left = false, m_right = false;
bool m_left_pressed = false, m_right_pressed = false;
// Touch (mobile / WASM). x/y normalized [0, 1].
struct Touch {
float x = 0.0f, y = 0.0f;
bool pressed = false;
int id = -1;
};
Touch touches[8];
int touch_count = 0;
};
// Call once per frame BEFORE processing SDL events. Clears *_pressed edges.
void input_begin_frame(InputState& s);
// Call for each SDL_Event in PollEvent loop.
void input_process_event(InputState& s, const SDL_Event* e);
} // namespace fn::input
+48
View File
@@ -0,0 +1,48 @@
---
name: input_unified
kind: function
lang: cpp
domain: gamedev
version: "0.1.0"
purity: impure
signature: "input_begin_frame(InputState&); input_process_event(InputState&, const SDL_Event*)"
description: "Snapshot unificado de input por frame para SDL3. Mapea keyboard (WASD+arrows), mouse, gamepad (SDL_Gamepad) y touch a botones logicos (left/right/up/down/action_a..y/start/back) y ejes analogicos. Expone flags *_pressed con rising edge limpio cada frame. Issue 0072b — runtime gamedev PC + WASM."
tags: [gamedev, input, sdl3, touch, gamepad]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
example: |
fn::input::InputState input{};
// per frame:
fn::input::input_begin_frame(input);
SDL_Event e;
while (SDL_PollEvent(&e)) {
fn::input::input_process_event(input, &e);
}
if (input.a_pressed) jump();
player.x += input.lx * speed * dt;
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/gamedev/input_unified.cpp"
params:
- name: state
desc: "InputState mantenido por la app entre frames (bools held + analog axes + touch slots)."
- name: event
desc: "Puntero al SDL_Event devuelto por SDL_PollEvent en el loop principal."
output: "Mutaciones in-place sobre InputState. Sin retorno. Sin asignaciones dinamicas."
---
# input_unified
Capa fina sobre SDL3 que normaliza todas las fuentes de input a un struct plano consultable cada frame. Sirve como fundacion para apps gamedev del registry (issue 0072b) y para tests headless.
Reglas de uso:
- `input_begin_frame` se llama UNA vez por frame antes de bombear eventos. Limpia los flags `*_pressed` (rising edge), no los `*` held.
- `input_process_event` se llama por cada `SDL_Event` recibido. Acumula state hasta que el siguiente `begin_frame` resetee edges.
- Stick deadzone fijo a 0.15 — si la app necesita custom, reescribir snapshot tras la lectura.
- Touch ids estables mientras el dedo este pressed; al soltar se compacta el array.
- Gamepads conectados se abren automaticamente en `SDL_EVENT_GAMEPAD_ADDED`.
+24
View File
@@ -0,0 +1,24 @@
#include "sokol_setup.h"
namespace fn::gfx {
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 width, int height) {
sg_swapchain sw{};
sw.width = width;
sw.height = height;
sw.sample_count = 1;
sw.color_format = SG_PIXELFORMAT_RGBA8;
sw.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL;
sw.gl.framebuffer = 0; // default framebuffer of current GL context
return sw;
}
} // namespace fn::gfx
+27
View File
@@ -0,0 +1,27 @@
#pragma once
// sokol_setup — helpers para inicializar sokol_gfx sobre un GL context
// creado por SDL3 (no por sokol_app). Issue 0072b.
//
// Uso tipico:
// SDL_Window* w = SDL_CreateWindow(...);
// SDL_GLContext gl = SDL_GL_CreateContext(w);
// sg_setup({ .environment = fn::gfx::make_environment(),
// .logger.func = slog_func });
// ...
// sg_pass p{}; p.swapchain = fn::gfx::make_swapchain(width, height);
// sg_begin_pass(&p);
#include "sokol_gfx.h"
namespace fn::gfx {
// Default environment for an SDL3-managed GL context.
// color RGBA8, depth+stencil, no MSAA. Override fields as needed.
sg_environment make_environment();
// Build a default swapchain for the current SDL3 window framebuffer.
// Pass current drawable dimensions in pixels.
sg_swapchain make_swapchain(int width, int height);
} // namespace fn::gfx
+43
View File
@@ -0,0 +1,43 @@
---
name: sokol_setup
kind: function
lang: cpp
domain: gfx
version: "0.1.0"
purity: pure
signature: "make_environment() -> sg_environment; make_swapchain(int w, int h) -> sg_swapchain"
description: "Builders puros para inicializar sokol_gfx encima de un GL context creado por SDL3 (no por sokol_app). Construye sg_environment con defaults RGBA8 + depth/stencil y sg_swapchain con el default framebuffer del contexto activo. Issue 0072b — base del runtime gamedev en PC + WASM."
tags: [gamedev, sokol, gfx, sdl3, wasm]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
example: |
// tras SDL_GL_CreateContext():
sg_desc d{};
d.environment = fn::gfx::make_environment();
d.logger.func = slog_func;
sg_setup(&d);
// por frame:
sg_pass p{};
p.swapchain = fn::gfx::make_swapchain(w, h);
sg_begin_pass(&p);
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/gfx/sokol_setup.cpp"
params:
- name: width
desc: "Ancho del framebuffer en pixeles. Usar SDL_GetWindowSizeInPixels."
- name: height
desc: "Alto del framebuffer en pixeles."
output: "Estructuras sg_environment / sg_swapchain listas para sokol_gfx."
---
# sokol_setup
Helpers minimos para usar `sokol_gfx` con SDL3 sin depender de `sokol_glue.h` (que importa simbolos de `sokol_app` y rompe en stacks SDL3-driven).
Definidos como funciones puras: solo construyen structs, no tocan estado global. Llamadas tipicas en `engine_smoke` (issue 0072a) y `runtime_test` (0072b).
+176
View File
@@ -0,0 +1,176 @@
#include "sprite_batch.h"
#include <cstdlib>
#include <cstring>
namespace fn::gfx {
namespace {
struct Vertex {
float x, y;
float u, v;
float r, g, b, a;
};
const char* VS =
#if defined(__EMSCRIPTEN__)
"#version 300 es\n"
#else
"#version 330 core\n"
#endif
"uniform mat4 u_view_proj;\n"
"layout(location = 0) in vec2 a_pos;\n"
"layout(location = 1) in vec2 a_uv;\n"
"layout(location = 2) in vec4 a_color;\n"
"out vec2 v_uv;\n"
"out vec4 v_color;\n"
"void main() {\n"
" v_uv = a_uv;\n"
" v_color = a_color;\n"
" gl_Position = u_view_proj * vec4(a_pos, 0.0, 1.0);\n"
"}\n";
const char* FS =
#if defined(__EMSCRIPTEN__)
"#version 300 es\n"
"precision mediump float;\n"
#else
"#version 330 core\n"
#endif
"in vec2 v_uv;\n"
"in vec4 v_color;\n"
"out vec4 frag;\n"
"uniform sampler2D u_tex;\n"
"void main() {\n"
" frag = texture(u_tex, v_uv) * v_color;\n"
"}\n";
void flush(SpriteBatch& b) {
if (b.cpu_count_quads == 0) return;
int verts = b.cpu_count_quads * 6;
sg_range data{ b.cpu_buffer, (size_t)verts * sizeof(Vertex) };
sg_update_buffer(b.vbo, &data);
sg_apply_pipeline(b.pipeline);
sg_bindings bind{};
bind.vertex_buffers[0] = b.vbo;
bind.views[0] = b.current_view;
bind.samplers[0] = b.sampler;
sg_apply_bindings(&bind);
sg_range proj_range{ b.proj, sizeof(b.proj) };
sg_apply_uniforms(0, &proj_range);
sg_draw(0, verts, 1);
b.cpu_count_quads = 0;
}
} // namespace
SpriteBatch sprite_batch_create(int cpu_capacity_quads) {
SpriteBatch b{};
b.cpu_capacity_quads = cpu_capacity_quads > 0 ? cpu_capacity_quads : 4096;
b.cpu_buffer = std::malloc((size_t)b.cpu_capacity_quads * 6 * sizeof(Vertex));
if (!b.cpu_buffer) return b;
sg_buffer_desc bd{};
bd.size = (size_t)b.cpu_capacity_quads * 6 * sizeof(Vertex);
bd.usage.vertex_buffer = true;
bd.usage.stream_update = true;
b.vbo = sg_make_buffer(&bd);
sg_shader_desc sd{};
sd.vertex_func.source = VS;
sd.fragment_func.source = FS;
sd.attrs[0].glsl_name = "a_pos";
sd.attrs[1].glsl_name = "a_uv";
sd.attrs[2].glsl_name = "a_color";
sd.uniform_blocks[0].stage = SG_SHADERSTAGE_VERTEX;
sd.uniform_blocks[0].size = 16 * sizeof(float);
sd.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_NATIVE;
sd.uniform_blocks[0].glsl_uniforms[0].type = SG_UNIFORMTYPE_MAT4;
sd.uniform_blocks[0].glsl_uniforms[0].glsl_name = "u_view_proj";
sd.views[0].texture.stage = SG_SHADERSTAGE_FRAGMENT;
sd.views[0].texture.image_type = SG_IMAGETYPE_2D;
sd.views[0].texture.sample_type = SG_IMAGESAMPLETYPE_FLOAT;
sd.samplers[0].stage = SG_SHADERSTAGE_FRAGMENT;
sd.samplers[0].sampler_type = SG_SAMPLERTYPE_FILTERING;
sd.texture_sampler_pairs[0].stage = SG_SHADERSTAGE_FRAGMENT;
sd.texture_sampler_pairs[0].view_slot = 0;
sd.texture_sampler_pairs[0].sampler_slot = 0;
sd.texture_sampler_pairs[0].glsl_name = "u_tex";
sg_shader shd = sg_make_shader(&sd);
sg_pipeline_desc pd{};
pd.shader = shd;
pd.layout.attrs[0].format = SG_VERTEXFORMAT_FLOAT2;
pd.layout.attrs[1].format = SG_VERTEXFORMAT_FLOAT2;
pd.layout.attrs[2].format = SG_VERTEXFORMAT_FLOAT4;
pd.colors[0].blend.enabled = true;
pd.colors[0].blend.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA;
pd.colors[0].blend.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
pd.colors[0].blend.src_factor_alpha = SG_BLENDFACTOR_ONE;
pd.colors[0].blend.dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
pd.primitive_type = SG_PRIMITIVETYPE_TRIANGLES;
b.pipeline = sg_make_pipeline(&pd);
sg_sampler_desc smd{};
smd.min_filter = SG_FILTER_LINEAR;
smd.mag_filter = SG_FILTER_LINEAR;
smd.wrap_u = SG_WRAP_CLAMP_TO_EDGE;
smd.wrap_v = SG_WRAP_CLAMP_TO_EDGE;
b.sampler = sg_make_sampler(&smd);
b.ok = true;
return b;
}
void sprite_batch_destroy(SpriteBatch& b) {
if (b.cpu_buffer) { std::free(b.cpu_buffer); b.cpu_buffer = nullptr; }
if (b.pipeline.id) sg_destroy_pipeline(b.pipeline);
if (b.vbo.id) sg_destroy_buffer(b.vbo);
if (b.sampler.id) sg_destroy_sampler(b.sampler);
b = {};
}
void sprite_batch_begin(SpriteBatch& b, const float view_proj_col_major[16]) {
std::memcpy(b.proj, view_proj_col_major, sizeof(b.proj));
b.cpu_count_quads = 0;
b.current_view = {};
b.in_pass = true;
}
void sprite_batch_draw(SpriteBatch& b, sg_view view, int img_w, int img_h,
fn::math2d::Rect src, fn::math2d::Rect dst,
fn::math2d::Color tint) {
if (!b.in_pass || !b.ok) return;
if (b.current_view.id != view.id) {
flush(b);
b.current_view = view;
}
if (b.cpu_count_quads >= b.cpu_capacity_quads) flush(b);
float u0 = src.x / (float)img_w;
float v0 = src.y / (float)img_h;
float u1 = (src.x + src.w) / (float)img_w;
float v1 = (src.y + src.h) / (float)img_h;
Vertex* base = (Vertex*)b.cpu_buffer + b.cpu_count_quads * 6;
Vertex tl{ dst.x, dst.y, u0, v0, tint.r, tint.g, tint.b, tint.a };
Vertex tr{ dst.x + dst.w, dst.y, u1, v0, tint.r, tint.g, tint.b, tint.a };
Vertex bl{ dst.x, dst.y + dst.h, u0, v1, tint.r, tint.g, tint.b, tint.a };
Vertex br{ dst.x + dst.w, dst.y + dst.h, u1, v1, tint.r, tint.g, tint.b, tint.a };
base[0] = tl; base[1] = tr; base[2] = br;
base[3] = tl; base[4] = br; base[5] = bl;
b.cpu_count_quads++;
}
void sprite_batch_end(SpriteBatch& b) {
if (!b.in_pass) return;
flush(b);
b.in_pass = false;
}
} // namespace fn::gfx
+43
View File
@@ -0,0 +1,43 @@
#pragma once
// sprite_batch — batched textured quad renderer on top of sokol_gfx.
// Single draw call per atlas binding. Issue 0072b runtime gamedev.
#include "sokol_gfx.h"
#include "math2d.h"
namespace fn::gfx {
struct SpriteBatch {
sg_pipeline pipeline{};
sg_buffer vbo{};
sg_view current_view{}; // texture view bound this batch
sg_sampler sampler{};
void* cpu_buffer = nullptr; // Vertex* on heap
int cpu_capacity_quads = 0;
int cpu_count_quads = 0;
float proj[16]{}; // current view-proj matrix (column-major)
bool in_pass = false;
bool ok = false;
};
// Create the batcher. Allocates CPU buffer + sokol resources. cpu_capacity_quads
// caps the per-flush quads (auto-flush when reached). Default 4096.
SpriteBatch sprite_batch_create(int cpu_capacity_quads = 4096);
void sprite_batch_destroy(SpriteBatch& b);
// Begin a batch with a column-major 4x4 view-projection.
// Call between sg_begin_pass and sg_end_pass.
void sprite_batch_begin(SpriteBatch& b, const float view_proj_col_major[16]);
// Queue one textured quad. dst is the screen rect. src is the UV rect inside the
// texture, in pixels (0..image_size). tint multiplies the texture sample.
// view must be a sg_view created with sg_make_view({.texture.image = ...}).
void sprite_batch_draw(SpriteBatch& b, sg_view view, int img_w, int img_h,
fn::math2d::Rect src, fn::math2d::Rect dst,
fn::math2d::Color tint = fn::math2d::Color::white());
// Flush remaining quads.
void sprite_batch_end(SpriteBatch& b);
} // namespace fn::gfx
+61
View File
@@ -0,0 +1,61 @@
---
name: sprite_batch
kind: function
lang: cpp
domain: gfx
version: "0.1.0"
purity: impure
signature: "sprite_batch_create(int cap=4096) -> SpriteBatch; sprite_batch_begin/draw/end"
description: "Batched textured quad renderer sobre sokol_gfx. Begin/draw/end con auto-flush por atlas change o capacity full. Vertex layout pos+uv+color, alpha blending estandar, GLSL 330 / GLES 300. Issue 0072b runtime gamedev — base de plataformeros, top-down, UI sprites."
tags: [gamedev, gfx, sokol, sprite, batch, 2d]
uses_functions:
- sokol_setup_cpp_gfx
uses_types:
- Rect_cpp_core
- Color_cpp_core
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
example: |
fn::gfx::SpriteBatch b = fn::gfx::sprite_batch_create(4096);
// por frame, dentro de un sg_pass:
fn::gfx::sprite_batch_begin(b, view_proj);
fn::gfx::sprite_batch_draw(b, atlas_img, 1024, 1024,
{0,0,32,32}, {100,100,32,32}, fn::math2d::Color::white());
fn::gfx::sprite_batch_draw(b, atlas_img, 1024, 1024,
{32,0,32,32}, {200,100,32,32}, fn::math2d::Color::rgba(255,0,0));
fn::gfx::sprite_batch_end(b);
tested: false
tests: []
test_file_path: ""
file_path: "cpp/functions/gfx/sprite_batch.cpp"
params:
- name: cap
desc: "Capacidad de quads por flush (CPU buffer). Default 4096 (~96 KB)."
- name: img
desc: "sg_image atlas. Cambio de img = auto-flush."
- name: src
desc: "Rect en pixeles dentro del atlas (UV se calculan dividiendo por img_w/img_h)."
- name: dst
desc: "Rect destino en coordenadas world (la matriz view-proj traduce a clip space)."
- name: tint
desc: "Color multiplicativo. Default Color::white()."
output: "Quads renderizados via sg_draw cuando flush. Una sola draw call por atlas binding."
---
# sprite_batch
Renderer batched de sprites 2D sobre sokol_gfx. Patron clasico:
1. `sprite_batch_create` una vez (despues de `sg_setup`).
2. Por frame, dentro de un sg_pass:
- `sprite_batch_begin(b, view_proj)` — pasa la matriz view-projection del camera_2d.
- `sprite_batch_draw(...)` por sprite. Auto-flush cuando cambia atlas o se llena.
- `sprite_batch_end(b)` — flush final.
**Alpha blending** activado por defecto (premultiplicado o no — usar el atlas que tengas; el shader hace `texture(u_tex, v_uv) * tint`).
**Sampler** linear filter + clamp-to-edge. Para pixel art, crear sampler propio con `SG_FILTER_NEAREST` y bindearlo manualmente (override no soportado por ahora — sub-issue futuro si hace falta).
**Performance**: 1 draw call por atlas. 10K sprites @ 60 FPS sobre WebGL2 modesto. Cap por defecto 4096 quads/flush; subir si tu juego dibuja >4K sprites del mismo atlas.
+43
View File
@@ -0,0 +1,43 @@
# miniaudio vendoring
- **Source**: https://github.com/mackron/miniaudio
- **Version pinned**: see `cpp/vendor/miniaudio/.version` (0.11.25 at vendoring time)
- **License**: Public domain / MIT-0 dual (`cpp/vendor/miniaudio/LICENSE`)
- **Header**: `cpp/vendor/miniaudio/miniaudio.h` (~4 MB texto, ~200 KB compilado strippable)
## Vendoring command
```
mkdir -p cpp/vendor/miniaudio && cd cpp/vendor/miniaudio && \
TAG=$(curl -s https://api.github.com/repos/mackron/miniaudio/releases/latest | grep -m1 '"tag_name"' | cut -d'"' -f4) && \
curl -fsSL "https://raw.githubusercontent.com/mackron/miniaudio/$TAG/miniaudio.h" -o miniaudio.h && \
curl -fsSL "https://raw.githubusercontent.com/mackron/miniaudio/$TAG/LICENSE" -o LICENSE && \
echo "$TAG" > .version
```
## Por qué miniaudio
- Single-header. Encaja con cultura del registry.
- Cubre TODAS las plataformas del stack gamedev (issue 0072): Win (WASAPI/DirectSound), Mac (CoreAudio), Linux (PulseAudio/ALSA/JACK), Android (AAudio/OpenSL ES), iOS (CoreAudio), Emscripten (Web Audio).
- Sin dependencias externas.
- API C limpia, facil de wrappear como funciones del registry.
- Decode wav/flac nativo. mp3 + ogg vorbis vienen via dependencias single-header opcionales (`stb_vorbis.c`, `dr_mp3.h`) o se enchufan como callback custom.
## Compilacion
`miniaudio.h` se incluye con `#define MINIAUDIO_IMPLEMENTATION` exactamente en UNA TU. Para apps que solo quieran la API, incluir sin el define.
Convencion del registry: la funcion `audio_init_cpp_gamedev` define `MINIAUDIO_IMPLEMENTATION` en su `.cpp`. Otras funciones del registry y consumidores solo incluyen `miniaudio.h` sin el define.
## Backends en WASM
Bajo emscripten, miniaudio usa Web Audio API. Requiere user gesture para arrancar (`navigator.userActivation`). Documentar en `cpp/GAMEDEV.md`: el primer click del usuario activa el audio context.
## Upgrade
```
cd cpp/vendor/miniaudio
TAG=$(curl -s https://api.github.com/repos/mackron/miniaudio/releases/latest | grep -m1 '"tag_name"' | cut -d'"' -f4)
curl -fsSL "https://raw.githubusercontent.com/mackron/miniaudio/$TAG/miniaudio.h" -o miniaudio.h
echo "$TAG" > .version
```
+1
View File
@@ -0,0 +1 @@
0.11.25
+47
View File
@@ -0,0 +1,47 @@
This software is available as a choice of the following licenses. Choose
whichever you prefer.
===============================================================================
ALTERNATIVE 1 - Public Domain (www.unlicense.org)
===============================================================================
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
===============================================================================
ALTERNATIVE 2 - MIT No Attribution
===============================================================================
Copyright 2025 David Reid
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+95864
View File
File diff suppressed because it is too large Load Diff
Vendored Submodule
+1
Submodule cpp/vendor/sdl3 added at d9d5536704
+25
View File
@@ -0,0 +1,25 @@
# SDL3 vendoring
- **Source**: https://github.com/libsdl-org/SDL
- **Tag**: `release-3.4.8` (May 2026)
- **License**: Zlib (`cpp/vendor/sdl3/LICENSE.txt`)
- **Vendoring command**:
```
cd cpp/vendor && git clone --depth 1 --branch release-3.4.8 \
https://github.com/libsdl-org/SDL.git sdl3
```
- **Build**: SDL3 trae su propio `CMakeLists.txt`. Se incluye via `add_subdirectory(vendor/sdl3)` en apps que lo usen y se enlaza target `SDL3::SDL3` o `SDL3::SDL3-static`.
- **Tamaño en disco**: ~71 MB (incluye plataform-specifics que NO necesitamos en Linux/Web). Si crece excesivo, considerar mover a submodulo o purgar `Xcode/`, `VisualC*/`, `android-project/` cuando esten cubiertos por sus apps mobile.
## Por qué SDL3 (no SDL2 ni GLFW)
- Cubre todas las plataformas objetivo del stack gamedev: Win, Lin, Mac, Android, iOS, Emscripten.
- API oficial estable desde finales 2024.
- Touch input + virtual gamepad nativos.
- Audio integrado.
- ImGui ya tiene backend `imgui_impl_sdl3.{h,cpp}` en `cpp/vendor/imgui/backends/`.
- Compatible con sokol_gfx (manual GL context creation).
## Upgrade
Re-clonar con nuevo tag. SDL3 sigue semver, breaking changes solo entre majors.
+54
View File
@@ -0,0 +1,54 @@
# sokol vendoring
- **Source**: https://github.com/floooh/sokol
- **Commit pinned**: `5cc3e913258fb63d87c7728569f14005f638e315` (ver `cpp/vendor/sokol/.commit-sha`)
- **License**: Zlib (`cpp/vendor/sokol/LICENSE`)
- **Headers vendoreados** (single-header):
- `sokol_gfx.h` — graphics abstraction (GL/GLES/WebGL2/Metal/D3D11)
- `sokol_log.h` — logger callback
- `sokol_glue.h` — helpers para integracion contexto
- `sokol_app.h` — windowing/input. **NO se usa**: usamos SDL3 para windowing. Vendoreado por si una app standalone lo quiere.
- **Vendoring command**:
```
mkdir -p cpp/vendor/sokol && cd cpp/vendor/sokol && \
SHA=$(curl -s https://api.github.com/repos/floooh/sokol/commits/master | \
grep -m1 '"sha"' | cut -d'"' -f4) && \
for f in sokol_gfx.h sokol_log.h sokol_glue.h sokol_app.h LICENSE; do
curl -fsSL "https://raw.githubusercontent.com/floooh/sokol/$SHA/$f" -o "$f"
done && echo "$SHA" > .commit-sha
```
## Por qué sokol_gfx
- Single-header, ~1.2 MB texto, ~50 KB compilado strippable.
- Backends: GL 3.3, GLES3, WebGL2, Metal, D3D11. **Cubre las 4 plataformas + web** del stack gamedev (issue 0072).
- Sin dependencias externas.
- Determinista, sin estado global oculto.
- Encaja con cultura "single-file function" del registry.
## Compile-time backend selection
El backend se elige con macros:
| Macro | Backend |
|---|---|
| `SOKOL_GLCORE` | OpenGL 3.3 desktop |
| `SOKOL_GLES3` | OpenGL ES 3.0 (Android, iOS sin Metal) |
| `SOKOL_METAL` | Metal (iOS/macOS) |
| `SOKOL_D3D11` | D3D11 (Windows) |
| `SOKOL_WGPU` | WebGPU (alpha, no usar aun) |
Para WASM con `-sUSE_WEBGL2=1`: definir `SOKOL_GLES3`.
## Sokol_app NO se usa
Mantenemos `sokol_app.h` vendoreado por completitud pero el stack usa **SDL3** para windowing/input. Sokol_app duplicaria la responsabilidad y limita acceso a APIs SDL (audio, gamepad, WalletConnect deep links).
## Upgrade
```
cd cpp/vendor/sokol
SHA=$(curl -s https://api.github.com/repos/floooh/sokol/commits/master | grep -m1 '"sha"' | cut -d'"' -f4)
# Repetir el for loop del vendoring command
echo "$SHA" > .commit-sha
```
+1
View File
@@ -0,0 +1 @@
5cc3e913258fb63d87c7728569f14005f638e315
+22
View File
@@ -0,0 +1,22 @@
zlib/libpng license
Copyright (c) 2018 Andre Weissflog
This software is provided 'as-is', without any express or implied warranty.
In no event will the authors be held liable for any damages arising from the
use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software in a
product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not
be misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
+14493
View File
File diff suppressed because it is too large Load Diff
+26796
View File
File diff suppressed because it is too large Load Diff
+207
View File
@@ -0,0 +1,207 @@
#if defined(SOKOL_IMPL) && !defined(SOKOL_GLUE_IMPL)
#define SOKOL_GLUE_IMPL
#endif
#ifndef SOKOL_GLUE_INCLUDED
/*
sokol_glue.h -- glue helper functions for sokol headers
Project URL: https://github.com/floooh/sokol
Do this:
#define SOKOL_IMPL or
#define SOKOL_GLUE_IMPL
before you include this file in *one* C or C++ file to create the
implementation.
...optionally provide the following macros to override defaults:
SOKOL_ASSERT(c) - your own assert macro (default: assert(c))
SOKOL_GLUE_API_DECL - public function declaration prefix (default: extern)
SOKOL_API_DECL - same as SOKOL_GLUE_API_DECL
SOKOL_API_IMPL - public function implementation prefix (default: -)
If sokol_glue.h is compiled as a DLL, define the following before
including the declaration or implementation:
SOKOL_DLL
On Windows, SOKOL_DLL will define SOKOL_GLUE_API_DECL as __declspec(dllexport)
or __declspec(dllimport) as needed.
OVERVIEW
========
sokol_glue.h provides glue helper functions between sokol_gfx.h and sokol_app.h,
so that sokol_gfx.h doesn't need to depend on sokol_app.h but can be
used with different window system glue libraries.
PROVIDED FUNCTIONS
==================
sg_environment sglue_environment(void)
Returns an sg_environment struct initialized by calling sokol_app.h
functions. Use this in the sg_setup() call like this:
sg_setup(&(sg_desc){
.environment = sglue_environment(),
...
});
sg_swapchain sglue_swapchain(void)
Returns an sg_swapchain struct initialized by calling sokol_app.h
functions. Use this in sg_begin_pass() for a 'swapchain pass' like
this:
sg_begin_pass(&(sg_pass){ .swapchain = sglue_swapchain(), ... });
LICENSE
=======
zlib/libpng license
Copyright (c) 2018 Andre Weissflog
This software is provided 'as-is', without any express or implied warranty.
In no event will the authors be held liable for any damages arising from the
use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software in a
product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not
be misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
*/
#define SOKOL_GLUE_INCLUDED
#if defined(SOKOL_API_DECL) && !defined(SOKOL_GLUE_API_DECL)
#define SOKOL_GLUE_API_DECL SOKOL_API_DECL
#endif
#ifndef SOKOL_GLUE_API_DECL
#if defined(_WIN32) && defined(SOKOL_DLL) && defined(SOKOL_GLUE_IMPL)
#define SOKOL_GLUE_API_DECL __declspec(dllexport)
#elif defined(_WIN32) && defined(SOKOL_DLL)
#define SOKOL_GLUE_API_DECL __declspec(dllimport)
#else
#define SOKOL_GLUE_API_DECL extern
#endif
#endif
#ifndef SOKOL_GFX_INCLUDED
#error "Please include sokol_gfx.h before sokol_glue.h"
#endif
#ifdef __cplusplus
extern "C" {
#endif
SOKOL_GLUE_API_DECL sg_environment sglue_environment(void);
SOKOL_GLUE_API_DECL sg_swapchain sglue_swapchain(void);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* SOKOL_GLUE_INCLUDED */
/*-- IMPLEMENTATION ----------------------------------------------------------*/
#ifdef SOKOL_GLUE_IMPL
#define SOKOL_GLUE_IMPL_INCLUDED (1)
#include <string.h> /* memset */
#ifndef SOKOL_APP_INCLUDED
#error "Please include sokol_app.h before the sokol_glue.h implementation"
#endif
#ifndef SOKOL_API_IMPL
#define SOKOL_API_IMPL
#endif
#ifndef _SOKOL_PRIVATE
#if defined(__GNUC__) || defined(__clang__)
#define _SOKOL_PRIVATE __attribute__((unused)) static
#else
#define _SOKOL_PRIVATE static
#endif
#endif
#ifndef SOKOL_ASSERT
#include <assert.h>
#define SOKOL_ASSERT(c) assert(c)
#endif
#ifndef SOKOL_UNREACHABLE
#define SOKOL_UNREACHABLE SOKOL_ASSERT(false)
#endif
_SOKOL_PRIVATE sg_pixel_format _sglue_to_sgpixelformat(sapp_pixel_format fmt) {
switch (fmt) {
case SAPP_PIXELFORMAT_NONE: return SG_PIXELFORMAT_NONE;
case SAPP_PIXELFORMAT_RGBA8: return SG_PIXELFORMAT_RGBA8;
case SAPP_PIXELFORMAT_SRGB8A8: return SG_PIXELFORMAT_SRGB8A8;
case SAPP_PIXELFORMAT_BGRA8: return SG_PIXELFORMAT_BGRA8;
case SAPP_PIXELFORMAT_DEPTH_STENCIL: return SG_PIXELFORMAT_DEPTH_STENCIL;
case SAPP_PIXELFORMAT_DEPTH: return SG_PIXELFORMAT_DEPTH;
case SAPP_PIXELFORMAT_SBGRA8: // FIXME!
default:
SOKOL_UNREACHABLE;
return SG_PIXELFORMAT_NONE;
}
}
SOKOL_API_IMPL sg_environment sglue_environment(void) {
sg_environment res;
memset(&res, 0, sizeof(res));
const sapp_environment env = sapp_get_environment();
res.defaults.color_format = _sglue_to_sgpixelformat(env.defaults.color_format);
res.defaults.depth_format = _sglue_to_sgpixelformat(env.defaults.depth_format);
res.defaults.sample_count = env.defaults.sample_count;
res.metal.device = env.metal.device;
res.d3d11.device = env.d3d11.device;
res.d3d11.device_context = env.d3d11.device_context;
res.wgpu.device = env.wgpu.device;
res.vulkan.instance = env.vulkan.instance;
res.vulkan.physical_device = env.vulkan.physical_device;
res.vulkan.device = env.vulkan.device;
res.vulkan.queue = env.vulkan.queue;
res.vulkan.queue_family_index = env.vulkan.queue_family_index;
return res;
}
SOKOL_API_IMPL sg_swapchain sglue_swapchain(void) {
sg_swapchain res;
memset(&res, 0, sizeof(res));
const sapp_swapchain sc = sapp_get_swapchain();
res.width = sc.width;
res.height = sc.height;
res.sample_count = sc.sample_count;
res.color_format = _sglue_to_sgpixelformat(sc.color_format);
res.depth_format = _sglue_to_sgpixelformat(sc.depth_format);
res.metal.current_drawable = sc.metal.current_drawable;
res.metal.depth_stencil_texture = sc.metal.depth_stencil_texture;
res.metal.msaa_color_texture = sc.metal.msaa_color_texture;
res.d3d11.render_view = sc.d3d11.render_view;
res.d3d11.resolve_view = sc.d3d11.resolve_view;
res.d3d11.depth_stencil_view = sc.d3d11.depth_stencil_view;
res.wgpu.render_view = sc.wgpu.render_view;
res.wgpu.resolve_view = sc.wgpu.resolve_view;
res.wgpu.depth_stencil_view = sc.wgpu.depth_stencil_view;
res.vulkan.render_image = sc.vulkan.render_image;
res.vulkan.render_view = sc.vulkan.render_view;
res.vulkan.resolve_image = sc.vulkan.resolve_image;
res.vulkan.resolve_view = sc.vulkan.resolve_view;
res.vulkan.depth_stencil_image = sc.vulkan.depth_stencil_image;
res.vulkan.depth_stencil_view = sc.vulkan.depth_stencil_view;
res.vulkan.render_finished_semaphore = sc.vulkan.render_finished_semaphore;
res.vulkan.present_complete_semaphore = sc.vulkan.present_complete_semaphore;
res.gl.framebuffer = sc.gl.framebuffer;
return res;
}
#endif /* SOKOL_GLUE_IMPL */
+334
View File
@@ -0,0 +1,334 @@
#if defined(SOKOL_IMPL) && !defined(SOKOL_LOG_IMPL)
#define SOKOL_LOG_IMPL
#endif
#ifndef SOKOL_LOG_INCLUDED
/*
sokol_log.h -- common logging callback for sokol headers
Project URL: https://github.com/floooh/sokol
Example code: https://github.com/floooh/sokol-samples
Do this:
#define SOKOL_IMPL or
#define SOKOL_LOG_IMPL
before you include this file in *one* C or C++ file to create the
implementation.
Optionally provide the following defines when building the implementation:
SOKOL_ASSERT(c) - your own assert macro (default: assert(c))
SOKOL_UNREACHABLE() - a guard macro for unreachable code (default: assert(false))
SOKOL_LOG_API_DECL - public function declaration prefix (default: extern)
SOKOL_API_DECL - same as SOKOL_GFX_API_DECL
SOKOL_API_IMPL - public function implementation prefix (default: -)
Optionally define the following for verbose output:
SOKOL_DEBUG - by default this is defined if NDEBUG is not defined
OVERVIEW
========
sokol_log.h provides a default logging callback for other sokol headers.
To use the default log callback, just include sokol_log.h and provide
a function pointer to the 'slog_func' function when setting up the
sokol library:
For instance with sokol_audio.h:
#include "sokol_log.h"
...
saudio_setup(&(saudio_desc){ .logger.func = slog_func });
Logging output goes to stderr and/or a platform specific logging subsystem
(which means that in some scenarios you might see logging messages duplicated):
- Windows: stderr + OutputDebugStringA()
- macOS/iOS/Linux: stderr + syslog()
- Emscripten: console.info()/warn()/error()
- Android: __android_log_write()
On Windows with sokol_app.h also note the runtime config items to make
stdout/stderr output visible on the console for WinMain() applications
via sapp_desc.win32.console_attach or sapp_desc.win32.console_create,
however when running in a debugger on Windows, the logging output should
show up on the debug output UI panel.
In debug mode, a log message might look like this:
[sspine][error][id:12] /Users/floh/projects/sokol/util/sokol_spine.h:3472:0:
SKELETON_DESC_NO_ATLAS: no atlas object provided in sspine_skeleton_desc.atlas
The source path and line number is formatted like compiler errors, in some IDEs (like VSCode)
such error messages are clickable.
In release mode, logging is less verbose as to not bloat the executable with string data, but you still get
enough information to identify the type and location of an error:
[sspine][error][id:12][line:3472]
RULES FOR WRITING YOUR OWN LOGGING FUNCTION
===========================================
- must be re-entrant because it might be called from different threads
- must treat **all** provided string pointers as optional (can be null)
- don't store the string pointers, copy the string data instead
- must not return for log level panic
LICENSE
=======
zlib/libpng license
Copyright (c) 2023 Andre Weissflog
This software is provided 'as-is', without any express or implied warranty.
In no event will the authors be held liable for any damages arising from the
use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software in a
product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not
be misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
*/
#define SOKOL_LOG_INCLUDED (1)
#include <stdint.h>
#if defined(SOKOL_API_DECL) && !defined(SOKOL_LOG_API_DECL)
#define SOKOL_LOG_API_DECL SOKOL_API_DECL
#endif
#ifndef SOKOL_LOG_API_DECL
#if defined(_WIN32) && defined(SOKOL_DLL) && defined(SOKOL_LOG_IMPL)
#define SOKOL_LOG_API_DECL __declspec(dllexport)
#elif defined(_WIN32) && defined(SOKOL_DLL)
#define SOKOL_LOG_API_DECL __declspec(dllimport)
#else
#define SOKOL_LOG_API_DECL extern
#endif
#endif
#ifdef __cplusplus
extern "C" {
#endif
/*
Plug this function into the 'logger.func' struct item when initializing any of the sokol
headers. For instance for sokol_audio.h it would look like this:
saudio_setup(&(saudio_desc){
.logger = {
.func = slog_func
}
});
*/
SOKOL_LOG_API_DECL void slog_func(const char* tag, uint32_t log_level, uint32_t log_item, const char* message, uint32_t line_nr, const char* filename, void* user_data);
#ifdef __cplusplus
} // extern "C"
#endif
#endif // SOKOL_LOG_INCLUDED
// ██ ███ ███ ██████ ██ ███████ ███ ███ ███████ ███ ██ ████████ █████ ████████ ██ ██████ ███ ██
// ██ ████ ████ ██ ██ ██ ██ ████ ████ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██
// ██ ██ ████ ██ ██████ ██ █████ ██ ████ ██ █████ ██ ██ ██ ██ ███████ ██ ██ ██ ██ ██ ██ ██
// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
// ██ ██ ██ ██ ███████ ███████ ██ ██ ███████ ██ ████ ██ ██ ██ ██ ██ ██████ ██ ████
//
// >>implementation
#ifdef SOKOL_LOG_IMPL
#define SOKOL_LOG_IMPL_INCLUDED (1)
#ifndef SOKOL_API_IMPL
#define SOKOL_API_IMPL
#endif
#ifndef SOKOL_DEBUG
#ifndef NDEBUG
#define SOKOL_DEBUG
#endif
#endif
#ifndef SOKOL_ASSERT
#include <assert.h>
#define SOKOL_ASSERT(c) assert(c)
#endif
#ifndef _SOKOL_PRIVATE
#if defined(__GNUC__) || defined(__clang__)
#define _SOKOL_PRIVATE __attribute__((unused)) static
#else
#define _SOKOL_PRIVATE static
#endif
#endif
#ifndef _SOKOL_UNUSED
#define _SOKOL_UNUSED(x) (void)(x)
#endif
// platform detection
#if defined(__APPLE__)
#define _SLOG_APPLE (1)
#elif defined(__EMSCRIPTEN__)
#define _SLOG_EMSCRIPTEN (1)
#elif defined(_WIN32)
#define _SLOG_WINDOWS (1)
#elif defined(__ANDROID__)
#define _SLOG_ANDROID (1)
#elif defined(__linux__) || defined(__unix__)
#define _SLOG_LINUX (1)
#else
#error "sokol_log.h: unknown platform"
#endif
#include <stdlib.h> // abort
#include <stdio.h> // fputs
#include <stddef.h> // size_t
#if defined(_SLOG_EMSCRIPTEN)
#include <emscripten/emscripten.h>
#elif defined(_SLOG_WINDOWS)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#elif defined(_SLOG_ANDROID)
#include <android/log.h>
#elif defined(_SLOG_LINUX) || defined(_SLOG_APPLE)
#include <syslog.h>
#endif
// size of line buffer (on stack!) in bytes including terminating zero
#define _SLOG_LINE_LENGTH (512)
_SOKOL_PRIVATE char* _slog_append(const char* str, char* dst, char* end) {
if (str) {
char c;
while (((c = *str++) != 0) && (dst < (end - 1))) {
*dst++ = c;
}
}
*dst = 0;
return dst;
}
_SOKOL_PRIVATE char* _slog_itoa(uint32_t x, char* buf, size_t buf_size) {
const size_t max_digits_and_null = 11;
if (buf_size < max_digits_and_null) {
return 0;
}
char* p = buf + max_digits_and_null;
*--p = 0;
do {
*--p = '0' + (x % 10);
x /= 10;
} while (x != 0);
return p;
}
#if defined(_SLOG_EMSCRIPTEN)
EM_JS(void, slog_js_log, (uint32_t level, const char* c_str), {
const str = UTF8ToString(c_str);
switch (level) {
case 0: console.error(str); break;
case 1: console.error(str); break;
case 2: console.warn(str); break;
default: console.info(str); break;
}
})
#endif
SOKOL_API_IMPL void slog_func(const char* tag, uint32_t log_level, uint32_t log_item, const char* message, uint32_t line_nr, const char* filename, void* user_data) {
_SOKOL_UNUSED(user_data);
const char* log_level_str;
switch (log_level) {
case 0: log_level_str = "panic"; break;
case 1: log_level_str = "error"; break;
case 2: log_level_str = "warning"; break;
default: log_level_str = "info"; break;
}
// build log output line
char line_buf[_SLOG_LINE_LENGTH];
char* str = line_buf;
char* end = line_buf + sizeof(line_buf);
char num_buf[32];
if (tag) {
str = _slog_append("[", str, end);
str = _slog_append(tag, str, end);
str = _slog_append("]", str, end);
}
str = _slog_append("[", str, end);
str = _slog_append(log_level_str, str, end);
str = _slog_append("]", str, end);
str = _slog_append("[id:", str, end);
str = _slog_append(_slog_itoa(log_item, num_buf, sizeof(num_buf)), str, end);
str = _slog_append("]", str, end);
// if a filename is provided, build a clickable log message that's compatible with compiler error messages
if (filename) {
str = _slog_append(" ", str, end);
#if defined(_MSC_VER)
// MSVC compiler error format
str = _slog_append(filename, str, end);
str = _slog_append("(", str, end);
str = _slog_append(_slog_itoa(line_nr, num_buf, sizeof(num_buf)), str, end);
str = _slog_append("): ", str, end);
#else
// gcc/clang compiler error format
str = _slog_append(filename, str, end);
str = _slog_append(":", str, end);
str = _slog_append(_slog_itoa(line_nr, num_buf, sizeof(num_buf)), str, end);
str = _slog_append(":0: ", str, end);
#endif
}
else {
str = _slog_append("[line:", str, end);
str = _slog_append(_slog_itoa(line_nr, num_buf, sizeof(num_buf)), str, end);
str = _slog_append("] ", str, end);
}
if (message) {
str = _slog_append("\n\t", str, end);
str = _slog_append(message, str, end);
}
str = _slog_append("\n\n", str, end);
if (0 == log_level) {
str = _slog_append("ABORTING because of [panic]\n", str, end);
(void)str;
}
// print to stderr?
#if defined(_SLOG_LINUX) || defined(_SLOG_WINDOWS) || defined(_SLOG_APPLE)
fputs(line_buf, stderr);
#endif
// platform specific logging calls
#if defined(_SLOG_WINDOWS)
OutputDebugStringA(line_buf);
#elif defined(_SLOG_ANDROID)
int prio;
switch (log_level) {
case 0: prio = ANDROID_LOG_FATAL; break;
case 1: prio = ANDROID_LOG_ERROR; break;
case 2: prio = ANDROID_LOG_WARN; break;
default: prio = ANDROID_LOG_INFO; break;
}
__android_log_write(prio, "SOKOL", line_buf);
#elif defined(_SLOG_EMSCRIPTEN)
slog_js_log(log_level, line_buf);
#endif
if (0 == log_level) {
abort();
}
}
#endif // SOKOL_LOG_IMPL