Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / commits / 92371d1

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

Cleanup

Christian Immanuel · 2026-06-09 20:36 · 92371d1c8514843ecb1cf16856b1a667b34c3eee

 info.txt                                           |   3 +
 src/states/game_state.cpp                          | 141 +++++++++++----------
 src/states/game_state.hpp                          |   2 +-
 src/systems/cpp_highlight_system.hpp               |   1 -
 .../{word_anim.cpp => word_anim_system.cpp}        |   2 +-
 .../{word_anim.hpp => word_anim_system.hpp}        |   0
 src/text/handler.cpp                               |  88 ++++++++++---
 src/text/handler.hpp                               |  53 ++++----
 src/text/text_mesh.cpp                             |  99 ++++++++++-----
 src/text/token.cpp                                 |   1 +
 src/text/token.hpp                                 |  14 ++
 11 files changed, 254 insertions(+), 150 deletions(-)

diff --git a/info.txt b/info.txt
index 9d0f111..69e2615 100644
--- a/info.txt
+++ b/info.txt
@@ -1,4 +1,7 @@
 
+i need to store the big info text meshes so displaying them is not that expensive.
+
+---------------------------------
 
 Text::Handler
     - handles tokens and lines as one commplete text structure so it can easily get tokenized from tree-sitter
diff --git a/src/states/game_state.cpp b/src/states/game_state.cpp
index bfcc1da..66ab04f 100644
--- a/src/states/game_state.cpp
+++ b/src/states/game_state.cpp
@@ -309,15 +309,22 @@ GameState::on_ShootAll()
             all_words.push_back(std::move(node->word));
 
     _carried = CarriedLine();
+    Aim::Result aim;
     if (!all_words.empty()) {
-        Aim::Result aim = Aim::crosshair_Target(*_ctx.camera, _ctx.words, _ctx.scenery,
-                                                 _text_walls, World::get_BoxScale());
-        _projectiles_sys->launch(_projectiles, std::move(all_words),
-                                 _ctx.camera->fwd, _ctx.camera->pos, aim);
+        { 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);
+        }
     }
 
-    _level_director->show_Details(_ctx);
-    _level_director->record_Shot ();
+    { FP_ZONE("dispatch.ShootAll.details");
+        _level_director->show_Details(_ctx); }
+    { FP_ZONE("dispatch.ShootAll.record");
+        _level_director->record_Shot (); }
 }
 
 
@@ -472,13 +479,10 @@ GameState::projectile_LandOnFloor(ProjectileResult& result)
 void
 GameState::projectile_LandOnWall(ProjectileResult& result)
 {
-    DynArray<Text::TokenPtr> editor_block;
-    std::string prog_line, comp_line, info_line;
+    DynArray<Text::TokenPtr> edit_block, prog_block, comp_block, info_block;
 
     for (Text::TokenPtr& w : result.words) {
         if (!w) continue;
-        if (w->type == ObjectType::Text)
-            w->set_Look(ConfigColor::keep, Text::Token::depth_wall);
         w->body.mode = PhysicsMode::Static;
         w->body.vel  = Vec3{0, 0, 0};
 
@@ -486,35 +490,27 @@ GameState::projectile_LandOnWall(ProjectileResult& result)
         if (!d.token) continue;
 
         switch (d.role) {
-            case Wall::Role::Editor:
-                editor_block.push_back(std::move(d.token));
-                break;
-            case Wall::Role::Program:
-                if (!prog_line.empty()) prog_line += ' ';
-                prog_line += d.token->text;
-                break;
-            case Wall::Role::Compiler:
-                if (!comp_line.empty()) comp_line += ' ';
-                comp_line += d.token->text;
-                break;
-            case Wall::Role::Info:
-                if (!info_line.empty()) info_line += ' ';
-                info_line += d.token->text;
-                break;
+            case Wall::Role::Editor:   edit_block.push_back(std::move(d.token)); break;
+            case Wall::Role::Program:  prog_block.push_back(std::move(d.token)); break;
+            case Wall::Role::Compiler: comp_block.push_back(std::move(d.token)); break;
+            case Wall::Role::Info:     info_block.push_back(std::move(d.token)); break;
         }
     }
 
-    if (!editor_block.empty()) {
+    if (!edit_block.empty()) {
         Vec3 impact{0, 0, 0};
-        for (const Text::TokenPtr& t : editor_block)
+        for (const Text::TokenPtr& t : edit_block)
             { impact.x += t->pos.x; impact.y += t->pos.y; impact.z += t->pos.z; }
-        const double inv = 1.0 / static_cast<double>(editor_block.size());
+        const double inv = 1.0 / static_cast<double>(edit_block.size());
         impact.x *= inv; impact.y *= inv; impact.z *= inv;
-        _editor.attach(std::move(editor_block), impact);
+        _editor.attach(std::move(edit_block), impact);
+    }
+
+    { FP_ZONE("flush.append");
+        if (!prog_block.empty()) _program .append_Tokens(std::move(prog_block));
+        if (!comp_block.empty()) _compiler.append_Tokens(std::move(comp_block));
+        if (!info_block.empty()) _info    .append_Tokens(std::move(info_block));
     }
-    if (!prog_line.empty()) _program .append_Line(prog_line);
-    if (!comp_line.empty()) _compiler.append_Line(comp_line);
-    if (!info_line.empty()) _info    .append_Line(info_line);
 }
 
 
@@ -582,50 +578,59 @@ void
 GameState::handle_Input(FrameInput& input, double dt) {
     for (InputCommand cmd : input.commands) {
         switch (cmd) {
-            case InputCommand::Jump:          on_Jump();                  break;
-            case InputCommand::Click:         on_Click();                 break;
-            case InputCommand::Drop:          on_DropLast();              break;
-            case InputCommand::ShootAll:      on_ShootAll();              break;
-            case InputCommand::Compile:       on_Compile();               break;
-            case InputCommand::Run:           on_Run();                   break;
-            case InputCommand::DebugPrint:    on_DebugPrint();            break;
-            case InputCommand::FreelookStart: on_FreelookStart();         break;
-            case InputCommand::FreelookEnd:   on_FreelookEnd();           break;
-            case InputCommand::NumberInc:     on_NumberAdjust(+1);        break;
-            case InputCommand::NumberDec:     on_NumberAdjust(-1);        break;
-            case InputCommand::TypingStart:   on_TypingStart (input);     break;
-            case InputCommand::TypingCommit:  on_TypingCommit(input);     break;
-            case InputCommand::TypingCancel:  on_TypingCancel(input);     break;
-            case InputCommand::SandboxRestore:    sandbox_Restore();      break;
-            case InputCommand::ConnectPultSubmit: on_WallConnectSubmit(); break;
-            case InputCommand::ConnectPultExit:   on_WallConnectExit();   break;
+            case InputCommand::Jump:              { FP_ZONE("on_Jump");             on_Jump();}              break;
+            case InputCommand::Click:             { FP_ZONE("on_Click");            on_Click();}             break;
+            case InputCommand::Drop:              { FP_ZONE("on_DropLast");         on_DropLast();}          break;
+            case InputCommand::ShootAll:          { FP_ZONE("on_ShootAll");         on_ShootAll();}          break;
+            case InputCommand::Compile:           { FP_ZONE("on_Compile");          on_Compile();}           break;
+            case InputCommand::Run:               { FP_ZONE("on_Run");              on_Run();}               break;
+            case InputCommand::DebugPrint:        { FP_ZONE("on_DebugPrint");       on_DebugPrint();}        break;
+            case InputCommand::FreelookStart:     { FP_ZONE("on_FreelookStart");    on_FreelookStart();}     break;
+            case InputCommand::FreelookEnd:       { FP_ZONE("on_FreelookEnd");      on_FreelookEnd();}       break;
+            case InputCommand::NumberInc:         { FP_ZONE("on_NumberAdjust");     on_NumberAdjust(+1);}    break;
+            case InputCommand::NumberDec:         { FP_ZONE("on_NumberAdjust");     on_NumberAdjust(-1);}    break;
+            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::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) {
-        Text::Handler::EditReq req = _editor.feed_Keys(input.vim_keys, _ctx.camera->pos.y);
-        input.vim_keys.clear();
-        switch (req) {
-            case Text::Handler::EditReq::Write:
-                _editor.mark_Saved();
-                break;
-            case Text::Handler::EditReq::WriteQuit:
-            case Text::Handler::EditReq::Exit:
-            case Text::Handler::EditReq::Discard:
-                _editor.end_Edit();
-                _wall_connection.end();
-                _frame_input.capture_vim = false;
-                break;
-            case Text::Handler::EditReq::None:
-                break;
+        { FP_ZONE("handle_input.Editor");
+            Text::Handler::EditReq req = _editor.feed_Keys(input.vim_keys, _ctx.camera->pos.y);
+            input.vim_keys.clear();
+            switch (req) {
+                case Text::Handler::EditReq::Write:
+                    _editor.mark_Saved();
+                    break;
+                case Text::Handler::EditReq::WriteQuit:
+                case Text::Handler::EditReq::Exit:
+                case Text::Handler::EditReq::Discard:
+                    _editor.end_Edit();
+                    _wall_connection.end();
+                    _frame_input.capture_vim = false;
+                    break;
+                case Text::Handler::EditReq::None:
+                    break;
+            }
         }
     }
     else if (_wall_connection.get_Target() == Wall::Connection::Target::Program) {
-        _wall_connection.set_ProgramBuffer(input.typing_buffer);
+        { FP_ZONE("handle_input.Program");
+            _wall_connection.set_ProgramBuffer(input.typing_buffer);
+        }
     }
     else if (input.typing_mode) {
-        input.forward = input.backward = input.left = input.right = false;
-        if (input.typing_buffer != _typing_last) { typing_RebuildPreview(input.typing_buffer); _typing_last = input.typing_buffer; }
+        { FP_ZONE("handle_input.typing_mode");
+            input.forward = input.backward = input.left = input.right = false;
+            if (input.typing_buffer != _typing_last) {
+                typing_RebuildPreview(input.typing_buffer);
+                _typing_last = input.typing_buffer;
+            }
+        }
     }
 
     if (_pick_cooldown > 0.0) _pick_cooldown -= dt;
@@ -634,7 +639,7 @@ GameState::handle_Input(FrameInput& input, double dt) {
 
     if (_shoot_cooldown > 0.0) _shoot_cooldown -= dt;
     if (input.shoot && _shoot_cooldown <= 0.0)
-        { if (on_ShootOne()) _shoot_cooldown = 0.15; }
+        { FP_ZONE("handle_input.onShootO"); if (on_ShootOne()) _shoot_cooldown = 0.15; }
 }
 
 void
diff --git a/src/states/game_state.hpp b/src/states/game_state.hpp
index 2bdd018..9cc242f 100644
--- a/src/states/game_state.hpp
+++ b/src/states/game_state.hpp
@@ -23,7 +23,7 @@
 #include "systems/pult_system.hpp"
 #include "systems/wall_connection.hpp"
 #include "systems/word_ai_system.hpp"
-#include "systems/word_anim.hpp"
+#include "systems/word_anim_system.hpp"
 #include "text/handler.hpp"          // Text::Handler (replaces Wall::Editor + Text::SrcView)
 #include "text/vim_buffer.hpp"
 #include "utils/globals.hpp"
diff --git a/src/systems/cpp_highlight_system.hpp b/src/systems/cpp_highlight_system.hpp
index bbfee6a..934cadf 100644
--- a/src/systems/cpp_highlight_system.hpp
+++ b/src/systems/cpp_highlight_system.hpp
@@ -16,7 +16,6 @@ public:
 
     void request(const std::string& source, uint64_t gen)
     {
-        // TODO: dont send a request twice?!
         if (source == _last_requested) return;
         _last_requested = source;
 
diff --git a/src/systems/word_anim.cpp b/src/systems/word_anim_system.cpp
similarity index 98%
rename from src/systems/word_anim.cpp
rename to src/systems/word_anim_system.cpp
index 36bf2a2..248223a 100644
--- a/src/systems/word_anim.cpp
+++ b/src/systems/word_anim_system.cpp
@@ -1,7 +1,7 @@
 #include <algorithm>
 
 #include "entities/world.hpp"
-#include "word_anim.hpp"
+#include "word_anim_system.hpp"
 #include "utils/config.hpp"
 
 
diff --git a/src/systems/word_anim.hpp b/src/systems/word_anim_system.hpp
similarity index 100%
rename from src/systems/word_anim.hpp
rename to src/systems/word_anim_system.hpp
diff --git a/src/text/handler.cpp b/src/text/handler.cpp
index 58f399b..821e4e5 100644
--- a/src/text/handler.cpp
+++ b/src/text/handler.cpp
@@ -68,7 +68,7 @@ set_NumberText(Token::Data* t, const std::string& s)
 Handler::Handler(Wall::Role role, float world_scale, LayoutMode mode)
     : _role(role), _layout(mode)
 {
-    _tf           = Wall::get_Transform(role, world_scale);
+    _transform    = Wall::get_Transform(role, world_scale);
     _line_height  = ConfigWall::line_height;
     _word_spacing = ConfigWall::free_space_dist;
     _append_color = ConfigColor::dark_gray;
@@ -127,8 +127,8 @@ Handler::set_Lines(const DynArray<std::string>& lines)
         std::istringstream in(text);
         std::string tk;
         while (in >> tk) {
-            TokenPtr t = make_Token(tk, _depth, _scale, _tf.position, ConfigColor::wall);
-            t->yaw = _tf.yaw;
+            TokenPtr t = make_Token(tk, _depth, _scale, _transform.position, ConfigColor::wall);
+            t->yaw = _transform.yaw;
             t->set_ColorSource(src);
             ln.tokens.push_back(std::move(t));
         }
@@ -143,6 +143,8 @@ Handler::set_Lines(const DynArray<std::string>& lines)
 void
 Handler::append_Line(const std::string& text)
 {
+    SPIKE("append_Line");
+
     for (Line& l : _lines) {
         l.y += _line_height;
         for (TokenPtr& t : l.tokens) if (t) t->pos.y += _line_height;
@@ -151,9 +153,9 @@ Handler::append_Line(const std::string& text)
     Line ln;
     ln.y = _append_y;
 
-    TokenPtr t = make_Token(text, _depth, _scale, _tf.position, _append_color);
-    t->yaw   = _tf.yaw;
-    t->pos   = _tf.position;
+    TokenPtr t = make_Token(text, _depth, _scale, _transform.position, _append_color);
+    t->yaw   = _transform.yaw;
+    t->pos   = _transform.position;
     t->pos.y = _append_y;
     t->set_Color(_append_color);
     ln.tokens.push_back(std::move(t));
@@ -162,9 +164,55 @@ Handler::append_Line(const std::string& text)
 }
 
 
+void
+Handler::append_Tokens(DynArray<TokenPtr> block)
+{
+    SPIKE("append_Tokens");
+
+    DynArray<TokenPtr> toks;
+    toks.reserve(block.size());
+    for (TokenPtr& t : block) if (t) toks.push_back(std::move(t));
+    if (toks.empty()) return;
+
+    for (Line& l : _lines) {
+        l.y += _line_height;
+        for (TokenPtr& t : l.tokens) if (t) t->pos.y += _line_height;
+    }
+
+    double row_w = 0.0;
+    for (size_t i = 0; i < toks.size(); ++i) {
+        row_w += 2.0 * (double)toks[i]->half_extent;
+        if (i + 1 < toks.size()) row_w += _word_spacing;
+    }
+
+    Line ln;
+    ln.y           = _append_y;
+    ln.line_height = _line_height;
+
+    double cursor = row_w * 0.5;
+    for (TokenPtr& t : toks) {
+        const double hw = (double)t->half_extent;
+        t->yaw          = _transform.yaw;
+        t->pos          = _transform.position;
+        t->pos.y        = _append_y;
+        t->pos.z        = cursor - hw;
+        t->body.mode    = PhysicsMode::Static;
+        t->body.vel     = Vec3{0, 0, 0};
+        t->set_ColorSource(ColorSource::Static);
+        t->set_Color(_append_color);
+        cursor = t->pos.z - hw - _word_spacing;
+        ln.tokens.push_back(std::move(t));
+    }
+
+    _lines.push_back(std::move(ln));
+    mark_Dirty();
+}
+
+
 TokenPtr
 Handler::detach(Token::Data* t)
 {
+    SPIKE("detach");
     if (edit_Active()) return nullptr;
 
     for (size_t li = 0; li < _lines.size(); ++li) {
@@ -187,6 +235,7 @@ Handler::detach(Token::Data* t)
 void
 Handler::attach(DynArray<TokenPtr> tokens, const Vec3& impact)
 {
+    SPIKE("attach");
     if (tokens.empty()) return;
     if (_layout == LayoutMode::Mono)
          attach_Mono   (tokens, impact);
@@ -317,7 +366,6 @@ Handler::apply_Target(const Target& t)
 void
 Handler::attach_Snapped(DynArray<TokenPtr>& block, const Vec3& impact)
 {
-    // Order the run visual left->right (decreasing z on the NegX wall).
     std::sort(block.begin(), block.end(),
         [](const TokenPtr& a, const TokenPtr& b){
             if (!a) return false;
@@ -325,11 +373,11 @@ Handler::attach_Snapped(DynArray<TokenPtr>& block, const Vec3& impact)
             return a->pos.z > b->pos.z;
         });
 
-    // Incoming editor tokens are lexed; face the wall.
     for (TokenPtr& w : block) {
         if (!w) continue;
-        w->yaw = _tf.yaw;
+        w->yaw = _transform.yaw;
         w->set_ColorSource(_lex_enabled ? ColorSource::Lexed : ColorSource::Static);
+        w->set_Depth(_depth);
     }
 
     double block_w = 0.0;
@@ -379,7 +427,7 @@ Handler::attach_Snapped(DynArray<TokenPtr>& block, const Vec3& impact)
         const double center_z = cursor - hw;
         w->pos.y = target.y;
         w->pos.z = center_z;
-        w->pos.x = _tf.position.x;
+        w->pos.x = _transform.position.x;
         cursor   = center_z - hw - ConfigWall::free_space_dist;
     }
 
@@ -550,7 +598,7 @@ Handler::relayout()
 void
 Handler::relayout_Snapped()
 {
-    const double x_wall = _tf.position.x;
+    const double x_wall = _transform.position.x;
     const double lh     = _line_height;
     const double gap    = 0.5;
     const double floor_y = World::get_FloorY<double>();
@@ -567,7 +615,7 @@ Handler::relayout_Snapped()
         if (l.is_Empty()) continue;
         double total_w = 0.0;
         for (TokenPtr& t : l.tokens) {
-            t->yaw  = _tf.yaw;
+            t->yaw  = _transform.yaw;
             total_w += 2.0 * (double)t->half_extent;
         }
         total_w += gap * static_cast<double>(l.tokens.size() - 1);
@@ -640,7 +688,7 @@ Handler::relayout_Mono()
         if (can_reuse && i < old.size() && old[i].num_left)
             br.num = std::move(old[i].num_left);
         else
-            br.num = make_Number(std::to_string(i + 1), _tf.position, _tf.yaw, 0.0022f);
+            br.num = make_Number(std::to_string(i + 1), _transform.position, _transform.yaw, 0.0022f);
 
         if (br.num) {
             const std::string s_no = std::to_string(i + 1);
@@ -662,8 +710,8 @@ Handler::relayout_Mono()
             if (reuse_line && ti < old[i].tokens.size())
                 t = std::move(old[i].tokens[ti]);
             else {
-                t = make_Token(tk, Token::depth_wall, 0.003f, _tf.position, ConfigColor::white);
-                t->yaw = _tf.yaw;
+                t = make_Token(tk, Token::depth_wall, 0.003f, _transform.position, ConfigColor::white);
+                t->yaw = _transform.yaw;
                 t->set_ColorSource(ColorSource::Lexed);
             }
             br.toks.push_back({ std::move(t), start, tk.size() });
@@ -690,7 +738,7 @@ Handler::relayout_Mono()
 
         if (built[i].num) {
             const double he = (double)built[i].num->half_extent;
-            built[i].num->pos   = _tf.position;
+            built[i].num->pos   = _transform.position;
             built[i].num->pos.y = row_y;
             built[i].num->pos.z = z_block_left - he;
             ln.num_left = std::move(built[i].num);
@@ -699,7 +747,7 @@ Handler::relayout_Mono()
         for (BT& bt : built[i].toks) {
             const double left_z = _code_left_z - static_cast<double>(bt.col) * _char_w;
             const double tok_w  = _char_w * static_cast<double>(bt.len);
-            bt.t->pos   = _tf.position;
+            bt.t->pos   = _transform.position;
             bt.t->pos.y = row_y;
             bt.t->pos.z = left_z - tok_w * 0.5;
             ln.tokens.push_back(std::move(bt.t));
@@ -740,10 +788,10 @@ Handler::maintain_Numbers(double dt, const Camera&)
         const std::string s = std::to_string(rank);
 
         if (!l.num_left)
-            l.num_left = make_Number(s, Vec3{_tf.position.x, l.y, 0.0}, _tf.yaw,
+            l.num_left = make_Number(s, Vec3{_transform.position.x, l.y, 0.0}, _transform.yaw,
                                      ConfigWall::line_number_scale);
         if (_both_columns && !l.num_right)
-            l.num_right = make_Number(s, Vec3{_tf.position.x, l.y, 0.0}, _tf.yaw,
+            l.num_right = make_Number(s, Vec3{_transform.position.x, l.y, 0.0}, _transform.yaw,
                                       ConfigWall::line_number_scale);
 
         if (l.number != rank) {
@@ -770,7 +818,7 @@ Handler::maintain_Numbers(double dt, const Camera&)
 
     for (size_t oi : order) {
         Line& l = _lines[oi];
-        double x = _tf.position.x;
+        double x = _transform.position.x;
         for (TokenPtr& w : l.tokens) { if (w) { x = w->pos.x; break; } }
 
         const double ny = l.y + l.bounce_off;
diff --git a/src/text/handler.hpp b/src/text/handler.hpp
index 555ab51..9001b48 100644
--- a/src/text/handler.hpp
+++ b/src/text/handler.hpp
@@ -45,44 +45,42 @@ public:
     void set_Bordered    (bool on)     { _bordered  = on; }
     void set_Underline   (bool on)     { _underline = on; }
     void set_DefaultStyle(float depth, float scale, double line_height, double word_spacing);
+    void set_Lines    (const DynArray<std::string>& lines);
 
     Wall::Role role  () const { return _role; }
     LayoutMode layout() const { return _layout; }
+    void set_AppendBaseline(double y)     { _append_y = y; }
+    void set_AppendColor   (SDL_FColor c) { _append_color = c; }
 
-    // ── content ────────────────────────────────────────────────────────────
     void clear();
-    void set_Lines  (const DynArray<std::string>& lines);
-    void append_Line(const std::string& text);
-    void set_AppendBaseline(double y) { _append_y = y; }
-    void set_AppendColor   (SDL_FColor c) { _append_color = c; }
 
-    // ── ownership transfer ────────────────────────────────────────────────
-    TokenPtr detach (Token::Data* t);
-    void     attach (DynArray<TokenPtr> tokens, const Vec3& impact);
-    void     clear_ToFloor(DynArray<TokenPtr>& out);
+    void append_Line  (const std::string& text);
+    const DynArray<Line>&
+         get_Lines    () const { return _lines; }
+    DynArray<std::string>
+         as_Lines     () const;
+    void append_Tokens  (DynArray<TokenPtr> block);
+    void apply_Highlight(const DynArray<HiSpan>& spans, uint64_t gen);
+
+    Token::Data* token_At_Byte(size_t off);
+    TokenPtr     detach       (Token::Data* t);
+    void         attach       (DynArray<TokenPtr> tokens, const Vec3& impact);
+    void         clear_ToFloor(DynArray<TokenPtr>& out);
+    double       scroll_DeltaToEye (size_t line_id, double eye_y) const;
+
+    void        bounce            (size_t line_id);
+    DynArray<NumberHit>
+                number_Hitboxes   () const;
 
-    // ── async lexing ──────────────────────────────────────────────────────
     bool        is_Dirty   () const { return _lex_enabled && _dirty; }
     uint64_t    generation () const { return _gen; }
     std::string render_Source();
-    void        apply_Highlight(const DynArray<HiSpan>& spans, uint64_t gen);
     void        mark_Dirty ();
 
-    // ── queries ───────────────────────────────────────────────────────────
-    Token::Data*                token_At_Byte(size_t off);
-    DynArray<std::string> as_Lines     () const;
-    const DynArray<Line>& get_Lines    () const { return _lines; }
-
-    // ── per-frame ──────────────────────────────────────────────────────────
     void update (double dt, const Camera& cam);
     void render (GlRenderer& gl, const Camera& cam, const Light& light) const;
     void scroll (double delta);
 
-    // ── line-number ───────────────────────────────────────────────────────
-    double scroll_DeltaToEye (size_t line_id, double eye_y) const;
-    void   bounce            (size_t line_id);
-    DynArray<NumberHit>
-           number_Hitboxes   () const;
 
     // ── vim cursor support (Mono) ──────────────────────────────────────────
     MonoMetrics mono_Metrics() const;
@@ -122,12 +120,12 @@ public:
     void draw          (GlRenderer& gl, const Camera& cam, const Light& light) const { render(gl, cam, light); }
 
 private:
-    DynArray<Line> _lines;
+    DynArray<Line>  _lines;
 
-    Wall::Role      _role   { Wall::Role::Editor };
-    Wall::Transform _tf     {};
-    LayoutMode      _layout { LayoutMode::Snapped };
-    FaceMode        _face   { FaceMode::Pitch };
+    Wall::Role      _role     { Wall::Role::Editor };
+    Wall::Transform _transform{};
+    LayoutMode      _layout   { LayoutMode::Snapped };
+    FaceMode        _face     { FaceMode::Pitch };
 
     float  _depth        { Token::depth_wall };
     float  _scale        { 0.003f };
@@ -197,4 +195,5 @@ private:
     bool   impact_To_RowCol(const Vec3& impact, int& row, int& col) const;
 };
 
+
 } // namespace Text
diff --git a/src/text/text_mesh.cpp b/src/text/text_mesh.cpp
index 364ff32..67edd81 100644
--- a/src/text/text_mesh.cpp
+++ b/src/text/text_mesh.cpp
@@ -9,16 +9,18 @@
 // Dependencies: freetype2, earcut.hpp (header-only)
 
 #include "text_mesh.hpp"
+#include "thirdparty/earcut/earcut.hpp"
+#include "token.hpp"
 
+#include <SDL3/SDL.h>
+#include <algorithm>
+#include <array>
 #include <ft2build.h>
+#include <unordered_map>
+
 #include FT_FREETYPE_H
 #include FT_OUTLINE_H
 
-#include "thirdparty/earcut/earcut.hpp"
-
-#include <SDL3/SDL.h>
-#include <array>
-#include <algorithm>
 
 static constexpr int   BEZIER_STEPS = 8;
 static constexpr float FT_SCALE     = 1.0f / 64.0f;
@@ -27,6 +29,48 @@ using Pt2  = std::array<float, 2>;
 using Ring = DynArray<Pt2>;
 using Poly = DynArray<Ring>;   // [0]=outer, [1..]=holes
 
+
+
+
+namespace {
+
+struct FaceKey {
+    std::string path;
+    int         size;
+    bool operator==(const FaceKey& o) const { return size == o.size && path == o.path; }
+};
+struct FaceKeyHash {
+    size_t operator()(const FaceKey& k) const {
+        return std::hash<std::string>{}(k.path) ^ (std::hash<int>{}(k.size) << 1);
+    }
+};
+
+FT_Library g_ft = nullptr;
+std::unordered_map<FaceKey, FT_Face, FaceKeyHash> g_faces;
+
+// Returns a ready-to-use face at the requested pixel size, or nullptr.
+FT_Face acquire_Face(const std::string& path, int size) {
+    if (!g_ft && FT_Init_FreeType(&g_ft)) { g_ft = nullptr; return nullptr; }
+
+    FaceKey key{ path, size };
+    auto it = g_faces.find(key);
+    if (it != g_faces.end()) {
+        FT_Set_Pixel_Sizes(it->second, 0, static_cast<FT_UInt>(size));  // cheap; face stays loaded
+        return it->second;
+    }
+
+    FT_Face face;
+    if (FT_New_Face(g_ft, path.c_str(), 0, &face)) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "FT_New_Face failed: %s", path.c_str());
+        return nullptr;
+    }
+    FT_Set_Pixel_Sizes(face, 0, static_cast<FT_UInt>(size));
+    g_faces.emplace(key, face);
+    return face;
+}
+
+} // namespace
+
 // ── FreeType outline callbacks ────────────────────────────────────────────────
 
 struct OutlineCtx {
@@ -207,25 +251,19 @@ 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();
-    //edges.clear();
 
-    FT_Library ft;
-    if (FT_Init_FreeType(&ft)) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "FT_Init_FreeType failed"); return false; }
-    FT_Face face;
-    if (FT_New_Face(ft, font_path.c_str(), 0, &face)) {
-        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "FT_New_Face failed: %s", font_path.c_str());
-        FT_Done_FreeType(ft); return false;
-    }
-    FT_Set_Pixel_Sizes(face, 0, static_cast<FT_UInt>(size));
+    FT_Face face = acquire_Face(font_path, size);   // inits g_ft once, caches the face
+    if (!face) return false;
 
     float pen_x = 0.0f;
 
     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 (FT_Load_Glyph(face, gi, FT_LOAD_NO_BITMAP))     continue;
         if (face->glyph->format != FT_GLYPH_FORMAT_OUTLINE) continue;
 
         OutlineCtx ctx;
@@ -258,16 +296,16 @@ TextMesh::build(const std::string& font_path,
             // 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) });
+                                  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) });
+                                  static_cast<double>(p[1]),
+                                  static_cast<double>( half_d) });
                 is_front.push_back(0.0f);
             }
 
@@ -280,8 +318,8 @@ TextMesh::build(const std::string& font_path,
             // 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]) });
+                                 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)
@@ -298,20 +336,20 @@ TextMesh::build(const std::string& font_path,
                     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
+                                      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
+                                      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
+                                      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
+                                      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 });
@@ -324,9 +362,6 @@ TextMesh::build(const std::string& font_path,
         pen_x += static_cast<float>(face->glyph->advance.x) * FT_SCALE;
     }
 
-    FT_Done_Face(face);
-    FT_Done_FreeType(ft);
-
     // Centre mesh on bounding box
     if (!verts.empty()) {
         float minX=1e9f, maxX=-1e9f;
diff --git a/src/text/token.cpp b/src/text/token.cpp
index 6cb6946..4791020 100644
--- a/src/text/token.cpp
+++ b/src/text/token.cpp
@@ -164,6 +164,7 @@ Data::get_Distance(Vec3 position) const
 void
 Data::rebuild_Mesh()
 {
+    SPIKE("rebuild_mesh");
     loaded = mesh.build(font_path, text, 64, depth, scale);
     if (!loaded) {
         SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
diff --git a/src/text/token.hpp b/src/text/token.hpp
index 26088e7..cac206e 100644
--- a/src/text/token.hpp
+++ b/src/text/token.hpp
@@ -181,6 +181,20 @@ Token
     };
 
 
+
+
+    struct SpikeTimer {
+        const char* name; Uint64 t0;
+        SpikeTimer(const char* n) : name(n), t0(SDL_GetPerformanceCounter()) {}
+        ~SpikeTimer() {
+            double ms = 1000.0 * double(SDL_GetPerformanceCounter() - t0)
+                               / double(SDL_GetPerformanceFrequency());
+            if (ms > 1.0) SDL_Log("SPIKE %-24s %.2f ms", name, ms);
+        }
+    };
+#define SPIKE(n) Text::Token::SpikeTimer _spk_##__LINE__(n)
+
+
 } // namespace Token::Data