倉庫
https://gitee.com/mrxiao_com/2d_game_6
https://gitee.com/mrxiao_com/2d_game_5
回顧并為今天設定階段
目前的開發工作主要回到了圖形渲染相關的部分。我們之前寫了自己的軟件渲染器,這個渲染器性能意外地好,甚至可以以相對不錯的幀率運行過場動畫。因此在很長一段時間里,我們根本沒有考慮使用硬件加速,也沒有感到迫切的需要。
不過隨著游戲功能逐步完善,特別是在進入“游戲調優”階段時,我們意識到要讓游戲穩定地以 60 幀每秒運行,就不能總是依賴對軟件渲染器進行微調和優化。與其不停優化每一幀渲染效率,還不如直接利用硬件加速。所以我們開始實現 OpenGL 的接口,以便啟用硬件加速渲染。
OpenGL 的整合工作本身其實并不復雜,為我們的目的所需的部分非常簡單,幾乎不需要多少額外的工作量。到目前為止,大部分關于 OpenGL 的基礎支持已經搭建完成,整個流程可以正常運行。但有一部分還沒有完成,那就是**紋理下載(Texture Downloads)**的部分。
當前的紋理處理邏輯基本上還是占位符,也就是說:
- 紋理下載并不是一個正式的流程,也沒有被系統化地集成到渲染流程中。
- 紋理數據的上傳仍然是靜態的或是臨時實現,缺乏真正意義上的動態紋理加載。
為了讓整個渲染流程更加健全,我們需要把紋理下載作為一個一等公民納入整個渲染管線。這包括明確何時、如何將圖像數據傳輸到 GPU,以及如何異步處理這些紋理加載操作,以免影響主線程的渲染效率。
接下來的開發方向會先從這個紋理部分入手。為了便于理解,會通過黑板或繪圖的方式說明紋理處理的整體邏輯,并梳理出目前存在的不足。同時還計劃在今天把 RGB 的讀取功能整理完善,這一塊工作相對簡單,應該不會耗費太久的時間。剩下的大部分精力將用于紋理系統的完善。
整個目標是:讓紋理加載成為正式、穩定且高效的一部分,為后續的圖形表現、資源管理打好基礎。完成這一部分后,渲染性能和靈活性都會有顯著提升,能更好地支撐復雜場景的繪制和更高頻率的畫面更新。
黑板:紋理下載
在目前的圖形處理架構中,存在一個非常重要但容易被忽略的核心問題:CPU 內存與 GPU 內存是完全分離的。尤其是在使用獨立顯卡的系統中,GPU 擁有自己的專用顯存,這些顯存芯片物理上就與 CPU 的內存分開,完全不共享。因此,在渲染流程中,我們必須明確處理數據在 CPU 和 GPU 之間的傳輸問題。
舉個例子,我們從磁盤中讀取紋理數據后,它首先是被加載進了 CPU 的內存中。這些數據雖然此時在內存中已經存在,但對 GPU 來說是不可見的,它無法直接訪問 CPU 內存。因此,我們必須有一個過程——將紋理從 CPU 內存傳輸到 GPU 內存中,這樣 GPU 才能使用這些紋理進行渲染。
這就引出了一個重要概念:紋理的上傳(Texture Upload)過程必須是明確的、可控的。不能每一幀都重新把紋理從 CPU 上傳到 GPU,這會帶來巨大的性能瓶頸:
- 一張未壓縮的紋理圖像,比如用于某個過場動畫的幀圖像,可能會達到 6MB。
- 如果每幀都重復上傳這些大體積紋理,會導致顯卡帶寬被嚴重占用。
- 帶寬被耗盡的結果就是我們每幀能使用的紋理總量會受到限制,直接影響渲染質量和性能。
- 真正瓶頸將不再是 GPU 顯存容量,而是 CPU 與 GPU 之間的傳輸速度。
因此,在 GPU 編程中必須引入某種程度的資源保留機制(Retained Mode),也就是我們不能每次都“即時傳送”紋理,而應該是:
- 在資源首次加載時,將其完整上傳至 GPU;
- 之后每幀只需在 GPU 中引用已經存在的紋理資源;
- 只有當紋理資源發生變化或者被替換時,才重新上傳。
這種模式不僅可以大幅減少數據傳輸量,還可以充分利用 GPU 的高速顯存,讓渲染系統穩定運行在高性能狀態。
這一部分的工作,就是在為后續紋理系統做鋪墊,目標是:
- 建立起紋理從磁盤到 GPU 的完整傳輸鏈;
- 實現一次性上傳、多次復用的機制;
- 避免重復上傳導致的性能損耗。
只有完成了這些基礎工作,我們的渲染系統才能真正進入高效運行狀態,并為更多圖像特效和復雜畫面打下堅實的基礎。
黑板:將 GPU 內存看作緩存
GPU 內存在本質上可以被看作是一種緩存機制,類似于 CPU 使用的緩存(Cache)。當 GPU 進行渲染操作時,理想的情況是所有需要使用的紋理都已經存在于 GPU 的專用顯存中,這樣可以在渲染過程中以極快的速度直接訪問這些數據,避免從主內存中重新讀取,提升性能。
但這并不意味著所有游戲紋理都可以常駐于 GPU 內存中。原因很簡單:資源可能遠遠超過 GPU 所能容納的容量。例如,我們可能擁有 2GB 的紋理資源,而 GPU 的顯存只有 1GB。這就需要一種策略來動態管理紋理——決定哪些紋理在當前時間段是必須的,應該保留在 GPU 內存中,而不需要的可以被替換出去。
這種行為就像緩存系統的管理方式一樣:
- 我們需要有策略判斷當前時間窗口(比如當前幀和接下來幾十幀)內將要用到哪些紋理。
- 我們需要確保這些紋理提前上傳到 GPU,避免在渲染關鍵路徑上出現延遲或卡頓。
- 當內存不夠時,我們需要主動地從 GPU 顯存中移除不再需要的紋理,為新的紋理騰出空間。
雖然在某種程度上,GPU 驅動會自動進行這類紋理的“緩存管理”(即虛擬化)——比如我們分配了超過顯存限制的紋理,驅動會自動根據使用情況進行換入換出。但是,如果完全依賴驅動這種自動管理,就會帶來一個嚴重的問題:性能不確定。
GPU 驅動會在我們毫不知情的情況下突然移除某個紋理,然后又嘗試加載另一個,從而導致幀率下降、卡頓等問題。特別是在對性能要求極高的場景(例如固定 60 幀每秒的實時渲染),這樣的行為是不可接受的。
所以我們必須:
- 主動管理 GPU 內存中的紋理資源,避免讓驅動做出臨時的“替換決策”;
- 避免過度分配顯存,即不要一次性將過多紋理綁定到 GPU 中;
- 精確控制上傳的時機,在紋理被真正需要之前就完成上傳。
然而,問題在于,顯卡廠商和驅動并不會完全公開 GPU 內部如何存儲和使用紋理的格式。因此,驅動在管理這些紋理時實際上擁有更多底層細節和優化空間。這意味著:
- 驅動自己做的換入換出,在某些情況下反而比我們手動管理要高效;
- 我們不能精準控制紋理在 GPU 內部的實際格式(比如是否壓縮、怎么排列等);
- 這限制了我們“完全掌控”紋理生命周期的能力。
綜上,我們必須在兩者之間尋找平衡:
- 在我們能控制的范圍內,盡量提前判斷紋理使用情況,合理調度資源;
- 在無法規避驅動自動管理的部分,理解其機制,并盡量規避其帶來的性能抖動;
- 建立一套紋理資源管理系統,模擬緩存行為,對紋理的生命周期進行主動控制,以實現更可控的性能表現。
黑板:紋理交換
所謂的 “swizzling”(混排或重新排列),我們曾經用這個術語來描述在處理圖形編程時對數據的重組操作。本質上,它指的是將某個數據結構中的元素順序重新排列,以滿足某種計算或存儲需求。
舉個例子,在 SIMD(單指令多數據)編程中,通常有多個“通道”或“通路”來并行處理數據,比如 4 個通道(lane),分別存放了 a、b、c、d 四個值。此時,如果想要改變這些通道的順序,使得新的排列變為 c、b、a、d,這個操作就稱為一次 swizzle。
這類操作在圖形編程和 GPU 著色器中非常常見,原因包括:
- 格式對齊:在渲染或紋理采樣中,不同的數據格式可能對元素順序有要求,swizzling 可以幫助滿足這些硬件要求;
- 優化計算:有些計算或指令在特定排列下執行效率更高,通過 swizzling 可以提升 SIMD 運算效率;
- 資源重用:某些情況下,只需要對現有通道做輕微重新組合,而不是重新加載新數據,可以節省內存帶寬;
- 邏輯編排:有時為了簡化計算邏輯(比如顏色通道順序轉換,從 RGBA 轉為 BGRA 等),也會使用 swizzling。
簡單來說,swizzling 就像是一種數據“調位”機制,我們可以自由地指定通道的讀取順序,比如將原本 a、b、c、d 的排列變成 c、b、a、d。這個操作雖然看似簡單,但在底層圖形運算中極其重要,因為它直接影響數據的訪問方式和運算效率。
在計算機圖形學中,swizzle(混排)是一類對向量進行分量重排的操作。Swizzle 可以將一個向量的分量重新排列,從而生成新的向量。這種操作不僅可以對分量位置進行變換,還可以實現向量維度的轉換,例如從一個三維向量生成一個二維向量或五維向量,只需重復或選取原向量中的部分分量即可。
舉個例子:
- 假設有一個四維向量 A = {1, 2, 3, 4},其分量分別為 x、y、z、w;
- 可以進行 swizzle 操作,比如 A.wwxy,結果是 {4, 4, 1, 2};
- 還可以生成不同維度的向量,如 A.wx 得到二維向量 {4, 1},或 A.xyzwx 得到五維向量 {1, 2, 3, 4, 1}。
Swizzle 也可以結合多個向量使用,通過這種方式可以快速組合、提取或重新組織向量數據。在 GPGPU(通用圖形處理器計算)應用中,這類操作非常常見,因為它們允許更高效地利用 GPU 的并行能力。
從線性代數的角度來看,swizzle 實際上相當于用一個特定構造的矩陣去左乘一個列向量,這個矩陣的每一行是一個標準基向量(standard basis vector),表示了目標向量中每個分量從原始向量中“抽取”的位置。例如,如果 A = (1, 2, 3, 4)^T,那么執行 A.wwxy 操作,相當于將 A 左乘一個這樣的矩陣:
[ 0 0 0 1 ] // 取 w -> 4
[ 0 0 0 1 ] // 再次取 w -> 4
[ 1 0 0 0 ] // 取 x -> 1
[ 0 1 0 0 ] // 取 y -> 2
這樣就得到結果向量 B = {4, 4, 1, 2}。
Swizzle 是 GPU 編程中的一個核心機制,尤其是在著色器(shader)中,經常用于處理向量數據、優化內存訪問和提高計算效率。
在圖形處理中,“swizzling”(混排)不僅應用于向量,同樣也會應用在紋理處理中,尤其是在紋理映射的場景里。紋理的 swizzling 主要是為了提升內存訪問的效率和緩存一致性,以適應圖形硬件對三角形紋理采樣的高性能要求。
我們原本以為紋理在內存中是線性排列的,比如一個 2D 圖像,第一行的像素排在內存前面,然后是第二行,依此類推。但實際上,為了提高效率,GPU 會對紋理進行重排,讓它在內存中以更適合采樣訪問的方式布局。
以下是核心內容的詳細總結:
Swizzling 的基本思想
- 本質是對數據在內存中的布局進行重新排列,原本是一行一行存儲像素,現在可能是小塊為單位(如 4x4)進行分塊排列。
- 這種分塊方式提高了緩存命中率,因為在三角形紋理采樣中,我們往往只訪問紋理中的小片區域,而非整行整列。
- 舉個例子:
- 原本順序是:一整行16個像素
- 重排后順序是:多個小 4x4 區塊,按塊為單位排列
- 當采樣一個小塊區域時(比如貼圖到三角形),GPU 一次性就能獲取這個塊里的全部像素,提高緩存利用率。
為什么需要對紋理做 Swizzle
- 在現代 GPU 渲染中,紋理通常不是整體用完,而是從大紋理圖中抽取小塊映射到三角形表面。
- 如果紋理以行優先存儲,每次從圖中間某小區域讀取數據,會導致緩存不連續、效率低下。
- 為了解決這個問題,GPU 將紋理重新排布為塊狀布局,讓鄰近像素在內存中也盡可能相鄰,提高讀取效率。
實際的 swizzle 行為細節
- 我們提交紋理到 OpenGL 時,驅動會自動執行 swizzle 操作。
- 有時這個操作是在顯卡上完成的;
- 有時如果顯卡不支持,CPU 會代替完成 swizzling,但代價就是性能開銷很大,需要手動訪問并重新安排整個紋理內存。
- 一旦紋理被 swizzle 后,即便還沒傳給 GPU,也可能會以 swizzle 后的格式保留在內存中,加快后續傳輸。
- OpenGL 沒有對 swizzle 的格式進行標準化:
- 我們無法手動在 CPU 側進行 swizzle 并直接提交 GPU;
- 因為我們并不知道具體應該怎么排布才能匹配 GPU 的要求;
- 不同的 GPU 廠商(如 NVIDIA、AMD)甚至不同型號的 GPU,可能采用不同的 swizzle 格式。
局限性和問題
- 雖然我們希望可以避免系統自動 swizzle(因為不可控、慢),但實際卻必須依賴它。
- 原因是:沒有官方公開的 swizzle 格式文檔,無法預先手動完成 swizzle 并跳過這一步。
- 這意味著我們在進行紋理上傳時,仍要接受不確定的性能開銷。
總結
- GPU 為了提升三角形紋理采樣性能,會對紋理做內存重排(swizzle),以優化內存訪問和緩存效率;
- 重排后的紋理不是按行線性排列,而是按小塊(如 4x4)存儲;
- OpenGL 在上傳紋理時自動進行 swizzle,這個過程不透明也不可控;
- swizzle 格式并未標準化,因此我們無法在 CPU 側安全地提前做這一步;
- 這雖帶來性能隱患,但目前是不可避免的流程。
這個機制體現了現代圖形硬件在性能與靈活性之間的權衡,也是圖形開發中一個比較底層但關鍵的細節。
黑板:2 的冪次紋理和現代矩形紋理處理
我們在圖形開發中使用的是矩形紋理,這可能使我們避免了某些昂貴的處理開銷,特別是在紋理提交和使用過程中。一些圖形硬件對矩形紋理的處理方式與傳統的“標準紋理”(主要是以2 的冪為邊長的紋理)存在差異。
以下是對這段內容的詳細中文總結:
紋理的形狀類型和區別
- 圖形硬件中最常見的紋理是“2 的冪紋理”(Power-of-Two, PoT),即紋理的寬高都是 2 的冪次方,例如:
- 256×256、512×256、1024×1024 等;
- 它們可以是正方形也可以是矩形,但邊長必須是 2 的冪。
- 早期的顯卡只能支持這種 PoT 紋理,不支持任意尺寸的紋理。
- 隨著硬件的發展,現在許多顯卡已經支持了非 2 的冪紋理,即任意尺寸的矩形紋理(例如 540×300)。
矩形紋理與 swizzling 的關系
- 一些現代顯卡對于矩形紋理可能不再進行復雜的 swizzling 操作,也就是說:
- 它們可能直接按順序存儲紋理,不做重新排列;
- 或者將這類紋理識別為特殊用途(比如視頻幀圖像、屏幕貼圖等),從而跳過 swizzle 流程。
- 有些顯卡廠商甚至會悄悄將非 2 的冪紋理在內部自動擴展為 2 的冪紋理,填充空白區域并浪費一部分內存。
- 還有的顯卡則可能真正支持任意尺寸的紋理,完全按照我們提供的矩形尺寸處理。
這些特性的意義與應用
- 如果我們使用的是矩形紋理:
- 在傳輸到 GPU 的過程中可能更快、更輕量,因為少了復雜的 swizzle 操作;
- 系統可能會以**“直接貼圖”的方式處理**這類紋理,更接近于視頻幀、UI元素的用法,而不是用來做 3D 模型的表面貼圖。
- 對于這類紋理,硬件假設我們只是以矩形形式直接繪制到屏幕或目標區域中,而不是將其包裹在三維模型表面。
- 因此,使用矩形紋理有時可以規避昂貴的 CPU 或 GPU 內部重排,尤其在簡單的 2D 渲染流程中更加高效。
技術深度與開發者關注點
- 這些技術細節屬于非常底層的圖形硬件知識,一般開發者(尤其是做通用軟件開發的)并不需要掌握所有細節。
- 但了解這些大致原理是有意義的:
- 一旦我們遇到紋理加載性能瓶頸或紋理貼圖異常行為,就可以快速定位是否與 swizzle 有關;
- 即便自己不是圖形專家,也能向更專業的人詢問問題時表達得更準確,例如向 GPU 廠商工程師、驅動維護者等反饋。
- 如果我們是專注性能優化的圖形開發者,或者從事 GPGPU 等底層開發工作,這類知識則是必須掌握的技術儲備。
總結
- 我們當前使用的矩形紋理在現代 GPU 中可能被“特殊對待”,可能不會經歷昂貴的 swizzling 流程;
- Power-of-Two 紋理仍然是大多數三維貼圖場景中的主流選擇;
- 對矩形紋理的優化主要基于其用途不同,例如用于視頻幀或 UI;
- 對一般開發者來說,只需要有大致了解,當問題出現時知道從哪里入手;
- 對性能要求極高的項目和專業圖形開發者來說,則需要深入掌握這些底層機制。
了解這些,可以幫助我們更高效地處理紋理資源,特別是在紋理加載速度、顯存使用和圖像顯示一致性方面作出更明智的選擇。
黑板:我們當前紋理下載方案的問題
我們當前的紋理使用方式存在兩個嚴重的問題,需要改進:
當前做法的問題
我們目前的做法是:當需要繪制某個位圖(bitmap)時,先檢查它是否已經提交給 GPU。如果沒有,就立即提交(上傳)紋理,然后再進行繪制。如果已經提交過,就直接使用已有的紋理句柄。這種做法存在兩個主要問題:
問題一:提交時機太晚
- 如果一個大紋理(比如 1920×1080 的圖像,大約 8MB)在繪制那一幀時才被提交,GPU 就必須等到整個紋理上傳完畢之后才能開始渲染這一幀。
- 這會造成顯著的性能瓶頸:渲染被阻塞,導致幀率下降甚至卡頓。
- 實際上,這相當于創建了一個“阻塞氣泡”(bubble),GPU必須等整個紋理上傳完成后才能進行下一步操作。
理想的處理方式
我們希望的是:
- 紋理上傳過程可以與繪制操作并行進行,也就是說在 GPU 繪制其他幀的同時,紋理可以在后臺悄悄地上傳;
- 這樣當某一幀需要某個紋理時,它已經準備好了,避免 GPU 空等;
- 本質上,這種優化方式和之前我們加載資源的分層結構是一致的:
- 從磁盤加載到內存;
- 從內存上傳到 GPU;
- 最后再進行繪制。
這個過程中,磁盤到內存我們已經是異步加載的;現在要做的,是讓內存到 GPU 的上傳也變成異步的。
上傳到 GPU 也是一個獨立的資源階段
我們需要把上傳到 GPU 也看作是資源加載鏈條中的一個階段。流程應該是這樣的:
磁盤 → 內存 → GPU → 屏幕
- 從磁盤到內存:已經異步化;
- 從內存到 GPU:當前是同步的,要想辦法異步;
- GPU 渲染到屏幕:實時進行。
因此,我們目標是讓“內存 → GPU”的過程放到另一個線程中處理。
多線程上傳紋理的可行性
以前在 Windows 下,多線程上傳紋理是幾乎不可能做到的,因為驅動、硬件兼容性問題嚴重,嘗試這么做基本是災難性的后果。
但在現在的環境下,情況已經好很多:
- 主流 GPU 和驅動都開始支持紋理流式上傳;
- 通過動態將紋理上傳到 GPU,可以顯著減少初始加載時間;
- 還可以支持大量高分辨率的資源,比如一個 27GB 的開放世界游戲,GPU 顯存可能只有 512MB,但可以根據需要流式上傳紋理內容,保持畫面質量。
風險與挑戰
盡管現在多線程上傳紋理已經可行,但仍然存在挑戰:
- Windows 上 GPU 驅動兼容性問題依然嚴重;
- 各種用戶環境差異巨大:不同的顯卡型號、超頻設置、舊驅動程序等都可能帶來不穩定性;
- 目前仍無法完全避免這些問題,但和我們在 Windows 平臺上做圖形開發時原本就要面臨的兼容性問題相比,并沒有特別糟糕。
總結
- 現有紋理上傳機制阻塞嚴重,需要改為異步;
- 應該將“上傳到 GPU”作為資源加載的一個完整階段;
- 現代圖形系統(特別是在 PC 上)大多數已經支持紋理流式上傳;
- 實現方式可以通過線程,在后臺持續上傳紋理內容;
- 盡管仍有兼容性風險,但相對圖形開發本身的問題來說,并沒有顯著增加復雜度;
- 對于高性能要求或大規模資源管理系統而言,這一步優化非常關鍵。
通過實現異步紋理上傳機制,我們可以顯著減少卡頓、優化幀率表現,并為未來更復雜的圖形資源調度打下基礎。
“驅動程序和 Windows,它們合謀“幫助”你”
Windows 系統中的顯卡驅動程序,常常在表面上看似“為我們提供幫助”,但實際情況遠沒有那么簡單,甚至可以說這些驅動在某些情況下反而“相互勾結”制造了更多問題。這背后的情況錯綜復雜:
驅動程序的“陰影操作”
- 驅動程序通常不是中立、標準化的,它們在實際工作中會隱藏很多底層行為,開發時很難預測這些行為到底會怎么影響性能或穩定性;
- 一些驅動在遇到未知指令、特定操作時,會進行**“自動優化”或偷偷修改執行路徑**,導致程序邏輯和預期完全不符;
- 某些驅動甚至可能在后臺將操作偷偷回退為軟件模擬,也就是說本以為在用 GPU 實際上是在 CPU 上跑,造成嚴重性能問題卻不容易發現。
驅動不一致帶來的兼容性災難
- Windows 系統中的顯卡驅動由各家廠商單獨維護,包括 NVIDIA、AMD、Intel 等;
- 不同廠商的驅動實現千差萬別,甚至同一廠商的不同版本驅動行為都可能不同;
- 一些驅動程序還存在**“未公開的功能”或“專屬 hack”**,只有特定游戲或軟件才能觸發到最優路徑,其它軟件就可能運行在更慢的模式;
- 導致我們開發出的程序,在自己的機器上測試完全正常,但一旦發布到用戶環境,立即暴露出各種崩潰、花屏、加載失敗、性能驟降等問題。
驅動“幫助”的表象與現實
- 表面上看,驅動為了“幫助”我們,提供了豐富的 API、自動內存管理、紋理壓縮、性能統計等工具;
- 實際上,這些“幫助”常常伴隨著不可控的副作用,比如:
- 自動合批造成延遲提交;
- 緩存行為和同步機制不透明;
- 顯存分配策略不一致,甚至不告訴你什么時候釋放了;
- 某些優化只能在調試關掉時觸發,導致開發版本和發布版本表現差異極大。
總結
- Windows 平臺下的顯卡驅動,并不像文檔描述的那樣“可靠”和“透明”;
- 它們會做很多表面上“優化”的事情,實際是為系統或硬件本身服務,而不是為我們的程序服務;
- 驅動之間行為差異極大,即使寫出了標準 API 的代碼,也不能保證在所有用戶機器上表現一致;
- 開發圖形程序時,必須非常小心對待這些驅動帶來的“幫助”,很多時候它們是潛在的問題源頭。
這也是為什么圖形編程中調試和兼容性測試占了如此大比重,我們不僅要寫對的代碼,還要設法與驅動共處、繞過驅動的坑、理解驅動行為,才能構建一個真正穩定、可用的系統。
黑板:資源存儲
我們當前所面臨的情況,主要問題已經很清楚了,而我們接下來要處理的,是一個雖然次要但同樣關鍵的問題,而且幸運的是,只要我們解決了主要問題,這個次要問題也會隨之得到修復。
當前問題概述
我們現在的機制是在繪制某個紋理時才檢查它是否已經上傳到 GPU。如果沒上傳,就立刻提交到 GPU 并使用這個紋理。這個方式存在兩個關鍵問題:
-
上傳延遲導致幀阻塞
大紋理(例如 1920x1080 這種)在首次上傳時,GPU 必須等待整個紋理傳輸完成后才能開始渲染這一幀,嚴重影響實時性。 -
紋理從不清除,導致內存泄漏
GPU 紋理內存中,一旦紋理被上傳,就永遠不會被刪除。即使我們在 CPU 的紋理緩存中已經將其驅逐出去,對應的 GPU 資源仍然殘留,最終導致 GPU 內存不斷膨脹,直到耗盡為止。
CPU 和 GPU 紋理緩存不同步的問題
我們現在使用的是一個固定大小的 CPU 紋理緩存。其運作機制是:
- 當有新的紋理需要時,先檢查 CPU 緩存;
- 如果緩存已滿,就基于 LRU(最近最少使用)算法驅逐一個舊紋理;
- 新紋理就會占據被驅逐位置;
但是:我們從未同步告訴 GPU 刪除對應紋理!
結果就是 GPU 中的紋理越來越多,即使它們在 CPU 緩存中早已不復存在。
最終后果:系統資源耗盡
- GPU 紋理內存不斷填充,不斷累積未使用的紋理;
- 最終會導致 GPU 內存耗盡;
- 顯卡驅動只能開啟“回退機制”,例如自動啟用主內存作為紋理備份(backing store);
- 但這也會進一步消耗主內存,直到整個系統內存用盡;
- 游戲表現為貼圖灰屏、閃退、嚴重卡頓或紋理加載失敗。
至少必須做的事:同步清除紋理
即使我們不做更加完善的資源管理機制,也至少必須確保:
- 每當我們在 CPU 的紋理緩存中驅逐一個紋理;
- 都必須同時通知 GPU,刪除其對應的 GPU 紋理資源;
- 否則我們的程序在運行一段時間后,必然會陷入嚴重的內存危機。
小結
- 我們當前的紋理上傳機制存在兩個問題:上傳延遲 和 紋理殘留;
- 解決上傳延遲問題時,也順帶可以解決紋理殘留問題;
- 紋理資源管理必須 CPU 和 GPU 同步進行,否則容易引發不可預期的問題;
- 即使沒有引入更復雜的紋理流式系統,同步清理已被驅逐的 GPU 資源 是一個最低限度必須完成的任務。
我們接下來需要做的,就是確保這兩部分資源始終保持一致,并開始為更好的紋理管理系統做準備,例如異步上傳和多線程預加載機制。
黑板:CPU 資源存儲如何工作
目前的 GPU 資源存儲(rcp you asset store) 的工作方式基本上是一個通用的分配器。具體來說,它會把資源放入存儲中,當有空閑空間時,如果釋放了足夠的資源,系統會將新資源放入這些空閑位置。這樣,我們的存儲系統就能按需使用空間。
當前實現存在的問題:
-
分配器實現不完善
當前的資源存儲機制雖然可以工作,但它并不是最優化的。我們把資源放入存儲后,沒有進行更多智能的處理和優化。這種做法是為了簡單快速地實現功能,但并不考慮更復雜的性能需求。 -
不考慮性能優化
現在的存儲方式并沒有進行過度優化。我們暫時沒有針對 GPU 資源存儲進行深入的性能測試,因為我們目前還不清楚存儲的具體需求是什么。所以,現在采取的是一種基礎且簡化的實現方式,等游戲運行并通過性能測試后,再根據實際情況來決定是否需要做更多優化。
避免過早優化的原因:
-
避免過早優化導致浪費
在沒有足夠數據支撐的情況下進行優化是不可取的。這是一個常見的編程原則:過早的優化是萬惡之源。我們不能在不知道系統需求的情況下,就開始做大量的性能優化,因為可能做出的優化并沒有解決實際問題。反而可能會浪費時間和資源,且最后這些優化可能會被證明是無效的。 -
需要真實數據來指導優化
如果沒有真實的游戲運行數據,根本不知道資源存儲的具體性能瓶頸在哪里。只有等游戲完全運行并進入實際的性能測試后,才能根據實際需求來進行針對性的優化。所以,應該等到有足夠的數據和實際表現后,再決定是否需要優化以及優化的方向。 -
避免做無意義的工作
如果在游戲開發的初期就開始過度優化系統,可能會導致在后期必須放棄這些優化,因為它們并沒有解決真實的問題,甚至可能是導致其他問題的根源。
小結:
當前的 GPU 資源存儲系統是一個基礎的實現,我們并沒有過早對其進行優化。未來是否進行優化,要等到游戲完全運行并經過性能測試后,根據真實的數據和需求來決定。這個做法遵循了編程中的經典原則:“過早優化是萬惡之源”。
黑板:GPU 資源存儲
現在的 GPU 資源存儲(GPU asset store)其實是一個通用的分配器,可以幫助管理資源。其核心目標是讓 GPU 驅動去管理資源的分配和回收,但目前的實現方式比較簡化。這里有兩個主要的方向,可以在將來優化和調整。
當前實現的問題:
-
GPU 資源存儲的工作方式:
現在的 GPU 資源存儲相當于一個通用的內存分配器,負責管理 GPU 內存中不同的資源。每當分配一個資源時,系統會獲取 GPU 返回的資源句柄并將其存儲在分配器中。如果不涉及軟件渲染,實際上可以將 GPU 資源存儲改成一個固定大小的內存分配器,它的唯一任務就是記錄 GPU 返回的句柄。 -
是否需要優化:
目前的資源存儲分配并沒有進行很大的優化。具體來說,如果未來不需要關注軟件渲染的表現(因為軟件渲染主要是為了教育目的,實際游戲運行中 GPU 渲染會更快),我們可以將資源存儲機制簡化為一個固定大小的分配器,它會從 GPU 中獲取句柄,然后直接管理這些句柄的分配和釋放。這樣就不需要再實現復雜的優化,也不需要堆疊多個通用的內存分配器,畢竟 GPU 驅動本身已經在做這些工作。 -
不需要額外優化的原因:
由于 GPU 驅動本身已經在管理內存分配,并且 GPU 渲染本身要比軟件渲染快得多,因此如果不涉及軟件渲染,我們完全可以簡化資源存儲的管理機制。過度優化可能沒有太大意義,反而會增加復雜性,而 GPU 驅動已經處理了大部分內存分配工作。
下一步計劃:
為了進一步提高資源管理效率,需要在當前的代碼中進行調整,尤其是將資源下載到 GPU 的過程與主線程分離。為了做到這一點,我們計劃將資源下載過程放入獨立的線程中,這樣可以避免主線程被阻塞,進而提高渲染的效率。
線程化資源加載:
我們目前的資源加載機制已經實現了多線程,下一步就是利用這些線程來加載紋理并將其盡早發送到 GPU 中。這是為了確保紋理盡快加載,避免在需要使用這些紋理時遇到延遲,從而影響渲染性能。
短期目標:
雖然時間不多,但我們可以開始分析現有的代碼,看看如何將資源下載和加載流程合理地線程化,以便在主線程中能夠更高效地使用這些資源。在這里,確保我們所有的代碼都在同一頁面上,明確目標是非常重要的。
總的來說,當前的 GPU 資源存儲機制基本上是一個簡化版的內存分配器,未來的改進可能會集中在簡化內存管理,避免過早優化,專注于合理利用 GPU 驅動的內存管理能力,并通過多線程加速紋理下載過程。
game_opengl.cpp:我們當前的加載方式
在當前的實現中,紋理綁定的過程在OpenGL文件中的render entry bitmap
部分進行。這是一個典型的“及時加載”機制,即當需要繪制一個紋理時,系統首先會檢查該紋理是否已經被加載到GPU中,并且是否有對應的句柄。如果有句柄,表示該紋理已經上傳到GPU,系統只需通知GPU準備好繪制該紋理;如果沒有句柄,表示該紋理還未上傳,系統需要重新綁定一個新的紋理并將其傳輸到GPU。
這個過程其實正是我們所關心的問題,尤其是在紋理下載的過程中,如何避免由于紋理傳輸的延遲而導致繪制過程卡頓。具體來說,glTexImage2D
這一部分代碼負責將紋理數據從內存傳輸到GPU。這一過程是異步的,因此我們并不知道傳輸需要多久完成,為了防止這一步驟阻塞渲染,我們希望能夠重疊紋理下載和渲染操作。
值得注意的是,很多現代顯卡(從GeForce 500系列及以上的顯卡開始)都配備了專門的復制引擎,這些復制引擎的作用就是在圖形渲染過程中異步地將紋理數據傳輸到GPU,而不會影響渲染的正常進行。這樣一來,在現代硬件上,紋理的傳輸過程實際上是可以與渲染操作并行進行的,而不會對幀的繪制造成明顯的卡頓。
總的來說,問題的核心是如何在傳輸紋理到GPU時避免阻塞渲染,而現代顯卡的硬件設計已經為此提供了很好的支持。通過利用這些硬件特性,能夠有效地實現紋理下載與渲染操作的重疊,提升整體的渲染效率。
game_asset.cpp:回顧 LoadBitmap 和 LoadAssetWorkDirectly 的作用
當前的問題在于,我們希望能夠異步地將紋理數據傳輸到GPU,但OpenGL本身并沒有設計來支持這種異步操作。OpenGL的設計假設每個線程只能有一個活動的上下文,這意味著無法在多個線程之間共享同一個OpenGL上下文。因此,如果我們嘗試在一個線程中執行glTexImage2D
(一個將紋理上傳到GPU的OpenGL函數),而該函數的調用發生在主線程之外,OpenGL會無法正確處理,因為主線程和子線程無法共享同一個上下文。
具體來說,當前的實現使用了異步加載的方式,例如在game_assets.cpp
文件中的LoadBitmap
函數。該函數負責加載紋理文件,并創建一個包含所有必要信息的工作結構。當需要加載一個資源時,會調用這個工作結構來加載文件內容并判斷該資源是否能夠直接使用。如果是位圖類型,系統會進一步處理它并執行glTexImage2D
,這時會將紋理數據傳輸到GPU。
在理想情況下,我們可以在加載位圖資源時直接將紋理數據傳輸到GPU,但問題在于glTexImage2D
調用依賴于當前線程的OpenGL上下文,這就限制了我們無法簡單地將其移動到另一個線程進行異步處理。每個OpenGL上下文只能在一個線程中使用,因此無法在不同的線程之間共享這個上下文。
盡管我們有了大部分必要的信息來執行紋理上傳(例如紋理的寬度、高度、數據類型和內存位置等),但由于OpenGL的上下文限制,我們無法直接在后臺線程中執行這些操作。如果沒有這些上下文限制,我們本可以將紋理上傳過程移至另一個線程,從而避免主線程被阻塞。但是,由于OpenGL的限制,我們必須找到一種方法來解決這個問題,才能實現異步上傳紋理數據的目標。
目前的方案可能需要在不同的線程之間管理OpenGL上下文,或使用某種方式將紋理上傳過程與渲染分離,這樣才能在不阻塞主線程的情況下執行紋理的上傳操作。
game_opengl.cpp:將 glTex* 調用移動到 game_asset.cpp,并考慮如果紋理未準備好就暫停幀
為了實現異步紋理加載和渲染優化,首先需要調整當前的代碼結構。目標是將紋理的綁定操作與主渲染流程分離,使得紋理的上傳過程能夠在后臺線程中執行,而不阻塞主渲染線程。在此過程中,首先會進行一些調整,例如:
-
簡化代碼結構:將與紋理綁定相關的代碼移到渲染路徑的開頭,避免在渲染的過程中反復執行與紋理處理相關的邏輯。每當需要渲染一個紋理時,直接綁定該紋理。如果紋理句柄為零,則說明該紋理還未加載,系統可以選擇不繪制該紋理,直接跳過這次繪制操作。
-
處理紋理加載的延遲:如果某個紋理尚未加載,系統有兩種選擇。第一種是暫停當前幀,等待紋理加載完成再繼續繪制。第二種是犧牲渲染質量,允許渲染幀繼續進行,盡管某些紋理可能未能加載完成。這時,主線程將繼續以60幀每秒的速度進行渲染,盡管某些紋理可能會出現未加載的情況。
-
清理OpenGL代碼:當前的代碼中涉及OpenGL函數調用,這樣做的問題在于我們不確定當前代碼是否真的運行在OpenGL環境中,可能在非OpenGL平臺上運行。因此,我們需要將OpenGL相關的代碼隔離出來,確保代碼路徑清晰,并將OpenGL的調用封裝成獨立的模塊。這樣,渲染邏輯與OpenGL的具體實現將相互解耦,保持代碼的可維護性。
-
處理OpenGL上下文:另一個關鍵問題是如何在異步操作中使用OpenGL。由于OpenGL不允許在多個線程中共享同一個上下文,我們需要為紋理加載過程創建一個新的OpenGL上下文,以便可以在后臺線程中執行紋理的上傳操作。這意味著我們必須在后臺線程中為紋理上傳操作提供一個獨立的OpenGL上下文,使得紋理的下載過程能夠獨立于主線程進行。
由于時間限制,當前無法完全實現這一目標,但已經開始為后續的實現做準備。通過整理和封裝OpenGL操作,可以為未來的異步紋理加載奠定基礎,同時確保渲染主線程不會被阻塞,提升渲染性能。
win32_game.cpp:將圖片的各個部分拼接起來
在討論OpenGL上下文的創建和線程管理時,核心任務是確保每個可能需要調用 glTexImage2D
的線程都擁有一個與主上下文共享的OpenGL上下文,以便能在不同線程間進行紋理上傳等操作。
首先,在創建OpenGL上下文時,涉及到的操作包括:
-
上下文的創建:最初,創建了一個OpenGL 1.0上下文,然后為了支持更現代的OpenGL版本,切換到OpenGL 4.0以上的版本。這個過程中,涉及到的上下文共享是非常關鍵的。具體來說,通過設置共享上下文,確保在一個線程中上傳的紋理可以被其他線程訪問。
-
共享上下文:在OpenGL中,多個上下文可以共享同一塊內存,這樣當紋理上傳到一個上下文時,其他上下文可以直接訪問這個紋理。在多線程環境中,我們希望創建的每一個線程都有一個共享的OpenGL上下文,這樣各個線程都能下載紋理而不發生沖突或失敗。
-
線程管理:在創建多個線程時,每個線程都需要與OpenGL上下文關聯,否則當調用
glTexImage2D
等OpenGL函數時,操作會失敗。為了處理這個問題,必須確保每個工作線程在執行時,都能訪問到已共享的OpenGL上下文。
- 工作隊列:每個工作線程會從工作隊列中獲取任務,當任務是與紋理相關時,它會調用
finalizeAsBitmap
來處理紋理。此時,線程需要一個有效的OpenGL上下文來執行紋理上傳操作。如果沒有正確的上下文,這些操作將無法成功。
因此,為了支持多線程紋理上傳操作,需要為每個工作線程創建一個OpenGL上下文,并確保這些上下文之間是共享的。這樣,每個線程就能在沒有阻塞主線程的情況下,異步地上傳紋理到GPU。同時,必須確保驅動程序支持多個線程同時下載紋理,才能避免因資源爭用導致的潛在問題。
win32_game.cpp:在 ThreadProc 中調用 Win32CreateOpenGLContextForWorkerThread
在多線程環境中,若要實現紋理數據的異步上傳到GPU,必須為每個工作線程創建獨立的、共享主上下文的OpenGL上下文。這是實現高效資源管理和避免阻塞主線程的關鍵步驟。以下是具體的分析與操作流程:
1. 在工作線程進入工作循環前創建OpenGL上下文
在每個工作線程執行任務之前,必須確保該線程擁有自己的OpenGL上下文。這個上下文應與主線程的OpenGL上下文共享資源,以便紋理數據上傳后能被渲染線程正確訪問。創建流程大致如下:
- 在線程啟動后、進入任務處理循環前,調用
Win32CreateOpenGLContextForWorkerThread
(或類似命名的函數)。 - 該函數需使用平臺相關的OpenGL上下文創建API(例如WGL的
wglCreateContextAttribsARB
)創建新的上下文。 - 創建上下文時,需指定共享上下文參數,確保資源一致性。
2. 平臺獨立性和調用OpenGL代碼的隔離
為了保持平臺代碼與OpenGL代碼的獨立性,避免在通用邏輯中硬編碼OpenGL函數調用,需做如下設計:
- 在線程初始化時調用平臺層代碼完成OpenGL上下文的創建與綁定(例如通過 Win32 平臺層調用)。
- 在資源加載邏輯中,僅進行標記或觸發機制(如將需要上傳的數據加入任務隊列),由線程內已設置好的OpenGL上下文完成實際上傳。
3. 避免紋理上傳直接插入核心邏輯
不建議直接在 FinalizeBitmap
等函數中插入 glTexImage2D
調用,原因如下:
- 這將導致平臺無關邏輯中摻雜OpenGL平臺相關代碼,破壞模塊劃分。
- 可能引入不可控的延遲和錯誤,特別是在沒有有效上下文時調用OpenGL API。
更好的方案包括:
- 方案一:在平臺層提供一個包裝接口,供資源系統在必要時觸發紋理上傳操作。
- 方案二:引入一個上傳隊列,將紋理數據標記并加入隊列,由另一個專門用于上傳的線程集中處理。
4. 線程安全與上下文有效性
由于OpenGL要求每個上下文在任何時間只能綁定一個線程,因此:
- 每個線程必須擁有獨立的上下文。
- 這些上下文需在創建時通過
wglShareLists
或wglCreateContextAttribsARB
等方式與主上下文共享。 - 上傳線程每次處理紋理前,需將自己的上下文設置為當前(使用
wglMakeCurrent
)。
5. 總結
我們當前的目標,是將紋理上傳過程從主線程中剝離出來,轉移到后臺線程并實現異步執行。為此,必須:
- 確保每個后臺線程擁有共享的OpenGL上下文。
- 保持平臺相關邏輯與核心游戲邏輯的隔離。
- 可能引入專門的上傳線程或上傳任務隊列,進一步解耦資源加載流程。
這樣的結構將確保即便在主線程需要高性能渲染時,也能并行處理大量紋理數據上傳,提升整體渲染系統的吞吐效率和響應能力。
win32_game.cpp:引入 Win32CreateOpenGLContextForWorkerThread
我們需要為每個工作線程創建一個獨立的 OpenGL 上下文,并確保它們共享主上下文的資源,這樣這些線程才能執行紋理下載操作。為此,我們要做的是以下幾個關鍵步驟,下面是對整個流程的詳細梳理與總結:
1. 創建用于工作線程的 OpenGL 上下文函數
我們需要新寫一個函數,例如 Win32CreateOpenGLContextForWorkerThread
,這個函數的唯一職責就是創建 OpenGL 上下文。核心邏輯幾乎和主上下文創建一致,但有以下區別:
- 必須傳入共享上下文:我們不能像主上下文那樣傳
0
,而是要傳入一個已有的上下文(例如主線程上下文),以實現資源共享。 - 可能需要傳入窗口的
DC
(設備上下文):雖然這個上下文不會直接渲染到窗口,但根據參考文檔的建議,傳入有效的DC
是合理且推薦的做法。
2. 如何獲取所需參數
要創建上下文,我們需要兩個關鍵數據:
- 共享上下文(OpenGL RC)
- 窗口設備上下文(Window DC)
我們可以在初始化主窗口時保存這些值,并在之后需要為工作線程創建上下文時傳遞過去。
3. 全局保存共享上下文和窗口 DC
為了讓每個線程都能訪問到這些信息,我們可以:
- 在初始化 OpenGL 的過程中將
HGLRC
(OpenGL 渲染上下文)和HDC
(設備上下文)保存到全局或結構體中。 - 將這些信息作為參數傳遞給線程隊列的初始化函數,使線程在啟動時可以使用這些值創建自己的上下文。
4. 修改線程隊列初始化流程
目前線程隊列是在創建窗口之前初始化的,因此拿不到窗口的句柄 HWND
和設備上下文。我們需要調整初始化順序:
- 先創建窗口
- 再初始化線程隊列
- 此時我們就可以將窗口句柄、OpenGL 上下文、設備上下文傳給線程隊列的初始化函數
這樣每個線程就可以在啟動時:
- 拿到這些參數
- 使用它們創建共享 OpenGL 上下文
- 將上下文設為當前上下文(使用
wglMakeCurrent
)
5. 統一 OpenGL 上下文屬性
我們使用一套統一的上下文屬性 attribs
,可以將它提取為全局常量(例如 Win32OpenGLAttribs
),以供所有上下文創建共享使用。這樣可以確保所有 OpenGL 上下文一致,避免兼容性問題。
6. 創建失敗的處理
如果創建上下文失敗(wglMakeCurrent
不成功),這是一個致命錯誤:
- 因為我們依賴該線程上傳紋理到 GPU
- 如果沒有上下文,該線程將無法進行任何 GPU 操作
- 此時程序可能需要直接終止或發出嚴重錯誤提示
7. 總結操作步驟
最終的操作步驟如下:
- 主線程初始化窗口,保存
HWND
和HDC
- 初始化主 OpenGL 上下文,保存
HGLRC
- 提取上下文屬性為全局常量
- 將
HDC
和HGLRC
傳遞給線程隊列創建函數 - 每個工作線程啟動時使用這些值創建自己的共享上下文
- 將上下文設為當前上下文,準備執行紋理上傳
通過這種方式,我們可以實現多線程環境下安全、正確的 OpenGL 紋理異步上傳,避免阻塞主線程渲染流程,提高系統響應效率。整個系統也更具可擴展性和清晰的模塊邊界。
https://developer.download.nvidia.com/GTC/PDF/GTC2012/PresentationPDF/S0356-GTC2012-Texture-Transfers.pdf
win32_game.cpp:引入結構體 win32_thread_startup
我們現在的目標是:當線程實際啟動并執行任務處理函數(ThreadProc
)時,確保線程擁有創建 OpenGL 上下文所需的全部數據。因此,我們計劃對現有結構和初始化方式進行擴展和調整。以下是詳細思路與操作步驟的總結:
1. 線程初始化參數結構擴展
我們計劃定義一個新的結構體,例如 Win32ThreadStartup
,用于在線程啟動時傳遞更多的信息。這個結構體將包含:
PlatformWorkQueue
:用于線程處理工作的任務隊列(原有功能)HWND WindowHandle
:窗口句柄,用于獲取HDC
HGLRC SharedGLRC
:用于與主線程共享資源的 OpenGL 上下文
這樣,每個線程在創建之初就擁有創建共享上下文所需的全部數據。
2. 改造線程啟動流程
原先線程通過一個比較簡單的參數(如平臺工作隊列)進行啟動。現在我們要把這個流程改造成:
- 創建
Win32ThreadStartup
實例 - 填入窗口句柄、主線程共享 OpenGL 上下文、平臺隊列等數據
- 將這個結構體作為參數傳入線程函數
這樣線程函數啟動后,就可以從這個結構體中提取需要的數據,并完成 OpenGL 上下文的初始化。
3. 區分主線程和工作線程數據
考慮到并不是每個線程都需要進行 OpenGL 操作(比如只有低優先級的隊列需要執行紋理上傳),我們會為有需要的線程傳入完整的 WindowHandle
和 SharedGLRC
,而其他線程可以傳入空值或默認值表示它們不需要這些功能。
4. 初始化線程時的數據構造
在主程序中初始化線程的時候,我們將:
- 先創建窗口,獲取
HWND
- 初始化主 OpenGL 上下文,獲得主
HGLRC
- 構建包含這些信息的
Win32ThreadStartup
數據 - 在線程創建時傳入這個數據
5. 線程內創建共享 OpenGL 上下文
在線程函數 ThreadProc
內部:
- 提取
Win32ThreadStartup
中的信息 - 使用提供的
HWND
獲取設備上下文HDC
- 調用封裝好的函數
Win32CreateOpenGLContextForWorkerThread
來創建共享 OpenGL 上下文 - 設置當前上下文為剛剛創建的上下文(
wglMakeCurrent
)
這樣線程就具備了執行 OpenGL 操作(例如上傳紋理)的能力。
6. 回退機制(可選)
如果 wglMakeCurrent
或上下文創建失敗,可以判斷為嚴重錯誤。目前我們的判斷是:
- 如果無法創建上下文,就無法上傳紋理
- 整個系統將喪失異步紋理傳輸能力
- 程序可能無法正常運行,需記錄錯誤并中止初始化或給出錯誤提示
7. 總結核心變更點
- 定義新的結構體
Win32ThreadStartup
,統一線程初始化所需的數據 - 修改線程創建方式,使用完整的結構體作為參數
- 在線程中創建與主上下文共享資源的 OpenGL 上下文
- 保證所有工作線程都能正確進行 GPU 操作(如上傳紋理)
通過這一系列改造,我們建立了一個靈活且可擴展的多線程 OpenGL 初始化機制,為實現異步紋理上傳等圖形優化任務打下了穩定的基礎。整個方案保持平臺層和工作隊列邏輯的清晰分離,同時最大限度地復用了已有的初始化邏輯與上下文管理方案。
win32_game.cpp:將 Startup 直接傳遞給 Win32MakeQueue
我們目前已經處于一個關鍵的位置,可以填充用于線程啟動的結構體了。接下來的工作是確保每個線程在創建時都能正確接收到其專屬的啟動信息。下面是詳細的中文總結:
1. 統一線程初始化結構體傳遞邏輯
我們設計了一個新的結構體 Win32ThreadStartup
,用于封裝線程啟動所需的數據。該結構體中包含:
- 線程要處理的任務隊列指針(
PlatformWorkQueue
) - 窗口句柄(
HWND
) - 主 OpenGL 上下文句柄(
HGLRC
)
在整個初始化過程中,我們始終會傳入這個結構體,因此我們可以認為每個線程在啟動時都會收到一個有效的 Win32ThreadStartup
實例。
2. 修改線程創建邏輯
在線程創建時:
- 不再單獨傳入工作隊列,而是將其作為結構體的一部分傳入
- 直接將
Win32ThreadStartup
實例作為線程函數的參數傳遞 - 保證每個線程獲得唯一的、屬于自己的結構體實例,防止線程競爭訪問共享數據
3. 在線程函數中提取數據
線程實際運行后,在 ThreadProc
中:
- 通過參數
lpParameter
拿到傳入的Win32ThreadStartup
實例 - 從中提取窗口句柄、OpenGL 上下文等信息
- 根據這些信息創建并設置線程自己的 OpenGL 上下文
4. 避免多線程數據競爭問題
由于多個線程幾乎同時啟動,每個線程都必須有自己獨立的 Win32ThreadStartup
實例。不能直接引用共享的結構體或數組中的某一項,因為可能還沒被正確初始化或者已經被另一個線程讀取。
我們必須:
- 在主線程中為每個線程單獨構造一個
Win32ThreadStartup
實例 - 確保在傳入線程之前,這些實例的數據已經全部準備好
- 不共享指向相同內存的結構體指針
5. 優化結構體傳遞方式
因為結構體中已經包含了我們所需的所有信息,我們可以完全取消以往只傳入隊列指針的舊做法,直接用結構體作為唯一參數。這讓代碼結構更加統一、邏輯更加清晰。
6. 后續步驟準備
接下來我們將會:
- 在主線程中為每個工作線程構造并傳入
Win32ThreadStartup
- 在線程中創建 OpenGL 上下文,并調用
wglMakeCurrent
設置為當前上下文 - 使線程具備進行紋理上傳等 OpenGL 操作的能力
- 確保整套機制能夠在多線程中穩定運行
通過這套機制,我們實現了一個線程安全、結構清晰的 OpenGL 多上下文系統,使每個線程都能在自己的上下文中獨立執行圖形操作,同時與主上下文共享資源。這為后續的多線程紋理處理打下了堅實基礎。
win32_game.cpp:撤銷并改為引入 GlobalOpenGLRC 和 GlobalDC
我們原本打算通過為每個線程傳遞一個包含必要信息的結構體(如 OpenGL 上下文、窗口句柄、設備上下文等)來完成初始化操作,但實際過程中發現這種做法帶來了很多問題。下面是這部分內容的詳細中文總結:
1. 原結構體傳遞方案的問題
我們起初嘗試為每個工作線程分配一個專屬的結構體,其中封裝了:
- 線程關聯的工作隊列指針
- OpenGL 上下文句柄(
HGLRC
) - 窗口句柄(
HWND
) - 設備上下文句柄(
HDC
)
但這樣做導致了一些復雜問題:
- 每個線程都必須擁有獨立的結構體副本,不能共享,否則數據會被覆蓋或讀取沖突
- 管理多個結構體副本變得很繁瑣
- 對線程函數的調用方式也變得臃腫復雜
- 新增邏輯不但沒有減少 bug,反而引入了更多潛在問題
因此我們決定廢棄這種做法。
2. 轉而使用全局變量
我們重新思考策略,決定使用全局變量來保存關鍵資源,這些資源在程序運行期間并不會更改:
- 全局保存的 OpenGL 上下文句柄(
globalHGLRC
) - 全局保存的設備上下文句柄(
globalHDC
)
在 OpenGL 初始化完成之后,我們直接將這些句柄存儲到全局變量中,供后續所有線程統一訪問。
3. 優勢與實現方式
這種方式的優勢在于:
- 所有線程都可以方便訪問這些共享資源
- 初始化邏輯更清晰
- 避免了線程間傳遞結構體帶來的同步和生命周期管理問題
- 減少了代碼冗余和復雜度
具體實現時:
- 在
Win32InitOpenGL
函數中,不再釋放獲取的HDC
,而是存入全局變量 - 創建 OpenGL 上下文后,同樣保存該
HGLRC
到全局變量中 - 每當有線程需要設置上下文,只需調用封裝函數
CreateOpenGLContextForWorkerThread
,它從全局變量中獲取數據完成配置
4. 注意事項與容錯
雖然這種方式更簡潔,但也有前提條件:
- 全局變量在任何線程使用前必須已經正確初始化
- 初始化必須在多線程創建之前完成
- 如果創建 OpenGL 上下文失敗,將導致整個圖形功能無法運行,這是一個“致命錯誤”場景,程序應當停止或降級運行
5. 相關函數的調整
我們對相關函數做了如下調整:
CreateContextARB
返回的 OpenGL 上下文不再是局部變量,而是直接保存到globalHGLRC
- 獲取的窗口
HDC
不再立即釋放,而是保存到globalHDC
CreateOpenGLContextForWorkerThread
不再接收參數,而是直接引用全局變量完成配置
6. 當前問題排查
我們發現最后函數簽名可能存在不一致的問題,比如傳入窗口句柄未被使用等小錯誤。這些會在后續調試中進一步完善和修復。
總結
我們將線程初始化信息從“結構體傳參”方式切換為使用“全局變量”,從而顯著簡化了線程資源配置的流程,降低了多線程同步復雜度。這一更改雖然在架構上退回了一步,但實際效果上卻更為穩健、安全,也更適合當前上下文中的 OpenGL 使用模式。
運行游戲,但什么也沒看到
我們目前的系統已經重新編譯并能夠運行,從理論上講已經具備多線程上下文的創建能力。但是,由于尚未真正實現紋理的下載和處理,屏幕上并不會顯示任何內容。因此盡管底層線程機制看似運作正常,整體功能仍處于未完成狀態。
當前存在的問題與缺失
-
紋理資源尚未加載:
雖然我們創建了多個工作線程,理論上具備處理能力,但目前沒有任何線程真正去處理或下載紋理資源。也就是說,盡管上下文存在,但未利用這些上下文去上傳或處理圖像數據。 -
渲染結果不可見:
因為沒有紋理資源,渲染階段自然無法展示任何實際圖像。這導致屏幕顯示為空,視覺上看起來程序似乎“什么都沒做”。
下一步工作
-
完善資源加載流程:
我們需要進入資源管理部分(asset file)并開始實現紋理的加載邏輯。這一步很關鍵,它將確保有實際的數據送入 GPU 進行渲染。 -
驗證 OpenGL 上下文使用是否正確:
除了資源加載,我們還需確認以下操作是否成功:- 每個工作線程是否成功獲取了 OpenGL 上下文
wglMakeCurrent
調用是否生效- 上下文之間是否真正可以共享資源
-
補全多線程渲染支持:
確保每個工作線程在正確的上下文下執行 OpenGL 操作,并能安全地進行資源上傳。所有上下文之間的共享機制必須經過測試驗證,確保不會引起崩潰或數據不同步的問題。
當前階段評估
目前系統的底層線程機制和 OpenGL 上下文支持看起來是架構合理的,但尚未完成任何實際可見的渲染工作。下一步的重點是連接紋理資源加載邏輯與線程上下文初始化邏輯,確保它們可以協同工作,最終實現紋理的異步上傳和顯示。
總的來說,我們還處于比較初級的階段,雖然已經打下了一定的技術基礎,但離實際的渲染輸出還有一段關鍵的開發工作要做。接下來需要逐步推進資源加載、上下文驗證以及線程安全機制,直到系統能夠正確渲染紋理內容。
win32_game.cpp:在 platform_work_queue 中添加 bool32 NeedsOpenGL
目前我們對 OpenGL 上下文的初始化和綁定邏輯做了進一步的優化,主要是為了避免對不需要使用 OpenGL 的線程進行無謂的上下文創建操作,簡化系統結構,提升效率。
當前的工作內容與優化思路
-
明確哪些線程需要 OpenGL:
只有低優先級線程才會進行資源下載(例如紋理),因此只有這些線程才需要綁定 OpenGL 上下文。高優先級線程(如主線程或計算密集型線程)并不涉及資源上傳工作,故無需進行 OpenGL 上下文創建。 -
擴展 PlatformWorkQueue 的結構:
在PlatformWorkQueue
中新增了一個字段,例如NeedsOpenGL
或類似的布爾變量,用于標記該隊列是否需要支持 OpenGL。通過這個標志,我們可以在創建線程時判斷是否需要附加 OpenGL 初始化流程。 -
線程初始化中做條件判斷:
在每個線程啟動時,檢查所屬隊列是否需要 OpenGL。如果NeedsOpenGL
為 true,就執行 OpenGL 上下文的創建、設置等邏輯,否則直接跳過,避免不必要的資源分配。
創建隊列時設置標志位
-
高優先級隊列:
創建前將NeedsOpenGL
設置為 false,表示它不進行資源處理,不需要 OpenGL。 -
低優先級隊列:
創建前將NeedsOpenGL
設置為 true,因為這些線程會處理諸如紋理下載等圖形相關任務,必須擁有上下文。
這樣一來,在調用 Win32MakeQueue
函數時,我們能根據實際用途動態設置隊列是否需要 OpenGL,從而合理分配資源并優化初始化過程。
總結現階段結果
- 成功為線程隊列系統引入了“是否需要 OpenGL”的判定機制。
- 避免為所有線程都創建 OpenGL 上下文,減少了開銷。
- 保持了系統邏輯上的清晰性,使得線程行為更明確、更可控。
- 后續還可以進一步優化,如根據線程的具體任務動態調整隊列標志,甚至在運行時切換。
這一步的優化為系統的資源管理和初始化打下了更加穩固的基礎,尤其是在多線程與圖形上下文交叉使用的場景中顯得尤為重要。后續工作將主要集中在確保線程按需執行圖形操作,同時保持系統的穩定性與性能。
運行游戲,依然看不到任何東西
現在系統的 OpenGL 上下文創建邏輯已經更加正確,僅為真正需要圖形處理的線程創建了兩個 OpenGL 上下文,避免了不必要的資源浪費,整體結構也變得更加清晰。
當前狀態總結:
-
OpenGL 上下文初始化完成:
成功地為兩個工作線程各自創建了獨立的 OpenGL 上下文,并且正確地根據線程是否需要處理圖形資源來決定是否需要上下文。 -
尚未處理紋理下載:
當前系統中還沒有真正實現紋理下載邏輯,資產系統并未調用或觸發任何資源加載功能,因此即使上下文已經就緒,也還沒有使用它們去處理圖像數據或 GPU 上傳等工作。
臨時方案:引入基礎測試用的回調函數
為了快速驗證整套系統流程是否正確,可以暫時添加一個簡單的回調函數作為測試,這樣能立刻觀察多線程環境中 OpenGL 上下文是否工作正常。
示例方案:
- 定義一個簡單的測試回調函數,在其中調用一個基礎的 OpenGL 函數,例如生成或綁定一個紋理,確保上下文有效。
- 將該回調加入低優先級的工作隊列中,確保該線程擁有 OpenGL 上下文權限。
- 觀察是否觸發錯誤,如上下文丟失、非法調用等。
該測試方案雖然簡單,但足以驗證當前結構的可用性。
后續可以計劃的步驟:
-
在資產系統中正式集成資源加載邏輯:
包括紋理、模型或著色器的讀取、解析與上傳流程。 -
構建資源調度與緩存系統:
讓加載過程具備狀態控制,避免重復上傳,增加性能控制點。 -
增加線程資源安全性校驗與容錯機制:
尤其是在 OpenGL 上下文切換或銷毀時,需確保不會誤操作或越權訪問。
小結
雖然當前還未真正觸發任何資源加載操作,但系統結構已基本成型。借助一個簡易的回調測試機制,可以快速驗證多線程 OpenGL 支持是否可靠,為后續正式資源處理打下堅實基礎。下一步只需將資產系統與圖形資源操作對接,就能實現完整的加載流程。整體來看,工作進度已步入關鍵節點,后續將更聚焦于實用功能與系統穩定性構建。
game_platform.h:在 platform_api 中添加 platform_allocate_texture 和 platform_deallocate_texture,并添加相應調用
為了讓 OpenGL 在紋理提交和釋放上能夠正確運作,需要確保平臺代碼中有機制能被觸發,以實際完成紋理下載與顯卡上傳的過程。整個系統需要具備一種方式來觸發這些平臺層的行為,并能夠反映圖像資源在圖形硬件上的狀態變化。
當前實現邏輯與設計思路:
-
目標:
實現平臺層紋理上傳和釋放的接口,使其可以從資產系統被調用,從而完成對 GPU 的操作。 -
結構劃分清晰:
已將整個調用流程分為兩大類函數:- 一類僅用于調試用途。
- 一類屬于平臺層暴露給游戲邏輯的正式接口,數量保持精簡,避免 API 泛濫。
-
接口簡化優化:
當前的接口中存在過多不必要的函數,因此進行了精簡,只保留對游戲邏輯真正有意義的操作接口,避免冗余暴露。
新增接口的設計思路:
- 提出一種新的接口設計方式,類似于統一的“紋理分配和釋放”機制。
- 將紋理提交過程簡化為兩個操作:
AllocateTexture
—— 向平臺層請求上傳紋理數據。DeallocateTexture
—— 向平臺層通知釋放指定紋理資源。
AllocateTexture:
- 接收紋理的
寬度
、高度
以及數據指針
。 - 返回值為一個紋理句柄(handle),可供后續引用和管理。
- 不涉及額外復雜的狀態數據,僅關注實際需要傳遞的基礎紋理信息。
- 未來如需支持壓縮格式、mipmap 等特性,可在此基礎上擴展。
DeallocateTexture:
- 接收紋理句柄作為參數。
- 僅需要知道需要釋放哪個紋理,不需額外參數,接口極簡化。
示例結構(偽代碼):
void* PlatformAllocateTexture(int width, int height, void* data);
void PlatformDeallocateTexture(void* textureHandle);
- 這樣一來,平臺層接收到調用請求后可在適當的線程和上下文中執行 OpenGL 紋理上傳操作。
- 回傳的句柄可用于后續繪制操作或銷毀。
總結:
通過新增這兩個核心接口,平臺層即可與資產系統打通,從而具備將紋理數據上傳到 GPU 的能力,同時保持接口極簡、清晰、易擴展。該方案目前足以滿足系統初期需求,并為后續高級圖形特性留出了擴展空間。下一步只需在適當線程中調用這些接口,即可開始處理實際紋理資源。整個架構向更合理的資源管理方式又前進了一步。
game_asset.cpp:在 FinalizeAsset_Bitmap 的情況下調用 Platform.AllocateTexture
為了讓平臺層實現紋理的實際上傳操作,我們在加載資源文件并讀取完數據之后,進一步添加了 PlatformAllocateTexture
的調用,用以創建 GPU 紋理資源。整個過程基于已有的資源加載邏輯,接入 OpenGL 相關函數,從而完成從磁盤數據到 GPU 顯存的完整路徑。
當前操作與結構整合流程:
-
平臺接口調用:
在PlatformReadDataFromFile
之后,插入PlatformAllocateTexture
的調用,該函數負責接收解析后的位圖數據并在 GPU 上創建相應紋理。 -
不再需要多余數據:
去除了一些舊代碼中不再使用的無關數據,只保留紋理上傳所需的核心信息:寬度、高度和原始像素數據。 -
平臺層函數定義與對接:
在回調定義區域中新增了PlatformAllocateTexture
和PlatformDeallocateTexture
的函數指針,并將這兩個新函數像PlatformAllocateMemory
一樣注冊進平臺回調結構中,使上層邏輯可以調用。
OpenGL 實際上傳邏輯:
-
句柄生成:
使用glGenTextures
生成新的紋理句柄,確保句柄未被占用。 -
綁定紋理句柄:
調用glBindTexture(GL_TEXTURE_2D, handle)
將新句柄綁定到當前 OpenGL 狀態中,準備接受像素數據。 -
上傳數據:
使用glTexImage2D
上傳從資源文件中加載出的圖像像素,參數包括紋理尺寸、格式以及數據指針。 -
解綁清理:
出于線程安全和狀態一致性考慮,在上傳完成后立刻使用glBindTexture(GL_TEXTURE_2D, 0)
解綁,防止線程污染當前上下文狀態。
位圖數據與句柄整合:
- 通過
work->Asset.Header.Bitmap
獲取所需的圖像信息,該結構體內已經包含寬高以及圖像指針。 - 上傳后返回生成的 OpenGL 句柄,并作為
void*
類型返回給調用者,以供后續繪制使用。 - 添加斷言以確保返回的句柄可以安全轉換為
void*
,提高健壯性。
釋放紋理資源:
- 提供
PlatformDeallocateTexture
接口,接收紋理句柄,調用glDeleteTextures
將其從顯存中移除。 - 由于紋理句柄在 OpenGL 中是唯一標識,釋放時無需附加任何額外信息。
總結:
通過將 PlatformAllocateTexture
接入資源加載流程,實現了資源從磁盤到 GPU 的自動流轉,構建了初步的資源上傳系統。紋理創建過程已封裝在平臺層中,與上層游戲邏輯解耦,確保接口簡潔且可維護。同時,上傳完成后立即解綁,有效避免了狀態污染問題,為多線程紋理加載奠定了基礎。接下來只需完成紋理釋放的對應路徑,即可形成完整生命周期管理。
win32_game.cpp:編寫 PLATFORM_DEALLOCATE_TEXURE
在處理紋理資源釋放的邏輯中,我們為 OpenGL 提供了對應的釋放函數 glDeleteTextures
,用于在不再需要某個紋理時回收顯存資源。為了實現這一點,我們對傳入釋放函數的指針進行了轉換和統一命名,確保其在整個系統中能清晰地被識別和追蹤。
核心邏輯詳解:
-
句柄轉換與識別:
平臺接口中,釋放紋理函數接收到的紋理標識是以void*
類型傳入的。為了能夠調用 OpenGL 的glDeleteTextures
,我們需要將其轉換為實際的GLuint
類型,即 OpenGL 的紋理句柄。這個轉換是安全的,并與申請階段的返回值保持一致。 -
追蹤紋理句柄來源:
為了在資源管理中保持一致性,我們回溯了紋理句柄的生成與儲存位置。發現原始的句柄在LoadedBitmap
結構體中被記錄,而此結構體本身很可能掛在某個更大的系統模塊下,例如渲染隊列或資源表等。 -
統一命名與清晰性改進:
原本該字段可能命名不清(例如僅為Handle
),導致無法直觀判斷其用途。于是對該成員進行了重命名為TextureHandle
,并在代碼中所有引用到它的位置統一修改,確保其含義明確,避免混淆。這樣一來,任何看到該字段的開發者都能立刻理解其為 GPU 上的紋理標識。 -
紋理句柄位置重構:
為了進一步優化結構設計,我們確認了該句柄應該屬于LoadedBitmap
類型中的一員,而非渲染分組或其他結構體中的字段。這樣能夠更合理地劃分責任,使LoadedBitmap
成為圖像資源在 GPU 與內存間狀態的橋梁。 -
恢復誤操作與編輯效率提升:
在過程中出現了誤刪或誤操作的情況,快速回顧并學習了撤銷/重做操作的快捷鍵,避免造成實際數據丟失,同時提升了開發效率。
整體結果與價值:
通過以上修改與整理:
- 明確了紋理句柄的生命周期,從生成、綁定、上傳到最終釋放,建立了完整閉環;
- 提高了代碼可讀性與結構清晰度,通過統一命名確保代碼在協作和維護過程中更易理解;
- 為后續的資源管理提供了良好基礎,避免了句柄誤用或內存泄漏等潛在問題;
- 保證了 OpenGL 接口調用的合法性與正確性,使平臺層與渲染層的集成更加順暢。
這一步完成后,紋理的分配與釋放流程就完全具備了,配合資源系統就能實現自動化的顯存管理。
在現階段,我們已經實現了紋理的后臺下載邏輯。通過 OpenGL 接口,利用 glTexImage2D
完成了紋理數據的上傳,并使用 glGenTextures
生成句柄,再通過 glBindTexture
完成綁定,最后解除綁定確保狀態干凈,避免后臺線程造成干擾。整體流程現在已經可以在后臺自動加載紋理。
當前進展與核心內容整理:
已完成部分:
-
紋理后臺加載機制搭建
- 下載線程中已能根據數據動態生成 OpenGL 紋理并上傳;
- 使用默認的內部格式與采樣設置;
- 上傳完成后立即解除綁定,保持 OpenGL 狀態整潔。
-
紋理句柄統一管理與命名
- 紋理句柄在
LoadedBitmap
中進行存儲; - 重命名為
TextureHandle
,明確表示用途,便于追蹤與管理。
- 紋理句柄在
-
平臺接口構建完成
- 已完成
PlatformAllocateTexture
和PlatformDeallocateTexture
函數; - 下載過程中申請紋理,資源釋放時手動告知 OpenGL 回收。
- 已完成
當前待解決問題:
-
資源釋放(Eviction)時類型判斷困難
在執行資產回收(eviction)邏輯時,需要決定是否要調用DeallocateTexture
,但當前結構中沒有清晰的類型標識來判斷某一資產是否是紋理(Bitmap)。 -
AssetMemoryHeader
信息不足- 當前僅包含索引、大小、代數信息;
- 沒有資產類型字段,因此在釋放資產時無法得知它是否屬于紋理資源,無法安全調用 OpenGL 接口釋放顯存。
-
需要擴展結構以記錄資產類型
- 建議在
AssetMemoryHeader
中新增字段,例如AssetType
; - 該字段在資源加載時設置,記錄資源是紋理、聲音、字體等;
- 在釋放邏輯中讀取此字段決定是否需要釋放 OpenGL 資源。
- 建議在
后續修改計劃:
-
擴展
AssetMemoryHeader
添加字段AssetType Type;
,枚舉值包含AssetType_Bitmap
、AssetType_Sound
等。 -
在資源加載過程中初始化類型字段
在加載紋理等資產時,設置對應的Type = AssetType_Bitmap
,以便回收階段識別。 -
更新釋放邏輯
在資源回收前判斷AssetType
,若為Bitmap
,則調用PlatformDeallocateTexture
,傳入句柄完成 OpenGL 資源回收。
總結:
整個后臺紋理加載系統已具備完整功能路徑,從加載、上傳、句柄保存到可調用釋放接口。但在回收邏輯中,當前結構尚未支持對資源類型的判斷,存在潛在顯存泄漏風險。通過為 AssetMemoryHeader
增加類型字段,可以完善整個生命周期管理機制,確保后臺資源清理邏輯與顯存狀態保持同步。這是下一步工作的重點。
GLuint Handle
不能直接、安全地轉成 void*
是因為這兩個類型在底層表示和語義上是不等價的,直接轉換存在類型不匹配與潛在平臺不兼容的問題。我們來詳細解釋為什么這么做有風險,并給出更安全的替代方案。
為什么不能直接 (void *)Handle
:
1. GLuint
是整數類型,而 void*
是指針類型
GLuint
實際上是一個無符號整型(通常是unsigned int
);void*
是一個通用指針類型,它代表某塊內存地址;- 把一個數字(如
5
)強制當成內存地址使用,在很多平臺上是未定義行為或者會引發崩潰。
2. 指針大小 ≠ 整數大小
- 在某些平臺(特別是 64 位系統)中,
void*
是 8 字節,而GLuint
仍然是 4 字節; - 把 32 位的
GLuint
轉為 64 位指針,有可能造成高位垃圾數據,逆轉時也可能出現不一致; - 在某些編譯器下還可能直接報錯或警告(例如使用
-Wint-to-void-pointer-cast
)。
3. 違背類型語義和API設計初衷
- OpenGL 返回的
GLuint
是用于作為句柄的整數,不應被當作內存指針傳遞; - 用
void*
表示紋理句柄本質上是語義不清的設計(濫用 void*),會讓后續閱讀代碼或維護變得困難。
正確做法推薦
方法一:使用平臺無關的 uintptr_t
中轉
這是 C/C++ 標準提供的類型,保證可以無損地轉換整數和指針。
#include <stdint.h>return (void *)(uintptr_t)Handle;
反向轉換時:
GLuint Handle = (GLuint)(uintptr_t)Pointer;
uintptr_t
是一個足夠大的無符號整型,用來安全地裝得下指針。
方法二:定義自己的紋理句柄類型
例如:
typedef struct TextureHandle {GLuint GLHandle;
} TextureHandle;
然后平臺返回:
TextureHandle *Result = AllocateFromMemory(...);
Result->GLHandle = Handle;
return (void *)Result;
這樣能保持類型安全,語義清晰,還便于未來擴展。
總結
直接將 GLuint
轉為 void*
是不安全的行為,在 64 位平臺可能會導致數據丟失或未定義行為。推薦使用 uintptr_t
做中轉,或使用更明確的結構體來傳遞句柄。這不僅能確保代碼跨平臺穩定運行,也使得整體結構更清晰、易于維護。
game_asset.h:在 asset_memory_header 中添加 AssetType
在這一部分,我們處理的是資源系統中如何跟蹤和識別資源類型,特別是在釋放資源(比如紋理)時能準確知道它的類型,以便執行對應的清理邏輯。以下是核心內容的詳細整理總結:
問題背景
我們之前的資源系統設計中,并沒有存儲每個資源的類型(比如紋理、聲音、字體等)。因為在加載和使用過程中,通過上下文就能判斷類型,所以也就沒有顯式記錄。這個做法雖然“巧妙”,但在釋放資源時就出現了問題:我們需要知道資源的類型,才能決定是否需要通知 OpenGL 去刪除一個紋理等。這時候如果沒有類型信息,就沒法正確處理資源釋放。
解決思路
我們決定在資源頭(Asset Memory Header)中添加一個新的字段來標識資源類型,這樣即使系統后來不再持有資源,也可以通過資源頭知道它曾經代表的是哪種類型,從而做出正確處理。步驟如下:
-
添加類型字段
- 定義一個枚舉(例如
asset_type_bitmap
、asset_type_sound
、asset_type_font
等)。 - 在資源頭結構中新增一個字段
AssetType
來存儲這個枚舉。
- 定義一個枚舉(例如
-
資源分配時設定類型
- 在資源實際分配內存的時候(
AcquireAssetMemory
函數),新增一個參數來指定該資源的類型。 - 在這個函數中,將傳入的類型保存到資源頭中的
AssetType
字段。
- 在資源實際分配內存的時候(
-
資源釋放時根據類型判斷處理邏輯
- 在釋放資源(比如位圖紋理)的時候,通過資源頭中的
AssetType
判斷是否需要調用平臺層的DeallocateTexture
函數。
- 在釋放資源(比如位圖紋理)的時候,通過資源頭中的
實施細節
- 在
AcquireAssetMemory
中添加AssetType
參數,使得每次分配內存時都要求明確標注資源類型。 - 修改資源加載函數(如
LoadBitmapAsset
、LoadSoundAsset
等),在它們調用分配內存函數時傳入正確的資源類型。 - 在資源頭插入鏈表的流程中(如
InsertAssetHeaderAtFront
)也確保AssetType
被正確設置。 - 對于還未分類或暫不處理的類型,可以默認設為
AssetType_None
。
最終效果
通過上述更改,現在我們能夠:
- 明確每個資源的類型;
- 在資源釋放時執行正確的處理邏輯(例如調用 OpenGL 的
glDeleteTextures
); - 保持資源系統的整潔和可維護性;
- 為將來支持更多類型的資源(如動畫、粒子系統等)打下基礎。
備注
雖然之前系統“巧妙地”避開了存儲類型這件事,節省了數據結構的復雜度,但一旦進入資源管理和生命周期控制層面,這類信息就變得不可或缺。及時補充這種結構性信息,是保持系統魯棒性的重要一環。
運行游戲,什么也沒看到,就這樣結束了
目前所有用于實現紋理下載的基礎設施已經搭建完成。接下來需要做的就是調試,找出紋理未正確加載的情況,確保資源在需要時能被正確獲取。整體的邏輯框架已經完備,現在只剩下調試和細節修正的階段。
此外,還有一個額外可選的優化措施可以考慮:使用 OpenGL 的“fence”(柵欄)機制來同步紋理下載。該機制的作用是在紋理尚未下載完成時自動暫停 OpenGL 渲染流程,等待資源準備就緒,從而避免使用未就緒的紋理造成錯誤。這項機制的使用取決于實際需求——如果系統總是能在正確的時間完成紋理的下載,那么就無需引入 fence;否則可以通過 fence 來保障一致性與穩定性。
在代碼中已預留了用于實現該機制的接口,雖然暫時還未決定是否啟用,但未來如有需要,可以很容易將其集成進來。簡而言之,目前的架構已支持完整的紋理下載流程,同時也為進一步的同步機制擴展做好了準備。現在的重點轉向調試和測試階段,確保整個系統在運行時的穩定性和正確性。
Q&A
如果機器中有多個顯卡,能告訴 OpenGL 使用哪個顯卡嗎?
在一臺設備中,如果存在多個顯卡,是否可以通過 OpenGL 指定使用某一個顯卡,這個問題的答案是:簡短地說,不可以;詳細來說,取決于具體情況。
首先,如果這臺機器中有兩塊顯卡,而且來自不同廠商(例如一塊是 AMD,另一塊是 NVIDIA),那么系統通常只會加載其中一個廠商的驅動程序,OpenGL 只能與當前被加載的驅動進行通信。也就是說,沒有辦法在 OpenGL 中手動選擇與哪一塊顯卡通信,因為上下文只會映射到當前活躍的那個驅動上。
如果兩塊顯卡來自同一家廠商,例如都是 NVIDIA 的顯卡,那么就稍微復雜一點。此時它們共享一個驅動,OpenGL 會根據用戶的系統配置自動決定使用哪塊顯卡,比如是否啟用了 SLI(多卡互聯),或者是獨立運行。開發者通常不能干預這個過程。
然而,對于某些特定顯卡類型,例如 NVIDIA 的 Quadro 系列,存在專門的圖形 API 接口,可以列舉系統中的所有顯卡,并為某一個特定顯卡創建 OpenGL 上下文,從而實現手動選擇顯卡的功能。但這項能力只限于 Quadro 這樣的專業卡。如果不是 Quadro,NVIDIA 的消費級顯卡就不支持這種方式。
而在 AMD 顯卡中,有類似的機制存在,可以通過 AMD 提供的接口選擇使用的顯卡(例如在多卡情況下分別使用),不過細節可能有所不同,但從理論上來說是可以的。
至于 Intel 的集成顯卡,通常系統中不會出現兩塊 Intel GPU 的情況。但在極端的配置中,比如雙插槽的 Xeon Skylake 系統,也可能出現兩個集成 GPU 的場景。不過這種情況極其少見,關于 OpenGL 如何處理這種配置,目前沒有確切的結論,可能系統本身會做隱藏處理。
總結:
- 不同廠商顯卡不能手動指定使用哪一塊,OpenGL 上下文只能綁定到當前加載的驅動。
- 相同廠商顯卡一般共享驅動,由系統決定使用哪一塊,開發者無法干預。
- NVIDIA Quadro 等專業卡可以手動選擇顯卡,消費級顯卡不能。
- AMD 也可能支持手動指定顯卡,視驅動支持情況而定。
- Intel GPU 的特殊情況較少見,理論上可能出現,但處理方式不明確。
因此,除非使用專業圖形工作站級別的設備并配套使用特定的 API,否則在常規使用中是無法通過 OpenGL 自行選擇顯卡的。
NVIDIA Quadro 和 AMD 顯卡(主要指 Radeon 系列)在定位、設計目標、驅動優化和價格等方面存在顯著區別。以下是它們之間的詳細對比,幫助我們理解兩者各自的優勢和適用場景。
一、定位與用途
項目 | NVIDIA Quadro | AMD Radeon(或 Radeon Pro) |
---|---|---|
定位 | 專業圖形工作站顯卡 | 消費級游戲顯卡(Radeon) / 專業圖形顯卡(Radeon Pro) |
適用領域 | CAD、3D 建模、工業仿真、科學可視化、影視后期等專業工作 | 游戲娛樂、日常使用(Radeon),部分專業應用(Radeon Pro) |
二、驅動與軟件支持
項目 | NVIDIA Quadro | AMD Radeon / Pro |
---|---|---|
驅動優化 | 專門為 AutoCAD、SolidWorks、Maya、3ds Max、Adobe 等軟件優化,經過認證測試,穩定性極高 | Radeon 驅動更偏向游戲性能;Radeon Pro 驅動也提供部分專業軟件優化,但不如 Quadro 完善 |
OpenGL/DirectX 支持 | OpenGL 支持強、兼容性好,適合專業圖形處理 | Radeon 對 DirectX 優化更好,OpenGL 相對較弱(Radeon Pro 改善了一些) |
專業認證(ISV) | 擁有大量 ISV(獨立軟件開發商)認證 | Radeon Pro 擁有部分認證;Radeon 無認證 |
三、硬件設計與特性
項目 | NVIDIA Quadro | AMD Radeon / Pro |
---|---|---|
顯存類型 | 多為 ECC 錯誤校驗顯存(如 ECC GDDR6 或 HBM2) | Radeon 多為 GDDR6,Radeon Pro 有時使用 ECC 顯存 |
精度支持 | 更高的雙精度浮點性能(FP64),適合科學計算 | Radeon FP64 性能一般,Radeon Pro 相對提升 |
穩定性 | 設計上偏向長時間高負載下穩定運行 | Radeon 偏向游戲場景,長期穩定性不如專業卡 |
散熱與結構 | 針對工作站優化設計,雙槽結構,多為渦輪風扇便于機架散熱 | Radeon 多為開放式散熱,更適合游戲機箱散熱結構 |
四、價格與性價比
項目 | NVIDIA Quadro | AMD Radeon / Pro |
---|---|---|
價格 | 通常非常昂貴,數千至數萬元人民幣 | Radeon 性價比高,適合預算有限;Radeon Pro 價格介于兩者之間 |
性價比 | 對于專業需求來說非常值,但價格昂貴 | 游戲或輕度專業應用,Radeon 更劃算 |
五、適用人群推薦
類型 | 推薦顯卡 |
---|---|
建筑設計、工業設計、工程模擬 | NVIDIA Quadro / AMD Radeon Pro |
圖形渲染、影視后期、視覺特效 | NVIDIA Quadro |
科學計算、深度學習(雙精度要求高) | NVIDIA Quadro(如 GV100) |
游戲玩家、普通3D建模者 | AMD Radeon / NVIDIA GeForce |
預算有限但偶爾做專業任務 | AMD Radeon Pro(性價比高) |
總結一句話:
- NVIDIA Quadro 是為專業圖形工作場景打造的重型武器,穩定、精準但昂貴;
- AMD Radeon 更適合游戲和一般使用,Radeon Pro 是其面向專業市場的折中選擇,適合預算有限的開發或設計人員。
NVIDIA 是公司名,Quadro 是它旗下的一類專業顯卡系列。
更詳細解釋:
名稱 | 說明 |
---|---|
NVIDIA(英偉達) | 一家總部位于美國的知名半導體公司,專門研發圖形處理器(GPU)、AI 芯片等技術。旗下有多個顯卡產品線。 |
Quadro | 是 NVIDIA 旗下的專業顯卡產品系列,主要用于工作站和專業圖形任務,例如 CAD、建模、渲染、影視制作等。已經在 2020 年后被逐步整合為 NVIDIA RTX A 系列(如 RTX A4000、A6000)。 |
NVIDIA 顯卡產品線劃分:
系列名稱 | 用途方向 | 舉例 |
---|---|---|
GeForce | 面向游戲和消費級用戶 | GeForce RTX 4090、RTX 3080 |
Quadro(舊名) RTX A 系列(新名) | 面向專業圖形工作站用戶 | Quadro P5000、Quadro RTX 6000、RTX A4000 |
Tesla / A100 / H100 | 面向深度學習、科學計算、AI 訓練 | Tesla V100、NVIDIA A100、H100 |
Jetson | 面向嵌入式設備和邊緣 AI | Jetson Nano、Jetson Xavier NX |
NVS | 面向商業辦公和多屏幕輸出 | NVS 510、NVS 810 |
總結一句話:
- NVIDIA 是公司,Quadro 是它專門給專業人士用的顯卡系列(主要用于圖形工作站)。
- 就像蘋果是公司,MacBook Pro 是它的產品系列一樣。
永遠保持 DC 是一個好主意嗎?如果用戶在運行時更改了顯示器配置怎么辦?
我們討論了關于是否應該一直保留設備上下文(DC)的問題,尤其是在用戶更換顯示器時的情形。最終得出以下幾點結論:
核心總結:
- 永久保留設備上下文(DC)并不是最糟糕的問題,即便用戶在程序運行時更換了顯示器。
- 真正關鍵的是 OpenGL 上下文(OpenGL Context)。如果設備上下文需要重建,OpenGL 上下文會失效,這才是最需要擔心的部分。
- 用戶在運行過程中更換顯示器理論上不會破壞原始的 OpenGL 上下文——它仍然可以工作。
進一步推理:
- 即使顯示器發生更換,窗口的設備上下文并不會自動失效,所以只要窗口沒有被破壞,通常OpenGL 上下文還能正常使用。
- 如果真的發生了變化(比如顯示分辨率改變或顯卡熱插拔等),可能需要我們重新設置窗口的位置或其他窗口參數,以確保渲染和顯示能繼續正常進行。
結論:
- 更換顯示器可能導致的后果在我們的架構中不是致命問題;
- 只需關注 OpenGL 上下文的穩定性;
- 必要時只需重新定位窗口,不必完全重建上下文。
分配和釋放紋理的功能是不是應該放在 opengl.cpp 層,而不是平臺層?
我們在討論代碼結構時認為,將 DeallocateTexture
這個函數放在 OpenGL 的實現部分會比放在平臺層(Platform Layer)更加合理。
主要結論如下:
- 把
DeallocateTexture
放在 OpenGL 層更加合適。 - 原因是:在該函數的實際實現中,并不涉及任何平臺相關的調用。
- 我們只是操作 OpenGL 的資源(如紋理釋放等),這些操作本身就是跨平臺的,不依賴于底層平臺 API(比如 Windows 的 GDI 或其他平臺接口)。
- 因此將其留在 OpenGL 層邏輯上更清晰,也能避免讓平臺層承擔本不該屬于它的職責。
設計哲學延伸:
- 平臺層應當只負責與操作系統或硬件平臺打交道的部分。
- 而像 OpenGL 這樣的圖形 API,已經在某種程度上屏蔽了平臺差異,其內部的資源管理邏輯理應歸屬于 OpenGL 實現模塊。
- 這種劃分可以增強模塊化和可維護性,也能讓代碼結構更易于理解和擴展。
實際后續建議:
我們可以將 DeallocateTexture
移到 OpenGL 實現文件中,比如 opengl.cpp
,并保持平臺層只提供如窗口創建、上下文初始化等職責。這樣我們的系統在結構上會更加清晰,職責分離也更明確。
game_opengl.cpp:將 PLATFORM_ALLOCATE_TEXTURE 從 win32_game.cpp 移入
我們認為將某些資源管理邏輯從平臺層移到 OpenGL 層是完全合理的,這樣做可以增強代碼的可移植性與共享性。
主要觀點總結如下:
- 現有的資源管理方式完全可以適用于其他平臺,比如 Linux。
- 這部分代碼邏輯并不依賴特定平臺的 API,也沒有調用操作系統相關的函數,因此具有良好的通用性。
- 因此,我們可以將這部分邏輯從平臺層中提取出來,轉移到 OpenGL 模塊中。
- 這樣一來,任何平臺如果想使用相同的紋理資源加載與卸載邏輯,只需使用共享的 OpenGL 實現即可,無需再重寫對應平臺的代碼。
- 這一設計選擇使我們能夠在未來更輕松地支持多平臺(例如 Windows、Linux 等),并提高整個架構的整潔性。
后續計劃:
我們決定立刻執行這項調整,將對應函數遷移到合適的位置,并確保它具備良好的平臺兼容性和模塊獨立性。這樣一來,無論哪個平臺接入 OpenGL 都能共享這套邏輯,提升了代碼的復用率與維護效率。
微軟為什么不允許 Universal Windows Platform 游戲使用獨占全屏模式?這似乎是在自掘墳墓
我們對微軟不允許通用 Windows 平臺(UWP)游戲使用成功的全屏模式這一決策表示不解。這種做法看起來就像是在自毀前程,令人困惑。
主要內容總結如下:
- 無法理解微軟為什么不支持 UWP 游戲使用真正的全屏模式,這看起來完全不符合開發者和用戶的利益。
- 微軟作為一家龐大的公司,內部有很多不同類型的人:
- 一些人可能是有經驗、想做正確決定的工程師。
- 一些人可能缺乏經驗,無法做出合適的技術決策。
- 還有一些人可能為了自己的目標或“勝利”,會故意推動一些對整體生態不利的決策。
- 所有這些不同層次、不同動機的人,加上項目時間壓力、資源分配等多種因素混合在一起,最終就產出了像 UWP 這樣的問題平臺。
- 無論這類問題是怎么產生的,最終的結果是我們不得不去適應這樣的平臺限制,即使它們并不合理。
- 對于是否支持 UWP 游戲,態度非常明確:除非這是在 Windows 上發布游戲的唯一方式,否則絕不會選擇 UWP。
補充觀點:
- 現在的現狀是,即便知道全屏支持的限制影響體驗,也無能為力。
- 開發者只能被動接受平臺的設計缺陷。
- 對這種平臺策略持消極態度,認為它并不適合游戲開發的實際需求。
我們只能繼續觀察平臺的演變,同時盡可能避開這些不必要的限制,選擇對用戶和開發者最友好的技術路徑。
是否有辦法像紋理一樣預加載頂點/法線/顏色到顯卡(例如,在調用 glDrawElements 之前),還是通常這不是問題?
我們在這里討論的是是否可以像紋理一樣,將頂點、法線和顏色等數據預先上傳到顯卡中,以避免每幀都進行數據傳輸。
詳細總結如下:
- 實際上,頂點、法線和顏色數據的處理方式與紋理完全類似,它們同樣可以通過緩沖區(Buffer)提前上傳到顯卡,就像紋理那樣。
- 當前之所以沒有這么做,是因為我們使用的頂點數據都是動態的,每一幀都會發生變化,例如場景中的元素移動或變化頻繁。
- 因此,在這種動態性很強的場景中,每一幀都重新發送頂點數據是可以接受的,沒有必要做預傳輸優化。
- 但是,如果未來我們遇到一些靜態對象(比如背景中的樹木等):
- 它們的位置和形狀幾乎不變。
- 并且這些對象的頂點數量較多,導致數據傳輸開銷變大。
- 那么完全可以通過創建**頂點緩沖區對象(Vertex Buffer Objects, VBO)**的方式,將這些頂點一次性上傳到顯卡,只傳一次,后續直接使用即可。
- 總體來說,只要是“傳輸成本較高的靜態數據”,無論是紋理還是頂點,都適合提前上傳緩存。
- 在我們當前使用的渲染流程中,大部分資源的負擔主要集中在紋理上,而頂點數據較少且經常變化,所以暫時無需做進一步優化。
這種處理方式提供了靈活性與性能之間的權衡:對動態數據實時更新、對靜態數據提前緩存,從而在實際項目中獲得更好的渲染效率和控制力。
對于 3D 模型,是否通常是將網格上傳到 GPU 一次,然后編寫一個著色器,通過變換矩陣繪制每個模型?
在這個討論中,主要關注的是如何處理頂點數據,尤其是在渲染時,是否可以通過優化來減少頂點數據的傳輸開銷。
詳細總結如下:
-
關于上傳網格數據:
- 在一般情況下,如果涉及到靜態網格或復雜的3D模型,確實可以提前上傳頂點數據,并使用矩陣進行轉換,從而在渲染時減少數據傳輸的開銷。這種做法對于大型模型尤其有效,尤其是當模型包含成千上萬的頂點時。
-
對于當前的精靈渲染:
- 在當前的精靈渲染場景中,每個精靈僅由四個頂點表示,且每個精靈都是一個簡單的矩形。因此,頂點數據傳輸的開銷非常小。
- 如果頂點傳輸的開銷過高,還可以通過**幾何著色器(Geometry Shader)或細分著色器(Tessellation Shader)**等技術來減少頂點數量。比如,程序只需傳遞一個頂點,系統可以通過著色器程序生成其它所需的頂點,這樣就能有效地降低頂點數據傳輸的需求。
-
頂點傳輸與變換的開銷:
- 即使是靜態網格的傳輸,通常發送的變換矩陣的大小也會比發送每個頂點的開銷大。因此,對于精靈這種簡單的圖形,頂點數據的傳輸開銷是非常小的,幾乎不值得擔心。
- 對于更復雜的情況,例如包含數十萬頂點的模型,可能需要像紋理一樣對頂點數據進行優化,使用**緩沖區對象(VBO)**等技術預先上傳數據,從而減少每幀傳輸的開銷。
-
動態頂點數據:
- 當前的精靈數據是動態的,即每一幀都會改變位置,因此每一幀都需要更新頂點信息。如果頂點數據只是靜態的,可以通過提前上傳頂點數據并使用變換矩陣來減少每幀的數據傳輸,但對于動態數據,這種方法不太適用。
- 在當前的精靈渲染場景中,每一幀都需要傳遞的位置數據是非常簡單的,因此沒有必要做進一步的優化。
-
壓縮頂點數據:
- 如果有需要,可以將頂點數據壓縮,只傳遞中心點和縮放信息,通過幾何著色器再生成完整的頂點。這種方法可以減少頂點數據的傳輸量,尤其是在頂點變化較少的情況下。
總體而言,對于當前的精靈渲染,頂點數據傳輸的開銷非常小,不需要過多優化。而對于復雜的3D模型,使用緩沖區和幾何著色器等技術來優化頂點數據的傳輸是非常有效的。
如何才能讀懂那些一字母的變量代碼?
在這個討論中,主要涉及了關于代碼中的單字母變量以及如何閱讀和理解這類代碼的內容。
詳細總結如下:
-
單字母變量的使用:
- 討論中提到,使用單字母變量名的代碼可以通過上下文推斷來理解其含義。這種寫法使得代碼變得更簡潔,但可能會導致可讀性降低,尤其是對于不熟悉代碼的人。
- 盡管使用單字母變量(如
w
,i
,j
)是為了提高代碼的簡潔性,但有時在需要解釋時,還是會使用較長的變量名,這樣就能在保持簡潔的同時,確保重要部分能夠明確理解。
-
代碼的可讀性和簡潔性:
- 代碼的簡潔性通常取決于上下文的清晰度,如果變量的意義可以從上下文中推斷出來,使用簡短的變量名是可以接受的。
- 例如,在處理循環變量時,使用
i
,j
,k
等短變量名是常見做法,因為這些通常用于計數,且其含義可以從上下文推知。
-
簡化與可讀性平衡:
- 對于一些較復雜的操作,雖然使用了簡化的變量名,但仍然會在需要說明的地方使用長變量名來確保代碼可理解。
- 討論中提到,即使使用簡短的變量名,如果某些內容需要額外解釋,還是會用較長的名字來確保不混淆,如
width
或height
會更明確。
-
代碼風格的差異:
- 不同的開發者有不同的編碼風格。一些開發者可能偏向于使用更多的簡寫和短變量名,而另一些則可能使用更長、更具描述性的變量名。這主要取決于開發者的個人偏好以及代碼所處的上下文。
- 討論還提到,簡化的代碼風格可以讓代碼更加緊湊和快速,但可能不適合所有場合。某些情況下,代碼的清晰性和可維護性比簡潔性更重要。
-
總結:
- 總體來說,單字母變量名的使用是可以的,只要上下文明確且有合理的假設,可以幫助代碼更簡潔高效。但對于更復雜的部分,仍然需要適當使用長變量名來確保代碼的可讀性和可維護性。
這段討論表明了在編碼過程中,如何在簡潔性與可讀性之間找到平衡,并且強調了根據上下文來判斷是否使用簡短變量名是合理的。