game_debug.cpp: 將ProfileGraph的尺寸初始化為相對較大的值
今天的討論主要圍繞性能分析器(Profiler)以及如何改進它的可用性展開。當前性能分析器已經能夠正常工作,但我們希望通過一些改進,使其更易于使用,特別是在調試和分析過程中。
首先,我們希望能夠暫停性能分析器的更新,以便可以查看特定的分析數據,而不會被每一幀的更新所打擾。這是因為當前性能分析器會每幀更新,導致在某些情況下,信息的呈現可能會變得不連貫。因此,我們需要一個暫停功能,讓我們能夠在某一時刻停下來,查看特定的數據,而不被不斷變化的內容影響。
其次,我們希望能夠引入更多的視圖功能。目前,性能分析器缺少一個重要的視圖——它無法顯示某一幀在特定例程中花費的總時間。我們已經能夠展示線程利用率視圖,但沒有做任何時間聚合操作,也就是沒有統計各個例程的執行時間并顯示出來。因此,增加這種聚合視圖將使我們能更全面地了解程序的性能表現。
為了改善當前的狀態,首先需要對初始化界面做一些改進。目前,界面的寬度默認被初始化為零,這顯然是不合理的。我們希望修改這一點,將界面的寬度設置為合理的初始值,并使其在大多數情況下能夠充滿整個屏幕。
在實現這一改動時,具體的做法是通過檢查某些條件(比如圖形的維度是否為零)來確定是否是第一次初始化圖形。如果是第一次初始化,我們就調整圖形的尺寸,使其更大,確保能夠完整地展示性能數據。
此外,還提到了一些小的代碼改進,比如將重復的代碼提取到一個單獨的函數中,這樣可以避免每次都手動輸入相同的代碼,從而提高代碼的可維護性和簡潔性。
總結來說,今天的主要任務是通過增加暫停功能、引入聚合視圖以及調整界面初始化的尺寸,使得性能分析器變得更加易用和高效。這些改進將幫助更好地分析和理解程序的性能瓶頸,從而優化游戲的運行效果。
運行游戲,查看更為實用的默認尺寸并為今天的工作做準備
目前,已經對性能分析器進行了一些改進,目的是使其更易于使用。當打開性能分析器時,現在默認的窗口大小已經更加合理,能夠更好地展示數據。這是相較于之前默認窗口過小的改進,至少現在打開時,界面看起來更加合適,使用起來更方便。
然而,仍然有一些功能尚未完善。首先,分析器界面目前缺少一些基本的按鈕和控件。希望能夠加入一些按鈕,用來切換視圖或調整顯示樣式等,這樣可以提升分析器的交互性和靈活性。此外,目前分析器雖然支持深入分析某些數據(即“鉆取”功能),但仍然缺乏一個返回按鈕,無法返回上一層視圖,這也會影響使用體驗。
另外,提到的一項改進建議是能夠查看每一幀的更多信息,比如每一幀的堆棧數據。這種“堆棧每幀”功能能夠幫助更好地理解每幀的執行情況,并且通過展示“時間條”這種形式的圖標,可以讓用戶直觀地看到不同操作在時間上的分布。這樣的視圖能夠更清晰地呈現出程序執行的每一部分,尤其是對于調試性能瓶頸非常有幫助。
總的來說,當前的改進已經讓性能分析器的基本功能更加實用,但為了更好地滿足需求,還需要加入更多的交互功能和視圖模式,比如增加切換視圖的按鈕、添加返回功能以及展示更詳細的每幀數據等。
game_debug.cpp: 引入DrawFrameBars,繪制多個幀的堆疊條形圖
我們正在嘗試實現一種新的分析視圖形式,稱為“幀條形圖(Frame Bars)”,這個視圖的目標是展示多幀數據,而不是僅僅關注單一幀的性能數據。
當前的實現通過水平條形圖來表示每一幀的執行數據,但新的想法是通過垂直條形圖來展示多個幀的執行數據。具體來說,我們將為每個事件繪制多個幀的數據,而不僅僅是選擇最新的一幀數據。為了實現這一點,我們考慮為每個事件提供多個幀的歷史數據,并通過垂直的條形圖逐幀展示每個事件在時間上的變化。
為了完成這個目標,我們設計了一個新的draw_frame_bars
函數。這個函數與現有的繪制函數類似,但有幾個關鍵區別:
-
多幀支持:我們不再局限于顯示最新的一幀數據,而是支持選擇并顯示多個幀的數據。這意味著,我們需要遍歷多個幀的事件數據并將其繪制出來。
-
每個事件的多個數據:在繪制時,我們將遍歷所有的存儲事件,并將這些事件的數據以條形圖的形式展示出來。每個條形圖的高度代表幀的某一部分數據,寬度則表示每一幀的時間跨度。
-
條形圖繪制方式的變化:我們從水平條形圖改為垂直條形圖,這樣可以更好地展示多個幀的數據。每個條形圖的寬度和高度將根據幀數和時間跨度來確定。
-
線程數據處理:對于每個事件,我們只考慮與其相關的線程數據,并確保每個線程的數據都位于同一條“顯示軌道”上。如果某些事件涉及多個線程,我們只會選擇顯示一個線程的數據顯示。這個過程中,我們將跳過無關的線程數據。
-
繪制方式的調整:我們使用
root
節點來獲取事件的基本信息,并計算每個事件的顯示跨度。然后,我們根據事件的時間跨度來確定條形圖的顯示位置和大小。通過這種方式,每一幀的多個數據都能被正確地繪制出來,確保每一幀的執行信息都有清晰的可視化展示。
目前的目標是通過這種方式,能夠讓開發者在分析器中看到多個幀的事件數據,并且為每一幀的執行情況提供更詳細的視圖。這項改進還沒有完全實現,但它展示了如何通過新的方式來展示和分析每一幀的數據。
game_debug.cpp: 調用DrawFrameBars而不是DrawProfileIn
我們在實現“幀條形圖”的過程中遇到了一些問題并進行了一些調整和反思。
首先,在一開始的實現中,我們將根節點設為了幀的根節點,但這個根節點實際包含了多個線程的數據。這導致在繪制過程中,不同線程的數據可能會互相重疊繪制在同一個區域上。也就是說,雖然這些數據可以被繪制出來,但由于它們重疊在一起,實際效果并不好,顯得混亂、不清晰。
初步運行時,繪制的元素數量似乎異常龐大,畫面被填滿了大量圖形,這顯然不是預期的行為。根據現有邏輯,在遍歷事件數據時,是通過從每幀的根事件開始,進入它的第一個子事件,然后通過“兄弟指針”遍歷每一個同級的子事件,這樣一來,在多幀數據的循環中,實際上是繪制了過多重復或不必要的圖形。
出現這個問題的可能原因包括:
-
沒有限制繪制幀的數量:當前代碼沒有明確指定繪制多少幀,導致可能遍歷了所有可用的幀,最終繪制出數量龐大的條形圖。
-
線程混合:因為根節點包含多個線程的數據,所以不同線程的數據沒有被分離,導致條形圖重疊。
-
缺少幀索引控制:我們還沒有添加幀索引相關的邏輯,例如指定繪制從哪一幀開始、繪制多少幀等,因此目前的繪制范圍完全依賴事件鏈的數量,而不是明確的幀控制。
接下來需要進行的調整包括:
- 添加明確的幀索引控制邏輯,例如:起始幀索引、最大繪制幀數等,從而限制繪制的數據范圍,避免繪制過多內容。
- 篩選出特定線程的數據,僅繪制與目標線程相關的事件,避免線程混合導致的重疊問題。
- 檢查事件遍歷邏輯,確保在多幀數據繪制時不會重復繪制或漏掉任何事件,同時避免無效事件導致圖形雜亂。
總之,這一部分的工作核心在于從當前的多線程混合與無控制繪制,逐步過渡到清晰、有組織、可控的逐幀多線程繪圖模式。通過對繪圖范圍、線程隔離、數據遍歷路徑的規范化,我們將能夠構建一個更具實用性和清晰度的性能分析視圖。
看上去寬高搞反了
game_debug.cpp: 限制FrameIndex
我們正在構建一個逐幀分析的可視化系統,為了確保條形圖的正確性和結構清晰,需要做幾個關鍵調整:
-
確保根事件存在:繪圖之前需要驗證每一幀是否存在有效的根事件。因為根事件是構建分析圖表的基礎,它代表了每一幀的入口節點。如果沒有它,后續的數據結構遍歷和繪制將無法進行。
-
記錄幀索引信息:在繪圖過程中,我們需要保留每一幀的幀索引。這樣做的目的是在調試時能夠明確知道當前繪制的是哪一幀的數據,同時也便于在多幀數據中進行定位、回退、或跳轉等操作。
-
限制繪圖數量:為避免一次性繪制大量幀而導致界面混亂或性能下降,當前設置為只繪制一幀,作為調試階段的簡化處理方式。之后可以根據需要擴展為多幀繪制。
-
統一事件位置:事件的索引、賬戶信息以及路由訪問路徑都要統一放置在相同的繪圖位置區域中,這樣有助于分析每一幀內具體事件在時序上的相對位置與調用結構。
整體目標是構建一個結構清晰、繪制精簡的幀分析視圖,使得在每幀中發生了哪些函數調用、花了多少時間等信息一目了然,并能夠快速進行調試和問題定位。當前階段以最小繪制單位(一幀)進行測試,為后續擴展到多幀堆疊、線程分層等打下基礎。
game_debug.cpp: 將FrameCount增加到10
我們正在嘗試將繪制的幀數量從 1 增加到 10,以便測試多幀數據顯示的可行性,但出現了一些問題,繪制結果不符合預期。
原本希望看到的是同一結構的重復條形圖(每幀一個),但現在的情況卻是圖形變得“有深度”,顯示成了嵌套的調用關系圖,這不是想要的效果。
經過分析,出現問題的原因包括以下幾個方面:
問題一:傳遞的事件類型不正確
目前傳入 draw_frame_bars
的事件并不合適,傳入的是某個根節點,而不是一個具有可遍歷幀序列的事件數據。
實際上,我們需要的是某種“最早幀”的起始事件,也就是說,要找到當前所能訪問的最舊的幀數據作為起點,這樣才能通過其 next
指針遍歷多個連續幀的數據。
問題二:數據結構設計缺陷
事件的組織方式導致我們無法輕松訪問多幀數據。理想情況是:
- 每一幀本身就是一個事件(或者幀事件),
- 每一幀的事件都通過
next
指針鏈接成鏈表, - 這樣就能很自然地遍歷多個幀。
但當前的實現方式中,每一幀的根節點是“合成”的,并未直接鏈接起來,導致不能通過 next
指針方便地進行多幀遍歷,從而使得繪制多幀條形圖的邏輯變得繁瑣甚至不可行。
當前解決嘗試
當前嘗試通過手動找到“最舊的一幀”并提取其根節點信息,再以此為基礎繪制接下來的多幀數據。但是,由于根節點之間缺乏鏈式結構,因此這種方式本質上還是依賴了一些“hack”方法,代碼顯得不夠健壯、清晰。
下一步建議與優化方向
- 優化數據結構設計,使每一幀具備事件身份且能夠通過鏈表方式連接;
- 在分析系統中引入一個統一的幀容器結構,便于統一訪問每幀數據;
- 更新繪制邏輯,確保傳入的對象能夠直接進行幀間遍歷;
- 進一步檢查當前事件系統中“synthetic nodes”的設計,看是否需要重新構造以支持多幀視圖。
總體來說,我們已經非常接近多幀條形圖的目標,只差對事件結構的重構與適配,接下來的工作將圍繞這方面展開。
點擊進去
為了方便測試多加一些函數進去
game_debug.cpp: 將RootNode設置為OldestEvent
我們當前正在進一步優化多幀繪制邏輯,目的是讓時間軸上的每一幀都能被準確地以垂直條形圖的形式展示,清晰顯示每個函數或任務在每一幀所消耗的時間,形成一種隨時間推移的可視化分析視圖。
當前處理思路
- 在代碼中獲取了當前正在查看的可視元素(viewing element);
- 然后不再進行復雜的搜索,而是直接將“最早的事件”設為繪圖的根節點;
- 使用這個最早事件作為起點,從而遍歷多個連續幀來繪制數據。
當前結果
- 現在,當選擇了一個具體的事件(如 debug collation)之后,可以看到它在后續每一幀中所占用的時間;
- 每幀的條形圖繪制成功地排列在水平方向,每條垂直柱狀代表一幀中該事件的持續時間;
- 如果繼續繪制直到空間不足(剩余空間為 0),圖形將向左滾動,展現后續幀。
優點
- 能夠直觀比較某一操作在不同幀之間的性能波動;
- 實現了從單幀分析向時間序列分析的轉變,數據的可視化維度更強;
- 避免了原先因事件未正確串聯而無法跨幀遍歷的問題。
存在問題
- 當前“正確”只是相對的:雖然繪圖看起來有效,但還存在潛在邏輯漏洞;
- 條形圖位置和比例仍可能存在不精確或邊界問題;
- 滾動邏輯尚未完全實現,僅在空間耗盡時表現為“自動移動”;
- 某些幀中事件信息可能丟失或未對齊,需要進一步調試。
下一步優化方向
- 確保事件遍歷邏輯嚴謹,杜絕非法幀或無效事件參與繪圖;
- 優化坐標系邏輯,確保條形圖尺寸與幀時間一一對應;
- 增加幀時間軸標尺或幀編號提示,提升可讀性;
- 進一步完善幀間滾動和縮放機制,實現可交互的歷史性能回放視圖。
通過這一步改進,我們已成功邁出了將性能分析工具從靜態走向動態、從單點走向時間線的關鍵一步。接下來將繼續在結構優化與交互細節方面打磨,使多幀分析功能更加實用與直觀。
game_debug.cpp: 將FrameCount增加到128
我們現在進一步拓展條形圖的幀數展示范圍,目標是一次性查看更多幀的數據變化趨勢,以便更好地觀察性能波動和行為模式。
主要調整內容
- 將條形圖數量設置為 128,也就是一次性展示最近的 128 幀;
- 修改繪圖參數,使其能處理更多幀數據的渲染;
- 每一幀繪制為一條垂直條形圖,用于展示某個特定事件在該幀中消耗的時間;
- 所有條形圖按時間順序排列,從左至右,構成時間軸上的柱狀分布。
預期效果
- 在畫面上同時呈現 128 個連續幀的性能數據;
- 可以直觀地看到某個事件隨時間的耗時變化,例如某個階段耗時突然增加;
- 提供歷史性能趨勢回顧,有助于發現突發性能瓶頸或優化點;
- 條形圖對齊整齊,形成清晰的數據對比畫面;
- 隨著后續幀的加入,畫面會逐漸填滿,最終達到最大顯示幀數,形成完整的視覺分析窗口。
當前實現狀態
- 調整后能夠渲染出指定數量的條形圖;
- 每個條形圖代表一幀數據,顏色和高度分別對應事件的種類與耗時;
- 時間軸完整拉伸,覆蓋了設定的 128 幀寬度;
- 若繪圖區空間足夠,將完整顯示;若不足,后續可加入滑動或壓縮機制。
后續優化方向
- 增加幀編號或時間刻度軸,提升數據可讀性;
- 實現縮放功能,使用戶能在不同粒度上觀察數據(例如:16、32、64、128幀切換);
- 增加交互功能,如鼠標懸停顯示每幀的詳細耗時數據;
- 自動居中或跟隨最新幀更新視圖,使數據瀏覽更加順暢;
- 添加篩選功能,讓用戶選擇特定線程或事件查看幀數據。
通過此次擴展,我們已經將視圖從“當前幀”拓展到了“多幀歷史”,為分析者提供了更強大的工具,幫助快速定位性能趨勢、穩定性問題或潛在瓶頸。未來結合交互與可縮放機制,這一視圖將成為關鍵的分析入口之一。
運行游戲,查看更精細的時間數據
我們現在進一步思考如何更有效地支持多幀條形圖的渲染,從而實現更細粒度的性能可視化。當前的實現雖然已經初步可以展示多個幀的數據,但存在一些結構性的問題,尤其是在事件鏈表與存儲方式方面。我們希望改進整體架構,以提升渲染效率、數據訪問便捷性和結構清晰度。
當前問題
- 目前的事件是通過鏈表串聯的,邏輯上比較復雜;
- 多幀事件的遍歷依賴動態鏈式結構,在繪制條形圖時不夠高效;
- 每個幀的數據事件不能快速定位,難以在幀維度上進行精準的數據提取;
- 多次出現的事件在同一幀中沒有明確組織,導致無法高效索引和繪制。
優化設想
1. 分配固定大小的后備存儲塊
- 為每個調試元素預留一個固定大小的事件數組(例如 128 幀或 256 幀);
- 避免動態鏈表構建和銷毀,改為直接覆蓋式環形緩存;
- 固定空間有助于維護內存一致性與訪問效率。
2. 引入幀索引表(Frame Index Table)
- 為每個調試元素維護一個幀索引表,對應最近若干幀(例如 128 幀);
- 每一項指向該調試元素在該幀發生的首個事件;
- 通過這個表,可以快速根據幀編號定位到該幀的事件列表;
- 保留事件之間的鏈式結構,用于同幀多個事件的串聯。
3. 快速幀數據訪問與繪制
- 繪制條形圖時,根據幀索引表快速查找到對應幀的事件;
- 對事件鏈進行迭代,收集當前幀所有發生的事件;
- 渲染時使用固定寬度和縱向布局,實現連續幀條形圖可視化;
- 這樣可以快速橫向滑動查看歷史幀趨勢,同時避免復雜的嵌套結構遍歷。
4. 支持更強的幀間對比能力
- 由于結構扁平化,每幀繪制邏輯簡潔明了;
- 能夠輕松對比多個幀中相同事件的耗時變化;
- 可后續添加交互(如懸停信息、點擊詳情等)進行深入分析。
結構總結
- 每個調試元素對應一個事件環形緩存 + 一個幀索引表;
- 每個幀索引表項指向該幀第一個事件,后續事件通過
next
指針鏈表訪問; - 內存結構清晰:固定容量、常數訪問時間、易于調試與擴展;
- 渲染邏輯簡化,僅遍歷幀索引表與對應事件鏈。
通過這種方式,我們不僅提升了性能數據的展示效果,也大大簡化了底層數據結構,提高了渲染效率和維護性。今后可以在此基礎上進一步支持線程切換、過濾篩選、時間縮放等高級調試功能。
game_debug.h: 移除debug_frame_region和MAX_REGIONS_PER_FRAME
我們在思考如何更合理地傳遞和管理存儲事件(storied event)時,開始重新梳理條形圖繪制邏輯。在繪制幀條形圖的過程中,我們目前遍歷的是每一幀對應的事件集,而接下來希望實現更系統的方式來組織和遍歷這些幀數據。
當前結構分析
- 當前的調試元素(debug element)中維護了事件的頭尾指針(head 和 tail),這些事件鏈表包含了該元素從過去到現在所有發生過的事件;
- 由于這些事件是永久累積的,在可視化時會導致數據龐大、不具時效性、不易管理;
- 此外,原本框架中還保留了一些
frame
的結構,但部分內容混亂或冗余,如MAX_REGIONS_PER_FRAME
等字段已經不再有實際用途,需要清理。
計劃優化方向
1. 限定幀的數量進行管理
- 將調試元素中維護的事件集合限制在固定幀范圍內(如最近的 128 幀),采用環形結構;
- 每個幀在存儲中擁有獨立的數據區域,便于迭代與訪問;
- 移除歷史無限累積的事件鏈,改為定量維護。
2. 使用調試元素中的幀滑動訪問
- 在繪制幀條形圖時,不再從所有歷史事件中篩選;
- 直接按順序訪問調試元素所持有的幀集合,遍歷每幀中存儲的事件集合進行繪制;
- 可通過一個循環變量模擬“滑動窗口”,在固定長度緩存中向前或向后滑動以獲取數據。
3. 清理無效數據結構
- 移除不再使用的舊
frames
結構,例如廢棄的MAX_REGIONS_PER_FRAME
字段; - 精簡框架內數據定義,確保每一項結構都與實際渲染邏輯緊密對應。
渲染行為預期
- 渲染時,從當前幀開始向后(或向前)依次繪制已記錄的若干幀;
- 每幀對應一組條形圖,顯示該幀內調試元素事件的耗時情況;
- 當空間不足時自動橫向滾動,形成流暢的幀時間線視覺反饋;
- 后續可引入交互方式,如拖動、縮放、幀跳轉等,增強分析能力。
通過這一調整,我們能夠實現更高效、清晰且具可控性的幀數據可視化,不再依賴雜亂的事件鏈表,也便于未來支持更大規模的調試分析。同時框架數據結構將更簡潔、更聚焦于實際可視化需求。
game_debug.h: 引入debug_element_frame,以便我們能夠跟蹤和計時事件
我們正在重構幀與調試元素之間的事件訪問機制,目標是實現更直接、快速的數據定位與分析,避免冗余的樹結構遍歷,提高幀數據可視化與調試效率。
目標與問題概述
我們面臨的主要問題是:當前幀(frame)結構本身無法直接提供我們希望訪問的事件信息,必須通過遍歷根節點構建的分析樹才能定位特定調試元素的事件數據,這既低效又復雜。我們真正需要的,是:
- 拿到一個幀索引(frame index)
- 拿到一個調試元素(debug element)
- 直接定位該元素在該幀上的所有事件
這才是理想的訪問模式。
數據結構改進方案
1. 引入 DebugElementFrame 結構
設計一種新的結構 DebugElementFrame
,作為調試元素在單幀上的事件集合,具備以下特性:
- 存儲該元素在該幀上發生的所有事件;
- 累計耗時統計(例如總 clock 數),便于分析每幀的時間消耗;
- 可以用于后續的排序或篩選,例如找出耗時最多的元素。
每個調試元素將維護一個長度固定的數組(如 128 或 256)的 DebugElementFrame
實例:
DebugElementFrame frames[MAX_FRAME_BACKLOG];
2. 使用固定長度循環數組管理幀數據
為了避免復雜的鏈表和動態內存操作,采用環形緩沖區結構:
- 總幀數固定(如 128);
- 有一個全局的總幀索引
totalFrameIndex
; - 每個調試元素內部用幀序號
frameOrdinal = totalFrameIndex % MAX_FRAME_BACKLOG
來定位在數組中的具體幀; - 不再維護
next
、free
等指針鏈表結構,直接訪問數組即可。
這種方式能在空間可控的前提下保持極高的訪問效率,并省去所有內存釋放與管理的開銷。
訪問邏輯與繪制優化
在繪制幀圖(Frame Bars)時:
- 使用當前調試元素;
- 根據幀索引直接訪問
DebugElementFrame
; - 獲取其事件集合與耗時;
- 渲染條形圖,無需再從根節點遍歷整個事件樹;
- 實現幀之間滑動查看(如向前或向后 128 幀的歷史數據)。
額外優勢
- 便于調試與數據對比:在幀之間輕松對比同一元素的耗時波動;
- 可以輕松添加排序功能:例如顯示當前幀耗時排名前十的調試元素;
- 數據結構更緊湊,維護與擴展更加方便;
- 移除了復雜的 free list 和 event 鏈表邏輯,清晰簡潔。
后續注意點
- 總幀索引
totalFrameIndex
是不斷遞增的,而frameOrdinal
是它在緩沖區中的映射(會循環); - 所有調試相關的幀數據必須基于這一循環數組維護;
- 若幀數超過緩存長度,則最舊幀的數據將被覆蓋(這是設計預期);
- 初始渲染時要處理好未填滿環形緩沖區的情況。
通過這一方案,我們可建立一個高效、清晰且便于維護的幀事件訪問系統,完全擺脫樹狀遍歷帶來的性能與邏輯復雜度,使調試系統更接近理想狀態。
game_debug.h: 引入FrameOrdinal和MostRecentFrameOrdinal,并使game_debug.cpp中的函數使用它們
我們正在對調試系統中的幀數據結構和訪問方式進行進一步清理和重構,以提高效率、簡化邏輯,并為后續的可視化交互(如時間軸回溯)提供良好基礎。
核心改動與設計意圖
-
用 frame ordinal 替代 frame count 邏輯
我們不再使用傳統的幀計數方式,而是引入了一個“幀序號”(frame ordinal),它表示的是在環形緩沖區中的下一個可用幀的位置。這個值從 0 開始,隨著每一幀的推進遞增,并在達到最大幀緩沖數量(如 128 或 256)時循環。
- 當前幀插入位置為
nextFreeFrame
- 最近繪制的幀為
nextFreeFrame - 1
(循環處理) - 如果要回看之前的幀,向后偏移相應的 ordinal 即可
- 當前幀插入位置為
-
將繪圖邏輯基于 frame ordinal 重寫
在進行調試元素繪制時:
- 引入
frameOrdinal
參數,明確要繪制哪一幀; - 在繪圖函數中不再從整個事件鏈查找,而是根據指定 ordinal,直接訪問存儲于調試元素內的事件幀數組;
- 簡化了
debug_draw_elements
等函數的輸入,改為通過debugState
獲取當前幀序號; - 添加了
MostRecentFrameOrdinal
統一用于查找“上一個完成的幀”,避免重復邏輯和混淆。
- 引入
-
繪圖交互支持幀回溯與時間軸滑動
- 隨著結構清晰化,可在 UI 上添加“滑動條”或“時間軸”控件,支持用戶瀏覽過去幀的數據;
- 通過
frameOrdinal
參數,輕松實現任意幀的數據展示; - 未來可以添加幀耗時排序、事件高亮等功能。
-
事件釋放機制精簡化
- 過去事件的釋放邏輯需要遍歷鏈表、管理 free list;
- 現在結構為定長數組 + ordinal,釋放幀時只需清空當前幀對應的數組槽位;
free list
、next
等邏輯完全移除,減少代碼復雜度;- 每次只釋放當前
nextFreeFrame
指向的那一幀下的所有調試元素事件。
整體結構優勢
- 定長數組 + 循環索引:訪問高效、邏輯清晰、不需要動態內存管理;
- 可預測的內存布局:方便調試、維護及未來優化;
- 支持任意幀訪問:調試工具可以快速訪問某幀中某調試元素的所有事件;
- 完全移除動態鏈式管理邏輯:減少錯誤源,提升性能與可維護性;
- 基礎設施為后續 UI 擴展打下良好基礎:滑動條、幀統計、歷史比較等功能變得易于實現。
總結
通過引入 frameOrdinal
、定長幀數組、移除 free list,我們重構了調試系統中幀事件存儲與訪問機制,極大地簡化了代碼結構,提高了訪問效率,并為后續的可視化和分析功能打下了堅實的結構基礎。后續只需圍繞這一結構繼續擴展即可快速獲得更強的調試與性能分析能力。
game_debug.cpp: 修改FreeFrame和FreeOldestFrame,并重構NewFrame為InitFrame
我們進一步完善了調試系統中的幀管理機制,目標是構建一個高效、簡潔、易于維護的事件收集與回溯結構。以下是關鍵設計與實現細節的總結:
核心目標
- 構建一個基于環形緩沖區的幀結構,每幀作為事件容器;
- 通過精簡舊幀的釋放邏輯,徹底移除自由鏈表(free list)和相關復雜性;
- 明確三類幀的概念:當前收集幀(Collation Frame)、最舊幀(Oldest Frame)、最新完成幀(Most Recent Frame);
- 在內存不足時,自動釋放舊幀以騰出空間;
- 保證永遠不釋放當前正在寫入的幀(Collation Frame)。
幀釋放機制重構
清除舊幀事件邏輯
釋放某一幀的邏輯被簡化為:
- 遍歷該幀中所有調試元素;
- 將與該幀序號相關的事件全部清除;
- 不再需要維護任何鏈式結構或事件自由列表;
- 調試元素中原有事件頭指針等結構全部移除;
- 所有清理動作均基于幀序號進行索引操作。
釋放幀指針前移
- 通過
nextFreeFrame
指針控制當前可覆蓋的幀槽位; - 每次釋放只需前移該指針;
- 同時更新
oldestFrameOrdinal
,表示當前緩沖區中最舊有效幀; - 若釋放操作移除了最新幀(即
oldestFrameOrdinal == mostRecentFrameOrdinal
),則需同步推進mostRecentFrameOrdinal
; - 所有序號變動均采用環形模運算包裝,保持在固定長度的幀數組內循環。
新幀生成與替換邏輯
- 不再“創建新幀”,而是復用并覆蓋舊幀槽位;
- 原函數
NewFrame()
重命名為EmitFrame()
更準確; - 每次
EmitFrame
僅更新當前收集幀所在的幀槽位內容; - 替換前,需釋放當前
nextFreeFrame
所指幀,避免內存泄露; - 然后將當前收集幀寫入該槽位,并更新幀指針。
存儲邏輯中的內存保護
- 寫入事件前,必須檢查當前幀是否有可用空間;
- 若內存不足,在寫入前可調用幀釋放函數
FreeOldestFrame()
; - 如果
collationFrameOrdinal == oldestFrameOrdinal
,則表示無法釋放,已達到最小幀容量下限,此時為系統級錯誤; - 否則可安全釋放并繼續寫入;
- 該邏輯為未來自動內存管理提供了基礎,甚至可以設置自適應回收策略。
幀序號管理結構(初始值)
名稱 | 含義 | 初始值 |
---|---|---|
collationFrameOrdinal | 當前收集幀的序號 | 1 |
oldestFrameOrdinal | 最早有效幀序號 | 0 |
mostRecentFrameOrdinal | 最近完成寫入的幀 | 0 |
所有三個變量通過模運算(wrap)保持在 frameCount
范圍內循環。
系統優勢與后續擴展
- 極簡事件清理:不再需要鏈表結構,按幀清理即可。
- 幀復用機制:沒有動態分配和釋放,避免碎片問題。
- 內存保護機制清晰:寫前檢查與安全釋放結合,容錯能力提升。
- UI 支持增強:可以輕松支持滑動條、時間軸、事件時間回放。
- 邏輯層清晰分明:幀狀態的三個角色(收集、最新、最舊)定義明確,操作分離,狀態易管理。
通過這些改動,我們完成了從鏈表式事件管理向環形幀緩沖系統的過渡。新系統性能更高,邏輯更清晰,也為未來調試工具擴展與自動回收機制打下了堅實的基礎。
game_debug.cpp: 引入IncrementFrameOrdinal和GetCollationFrame
我們進一步調整了幀系統的序號管理和訪問方式,目的是優化幀的引用邏輯,使其更加清晰、高效,同時為后續可能的調整留下靈活空間。以下是本輪工作的詳細總結:
幀序號(Frame Ordinal)遞增機制
- 每當產生一幀時,我們都會遞增幀序號(frame ordinal),保持其單調遞增;
- 無論幀是否被覆蓋、是否存入數組,我們都確保有一個邏輯上嚴格遞增的幀標識;
- 該幀標識不直接等同于幀緩沖數組中的索引,而是通過模運算映射到對應槽位;
- 實際用于訪問的數組下標由
frameOrdinal % kDebugFrameCount
得到; - 這種方式將“幀在歷史時間軸上的位置”與“幀在緩沖區中的位置”解耦。
最終幀狀態變量及其含義
我們維護了幾個關鍵變量用于標識不同幀的狀態:
變量名 | 含義 | 說明 |
---|---|---|
CollationFrameOrdinal | 當前收集中的幀 | 正在寫入的新事件所在的幀 |
mostRecentFrameOrdinal | 最近完成的幀 | 最后一個寫入完成,可供讀取和繪制的幀 |
oldestFrameOrdinal | 緩沖區中最舊的有效幀 | 當前仍被保留的最早幀,釋放時從這里開始 |
frameOrdinal | 全局幀編號計數器 | 每次產生幀時遞增,僅用于記錄時間軸位置 |
訪問幀數據的兩種策略(設計考慮)
我們目前討論了兩種獲取當前幀指針的方法:
-
保持當前設計:
- 使用
frameOrdinal % kDebugFrameCount
每次動態求出緩沖區索引; - 邏輯清晰,簡單易維護;
- 開銷略微增加但可忽略。
- 使用
-
添加映射指針緩存:
- 每次遞增幀序號時,同時維護一個指向對應幀數據的指針變量;
- 快速訪問當前幀的指針,無需重復計算模;
- 稍微增加狀態復雜度,但可提升訪問效率。
目前我們選擇保留現有方式(動態模運算),但保留后續優化可能性,后續如頻繁訪問幀數據,則可再考慮引入幀指針緩存。
一致性與完整性保障
為避免幀狀態混亂,添加了以下保護措施:
- 每次遞增幀序號時,始終同步更新
CollationFrameOrdinal
; mostRecentFrameOrdinal
總是小于CollationFrameOrdinal
;- 不允許釋放
CollationFrameOrdinal
所指幀,一旦檢測到即視為嚴重錯誤(代表緩沖區容量太小); - 所有幀操作必須使用標準化入口,以防狀態不一致。
后續考慮
- 可為調試用途增加接口:將幀序號直接映射為可視化滑動條的下標;
- 若內存極度緊張,可增加“自動幀回收”邏輯,根據占用大小動態釋放舊幀;
- 在多線程寫入時可將
CollationFrameOrdinal
隔離成每線程變量,但目前先按單線程處理。
通過本輪優化,我們確保幀序號管理邏輯完整、自洽,并為進一步擴展幀索引模式或幀數據結構留足空間。整個系統保持了高可讀性和可維護性,后續可靈活調整而不會引入混亂。
game_debug.cpp: 解決編譯錯誤
我們對調試幀系統進行了深入的優化和重構,主要圍繞幀的序號管理、幀輪轉、幀初始化及清理流程進行了調整,以下是詳細的總結:
幀輪轉與初始化邏輯優化
- 利用了當前正在收集事件的幀序號(collation frame ordinal),可以直接定位當前正在操作的幀;
- 通過
debugElementFrame = elementFrames + currentCollationOrdinal
獲取對應幀位置,無需頻繁地訪問全局狀態結構; - 每次幀推進后,執行
initFrame()
對新幀進行初始化,確保其狀態干凈,準備接收新的事件數據; - 優化了舊邏輯中對 debugState 的頻繁引用,統一在局部緩存變量中處理,提升可讀性和效率。
幀序號及狀態標記邏輯更新
- 明確使用三大標記值來跟蹤調試幀狀態:
標記名 | 含義 | 說明 |
---|---|---|
oldestFrameOrdinal | 當前仍然有效的最早幀序號 | 如果新幀即將覆蓋此幀,需先釋放它 |
mostRecentFrameOrdinal | 剛剛完成寫入的幀 | 最新可用于展示或分析的幀 |
collationFrameOrdinal | 當前正在被寫入的幀 | 每次事件寫入都指向此幀 |
-
每次新幀產生時:
collationFrameOrdinal
向前推進;- 將上一幀的序號寫入
mostRecentFrameOrdinal
; - 若新序號覆蓋到
oldestFrameOrdinal
,則需先釋放最舊幀并推進該序號。
-
所有幀的訪問均基于
frameOrdinal % frameCount
映射到實際緩沖區位置,形成循環使用的幀緩沖池。
清理邏輯與冗余剔除
- 清除了項目初始化時對所有幀“清零”的代碼段,因實際過程中每幀都會在使用前初始化,無需統一清空;
- 刪除了不再必要的條件語句,比如舊邏輯中的 if 判斷初始化是否成功或是否需要重置;
- 整合了多個地方冗余的幀訪問方式,統一使用
debugState.frames + ordinal
進行引用,邏輯更清晰; - 某些調試打印邏輯被移除或精簡,改為直接從
mostRecentFrameOrdinal
讀取目標幀指針,提升效率。
統一幀推進和釋放流程
在幀推進時執行以下步驟順序:
-
記錄當前幀為“最近幀”:
mostRecentFrameOrdinal = collationFrameOrdinal
-
推進當前幀到下一個槽位:
collationFrameOrdinal++
-
檢查是否與最舊幀沖突:
- 若
collationFrameOrdinal == oldestFrameOrdinal
,表示緩沖池已滿,必須釋放舊幀; - 執行釋放邏輯并推進
oldestFrameOrdinal
。
- 若
-
初始化新幀:
- 執行
initFrame()
對新幀進行清理和初始化,準備寫入新數據。
- 執行
這樣保證緩沖區始終以循環方式管理幀內存,不會內存泄漏或訪問臟數據。
總幀數統計及簡化
- 將
frameIndex
簡化為一個totalFrameCount
,單純作為產生的幀數量統計; - 刪除了原本多余的幀索引變量,防止混淆;
- 調試接口只需根據
mostRecentFrameOrdinal
和環形緩沖結構,快速定位當前可查看幀。
小結與后續思路
-
整體幀結構管理已從“靜態索引”過渡為“序號驅動+循環緩沖”方式;
-
移除了大量不再適用的初始化和訪問邏輯,代碼結構大幅簡化;
-
當前流程對內存和性能都更加友好;
-
后續可根據內存壓力,擴展自動幀釋放策略或支持更大的環形緩沖區。
這個重構極大地提升了幀系統的健壯性和可維護性,同時也為后續功能(如快照回溯、事件回放)打下了清晰穩定的基礎。
FrameIndex = 511 GUI 是空
寫代碼打斷點試試
繼續看DebugState->Frames[1] 怎么初始化的
RootProfileNode 好像有問題
運行游戲,查看它是否(幾乎)正常運行
我們完成了幀管理系統的大部分重構和邏輯梳理后,雖然心里清楚肯定還有一些細節被遺漏,預期第一次運行時大概率不會完全成功,但決定先嘗試運行一遍看看效果。
初次運行結果
- 出乎意料的是,系統基本上成功運行了,初步看來邏輯并沒有立即崩潰;
- 初始輸出沒有明顯錯誤,也沒有立刻崩掉,說明大部分關鍵路徑已經被正確梳理;
- 對某些調試數據進行打印和驗證,確實“看起來像是起作用了”。
驗證邊界情況:幀大小上限
- 為了進一步驗證邏輯的健壯性,嘗試故意觸碰幀緩沖區的上限,即讓幀序號在環形緩沖區中“打滿”,驗證是否能正確回收和覆蓋最舊幀;
- 如預期一樣,在這一操作下系統出現了崩潰或錯誤,說明雖然基本路徑已完成,但在處理幀上限或內存回收邊界時還存在漏洞;
- 此次崩潰是預料之中的,因此也說明測試邏輯正常,并驗證了我們還需處理更細致的幀覆蓋邏輯或內存釋放流程。
當前狀態總結
- 當前階段主要目標是將基本幀推進、初始化、訪問和回收機制打通;
- 主路徑邏輯基本可以運行;
- 對于邊界條件,如緩沖區滿、幀回收、長時間運行的行為等,仍需進一步完善處理機制;
- 系統整體已經從“結構重構”階段進入“邊界調試與修復”階段。
我們下一步的重點將是識別并修復在幀緩沖區滿時可能出現的崩潰問題,以及確保所有幀管理行為都嚴格遵守我們的循環緩沖設計原則。
調試器:在FreeFrame處斷點,檢查序列號
我們在幀管理機制中遇到了新的問題,并對幀釋放邏輯進行了深入排查和調試。以下是詳細的總結:
當前狀態下幀序號的錯誤行為
- 最新幀(most recent frame)被錯誤地設置為252;
- 合并幀(collation frame)卻從1到7;
- 最舊幀(oldest frame)同樣為252;
- 這種狀態明顯不合理,幀序號重疊而邏輯不對,導致崩潰或錯誤行為。
初步調試流程和驗證
-
設置斷點觀察幀釋放邏輯:
- 程序跑到幀255后,再嘗試合并幀0,釋放最舊幀;
- 進入
free_frame
時,檢查幀0上是否掛有事件; - 發現幀0是空的,說明這是預期行為(因為合并是從幀1開始的);
- 再進一步驗證
free_frame
中的釋放路徑是否觸發,結果確實沒有觸發釋放動作; - 初步得出結論:空幀不需要釋放,邏輯暫時正常。
-
接著觀察幀推進過程:
- 每次幀推進時,會執行釋放最舊幀、合并幀推進;
- 當前邏輯看起來是合理的;
- 釋放時
get_free_event
被調用,成功釋放出分配的內存塊; - 驗證釋放動作確實發生了。
發現新的潛在內存泄漏問題
- 雖然上述釋放流程正常運作,但仍有內存未被釋放的問題;
- 觀察現象:幀不斷推進,內存使用未見明顯下降,推斷是某些資源未被正確釋放;
- 猜測問題可能發生在某些幀分配的對象未被納入釋放流程中。
懷疑釋放路徑遺漏了某些數據
- 目前的釋放邏輯只處理了 event 分配數據;
- 質疑某些數據結構為何采用了指針形式,并沒有內嵌在幀結構中;
- 如果這些數據(如某些臨時存儲結構)是通過
malloc/new
分配的,那釋放邏輯中必須包含相應的free/delete
調用; - 當前
free_frame
中似乎缺乏對這些結構的釋放,導致內存泄漏。
下一步修復方向
-
排查所有在幀生命周期中動態分配的對象:
- 包括事件、臨時緩存、調試信息等;
- 明確其是否在
free_frame
中被正確處理。
-
統一內存管理結構:
- 若某些數據是指針形式,應確認是否需要轉為嵌套結構;
- 或者在幀釋放時顯式調用其析構邏輯。
-
增加幀釋放后的內存驗證邏輯:
- 如:統計每幀分配/釋放總量,進行對比;
- 防止靜默內存泄漏持續存在。
總結
目前幀系統的主循環、推進機制基本構建完成,但在內存回收路徑上仍存在遺漏。某些動態分配的數據未被釋放,尤其可能是未納入幀事件列表中的結構。接下來要集中精力修復內存泄漏路徑,確保每幀推進時確實回收上一幀的所有資源。
game_debug.cpp: 有條件地在FreeFrame中執行FREELIST_DEALLOCATE
我們在執行 free frame 操作時,會查看 framework 編號,然后依次處理元素幀、標簽幀等,設置 frame 等于當前狀態,再將其加入 frames 中,同時處理其序號。
如果 frame 的 profile 編號存在,我們就會嘗試分配內存(RAM),但在某些情況下內存增長被阻止了。當前內存使用情況仍未處理完畢,因此想確認這些內存到底來自哪里。但這個來源并沒有明確指向,因為時間已經超出很久了,處理已經延遲十分鐘左右。
我們現在的問題是,如果當前機制并沒有釋放所有內容,那么到底有哪些部分沒有被釋放?這個是我們要明確的關鍵點。
我們檢查了 debug 樹,發現這些是正常的,不在回收流程中;deep views 同樣不是回收流程的一部分。還有一些 debug threads,理論上它們應該是單獨被正確處理的,這些也屬于 debug arena 而不是 periphery arena。
我們可以確認每個 frame 只使用一個 per frame arena。因此,我們真正需要關注的是 periphery arena 中到底產生了什么內容。
進一步分析后發現,在整個流程中,只有一個地方會從 arena 中取出內容,那就是 store event。如果這是唯一一個提取操作的地方,那就意味著我們在存儲事件時,有些事件在后續的 free event 遍歷過程中根本沒有被回收。這就是造成內存未釋放的根本原因。
因此,當前的關鍵問題在于事件的存儲和釋放流程之間存在斷層,部分事件被保留在內存中但未能被成功釋放。
game_debug.cpp: 仔細檢查FreeFrame并對*ElementFrame進行ZeroStruct處理
我們在這一部分明顯處理錯誤了。
在遍歷哈希表并嘗試提取準備釋放的 frame 時,用來恢復所有事件的方法已經不再準確,不確定具體原因,但可以肯定當前方式無法有效找回所有事件。
free event 操作中,我們會先找出 frame 中最早的事件,然后將該事件之后的事件設為新的最早事件,并釋放掉當前正在處理的事件。按照這個邏輯,所有該 frame 中的事件都應該被依次釋放出去。這個過程會對哈希表中所有的條目進行處理。
問題在于,哈希表中到底還包含了什么內容?這一點值得進一步調查。
此外,frame 處理完之后,理論上應該執行一次清理,比如將其置零或者提取清除(zero extract wipe),目前這一部分似乎未進行。但即便如此,照理來說這不應該影響事件釋放的主要流程。
綜合來看,事件釋放操作的邏輯在設計上看似完整,實際上存在未被釋放或未被正確遍歷的部分,特別是在哈希表的遍歷和事件鏈表更新中可能出現遺漏。這可能導致部分事件一直殘留在內存中,未被釋放。需要重點檢查 frame 中事件的鏈表結構更新是否有遺漏,以及哈希表在處理后是否確實被清理干凈。
運行游戲并再次確認我們正在泄漏內存
當前的問題可能需要等到明天再繼續深入調查,目前還沒能立即找到明確的解決辦法,但初步現象已經比較明顯:內存確實發生了泄漏。
原本事件在使用完畢后是能夠被正常釋放的,后來某次修改中破壞了這一機制,導致現在事件不能再在處理完后被釋放,而是直接被泄漏掉了。
內存使用趨勢本應是下降的,但現在卻一直持續增長,說明釋放機制失效了。最終,在內存達到一定程度后,系統就會出現阻塞或者卡住的情況。
表現為,當嘗試分配下一個事件所需的內存時,系統會反復嘗試釋放舊事件以騰出空間。然而,它“認為”已經釋放了所有可以釋放的事件,因此會陷入死循環,在循環緩沖區中反復查找,卻永遠找不到可以釋放的內容。
也就是說,當前的狀態是:系統錯誤地認為已經完成了所有的釋放操作,但實際上事件仍舊殘留在內存中未被釋放,導致下一次事件存儲時無法分配新內存,從而陷入卡死狀態。
這個邏輯問題非常棘手,因為它混淆了系統的釋放狀態與真實的內存使用情況,造成釋放路徑被打斷,而內存不斷積壓、無法回收。后續需要重點排查釋放邏輯是否完整執行,以及事件狀態是否被正確標記和更新。
問答環節
是否需要將+FrameOrdinal包裝到FreeFrame函數中?
在處理 + FrameOrdinal
時,是否需要進行封裝(wrap)的問題需要明確。
當前存在兩個相關位置,一個在這里,另一個也在相應位置。提到“是否需要 wrap”,但從邏輯上看,這里并不需要進行取模(mod)運算或手動封裝操作。
原因在于,所有傳入的 ordinal 都已經是預先處理過的,范圍已經被正確限制在允許的區間內,因此不會超出預期范圍。換句話說,在調用 free
函數時傳入的 ordinal 已經是封裝后的值,不存在越界風險。
也就是說,free 函數接收到的 ordinal 是一個已經 wrap 過的值,所以在內部處理時不需要再次進行 wrap 或 mod 操作。整個系統設計中,ordinal 的生命周期中已經保證了它始終處于有效范圍內,這避免了重復封裝的必要性。關鍵在于保持 ordinal 的有效性在它被傳遞前就已經保證。
game_debug.cpp: 斷言FrameOrdinal小于DEBUG_FRAME_COUNT
我們可以在這里加入一個斷言(assert)來確保安全性,確認傳入的 ordinal 沒有超出 frame 的有效范圍。實際上我們可能確實應該這么做,比如在嘗試釋放事件時加一句提示:如果有人試圖釋放一個不在 frame 范圍內的內容,那么應該立即觸發異常。雖然這種情況理論上不應該發生,因為沒人會傳入一個超出范圍的值,但加上斷言可以提高代碼的健壯性,防止潛在的問題悄無聲息地擴散。
此外,還有一個地方讓人感覺有些奇怪,可能是哪里遺漏了什么非常基礎但細節上的東西。雖然邏輯上看似合理,但總感覺忘記了某個關鍵點。
尤其是在 store event
的處理上,這部分邏輯顯得有些混亂。通過相關的 correlation framework tunnel,我們是知道在一個 frame 上存儲了多少個事件的,這意味著我們應當能明確掌握每個 frame 中事件的數量和狀態。
問題可能就隱藏在這種“我們以為知道”的假設中,某處微小的不一致可能造成了事件未被釋放,或者事件數量與實際情況不符,導致后續釋放操作失效,進而引發內存泄漏或資源積壓。
接下來應該重點回顧 store event
的實際行為是否真的與我們預期完全一致,特別是事件數量更新與 frame 狀態同步的部分,可能隱藏著邏輯遺漏或者邊界處理錯誤。
game_debug.cpp: 追蹤FreedEventCount
還有一個有趣的點是,可以通過斷言機制進行校驗,從而捕捉問題。
具體來說,可以引入一個 freed_event_count
變量,每次有事件被釋放到 free list 時,就對這個計數進行累加。這樣在處理完一個 frame 之后,就可以對比該 frame 的 stored_event_count
和 freed_event_count
是否相等。
如果兩者相等,就能確認這個 frame 中的事件全部被成功釋放;否則說明存在泄漏或釋放遺漏的情況。這個對比可以通過 assert 實現,用來在調試階段直接捕捉問題。
不過,為了讓這個校驗成立,還需要確保所有釋放路徑都參與計數,所有存儲路徑也正確統計事件數量。這意味著除了釋放操作要累計 freed_event_count
,在存儲事件時也要準確維護 stored_event_count
,確保兩者始終同步。
這個方法雖然簡單,但能夠有效驗證釋放邏輯是否和存儲邏輯嚴格對應,是定位內存泄漏或 frame 內事件殘留問題的有效手段。關鍵是要保證每次操作的完整性,并對每個 frame 的事件生命周期進行精準跟蹤。
運行游戲,崩潰并發現我們釋放了比存儲的事件更多的事件
情況開始變得混亂。
一開始看上去是還沒有進入 physics frames,所以事件數量還沒對上。但即便如此,數量依舊對不上,這就更讓人無法理解了。
統計顯示存儲了 56 個事件,卻只釋放了 50 個,這種不對稱意味著有事件在某處被遺漏了釋放。
而且這段邏輯中只有一個地方可以生成這樣的事件,因此理論上事件數量應該是完全對得上的。如果只有一個入口,那就不可能出現多余的事件。問題在于事實卻不符。
開始懷疑這些事件是如何被存儲的。查看了 store events
的調用路徑,發現確實是在 frame 上進行事件存儲操作的。
進一步追查具體存儲的對象——element,到底是什么類型或結構?這個 element 被用來存儲事件,但它的來源可能影響整個流程。
疑點集中在以下幾個方面:
- frame 上的事件數量和釋放數量不一致;
- 事件的唯一生成路徑理論上應保證數量可控,但實際上卻存在偏差;
- element 的具體身份可能隱藏了某些間接存儲路徑,導致事件數量意外增加;
- profile node 的分配位置也值得關注,可能間接產生了事件或造成未釋放的殘留。
下一步需要仔細檢查 element 是如何構成的,它是否間接導致了事件的創建或者存儲行為;同時也要排查是否有其他隱藏路徑繞過了釋放流程。這個偏差可能來源于某些未顯式標記或未明確注冊的事件實例。
game_debug.cpp: 移除一個FREELIST_DEALLOCATE
事件實際上是被包裝在一個 element 中的,這樣做的目的是為了簡化處理流程。
但關鍵在于,這個事件并不一定需要被真正存儲下來。也就是說,雖然看起來事件被“包裝”了,但并沒有明確指明它必須作為一個正式的存儲操作被注冊到 frame 中。
因此,某些事件雖然經過包裝處理,但并沒有進入實際的存儲流程,這種情況下就不應該計入 stored_event_count,也不需要參與釋放計數。
換句話說,這部分邏輯之前的理解存在偏差,誤以為所有包裝過的事件都會被存儲,實際上只有明確被標記存儲的事件才會計入統計。由于這一點沒有區分清楚,導致事件數量統計出現異常,看起來像是“少釋放”了,實際上可能只是多算了沒有真正存儲的事件。
這種設計雖然靈活,但也容易混淆,需要明確哪些包裝操作只是中間處理,哪些才是真正的存儲行為,避免將不必要的內容納入釋放邏輯,導致事件計數不一致。接下來要重點理清包裝操作和存儲操作之間的邊界,確保事件生命周期的判斷準確。
重新構建并運行,發現錯誤已解決
重新構建并運行之后,現在可以通過斷言檢測事件數量是否不一致。如果斷言沒有觸發,那是最理想的情況,意味著存儲和釋放數量完全匹配。
接著又發生了一次不一致的情況。追蹤發現問題是由于事件被重復釋放了,也就是說事件被第二次放入 free list 中,這顯然是錯誤操作,會破壞整個 free list 的結構。
雖然這解釋了當前為什么出錯,但令人不解的是,在引入重復釋放之前,系統就已經表現出異常了。這就非常奇怪,理論上那時候還沒有做出破壞性的修改,不應該出問題。
最終確認,其實最初并沒有 bug,錯誤是在試圖修復“假設中的 bug”時人為引入的。也就是說,原本的系統邏輯是正常的,是在修改過程中誤傷了已有的正確流程。
總結起來,這次的問題是對系統行為的誤判導致了多余的干預,從而引發了真正的錯誤。教訓在于,在未能充分確認問題根源之前貿然修復,可能會帶來新的、更嚴重的問題。需要更加謹慎地驗證現象與根因之間的關系,避免“修 bug 反而造 bug”的情況發生。
我有點落后于進度,但你通常是否會跳過一些簡化的內容(例如像第39集那樣繪制位圖的內容),直接進入渲染器?另外還有結構化資源之類的內容?
目前在進度上稍有落后,不過在某些方面有時也會有意選擇繞過某些過程,比如像奧地利式編碼那種方式。
很多簡化的流程,例如渲染位圖之類的內容,和最終目標關系不大時,會選擇跳過,比如之前處理 drub bitmap 或 observatory night 那樣的場景,直接進入最終渲染邏輯處理。同樣地,對于像結構化資源(structured assets)這類系統性內容,如果已經清楚最終目標,通常也不會再繞遠路。
換句話說,如果已經知道最終不會使用位圖渲染,就沒必要花時間去實現一個純粹的軟件渲染器。如果目標最終是 GPU 渲染,那就會直接從 GPU 著手,不會再在軟件渲染上投入太多精力。
當然這并不是說從來沒有做過軟件渲染,早期確實寫過,那時 PC 上還沒有 GPU,早期寫的幾個渲染器就是純粹的軟件渲染器。正因如此,才對 GPU 渲染原理理解得更透徹。通過寫軟件渲染器,能真正理解圖形管線是如何工作的;否則,直接跳到 GPU API,反而會忽略底層的圖形處理邏輯。
不過如果已經掌握了軟件渲染器的實現原理,就不再需要每次都從頭寫一個軟件渲染器,除非有特別充分的理由,否則通常不會再重復做這件事。
至于調試系統(debug system),這部分確實是邊做邊探索。并沒有一開始就確定好結構,而是通過不斷嘗試、逐步摸索出適合當前需求的架構方式。
設計一個復雜系統本身就是實驗性的過程。如果是全新的系統,顯然不可能一開始就完全知道如何構建,必須通過嘗試逐步完善。尤其是在這次的調試系統中,嘗試實現一些以往沒做過的新功能,在探索過程中也發現了一些有趣的點。
這也是一種學習方式——展示如何通過試驗構建復雜系統。這種設計方式在構建全新功能時依然適用,即便經驗再豐富,也無法跳過探索過程。
至于 profile 的重命名,可以留到明天再做,那部分已經規劃好,也將會是一個良好的補充。整體進展是積極的,很多嘗試都具有探索和教育意義。
對于使用靜態數組而不是列表有何想法?
之所以選擇使用靜態數組而不是鏈表,是因為考慮到大多數需要實現的功能更偏向于“隨機訪問”某個特定幀的操作。每次都遍歷鏈表去定位某一幀在效率上并不理想。
很多目標操作都與幀相關,例如:
- 跳轉并繪制某一特定幀;
- 繪制當前幀以及前一幀;
- 同時繪制前五幀;
- 或者繪制所有實體的第 4 幀。
這些操作如果使用鏈表結構,就需要反復遍歷查找,而用靜態數組則能通過索引直接訪問,大大簡化邏輯和提高效率。
雖然不能完全排除鏈表也可以實現這些功能,但可以預見,當這種幀間跳轉和并行訪問需求頻繁出現時,鏈表結構會變得笨重且難以維護。使用靜態數組則能讓這類操作更直觀、更高效,也更容易組織和擴展。
正是基于這種對系統使用方式的預判,才最終決定采用靜態數組結構來管理幀數據。這個選擇是為了更好地支持后續的操作便利性和性能表現。
“繼承和封裝是人類最好的發明”。討論一下
有觀點稱“繼承”和“封裝”是人類最偉大的發明之一,這種說法簡直令人震驚,幾乎可以說是徹底偏離了理性。簡直就像是在“失控”一樣。
這樣的評價實在太過夸張,把面向對象編程的一些特性神化到這種程度,完全不符合實際經驗。現實中,這些機制雖然有其用途,但也經常成為系統設計混亂、難以維護的根源。
盲目推崇繼承和封裝,往往會導致架構臃腫、代碼層級混亂、依賴難以理清,反而讓維護成本成倍增加。很多情況下,繼承濫用會讓代碼更難以理解,封裝過度也可能掩蓋真正的問題。要真正寫出高質量、可維護的系統,關鍵在于合理設計、明確結構,而不是依賴某種抽象語法機制本身。
所以,聽到這種話時只能感嘆——實在是太離譜了。
你喜歡使用模運算還是掩碼進行循環索引?如果操作數是2的冪減1,編譯器是否會將模運算替換為按位與?
在循環索引數組時,對于使用取模運算(modulo)還是按位與(masking)的問題,通常的觀點是:
大多數情況下直接使用取模運算是沒問題的,特別是在調試器、日志系統等執行頻率較低的場景中。此時數組大小不必一定是2的冪,這樣更靈活,開發更方便。
但在對性能有更高要求的場景中,例如對速度非常敏感的系統,就可能更傾向于使用按位與操作,因為取模在底層是整數除法,性能上相對較慢,而按位與則可以更快地完成相同的操作,前提是數組大小是2的冪。
編譯器在優化方面通常也足夠智能,當取模操作的除數是2的冪時,大多數現代編譯器會自動將其替換為更高效的按位與操作。不過,雖然理論上編譯器會做這樣的優化,但并沒有專門驗證過是否在所有情況下都如此。
因此,在特別關注性能的場合,傾向于直接手動使用按位與操作來確保效率。而在常規場合下,直接使用取模運算通常是可以接受的,不需要對這類細節過于“看管”或擔心編譯器是否優化得當。
我們還會做馬拉松直播來應對周一的問答瘋狂嗎?
我們現在是否還在進行那種“馬拉松式”的連續工作,具體情況還不確定。我們也不確定是否會繼續以那種瘋狂的節奏推進。
不過,我們也保留那樣做的可能性。如果有需要,或者在某些情況下確實合適,我們可能還是會繼續采用那種高強度的工作方式。雖然現在不一定會那么做,但也不能完全排除那種可能性。
你幾周前運行的性能分析代碼怎么樣了?你之前實現的多個通道功能似乎消失了
為了實現理想的版本(例如發布 Arisia 版本),之前做過的一些功能在過程中出現了問題,其中有些模塊甚至“崩塌”了,比如多通道支持。不過多通道功能目前仍然保留著。
其實仍然可以展示那部分功能,盡管一開始說打算明天再進行重命名和整理,但現在仍然可以臨時啟用它。
以這個最早的測試用例為例,就是當初在處理多通道渲染時使用的第一個測試對象。從這個例子可以看到,多通道功能從早期就存在并一直保留至今,只是有些部分可能還沒有最終整合或發布,尚處于開發迭代過程中。整體來看,雖然有些模塊經歷過調整甚至暫時失效,但功能思路并沒有被完全放棄,而是在逐步演化和完善中。
game_debug.cpp: 重新啟用性能分析通道
現在需要將這段代碼進行移植。由于現在已經有了數組結構,所以原本需要長期保留每個 frame 的邏輯現在變得簡單得多。
首先,可以通過 debug_state
獲取最近一次的幀編號,賦值給 most_recent_frame_ordinal
。接著,在獲取 viewing_element
(假設它存在)的過程中,可以直接訪問其根節點,并通過多種方式進行操作。
例如可以先從 viewing_element.frames
中,取出最舊的事件 oldest_event
,也可以從 viewing_element.frames
中,通過 most_recent_frame_ordinal
索引獲取數據。
同樣地,也可以直接從 debug_state.frames
中,通過 most_recent_frame_ordinal
獲取某個幀信息,用于進一步的調試或顯示。不過,在使用過程中發現 debug_frame
并未重載索引操作符,因此代碼在當前狀態下會有報錯,必須先處理這個問題,才能繼續使用類似數組下標的方式來訪問具體幀數據。
運行游戲,查看多個通道,啟用軟件渲染器并崩潰
當前的性能分析模塊代碼中可以看到多通道視圖已經實現,畫面上有三條獨立的通道線,但目前其他線程中沒有活動數據顯示。
如果希望在這些線程中看到實際內容,可以通過啟用軟件渲染(soft surrender)功能來實現。
不過目前的顯示狀態并不理想,出現了可視化混亂或顯示異常的情況。可能的原因是最近進行了某些更改,尤其是和多幀視圖功能(multiple frame view)有關,而這些改動可能引入了一些問題,導致當前的顯示呈現出雜亂無序的狀態。
另外提到的 drawrectangle
看起來像是渲染過程中出現的某種問題或對象狀態異常,也可能是此次顯示出錯的直接原因之一。整體上看,這是調試過程中常見的情況,接下來需要針對顯示混亂的來源逐步排查。
game_debug.cpp: 暫時禁用遞歸并運行游戲
問題的根本原因可能是因為在性能分析(drug profile)時,繪制了大量的事件。因為事件數量太多,導致顯示變得過于復雜和混亂。解決辦法是暫時關閉遞歸,這樣就不會繪制所有的事件,減少渲染的負擔。
在此基礎上,我們現在已經能夠看到多通道視圖(multi-lane view)的實際運行效果。通過啟用這個視圖,多個通道現在都在正常運行中。
通常來說,在使用 GPU 渲染時,其他核心或 CPU 的工作量相對較少,因為大部分工作由 GPU 完成。但在軟件渲染時,可以看到每個線程被多次調用,處理大量的渲染任務,這使得每個線程的工作量大幅增加。
簡而言之,問題的關鍵在于渲染時事件的數量,導致了不必要的復雜性,關閉遞歸和減少繪制的事件數量是暫時的解決方法,能夠幫助我們更清晰地看到渲染效果。
如果你真的沒有從游戲中學到東西怎么辦?我已經看了1-28集四遍,覺得自己應該懂了。我嘗試做簡化的win32層,想要達到獨立層,但發現做不到。文檔不清晰,我也忘記了哪些東西是做什么的。你會對那些覺得自己沒學到東西的人說些什么?
如果發現自己在學習過程中遇到困難,可能有兩種原因。首先,可能是需要更加努力地去嘗試,克服困難。其次,可能是因為學習的材料對初學者來說有些過于復雜或過早。對于這樣的課程,它并不是為從未編程過的人設計的,而是為那些已經掌握編程基礎,但對游戲編程或游戲引擎編程不熟悉的人準備的。因此,如果你還不夠熟悉編程,可能會覺得跟不上進度,因為這個課程并沒有足夠的入門材料來幫助完全沒有編程基礎的人。
在這種情況下,可以先進行自我評估,看看自己是否掌握了足夠的基礎知識。如果覺得自己的基礎還不夠扎實,可以先學習一些基礎的編程材料,如初級的C語言教程或Windows編程材料,直到能夠更熟練地掌握這些基本概念。然后再回過頭來學習中的內容,這樣會更容易理解。
總之,學習過程中遇到困難可能是因為基礎知識不夠或者材料對自己來說過于復雜。通過自我評估,找到問題所在,再通過補充基礎知識,逐步提高編程能力,最終會順利掌握游戲編程和游戲引擎編程的內容。
game_debug.cpp: 從DrawProfileIn切換到DrawFrameBars并查看性能分析
現在已經完全準備好,明天可以進入幀條部分了。這樣就可以繪制出某個特定元素的所有幀,感覺對這一進展非常滿意。