設定今天的工作計劃
今天我們本來是打算繼續開發性能分析器(Profiler),但在此之前,我們認為有一些問題應該先清理一下。雖然這類事情不是我們最關心的核心內容,但我們覺得現在是時候處理一下了,特別是為了讓別人能更順利地運行我們目前的版本。
目前存在的一些問題會讓其他人嘗試運行這個游戲變得困難,尤其是我們為了讓 OBS 在處理 OpenGL 程序時能夠正常流暢地捕捉畫面而做的特殊處理。這段代碼本身其實是個很不合理的“黑科技”——可以說是個壞主意。
在構建腳本 build.bat
中,我們設置了一個環境變量 GAME_STREAMING=1
。這個變量控制了在 game_opengl.cpp
文件中的特定代碼路徑。在那段路徑中,我們把 wgl
雙緩沖設置為了 false
,即使我們明明在使用雙緩沖。這種設置理論上是非法的,我們甚至不知道它為何能工作——但它確實讓 OBS 可以更好地捕捉畫面,不需要我們使用一些更復雜的技巧。
對我們來說,這種做法確實讓直播更輕松,但問題是,如果有其他人嘗試構建游戲而沒有意識到必須關閉這個變量(或不知道有這回事),那么他們的構建結果將會是完全出錯的——可能是渲染混亂、崩潰,或者別的奇怪錯誤。
因此,現在需要清理這段臨時代碼,或者至少讓構建系統在默認狀態下是“對其他開發者友好的”,不需要他們了解直播中為了兼容 OBS 所做的特別處理。
編輯 build.bat
:引入并檢查 GAME_STREAMING
環境變量
我們希望實現一個功能:在我們的構建版本中,默認開啟游戲串流(game streaming),而在其他人的構建版本中默認關閉。為此,我們考慮通過設置環境變量的方式來控制這個行為。也就是說,只有在設置了特定環境變量的情況下,游戲串流功能才會被啟用;否則,它將保持默認關閉。
具體設想如下:
-
使用環境變量控制功能開關
我們計劃使用一個自定義的環境變量,例如GAME_STREAMING
,來判斷是否啟用游戲串流。只有當該變量存在并且非空時,我們才會將游戲串流相關的編譯選項加入到編譯參數中。 -
批處理腳本實現邏輯
在編譯腳本中(如.bat
文件),我們添加邏輯判斷該環境變量是否被定義:- 如果沒有定義該變量,則跳轉到一個標簽(如
:no_streaming
),跳過添加編譯標志的部分; - 如果變量已定義(即我們設置了),則將相應的編譯標志(如啟用游戲串流)追加到通用編譯參數中。
- 如果沒有定義該變量,則跳轉到一個標簽(如
-
編譯驗證與測試
- 我們首先驗證未設置變量時的構建結果,確保不會啟用游戲串流;
- 然后測試設置了環境變量的情況,確保游戲串流功能被正確開啟;
- 還通過手動在腳本中插入測試信息(如設置不同值或輸出信息)來驗證判斷分支是否正確執行。
-
設置系統環境變量
我們還嘗試在系統中設置該環境變量,以確保每次打開構建環境時都會自動啟用該功能。雖然一開始沒找到正確的設置路徑,但我們知道環境變量設置應該在系統設置的某個位置可以配置,后續可以完成這部分。 -
最終效果
實現后:- 我們的本地構建默認開啟游戲串流功能;
- 其他人的構建將不會受影響,仍然保持默認關閉;
- 該設置對他人完全透明,他們無需做任何更改或了解這一機制;
- 編譯流程仍保持簡潔、自動化。
這個方案簡單、可控、易于維護,同時不影響他人的開發流程。
在 CMakeLists.txt
中,檢查一個環境變量是否被設置,可以使用 CMake 的 ENV{}
語法來訪問環境變量,并結合 IF
語句判斷。以下是具體方法和中文解釋。
檢查環境變量是否被設置(是否非空)
if(DEFINED ENV{GAME_STREAMING})message(STATUS "環境變量 GAME_STREAMING 已設置,啟用游戲串流功能。")add_definitions(-DENABLE_GAME_STREAMING=1)
else()message(STATUS "未設置 GAME_STREAMING,默認關閉游戲串流功能。")
endif()
中文說明:
ENV{變量名}
用于訪問系統環境變量;DEFINED ENV{變量名}
用于判斷該環境變量是否存在;message(STATUS "...")
會在 CMake 配置階段輸出提示;add_definitions()
會向編譯器添加宏定義,例如-DENABLE_GAME_STREAMING=1
;- 你也可以使用
set()
把變量保存下來,稍后再判斷或使用。
如果想進一步判斷變量值(例如只在值為 1
時啟用功能):
if(DEFINED ENV{GAME_STREAMING} AND "$ENV{GAME_STREAMING}" STREQUAL "1")message(STATUS "GAME_STREAMING=1,啟用游戲串流。")add_definitions(-DENABLE_GAME_STREAMING=1)
else()message(STATUS "GAME_STREAMING 未設置為 1,跳過游戲串流。")
endif()
測試環境變量(在命令行里設置環境變量再運行 CMake):
Windows CMD:
set GAME_STREAMING=1
cmake ..
Unix / Linux / macOS:
export GAME_STREAMING=1
cmake ..
這樣就可以在 CMake 中根據環境變量是否存在或其值來決定是否啟用某些功能。非常適合做本地構建開關控制。
這是主播為了防止別人OSB直播看不到設置的
設置環境變量試試
重啟vscode
話說現在好像不用去.clangd 配置這個宏都高亮了
這個宏cmake中定義的
看來是clangd讀取了compile_commands.json里面的宏
奇怪我如果關閉WGL_DOUBLE_BUFFER_ARB 就看不到了
刪除一堆已經不再使用的舊代碼
以下是對內容的中文詳細總結,不涉及個人、開發者或作者,僅以“我們”視角進行客觀陳述:
我們在構建流程中檢查了幾個模塊的使用情況,發現某些部分目前并未實際啟用,或已經不再使用,因此決定進行精簡和清理,以簡化構建邏輯并減少維護負擔。
首先確認了預處理器相關的功能目前并沒有實際使用,因此選擇將該部分邏輯禁用,以防止它在構建過程中被不必要地調用,避免對其他人造成困擾。與此類似,“game_generated” 文件部分也未被使用,因此一并處理并移除。此外,“game_metadata” 的引用也被刪除,因為文件已經不再存在,再保留相關代碼會導致構建報錯。
隨后檢查了其他可能冗余的調試代碼,例如 debug_dump_struct
相關邏輯。確認已經沒有調用,因此決定清除這些無效代碼,以減少項目復雜度。這樣做的目的是讓其他開發人員不需要再處理這部分內容,降低理解和調試負擔。
在 OpenGL 的部分代碼中,存在一些類型轉換操作。了解到這些轉換對 GCC 編譯器存在兼容性問題,因此考慮移除相關轉換邏輯。部分代碼中指針轉換看似多余,因此也一并清理。
另外提到項目中有大量加載的文件,因此非常希望實現一個工具,可以掃描并搜索當前項目中所有加載的文件路徑。盡管開發這個工具本身非常簡單,目前尚未實現,僅因時間緊張和事務繁忙所致。
繼續清理中,發現仍存在一些舊的頭文件,如 metadata.h
等,這些文件也已經不再被需要,因此決定將它們完全移除。
最后,為了繼續簡化渲染邏輯,對 bitmap 相關部分進行排查,判斷其位于哪個模塊。回憶之后確定該部分代碼可能存在于 render_group
模塊內,因此接下來將轉向該模塊繼續清理和優化工作。
總的來說,以上操作旨在清除無用代碼、簡化構建流程、提升項目整潔度,并降低他人理解和編譯時所面臨的復雜性。
修改 game_platform.h
:將 PointerToU32
改名為 U32FromPointer
并相應更改調用位置,避免 GCC 和 Clang 的編譯警告
我們在處理紋理句柄(texture handle
)的過程中,發現由于其定義為 void*
指針類型,在某些編譯器(如 GCC)中進行類型轉換時會觸發不必要的強制類型轉換警告。盡管顯式進行了類型轉換(cast),編譯器依然認為這是不被推薦的操作,表現得過于嚴格。
為了避免這些多余的警告,同時也提升代碼的整潔性和可移植性,我們決定引入輔助宏或內聯函數來封裝這類轉換操作。具體來說:
- 定義了一個
uint32_from_pointer
(或類似命名)的方法,將void*
類型指針安全地轉換為uint32_t
; - 也定義了反向轉換
pointer_from_uint32
,用于從uint32_t
轉換回void*
; - 為了更通用和可擴展,可能進一步抽象成模板形式或帶類型參數的宏函數,例如通過參數指定目標類型并完成轉換;
- 在轉換過程中,先將指針值提升到合適的整數寬度(如
uintptr_t
或uint64_t
),再進行類型轉換; - 所有這些目的是為了讓編譯器接受這些轉換而不報錯,特別是在 Clang 和 GCC 中保持一致性。
除了轉換函數本身,我們還審查了當前平臺層中的一些紋理分配或架構接口,發現其中某些部分早已從特定平臺模塊中獨立出來,因此不再應當放置于 Windows 相關代碼段內。于是計劃將相關函數移動到更合理的通用位置,以反映架構變化并簡化結構。
此外,我們也準備在項目中各處將原先手動類型轉換的地方,替換為剛剛定義的轉換函數。這種做法不僅統一風格,還避免開發者在不熟悉編譯器行為時遇到困惑或錯誤信息。
整體目標是減少編譯器發出的不必要警告,使代碼更穩定、更易維護,并改善他人的開發體驗。
根據clangd 的警告去掉警告
運行游戲,確認一切正常運行
經過一番調整和清理之后,我們的代碼現在運行正常,達到了預期效果。這些調整包括解決了幾個簡單且被遺留的問題,尤其是那些能夠迅速解決的小問題,這使得項目變得更加整潔,減少了不必要的復雜性。
雖然這些問題已經被清理掉,但我們計劃繼續進一步檢查相關的功能,確認是否還有其他類似的問題存在。不過,目前我們已經解決了最緊迫且最明顯的問題,所以我們決定先處理這些簡單的任務,以便盡早清除掉積壓的工作。
接著,我們意識到已經花費了一些時間,檢查了代碼的執行情況并進行了優化。現在,剩下大約 40 分鐘的時間。為了高效利用這段時間,我們計劃繼續進行性能分析(profiling),以進一步提高項目的性能表現。然而,在此之前,我們還在考慮是否有其他重要的任務可以一并解決。
在接下來的工作中,我們會關注變量初始化失敗的相關問題,這是之前被提到的一項需要修復的情況。同時,也有團隊成員討論了如何在 OpenGL 環境下保持 fader
(漸變效果)的正常工作,這也是一個需要進一步解決的問題。
總體來說,盡管當前已經取得了一些進展,但仍有其他問題需要繼續優化和修復,以確保項目在不同平臺和環境下的穩定性和性能。
修改 win32_game.cpp
:移除淡入淡出效果(fader)
關于漸變效果(fading in and out),我們并不想繼續支持這一功能。最初添加這個功能是因為某個團隊成員想知道如何實現它,但我們認為這其實是一個非常糟糕的主意。原因在于,這個功能涉及到對 3D 圖形卡的初始化,這本身就很容易出問題,而漸變效果只是給這個問題增加了更多的復雜性,可能會導致更多的錯誤。
因此,理想的做法是徹底移除漸變效果的代碼。雖然我們已經展示了如何添加這個功能,但如果某些開發者想使用它,可以參考我們的代碼并自行實現,我們不會繼續支持這一功能。我們認為,將這個功能保留在項目中是沒有意義的,特別是在實際發布時,這個功能可能會導致一些圖形驅動程序初始化失敗或出現其他問題。
所以,我們決定去掉這一功能,并刪除與其相關的所有代碼。具體來說,我們會刪除與漸變效果相關的 theater
代碼,并且去掉其中的窗口顯示控制和其他不必要的部分。我們不再需要這些代碼來顯示窗口或處理窗口可見性的問題,因此這些代碼也將被移除。
我們會在游戲啟動前確保窗口顯示邏輯正常,并在初始化完成后直接顯示窗口。這樣,整個程序在啟動時就可以順利運行,而不需要處理漸變效果帶來的額外麻煩。
如果有人真的想要在自己的項目中使用漸變效果,可以參考原來的實現,但這并不在我們需要支持的范圍內。我們決定不再將這個功能包括在我們的發布版本中,因為它可能會影響到不同機器上的圖形驅動,導致各種潛在的問題。因此,漸變效果會被完全移除,任何人如果愿意,可以自行處理和實現。
去掉結構體挨著修改錯誤就行
考慮是否要移除多線程的 OpenGL 上下文
我們目前還在思考另一件事情,就是是否要撤銷 OpenGL 多線程上下文的實現。從目前了解的情況來看,多上下文的處理實際上并沒有帶來太大實際好處,除非使用的是高端顯卡,比如專業的 NVIDIA Quadro 系列。這類顯卡提供了真正的“復制引擎”(copy engines),使得多上下文的圖像資源復制能夠帶來性能提升。
但在大多數消費級顯卡上,并沒有這種優化機制存在。因此,在這些普通顯卡上使用多個 OpenGL 上下文,實際上只是增加了系統的復雜性,卻沒有帶來任何性能上的好處。
由此可以得出一個結論:我們實際上并不需要額外的上下文。更有效的方式可能是在主線程中直接準備好紋理資源。我們可以在程序初始化或資源加載階段預設好所有紋理,比如使用像 Pixel Buffer Object(PBO)這樣的機制來提前準備好紋理數據,然后僅在需要時調用一次 glTexImage
進行上傳,而不再通過多線程上下文去提交這些紋理。
采用這種方式,流程將變得更加簡單,數據準備會更清晰,紋理上傳也不會被其他線程打斷。同時也能避免潛在的多線程 OpenGL 同步和狀態管理的問題。
目前還不確定是否要立即對現有的多上下文處理進行改動,畢竟雖然這樣做可能不是最有效率的,但也勉強可以正常運作。可以暫時保留當前實現,日后再做優化調整。
總之,現在已經成功移除漸變效果(fader)相關功能,這是一個令人滿意的階段性成果。當前正處于項目清理的階段,雖然這些工作比較瑣碎無趣,但對整體架構的精簡和未來維護都是有益的。接下來是否進一步優化紋理上傳機制,視時間安排和優先級決定。
Pixel Buffer Object(PBO) 是 OpenGL 中用于異步像素數據傳輸的緩沖對象,主要用于提高紋理上傳和像素讀取的性能。它是 OpenGL 的一個擴展,允許在 CPU 和 GPU 之間更高效地傳遞圖像數據。
簡單理解:
在不使用 PBO 的傳統方式中,紋理上傳或讀取像素數據(如 glTexImage2D
、glReadPixels
)是同步阻塞操作,CPU 會等待 GPU 完成操作,導致性能瓶頸。
使用 PBO 后,這些操作可以變成異步的:
- CPU 可以把像素數據傳給 PBO,繼續干別的事;
- GPU 在后臺處理這些數據,不阻塞 CPU;
- 這就實現了CPU 和 GPU 并行工作,提升了效率。
用法總結:
1. 創建 PBO
GLuint pbo;
glGenBuffers(1, &pbo);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
glBufferData(GL_PIXEL_UNPACK_BUFFER, size, NULL, GL_STREAM_DRAW);
2. 向 PBO 寫入數據
void* ptr = glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
// 寫入數據到 ptr 指向的內存
glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
3. 上傳紋理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
// 這里的 `0` 表示從當前綁定的 PBO 中取數據
4. 解綁 PBO
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
典型用途:
- 高頻紋理更新(如視頻流、屏幕錄制、游戲串流)
- 讀取像素數據時避免阻塞(比如截圖)
- 多緩沖技術(雙 PBO 交替讀寫)
注意事項:
- PBO 不自動提高性能,只有在正確使用異步特性時才有優勢;
- 實現和效果依賴顯卡和驅動;
- 對于小圖像數據,開銷反而可能更大;
- 常與
GL_STREAM_DRAW
、GL_STREAM_READ
配合使用。
如果你正在做一個需要頻繁傳輸紋理數據的程序(比如串流游戲圖像),PBO 是一個可以顯著減少卡頓和提高幀率的重要工具。
再次運行游戲,并指出熱重載時分析器存在的一個 bug
我們之前注意到一個可能可以復現的 bug,雖然沒有人專門在 GitHub 上提到它,但我們觀察到在某些情況下,當執行重構并觸發重新加載時,字符串數據似乎會丟失,導致某些功能異常。比如在某些情況下,profiling 數據(性能分析數據)會消失,而這些應該是正常保留的。
我們懷疑這個問題可能出在字符串的管理或重載邏輯上。我們嘗試通過修改代碼、強制重建并重新加載程序的方式來觀察這個 bug 是否能復現。最初的問題表現為,在重新加載之后,profiling 數據沒有出現,或者某些字符串數據在界面上丟失。
我們嘗試做一些比較明顯的代碼更改,比如直接去掉用于渲染場景中元素(比如墻體、武器、怪物等)的全部代碼,來制造比較大的差異,強制觸發重新加載機制。但是在實際操作中,程序并沒有像我們預期的那樣出錯,反而依然運行正常,profile 數據雖然沒有顯示,但也沒有直接報錯。這說明可能 bug 本身未能觸發,或者之前的問題被不經意間修復了。
在嘗試制造代碼出錯行為時,我們進入了反匯編調試模式,試圖理解跳轉邏輯和條件斷點設置的問題。我們分析了跳轉指令、斷點條件以及寄存器的變化,最后決定繞過特定的跳轉條件,通過人工覆蓋跳轉目標的方式來避免斷言失敗。我們考慮使用一些簡單的填充指令(例如 NOP)或直接修改指令流,使其“跳過”有問題的邏輯路徑,這樣就可以繼續執行程序,而不會中斷在不想要的地方。
總結來看:
- 我們曾遇到字符串數據或 profile 數據在重新加載后消失的問題。
- 試圖重現這個 bug,但目前似乎無法復現。
- 懷疑問題可能與重載邏輯、字符串表、斷言機制或跳轉指令有關。
- 通過手動修改反匯編邏輯試圖繼續程序執行,跳過錯誤斷點。
- 目前仍在探索問題的根源,不過已經有一些方向,例如字符串生命周期管理或條件分支處理。
后續可能還會進一步深入調試,找到數據丟失的真正觸發點,并分析其與熱重載系統或運行時內存管理之間的關系。
調試器中:手動寫入 XOR 指令到內存
我們正在處理一個斷言觸發的問題,這個斷言的邏輯是用于檢測在渲染實體過程中是否遺漏了某些必須處理的情況。我們修改了一部分代碼邏輯,導致原來的斷言開始報錯,因為它檢測到我們沒有處理所有應有的實體種類。由于我們現在只是臨時注釋或移除了一部分邏輯,斷言因此失敗是意料之中的。
為了解決這個問題,我們沒有直接修改斷言的條件或者繞過調用路徑,而是采用了“外科手術式”的二進制補丁方式。我們進入了反匯編視圖,定位到了斷言觸發前的匯編代碼段,并找到了具體的地址區域。
我們分析了跳轉指令和寄存器操作的具體內存布局,識別出用于斷言判斷的 test
指令和 xor
指令所在位置。然后我們決定將這些指令覆蓋成一些無害的操作,以便程序可以繼續執行而不中斷。
我們采取的策略是用一系列 test eax, eax
(對應匯編為 85 C0
)指令填充原位置,這些指令不會對程序狀態造成副作用。接著,我們插入一個 xor eax, eax
指令來維持寄存器狀態的一致性,確保后續指令仍能正常運行。
因為 x86-64 是變長指令集,所以我們需要精確控制覆蓋區域的字節長度,確保既不截斷現有指令,也不留下無效的機器碼,從而避免運行時崩潰或未定義行為。我們確認整個補丁操作在語義上是“無害”的,僅僅是跳過了不必要的斷言檢測。
最終結果是:斷言被成功繞過,程序邏輯仍能正常繼續執行,且不會觸發錯誤或中斷。
總結:
- 我們遇到了一個由于實體未完全處理而觸發的斷言。
- 為了避免斷言中斷程序,我們通過手動修改內存指令的方式繞過斷言。
- 使用了一系列安全的
test eax, eax
和xor eax, eax
來填充指令空間。 - 這樣修改后程序能穩定運行,便于繼續調試和觀察后續行為。
這是一種典型的底層調試技術,適用于需要快速驗證變更影響但不想修改大量高層代碼邏輯的場景。我們做到了最小化干擾且保證執行環境穩定。
這個修改內存應該打開vs
用vs打開二進制文件的路徑
哎不對pdb沒刪掉嗎
之前把依賴項刪掉了
重新編譯之后可以了
觸發之前的段錯誤
換成提示的vssetings看上去還行
這段機器碼替換是你在調試或運行時手動修改內存的一種方式,目的是繞過崩潰或斷言等非法路徑。我們逐字節來解釋發生了什么,前后含義完全不同,下面是詳細對比與解釋。
原始機器碼(崩潰前):
c7 04 25 00 00 00 00 00 00 00 00
逐字節解釋:
這是一個 mov
指令,操作的是絕對地址:
c7 04 25 00 00 00 00 -> mov dword ptr [0x00000000], 0x00000000
它的結構如下:
c7 04 25
:表示使用絕對地址模式(mov [imm32], imm32
)00 00 00 00
:目標內存地址 =0x00000000
00 00 00 00
:寫入的值 =0x00000000
問題:
試圖寫內存地址 0x00000000
(空指針地址),在現代操作系統下會觸發段錯誤(segmentation fault),因為這是無效地址,操作系統保護了該頁不被訪問。
修改后的機器碼:
48 85 c0
48 85 c0
48 85 c0
33 c0
拆解解釋:
按順序來:
-
48 85 c0
→test rax, rax
測試rax
是否為 0,不會改變寄存器值,只會影響標志位。(重復了三次,是為了覆蓋原先等長的字節)
-
33 c0
→xor eax, eax
將eax
清零,等價于mov eax, 0
,但更高效常用。
為什么這樣修改?
- 原本是個非法寫操作(空指針寫),會崩潰。
- 改成無害操作,比如
test
(邏輯判斷但無副作用)+xor
(清零但合法) - 確保總共是 11 字節(覆蓋原指令長度),避免執行跳轉錯位。
總結用途
原始指令 | 作用 | 問題 |
---|---|---|
mov [0x0], 0x0 | 向 NULL 地址寫入 | 觸發段錯誤(崩潰) |
替換后指令 | 作用 | 效果 |
---|---|---|
test rax, rax x3 + xor eax, eax | 無害清零/判斷 | 不會崩潰,繼續執行 |
適用場景
- 繞過無效內存訪問
- 臨時屏蔽斷言或危險行為
- 繼續運行程序做后續調試
如果你還想了解怎么在調試器里實際操作這段替換(例如在 GDB 或 WinDbg 中),我可以進一步示例操作。是否需要?
點擊continue 繼續運行
運行游戲,發現字符串顯示出問題
這個我沒出現
我們發現程序中的字符串內容出現了破損(損壞、亂碼或丟失),經過驗證,這種問題確實存在,并且可以穩定復現。目前已確認字符串數據在某些條件下被破壞,這意味著之前關于“字符串應該被正確保存”的假設并不成立,保存過程存在問題,具體原因尚待進一步調查。
為排查問題,我們執行了一些操作:
- 通過一次字符串讀取的場景確認字符串確實出錯;
- 將某些代碼邏輯注釋掉,避免每次重現問題時都需要復雜操作(例如手動修改匯編指令或補丁內存);
- 成功建立了一個穩定的復現步驟:只要執行某段邏輯或設置某個標志為 0,就可以觸發字符串損壞;
- 這讓我們可以方便地進入調試流程,進一步分析字符串損壞發生的位置和原因。
我們推測字符串損壞的根本原因可能很簡單,比如字符串沒有正確拷貝或指針懸空,但由于這是一個熱更新/代碼重載相關的流程,涉及到較多的底層內存操作,不能掉以輕心。這類系統非常敏感,尤其是與調試功能結合使用時,如果字符串系統不可靠,會嚴重影響調試信息的可讀性,進而影響開發效率。
當前計劃是基于這個可復現用例,繼續深入排查到底是哪個階段破壞了字符串,確保熱重載和調試系統可以正常共存。我們需要特別關注字符串的生命周期管理、靜態存儲與動態更新之間是否存在沖突,或者是否有未處理好的數據拷貝邊界問題。
修改 game_debug.cpp
:修復字符串處理問題
我們發現當前調試菜單中顯示的字符串(特別是頭部字符串)出現損壞的問題。這些字符串不是存儲在單個調試元素中,而是保存在調試樹結構中的“變量組”節點上,通過這些節點中的 name
和 name_length
字段進行引用。經過分析和代碼排查,我們意識到這些字符串在熱更新或重載后被錯誤地引用或丟失,其根本原因是指針仍然指向舊的(可能已被釋放或無效的)內存區域。
為了解決這個問題,我們進行了如下處理和優化:
一、定位問題字符串的來源
- 字符串來源于變量組的層級結構,而非單個調試項;
- 每個變量組在創建時都會傳入一段字符串作為名稱,我們發現這部分字符串可能未被正確拷貝,而是直接引用原始內存。
二、改進變量組字符串存儲方式
我們決定不再直接使用外部指針來引用名稱字符串,而是在創建變量組時主動拷貝字符串內容,確保其生命周期獨立、穩定:
- 刪除了原本通過
name
和name_length
直接引用外部內存的方式; - 改為使用內存池進行字符串復制;
- 實現了一個新的函數
PushStringNoTerminate
,用于將字符串按指定長度拷貝到內存池中,并在結尾自動添加 null 終止符; - 這樣可以防止重載代碼或熱更新后原內存失效造成字符串內容混亂。
三、對克隆邏輯進行優化
- 對于已經存在的變量組,在克隆子節點時,不再復制字符串內容,而是直接讓子節點指向原始字符串;
- 這樣可以避免重復拷貝相同的字符串內容,節省內存,同時保持一致性。
四、清理和精簡判斷邏輯
- 精簡了字符串比較函數
StringsAreEqual
的使用; - 通過簡化判斷過程提高代碼可讀性,減少冗余邏輯。
最終目標和狀態
通過這一輪修改,我們確保了變量組中的名稱字符串:
- 始終存儲在有效的內存中;
- 不依賴外部不可控的生命周期;
- 不會在代碼重載或調試時發生內容損壞;
- 能夠穩定顯示在調試菜單中,保證調試信息的可用性。
整體來說,這是一次圍繞調試系統穩定性的改進,重點解決了內存生命周期與指針引用不當導致的字符串損壞問題,增強了程序在開發過程中的可維護性和可靠性。
恢復之前代碼
運行出現奇怪的Bug
再次運行并測試熱重載,字符串穩定但發生崩潰
我們繼續調試整個系統,重點是驗證字符串相關問題是否已經徹底解決,并且順便檢查另一個尚未明確的問題——性能分析(Profile)數據為何消失。
一、字符串問題修復后的驗證
我們在修復字符串復制邏輯后進行了以下驗證操作:
- 重新回到之前的問題點,例如重新加載渲染組;
- 檢查熱重載或代碼修改后是否仍然出現字符串被破壞的問題;
- 結果顯示當前字符串已經可以穩定保留,不再在多輪運行或修改后出現亂碼或丟失;
- 渲染系統中的調試信息顯示正常,表明我們對內存池中字符串拷貝與指針引用的修復有效。
二、跨模塊驗證穩定性
- 進一步進入游戲邏輯部分,而不是僅僅停留在調試模塊中;
- 原先我們觀察到的字符串破壞其實是出現在實際游戲運行中,而非調試菜單本身;
- 當前進入游戲驗證時,發現系統運行出現異常,似乎崩潰了;
- 崩潰發生的位置較為奇怪,和之前的渲染或字符串邏輯并不直接相關。
三、當前狀態總結
- 字符串復制和管理的問題目前看起來已經被成功解決;
- 字符串不再在熱重載過程中丟失,調試數據的可讀性也已恢復;
- 然而,系統在實際運行中仍存在潛在崩潰點,可能是另一個獨立的問題,暫時還未定位;
- 性能分析(Profile)功能仍無法正常使用,需要作為下一個重點進行排查;
- 當前程序可以穩定編譯、運行并顯示調試信息,但仍有其他系統性問題等待解決。
后續計劃
- 跟進崩潰點分析,確定具體的出錯模塊;
- 查明性能分析功能失效的原因,確保調試工具完整可用;
- 全面驗證字符串處理在所有運行路徑中的一致性,確保不會在邊緣情況中回退為舊邏輯或觸發內存越界。
調試器中:檢查線程,發現 OpeningEvent
不一定是有效的
我們在調試過程中發現一個新的問題,這次和字符串無關,而是出現在碰撞幀(collision frames)處理流程中,但問題仍可能與內存一致性或重載過程相關。
一、碰撞幀邏輯異常觸發
- 碰撞幀本身和字符串并沒有直接關系;
- 但是在重新加載(reload)代碼或資源后,系統在處理碰撞幀時卻觸發了錯誤;
- 初步看上去像是某個狀態不一致導致的異常。
二、線程狀態分析
- 相關線程是有效的,說明調度或任務上下文沒有問題;
- 第一個打開的代碼塊也是有效的,沒有指針懸掛或非法訪問;
- 但“opening event”(開啟事件)似乎存在問題。
三、開啟事件可能的問題
- 雖然這個事件本身仍在緩沖區(buffer)中;
- 但這個緩沖區是一個靜態緩沖區(static buffer);
- 靜態緩沖區通常意味著生命周期貫穿整個程序運行;
- 然而,可能在熱重載過程中,緩沖區數據沒有正確更新,或者某個狀態被污染了;
- 因此,事件數據雖然“形式上”還在,但可能已經無效,導致了崩潰或錯誤。
四、當前結論
- 本次異常出現在碰撞幀處理過程中;
- 涉及的數據結構看似存在,但內部狀態可能已經不可用;
- 可能與靜態緩沖區的生命周期或熱重載后的狀態恢復有關;
- 雖然字符串模塊已修復,但系統其他部分在重載后仍可能遺留無效指針或狀態不一致的問題;
- 需要進一步確認靜態資源在重載流程中的有效性與更新邏輯,避免引用舊數據或未同步數據。
后續排查方向
- 檢查熱重載過程中靜態緩沖區是否被正確刷新或重新初始化;
- 確認事件系統中所有指針和狀態是否同步;
- 驗證碰撞幀中是否還有其他類似的數據使用了“看似有效實則無效”的引用;
- 建議對所有跨重載生命周期的對象添加更嚴格的校驗機制。
問題探討:為何 GlobalDebugTable
是靜態緩沖區?
這個問題的核心是關于為什么在某個地方使用了靜態緩沖區(static buffer)。經過分析,似乎這個設計存在一些不合理之處,可能是遺留問題。
一、靜態緩沖區的使用疑問
-
靜態緩沖區的定義: 這里的靜態緩沖區是作為全局調試表(global debug table)來使用的,數據被寫入這個表中。
-
為什么使用靜態? 使用靜態緩沖區的原因不明確,可能是因為歷史原因或某種特定需求,但這種做法可能帶來一些問題。
-
靜態的潛在問題:
- 潛在重定位: 靜態緩沖區在程序重載時可能會被重新定位,這會導致原本應該持續有效的數據失效或出錯。
- 位置不合理: 靜態緩沖區的使用地點不當。如果它確實是全局的,那么應該放在平臺層(platform layer)而不是游戲層(game layer)。這樣做不僅可以減少游戲層的復雜性,還能使平臺層更好地管理底層資源。
二、設計缺陷分析
- 靜態緩沖區放置錯誤: 把靜態緩沖區放在游戲層可能帶來不必要的麻煩,尤其是當需要重載或進行內存操作時,游戲層對它的管理可能不夠細致。
- 平臺層的更合理選擇: 理論上,平臺層應該負責管理這些底層資源,并且將其傳遞給游戲層使用。這樣,平臺層可以確保資源的生命周期被正確控制,避免了游戲層和底層邏輯之間的不必要耦合。
三、反思與總結
- 設計問題: 當前的設計讓調試表作為靜態緩沖區存在于游戲層,這種做法顯得有些不合理,也容易在重載或狀態更新時導致問題。游戲層并不應該直接管理這些底層的資源。
- 更好的設計方式: 應該將靜態緩沖區的管理職責移到平臺層,確保平臺層對這些資源有更清晰的控制權和生命周期管理。平臺層將這些資源傳遞給游戲層,這樣可以減少不必要的復雜性,也能避免許多潛在的錯誤。
- 歷史遺留問題: 看起來這個設計可能是過去的遺留問題,當前應該進行重新審視和修正,以確保代碼更加健壯、靈活。
后續行動
- 重構建議: 對現有的資源管理進行重構,特別是將靜態緩沖區的管理放到平臺層,確保資源的生命周期被正確控制,避免直接將其放置在游戲層中。
- 進一步調試: 在修正資源管理問題的同時,確保所有的調試數據都能被正確保存和讀取,避免再次出現類似的字符串損壞問題。
在 win32_game.cpp
中讓 GlobalDebugTable
成為主控版本,并重寫其整理邏輯
問題與分析:
當前的設計存在一個核心問題,即如何管理和傳遞調試表(debug table)。目前,調試表的傳遞方式存在一些混亂和不合理的地方,特別是在游戲和平臺之間的交互上。分析后發現,現有的實現方式可能并沒有經過深思熟慮,導致一些不必要的復雜性和潛在問題。
1. 當前的實現方式問題:
- 調試表的傳遞問題:目前,調試表的傳遞方式是游戲從外部獲取調試表,這樣的做法不清晰,也容易導致問題。我們并不清楚為什么要這么設計,尤其是為什么要從游戲獲取調試表。
- 不必要的初始化:例如,事件數組的索引(
event array index
)被設置為零,可能是為了防止在沒有游戲數據時,末尾的數據被覆蓋,但這種做法顯得不夠合理。 - 靜態緩沖區的管理:當前的靜態調試表緩沖區放置在游戲層,容易導致重載時出現問題。正確的做法應該是將靜態調試表放到平臺層,平臺層再將其傳遞給游戲層。
2. 計劃的改進方案:
- 將調試表放置在平臺層:理想的做法是將調試表放到平臺層,而不是游戲層。平臺層負責管理調試表,游戲層只需要使用它。這種方式能夠更好地分離游戲邏輯和平臺層資源的管理。
- 傳遞方式調整:應該在初始化階段將調試表傳遞給游戲層。具體做法是在游戲開始時,通過
memory debug table
來設置全局調試表,避免游戲層管理調試表的生命周期。也就是說,調試表應該在程序開始時初始化,并直接傳遞給游戲使用。 - 刪除不必要的返回值:在新設計中,調試表不再需要通過返回值從游戲層傳遞出去,而是直接在初始化階段通過平臺層傳遞給游戲。
3. 改進步驟:
-
調整調試表的管理方式:確保調試表作為全局變量,應該由平臺層負責管理。游戲層只需要在初始化時接收并使用調試表。
-
初始化調試表:在游戲的更新和渲染階段,初始化時直接通過
global debug table
來設置調試表,而不再依賴返回值傳遞調試信息。 -
簡化代碼:去除多余的返回和不必要的操作,簡化代碼邏輯,減少復雜性。例如,將一些冗余的操作(如事件索引的設置)去除,避免不必要的狀態更改。
-
代碼結構調整:通過將全局調試表的管理從游戲層移至平臺層,并確保初始化時直接傳遞,來提升代碼的清晰度和可維護性。
4. 結果預期:
通過以上改進,調試表的管理會更加清晰和規范。平臺層和游戲層之間的交互將變得更加明確,減少了因設計不當導致的潛在錯誤。同時,通過簡化代碼,能提高代碼的可讀性和可維護性,也能降低后續修改和擴展的復雜性。
最終的目標是確保調試表能夠穩定地存儲和傳遞,而不在游戲層和平臺層之間產生不必要的混淆和沖突。
運行游戲并做些微調
問題與目標:
當前目標是確保調試代碼可以穩定運行,并在調試過程中避免崩潰。希望通過反復操作,確保在多線程和跨幀邊界的情況下,調試代碼能夠正確報告問題。
1. 調試代碼的重新加載:
調試代碼正在重新加載,并且不再僅僅依賴于單一的線程和幀。這意味著調試系統現在可以處理更復雜的場景,尤其是在多個線程并且跨幀邊界的情況下進行捕獲和報告。
2. 增加多線程支持:
通過對調試系統的改進,能夠處理多個線程的調試信息,這些線程可能會跨越幀的邊界,確保即使在這種復雜的多線程和跨幀的環境下,調試信息也能被正確地捕捉和報告。
3. 程序的穩定性:
盡管增加了復雜的調試機制,當前的目標是在調試過程中盡量避免程序崩潰。通過對調試代碼的進一步操作,確保它能在多種環境下穩定運行,不會因復雜的操作導致崩潰。
4. 調試代碼的靈活性與執行:
這次的更新使得調試代碼更具靈活性,能夠適應更多的復雜場景和多線程的需求。通過在程序執行過程中加入更多的調試工具,能夠更好地監控和捕捉問題。
5. 個人對引擎編程的偏好:
在過程中,明確表示了對于引擎編程的興趣和對游戲編程的興趣相對較低。這也表明在調試和開發過程中,優先關注的方向是引擎本身的穩定性和調試功能,而非游戲邏輯本身的開發。
總結:
整個過程中,調試代碼得到了改進,增強了多線程支持,確保即使在復雜的操作和跨幀情況下,調試信息也能正確捕捉和報告。程序的穩定性在逐步提升,而開發重點則更多地放在引擎層面的優化與調試功能的增強。
調查分析器(Profiler)存在的問題
問題描述:
當前的關鍵問題是,為何配置文件(profile)會消失。在調試系統中,配置文件本應保持存在,但在實際運行時,配置文件卻意外消失。我們懷疑這可能與調試系統的某些操作有關,但不清楚具體的原因。
1. 分析調試代碼:
調試代碼中,我們會進行調試塊的操作,并查看是否有正確的根節點配置。調試的過程中,系統會執行一系列開始和結束的調試塊操作,并進行數據記錄。正常情況下,這些操作是配對的,且每一幀的開始都應該有對應的“根”配置節點。
2. 代碼重載的問題:
一個可能的原因是,代碼重載過程中沒有正確創建根配置節點。在重載代碼時,系統本應根據當前的調試塊,重新創建根節點。如果在此過程中出了問題,可能會導致沒有設置根配置節點,從而造成調試信息丟失,甚至配置文件消失。
3. 推測的原因:
假設代碼重載過程中沒有正確處理調試塊,導致在某些情況下,調試系統沒有正常關閉之前的塊,從而未能正確創建根配置節點。這會導致后續的調試操作中,未能正確識別和設置根節點,從而出現配置文件消失的現象。
4. 當前的調試流程:
在正常情況下,調試系統會在執行時創建調試塊,每個塊都有開始和結束的操作,確保調試信息的完整性。當執行完一個操作時,調試系統應關閉當前塊并開始新的塊。這些操作和調試信息應當匹配,不應該丟失。
5. 事件數組的問題:
另一個需要注意的問題是,事件數組的索引為什么會被重置為0。根據調試信息,我們發現事件數組的索引被錯誤地設置為零,這可能是問題的根源。如果事件數組的索引始終為零,系統可能只能使用一個事件數組,這就會導致在進行調試時錯過某些數據,影響調試信息的收集。
6. 調試表的操作:
在調試表中,有兩個事件數組交替使用,即pingpong。然而,當我們將事件索引設置為零時,實際上表明我們只使用了其中一個數組。這會導致調試信息丟失,因為系統只會記錄一個數組的內容,而另一個數組的內容可能被忽略。
7. 問題的核心:
根本問題是,調試系統中的事件數組和調試塊的管理不當,特別是在代碼重載和更新過程中,導致根配置節點沒有正確設置或事件數組索引被重置。最終,調試信息無法完整記錄,導致配置文件丟失。
解決方案方向:
- 修復代碼重載中的根配置節點創建問題,確保每次代碼重載后,都會重新創建并正確設置根配置節點。
- 避免將事件數組的索引重置為零,確保每次調試操作都使用正確的數組,避免錯過調試信息。
- 優化調試塊的管理,確保每個調試塊的開始和結束都能正確匹配,避免在調試過程中丟失數據。
通過這些調整,可以解決配置文件消失的問題,并保證調試系統的穩定性和可靠性。
在 win32_game.cpp
中添加條件清除:若游戲加載失敗則清除調試事件數組
我們分析當前調試系統中事件數組索引的行為,并嘗試確認其中的問題。具體邏輯如下:
1. 當前事件數組索引的切換機制:
事件數組索引(event array index
)通過一種**交替切換(ping-pong)**的機制在兩個數組之間來回切換。每一幀結束時,會通過 原子交換(atomic exchange) 操作來交換當前索引的位置,這樣可以保證新的調試事件寫入到一個干凈的區域,避免與之前幀的數據混淆。
2. 原子交換的作用:
- 原子交換操作不僅負責切換當前使用的事件數組索引,同時也會清空被切換到的那一組事件數組中的數據;
- 這樣可以確保下一次使用時,調試系統寫入的是一個全新的空數組,保證數據干凈且一致。
3. 對原先邏輯的懷疑:
回顧舊邏輯,存在一段代碼在每一幀結束(frame end)時強制將事件數組索引清零。這段邏輯很可能是過去在調試系統中共享調試表(debug table)時的遺留物。
這種強制清零可能是為了清理未被主動清空的數組數據,防止因游戲未加載而導致數據積壓。也就是說,如果游戲未成功加載,就無法進入正常的幀結束流程,也就無法正確清除舊數據。因此添加了這段邏輯作為保護機制。
4. 對現狀的改進建議:
- 當前系統已不再使用共享調試表,因此這段強制清零的代碼已經不再必要,甚至可能引起調試數據丟失;
- 應該只在一種情況清空事件數組索引:當游戲加載失敗時。這時確實需要清理調試數據,以免堆積;
- 正常加載游戲的情況下,絕不應該強行清零事件數組索引,否則會導致調試信息在每幀結束前丟失或錯亂。
5. 改進邏輯建議:
if (game_load_failed) {// 只有在游戲加載失敗的情況下清零debug_event_array_index = 0;
}
這樣做可以確保:
- 游戲未加載成功 → 清除調試事件數組,避免積壓;
- 游戲加載成功 → 繼續使用正常的 ping-pong 索引切換,不清空,保持調試數據完整。
6. 總結:
調試系統中原本用于清理事件數組索引的邏輯應當僅限于游戲加載失敗的情況。其他情況下,這一行為會導致調試數據出錯或丟失。通過引入條件判斷邏輯,可以使系統在保留必要保護機制的同時,確保調試信息的完整性與準確性。
運行游戲并繼續問題調查
我們認為當前的修改更為正確。之前的做法存在嚴重問題:調試事件總是寫入到同一個事件數組中,這種做法會導致多個線程在寫入調試信息時發生沖突。若其他線程仍在使用同一個數組位置,調試信息就會被覆蓋或丟失,造成調試數據異常。這種行為是錯誤的,可能會導致分析時出現誤判或信息缺失。
經過更正后,調試事件數組通過 ping-pong 機制交替切換,避免線程之間的沖突。這是符合我們預期的行為。
隨后進行了驗證操作,雖然一開始認為這個問題并不是導致性能分析信息丟失的根本原因,但修改后發現性能分析信息重新顯示出來了,說明問題的確是出在調試事件數組被錯誤清空上。
這說明,事件數組被清空后丟失了根分析節點,導致后續幀中的性能數據無法正確關聯和顯示。
進一步分析表明,性能分析信息的消失可能還有其他隱患,例如線程調試塊未正常關閉。如果在代碼重載或幀切換時存在懸掛的線程塊(thread block)未被正確結束,那么整個分析樹的結構就會不完整,從而無法正確顯示分析信息。這一點也必須特別關注。
總結如下:
問題診斷與修復過程:
-
原問題:
- 每一幀結束時錯誤地強制將事件數組索引重置為零;
- 導致調試系統始終寫入同一個數組;
- 多線程調試信息發生覆蓋,性能數據丟失;
- 導致性能分析界面無法正確顯示根節點及相關數據。
-
修復措施:
- 移除無條件清零操作;
- 僅在游戲加載失敗的情況下清空事件數組;
- 恢復 ping-pong 切換機制,實現調試數組安全交替使用。
-
驗證效果:
- 修復后性能分析界面正常顯示;
- 說明問題確實與調試數組錯誤清空有關;
- 原以為不是這個原因,但實際驗證表明它就是核心問題之一。
-
后續需要注意:
- 檢查是否存在未正確關閉的線程塊;
- 特別是在代碼重載或幀切換時;
- 確保調試信息結構完整,避免掛起狀態。
這一過程體現了系統調試中隱藏狀態的復雜性,一點小的邏輯失誤可能導致整個調試系統失效。目前通過定位并修正清零邏輯,已經顯著改善了系統行為。后續仍需持續關注線程塊生命周期的正確管理。
調試器中:進入 BeginBlock
并檢查 DebugState
我們當前正處于問題觸發的狀態,因此我們深入查看了調試信息跟蹤相關的代碼邏輯,試圖驗證并找出具體原因。
首先檢查了線程的調試狀態。在調試系統中,每個線程都有對應的調試信息,其中包括一個“當前打開的代碼塊”字段。我們查看了當前活躍線程的調試狀態,發現其打開的塊是“調試整理塊”(debug collation block),而這個塊的父節點居然指向它自己,這是明顯不正確的。
這種情況通常只會出現在遞歸調用中,但這里并不是遞歸函數,意味著這個塊并沒有被正確關閉。這種異常恰好與我們觀察到的問題相符 —— 性能分析信息缺失,很可能是因為存在未正確結束的調試塊。
我們定位到這段問題代碼是在某個位置。問題塊出現在執行 begin_block()
后,本應執行 end_block()
進行關閉,但在某種情況下,end_block()
并沒有被調用。具體來說:
- 這是一個包裹在游戲代碼熱重載過程中的塊;
- 在
begin_block()
調用之后,游戲代碼被卸載或刷新; - 而在新代碼重新加載之前,
end_block()
還沒來得及執行; - 由于重載發生,調試系統的數據結構也會重置;
- 導致這個“懸掛”的塊始終保持打開狀態,不再被關閉。
這就解釋了為什么性能分析樹無法正確顯示根節點或其他信息 —— 根本沒有形成完整的塊結構。
進一步思考了熱重載對調試系統的影響,發現問題可能出在塊匹配機制本身。系統匹配 begin_block()
和 end_block()
時,可能是依賴某些標識符,例如一個 GUID id
,而這個標識符在代碼重載過程中可能會變化。
如果在熱重載后標識符不一致,調試系統就無法正確匹配并關閉原本打開的塊,從而留下“懸掛塊”。這種掛起狀態會導致后續幀無法以正確方式嵌套新的分析塊。
不過也有疑問,即熱重載是否真的會導致這些關鍵字段(如 GUID)變化?一度懷疑 GUID 是問題根源,但后來又認為也許 GUID 沒有實際變化,可能還需進一步驗證。
詳細總結:
-
問題現象:
- 某些調試塊未被正常關閉;
- 導致調試樹結構損壞,性能分析數據無法正確顯示;
- 調試狀態中存在自指(自身為父)塊,明顯不合法。
-
具體原因:
- 游戲代碼重載過程中,
begin_block()
已執行但end_block()
尚未執行; - 重載觸發后調試系統重置,原有塊信息丟失;
- 殘留塊無法關閉,形成“掛起狀態”;
- 進一步導致調試樹邏輯異常。
- 游戲代碼重載過程中,
-
潛在根本原因:
- 塊匹配機制依賴某些標識(如 GUID id);
- 熱重載可能導致這些標識不一致,匹配失敗;
- 導致新舊塊之間邏輯斷裂。
-
可能的解決方向:
- 在執行重載之前,強制關閉所有尚未結束的調試塊;
- 或者確保重載不會清空調試塊的狀態結構;
- 或對調試塊增加更可靠的唯一識別機制,避免誤匹配。
當前判斷比較明確:確實是因為調試塊在代碼重載過程中未能正常結束,造成掛起,從而引發分析數據異常。這是調試系統與熱重載機制交互中的一個典型邊界問題,后續應在熱重載前后處理邏輯中加入強制性調試塊閉合保障。
檢查事件的 GUID 并進入 EventsMatch
函數
我們正在深入檢查調試系統中“事件匹配”的機制,目的是確認在代碼重載前后,是否由于事件匹配邏輯的問題,導致調試塊無法正確閉合,從而造成性能分析數據異常。
首先,我們觀察了兩個調試事件:
- 一個事件來自
game.cpp
,位置在 2412:34; - 另一個事件來自
debug_interface
,也是在相同的位置 2412:34; - 兩者的
source location
顯然是相同的。
從表面看,這兩個事件應該可以匹配。如果事件匹配邏輯正確,這樣的定位信息應該能對應成功。
接著,我們進一步檢查事件的匹配機制到底是如何工作的。
我們的目標是搞清楚在判斷兩個調試事件是否匹配時,系統依據了哪些字段或邏輯條件。也就是說:
- 系統是否只基于 文件名 + 行號 + 列號 來判斷兩個事件是否屬于同一個調試塊?
- 或者還依賴其他標識,如
grid id
、函數地址、某種運行時 ID? - 如果熱重載過程改變了這些關鍵標識(如內存地址、指針等),就會導致兩個邏輯上相同的事件被識別為不同,進而無法匹配
begin_block
和end_block
。
這一點至關重要,因為:
- 如果事件匹配依賴了會在重載后改變的字段(比如內存地址、函數指針等),那么
end_block
在新代碼加載后執行時就無法正確關閉此前的begin_block
; - 導致這個“懸掛的調試塊”一直保留在線程狀態中,成為性能分析樹的異常根源;
- 這將使得每一幀都誤認為存在一個未關閉的根塊,進一步破壞整棵分析樹的結構。
因此,我們下一步的重點是:
- 明確匹配邏輯到底用到了哪些字段;
- 判斷這些字段是否在熱重載前后會發生改變;
- 如果會改變,就需要重新設計匹配邏輯或在重載前主動清理這些未結束塊。
總結如下:
當前調試步驟總結(中文):
-
觀察:
- 兩個事件來源于同一位置
game.cpp
的 2412:34; - 看起來應當可以正確匹配。
- 兩個事件來源于同一位置
-
推測問題:
- 實際可能沒有匹配成功;
- 原因可能是匹配邏輯依賴了某些會被熱重載重置的字段。
-
正在調查:
- 調試系統內部是如何判斷兩個事件是否“屬于同一個塊”的;
- 是基于源代碼位置,還是其他更易變的運行時信息。
-
關鍵問題:
- 如果匹配邏輯中包含了在熱重載中會改變的內容(如指針或 grid 編號),將造成調試塊無法閉合,產生“懸掛塊”;
- 從而破壞性能分析的完整性。
接下來,我們需要具體定位匹配邏輯的代碼,并確認其字段來源與行為。如果需要,我也可以幫助分析匹配函數內部邏輯,只需貼出對應的判斷代碼。
在 game_debug.cpp
中臨時添加斷言來檢查 EventsMatch
的行為
我們在調試過程中深入檢查了性能分析系統中調試塊(debug block)的匹配機制,重點是分析事件記錄過程是否存在邏輯錯誤,尤其是在處理打開塊(opening event)時。以下是詳細的中文總結:
問題背景分析:
我們希望觀察調試塊在熱重載過程中的行為是否正確,尤其是“打開事件(opening event)”是否被正確記錄并能與對應的“結束事件(end block)”匹配。為此臨時把原本的條件判斷替換為一個斷言(assert),以確保當發生不符合預期的情況時可以立刻被發現。
一旦程序命中斷言,我們觀察到以下異常現象:
- 打開的事件(opening event)的
grid
竟然指向了一個結束塊(end block); - 這是不合理的,因為打開事件的
grid
應該對應的是一個“打開中的塊”,而不是某種已結束的結構; - 同時,事件記錄中的
record_span
起點被標注為了幀序列開頭,但這并不準確。
關鍵調查步驟:
-
檢查事件結構:
- 我們打印并比較了當前打開事件(opening event)和現有事件(event)的結構,發現它們之間的
grid
指針設置似乎不對。
- 我們打印并比較了當前打開事件(opening event)和現有事件(event)的結構,發現它們之間的
-
查看調試元素內容:
opening event
對應的調試元素是LoadAssetWorkDirectly
,這是合理的,表示這是在資源加載任務中的某個工作塊。
-
定位設置流程:
- 我們追蹤了設置
debug block
的過程,發現事件是在調用AllocateDebugBlock
時被分配和設置的; - 但是進一步查看后發現,分配過程中將一個事件的指針賦值給了調試塊結構中的某個字段,但這個指針是臨時變量的地址,并沒有存儲到安全的內存區域中;
- 換句話說,我們把一個將要失效的指針當作“持久引用”存儲進去了。
- 我們追蹤了設置
根本問題定位:
我們把一個臨時事件結構的地址作為指針保存進了 debug block
,而沒有將其正確拷貝到持久內存或事件數組中。因為:
- 這個事件結構指針是分配時局部變量或臨時緩存的地址;
- 它在事件生命周期結束后被釋放或覆蓋;
- 導致后續訪問這個指針時會得到無效的數據;
- 這也解釋了為什么
opening event
會指向一個不合理的結束塊,甚至自身變得無效。
總結結論(中文):
- 當前斷言命中說明事件匹配機制中存在邏輯問題,特別是
opening event
的來源不穩定; - 問題根源是錯誤地將臨時指針作為持久引用存儲,違反了事件系統的內存管理原則;
- 應當在設置調試塊時,將事件從臨時指針拷貝到穩定的事件表或結構中,避免使用生命周期不確定的引用;
- 這個 bug 很可能就是調試系統中無法正確關閉塊、產生懸掛結構、性能分析樹異常的直接原因;
- 接下來應修改調試塊的初始化邏輯,確保引用的事件數據穩定可靠。
如需我進一步寫出建議的修復代碼或繼續分析事件分配函數的具體實現邏輯,請繼續說明,我可以立即協助。
注意到事件是臨時的,因此現在改為使用 StoredEvent
我們在對調試事件處理流程進行深入分析時,識別出當前事件管理中存在一項關鍵的生命周期管理問題。以下是詳細的中文總結:
分析背景:
我們正在處理的事件是短暫的(transitory),也就是說:
- 事件在被處理完之后就不再有效,不能再被依賴;
- 當前采用的是增量解析方式(incremental parsing),因此事件處理后會被清空或覆蓋;
- 一旦緩沖區被刷新,事件對象就會失效。
關鍵問題識別:
由于事件失效,不能直接在調試塊中保留對事件的引用。然而在現有設計中,調試塊結構可能保存了對這些臨時事件的引用,從而導致:
- 數據引用懸空;
- 匹配邏輯混亂;
- 分析結構可能遭到破壞。
正確做法明確:
我們可以放心引用的是復制并保存下來的事件(stored event),因為它的生命周期足夠長,至少可以跨多個幀使用。相比之下:
- 當前事件是短暫的;
- 復制后的事件是持久的(或更長久的);
- 所以,我們應該只在調試塊中引用持久化的事件副本,而不是臨時數據。
更進一步的優化思路:
考慮到即便是**存儲事件(stored events)**也可能在之后被釋放(因為幀數據數量受限),我們提出了更保險的改進方案:
- 直接將需要的調試信息復制到調試塊本身內部;
- 不再依賴外部事件對象的生命周期;
- 這樣就能確保調試塊內容在任何時候都穩定、獨立、可靠。
例如:
- 將事件中的
file
,line
,frame index
,event ID
等重要信息,直接復制進調試塊; - 避免保留原始事件的指針或引用。
當前實現檢查:
我們回頭確認了當前代碼的行為,觀察如下:
- 正確地調用了
StoreEvent
函數; - 成功地將
event
,element
,frame index
,first open GUID
等復制進入結構中; - 但可能尚未完全脫離對臨時數據的依賴。
總結結論(中文):
- 當前事件處理方式為增量解析,事件本身生命周期極短;
- 調試塊不能依賴這些短暫事件中的任何指針或引用;
- 應當優先使用復制后的“持久事件副本”;
- 最理想做法是直接將所需字段拷貝進調試塊本體,保證結構獨立性;
- 此改動能根本解決事件失效導致的調試數據損壞問題;
- 目前代碼雖然部分邏輯正確,但仍存在可優化之處,值得進一步修正。
調試器中檢查 Events
與 StoredEvent
我們現在繼續檢查這些調試事件,雖然其他部分可能仍然存在問題,但有一點已經可以確認是明顯錯誤的。
當前觀察重點:
我們查看了一個調試事件,屬于打開狀態(open event),在調試顯示中是綠色的。然而發現了一個異常值:
- 該事件的 GUID 為 0(Good 為 0)
這是一個明顯不對的現象:
- 在正常情況下,每個調試事件應當有唯一的標識符(GUID),用于匹配開始與結束事件、建立事件層級關系等;
- GUID 為 0 意味著它沒有正確初始化,或者數據在創建或賦值時被忽略或清空了;
- 一個事件如果是“打開”的,意味著它是某種邏輯塊的起始點,更應該擁有合法的標識符;
- 出現 GUID 為 0 的現象說明該事件不是一個有效的調試結構的一部分,或者已經破壞了調試系統的基本一致性。
進一步的思路:
目前這個問題可能來自:
- 事件創建時未正確生成或賦值 GUID;
- 事件結構被拷貝或傳遞過程中發生值丟失;
- 事件指針引用了臨時或無效的對象,例如之前提到的“臨時事件生命周期過短”的問題;
- 調試系統中用于設置 GUID 的邏輯分支未命中,即創建路徑異常。
下一步建議:
為了定位和解決問題,我們應當:
- 回到事件分配和初始化的代碼路徑,檢查 GUID 是如何生成和賦值的;
- 對調試塊中的所有開放事件做一致性檢查,確保所有“打開塊”都有有效 GUID;
- 在調試視圖中增加斷言,強制所有開放事件必須擁有非零 GUID;
- 若發現使用的是臨時事件副本,考慮使用持久版本或將 GUID 顯式復制進去。
總結要點(中文):
- 當前看到的調試事件是一個“打開狀態”事件;
- 它的 GUID 為 0,這是不正確的,說明事件結構不完整或創建邏輯出錯;
- GUID 是建立事件關聯關系的核心字段,必須保證存在且正確;
- 問題可能來自事件生命周期混亂、賦值遺漏或使用了臨時無效數據;
- 需要回溯事件生成與賦值流程,并加入一致性斷言以避免此類錯誤再次發生。
修改 game_debug.h
:讓 open_debug_block
不再保存整個 debug_event
,而只保存所需數據
當前我們識別出一個關鍵性設計錯誤,并對調試系統進行了徹底的結構調整,旨在修復調試塊匹配邏輯中的一系列隱患與冗余處理。
問題核心
我們原先在調試塊(debug block)中直接存儲了“打開事件”的引用(open event),但這是錯誤的。原因如下:
- “打開事件”只是一個臨時對象,在事件解析結束后即會被釋放;
- 調試系統中的塊結構不應該引用一個生命周期不穩定的對象;
- 如果之后需要訪問事件中關鍵數據,如
clock
或thread_id
,就會發生懸空引用或數據不一致。
正確做法與修改內容
我們已經對系統做了以下調整:
-
不再存儲事件本體指針:
- 原來的設計中,open debug block 存儲了一個指向 open event 的引用;
- 現在直接將我們需要的數據從事件中提取出來并存儲,如
begin_clock
。
-
簡化匹配邏輯:
- 原先有一個復雜的
events_match
判斷邏輯,嘗試通過比較事件結構判斷配對; - 實際上,我們只需要比較線程 ID 是否一致;
- 現在將其簡化為一個斷言(assert):
thread_id
必須匹配。
- 原先有一個復雜的
-
移除冗余結構:
- 清理了用于事件匹配的一些無用字段和函數;
- 對
open_event.clock
等成員引用全部取消,替換為matching_block.begin_clock
等更穩定的字段; - 去掉了一些重復的
events_match
判定邏輯。
-
在 End Block 階段立即處理需要的數據:
- 事件的任何重要信息都必須在
end_block
執行時立即提取和存儲; - 不允許后續再依賴
open_event
,因為它已無效。
- 事件的任何重要信息都必須在
整體目標與收益
- 保證調試系統對調試塊的引用都是長期有效的;
- 消除因事件生命周期失效導致的崩潰或未定義行為;
- 提高代碼清晰度與可維護性;
- 簡化事件配對邏輯,避免誤判、遺漏或跨線程匹配。
總結重點(中文)
- 原來的做法錯誤地在調試塊中存儲了臨時事件指針;
- 正確做法是提取所需字段(如
clock
和thread_id
)直接存儲; - 配對邏輯現在只依賴線程 ID,已改為斷言;
- 所有引用事件數據的場合都改為使用
matching_block
中穩定的數據; events_match
邏輯和相關引用字段已移除,調試系統更加簡潔安全。
運行游戲,發現分析器數據依然會丟失
目前我們已經進入一個更穩定的狀態,之前一些不正確的代碼邏輯已經被清理掉,系統整體運行狀態顯著改善。以下是當前工作的總結:
當前狀態分析
- 系統現在看起來運行正常,核心調試數據結構的管理更加清晰;
- 嘗試切換回“世界模式”(world mode)后,仍然丟失了性能剖析數據(profile);
- 盡管如此,這次已經排除了之前存在的冗余邏輯與無效引用問題;
- 系統內部的一些“垃圾邏輯”已被移除,那些代碼原本就是不正確的,現在清理干凈之后,后續調試將更加高效可靠。
已完成的清理工作價值
- 修復了調試事件生命周期管理錯誤;
- 避免了使用臨時對象指針造成的數據失效與潛在崩潰;
- 清除不必要的匹配函數與冗余邏輯判斷;
- 優化了調試塊的數據結構,提升可維護性與穩定性。
接下來的計劃
- 當前仍存在剖析數據在切換模式后丟失的問題;
- 這個問題將留待下一輪排查,預計可以較快解決;
- 重點將放在“world mode”切換邏輯與 profile 數據同步機制上;
- 當前清理工作為后續排查打下了干凈的基礎,避免了被舊邏輯干擾。
總結重點(中文)
- 系統運行狀態穩定性明顯提升;
- 原有調試邏輯中的不當引用已徹底清理;
- 剖析數據丟失的問題仍未解決,但問題范圍已大大縮小;
- 下一步將重點排查模式切換與 profile 數據之間的關系;
- 當前階段的清理工作極為必要,提升了系統整體正確性與可調試性。
如需繼續整理明天的調試排查思路,也可以為此提前準備一個定位路徑。
Q&A
是否可以將調試 UI 拆分為獨立程序,通過管道或套接字與游戲通信?這樣調試數據不會占用游戲資源,游戲崩潰時依然能檢查幀數據或保存
這是一個非常合理的想法,我們完全可以將調試 GUI 獨立出來,變成一個與游戲程序通過管道(pipes)或套接字(sockets)通信的獨立進程。這種架構設計可以帶來多方面的優勢:
🌐 架構分離的優點
-
避免調試開銷影響游戲性能
游戲僅負責采集和發送調試數據,而不再進行調試可視化渲染,減少性能開銷,尤其是在性能敏感的場景下尤為重要。 -
提升穩定性
如果游戲崩潰,調試界面依然可以運行,便于分析崩潰前的最后幾幀狀態,甚至可以將調試幀獨立保存到磁盤中進行離線分析。 -
實現遠程調試
架構分離后,可以在另一臺機器上運行調試 GUI,這對于主機平臺(如 PlayStation、Xbox 等)開發尤其重要。例如在主機上運行游戲,而在 PC 上查看調試信息,是常見需求。 -
便于商業化與工具鏈拓展
如果將來希望將調試系統打包成通用工具,分離式架構是專業工具的基礎,有利于模塊化、標準化。
當前實現架構的適配性
- 當前系統設計采用寫入共享緩沖區的方式,已經具備一定的解耦結構;
- 將寫入行為替換為通過網絡或 IPC(進程間通信)發送數據不會造成結構上的劇烈變更;
- 唯一需要額外注意的部分是“事件相關性處理”(correlation 部分),需要小心同步和順序的問題;
- 只要將這部分邏輯提取到獨立程序中,通信協議定義清晰,即可實現完整功能遷移。
實現展望
可以考慮:
- 實現一個監聽套接字的調試服務端程序,接收來自游戲的調試數據;
- 游戲只負責發包,并盡可能不參與調試數據的解析或顯示;
- 使用自定義協議(或輕量級序列化格式)傳輸數據;
- 支持緩存、斷線重連、調試幀回放等功能,增強調試效率與健壯性。
總結
將調試界面獨立為一個與游戲通信的程序,確實是一個非常有價值且專業的架構優化方向,不但提升穩定性、性能,也為后續遠程調試、跨平臺開發、甚至商業化工具化鋪平了道路。目前架構已經有基礎,如果需要擴展到這一方向,實現成本也較低。
你在 GAME_INTERNAL
宏外部引用了 GlobalDebugTable
,而它是在 UpdateAndRender
中初始化的
在這個階段,擔心將全局調試表暴露到外部并沒有使用適當的內部保護機制,可能會導致很多潛在的問題。全局調試表如果沒有適當的控制和保護,就有可能被外部意外修改或誤用,導致調試信息的不一致或者錯誤的調試結果。
主要問題:
-
外部暴露調試數據
如果沒有防護機制,調試表可能會被外部代碼修改,導致程序的調試過程受到干擾。 -
潛在的不一致性
如果調試表被不適當地修改,可能會導致不同模塊之間的數據不一致,進而影響調試結果的準確性。
解決策略:
-
啟用內部保護機制
必須確保調試表只在受控的環境下訪問和修改。可以通過引入“內部守護程序”來防止外部直接修改調試數據。 -
清理冗余數據
等到調試功能關閉或不再使用時,才對調試表進行徹底清理,避免冗余數據的累積,從而影響程序性能或導致調試信息混亂。
通過這種方法,可以確保調試數據的安全性和準確性,并防止潛在的問題影響調試過程。
你覺得模塊化編輯器(modular editing)目前體驗如何?
目前,處理對象和編輯的速度還是比較慢的。原因是還需要做一些工作來簡化清理過程,尤其是避免頻繁切換不同的模式。只要完成這些工作,預計會變得更快,甚至可能比之前還要高效。
關于全局調試表的討論,問題出在初始化時,它在更新和渲染函數中被使用,而這個過程沒有適當地被內部保護機制限制。具體來說,調試表的初始化和使用沒有進行適當的保護,導致它在外部可能會被修改,從而影響程序的穩定性和數據的完整性。
為什么不用 DebugBreak
替代 *(int*)0 = 0
,這樣可以在需要時跳過斷點同時也能觸發調試器
我們討論了一種名為“cubicle”的工具,它使用了一種深度插入(deep insertion)機制,但我們選擇打破這種機制,改用“step over”方式。這種方式允許我們在表面上操作,而無需插入。原因在于,我們考慮到了Zibo橋梁和平臺的需求。相比之下,大多數西方平臺需要不同的處理方式,因為這些平臺通常只在Windows系統上運行。對于Unix系統,雖然存在一個等效的工具,但其功能和操作方式與Windows版本有所不同。
什么是模塊化編輯?
這是關于“模態編輯”的討論。模態編輯是指在不同的編輯模式下,鍵盤輸入的行為會有所不同。在一種模式下,比如綠色模式,鍵入的內容是普通的文本輸入,而在另一種模式下,如紅色模式,所有的輸入實際上都會被視為命令。在這種模式下,不需要按住控制鍵(Control)或其他任何鍵來觸發命令,所有的輸入都是基于當前模式的行為。
你寫自己的渲染器時是用左手坐標系還是右手坐標系?為什么?
在編寫自己的渲染代碼時,通常使用右手坐標系。因為右手坐標系是數學領域中最常用的標準,所以沒有特別的理由去選擇左手坐標系。
你禁用樹渲染后注意到調整分析器窗口大小的白點在淺色背景上很難看清,就像加陰影前的文字一樣
注意到當禁用樹的渲染時,白點在淺色背景上變得難以看清,類似于文本之前難以閱讀的情況,直到添加了陰影才有所改善。確實,如果進入美化階段,應該做一些調整。首先,應該讓這個點一開始就變得更大。另外,還要確保它的大小合適。現在它還沒有做出這種調整。我們并沒有特別處理這個點的繪制方式,因此需要明確一個問題:應該同時展示多少個配置文件?我覺得我們可能希望顯示更多的配置文件。
你覺得邊講解邊編碼對你理解或編程本身有幫助嗎?
在編程過程中,注釋主要是為了記錄一些以后可能會忘記的東西。通常,注釋并不是幫助思考的工具,而是為了確保不會遺忘某些細節,特別是在處理復雜代碼或長時間不再涉及某些部分時。
題外話:我注意到 #pragma pack
會改變結構體對齊方式,有辦法保留原始對齊方式嗎?
#pragma pack
的作用是改變結構體的對齊方式,它的目的是控制結構體成員的內存對齊,以節省內存空間。默認情況下,結構體成員的對齊方式通常會根據最大類型的對齊要求來決定。使用 #pragma pack
可以調整這個對齊方式,從而減少內存的浪費。然而,一旦使用了 #pragma pack
,結構體的內存布局會被壓縮,這也可能導致性能問題,因為不同平臺的 CPU 在訪問不對齊的內存時可能會變慢。
如果想要保持原有的對齊方式,通常不使用 #pragma pack
,而是依照默認的對齊規則。如果已經應用了 #pragma pack
,想恢復原來的對齊方式,可以通過 #pragma pack(pop)
來恢復到之前的對齊設置。
是否能保存 DebugUI 的布局,讓撕開的窗口在下次打開時仍保持上次狀態?
在討論游戲調試時,考慮到是否需要保存調試視圖的布局以及當前打開的窗口,盡管這樣的功能是有用的,但在當前的情況下,可能并不會特別去實現。相反,可能會選擇在代碼中實現這種功能,通過編程的方式來創建額外的視圖。這樣可以靈活控制調試信息的顯示,而不需要每次都保存和恢復調試狀態。
如果這是一個商業系統,尤其是當調試系統作為獨立的組件進行發布時,保存和恢復調試視圖的狀態就變得非常重要。在這種情況下,肯定會加入這種功能,以確保開發人員和用戶能夠方便地恢復到之前的調試環境。因此,是否實現這個功能取決于當前的項目需求和目標。