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 void Install(lua_State* L);
45
46 // Removes our line hook from the lua_State and, if LuaPanda is loaded,
47 // tries to restore its hook so it can resume polling for a VS Code
48 // connection. Called by the panel's "Active" toggle when the user wants
49 // to hand off to LuaPanda without restarting the editor.
50 void Uninstall();
51
52 bool IsInstalled() const { return mInstalled; }
53
54 // Persists the user's "Active" preference to a JSON file in the editor
55 // preferences directory so it carries across editor restarts. The
56 // preference defaults to true (in-engine debugger active) on first run.
57 static bool LoadActivePreference(); // returns last-saved value, or true
58 static void SaveActivePreference(bool active);
59
60 // Persists the current breakpoint set to the same JSON file. Called
61 // automatically on every Set/Clear/Toggle so it stays in sync; loaded
62 // once at Install so F9 breakpoints survive editor restarts.
63 void LoadBreakpoints();
64 void SaveBreakpoints();
65
66 // ----- Breakpoints --------------------------------------------------
67
68 void ToggleBreakpoint(const std::string& sourceFile, int line);
69 void SetBreakpoint(const std::string& sourceFile, int line);
70 void ClearBreakpoint(const std::string& sourceFile, int line);
71 void ClearAllBreakpoints();
72 bool HasBreakpoint(const std::string& sourceFile, int line) const;
73 std::set<int> GetBreakpointsForFile(const std::string& sourceFile) const;
74
75 // Returns a flat list of (normalized-file, line) pairs for the panel.
76 struct BreakpointEntry { std::string mFile; int mLine; };
77 std::vector<BreakpointEntry> GetAllBreakpoints() const;
78
79 // ----- Pause state --------------------------------------------------
80
81 bool IsPaused() const { return mPaused.load(); }
82 const std::string& GetPauseMessage() const { return mPauseMessage; }
83 const std::string& GetPauseFile() const { return mPauseFile; }
84 int GetPauseLine() const { return mPauseLine; }
85
86 void RequestContinue();
87
88 // Clears transient pause/skip state. Called when Play-In-Editor restarts
89 // so a "skip-once" left over from the previous run doesn't suppress the
90 // first Debugger.Break / Debugger.Snapshot of the next run.
91 void ResetTransientState();
92
93 // Called from Debugger.Break() in Lua to pause at the call site.
94 // Captures the snapshot for the caller's frame, sets paused, then
95 // throws a Lua error to abort the surrounding pcall. Does not return.
96 static int LuaBreakBinding(lua_State* L);
97
98 // Called from Debugger.Snapshot() in Lua. Soft variant: captures the
99 // snapshot + sets paused, then RETURNS so the surrounding Lua call can
100 // finish naturally before the world freezes next frame.
101 static int LuaSnapshotBinding(lua_State* L);
102
103 // ----- Snapshot (valid while paused) --------------------------------
104
105 struct StackFrame
106 {
107 std::string mSource; // normalized
108 std::string mFuncName; // may be empty for anonymous frames
109 std::string mWhat; // "Lua", "C", "main", "tail"
110 int mCurrentLine = -1;
111 };
112
113 struct LocalVar
114 {
115 std::string mName;
116 std::string mTypeStr;
117 std::string mValueStr;
118 };
119 void CaptureSnapshot(lua_State* L, int startLevel = 0);
120
121 const std::vector<StackFrame>& GetCallStack() const { return mCallStack; }
122
123 // Returns locals (kind = 0) or upvalues (kind = 1) for a given frame
124 // index in the captured snapshot. Empty if frame index is out of range.
125 enum class VarKind { Local, Upvalue };
126 std::vector<LocalVar> GetSnapshotVars(int frameIndex, VarKind kind) const;
127
128 // ----- Hook trampoline ---------------------------------------------
129
130 static void OnHookTrampoline(lua_State* L, lua_Debug* ar);
131
132 // ----- Helpers ------------------------------------------------------
133
134 // Sentinel used both for the lua_error message and for matching it back
135 // out in ScriptUtils::CallLuaFunc so we don't log it as a real error.
136 static const char* GetPauseSentinel();
137
138 // Strip leading '@', drop ".lua", lowercase on Windows, replace '\' -> '/'.
139 static std::string NormalizeSource(const char* luaSource);
140
141 // True if LuaPanda has installed itself on this state. Used during
142 // Install() to avoid fighting LuaPanda over lua_sethook.
143 static bool IsLuaPandaActive(lua_State* L);
144
145private:
146 LuaDebugger() = default;
147
148 void OnHook(lua_State* L, lua_Debug* ar);
149
150 // Snapshot + pause-flag, but DOES call lua_error to abort the running
151 // pcall. Used by line breakpoints, where stopping mid-line is the only
152 // way to actually halt execution.
153 void EnterPaused(lua_State* L, lua_Debug* ar, const char* optionalMessage, int snapshotStartLevel = 0);
154
155 // Snapshot + pause-flag without lua_error. The current Lua function
156 // continues to its natural end; world freezes from the next frame.
157 // Used by Debugger.Break() so init code (Start, etc.) completes before
158 // the world freezes -- otherwise Continue can't recover the state.
159 void EnterPausedSoft(lua_State* L, lua_Debug* ar, const char* optionalMessage, int snapshotStartLevel = 0);
160
161
162 static std::string FormatLuaValue(lua_State* L, int idx);
163
164 static LuaDebugger* sInstance;
165
166 bool mInstalled = false;
167 bool mFirstHookLogged = false;
168 lua_State* mL = nullptr;
169
170 // Saved cursor state captured when we entered the pause, restored on
171 // Continue. Lets the user actually click the panel during PIE pause
172 // (where the game would otherwise have hidden / locked / trapped the
173 // cursor for mouselook). Also captures EditorState::mGamePreviewCaptured
174 // because GamePreview::DrawPanel re-traps the cursor every frame while
175 // it's true.
176 bool mSavedCursorShown = true;
177 bool mSavedCursorLocked = false;
178 bool mSavedCursorTrapped = false;
179 bool mSavedGamePreviewCapture = false;
180 bool mSavedCursorValid = false;
181
182 void FreeCursorForInspection();
183 void RestoreCursor();
184
185 mutable std::mutex mBreakpointMutex;
186 std::map<std::string, std::set<int>> mBreakpoints; // key: normalized file
187
188 std::atomic<bool> mPaused{false};
189 std::string mPauseMessage;
190 std::string mPauseFile;
191 int mPauseLine = -1;
192
193 // Captured snapshot data
194 std::vector<StackFrame> mCallStack;
195 // Per-frame local/upvalue lists, indexed [frame][kind].
196 std::vector<std::vector<LocalVar>> mFrameLocals;
197 std::vector<std::vector<LocalVar>> mFrameUpvalues;
198
199 // After Continue, suppress the next single hook event at this exact
200 // (file, line) so the script can step past its own breakpoint.
201 bool mSkipOnceArmed = false;
202 std::string mSkipOnceFile;
203 int mSkipOnceLine = -1;
204
205 // Same idea but for Debugger.Break() calls (which don't go through the
206 // line hook). After Continue, suppress the next Debugger.Break call from
207 // this exact (file, line) so a Break in a per-frame Tick doesn't re-trap
208 // immediately.
209 bool mSkipBreakOnceArmed = false;
210 std::string mSkipBreakFile;
211 int mSkipBreakLine = -1;
212};
213
214#endif // EDITOR
bool IsPaused()
Definition Engine.cpp:1473