Polyphase Game Engine
Loading...
Searching...
No Matches
LuaDebugger.h
Go to the documentation of this file.
1#pragma once
2
3#if EDITOR
4
5#include <atomic>
6#include <map>
7#include <mutex>
8#include <set>
9#include <string>
10#include <vector>
11
12extern "C" {
13 struct lua_State;
14 struct lua_Debug;
15}
16
17// In-engine Lua breakpoint / pause facility. Editor-only.
18//
19// Architecture (v1):
20// - lua_sethook(LUA_MASKLINE) installed once on the main lua_State.
21// - On a LINE event, we look up (file, line) in mBreakpoints. On a hit (or
22// when Debugger.Break() is called from script), we capture a snapshot of
23// the call stack / locals / upvalues, set mPaused = true, and then call
24// lua_error with a sentinel string to longjmp out of the script.
25// - ScriptUtils::CallLuaFunc detects the sentinel and swallows it silently.
26// - While paused, Script::CallTick / CallFunction early-return so the world
27// is effectively frozen. The editor keeps rendering normally and the
28// LuaDebuggerPanel shows the snapshot.
29// - Continue clears mPaused and arms a "skip-once" record for the last
30// break location so the immediately-next firing of the hook at that
31// same (file, line) is suppressed. This lets the next Tick run past the
32// breakpoint instead of trapping on it again.
33//
34// Live in-frame stepping (over/into/out) is intentionally NOT in v1: it
35// requires a reusable editor-frame-pump extraction that is too invasive to
36// land without supervision. Tracked for v2.
37class LuaDebugger
38{
39public:
40 static void Create();
41 static void Destroy();
42 static LuaDebugger* Get();
43
44 // Records the lua_State and installs the per-line hook, unless nothing
45 // needs it yet: with no breakpoints the install is deferred
46 // (LUA_MASKLINE costs a callback on every Lua line executed, which is
47 // measurable on slow machines). Showing the Lua Debugger tab or setting
48 // a breakpoint calls EnsureInstalled().
49 void Install(lua_State* L);
50
51 // Completes a deferred Install() if the 'active' preference allows it.
52 void EnsureInstalled();
53
54 // Removes our line hook from the lua_State and, if LuaPanda is loaded,
55 // tries to restore its hook so it can resume polling for a VS Code
56 // connection. Called by the panel's "Active" toggle when the user wants
57 // to hand off to LuaPanda without restarting the editor.
58 void Uninstall();
59
60 bool IsInstalled() const { return mInstalled; }
61
62 // Persists the user's "Active" preference to a JSON file in the editor
63 // preferences directory so it carries across editor restarts. The
64 // preference defaults to true (in-engine debugger active) on first run.
65 static bool LoadActivePreference(); // returns last-saved value, or true
66 static void SaveActivePreference(bool active);
67
68 // Persists the current breakpoint set to the same JSON file. Called
69 // automatically on every Set/Clear/Toggle so it stays in sync; loaded
70 // once at Install so F9 breakpoints survive editor restarts.
71 void LoadBreakpoints();
72 void SaveBreakpoints();
73
74 // ----- Breakpoints --------------------------------------------------
75
76 void ToggleBreakpoint(const std::string& sourceFile, int line);
77 void SetBreakpoint(const std::string& sourceFile, int line);
78 void ClearBreakpoint(const std::string& sourceFile, int line);
79 void ClearAllBreakpoints();
80 bool HasBreakpoint(const std::string& sourceFile, int line) const;
81 std::set<int> GetBreakpointsForFile(const std::string& sourceFile) const;
82
83 // Returns a flat list of (normalized-file, line) pairs for the panel.
84 struct BreakpointEntry { std::string mFile; int mLine; };
85 std::vector<BreakpointEntry> GetAllBreakpoints() const;
86
87 // ----- Pause state --------------------------------------------------
88
89 bool IsPaused() const { return mPaused.load(); }
90 const std::string& GetPauseMessage() const { return mPauseMessage; }
91 const std::string& GetPauseFile() const { return mPauseFile; }
92 int GetPauseLine() const { return mPauseLine; }
93
94 void RequestContinue();
95
96 // Clears transient pause/skip state. Called when Play-In-Editor restarts
97 // so a "skip-once" left over from the previous run doesn't suppress the
98 // first Debugger.Break / Debugger.Snapshot of the next run.
99 void ResetTransientState();
100
101 // Called from Debugger.Break() in Lua to pause at the call site.
102 // Captures the snapshot for the caller's frame, sets paused, then
103 // throws a Lua error to abort the surrounding pcall. Does not return.
104 static int LuaBreakBinding(lua_State* L);
105
106 // Called from Debugger.Snapshot() in Lua. Soft variant: captures the
107 // snapshot + sets paused, then RETURNS so the surrounding Lua call can
108 // finish naturally before the world freezes next frame.
109 static int LuaSnapshotBinding(lua_State* L);
110
111 // ----- Snapshot (valid while paused) --------------------------------
112
113 struct StackFrame
114 {
115 std::string mSource; // normalized
116 std::string mFuncName; // may be empty for anonymous frames
117 std::string mWhat; // "Lua", "C", "main", "tail"
118 int mCurrentLine = -1;
119 };
120
121 struct LocalVar
122 {
123 std::string mName;
124 std::string mTypeStr;
125 std::string mValueStr;
126 };
127 void CaptureSnapshot(lua_State* L, int startLevel = 0);
128
129 const std::vector<StackFrame>& GetCallStack() const { return mCallStack; }
130
131 // Returns locals (kind = 0) or upvalues (kind = 1) for a given frame
132 // index in the captured snapshot. Empty if frame index is out of range.
133 enum class VarKind { Local, Upvalue };
134 std::vector<LocalVar> GetSnapshotVars(int frameIndex, VarKind kind) const;
135
136 // ----- Hook trampoline ---------------------------------------------
137
138 static void OnHookTrampoline(lua_State* L, lua_Debug* ar);
139
140 // ----- Helpers ------------------------------------------------------
141
142 // Sentinel used both for the lua_error message and for matching it back
143 // out in ScriptUtils::CallLuaFunc so we don't log it as a real error.
144 static const char* GetPauseSentinel();
145
146 // Strip leading '@', drop ".lua", lowercase on Windows, replace '\' -> '/'.
147 static std::string NormalizeSource(const char* luaSource);
148
149 // True if LuaPanda has installed itself on this state. Used during
150 // Install() to avoid fighting LuaPanda over lua_sethook.
151 static bool IsLuaPandaActive(lua_State* L);
152
153private:
154 LuaDebugger() = default;
155
156 void OnHook(lua_State* L, lua_Debug* ar);
157
158 // Snapshot + pause-flag, but DOES call lua_error to abort the running
159 // pcall. Used by line breakpoints, where stopping mid-line is the only
160 // way to actually halt execution.
161 void EnterPaused(lua_State* L, lua_Debug* ar, const char* optionalMessage, int snapshotStartLevel = 0);
162
163 // Snapshot + pause-flag without lua_error. The current Lua function
164 // continues to its natural end; world freezes from the next frame.
165 // Used by Debugger.Break() so init code (Start, etc.) completes before
166 // the world freezes -- otherwise Continue can't recover the state.
167 void EnterPausedSoft(lua_State* L, lua_Debug* ar, const char* optionalMessage, int snapshotStartLevel = 0);
168
169
170 static std::string FormatLuaValue(lua_State* L, int idx);
171
172 static LuaDebugger* sInstance;
173
174 void InstallNow(lua_State* L);
175
176 bool mInstalled = false;
177 bool mDeferred = false;
178 bool mFirstHookLogged = false;
179 lua_State* mL = nullptr;
180
181 // Saved cursor state captured when we entered the pause, restored on
182 // Continue. Lets the user actually click the panel during PIE pause
183 // (where the game would otherwise have hidden / locked / trapped the
184 // cursor for mouselook). Also captures EditorState::mGamePreviewCaptured
185 // because GamePreview::DrawPanel re-traps the cursor every frame while
186 // it's true.
187 bool mSavedCursorShown = true;
188 bool mSavedCursorLocked = false;
189 bool mSavedCursorTrapped = false;
190 bool mSavedGamePreviewCapture = false;
191 bool mSavedCursorValid = false;
192
193 void FreeCursorForInspection();
194 void RestoreCursor();
195
196 mutable std::mutex mBreakpointMutex;
197 std::map<std::string, std::set<int>> mBreakpoints; // key: normalized file
198
199 std::atomic<bool> mPaused{false};
200 std::string mPauseMessage;
201 std::string mPauseFile;
202 int mPauseLine = -1;
203
204 // Captured snapshot data
205 std::vector<StackFrame> mCallStack;
206 // Per-frame local/upvalue lists, indexed [frame][kind].
207 std::vector<std::vector<LocalVar>> mFrameLocals;
208 std::vector<std::vector<LocalVar>> mFrameUpvalues;
209
210 // After Continue, suppress the next single hook event at this exact
211 // (file, line) so the script can step past its own breakpoint.
212 bool mSkipOnceArmed = false;
213 std::string mSkipOnceFile;
214 int mSkipOnceLine = -1;
215
216 // Same idea but for Debugger.Break() calls (which don't go through the
217 // line hook). After Continue, suppress the next Debugger.Break call from
218 // this exact (file, line) so a Break in a per-frame Tick doesn't re-trap
219 // immediately.
220 bool mSkipBreakOnceArmed = false;
221 std::string mSkipBreakFile;
222 int mSkipBreakLine = -1;
223};
224
225#endif // EDITOR
bool IsPaused()
Definition Engine.cpp:2021