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:
@@ -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
@@ -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
@@ -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
+1
Submodule cpp/apps/engine_smoke added at bed33856e7
@@ -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).
|
||||
@@ -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
+1
Submodule cpp/apps/runtime_test added at 49a9f3273d
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`).
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Vendored
+43
@@ -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
|
||||
```
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
0.11.25
|
||||
Vendored
+47
@@ -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.
|
||||
Vendored
+95864
File diff suppressed because it is too large
Load Diff
+1
Submodule cpp/vendor/sdl3 added at d9d5536704
Vendored
+25
@@ -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.
|
||||
Vendored
+54
@@ -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
|
||||
```
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
5cc3e913258fb63d87c7728569f14005f638e315
|
||||
Vendored
+22
@@ -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.
|
||||
Vendored
+14493
File diff suppressed because it is too large
Load Diff
Vendored
+26796
File diff suppressed because it is too large
Load Diff
Vendored
+207
@@ -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 */
|
||||
Vendored
+334
@@ -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
|
||||
Reference in New Issue
Block a user