Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / commits / 5fd7763

sdl_runtime_compiler git · main

SDL3 game for running and compiling code at runtime

sdl3 c++ compiler dlopen cmake · first commit 2026-04-19 · last commit 2026-07-03 (3 months ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-runtime-compiler

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.gz

Cash Meshes and Add saved colors ToText With Claude

Christian Immanuel · 2026-06-09 23:57 · 5fd77632b4f633dd863d00fbb81d839774ba7327

 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;