sdl_runtime_compiler git · main
SDL3 game for running and compiling code at runtime
C++ 72.3% C 26.2%git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gzCash Meshes and Add saved colors ToText With Claude
benchmark/bench_text_mesh.cpp | 2 +-
src/core/main.cpp | 1 +
src/levels/director.cpp | 16 ++-
src/levels/director.hpp | 2 +
src/states/game_state.cpp | 32 ++---
src/states/game_state.hpp | 2 +-
src/systems/program_system.cpp | 6 +-
src/text/cpp_highlight.cpp | 206 +++++++++++++++++++++++--------
src/text/handler.cpp | 20 +--
src/text/{text_mesh.cpp => mesh.cpp} | 231 ++++++++++++++++++-----------------
src/text/{text_mesh.hpp => mesh.hpp} | 0
src/text/text.hpp | 2 +-
src/text/token.cpp | 65 +++++++---
src/text/token.hpp | 28 ++++-
src/text/token_db.cpp | 4 +-
src/utils/frame_profiler.hpp | 19 ++-
src/wall/wall.cpp | 2 +-
17 files changed, 415 insertions(+), 223 deletions(-)
diff --git a/benchmark/bench_text_mesh.cpp b/benchmark/bench_text_mesh.cpp
index 02af821..5accf32 100644
--- a/benchmark/bench_text_mesh.cpp
+++ b/benchmark/bench_text_mesh.cpp
@@ -1,7 +1,7 @@
#include <benchmark/benchmark.h>
#include <string>
-#include "text/text_mesh.hpp"
+#include "text/mesh.hpp"
static const std::string FONT = "/usr/share/fonts/dejavu/DejaVuSansMono.ttf";
diff --git a/src/core/main.cpp b/src/core/main.cpp
index a2f545e..37d3cc6 100644
--- a/src/core/main.cpp
+++ b/src/core/main.cpp
@@ -11,6 +11,7 @@
int main()
{
+ FRAME_SORT_PEAK;
if (!SDL_Init(SDL_INIT_VIDEO)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_Init failed: %s", SDL_GetError());
diff --git a/src/levels/director.cpp b/src/levels/director.cpp
index 99c4922..dae0cf8 100644
--- a/src/levels/director.cpp
+++ b/src/levels/director.cpp
@@ -145,19 +145,23 @@ Director::teardown_Scene(GameContext& ctx)
void
Director::show_Details(GameContext& ctx) const
{
+ SPIKE("direktor.show_Details");
if (!ctx.info_wall) return;
Text::Handler& info = *ctx.info_wall;
info.clear();
if (_runner->is_Complete()) return;
- const std::string& details = _runner->get_Current().details;
- if (details.empty()) return;
-
- constexpr size_t MAX_INFO_LINE_CHARS = 92;
- DynArray<std::string> lines = Text::wrap_String(details, MAX_INFO_LINE_CHARS);
+ const Scene::Data* key = &_runner->get_Current().scene;
+ if (key != _detail_key) {
+ const std::string& details = _runner->get_Current().details;
+ _detail_lines = details.empty()
+ ? DynArray<std::string>{}
+ : Text::wrap_String(details, 92);
+ _detail_key = key;
+ }
- for (const std::string& line : lines)
+ for (const std::string& line : _detail_lines)
if (!line.empty()) info.append_Line(line);
}
diff --git a/src/levels/director.hpp b/src/levels/director.hpp
index f98a5a6..7a9593b 100644
--- a/src/levels/director.hpp
+++ b/src/levels/director.hpp
@@ -58,6 +58,8 @@ class Director
StepLedger _ledger;
DynArray<StepPlate> _step_plates;
DynArray<GameObject*> _step_scenery;
+ mutable const Scene::Data* _detail_key { nullptr };
+ mutable DynArray<std::string> _detail_lines;
void spawn_Grants (GameContext& ctx);
void setup_Scene (GameContext& ctx, const Scene::Data& scene);
diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp
index 66ab04f..2c60e79 100644
--- a/src/states/game_state.cpp
+++ b/src/states/game_state.cpp
@@ -229,8 +229,7 @@ GameState::on_Pick()
_level_director->record_PickedUp(word->text);
word->set_Look (ConfigColor::keep, Text::Token::depth_floor);
Wall::show_TokenInfo(_info, word->text);
-
- carry_BeginAnim(std::move(word), false);
+ carry_BeginAnim (std::move(word), false);
return true;
}
return false;
@@ -310,17 +309,15 @@ GameState::on_ShootAll()
_carried = CarriedLine();
Aim::Result aim;
- if (!all_words.empty()) {
- { FP_ZONE("dispatch.ShootAll.aim");
- aim = Aim::crosshair_Target(*_ctx.camera, _ctx.words, _ctx.scenery,
- _text_walls, World::get_BoxScale());
- }
- { FP_ZONE("dispatch.ShootAll.launch");
- _projectiles_sys->launch(_projectiles, std::move(all_words),
- _ctx.camera->fwd, _ctx.camera->pos, aim);
- }
- }
+ if (all_words.empty())
+ return;
+ { FP_ZONE("dispatch.ShootAll.aim");
+ aim = Aim::crosshair_Target(*_ctx.camera, _ctx.words, _ctx.scenery,
+ _text_walls, World::get_BoxScale()); }
+ { FP_ZONE("dispatch.ShootAll.launch");
+ _projectiles_sys->launch(_projectiles, std::move(all_words),
+ _ctx.camera->fwd, _ctx.camera->pos, aim); }
{ FP_ZONE("dispatch.ShootAll.details");
_level_director->show_Details(_ctx); }
{ FP_ZONE("dispatch.ShootAll.record");
@@ -592,13 +589,15 @@ GameState::handle_Input(FrameInput& input, double dt) {
case InputCommand::TypingStart: { FP_ZONE("on_TypingStart "); on_TypingStart (input);} break;
case InputCommand::TypingCommit: { FP_ZONE("on_TypingCommit"); on_TypingCommit(input);} break;
case InputCommand::TypingCancel: { FP_ZONE("on_TypingCancel"); on_TypingCancel(input);} break;
- case InputCommand::SandboxRestore: { FP_ZONE("on_TypingCancel"); sandbox_Restore();} break;
+ case InputCommand::SandboxRestore: { FP_ZONE("on_TypingCancel"); on_SandboxRestore();} break;
case InputCommand::ConnectPultSubmit: { FP_ZONE("on_WallConnectSubmit");on_WallConnectSubmit();} break;
case InputCommand::ConnectPultExit: { FP_ZONE("on_WallConnectExit"); on_WallConnectExit();} break;
}
}
- if (_wall_connection.get_Target() == Wall::Connection::Target::Editor) {
+ Wall::Connection::Target
+ target = _wall_connection.get_Target();
+ if (target == Wall::Connection::Target::Editor) {
{ FP_ZONE("handle_input.Editor");
Text::Handler::EditReq req = _editor.feed_Keys(input.vim_keys, _ctx.camera->pos.y);
input.vim_keys.clear();
@@ -618,7 +617,7 @@ GameState::handle_Input(FrameInput& input, double dt) {
}
}
}
- else if (_wall_connection.get_Target() == Wall::Connection::Target::Program) {
+ else if (target == Wall::Connection::Target::Program) {
{ FP_ZONE("handle_input.Program");
_wall_connection.set_ProgramBuffer(input.typing_buffer);
}
@@ -642,6 +641,7 @@ GameState::handle_Input(FrameInput& input, double dt) {
{ FP_ZONE("handle_input.onShootO"); if (on_ShootOne()) _shoot_cooldown = 0.15; }
}
+
void
GameState::update_PlayerCamera(FrameInput& input, double dt)
{
@@ -1072,7 +1072,7 @@ GameState::sandbox_LoadFileToWall()
void
-GameState::sandbox_Restore() {
+GameState::on_SandboxRestore() {
if (!_level_director || !_level_director->get_Runner().is_DynamicLib()) return;
if (SourceTokens::copy_File(sandbox_BackupPath(), sandbox_CanonicalPath()))
sandbox_LoadFileToWall();
diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp
index 9cc242f..1edad80 100644
--- a/src/states/game_state.hpp
+++ b/src/states/game_state.hpp
@@ -134,7 +134,7 @@ class GameState : public IState
void dyn_Clear ();
void dyn_Apply (const DynArray<DynObject>& objs);
void sandbox_LoadFileToWall ();
- void sandbox_Restore ();
+ void on_SandboxRestore ();
std::string sandbox_CanonicalPath() const;
std::string sandbox_BackupPath () const;
// dynamic-lib sandbox END
diff --git a/src/systems/program_system.cpp b/src/systems/program_system.cpp
index f1bbd02..1d3fcb4 100644
--- a/src/systems/program_system.cpp
+++ b/src/systems/program_system.cpp
@@ -38,18 +38,16 @@ ProgramSystem::close_Child()
void
ProgramSystem::run(Text::Handler& out)
{
- out.clear(); // wipe previous output from the Program wall
+ out.clear();
_count = 0;
- close_Child(); // stop any previous run cleanly
+ close_Child();
- // Nothing built yet — tell the player to compile first.
if (access(kProgramPath, X_OK) != 0) {
push_Line(out, "no program - compile first");
return;
}
- // Writing to a pipe whose read end has closed must not kill the game.
signal(SIGPIPE, SIG_IGN);
int in_pipe[2] { -1, -1 }; // parent writes [1] → child reads [0] (stdin)
diff --git a/src/text/cpp_highlight.cpp b/src/text/cpp_highlight.cpp
index 32b01dd..5bc5159 100644
--- a/src/text/cpp_highlight.cpp
+++ b/src/text/cpp_highlight.cpp
@@ -1,7 +1,8 @@
#include <cstring>
#include <mutex>
-#include <tree_sitter/api.h>
#include <unordered_map>
+#include <string>
+#include <tree_sitter/api.h>
#include "text/text.hpp"
#include "text/token.hpp"
@@ -10,79 +11,194 @@ extern "C" const TSLanguage* tree_sitter_cpp(void);
namespace {
-// Map a node's type string (+ its parent's) to our Text::Token::Type.
-// Returns false if this node should not emit a span (recurse into children).
-bool
-classify_Node(TSNode node, Text::Token::Type& out)
+thread_local TSParser* parser = [] {
+ TSParser* p = ts_parser_new();
+ ts_parser_set_language(p, tree_sitter_cpp());
+ return p;
+}();
+
+// ---------------------------
+// Keyword / literal fast path
+// ---------------------------
+Text::Token::Type classify_Keyword(const std::string& w)
+{
+ static const std::unordered_map<std::string, Text::Token::Type> kw = {
+ {"int", Text::Token::Type::Type},
+ {"float", Text::Token::Type::Type},
+ {"double", Text::Token::Type::Type},
+ {"char", Text::Token::Type::Type},
+ {"bool", Text::Token::Type::Type},
+ {"void", Text::Token::Type::Type},
+
+ {"true", Text::Token::Type::NumberLit},
+ {"false", Text::Token::Type::NumberLit},
+ {"nullptr", Text::Token::Type::NumberLit},
+
+ {"this", Text::Token::Type::Variable},
+
+ {"return", Text::Token::Type::Preprocessor},
+ {"namespace", Text::Token::Type::Preprocessor},
+ {"using", Text::Token::Type::Preprocessor},
+ {"template", Text::Token::Type::Preprocessor},
+ {"concept", Text::Token::Type::Preprocessor},
+ };
+
+ auto it = kw.find(w);
+ if (it != kw.end())
+ return it->second;
+
+ return Text::Token::Type::Identifier;
+}
+
+// ---------------------------
+// Tree-sitter node classifier
+// ---------------------------
+bool classify_Node(TSNode node, Text::Token::Type& out)
{
const char* t = ts_node_type(node);
- // Leaf categories that color directly.
- if (std::strcmp(t, "comment") == 0) { out = Text::Token::Type::Comment; return true; }
+ // comments
+ if (std::strcmp(t, "comment") == 0) {
+ out = Text::Token::Type::Comment;
+ return true;
+ }
+
+ // strings
if (std::strcmp(t, "string_literal") == 0 ||
std::strcmp(t, "raw_string_literal") == 0 ||
std::strcmp(t, "char_literal") == 0 ||
- std::strcmp(t, "system_lib_string") == 0) { out = Text::Token::Type::StringLit; return true; }
- if (std::strcmp(t, "number_literal") == 0) { out = Text::Token::Type::NumberLit; return true; }
+ std::strcmp(t, "system_lib_string") == 0) {
+ out = Text::Token::Type::StringLit;
+ return true;
+ }
+
+ // numbers
+ if (std::strcmp(t, "number_literal") == 0 ||
+ std::strcmp(t, "integer_literal") == 0 ||
+ std::strcmp(t, "floating_point_literal") == 0) {
+ out = Text::Token::Type::NumberLit;
+ return true;
+ }
+
+ // types
if (std::strcmp(t, "primitive_type") == 0 ||
std::strcmp(t, "type_identifier") == 0 ||
- std::strcmp(t, "sized_type_specifier") == 0) { out = Text::Token::Type::Type; return true; }
- if (std::strcmp(t, "namespace_identifier") == 0) { out = Text::Token::Type::Namespace; return true; }
- if (std::strcmp(t, "field_identifier") == 0) { out = Text::Token::Type::Variable; return true; }
+ std::strcmp(t, "sized_type_specifier") == 0 ||
+ std::strcmp(t, "template_type") == 0) {
+ out = Text::Token::Type::Type;
+ return true;
+ }
- // Identifiers: depend on parent.
+ // namespaces
+ if (std::strcmp(t, "namespace_identifier") == 0 ||
+ std::strcmp(t, "namespace_definition") == 0) {
+ out = Text::Token::Type::Namespace;
+ return true;
+ }
+
+ // fields
+ if (std::strcmp(t, "field_identifier") == 0) {
+ out = Text::Token::Type::Variable;
+ return true;
+ }
+
+ // operators / operator overloads
+ if (std::strcmp(t, "operator_name") == 0) {
+ out = Text::Token::Type::Function;
+ return true;
+ }
+
+ // preprocessor (keep tight, not prefix-based)
+ if (std::strncmp(t, "preproc_", 8) == 0) {
+ out = Text::Token::Type::Preprocessor;
+ return true;
+ }
+
+ // identifiers (context sensitive)
if (std::strcmp(t, "identifier") == 0) {
TSNode parent = ts_node_parent(node);
const char* p = ts_node_is_null(parent) ? "" : ts_node_type(parent);
+
if (std::strcmp(p, "function_declarator") == 0 ||
- std::strcmp(p, "call_expression") == 0)
- { out = Text::Token::Type::Function; return true; }
- out = Text::Token::Type::Variable; return true;
+ std::strcmp(p, "call_expression") == 0 ||
+ std::strcmp(p, "operator_name") == 0 ||
+ std::strcmp(p, "template_function") == 0) {
+ out = Text::Token::Type::Function;
+ return true;
+ }
+
+ if (std::strcmp(p, "qualified_identifier") == 0 ||
+ std::strcmp(p, "namespace_identifier") == 0) {
+ out = Text::Token::Type::Namespace;
+ return true;
+ }
+
+ out = Text::Token::Type::Variable;
+ return true;
}
- // Preprocessor: the #include/#define lines.
- if (std::strncmp(t, "preproc", 7) == 0) { out = Text::Token::Type::Preprocessor; return true; }
-
- return false; // not a terminal we color — recurse into children
+ return false;
}
-void
-walk(TSNode node, DynArray<Text::HiSpan>& out)
+// ---------------------------
+// Tree walk
+// ---------------------------
+void walk(TSNode node, DynArray<Text::HiSpan>& out)
{
Text::Token::Type type;
- if (ts_node_child_count(node) == 0) {
- // leaf — classify and emit if recognized
- if (classify_Node(node, type))
- out.push_back({ ts_node_start_byte(node), ts_node_end_byte(node), type });
+
+ uint32_t count = ts_node_child_count(node);
+
+ if (count == 0) {
+ if (classify_Node(node, type)) {
+ out.push_back({
+ ts_node_start_byte(node),
+ ts_node_end_byte(node),
+ type
+ });
+ }
return;
}
- // named-but-classifiable interior nodes (comments, strings can be interior)
+
if (classify_Node(node, type)) {
- out.push_back({ ts_node_start_byte(node), ts_node_end_byte(node), type });
- return; // don't descend into a classified span
+ out.push_back({
+ ts_node_start_byte(node),
+ ts_node_end_byte(node),
+ type
+ });
+ return;
}
- const uint32_t n = ts_node_child_count(node);
- for (uint32_t i = 0; i < n; ++i)
+
+ for (uint32_t i = 0; i < count; ++i)
walk(ts_node_child(node, i), out);
}
} // namespace
+// ---------------------------
+// Public API
+// ---------------------------
DynArray<Text::HiSpan>
Text::CppHighlight::classify(const std::string& source)
{
DynArray<Text::HiSpan> spans;
- TSParser* p = ts_parser_new();
- ts_parser_set_language(p, tree_sitter_cpp());
- TSTree* tree = ts_parser_parse_string(p, nullptr, source.c_str(),
- (uint32_t)source.size());
+
+ TSTree* tree = ts_parser_parse_string(
+ parser,
+ nullptr,
+ source.c_str(),
+ (uint32_t)source.size()
+ );
+
walk(ts_tree_root_node(tree), spans);
+
ts_tree_delete(tree);
- ts_parser_delete(p);
return spans;
}
-
+// ---------------------------
+// Word classification (FIXED)
+// ---------------------------
Text::Token::Type
Text::CppHighlight::classify_Word(const std::string& word)
{
@@ -92,23 +208,17 @@ Text::CppHighlight::classify_Word(const std::string& word)
{
std::lock_guard<std::mutex> lk(mtx);
auto it = cache.find(word);
- if (it != cache.end()) return it->second;
+ if (it != cache.end())
+ return it->second;
}
- // First sight: classify this lone token with tree-sitter.
- DynArray<Text::HiSpan> spans = classify(word);
-
- // Pick the span covering the most of the word (usually the only one).
- Text::Token::Type type = Text::Token::Type::Identifier;
- size_t best_len = 0;
- for (const Text::HiSpan& s : spans) {
- size_t len = s.byte_end - s.byte_begin;
- if (len > best_len) { best_len = len; type = s.type; }
- }
+ // fast deterministic classification (NO tree-sitter here)
+ Text::Token::Type type = classify_Keyword(word);
{
std::lock_guard<std::mutex> lk(mtx);
cache[word] = type;
}
+
return type;
}
diff --git a/src/text/handler.cpp b/src/text/handler.cpp
index 821e4e5..8fa461d 100644
--- a/src/text/handler.cpp
+++ b/src/text/handler.cpp
@@ -49,6 +49,7 @@ make_Number(const std::string& s, const Vec3& pos, float yaw, float scale)
TokenPtr t = make_Token(s, Token::depth_wall, scale, pos, ConfigColor::orange);
t->yaw = yaw;
t->set_Color(ConfigColor::orange);
+ t->set_ColorSource(ColorSource::Static);
return t;
}
@@ -57,9 +58,9 @@ void
set_NumberText(Token::Data* t, const std::string& s)
{
if (t->text == s) return;
- t->text = s;
- t->rebuild_Mesh();
- t->set_Color (ConfigColor::orange);
+ t->retext(s);
+ t->set_Color (ConfigColor::orange);
+ t->set_ColorSource(ColorSource::Static);
}
} // namespace
@@ -551,8 +552,7 @@ Handler::apply_Highlight(const DynArray<HiSpan>& spans, uint64_t gen)
if (ov > best) { best = ov; type = s.type; found = true; }
}
}
- if (found)
- t->color = Token::get_Color(type);
+ if (found) t->set_Context(Token::get_Color(type));
}
}
}
@@ -968,9 +968,13 @@ Handler::end_Edit()
if (_vim) for (const std::string& s : _vim->get_Lines()) lines.push_back(s);
_vim.reset();
- _face = FaceMode::Pitch;
- _layout = LayoutMode::Snapped;
- set_Lines(lines);
+ _face = FaceMode::Pitch; _layout = LayoutMode::Snapped;
+
+ for (Line& l : _lines)
+ for (TokenPtr& t : l.tokens)
+ if (t) t->set_ColorSource(ColorSource::Lexed);
+ relayout_Snapped();
+ mark_Dirty();
}
diff --git a/src/text/text_mesh.cpp b/src/text/mesh.cpp
similarity index 62%
rename from src/text/text_mesh.cpp
rename to src/text/mesh.cpp
index 67edd81..fc54790 100644
--- a/src/text/text_mesh.cpp
+++ b/src/text/mesh.cpp
@@ -8,7 +8,7 @@
//
// Dependencies: freetype2, earcut.hpp (header-only)
-#include "text_mesh.hpp"
+#include "mesh.hpp"
#include "thirdparty/earcut/earcut.hpp"
#include "token.hpp"
@@ -247,139 +247,152 @@ void computeVertexNormals(TextMesh& mesh)
}
}
-bool
-TextMesh::build(const std::string& font_path,
- const std::string& text, int size, float depth, float scale)
+
+// ── Per-glyph geometry cache ──────────────────────────────────────────────────
+// A glyph's tessellated+extruded geometry depends only on (codepoint, scale)
+// now that depth is a draw-time Z-scale (mesh built at unit depth, faces ±0.5).
+// Build each glyph once; build() just stitches cached glyphs with an x-offset.
+namespace {
+
+struct GlyphKey {
+ uint32_t ch;
+ int scale_q; // scale quantised → stable float key
+ bool operator==(const GlyphKey& o) const { return ch == o.ch && scale_q == o.scale_q; }
+};
+struct GlyphKeyHash {
+ size_t operator()(const GlyphKey& k) const {
+ return std::hash<uint32_t>{}(k.ch) * 1000003u ^ static_cast<size_t>(k.scale_q);
+ }
+};
+
+struct GlyphMesh {
+ TextMesh m; // verts/tris/vertex_normals/is_front, at origin, unit depth
+ float advance = 0.0f; // scaled world-unit x advance
+};
+
+std::unordered_map<GlyphKey, GlyphMesh, GlyphKeyHash> g_glyphs;
+
+// Extrude one glyph's polys into `out` at unit depth (faces at z = ±0.5).
+void
+extrude_Polys(TextMesh& out, const DynArray<Poly>& polys)
{
- SPIKE("mesh.build");
- verts .clear();
- tris .clear();
- is_front.clear();
+ constexpr float half_d = 0.5f; // UNIT depth — scaled per-instance at draw
- FT_Face face = acquire_Face(font_path, size); // inits g_ft once, caches the face
- if (!face) return false;
+ for (const Poly& poly : polys) {
+ if (poly.empty() || poly[0].size() < 3) continue;
+
+ DynArray<uint32_t> indices = mapbox::earcut<uint32_t>(poly);
+ if (indices.empty()) continue;
+
+ DynArray<Pt2> flat;
+ for (auto& ring : poly) for (auto& p : ring) flat.push_back(p);
+
+ int base = static_cast<int>(out.verts.size());
+
+ for (auto& p : flat) { // front (z = -0.5)
+ out.verts.push_back({ (double)p[0], (double)p[1], (double)(-half_d) });
+ out.is_front.push_back(1.0f);
+ }
+ int back_base = static_cast<int>(out.verts.size());
+ for (auto& p : flat) { // back (z = +0.5)
+ out.verts.push_back({ (double)p[0], (double)p[1], (double)( half_d) });
+ out.is_front.push_back(0.0f);
+ }
+
+ for (size_t i = 0; i + 2 < indices.size(); i += 3)
+ out.tris.push_back({ base + (int)indices[i], base + (int)indices[i+2], base + (int)indices[i+1] });
+ for (size_t i = 0; i + 2 < indices.size(); i += 3)
+ out.tris.push_back({ back_base + (int)indices[i], back_base + (int)indices[i+1], back_base + (int)indices[i+2] });
+
+ for (const auto& ring : poly) { // side walls
+ int rn = static_cast<int>(ring.size());
+ for (int i = 0; i < rn; i++) {
+ int i1 = (i + 1) % rn;
+ int wb = static_cast<int>(out.verts.size());
+ const Pt2& p0 = ring[(size_t)i];
+ const Pt2& p1 = ring[(size_t)i1];
+ out.verts.push_back({ (double)p0[0], (double)p0[1], (double)(-half_d) }); out.is_front.push_back(0.0f);
+ out.verts.push_back({ (double)p1[0], (double)p1[1], (double)(-half_d) }); out.is_front.push_back(0.0f);
+ out.verts.push_back({ (double)p1[0], (double)p1[1], (double)( half_d) }); out.is_front.push_back(0.0f);
+ out.verts.push_back({ (double)p0[0], (double)p0[1], (double)( half_d) }); out.is_front.push_back(0.0f);
+ out.tris.push_back({ wb+0, wb+1, wb+2 });
+ out.tris.push_back({ wb+0, wb+2, wb+3 });
+ }
+ }
+ }
+}
+
+const GlyphMesh*
+acquire_Glyph(FT_Face face, uint32_t ch, float scale)
+{
+ GlyphKey key{ ch, (int)SDL_lround((double)scale * 1e7) };
+ auto it = g_glyphs.find(key);
+ if (it != g_glyphs.end()) return &it->second;
- float pen_x = 0.0f;
+ GlyphMesh gm;
+ FT_UInt gi = FT_Get_Char_Index(face, (FT_ULong)ch);
- for (const char ch : text) {
- FT_UInt gi = FT_Get_Char_Index(face, static_cast<FT_ULong>(static_cast<unsigned char>(ch)));
- if (FT_Load_Glyph(face, gi, FT_LOAD_NO_BITMAP)) continue;
- if (face->glyph->format != FT_GLYPH_FORMAT_OUTLINE) continue;
+ if (!FT_Load_Glyph(face, gi, FT_LOAD_NO_BITMAP) &&
+ face->glyph->format == FT_GLYPH_FORMAT_OUTLINE)
+ {
+ gm.advance = (float)face->glyph->advance.x * FT_SCALE * scale;
OutlineCtx ctx;
FT_Outline_Decompose(&face->glyph->outline, &kFTFuncs, &ctx);
ctx.flush();
-
- if (ctx.rings.empty()) {
- pen_x += static_cast<float>(face->glyph->advance.x) * FT_SCALE;
- continue;
+ if (!ctx.rings.empty()) {
+ DynArray<Poly> polys = groupContours(ctx.rings, 0.0f, scale); // origin, scaled
+ extrude_Polys(gm.m, polys);
+ computeVertexNormals(gm.m); // per-glyph, once
}
+ }
+ // failed load / non-outline → advance 0, no geometry (matches old skip)
- // Group into outer+holes polygons, handling multi-contour glyphs ('i','j','!',...)
- DynArray<Poly> polys = groupContours(ctx.rings, pen_x, scale);
+ auto [ins, ok] = g_glyphs.emplace(std::move(key), std::move(gm));
+ return &ins->second;
+}
- for (Poly& poly : polys) {
- if (poly.empty() || poly[0].size() < 3) continue;
+} // namespace
- DynArray<uint32_t> indices = mapbox::earcut<uint32_t>(poly);
- if (indices.empty()) continue;
- // Flatten rings for vertex lookup
- DynArray<Pt2> flat;
- for (auto& ring : poly)
- for (auto& p : ring)
- flat.push_back(p);
+bool
+TextMesh::build(const std::string& font_path,
+ const std::string& text, int size, float /*depth*/, float scale)
+{
+ SPIKE("mesh.build");
+ verts.clear(); tris.clear(); is_front.clear(); vertex_normals.clear();
- int base = static_cast<int>(verts.size());
- float half_d = depth * 0.5f;
+ FT_Face face = acquire_Face(font_path, size);
+ if (!face) return false;
- // Front verts (z = -half_d, facing -Z = toward viewer)
- for (auto& p : flat) {
- verts.push_back({ static_cast<double>(p[0]),
- static_cast<double>(p[1]),
- static_cast<double>(-half_d) });
- is_front.push_back(1.0f);
- }
- int back_base = static_cast<int>(verts.size());
- // Back verts (z = +half_d)
- for (auto& p : flat) {
- verts.push_back({ static_cast<double>(p[0]),
- static_cast<double>(p[1]),
- static_cast<double>( half_d) });
- is_front.push_back(0.0f);
- }
+ float pen_x = 0.0f; // scaled world units
+ for (const char chc : text) {
+ const GlyphMesh* g = acquire_Glyph(face, (unsigned char)chc, scale);
+ if (!g) continue;
- // Front face triangles
- for (size_t i = 0; i+2 < indices.size(); i += 3) {
- tris.push_back({ base + static_cast<int>(indices[i] ),
- base + static_cast<int>(indices[i+2]),
- base + static_cast<int>(indices[i+1]) });
- }
- // Back face triangles — natural winding so normal faces -Z.
- for (size_t i = 0; i+2 < indices.size(); i += 3)
- tris.push_back({ back_base + static_cast<int>(indices[i] ),
- back_base + static_cast<int>(indices[i+1]),
- back_base + static_cast<int>(indices[i+2]) });
-
- // Side walls: duplicate verts so they get hard outward normals
- // (NOT shared with face verts — this is the key fix for shading artifacts)
- int ring_offset_flat = base; // offset into flat[] for this ring
- for (auto& ring : poly) {
- int rn = static_cast<int>(ring.size());
- for (int i = 0; i < rn; i++) {
- int i1 = (i + 1) % rn;
-
- // 4 unique verts per quad — no sharing with face verts
- int wb = static_cast<int>(verts.size());
-
- const Pt2& p0 = ring[static_cast<size_t>(i)];
- const Pt2& p1 = ring[static_cast<size_t>(i1)];
-
- verts.push_back({ static_cast<double>(p0[0]),
- static_cast<double>(p0[1]),
- static_cast<double>(-half_d) }); // front-left
- is_front.push_back(0.0f);
- verts.push_back({ static_cast<double>(p1[0]),
- static_cast<double>(p1[1]),
- static_cast<double>(-half_d) }); // front-right
- is_front.push_back(0.0f);
- verts.push_back({ static_cast<double>(p1[0]),
- static_cast<double>(p1[1]),
- static_cast<double>( half_d) }); // back-right
- is_front.push_back(0.0f);
- verts.push_back({ static_cast<double>(p0[0]),
- static_cast<double>(p0[1]),
- static_cast<double>( half_d) }); // back-left
- is_front.push_back(0.0f);
-
- tris.push_back({ wb+0, wb+1, wb+2 });
- tris.push_back({ wb+0, wb+2, wb+3 });
- }
- ring_offset_flat += rn;
- }
- }
+ const int base = static_cast<int>(verts.size());
+ for (const Vec3& v : g->m.verts)
+ verts.push_back({ v.x + (double)pen_x, v.y, v.z }); // z stays unit (±0.5)
+ for (const Vec3& n : g->m.vertex_normals) vertex_normals.push_back(n);
+ for (float f : g->m.is_front) is_front.push_back(f);
+ for (const MeshTri& t : g->m.tris)
+ tris.push_back({ t.a + base, t.b + base, t.c + base });
- pen_x += static_cast<float>(face->glyph->advance.x) * FT_SCALE;
+ pen_x += g->advance;
}
- // Centre mesh on bounding box
+ // Centre on bounding box (x only) — unchanged.
if (!verts.empty()) {
- float minX=1e9f, maxX=-1e9f;
-
+ float minX = 1e9f, maxX = -1e9f;
for (const Vec3& v : verts) {
- float vx = static_cast<float>(v.x);
+ float vx = (float)v.x;
if (vx < minX) minX = vx;
if (vx > maxX) maxX = vx;
}
-
- double cx = static_cast<double>((minX + maxX) * 0.5f);
-
- for (Vec3& v : verts) {
- v.x -= cx;
- }
+ double cx = (double)((minX + maxX) * 0.5f);
+ for (Vec3& v : verts) v.x -= cx;
}
- computeVertexNormals(*this);
-
+ // Normals are cached per glyph — no whole-word recompute needed.
return !verts.empty();
}
diff --git a/src/text/text_mesh.hpp b/src/text/mesh.hpp
similarity index 100%
rename from src/text/text_mesh.hpp
rename to src/text/mesh.hpp
diff --git a/src/text/text.hpp b/src/text/text.hpp
index 13b0103..de2dc1b 100644
--- a/src/text/text.hpp
+++ b/src/text/text.hpp
@@ -15,7 +15,7 @@ Text
namespace Token {
struct Ai;
struct Data;
- struct Desc;
+ struct Description;
enum class Type : uint8_t;
}
diff --git a/src/text/token.cpp b/src/text/token.cpp
index 4791020..a0bb79b 100644
--- a/src/text/token.cpp
+++ b/src/text/token.cpp
@@ -86,7 +86,9 @@ Data::Data(const std::string& txt,
true, true, true),
text(txt), color(colr), depth(dpth), scale(scle)
{
- set_Color(Text::Token::get_Color(CppHighlight::classify_Word(text)));
+ single_color = Text::Token::get_Color(CppHighlight::classify_Word(text));
+ context_color = single_color; // until a lexer overrides
+ // `color` (Static slot) already initialised to `colr` in the member init list.
rebuild_Mesh();
body.hover_phase = Random::unit() * SDL_PI_D * 2.0;
@@ -108,34 +110,56 @@ Data::~Data()
void
-Data::set_Color(SDL_FColor c)
+Data::set_Color(SDL_FColor c) // the Static fixed colour
{
color = c;
}
+void
+Data::set_Context(SDL_FColor c) // lexer result inside a document
+{
+ context_color = c;
+ has_context = true;
+}
+
+void
+Data::clear_Context() // leaving a document → fall back to single
+{
+ has_context = false;
+}
+
void
Data::set_Look(SDL_FColor c, float new_depth)
{
- if (c.r == -1.0f) color = Text::Token::get_Color(CppHighlight::classify_Word(text));
- else if (c.g == -1.0f) { /* keep current colour */ }
- else color = c;
+ if (c.r == -1.0f) single_color = Text::Token::get_Color(CppHighlight::classify_Word(text));
+ else if (c.g == -1.0f) { /* keep current colours */ }
+ else color = c; // explicit colour → Static slot
set_Depth(new_depth);
}
void
-Data::recolor()
+Data::recolor() // recompute the tokenized (single) colour
{
- if (color_src == ColorSource::Lint)
- color = Text::Token::get_Color(CppHighlight::classify_Word(text));
+ single_color = Text::Token::get_Color(CppHighlight::classify_Word(text));
+ if (!has_context) context_color = single_color;
+}
+
+void
+Data::retext(const std::string& s) // text changed → new single colour + new mesh
+{
+ if (text == s) return;
+ text = s;
+ single_color = Text::Token::get_Color(CppHighlight::classify_Word(text));
+ if (!has_context) context_color = single_color;
+ rebuild_Mesh();
}
void
Data::set_Depth(float new_depth)
{
- if (new_depth == depth) return;
depth = new_depth;
- rebuild_Mesh();
+ shape.half_extents.z = (double)depth * 0.5;
}
@@ -231,19 +255,26 @@ Data::draw(GlRenderer& gl, const Camera& camera, const Light& light) const
{
if (!loaded || packed_indices.empty()) return;
- Mat4 proj = gl.get_Projection();
- Mat4 view = buildViewMat4 (camera);
- Mat4 model = buildModelMat4 (pos, yaw, pitch);
- Mat4 mvp = Mat4::mul (proj, Mat4::mul(view, model));
+ Mat4 proj = gl.get_Projection();
+ Mat4 view = buildViewMat4(camera);
+ Mat4 model_rot = buildModelMat4(pos, yaw, pitch);
+
+ Mat4 model = model_rot;
+ model.m[8] *= depth;
+ model.m[9] *= depth;
+ model.m[10] *= depth;
+
+ Mat4 mvp = Mat4::mul(proj, Mat4::mul(view, model));
+ const SDL_FColor ac = active_Color();
gl.set_Wireframe(false);
gl.set_Light (light);
gl.set_MVP (model, mvp);
- gl.set_NormalMat(model);
- gl.set_FaceShade(color.r, color.g, color.b, back_dim, front_tint);
+ gl.set_NormalMat(model_rot);
+ gl.set_FaceShade(ac.r, ac.g, ac.b, back_dim, front_tint);
if (glow_enabled && glow_strength > 0.0f)
- gl.set_Emissive(color.r, color.g, color.b, glow_strength);
+ gl.set_Emissive(ac.r, ac.g, ac.b, glow_strength);
else
gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f);
diff --git a/src/text/token.hpp b/src/text/token.hpp
index cac206e..ff05a7a 100644
--- a/src/text/token.hpp
+++ b/src/text/token.hpp
@@ -6,7 +6,7 @@
#include "entities/game_object.hpp"
#include "text/text.hpp"
-#include "text/text_mesh.hpp"
+#include "text/mesh.hpp"
#include "utils/config.hpp"
#include "utils/globals.hpp"
#include "utils/light.hpp"
@@ -24,7 +24,7 @@ Token
{
struct Ai;
struct Data;
- struct Desc;
+ struct Description;
enum class Type : uint8_t
{
@@ -53,7 +53,7 @@ Token
const char* get_TypeName (Type type);
SDL_FColor get_Color (Type t);
- Desc get_Description(const std::string& text, Type type);
+ Description get_Description(const std::string& text, Type type);
DynArray<std::string>
get_TestArrayAllKeywords();
// token_db.cpp END
@@ -81,7 +81,7 @@ namespace
Token
{
- struct Desc
+ struct Description
{
std::string short_desc;
std::string long_desc;
@@ -112,7 +112,10 @@ Token
struct Data final : public GameObject
{
std::string text;
- SDL_FColor color;
+ SDL_FColor color; // Static fixed colour (hint/label/info/append/numbers/floor)
+ SDL_FColor single_color {}; // classify_Word(text) — this word alone (Lint fallback)
+ SDL_FColor context_color {}; // lexer result inside a document (Lexed)
+ bool has_context { false }; // is context_color currently valid?
TextMesh mesh;
ColorSource color_src { ColorSource::Lint };
@@ -165,10 +168,23 @@ Token
~Data() override;
- void set_Color (SDL_FColor c);
+ void set_Color (SDL_FColor c); // sets the Static fixed colour
+ void set_Context (SDL_FColor c); // lexer: sets context_color + has_context
+ void clear_Context (); // leaving a document: drop context
void set_Look (SDL_FColor c, float new_depth);
void set_ColorSource(ColorSource src) { color_src = src; }
void recolor ();
+ void retext (const std::string& s); // text changed → recompute single_color + mesh
+
+ // Draw-time colour selection: state? → Static → context(if Lexed+has) → single.
+ SDL_FColor active_Color() const {
+ switch (color_src) {
+ case ColorSource::Static: return color;
+ case ColorSource::Lexed: return has_context ? context_color : single_color;
+ case ColorSource::Lint: return single_color;
+ }
+ return single_color;
+ }
void set_Depth (float depth);
void rebuild_Mesh();
diff --git a/src/text/token_db.cpp b/src/text/token_db.cpp
index 9b95079..df11d47 100644
--- a/src/text/token_db.cpp
+++ b/src/text/token_db.cpp
@@ -127,7 +127,7 @@ get_Color(Type t)
}
-Desc
+Description
generic_For(Type type)
{
switch (type) {
@@ -149,7 +149,7 @@ generic_For(Type type)
}
-Desc
+Description
get_Description(const std::string& text, Type type)
{
const auto& t = table();
diff --git a/src/utils/frame_profiler.hpp b/src/utils/frame_profiler.hpp
index 3fce987..e362eb4 100644
--- a/src/utils/frame_profiler.hpp
+++ b/src/utils/frame_profiler.hpp
@@ -33,12 +33,16 @@
# define FP_ZONE(name) ((void)0)
# define FRAME_BEGIN() ((void)0)
# define FRAME_END() ((void)0)
+# define FRAME_SORT(s) ((void)0)
#else
namespace fp
{
+enum class SortMode { Total, Peak, Avg };
+
+
inline double
perf_to_ms(Uint64 ticks)
{
@@ -67,6 +71,7 @@ struct Frame
Uint64 window_start {0}; // perf-counter ticks
Uint64 report_every_ns{1'000'000'000ull}; // 1 second
+ SortMode sort_mode {SortMode::Total};
std::array<Zone, MAX_ZONES> zones{};
@@ -117,9 +122,14 @@ struct Frame
std::array<size_t, MAX_ZONES> order{};
for (size_t i = 0; i < num_zones; ++i) order[i] = i;
std::sort(order.begin(), order.begin() + num_zones,
- [this](size_t a, size_t b) {
- return zones[a].total > zones[b].total;
- });
+ [this](size_t a, size_t b) {
+ switch (sort_mode) {
+ case SortMode::Peak: return zones[a].peak > zones[b].peak;
+ case SortMode::Avg: return zones[a].total > zones[b].total;
+ case SortMode::Total:
+ default: return zones[a].total > zones[b].total;
+ }
+ });
double frame_avg_ms = elapsed_ms / static_cast<double>(frames_in_win);
double fps = 1000.0 / frame_avg_ms;
@@ -172,5 +182,8 @@ struct ScopedZone {
#define FRAME_BEGIN() ::fp::frame().begin_Frame()
#define FRAME_END() ::fp::frame().end_Frame()
+#define FRAME_SORT_PEAK ::fp::frame().sort_mode = fp::SortMode::Peak;
+#define FRAME_SORT_TOTAL ::fp::frame().sort_mode = fp::SortMode::Total;
+#define FRAME_SORT_AVERAGE ::fp::frame().sort_mode = fp::SortMode::Avg;
#endif // !FRAME_PROFILER_DISABLED
diff --git a/src/wall/wall.cpp b/src/wall/wall.cpp
index 3a9eeb1..d5373f5 100644
--- a/src/wall/wall.cpp
+++ b/src/wall/wall.cpp
@@ -113,7 +113,7 @@ namespace Wall
info.clear();
Text::Token::Type tt = Text::CppHighlight::classify_Word(token_text);
- Text::Token::Desc d = Text::Token::get_Description(token_text, tt);
+ Text::Token::Description d = Text::Token::get_Description(token_text, tt);
const std::string line1 = token_text + " [" + Text::Token::get_TypeName(tt) + "]";
const std::string line2 = d.short_desc;