游戲引擎學習第305天:在平臺層中使用內存 Arena 的方法與思路

回顧前一天內容,并為今天的開發工作設定方向

我們正在直播制作完整游戲,當前正在實現一個精靈圖(sprite graph)的排序系統。排序的代碼已經寫完,過程并不復雜,雖然還沒做太多優化,但總體思路比較直接,已經實現了基本功能。

為了讓這個系統正常運行,需要對一些底層基礎設施進行升級和改進。昨天結束時想到這些事情,我提到希望相關代碼能放到平臺層(platform layer)去實現。現在我們意識到,也許可以趁機把大部分渲染相關的代碼都移回平臺層。因為實際上我們調用的接口不多,而且之前做了不少隔離工作,所以這樣做是可行的。

將渲染代碼放回平臺層還會帶來好處,比如能夠重用已有的資源和邏輯,提升整體架構的合理性和維護性,這看起來是非常值得嘗試的事情。當前會先仔細看看這些代碼和結構,暫時還沒決定具體方案,計劃今天深入研究后再做判斷。

game_opengl.cpp:考慮將整個文件變為平臺無關代碼

我們現在思考的是關于OpenGL部分的代碼結構。觀察OpenGL相關的代碼,發現其實回調函數調用非常少。主要的回調是平臺層的紋理分配函數(platform allocate texture),但它們實際上是從反方向調用的。如果把這些代碼移到主代碼部分,就不再需要這些回調,這樣可以減少邊界轉換,減少邊界切換總是有好處的,能提升性能和代碼簡潔度。

除此之外,其他代碼大部分是內聯函數,結構相當簡單。唯一比較特別的是一個叫“knit call”的調用,還有一個OpenGL渲染命令調用。想到這里,我們可以考慮把一些不必要的調用刪掉,甚至不需要寫代碼就能去掉它們。

目前感覺,既然OpenGL是跨平臺的,我們完全可以把它相關的東西整理好,進行部分重構。比如,OpenGL代碼里有一部分是和“wiggle”相關的(可能是調試或動畫效果),這部分只在Win32平臺層執行,可以把這部分特定代碼移到平臺層,而把和OpenGL本身無關的部分留在游戲代碼中。游戲代碼只需要暴露一兩個調用,比如OpenGL渲染命令調用,大概只需要調用一次,初始化也可能需要。

另外,OpenGL顯示位圖的功能主要是為了支持軟件渲染模式。這個功能的處理方式和OpenGL渲染命令類似。如果我們想繼續支持兩種顯示模式——一種是通過OpenGL,一種是直接調用Windows的圖形顯示接口(GDI),那么就可以分別保留這兩套調用。比如一種顯示方式是把圖像直接拉伸后顯示,另一種是通過OpenGL路徑提交給顯卡。

總結來說,可以考慮把OpenGL相關渲染代碼和平臺相關的窗口操作代碼適當拆分和歸類,優化邊界轉換,同時保留多種顯示模式的支持,提升代碼的整潔度和擴展性。

game_platform.h:考慮將 platform_opengl_display_bitmapplatform_opengl_render_commands 從 game_opengl.cpp 移到這里

我們現在開始著手進行代碼重構的操作,初步判斷這是一個比較安全的改動,沒有明顯的理由不去做。因此決定逐步推進這項調整,如果過程中發現問題或操作不太順利,也可以隨時撤回,不會有太大代價。

計劃采用謹慎推進的方式,一步一步來操作,確保每個步驟都符合預期。如果有任何部分讓人感覺不對勁,可以立即停止調整,回退到原狀態。

首先查看當前的 platform_api 接口,雖然這個接口本身已經顯得有些復雜,今后也許應該對其做一次簡化整理。但從目前來看,調整不會進一步加重接口復雜度,因為本質上我們只是新增類似 platform_opengl_display_bitmapplatform_opengl_render_commands 這樣的函數而已。

這樣一來,從接口邊界管理上看也不會變得更復雜,算是一種“等價交換”,不會增加額外的負擔。

但在進一步檢查之后,意識到先前的想法有誤:原本認為可以通過在 platform_api 中增加這些OpenGL函數來處理,但這些函數其實是從游戲層傳入平臺層的,而不是從平臺層傳給游戲層的。因此,最終并不會在 platform_api 中添加這些,而是要在類似 GameUpdateAndRender 的接口層面新增一個用于執行圖形渲染工作的調用接口。

所以重新調整思路,不是在原來的平臺API中添加函數,而是新增一個從平臺傳入游戲代碼的圖形渲染調用接口。最終的結構將稍有不同,但仍然保持邊界職責明確、復雜度不變的目標。這個結構更符合程序模塊之間的職責分工,也更利于后續維護和擴展。

討論讓所有平臺層都包含 OpenGL 的綁定并鏈接 DLL 的弊端

我們最初設想將 OpenGL 渲染相關的函數移入主游戲代碼中,這樣所有的 OpenGL 調用也會跟著進入游戲模塊。理論上,這并不成問題,因為我們可以在每個平臺上鏈接對應的 OpenGL 庫,并引入所需的綁定。這種做法在技術上是可行的,每個平臺編譯時都鏈接其平臺特有的 OpenGL 實現就行了。

但在進一步思考后,我們對這種做法產生了猶豫。原因是,一旦將所有 OpenGL 調用放入游戲模塊中,就會引入平臺特有的調用,而當前我們的游戲代碼其實并不直接依賴任何平臺特性。現有結構允許游戲代碼被完全獨立地編譯和使用,不依賴平臺層實現的內容。

如果強行讓游戲模塊依賴 OpenGL,那么游戲模塊在編譯時將不得不鏈接特定平臺的圖形庫。這樣會破壞我們保持游戲代碼平臺中立性的設計。我們理想中的狀態是,甚至可以直接將 Windows 平臺上構建好的 DLL,在 macOS 或 Linux 中通過平臺層的適配器加載和運行——因為游戲邏輯本身并不直接調用任何平臺接口。

當前這種結構是非常有價值的,它為跨平臺部署提供了極大的靈活性。我們只需為每個平臺實現不同的“平臺層”,就可以在不改動游戲代碼的前提下實現平臺兼容。如果現在將渲染代碼塞進游戲層,這種靈活性將被破壞。

當然,如果我們以后確實需要對渲染模塊支持熱重載,也可以將其從平臺層中單獨拆出來,作為另一個動態庫加載。但從目前來看,這樣的復雜度遠超當前需求,也沒有足夠的收益。

因此,我們最終決定放棄這個想法,不再將 OpenGL 渲染邏輯移入游戲代碼中。還是沿用之前討論的方案,保留平臺代碼與游戲代碼的明確分界,不做過于激進的結構調整。這樣既可維護性好,也保留了跨平臺的靈活性。

win32_game.cpp:讓 Win32DisplayBufferInWindow() 調用 SortEntries(),同時考慮是否將排序或 memory_arena 移入平臺專屬層

現在我們有兩種方式可以處理當前的問題,但我們還沒有最終決定采用哪種方案。

之前我們在代碼中實現了一個 SortEntries 的函數,它的功能是對一些圖形項進行排序。但這個函數的問題在于它期望接收一個 memory_arena(內存分配區域)作為參數。而我們在調用它的時候,并沒有傳入這個參數,這就導致了使用上的不便。

我們真正想要的是在調用類似 SortEntries 這樣的函數時,能夠自然地傳入內存區域,從而簡化代碼邏輯并使其更具可用性。

目前我們在兩個方向之間做權衡:


方案一:

將排序邏輯移入平臺層
也就是說,把和排序相關的功能放在平臺特定的代碼中。這樣做的好處是所有需要用到內存區域的臨時操作都可以集中在平臺層里完成。我們可以在平臺層內自由創建臨時內存,進行排序,然后把結果交給游戲邏輯。這種方式封裝性好,但可能讓平臺層變得更重。


方案二:

memory_arena 的定義移動到平臺層中,使其成為共享功能
也就是說,把 memory_arena 這種通用的數據結構和相關工具,直接作為平臺層的一部分進行共享。這樣做的好處是無論平臺層還是游戲層,都可以使用統一的內存分配策略,也讓平臺層代碼更易復用現有的分配邏輯。

我們傾向于方案二的理由是:
當前在平臺層(比如 Windows 平臺的具體實現中)已經有很多地方本可以用 memory_arena 簡化內存操作,但由于我們沒有包含它,所以不得不寫了一些重復甚至冗余的邏輯。如果把 memory_arena 移出來,放在平臺共享代碼中,以后在平臺層需要分配臨時內存或管理短期對象的時候,就可以直接使用這些結構,而不用重復造輪子。

這也是一個好時機來完成這件事——將 memory_arena 移動到平臺級共享位置中,這樣今后就不需要在調用這些工具函數時再擔心是否可以使用臨時內存或手動管理分配。


接下來我們就準備著手進行這項調整。這樣一來,我們就為平臺層和游戲邏輯之間搭建了更自然、更通用的內存共享機制。

新建 game_memory.h,并在 game_shared.h 中引入

我們現在進入了 game 的代碼目錄,準備進行一些代碼結構調整。

首先,我們創建了一個新的頭文件 game_memory.h 和一個對應的 game_memory.cpp 文件。這兩個文件的目的是將原來分散在其他文件中的內存管理邏輯獨立出來,使其成為一個統一的、平臺與游戲邏輯都可以共享的組件。

我們從已有的內存管理代碼中復制了一些功能,包括:

  • memory_arena(內存分配區)結構體
  • 臨時內存分配器(temporary arena)
  • 用于對齊和清零的輔助宏和函數
  • 內存區域的分配與釋放工具(如 PushSize、PushArray)
  • 一些零大小結構體與內聯工具函數

這一部分本身就具備很強的獨立性,幾乎構成了一個完整的內存模塊。所以我們只需要把它拷貝出來,放入單獨的文件中,再清理掉那些依賴于上下文但不屬于該模塊的雜項引用就行。

.cpp 文件中,目前并不需要寫什么內容,因為這個內存模塊的功能基本上都通過內聯函數實現,邏輯相對簡單,暫時沒有復雜的運行時邏輯。如果將來需要添加線程安全或其他高級內存管理功能,可以擴展 .cpp 文件來處理。

接下來,為了使這部分模塊在平臺層與游戲邏輯中都能使用,我們需要確保它被正確地包含。最初我們查看了 game_shared.h 文件,這是一個用于多個子系統之間共享聲明的頭文件。我們發現它本來是用于預處理器(比如我們之前寫的小型預編譯器)共享的一些類型聲明的,但現在它也可以承擔更多共享邏輯的職責。

因此,我們決定將 #include "game_memory.h" 添加進 game_shared.h,這樣所有包含該共享頭文件的模塊都能直接訪問 memory_arena 等結構。這一步統一了依賴,避免在多個地方重復包含。

接下來,我們回到了原始內存相關代碼所在的地方,并刪除了那些現在已經被獨立出來的部分。這樣做可以保持源文件整潔,也明確了模塊之間的邊界。

這一操作完成后,我們就可以在任何需要使用內存分配器的地方直接聲明 memory_arena 并進行內存操作了,例如排序操作。

現在只需要做一件事:確保傳遞 memory_arena 給排序函數時,有一塊可用的內存可供使用。

同時,我們還注意到另一個待辦事項:out_index_array,這是我們之后渲染過程中想要依賴的數據結構。目前渲染流程還沒有利用它,但我們希望將來能改進這一點,把渲染流程與這個輸出索引數組連接起來。這部分工作將會在稍后的流程中分開處理。

總結如下:

  • 創建并引入了 game_memory.h/cpp,統一并封裝了內存管理邏輯。
  • 將內存模塊抽離為獨立組件,方便平臺與游戲層復用。
  • 清理了原始代碼中的重復實現。
  • 修改了共享頭文件以提供模塊訪問能力。
  • 準備將其用于更高級的用途,例如排序與渲染。

后續計劃包括把排序結果與渲染邏輯結合,并進一步理清 out_index_array 的作用和使用方式。

當前沒有引用.cpp 文件因此之前沒有引入并且不需要這個shared文件

memory 相關的提取到memory中

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

引入 overflow memory state(溢出內存狀態)的概念

現在是一個很合適的時機來探索一下我們剛剛抽離出來的內存管理系統,并對其進行進一步擴展和增強。一個很值得嘗試的改進方向是加入“溢出內存狀態”(overflow memory state)的概念。

目前我們對內存的使用模式是一次性地預分配一整塊內存,然后在這塊內存中進行分配。這種方式在一些環境下非常理想,比如:

  • 我們清楚地知道平臺上可用的內存量;
  • 需要最大限度保證確定性與性能;
  • 類似舊式游戲主機或內嵌系統的架構,這種方法非常穩定可控。

但在現代系統,尤其是32位 Windows 上,內存管理變得更加復雜。雖然理論上我們可以申請比如 1GB 或更多內存,但實際上可能無法獲得一整塊連續的地址空間。原因是系統的地址空間被碎片化了,即使系統有足夠的空閑內存,也可能無法分配出一整塊連續的大內存區域。舉個例子:

  • 嘗試一次性申請 1GB 內存,有時會失敗;
  • 系統中雖然確實有 1GB 空閑內存,但它被切成了多個小塊;
  • 系統可能只愿意分配多塊小區域,而不是一整塊連續的空間。

在這種情況下,預先一次性分配大塊內存的方式就不太適用了。為此,我們可以讓內存分配器支持“分塊擴展式”的模式:


可擴展的內存分配模式設想:

我們為內存分配器加入兩種工作模式:

1. 固定塊模式(當前已有)
  • 提前分配好一整塊內存;
  • 分配失敗即為整體失敗;
  • 適用于對資源調度嚴格、內存確定性的應用。
2. 溢出塊模式(待添加)
  • 初始分配一個塊,使用完之后繼續動態分配新的內存塊;
  • 每次內存不足時,內部自動添加新的區塊接入分配鏈;
  • 允許系統動態決定能分配多少;
  • 適合需要動態擴展內存的場景;
  • 直到系統拒絕再分配,才真正“用盡內存”。

通過這種方式,我們可以支持以下情況:

  • 游戲在運行時根據實際需要擴展內存;
  • 玩家可以創建大量或無限量的游戲對象;
  • 某些具有高動態內容密度的游戲場景(如大世界模擬、開放世界生成);
  • 可以在64位系統中使用幾乎無限的內存空間,也能盡可能兼容32位系統的限制。

現有代碼的兼容性

我們目前的內存分配器設計非常靈活,從邏輯結構上來看已經為這種擴展方式做好了準備。因此實現這類功能并不需要大幅改動代碼結構,只需要:

  • memory_arena 中增加鏈式存儲多個內存塊的能力;
  • 在每次分配失敗時自動擴展新的內存塊;
  • 在內存釋放或重置時,統一處理多塊內存的回收。

這種功能雖然在某些項目中可能并不會被用到,比如體量較小的游戲或邏輯非常受限的應用,但一旦涉及到玩家行為高度不可預測或數據規模非常龐大的應用場景,就能展現出它的巨大價值。

因此,這會是一個值得實現的功能點,也可以作為下一步繼續開發與探索的內容。

win32_game.cpp:修改 Win32DisplayBufferInWindow(),讓它接受一個 memory_arena,并創建臨時內存供 SortEntries()LinearizeClipRects() 使用

我們注意到,在調用 Win32DisplayBufferInWindow 的時候傳入了兩種不同類型的內存,但實際上沒必要這么做。我們完全可以使用一個共享的臨時內存池(temporary arena),將其作為參數傳遞給相關模塊,由它們自己在其中申請所需的內存,這樣既簡潔又高效。

我們的想法是把這個臨時內存池傳遞給如排序(SortEntries)或線性化裁剪矩形(LinearizeClipRects)這些流程,讓它們直接從這個共享內存池中分配數據結構所需的內存。我們只需確保該內存池預分配的內存足夠大即可。

以排序為例,我們可以將這個臨時內存池傳入 SortEntries,在其中分配排序后的索引數組(例如 out_index_array)。也就是說,這個索引數組完全可以由 SortEntries 通過臨時內存池自動分配出來,而不是外部調用者顯式提供。

這樣一來:

  • 排序的輸出數組通過內存池自動分配;
  • 裁剪矩形的線性化輸出也可以通過相同機制處理;
  • 所有臨時分配都綁定在一次繪制(render)調用生命周期之內;
  • 在渲染結束后,統一調用釋放或重置該內存池,就能清理所有內存,過程簡單干凈。

此外,我們可以通過 BeginTemporaryMemoryEndTemporaryMemory 來管理整個渲染過程的內存生命周期,這樣就不必擔心遺留內存泄漏或資源無法釋放的問題。


我們注意到部分函數如 CreateWindowClipRects 理應返回某些結果或數據結構,但從現有調用來看,似乎并未實際返回或傳遞。這個行為可能是因為這些數據被嵌入到了 RenderQueueGameRenderCommands 之中,通過統一的渲染指令流傳遞下去。

從代碼組織的角度看,雖然這種做法可能略顯隱晦,但它的好處是:

  • 所有渲染相關的數據結構統一附著在 GameRenderCommands 結構上;
  • 方便傳遞,避免修改過多調用者接口;
  • 易于擴展,后續添加渲染數據只需修改該結構,無需到處傳參。

雖然這種做法在結構清晰度上稍有折中,但從維護成本和代碼一致性的角度來看是合理的,因此我們決定保留現有機制。


總結來說:

  • 使用統一的臨時內存池代替多種內存類型傳遞,更簡潔高效;
  • 排序與裁剪流程內部直接從該內存池分配輸出數據;
  • 使用 BeginTemporaryMemory/EndTemporaryMemory 管理內存生命周期;
  • 保留 GameRenderCommands 作為統一的數據載體,避免接口碎片化;
  • 后續可繼續強化這種模式,提升系統靈活性與可維護性。
    在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

game_opengl.cpp:在 OpenGLRenderCommands() 中調用 GetSortedIndices()

我們設想如果要在這個位置進行渲染索引的排序(DrawIndices)處理,那么整個邏輯就會變得很清晰。

核心變化是:我們不再單獨去獲取 SortEntries 了,也就是說,不再在這個模塊里顯式調用 GetSortEntries,而是直接通過 SortDrawIndices 來獲取需要的渲染順序。這個排序處理會向前傳遞、穿過調用鏈,而不在每一層手動操作排序過程。

具體執行過程如下:

  • SortDrawIndices 會返回一個排好序的索引數組;
  • 然后我們直接使用這個數組的索引順序,遍歷其中的每一項進行繪制;
  • 這個排序索引數組是 uint32 類型的數組,其中每一個元素表示一個 entry 的下標;
  • 實際遍歷的時候,不再使用額外的中間結構來存儲 entry 本身,只需要用這些排好序的索引即可;
  • 每次渲染時,通過索引數組獲取目標 entry,然后進行相應操作;
  • 原來的 entry 數組下標不再直接參與渲染順序的邏輯,完全由排序后的索引數組控制流程。

這種做法帶來的好處:

  • 簡化了邏輯:不需要額外維護復雜的 SortEntry 結構;
  • 減少了內存使用:只用一個索引數組就能完成渲染排序;
  • 避免重復計算:排序操作在前置步驟中集中完成;
  • 更靈活:后續要調整排序策略,只需更改 SortDrawIndices 的實現,無需波及渲染過程;
  • 渲染流程變得更清晰:渲染順序和數據訪問分離,提高可讀性和可維護性。

總之,我們調整了結構,不再以結構體 SortEntry 為核心中介,而是讓排序階段只生成一個 uint32 數組作為索引參考。渲染階段僅依賴這個排序后的索引數組,從而讓整個流程變得更加線性、高效和清晰。
在這里插入圖片描述

在這里插入圖片描述

game_render.cpp:在 RenderCommandsToBitmap() 中調用 GetSortedIndices()

我們現在可以回到渲染邏輯中,做一模一樣的調整。因為本質上,這兩個渲染循環之間沒有本質區別,它們只是前端接口不同,背后執行的是同樣的操作:都要遍歷渲染命令并進行渲染。

這意味著,它們前半部分的邏輯幾乎完全一樣,唯一的區別在于循環內部實際執行的渲染方式不同。由于它們都依賴同一套渲染命令結構并按照順序執行操作,我們可以將處理流程統一,進一步簡化和標準化渲染系統的結構。

我們所做的改動是:用排好序的索引數組來代替原本的 entry 遍歷。

具體處理方式如下:

  • 在渲染模塊中也替換為使用排序后的 uint32 索引數組;
  • 不再顯式遍歷原始渲染命令數組,而是通過這個索引數組訪問對應的 entry
  • 我們將這些排序索引放入渲染命令結構中,使其可以在后續渲染階段統一訪問;
  • 將原來的排序邏輯封裝成一個函數 GetSortedIndices,它用于生成這個索引數組;
  • 渲染階段只關注一個結果:已排序的索引,然后按照這個順序讀取命令并執行;
  • 索引數組在 clip_rects 的基礎上生成,和之前我們對 SortEntries 的設想類似;
  • 這樣整個渲染流程變得統一、簡潔、易于維護。

最終,所有渲染系統只需關注如何根據排序后的索引執行命令,而無需重復進行排序或管理中間結構。排序與渲染邏輯徹底解耦,系統更具靈活性,適應性也更強。只需根據不同情況替換排序策略,不必改動渲染代碼,結構更加穩定可靠。
在這里插入圖片描述

game_platform.h:引入 game_render_prep 結構,并讓必要的函數使用它

我們現在需要做的一件事,是為“已排序的索引”提供存儲空間。這個需求之所以重要,是因為整個渲染過程可以劃分為兩個本質不同的階段:


第一階段:構建渲染命令

這個階段是我們在游戲層面創建渲染命令并交由平臺層處理。這個結構叫做“游戲渲染命令”(GameRenderCommands),它基本上運行良好,結構也算清晰。


第二階段:準備渲染數據

這個階段才是真正的“渲染準備”,在這里:

  • 渲染命令不再直接使用,而是被加工成更適合渲染管線的形式;
  • clip_rects 在這個階段才被線性化(linearized);
  • “已排序的索引”數組也是在這個階段生成;
  • 這些數據是臨時性的,在整個渲染階段結束后就會被清理。

結構問題與重構方向

目前我們把 clip_rects 硬塞進了渲染命令結構中,看起來不太自然。實際上,這些東西只在渲染準備階段才真正存在,不屬于游戲邏輯中的數據。為了更清晰地表達不同階段的職責,我們應該把這些“后處理生成的數據”移到一個新的結構中,比如叫 GameRenderPrep

這個 GameRenderPrep 用于承載所有與“渲染準備階段”相關的內容:

  • 線性化后的 clip_rects
  • 排序完成的 uint32 索引數組;
  • 所需的臨時內存。

這樣做的好處:

  1. 職責分離明確:渲染命令結構只關注游戲邏輯產生的數據,渲染準備結構關注從這些命令中加工出來的派生數據;
  2. 便于維護和理解:清晰地知道哪一部分是輸入,哪一部分是為渲染過程服務的中間態;
  3. 更符合系統設計原則:讓數據的生命周期更加清晰,避免混亂;
  4. 渲染接口更清晰:例如傳遞給 OpenGL 渲染器時,統一傳遞 GameRenderCommandsGameRenderPrep,渲染器從中提取必要的數據,無需再通過函數調用“偷”數據或全局訪問。

實際操作中的應用變更:

  1. 在 OpenGL 渲染部分,我們從 GameRenderPrep 中提取 sorted_indicesclip_rects
  2. Metal 渲染部分也采用完全相同的方式;
  3. 我們將原來的 GetSortedIndices 那些操作封裝為“渲染準備”階段的一部分,不再混雜在渲染命令處理流程中;
  4. 原來和排序有關的實現文件也不再適合當前用途,排序邏輯演變為圖遍歷(graph traversal)類型,建議將其遷移到新的、更合適的位置中;
  5. 不再使用那些 MergeSort, RadixSort 等原始排序邏輯,這些已不再服務于當前渲染結構。

總結:

我們現在通過引入 GameRenderPrep,將渲染數據預處理(如排序、裁剪矩形線性化等)從主渲染命令中剝離,形成清晰的兩階段渲染體系。這樣一來,渲染流程更整潔、邏輯更清晰、接口更統一,未來拓展、調試、優化都將更加方便。
在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

game_render.cpp:引入 PrepForRender() 函數

我們會創建一個新模塊,用來生成 GameRenderPrep 結構,這樣的設計更簡潔、更具擴展性。新的設計思路如下:


引入獨立的渲染準備模塊

我們會專門有一個函數或流程來生成 GameRenderPrep,這個函數的職責是:

  • 接收渲染命令 GameRenderCommands

  • 接收用于臨時分配的內存區 Arena

  • 構建出 GameRenderPrep,其中包含:

    • 已排序的繪制條目索引;
    • 線性化后的裁剪矩形;
    • 可能還有其他臨時性的渲染數據。

排序算法變得非常簡單

由于排序邏輯已經被簡化,不再依賴復雜的手寫排序算法,我們可以將排序代碼抽出,放入通用模塊(如 shared 或 utilities),方便共享與復用。這個排序功能已經足夠獨立,不再依賴渲染管線中其他細節,因此將其放到平臺無關的公共代碼中更加合理。


渲染平臺不再關心處理細節

我們讓每個平臺的渲染層(OpenGL、Metal、Vulkan 等)都只接收已經準備好的 GameRenderPrep

  • 平臺層不再負責排序、不再處理裁剪矩形轉換;
  • 只需調用通用的 PrepareForRender() 函數;
  • 渲染平臺只關注執行,不管背后數據如何構建;
  • 使渲染邏輯清晰分工,職責明確。

新的 PrepareForRender 函數

這個函數位于平臺無關模塊中,它非常簡單,主要做兩件事:

  1. 接收兩個參數:

    • 渲染命令 GameRenderCommands
    • 內存 arena;
  2. 內部完成一整套的渲染準備工作,包括:

    • 排序;
    • 生成索引;
    • 線性化 clip_rects
    • 構建 GameRenderPrep 對象。

最后,這個函數將 GameRenderPrep 返回給調用者,由調用者決定將其傳遞給哪個平臺渲染實現。


優點總結:

  • 平臺解耦:不同平臺都用同樣的入口函數 PrepareForRender(),不再處理準備細節;
  • 邏輯清晰:職責劃分明確,渲染命令構建與數據準備完全分開;
  • 更易維護:排序和準備工作封裝在一處,代碼集中易于修改;
  • 擴展靈活:未來添加更多的中間處理步驟也能統一放進 PrepareForRender 中處理;
  • 復用性高:通用的排序、裁剪轉換等邏輯可以跨平臺共享。

總的來說,我們的渲染準備系統實現了清晰的職責劃分,通過引入 GameRenderPrep,使平臺渲染邏輯解耦并簡化。整個渲染流程被拆分成清晰的階段:命令生成 → 渲染準備 → 平臺執行,讓系統更加健壯、可維護,也方便后續的調試與優化。
在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

win32_game.cpp:讓 WinMain() 不再處理 ClipMemory,而由 game_render.cpp 中的 LinearizeClipRects() 自行處理

我們決定重新組織線性化裁剪矩形(clip rects)的生成方式,以更簡潔、更高內聚的方式處理這部分邏輯。


移除平臺層對臨時內存的干預

過去平臺層需要了解裁剪矩形的數量、計算內存大小、手動分配一塊臨時內存,然后將其傳遞下來供裁剪處理函數使用。這種做法:

  • 增加了平臺層的復雜度;
  • 導致了職責混亂;
  • 不符合模塊封裝的設計原則。

現在我們完全取消平臺層對此流程的干預,所有相關邏輯集中在裁剪處理模塊內部。


在線性化裁剪時自動分配內存

我們將在 LinearizeClipRects 函數中自行處理所有必要的內存分配。這部分邏輯將被集中管理,包括:

  • 根據 first_rectlast_rect 確定所需裁剪矩形數量;
  • 使用內存 arena 分配目標數組空間;
  • 不再要求調用者提供目標內存,只需要提供 arena;
  • 返回包含所有線性化后裁剪矩形的數組引用(或結構體封裝)。

這讓調用變得更簡單、更清晰。


計算目標數組大小邏輯

我們使用一種可靠方式來計算需要多少 RenderEntryClipRect

count = (last_rect_index - first_rect_index + 1);

然后調用類似 PushArray(arena, count, RenderEntryClipRect) 的分配方式,把內存交給 arena 自動管理,不需要平臺層處理。


簡化調用流程

以前平臺層調用 GetTempSortMemoryAndClipMemory(...),然后再手動組織參數傳入裁剪處理函數。而現在我們只需要做:

clip_rects = LinearizeClipRects(commands, arena);

然后后續直接使用 clip_rects 即可。平臺層既不再關心內存大小,也不再關心裁剪矩形數量,只需要關心自己傳入 arena 即可。


邏輯職責劃分更明確

  • 平臺層:

    • 只關心準備好 arena;
    • 調用渲染準備函數;
    • 接收處理完的結構體。
  • 渲染準備函數:

    • 完整處理所有需要構建的中間結構,如:

      • 排序索引;
      • 裁剪矩形數組;
      • 其他需要線性化或緩存的數據。
  • 各渲染后端(如 OpenGL):

    • 使用渲染準備結構;
    • 不再執行排序或分配等準備邏輯;
    • 只關注執行。

優化后的結構和思路優勢

  • 移除冗余和重復代碼;
  • 提高模塊獨立性和復用性;
  • 避免錯誤分配或忘記釋放;
  • 更易維護和調試;
  • 符合高內聚低耦合的設計模式。

整體來說,這種優化重構不僅提升了代碼可讀性和可維護性,還讓平臺層更加干凈,渲染準備流程更加集中和統一,為后續擴展和平臺適配打下良好基礎。
在這里插入圖片描述

在這里插入圖片描述

強調擁有良好工具函數的重要性

我們對渲染準備流程進行了一次徹底清理和模塊劃分,目標是讓內存管理和職責分離變得更加明確和簡潔。以下是本次工作內容的詳細總結:


利用內存Arena構建更健壯的實用工具邏輯

我們強調了**內存Arena(Memory Arena)**這類實用工具功能的關鍵作用。雖然不是“類”概念,但這種工具提供了類似“標準操作集合”的功能,使代碼更易讀、更健壯、出錯更少。這類通用內存管理模塊的優勢包括:

  • 避免臨時手動計算內存需求;
  • 不需要到處傳遞“需要多少內存”之類的信息;
  • 所有資源的分配歸于統一系統管理,便于生命周期控制;
  • 提升可維護性與代碼清晰度。

渲染準備模塊中集成裁剪與排序的內存管理

渲染準備階段(PrepareRender)現在完全包辦了線性化裁剪矩形(ClipRects)和排序索引(Sorted Indices)的內存申請與構建:

  • 在函數內部完成所需的臨時內存計算;
  • 使用Arena分配內存,不再通過平臺層暴露裁剪數量;
  • 渲染準備結構中包含線性化后的數據;
  • 平臺層只需傳遞Arena和渲染命令,完全不需關心細節。

例如,之前通過平臺層調用如下代碼:

GetTempSortMemoryAndClipMemory(...)

現在可以改為:

clip_rects = LinearizeClipRects(commands, arena);

這大大簡化了調用邏輯。


對平臺層的影響

通過將所有與渲染有關的內存分配邏輯移出平臺層:

  • 平臺層無需了解裁剪矩形數量、排序數據格式等信息;
  • 移植平臺層變得更簡單,開發者無需了解渲染細節;
  • 代碼分層更清晰,降低系統耦合度;
  • 由于平臺層極有可能在多個系統間反復重寫(如 Windows、Linux、Mac、Raspberry Pi等),這種去耦合設計非常關鍵。

我們明確了一個設計理念:

平臺層應當盡可能“貧血”,不參與渲染核心邏輯。


渲染準備結構組織

渲染準備結構 RenderPrep 包含以下數據:

  • SortedIndices:已按繪制順序排列的索引;
  • LinearizedClipRects:從命令中提取并壓縮后的裁剪矩形集合;
  • 所有數據均由傳入的 MemoryArena 統一管理。

這一設計允許其他系統完全不關心底層如何生成這些數據,只需使用即可。


內存線性化優化

針對線性化部分:

  • 使用 PushArray 等工具函數,確保類型安全;
  • 自動根據 first_rectlast_rect 計算大小;
  • 避免平臺層誤用或漏掉內存分配。

最終生成類似:

RenderEntryClipRect *clip_rects = PushArray(arena, count, RenderEntryClipRect);

排序邏輯進一步簡化

排序也不再依賴外部傳入緩沖區或自定義排序函數:

  • 如果排序方式簡單(如合并排序或拓撲遍歷),可以放入共享模塊;
  • 渲染模塊只需給出排序結果,不暴露排序實現;
  • 后期我們還可以統一一個 SortEntries(RenderCommands, Arena) 方法,對外透明。

清理過時的臨時邏輯

我們徹底移除了舊的、分散的內存管理代碼,包括:

  • 手動傳遞排序內存、裁剪內存大小;
  • 各平臺分配 TempSortMemory 等臨時邏輯;
  • 大量的前向聲明、類型轉換代碼;
  • 雜亂的結構體字段,如過多與 SpriteBounds 相關字段。

最終效果

  • 模塊更清晰:職責分工明確,渲染邏輯歸渲染,平臺邏輯歸平臺;
  • 接口更簡潔:調用更自然,參數更少;
  • 易于維護和替換:適配不同平臺或重構某部分都不需要了解整個系統;
  • 系統更安全:由統一的內存管理系統控制所有臨時資源,避免內存泄露或混亂使用。

通過這次優化,渲染準備流程從繁雜分散轉變為集中高效,真正做到高內聚、低耦合,使代碼更加健壯、易維護并具備良好的可移植性。
在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

新建 game_render.h 并將 sprite 相關結構體放入其中

我們對渲染系統進行了結構性優化和清理,主要集中在內存管理、頭文件組織、代碼職責劃分、初始化流程規范化等方面,以下是詳細總結:


重構渲染模塊的頭文件與依賴關系

  • handmade_render.h 獨立為正式頭文件并開始在渲染模塊中引入;
  • RenderGroup 初始化邏輯中,加入 #include "handmade_render.h",確保所有渲染相關定義集中統一;
  • 清理掉冗余的或已廢棄的頭文件引用,簡化編譯依賴。

清理冗余與錯誤的代碼引用

  • 刪除了已經不存在的文件引用、無效的變量和宏;
  • 比如之前誤保存的無效內容或臨時代碼塊被徹底移除;
  • 明確了 RenderCommands 中哪些數據是長期保留的(如 ClipRectCount),哪些應該是臨時數據。

明確渲染準備結構(RenderPrep)傳遞邏輯

  • 渲染器需要準備渲染前的數據(如裁剪矩形、排序索引等),這些數據已經在 RenderPrep 結構中完成準備;
  • 在調用軟件渲染函數 SoftwareRenderCommands 時,直接傳入 RenderPrep* 指針;
  • 所有必要的輸出數據如索引數組、ClipRects 都從 Prep 中讀取,而不再依賴外部單獨傳參;
  • 所有初始化、排序、ClipRects 線性化等,統一使用內存 Arena 分配。

重建臨時內存Arena系統

  • 引入了統一的 MemoryArena 命名為 FrameArena,專門為每幀中所有臨時性資源分配服務;

  • 刪除舊的臨時分配邏輯如 SortMemoryClipMemory 等,徹底轉向 PushArray() 類工具分配;

  • 示例代碼:

    MemoryArena FrameArena;
    void *ArenaBase = PlatformAllocateMemory(64 * 1024 * 1024); // 64MB
    InitializeArena(&FrameArena, ArenaBase, 64 * 1024 * 1024);
    
  • 所有渲染前處理(如排序、裁剪)數據均從該 Arena 分配,生命周期與幀同步。


清理和重建排序索引分配邏輯

  • 刪除硬編碼的 SortMemory 管理;
  • 通過 PushArray(&Arena, count, uint32_t) 直接生成排序所需的索引數組;
  • 索引數量與渲染命令條目數量一致;
  • 所有操作均使用類型安全的 Arena 工具函數,無需手動轉換和校驗。

簡化并標準化初始化與釋放流程

  • 所有臨時資源僅需在一處初始化 Arena,所有子模塊共享使用;
  • 不再需要每個模塊手動指定排序緩沖區、裁剪緩沖區;
  • 平臺層只需提供一段內存并傳入 Arena,具體使用細節由渲染系統內部控制。

完善命名與邏輯表達

  • 明確變量命名,例如 result 用于存儲排序或裁剪處理后的結果;
  • 消除了命名歧義,使代碼結構更清晰、更容易跟蹤調試;
  • 所有數據結構組織邏輯更統一,避免了模塊間信息泄露。

進入調試準備階段

  • 由于大量重構操作,預期會出現編譯錯誤或功能邏輯缺失;
  • 暫時關閉一些調試邏輯(如排序順序校驗),確保系統先能正確運行;
  • 下一步將逐步修復編譯器提示的問題,并確認所有模塊間正確連接。

最終目標和方向

  • 渲染邏輯模塊化:所有數據準備邏輯集中于渲染模塊;
  • 平臺邏輯簡化:只需分配統一的大塊內存,完全不涉及渲染細節;
  • 內存管理集中:所有臨時資源集中使用 FrameArena 管理,生命周期清晰;
  • 可維護性提升:結構清晰,模塊獨立,易于移植與調試;
  • 接口清晰:平臺傳 Arena,渲染模塊自行決定使用方式,完全對外隱藏內部實現。

通過這一輪重構,渲染準備和內存管理體系得到了極大優化,整個系統從雜亂無章的臨時邏輯轉變為結構清晰、職責明確、高度可維護的架構,為后續支持多平臺渲染和高性能渲染打下了堅實基礎。
在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

未定義

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

運行游戲,命中 OpenGLRenderCommands() 中的斷言

當前遇到的問題是關于渲染命令中裁剪矩形(clip rect)索引越界的斷言失敗,具體表現為“clipped rect index 大于命令數量”,導致讀取了無效或垃圾數據。


詳細分析:

  • 斷言失敗表明索引超出了合法范圍,意味著在訪問裁剪矩形時,索引值遠超預期,比如出現了 560 這樣的非法數字,而有效的渲染類型數量遠遠少于此(僅有0到4共5種類型);
  • 由于索引越界,程序實際上讀取的是無效內存或者隨機數據,因此產生了錯誤或斷言;
  • 調試過程中發現,盡管索引為0且有多個條目存在,理論上不應出錯,但可能是條目0處本身沒有有效數據,導致訪問無效內容;
  • 進一步懷疑是在訪問渲染條目數據時,未正確初始化或傳遞數據,造成裁剪矩形索引沒有被正確設置,或者訪問順序錯誤。

可能的根源與方向:

  • 渲染命令數組或裁剪矩形數組可能存在內存污染,未正確清理或初始化,導致舊數據或隨機值殘留;
  • 對裁剪矩形索引和渲染命令數量的管理不嚴謹,沒有統一驗證機制防止越界訪問;
  • 需要檢查渲染命令準備階段對裁剪矩形索引的賦值過程,確保索引合法且不會超出命令數組范圍;
  • 應該添加更多的防御性編程措施,比如對索引范圍進行嚴格校驗,防止非法索引流入渲染流程;
  • 對渲染條目的有效性進行校驗,避免訪問空或無效條目導致錯誤。

結論:

當前遇到的斷言錯誤源于渲染命令中裁剪矩形索引越界,表現為讀取非法數據。需要對渲染命令生成和裁剪矩形索引的管理進行全面檢查,確保所有索引均在合法范圍內,并且渲染條目正確初始化,避免訪問無效數據。這是定位和解決問題的關鍵方向。
在這里插入圖片描述

在這里插入圖片描述

game_render.cpp:在 WalkSpriteGraph() 中添加斷言,確保 OutIndex - OutIndexArray == InputNodeCount,并修改 SortEntries() 遍歷節點并寫入索引

當前的重點是確認在處理圖形遍歷(graph walk)過程中,是否確實向輸出數組寫入了有效的數據。具體分析如下:


具體內容總結:

  • 之前在圖形遍歷和相關處理時,懷疑實際并沒有寫入任何數據到輸出索引數組中;
  • 為了驗證,添加了斷言來確保寫入的輸出數量與輸入節點數量相匹配,保證每個節點對應的索引都被正確記錄;
  • 如果寫入數量和節點數量不一致,說明圖形遍歷過程中未正確生成或輸出所有數據,存在遺漏;
  • 為了簡化問題,暫時繞過復雜的圖形排序邏輯,使用一個更簡單的循環,直接遍歷所有節點,將它們對應的索引直接復制到輸出數組;
  • 這樣做的目的是驗證基礎的寫入機制是否正常,即使沒有復雜排序邏輯,也能保證索引正確輸出;
  • 簡單循環的作用是快速排查問題,確保數據生成和寫入流程的基本正確性,然后再逐步恢復更復雜的處理邏輯。

結論:在這里插入圖片描述

當前通過添加斷言和使用簡化循環確認,驗證圖形遍歷階段是否向輸出數組寫入了正確且完整的數據,確保基礎流程的有效性。這是排查圖形索引輸出異常的關鍵步驟。
在這里插入圖片描述

運行游戲,看到一切流程正常,沒有崩潰

整體流程現在運行穩定,沒有崩潰,這正是想要確認的情況。做了許多改動之后,檢查了提交的代碼量,感覺合理,沒有出現異常增長或者不受控的資源占用問題。因為還沒有實現動態內存申請,所以不太可能出現內存無節制增長的情況。總體來看,系統保持了穩定性,沒有出現明顯的異常或錯誤。

game_render.cpp:修正 RecursiveFromToBack() 中未正確遞增 OutIndex 的問題

現在回過頭來看代碼,發現一些地方處理得不正確。很快就會發現,我們要么觸發斷言失敗,要么寫出了錯誤的值。事實上,問題確實出在這里,寫出的索引并不是我們真正需要的那個。顯然,我們想要的索引應該是另一個值。這里變量命名也有些不太合適,比如用“in next”這個名字其實不太準確,但也只能這樣用了。總體來說,就是索引的邏輯需要調整,不能用當前這個錯誤的值。
在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

運行游戲,再次命中 BuildSpriteGraph() 中的斷言

總之,在這個情況下,我們斷言flags為零,但實際上flags并沒有在上游被清理干凈。因此,需要確保flags在上游階段確實被清零,避免后續邏輯出現異常。
在這里插入圖片描述

game_render.cpp:講解 RecursiveFromToBack() 的具體工作原理

我們遇到了兩個“索引”的概念混淆問題。第一種是普通的索引,比如對sprite(精靈)數組中的元素編號,像第一個是0,第二個是1,依此類推。這是我們用來排序的數組索引。第二種則是偏移量,指的是數據在push buffer(推送緩沖區)中的位置。渲染系統并不能用普通的索引來定位數據,而是需要用這個偏移量來找到實際數據的位置。

之前寫代碼時,把這個偏移量當成了普通索引來寫入,導致渲染時取到的是無效的內存區域,引發了問題。這個問題雖然基本,但因為編程中涉及的概念多且復雜,容易讓人混淆。

為了解決這個問題,我們打算把這個“索引”變量改名為“offset”,明確它是指緩沖區中的偏移量,而不是數組的索引。同時,在寫入這些條目時,也要注意正確區分和使用索引與偏移量。這樣做可以幫助理清代碼邏輯,避免后續類似混淆帶來的錯誤。
在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

game_render_group.cpp:在 PushRenderElement_() 中清除 Flags

我們確認需要將flags設置為零,但目前flags并沒有被正確地清零。除了flags,我們還發現“first edge with me as front”(這里指的應該是某個標志位或狀態變量)也沒有被清理,而這個值目前我們還不清楚它具體應該是什么,清理起來比較麻煩。雖然我們不確定是否必須清理它,但從安全和穩定的角度來看,清理應該是必要的。

為了避免潛在問題,在渲染排序階段遍歷數據時,除了斷言flags為零,我們還需要檢查“first edge with me as front”是否也被清理為零。雖然有些情況下它可能不一定是零,但至少我們可以在關鍵點做一個斷言或者投射(project),確保它的狀態符合預期。

總的來說,當前的任務包括確保所有相關的標志和狀態變量都被適當清理,避免舊數據殘留導致渲染邏輯出現錯誤,這樣能夠提升代碼的健壯性和穩定性。
在這里插入圖片描述

運行游戲,確認排序未生效,并思考如何修復

現在的問題是,我們雖然完成了大部分需要做的工作,但排序功能還無法正常工作。即使沒有程序上的錯誤,排序依然不起作用,原因是屏幕區域(screen area)沒有被正確設置。

當前的屏幕區域數據是垃圾數據,完全沒有初始化或賦值,因此任何關于矩形相交的判斷都會失敗或者變得隨機。排序算法依賴于這些屏幕區域的邊界來判斷元素間的相對關系,如果邊界信息不準確,排序結果自然也會出現錯誤,導致渲染異常。這是一個非常重要的細節,需要特別注意。

在執行推送渲染元素(push render element)的時候,程序中有幾個地方負責生成這些屏幕區域的邊界矩形。但是目前這些地方沒有正確調用,導致屏幕區域沒有被正確賦值。我們必須確保在這些位置正確生成并設置屏幕區域的矩形信息,以便排序算法能夠基于準確的邊界進行工作。

其中比較簡單的是“清屏操作”(clear)和“位圖”(bitmap)對應的矩形,因為它們本身就是矩形形狀,計算屏幕區域邊界較為直接。但是更復雜的地方是大地圖(big map)和帶有傾斜變換(shearing)等變換的元素,這就需要計算變換后的所有頂點坐標,從而得到準確的屏幕空間包圍盒。這個過程相對復雜,需要檢查所有頂點并計算出正確的邊界矩形。

此外,還有一些細節,比如當前的清屏顏色操作,是否應該在所有排序圖里執行,這可能是多余的,也可以后續優化。

總之,接下來重點是實現和完善屏幕區域的計算和賦值,保證排序功能能基于真實有效的屏幕邊界正確運行。這個任務相對復雜,需要細致處理頂點變換和邊界計算,準備在后續時間里專門解決。
在這里插入圖片描述

問答環節

是否最終游戲將使用正交投影?

討論確認最終游戲采用正交投影視角,游戲整體是俯視角度的。提到目前還在處理精靈相關的問題,一旦精靈部分處理好,接下來將專注于游戲的資源包(art pack)。整體期待完成當前工作后能夠順利推進到后續內容。

在預分配緩沖區并計算偏移時,是否容易遇到指針錯誤?是否經驗越多越不成問題?

當計算預分配緩沖區位置和偏移時,指針相關的錯誤是否難以追蹤,或者是否因為經常使用而不成問題?

對此的回答是,雖然編程多年,對指針操作已經習慣了,但有時仍會遇到難以定位的錯誤。剛開始接觸指針時確實比較困難,記憶中那時適應指針操作很不容易。隨著經驗積累,出現指針相關難以發現的bug變少了,因為經常使用,對指針的理解和操作變得熟練。

即使編程三十多年,有時仍會遇到非常難排查的bug,可能花費幾天時間定位。其他資深程序員也有類似經歷。那些難找的bug通常不是簡單的指針錯誤,而是源于對程序狀態的誤解,比如數學計算沒直觀可視化,或假設了某種情況卻實際上是另一種情況,導致錯誤難以察覺。簡單來說,大多數難找的bug源于認知偏差,即認為代碼做的是X,但實際執行的是Y。

不論是指針算術、數學運算、字符串拼接還是解析,只要代碼復雜且理解有偏差,定位bug都很困難。即使是經驗豐富的程序員,尋找自己沒預料到的bug也不容易。很多時候,真正的問題在于心智模型出了錯,沒有做足夠的測試來驗證假設,導致錯誤一直沒被發現。

針對指針算術,雖然它會導致內存崩潰或數據破壞,但通常能比較快定位,因為崩潰會立即表現出來,可以通過斷點監視數據寫入找到原因。問題復雜的是那些并非真正指針算術錯誤,但由于某些鏈條上的有效操作最終導致錯誤的情況,這種錯誤排查就更費時費力。

另外,編程復雜度不僅僅是指針操作的專利,像JavaScript這種不直接使用指針的語言,也能寫出極其復雜難懂且難調試的代碼。指針并不比其他編程語言特性更難理解或更難調試,害怕指針多半是心理障礙。

程序中的bug不論是否因指針導致,都是需要解決的問題。崩潰bug雖嚇人,但邏輯錯誤、性能問題或數據錯誤同樣嚴重,甚至更糟。指針引發的崩潰只是眾多問題之一,其他問題往往也有很大影響。

總結來看,指針只是編程中眾多復雜因素之一,不必過度恐懼。編程復雜的東西很多,很多非指針特性也同樣讓人頭疼。真正重要的是建立正確的心智模型,細致驗證假設,良好設計和測試代碼,這樣才能有效減少難找的bug。

除了用于調試函數耗時的代碼,是否還有用于調試 memory arena 使用量的代碼?

目前確實存在用于調試函數執行時間的代碼,但還沒有用于監控內存使用情況的 Java 代碼。雖然這方面的功能還未實現,但確實有必要編寫相關工具。調試系統的開發已經耗費了相當多的時間和精力,因此沒有覆蓋所有潛在的調試需求,比如內存監測這一塊。盡管如此,將來還是應該補充這個功能,因為它非常有用。只是現在為止,還沒有寫相關的實現。

目前調試部分的工作重心主要放在執行時間和功能正確性上,因此沒有深入實現所有可能的調試工具。雖然投入了大量時間進行調試系統的開發,但還是有一些可以補充的內容,比如內存監控代碼,這也是之后值得添加的一個功能點。當前,如果需要監控內存使用,只能依賴其他外部工具或者系統級監視手段,而不是項目內部自帶的功能模塊。

回憶 Rainbow 100 和 Turbo Pascal 的使用經歷

我們最早使用的是一臺 DEC Rainbow 計算機,那是一臺非常有趣的電腦,當時用的是某種 BASIC 語言進行編程。雖然不確定是哪種 BASIC,但由于那臺電腦是從家里帶回來的,很有可能是出廠預裝的,因此很可能是 Microsoft BASIC。那是我們最開始學習編程時所用的語言,在那臺機器上也沒有學過其他編程語言。

在學習完 BASIC 之后,我們轉而開始學習 Pascal,但那已經是在另一臺計算機上了。當時家里之所以有這些電腦,是因為父親的工作單位配有電腦,可以帶回家使用。他在 Digital Equipment Corporation(DEC)工作,我們使用的電腦就是從那里帶回來的。DEC Rainbow 最終被歸還或換掉了,之后使用的應該是另一種機型,例如 VAXstation 或者某種基于 VAX 架構的機器。

之后使用的計算機可能是 VAXstation 26 系列之類的設備,記得處理器是 VAX-11/26,也記得大概是配了 8MB 內存。這臺機器上我們開始學習 Pascal 編程語言。不過,具體使用的是哪一個版本的 Pascal 已經不太記得了。雖然最常見的 Pascal 編譯器是 Turbo Pascal,但我們印象中使用的版本似乎并不是 Turbo Pascal,可能是另一個 Pascal 變種。

當時最大的遺憾之一就是沒有匯編語言的參考資料,也不知道如何編寫匯編程序。我們從未擁有過相關的文檔,也不知道如何操作硬件。因此在那段時間只能使用 Pascal 編程,而 Pascal 在這些電腦上很難做出高性能的圖形程序。即便機器性能還不錯,比如 VAX-11/26 搭配匯編語言是完全可以實現很多圖形操作的,但我們當時缺乏這方面的知識,導致只能做一些效率低、效果不理想的小程序。

后來回顧起來,我們一直很遺憾在那個階段沒能掌握匯編語言,也沒有接觸相關資源,否則在那些設備上完全可以做出更復雜和高效的程序。

最后也試圖回憶當時使用的 Turbo Pascal 是什么樣子,記憶中和后來熟知的彩色版本界面不太一樣,可能因為使用的是單色顯示器。最終在看到單色版本的 Turbo Pascal 截圖時,才覺得更加熟悉和接近當時的實際使用體驗。回憶至此,也就準備結束當天的使用。

是否編程過 Amiga 系統?

我們后來確實在 Amiga 上進行過編程,不過那是很久之后的事情了,應該是在 1987 年或 1988 年才開始接觸 Amiga。當時并不是一開始就使用 Amiga 進行編程,而是在接觸其他計算機系統之后才轉向它。

雖然具體是哪一年開始已經記不太清楚,但當時 Amiga 的出現確實引起了我們的關注。Amiga 在圖形和聲音處理方面具有相當強的能力,吸引了不少熱衷于圖形編程和多媒體開發的使用者。正是這種強大的硬件能力,使得我們后來也開始在這個平臺上嘗試進行一些開發工作。雖然最初主要是在其他平臺上使用 Pascal 和 BASIC,但到了接觸 Amiga 的時候,可能就更多開始涉及底層或多媒體相關的內容了。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/80905.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/80905.shtml
英文地址,請注明出處:http://en.pswp.cn/web/80905.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

PHP-FPM 調優配置建議

1、動態模式 pm dynamic; 最大子進程數(根據服務器內存調整) pm.max_children 100 //每個PHP-FPM進程大約占用30-50MB內存(ThinkPHP框架本身有一定內存開銷)安全值:8GB內存 / 50MB ≈ 160,保守設置為100 ; 啟動時創建的進程數&…

騰訊2025年校招筆試真題手撕(一)

一、題目 有n 把鑰匙,m 個鎖,每把鎖只能由一把特定的鑰匙打開,其他鑰匙都無法打開。一把鑰匙可能可以打開多把鎖,鑰匙也可以重復使用。 對于任意一把鎖來說,打開它的鑰匙是哪一把是等概率的。但你無法事先知道是哪一把…

【北郵通信系統建模與仿真simulink筆記】(2)2.3搭建仿真模型模塊操作運行仿真

【聲明】 本博客僅用于記錄博主學習內容、分享筆記經驗,不得用作其他非學術、非正規用途,不得商用。本聲明對本博客永久生效,若違反聲明所導致的一切后果,本博客均不負責。 目錄 【聲明】 一、搭建第一個仿真模型 二、模塊操作…

系統與賬戶安全

SYS-01:Windows的賬戶安全 安全配置核心原則: 強密碼策略: 通過組策略設置密碼復雜度: # 啟用密碼復雜度要求 secedit /export /cfg secpolicy.inf # 修改文件中的 "PasswordComplexity 1" secedit /configure /db …

COMPUTEX 2025 | 廣和通5G AI MiFi解決方案助力移動寬帶終端邁向AI新未來

隨著5G與AI不斷融合,穩定高速、智能的移動網絡已成為商務、旅行、戶外作業等場景的剛需。廣和通5G AI MiFi方案憑借領先技術與創新設計,重新定義5G移動網絡體驗。 廣和通5G AI MiFi 方案搭載高通 4nm制程QCM4490平臺,融合手機級超低功耗技術…

免費開放試乘體驗!蘇州金龍自動駕駛巴士即將上線陽澄數谷

近日,蘇州自動駕駛巴士線路——陽澄數谷示范線正式上線,即日起向全民免費開放試乘體驗! 在蘇州工業園區地鐵3號線倪浜?陽澄數谷站外,一輛輛黑、白配色的小巴正在道路上有條不紊地行駛。與普通公交不同的是,小巴造型奇…

嵌入式軟件架構規范之 - 分層設計

一、規范的核心思想:驅動文件的“獨立性”與“復用性” 該規范的本質是通過分層隔離,實現驅動代碼的高復用性、低耦合性,確保驅動模塊僅關注“硬件操作邏輯”,不依賴上層業務或下層硬件接口的具體實現細節。其核心要求包括&#…

PyQt5繪圖全攻略:QPainter、QPen、QBrush與QPixmap詳解

摘要:掌握PyQt5繪圖核心控件,輕松實現窗體繪圖、文字渲染、幾何圖形繪制及圖像加載。本文附帶完整代碼示例與效果圖,助你快速上手GUI圖形開發。 繪圖基礎:為什么需要這些控件? 在GUI開發中,繪圖功能是數據…

C++學習:六個月從基礎到就業——多線程編程:std::thread基礎

C學習:六個月從基礎到就業——多線程編程:std::thread基礎 本文是我C學習之旅系列的第五十四篇技術文章,也是第四階段"并發與高級主題"的第一篇,介紹C11引入的多線程編程基礎知識。查看完整系列目錄了解更多內容。 引言…

【計算機網絡】TCP如何保障傳輸可靠性_筆記

文章目錄 一、傳輸可靠性的6方面保障二、分段機制三、超時重傳機制四、流量控制五、擁塞控制 提示:以下是本篇文章正文內容,下面案例可供參考 源網站 按TCP/IP 4層體系,TCP位于傳輸層,為應用層提供服務 一、傳輸可靠性的6方面保障…

2025年保姆級教程:Powershell命令補全、主題美化、文件夾美化及Git擴展

文章目錄 1. 美化 Powershell 緣起2. 安裝 oh-my-posh 和 posh-git3. 安裝文件夾美化主題【可選】 1. 美化 Powershell 緣起 背景:用了 N 年的 Windows 系統突然覺得命令行實在太難用了,沒有補全功能、界面也不美觀。所以,我決定改變它。但是…

基于Mongodb的分布式文件存儲實現

分布式文件存儲的方案有很多,今天分享一個基于mongodb數據庫來實現文件的存儲,mongodb支持分布式部署,以此來實現文件的分布式存儲。 基于 MongoDB GridFS 的分布式文件存儲實現:從原理到實戰 一、引言 當系統存在大量的圖片、…

【Linux】Linux安裝并配置Redis

目錄 1.安裝 2.啟動服務 3.配置 3.1.綁定地址 3.2.保護模式 3.3.持久化選項 3.3.1.RDB 持久化 3.3.2.AOF 持久化 3.3.3.如何選擇 1.安裝 Redis 可以從默認的 CentOS 軟件倉庫中安裝。運行以下命令來安裝 Redis sudo dnf install redis -y 響應如下 2.啟動服務 安裝完成后&…

python-數據可視化(大數據、數據分析、可視化圖像、HTML頁面)

通過 Python 讀取 XLS 、CSV文件中的數據,對數據進行處理,然后生成包含柱狀圖、扇形圖和折線圖的 HTML 報告。這個方案使用了 pandas 處理數據,matplotlib 生成圖表,并將圖表嵌入到 HTML 頁面中。 1.XSL文件生成可視化圖像、生成h…

黑馬點評相關知識總結

黑馬點評的項目總結 主要就黑馬點評項目里面的一些比較重要部分的一次總結,方便以后做復習。 基于Session實現短信登錄 短信驗證碼登錄 這部分使用常規的session來存儲用戶的登錄狀態,其中短信發送采取邏輯形式,并不配置云服務驗證碼功能。…

手搓四人麻將程序

一、麻將牌的表示 在麻將游戲中,總共有一百四十四張牌,這些牌被分為多個類別,每個類別又包含了不同的牌型。具體來說,麻將牌主要包括序數牌、字牌和花牌三大類。序數牌中,包含有萬子、條子和筒子,每種花色…

【Java高階面經:數據庫篇】17、分庫分表分頁查詢優化:告別慢查詢與內存爆炸

一、分庫分表基礎:策略與中間件形態 1.1 分庫分表核心策略 分庫分表是應對海量數據存儲和高并發訪問的關鍵架構設計,其核心在于將數據分散到不同的數據庫或表中,以突破單庫單表的性能限制。常見的分庫分表策略包括: 1.1.1 哈希…

貪心算法之跳躍游戲問題

問題背景 本文背景是leetcode的一道經典題目:跳躍游戲,描述如下: 給定一個非負整數數組 nums,初始位于數組的第一個位置(下標0)。數組中的每個元素表示在該位置可以跳躍的最大長度。判斷是否能夠到達最后…

Label Studio:開源標注神器

目錄 一、Label Studio 是什么? 二、核心功能大揭秘 2.1 多類型數據全兼容 2.2 個性化定制隨心配 2.3 團隊協作超給力 2.4 機器學習巧集成 三、上手實操超簡單 3.1 安裝部署不頭疼 3.1.1 Docker安裝 3.1.2 pip安裝 3.1.3 Anaconda安裝 3.2 快速開啟標注…

創建信任所有證書的HttpClient:Java 實現 HTTPS 接口調用,等效于curl -k

在 Java 生態中,HttpClient 和 Feign 都是調用第三方接口的常用工具,但它們的定位、設計理念和使用場景有顯著差異。以下是詳細對比: DIFF1. 定位與抽象層級 特性HttpClientFeign層級底層 HTTP 客戶端庫(處理原始請求/響應&#…