CppCon 2017 學習:Design Patterns for Low-Level Real-Time Rendering

這段內容講的是離散顯卡(Discrete GPU)中的內存管理模型,重點是CPU和GPU各自獨立管理自己的物理內存,以及它們如何通過虛擬內存和DMA引擎實現高效通信。以下是詳細的理解和梳理:

1. 基本概念

  • CPU 和 GPU 是兩個獨立的處理單元
  • 它們各自擁有自己的物理內存區域:
    • CPU的物理內存叫 System Memory(系統內存,即RAM)
    • GPU的物理內存叫 Video Memory(顯存,即VRAM)

2. 虛擬內存和內存管理單元(MMU)

  • CPU通過**內存管理單元(MMU)**訪問系統內存
  • MMU 提供虛擬內存支持,使得:
    • 物理內存中不連續的內存頁,可以映射為連續的虛擬地址空間
    • 允許操作系統將不常用內存頁暫存到磁盤,實現“虛擬內存擴展”
  • GPU也有自己的MMU,實現類似的虛擬內存抽象,管理顯存

3. CPU和GPU共享內存的兩種情況

  • CPU物理內存(系統內存)映射給GPU訪問
    • 系統內存中的某些物理頁,被映射到CPU和GPU的虛擬地址空間中
    • CPU和GPU都可以通過自己的虛擬地址訪問這塊共享的物理內存
    • 這實現了CPU和GPU間通過內存通信的通道
  • GPU物理內存(顯存)映射給CPU訪問
    • 顯存中的物理頁也可以映射到CPU和GPU的虛擬地址空間
    • 這樣CPU可以直接訪問顯存中的數據

4. DMA引擎(Direct Memory Access)

  • DMA引擎是GPU內部的專用硬件,用于高效地在CPU內存和GPU顯存間復制數據
  • 重要的是,DMA通常不處理虛擬內存,只能直接對物理內存進行操作
  • 因為物理內存頁通常是不連續的,DMA復制時需要逐頁操作

5. 總結和理解

角色作用及特點
CPU有自己的系統內存和MMU,管理虛擬內存
GPU有自己的顯存和MMU,管理自己的虛擬內存
虛擬內存將不連續的物理頁映射為連續的虛擬地址
CPU/GPU共享內存系統內存或顯存頁可以映射到雙方的虛擬地址空間,支持通信
DMA引擎高效復制CPU <-> GPU物理內存數據,需逐頁復制
這套模型幫助理解離散顯卡內存架構的復雜性,尤其是為什么數據復制效率不易達到理論最高,以及現代GPU/CPU設計如何通過虛擬內存和DMA硬件優化性能。

集成顯卡(Integrated GPU)中的內存管理模型,和之前的離散顯卡相比,有些不同。下面幫你詳細解讀:

1. 集成顯卡的CPU和GPU關系

  • CPU和GPU幾乎是合成一體的單個芯片或單元
  • 他們共享同一塊物理內存區域(系統內存),不再是分開的系統內存和顯存
  • 但CPU和GPU依然擁有各自獨立的虛擬地址空間
    • 這意味著雖然物理內存共享,但兩者的虛擬地址映射可能不同
    • 理論上存在共享虛擬地址空間的可能(如OpenCL的共享虛擬內存SVM),但并不通用,不能依賴

2. 共享物理內存,但虛擬地址空間分離

  • CPU和GPU訪問的是同一塊物理內存(系統內存),
  • 但各自的MMU管理各自的虛擬地址映射
  • 這種情況下,CPU和GPU可以直接通過物理內存通信,避免了之前離散顯卡中頻繁的跨內存拷貝

3. 文件系統和頁面調度的限制

  • GPU訪問的系統內存不連接文件系統
    • 這點和離散GPU類似
    • 也就是說,GPU不能像CPU一樣依賴操作系統的頁面交換機制(如頁面置換到硬盤)
    • GPU無法利用文件系統進行“自動頁面缺失(page fault)”處理
  • 雖然技術上可以實現GPU頁缺失機制,但這不是主流或廣泛支持的用法

4. 總結和理解

方面說明
CPU & GPU物理上融合為一個單元,使用同一塊物理內存
虛擬地址空間CPU和GPU有各自獨立的虛擬地址空間
共享虛擬地址空間存在可能(如OpenCL SVM),但不可假設普遍支持
文件系統支持GPU訪問的內存不連接文件系統,無法使用文件系統頁面調度
性能影響共享物理內存減少數據拷貝成本,但缺頁處理受限

你可以想象

  • 離散GPU就像是兩臺電腦互相通過網線(PCIe)通信,數據需要復制通過通道
  • 集成GPU則像是一個電腦內的兩個核心共享一塊內存,但每個核心訪問內存的“視角”不同(各自虛擬地址空間)

這部分內容講的是 Command Lists(命令列表) 在CPU和GPU協作中的作用,下面幫你詳細拆解和理解:

1. CPU和GPU的執行模型差異

  • CPU是**亂序執行(Out-of-Order)**的處理器
    • 可以動態調整指令執行順序,以提升效率(比如推測執行)
  • GPU是**順序執行(In-Order)**的處理器
    • 嚴格按照命令給出的順序執行任務,不會亂序
    • 這就要求提交給GPU的工作順序必須合理高效

2. Command Lists是什么?

  • 命令列表是CPU準備好的“工作清單”
  • CPU負責構建這份命令調度(Schedule),組織好任務順序
  • GPU接收到命令列表后,順序執行其中的命令

3. 類比:Gromit和火車

  • 火車代表GPU,只能沿著鋪好的軌道前進(嚴格順序執行)
  • Gromit代表CPU,負責提前鋪好軌道(生成命令列表)
  • 軌道即命令列表,必須鋪設合理,火車才能順暢運行

4. Fences(柵欄)是什么?

  • Fence是一個操作系統對象,用來監控GPU的執行進度
  • 舉例:初始Fence值是0
    • GPU執行命令,執行到第一個Fence位置時,Fence值變成1
    • 表示GPU完成了第1段命令
    • GPU繼續執行,到達Fence 2時,Fence值變成2,依此類推
  • 通過Fence,CPU或系統可以檢測GPU執行到什么程度了,方便同步或資源管理

5. 總結

角色作用
CPU準備命令列表(合理安排任務順序)
GPU按命令列表順序執行任務
Command ListCPU提交給GPU的任務清單
Fence用于追蹤GPU執行進度的同步機制

如何利用Fence實現CPU和GPU之間的同步

場景說明

  • Fence初始值是1(這個值一般是任意的起始狀態)
  • CPU準備了一串命令列表,并在命令列表的末尾放了一個特殊命令——signal(信號命令)
  • 這個signal命令的作用是:當GPU執行到這里時,會把Fence的值從1更新到2

執行流程

  1. CPU階段
    • CPU生成命令列表,命令序列可能是:cmd1, cmd2, cmd3, …, signal
    • signal命令附帶的效果是“將Fence的值設為2”
  2. GPU階段
    • GPU接收命令列表,順序執行
    • 執行每條命令,最后執行signal命令時,GPU會把Fence值從1改成2
  3. 同步機制
    • CPU通過檢查Fence的值是否≥2來判斷GPU是否完成了命令列表中的所有工作
    • 如果Fence≥2,CPU就知道GPU已經執行到signal命令之后了,意味著GPU完成了之前的所有命令
    • 同理,GPU內部也能用Fence進行同步,比如多個命令隊列之間協調執行順序

形象解釋

  • Fence就是一個“進度條”或者“里程碑”,它的值隨著GPU執行進度遞增
  • CPU可以通過Fence知道GPU跑到了哪一步,從而決定下一步的操作(比如是否提交新的命令、釋放資源等)
  • GPU也能利用Fence同步自身的不同工作流

總結表格

角色行為
CPU生成命令列表,并在結尾加上signal命令,表示GPU執行到這兒時更新Fence的值
GPU按順序執行命令,執行到signal時更新Fence值
Fence共享變量,CPU和GPU通過檢查它的值來判斷GPU執行進度,實現同步

GPU 命令列表中常見的命令類型及其背后的抽象模型,我來幫你系統性地理解這些內容:

大致分類(GPU 命令的種類)

GPU 接收的命令并不是像 CPU 那樣執行通用指令,而是高度結構化的“任務請求”,主要分為以下幾類:

1. DMA 拷貝類(數據傳輸命令)

示例:
Copy{src, dst}
  • 用于在 CPU 和 GPU、或 GPU 的不同區域之間復制數據
  • 通常映射到底層的 DMA Engine,能實現高性能、異步的數據拷貝

2. 執行 GPU 程序(計算/繪圖調用)

示例:
SetProgram{program}
SetParams{buf, tex, 1337}
Draw{N vertices}     // 圖形渲染
Dispatch{N threads}  // 通用計算(GPGPU)
  • 類似于函數調用的抽象模型:
    1. 設置程序(類似函數名)
    2. 設置參數(函數參數)
    3. 發起執行(函數調用)
      GPU編程流程就像是:
set_registers(params);
jump_to_shader_entry();

3. 渲染控制命令(圖形管線配置)

示例:
SetRenderTarget{rt}
SetGfxPipeline{pipeline}
  • 設置渲染目標(比如一塊屏幕區域或紋理)
  • 設置圖形流水線(配置如光柵化、混合、深度測試等狀態)

4. 內存 & 對象模型(資源管理與狀態同步)

示例:
MemoryBarrier{object}
Transition{object, a, b}
Construct{object}
Destruct{object}
  • MemoryBarrier:手動控制數據一致性(GPU 不像 CPU 有自動緩存一致性機制)
  • Transition:GPU 的某些資源在不同用途間需要顯式“狀態轉換”
    • 例如:一張紋理從“渲染輸出”變為“采樣輸入”,需要做狀態轉換
  • Construct / Destruct
    • 抽象上的資源生命周期控制
    • 類似 C++ 的 placement new/delete
    • 實際底層不會叫這個名字,而是分成一堆底層資源創建與釋放命令

思維類比:函數調用 vs GPU命令序列

CPU/C++GPU Command List
設置函數參數SetParams{...}
調用函數Dispatch{...}Draw{...}
析構對象Destruct{object}(抽象)
局部變量構造Construct{object}(抽象)

小結

  • GPU 命令本質上是一個面向任務的指令模型,種類清晰、目標明確
  • 命令的構成是高度結構化的,分配了明確的職責,比如數據傳輸、程序調用、渲染狀態設置、資源同步等
  • 類似 CPU 的函數調用和內存管理機制,但需要手動控制同步、狀態轉換和資源生命周期

GPU 編程中一個非常關鍵但經常被忽視的細節 —— 命令參數的讀取時機(indirection,間接尋址),我來幫你逐步理解:

核心問題:命令參數何時讀取?

GPU 在什么時候讀取命令參數?是在CPU 錄制命令的時候,還是在GPU 執行命令的時候

兩種方式的對比

方式含義示例特點
Record-Time(錄制時)參數在 CPU 構建命令列表時就確定了Dispatch{512}更快
更容易優化
靈活性差
Execute-Time(執行時)參數在 GPU 執行時從內存中讀取DispatchIndirect{&512}更靈活
數據驅動
可能較慢

舉個例子說明:

Dispatch{N_threads}

// 直接記錄:512 個線程
command_list.record(Dispatch{512});
  • 參數是個立即數(value)
  • GPU 可以在命令列表生成階段提前優化好調度
  • 不依賴運行時數據

DispatchIndirect{&N_threads}

// 從內存中讀取線程數
uint32_t N = ComputeThreadCountSomehow();
command_list.record(DispatchIndirect{&N});
  • 參數是引用 / 指針
  • GPU 執行時從內存中讀取這個值
  • 可以根據其他計算結果動態決定線程數
  • 更靈活,但更難優化

Trade-off 總結

方面Record-Time 參數Execute-Time 參數(Indirect)
性能更高,易優化可能慢,不易推測
靈活性高(數據驅動、邏輯更豐富)
實現復雜度
示例Draw{100}DrawIndirect{&count}

實際應用場景

  • Record-time(立即值)適合固定流程:大部分渲染、預定義計算
  • Execute-time(間接尋址)適合數據驅動流程:基于之前 GPU 輸出決定下一步行為(如 GPU 級剔除后的渲染)

接下來的話題:Descriptors

你這段最后提到要進入 descriptors —— 它跟資源綁定密切相關,比如紋理、緩沖區綁定到著色器,是現代 GPU API(如 Vulkan、D3D12)中非常核心的機制。

GPU 編程中極為重要的概念 —— Descriptor(描述符),它是現代圖形/計算 API(如 D3D12、Vulkan、Metal)核心機制之一。下面是逐點解析,幫助你真正吃透這個概念:

什么是 Descriptor?

簡單說:Descriptor 就是 GPU 眼中對資源的定義

  • 描述符 = 內存地址 + 元數據
  • 描述的是 buffertexture 的布局與位置

類比理解

你可以把 descriptor 想象成一本圖書館里的卡片:

  • 地址(Address):告訴你書在哪一排哪一列(物理地址)
  • 元數據(Metadata):告訴你這本書有幾頁、是什么語言、是否彩印(數據大小、類型、格式等)

例子:AMD GCN3 Texture Descriptor(128 位)

在 GCN3 架構中,一個紋理描述符的具體布局如下:

位范圍含義
[39:0]Address:紋理內存地址
[77:64]Width:寬度(像素)
[91:78]Height:高度(像素)
[127:92]其他標志/格式信息
這些字段告訴 GPU:
  • 要訪問哪一塊內存?
  • 數據的形狀是什么?
  • 怎么解釋這塊數據?

用法示例:偽裝配代碼

dst = image_sample(xy, texture_descriptor, sampler_descriptor);

解釋:

  • texture_descriptor:告訴 GPU 從哪里讀取紋理、如何解釋紋理
  • sampler_descriptor:告訴 GPU 如何采樣(過濾、邊緣處理等)
  • xy:采樣坐標
  • dst:結果寫入的位置(顏色、深度等)
    這個調用最終會變成底層硬件執行的采樣操作,離開了 descriptor,GPU 連紋理在哪都不知道。

為什么需要 Descriptor?

  • GPU 是高度并行的,需要能獨立高效地訪問資源
  • 每個線程不能靠 CPU 傳參,它需要從描述符表中快速查表
  • 提前定義好描述符能讓 GPU 直接執行,不需要解釋

延伸:Descriptor Heap / Table

在 D3D12、Vulkan 中,你會維護一張 “描述符表” 或 “描述符堆”:

  • 就像 GPU 的“資源目錄”
  • 每個描述符代表一個資源(紋理、緩沖區等)
  • 著色器訪問資源時,不是直接用地址,而是通過“索引 + 根參數 + offset”來找到 descriptor

總結記憶

項目內容
什么是 Descriptor?GPU 用來訪問資源的硬件對象:地址 + 元數據
描述哪些資源?Texture、Buffer、Sampler 等
存儲在哪里?通常在描述符表 / 堆中,供 GPU 索引
為什么重要?GPU 無需 CPU 干預即可并行訪問資源
示例?image_sample(xy, texture_desc, sampler_desc);

這部分講的是 實時渲染器架構(Real-Time Renderer Architecture) 的大局觀設計 —— 即如何把“游戲世界的邏輯”轉換成“屏幕上的圖像”。我們來逐步拆解,幫你建立對整套架構的清晰認知:

實時渲染系統:核心流程圖解

        Continuous              DiscreteBehaviors               Events↓                      ↓+------------------------------+|           Simulation         |   ← 游戲邏輯|      ("Game Objects")        |+------------------------------+↓Update Scene Graph↓+------------------------------+|             Scene            |   ← 高層描述:幾何體、材質、燈光等+------------------------------+↓+------------------------------+|           Renderer           |   ← 將場景轉為GPU指令(圖形資源)+------------------------------+↓Submit Commands↓GPU Execution↓+------------------------------+|          Swap Chain          |   ← 雙緩沖幀管理(顯示幀切換)+------------------------------+↓Compositing / Display

概念拆解

1. Simulation(模擬)

  • 負責應用邏輯,如物理、AI、動畫、用戶輸入
  • 抽象單位是“Game Object”
  • 處理連續狀態變化 + 離散事件(如按鍵、碰撞等)

2. Scene(場景)

  • 表示“需要被渲染的世界狀態”
  • 高層資源描述,如:
    • Geometry:網格、頂點數據
    • Material:表面著色邏輯、紋理
    • Instances:幾何體的具體位置與變換
    • Camera:觀察視角
    • Light:光照源及其類型/位置/顏色等
      Scene ≠ Renderer,它只是個邏輯數據結構,不關心如何畫。

3. Renderer(渲染器)

  • 讀取 Scene,生成圖形命令(Command Lists)
  • 管理 GPU 資源:
    • Buffers:存儲數據(如變換矩陣、頂點、索引)
    • Textures:紋理圖像
    • Shaders:程序(Vertex、Fragment、Compute 等)
    • Passes:渲染流程的階段(如 Shadow Pass, G-Buffer Pass)

4. GPU 提交 + Swap Chain 顯示

  • 命令列表被提交給 GPU 執行
  • 完成后輸出到 Swap Chain:
    • 顯示幀緩存機制(通常雙緩沖/三緩沖)
    • 如果不是全屏,幀最終會被桌面 compositor 混合顯示
  • 最終呈現在顯示器上

性能目標:10~100 毫秒/幀

應用類型推薦幀時間幀率目標
快節奏游戲10~16ms60~90 FPS
普通游戲16~33ms30~60 FPS
工具類程序(如 3D 建模)可接受更高延遲可低至 15 FPS

總結記憶

模塊職責
Simulation游戲邏輯 + 控制場景內容變化
Scene高層資源組織結構
Renderer構建 GPU 指令 + 管理資源
GPU + Swap Chain執行繪制 + 顯示輸出
你可以把整個系統想象成一臺流水線工廠,Simulation 是設計圖紙、Scene 是原材料倉庫、Renderer 是裝配線操作員,GPU 是機器人臂,Swap Chain 是成品傳送帶。

這部分介紹了**Ring Buffer(環形緩沖區)**在實時渲染系統中的應用,尤其是用于 CPU → GPU 數據傳輸 的高效機制。下面是結構化總結與理解:

Ring Buffer(環形緩沖區)

基本定義

  • 是一種循環使用的連續內存區域,用來進行數據流的生產與消費。
  • 適用于CPU 和 GPU 并行工作場景(producer-consumer 模型)。

應用場景:CPU 向 GPU 傳輸數據

  • CPU:不斷地寫入新的數據。
  • GPU:異步讀取這些數據進行渲染。
  • 二者操作同一個環形緩沖區,但操作的“位置”不同,避免沖突。
    優勢:
  • 避免頻繁創建/銷毀資源。
  • 實現高效、可重用的上傳路徑。
  • 提供流式數據更新方式(如動態相機參數、動畫數據等)。

示例 API

auto [pCPU, pGPU] = pRing->Alloc<Camera>();
*pCPU = camera;
pCmdList->SetDrawParam(CAMERA_PARAM_IDX, pGPU);

解讀:

  • Alloc<T>() 分配一段用于 T 類型(如 Camera)數據的空間。
  • 返回值是二元組:一個是 pCPU(CPU 虛擬地址),另一個是 pGPU(GPU 虛擬地址)。
  • 使用 pCPU 寫數據,用 pGPU 作為繪制參數傳給命令列表。

Descriptor Ring Buffer 也是一種 Ring Buffer!

auto [pCPU, pGPU] = pDescriptorRing->Alloc(1);
WriteDescriptor(pCPU, desc);
pCmdList->SetDrawParam(TEXTURE_PARAM_IDX, pGPU);
  • 用法類似,用于分配臨時描述符(綁定紋理、緩沖等資源的元數據結構)。
  • 描述符是 GPU 使用資源時的“視圖”,包含地址和訪問方式等信息。

性能提醒:

對于 離散 GPU(如獨顯),數據最終應拷貝到 專用顯存(video memory),否則系統內存讀性能可能拖慢渲染。

總結一句話:

Ring Buffer 是連接 CPU 和 GPU 的高效流式橋梁,支持異步上傳數據或描述符,并最大限度減少內存分配成本。

這一部分深入講解了 Ring Buffer 的兩個棘手問題內存不足(Out-of-Memory)環繞(Wrap-Around),并提供了實際工程上的處理建議。

1. Ring Buffer: Out-of-Memory(內存不足)

問題場景:

  • CPU 想在 ring buffer 中分配一段內存。
  • 但這一段內存 仍在被 GPU 讀取中,尚未完成處理。

解決方法:

  • CPU 阻塞等待 GPU 完成
  • 使用 GPU-Fence 來同步:
    if (AllocWouldOverlapWithGPU()) {WaitForFence(gpuFence);
    }
    

本質:
避免 CPU 寫入還未被 GPU 消費的數據區域,確保數據一致性。

2. Ring Buffer: Wrap-Around(緩沖區環繞)

問題:

  • Ring Buffer 是循環結構,當分配位置接近尾部時,可能需要“從頭開始”。
  • 如果處理不好,可能出現 數據重疊或邏輯混亂

推薦做法 1:使用 虛擬偏移(virtual offset)

// virtualOffset 持續增長,不受 RING_SIZE 限制
uint64_t virtualOffset = ...;
uint64_t realOffset = virtualOffset & (RING_SIZE - 1);
data = buffer[realOffset];
優勢:
  • 避免 wrap-around 檢查邏輯復雜化。
  • 配合 Fence 使用更簡單:Fence value 可直接綁定 virtual offset。
  • Power-of-two 大小 ring 幫助簡化位運算。

推薦做法 2:禁用 wrap-around,按幀預分配

// 每幀分配固定大小內存
const size_t PER_FRAME_BUDGET = 64KB;
if (allocSize > PER_FRAME_BUDGET) {assert(false && "Need to increase per-frame memory budget");
}
優勢:
  • 極簡實現。
  • 保證分幀分配邏輯清晰。
  • 適合大多數典型幀時間控制的實時應用(如游戲渲染)。

推薦閱讀:

Fabian Giesen 的博客
? 深入講解 ring buffer 的數據結構原理、同步策略,以及設計不變量(invariants)。

總結一句話:

用虛擬偏移簡化 wrap-around,用 Fence 處理同步沖突,用幀預算確保穩定性 —— Ring Buffer 成為 GPU 數據流式傳輸的可靠利器。

這一節講述了如何在 Ring Buffer 中實現無鎖(Lock-Free)內存分配 —— 這對于高性能 GPU 數據流非常關鍵,尤其是在多線程或并發渲染環境下。

核心思想:用原子變量 std::atomic<uint64_t> offset 實現無鎖分配

基本做法(結構化數據):

std::atomic<uint64_t> offset;
uint64_t alloc(uint64_t n) {return offset.fetch_add(n); // 原子地分配 n 個單位
}
  • fetch_add(n) 會原子地把 offset 增加 n,返回舊值。
  • 所以你拿到的就是你該寫入的位置。
  • 多線程下也不會有數據覆蓋或競爭問題。

應對 原始數據(Raw Byte Data)+ GPU 對齊需求

GPU 通常有對齊要求,比如 DirectX 12 下最大可能要求是 512 字節對齊(如上傳貼圖數據時)。

對齊版本的分配函數:

#define WORST_ALIGNMENT 512
uint64_t aligned_alloc(uint64_t sz) {uint64_t padded_sz = (sz + WORST_ALIGNMENT - 1) & ~(WORST_ALIGNMENT - 1);uint64_t alloced = offset.fetch_add(padded_sz);assert(alloced + sz <= RING_SIZE); // 簡單溢出檢查return alloced;
}

技巧說明:

概念解釋
fetch_add原子操作,避免加鎖,天然線程安全。
對齊(Alignment)(sz + A - 1) & ~(A - 1) 是常見的向上對齊寫法。
512 字節對齊適配所有 GPU 子系統的最壞情況,雖然浪費點空間,但簡單可靠。
無鎖優勢性能極高,線程間不會互相阻塞,適合現代多核系統中的實時任務。

總結一句話:

atomic::fetch_add 實現無鎖并發分配,用統一對齊消除 GPU 對齊問題 —— 簡單、穩定、快得離譜。

Ring Buffer(環形緩沖區)在實時渲染中的優缺點,特別是在 CPU → GPU 數據流場景中的應用。以下是中文結構化的要點歸納:

Ring Buffer 優點(Pros)

優點說明
簡化內存管理統一分配策略,避免手動管理多個小 buffer
API 極其簡單通過一個 Alloc() 函數即可分配空間
避免碎片化連續線性分配,不會留下“內存洞”
多功能構建塊適用于各種用途,如幾何體上傳、紋理流、sprite 批渲染等

典型用法示例:

  • Procedural Geometry(程序化幾何體):直接上傳 vertex/index 數據。
  • Texture Streaming(紋理流式傳輸):分段上傳 mipmap 或貼圖 block。
  • Sprite Batch:合并多個小對象的繪制調用,提高效率。

Ring Buffer 缺點(Cons)

缺點說明
內存大小需要“校準”太小:性能差/易崩潰;太大:浪費寶貴顯存/系統內存
沒有統一配置方式不同用例對內存類型和對齊策略需求不同
緩存策略復雜寫合并(Write-Combined)、寫回(Write-Back)等策略影響訪問性能
物理內存選擇困難系統內存 vs. 顯存:取決于設備架構和數據使用模式

內存策略的選擇影響極大

比如:

  • 寫合并(Write-Combined, WC)
    • 對 CPU 寫入性能好,但不能頻繁讀取。
    • 不適合需要頻繁讀取或修改的 procedural data。
  • 寫回(Write-Back, WB)
    • 更通用,適合需要讀取的 CPU 數據結構。
  • 內存類型選擇
    • System Memory:適用于集成顯卡或數據上傳階段。
    • Video Memory:適合長期 GPU 訪問的資源(如材質、頂點緩沖)。

總結建議:

Ring Buffer 是非常強大的構建塊,但它不是“萬能工具”。你必須根據具體用途(渲染什么、更新頻率、訪問模式)合理設置大小、內存類型和緩存屬性。

并行命令錄制(Parallel Command Recording),這是 Vulkan 和 DirectX 12 等現代圖形 API 中的核心優化特性之一。下面是中文歸納與重點說明:

并行命令錄制:核心思想

CPU 寫命令是很重的任務。

如果你要渲染大量對象(比如成千上萬個),那么即使還沒交給 GPU 執行,僅在 CPU 上構造這些命令本身就會成為性能瓶頸。

解決方案:

命令錄制任務分配到多個 CPU 線程上并行執行,每個線程寫自己的命令緩沖區(Command List),最后把它們匯總提交給 GPU。

示例結構圖(可視化):

Thread 0:   cmd cmd cmd        ↘
Thread 1:   cmd cmd cmd cmd    → [Submit to GPU]
Thread N:   cmd cmd cmd        ↗

每個線程獨立錄制命令,最后統一提交。

簡單用例:場景中有大量“規律性”的工作

CmdList lists[NUM_JOBS];
parallel_for (jobID = 0 .. NUM_JOBS) {lists[jobID].SetRenderTarget(rt);foreach (object in job) {lists[jobID].SetGeometry(object.geometry);lists[jobID].SetMaterial(object.material);lists[jobID].Draw(object.num_vertices);}
}
Submit(lists); // 提交所有命令緩沖區

說明:

  • 將場景對象分成若干“工作塊”(jobs)。
  • 每個線程獨立處理一個 job。
  • 每個線程錄制自己的命令列表。
  • 所有命令列表錄制完后一次性提交。

實用建議

建議原因
不要過早并行化對于小批量命令(例如 UI 渲染),并行錄制得不償失
每個命令緩沖區應耗費至少 50μs 的 GPU 時間太短會導致提交開銷掩蓋了收益
每次 Submit 盡量包含 ≥500μs 的 GPU 工作量提交操作本身有代價,別太頻繁

小結

優勢應用場景
更高 CPU 并發利用率大量對象渲染(例如:開放世界游戲、海量粒子)
降低單線程瓶頸命令錄制分散到多個核心
和現代 GPU API 匹配Vulkan / DX12 等天然支持多線程命令構建

這部分講的是 “困難情況:不規則的工作量”,也就是當你渲染的對象種類很多,且每個對象準備命令所需的 CPU 工作量差異較大時,命令錄制并行化就沒那么簡單了。

核心點總結:

  • 對象異構(Heterogeneous)
    場景中有不同類型的 Drawable,比如:
    • Blob:需要運行marching cubes算法進行多邊形化
    • Subdiv:需要進行三角形細分
    • Particles:需要進行粒子分箱和排序
  • 工作量不均勻
    不同對象的命令準備時間相差很大,甚至受其它因素影響,比如物體距離攝像機的遠近或遮擋情況。

為什么這很難?

  • 很難均勻分配任務給多個線程,因為:
    • 有些線程會處理復雜對象,耗時長
    • 有些線程處理簡單對象,耗時短,導致 CPU 線程負載不均
  • 會導致線程等待,浪費多核資源,降低并行效率。

現實中的挑戰與思考:

  • 動態負載均衡:需要設計工作竊取(work stealing)或者更智能的任務分配策略,避免某些線程提前完成而空閑。
  • 優先級和剔除:可以根據距離攝像機或遮擋情況提前剔除或降低處理優先級,減少命令準備的總開銷。
  • 異步任務處理:復雜計算盡量異步或在后臺線程中進行,渲染線程專注于輕量命令錄制。

這是解決**不規則工作量(Irregular Work)**的經典方法——Fork/Join 并行

// 遞歸并行繪制函數,參數:
// cmdlist - 當前線程/任務用的命令列表
// drawables - 當前需要繪制的對象集合
void draw_par(cmdlist, drawables) {// 判斷當前任務的“工作量”是否低于閾值// 如果工作量小,直接串行繪制,避免過度拆分導致開銷過大if (drawables.cost() < WORTH_SPLITTING) {draw_seq(cmdlist, drawables);  // 串行繪制當前這批對象} else {// 否則將任務拆分為兩個子任務,分別處理左右兩半對象auto [left, right] = drawables.split();// 并行啟動繪制左半部分,繼續使用當前命令列表spawn draw_par(cmdlist, left);// 并行啟動繪制右半部分,給右邊子任務新建獨立命令列表,避免線程沖突spawn draw_par(new CmdList(), right);// 等待左右兩個子任務都完成繪制命令的錄制sync;}// 這行看起來像注釋或示意(非實際代碼)// 可能表示這里有對命令列表3的操作或者繪制// draw_seq// CmdList 3
}
// 從外部調用入口,傳入新的命令列表和所有待繪制對象,開始遞歸并行繪制
draw_par(new CmdList(), all_drawables);

核心思路:

  • 遞歸拆分任務
    把待繪制對象列表遞歸拆分成更小的子列表,直到任務足夠小(低于某個“值得拆分”的閾值 WORTH_SPLITTING),然后串行執行。
  • 多線程并行執行
    大任務拆分后,利用任務調度器(task scheduler)異步運行左右兩個子任務(spawn),并在最后用同步(sync)等待子任務完成。
  • 獨立命令列表
    每個子任務有自己的 CmdList,獨立記錄 GPU 命令,最后匯總提交。

優點:

  • 任務細分能適配不同復雜度的對象,實現負載均衡。
  • 結合任務調度器(如工作竊取 Work Stealing),能高效利用多核 CPU。
  • 代碼結構清晰,易維護。

背景問題

  • 在并行繪制(fork/join)不規則工作負載時,每個子任務通常創建獨立的命令列表(CmdList)。
  • 如果多個任務實際上在同一個CPU上順序執行,創建多個命令列表就顯得浪費,增加額外開銷。

關鍵思想:Hyperobject優化

  • Hyperobject 是一種語言特性(起源于Cilk++),用于在并行任務中管理線程/任務局部狀態。
  • 通過“偷用父任務的命令列表”,可以讓運行于同一個CPU的連續任務共用同一個命令列表,避免重復創建。
  • 這種方式減少了命令列表的數量,從而降低了管理和合并命令列表的開銷。

重要特性

  • 保持繪制順序不變
    Hyperobject管理的命令列表不會改變繪制命令的提交順序,這對于避免圖像閃爍和性能波動至關重要。
  • 優化性能
    避免過度拆分命令列表,提高CPU多線程寫命令效率。

推薦資料

  • “Reducers and Other Cilk++ Hyperobjects” 論文
  • 這些論文介紹了Cilk調度器中如何整合Hyperobject及其實現細節。

總結

使用Hyperobject優化方案,可以更智能地管理不規則并行工作負載中的命令列表,減少開銷并保持繪制順序的穩定性,是一種非常優雅的設計思路。

GPU調度(Scheduling GPU Work & Memory)核心概念

1. 大框架(Big Picture)
  • GPU的工作可以看成是一個幀圖(frame graph),由多個**渲染通道(passes)**組成。
  • 這些通道之間會通過共享的資源(如紋理Texture、緩沖Buffer)傳遞數據。
  • 調度器的任務就是根據這個依賴關系圖,按照正確且高效的順序提交GPU任務。
2. 調度職責(Duties)
  • 任務提交順序有效且高效
    需要確保依賴關系被滿足,不會提前使用未生成的數據,同時還要盡量減少GPU空閑,提升性能。
  • 資源管理
    調度器負責管理內存資源(比如紋理和緩沖區)的分配與釋放,盡可能地在資源的生命周期內使用,減少內存浪費。
    • 例如:紋理Texture 2在Pass 1中生成,Pass 2中使用,用完就釋放。
  • 動態適應
    因為實時渲染中場景內容不斷變化,調度器應支持動態重建任務圖(每幀可能不同),不能假設任務圖是靜態的。
3. 實際意義
  • 通過這種幀圖調度方式,GPU的工作被組織得更清晰且高效,避免了資源沖突和冗余占用。
  • 支持復雜渲染管線,同時保持靈活性和性能。

Scheduling: Classic Multi-Pass Approach(經典多通道渲染調度)

1. 代碼結構:
  • 第一個Pass:
    • 綁定渲染目標為 shadowMap(深度緩沖區)。
    • 設置陰影繪制管線(shadowPipeline)。
    • 遍歷場景中的所有物體,設置幾何體并繪制。
    • 該Pass輸出深度信息,寫入 shadowMap(可讀寫)。
  • 第二個Pass:
    • 綁定回屏緩沖和深度緩沖為渲染目標。
    • 設置場景繪制管線(scenePipeline)。
    • 設置陰影貼圖(shadowMap)作為輸入紋理參數(只讀)。
    • 遍歷場景中的物體,設置幾何體、材質,繪制。
    • 該Pass使用第一個Pass產生的陰影深度貼圖,進行光照計算等。
2. 狀態轉換:
  • shadowMap 從第一Pass的讀寫深度緩沖變成第二Pass的只讀紋理
  • 這種狀態切換需要顯式的資源屏障(barriers),保證GPU正確處理同步和內存一致性。
3. API差異:
  • 傳統的OpenGL和Direct3D 11驅動通常會自動幫你處理資源狀態切換和同步。
  • 低級別API(如Vulkan和DirectX 12)要求程序員顯式管理這些狀態轉換和資源屏障。
4. 總結:
  • 經典多通道渲染是一種典型的圖形編程模式,先寫入一個渲染目標,然后在后續Pass讀取它。
  • 在現代低級圖形API中,正確管理資源狀態和同步是顯式且必要的,這增加了代碼的復雜度但提高了性能可控性。

Frame Graph 和作業提交(Work Submission)概述

1. Frame Graph是什么?
  • Frame Graph(幀圖)是現代渲染管線中用來組織和調度渲染任務的一種數據結構。
  • 它將渲染過程拆分為多個“Pass”(渲染通道),每個Pass讀寫不同的資源(如紋理、緩沖區)。
  • 通過分析Pass之間的依賴關系,Frame Graph能幫助優化渲染工作流,比如去除不必要的渲染操作,合理安排Pass執行順序,以及有效管理內存資源。
2. 關鍵優化點:
  • 死代碼消除(Dead Code Elimination):
    如果某個Pass產生的資源沒有被后續Pass使用(比如某個紋理沒被采樣),整個Pass可以跳過,避免浪費計算資源。
  • 數據訪問沖突和屏障插入(Data Hazard & Barriers):
    如果Pass1寫入了某個資源,而Pass2需要讀取這個資源,必須在兩者之間插入同步屏障(memory barrier),保證數據正確傳遞,避免訪問沖突。
  • Pass間的延遲管理(Latency Management):
    有些Pass緊密連在一起執行,緩存利用率高但可能造成CPU或GPU等待;而把它們錯開執行可以減少阻塞但會增加內存開銷。如何平衡這兩者是一個復雜的調度問題。
  • 命令列表提交(Command List Recording & Submission):
    調度器根據依賴關系和優先級,依次執行各Pass對應的命令錄制和提交操作。
3. 調度算法:List Scheduling
  • 給每個任務(Pass)分配一個優先級(優先級是通過啟發式算法決定的)。
  • 選擇當前所有滿足資源和依賴要求的最高優先級任務執行。
  • 依次重復直到所有任務完成。
  • 例如任務A優先級最高,先執行;如果任務B資源不足,先執行C。
4. 內存和資源生命周期管理
  • 資源(紋理、緩沖等)只在其被使用的Pass生命周期內存在。
  • 用完后可以釋放資源,或者在條件允許時復用內存(比如兩個資源大小和格式相同且生命周期不重疊)。
  • 這種復用降低了顯存占用,提高效率。
  • 類似于D3D12中的CreatePlacedResource,需要顯式管理內存和資源綁定。

總結

  • Frame Graph是一種把渲染流程視為任務圖,進行全局優化的技術。
  • 通過死代碼剔除、同步屏障插入、合理調度和內存復用,實現高性能且內存友好的渲染流程。
  • 新的底層圖形API(Vulkan、D3D12)需要程序員手動管理資源狀態和屏障,復雜但控制更細。
  • 需要根據具體應用和硬件做出權衡和優化。

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

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

相關文章

【單調隊列】-----【原理+模版】

單調隊列 一、什么是單調隊列&#xff1f; 單調隊列是一種在滑動窗口或區間查詢中維護候選元素單調性的數據結構&#xff0c;通常用于解決“滑動窗口最大值/最小值”等問題。 核心思想是&#xff1a;利用雙端隊列&#xff08;deque&#xff09;維護當前窗口內或候選范圍內元素…

CSS語法中的選擇器與屬性詳解

CSS:層疊樣式表&#xff0c;Cascading Style Sheets 層疊樣式表 內容和樣式分離解耦&#xff0c;便于修改樣式。 特殊說明&#xff1a; 最后一條聲明可以沒有分號&#xff0c;但是為了以后修改方便&#xff0c;一般也加上分號為了使用樣式更加容易閱讀&#xff0c;可以將每條代…

模擬設計的軟件工程項目

考核題目 論文論述題&#xff1a;結合你 參與開發、調研或模擬設計的軟件工程項目 &#xff0c;撰寫一篇論文 完成以下任務&#xff0c;論文題目為《面向微服務架構的軟件系統設計與建模分析》&#xff0c;總分&#xff1a; 100 分。 1. 考核內容&#xff1a; 一、系統論述…

個人理解redis中IO多路復用整個網絡處理流

文章目錄 1.redis網絡處理流2.理解通知機制 1.redis網絡處理流 10個客戶端通過TCP與Redis建立socket連接&#xff0c;發送GET name指令到服務器端。服務器端的網卡接收數據&#xff0c;數據進入內核態的網絡協議棧。Redis通過IO多路復用機制中的epoll向內核注冊監聽這些socket的…

【鄭州輕工業大學|數據庫】數據庫課設-酒店管理系統

該數據課設是一個基于酒店管理系統的數據庫設計 建庫語句 create database hotel_room default charset utf8 collate utf8_general_ci;建表語句 use hotel_room;-- 房型表 create table room_type( id bigint primary key auto_increment comment 房型id, name varchar(50)…

TCP 三次握手與四次揮手詳解

前言 在當今互聯網時代&#xff0c;前端開發的工作范疇早已超越了簡單的頁面布局和交互設計。隨著前端應用復雜度的不斷提高&#xff0c;對網絡性能的優化已成為前端工程師不可忽視的重要職責。而要真正理解并優化網絡性能&#xff0c;就需要探究支撐整個互聯網的基礎協議——…

RTD2735TD/RTD2738 (HDMI,DP轉EDP 高分辨率高刷新率顯示器驅動芯片)

一、芯片概述 RTD2738是瑞昱半導體&#xff08;Realtek&#xff09;推出的一款高性能顯示驅動芯片&#xff0c;專為高端顯示器、便攜屏、專業顯示設備及多屏拼接系統設計。其核心優勢在于支持4K分辨率下240Hz高刷新率及8K30Hz顯示&#xff0c;通過集成DisplayPort 1.4a與HDMI …

C++實現手寫strlen函數

要實現求字符串長度的函數&#xff0c;核心思路是通過指針或索引遍歷字符串&#xff0c;直到遇到字符串結束標志 \0 。以下是兩種常見的實現方式&#xff1a; 指針遍歷版本 #include <iostream> using namespace std; // 指針方式實現strlen size_t myStrlen(const cha…

NVPL 函數庫介紹和使用

文章目錄 NVPL 函數庫介紹和使用什么是 NVPLNVPL 的主要組件NVPL 的優勢安裝 NVPL基本使用示例示例1&#xff1a;使用 NVPL RAND 生成隨機數示例2&#xff1a;使用 NVPL FFT 進行快速傅里葉變換 編譯 NVPL 程序性能優化建議總結 NVPL 函數庫介紹和使用 什么是 NVPL NVPL (NVI…

HTTP相關內容補充

目錄 一、URI 和 URL 二、使用 Cookie 的狀態管理 三、返回結果的 HTTP狀態碼 一、URI 和 URL URI &#xff1a;統一資源標識符 URL&#xff1a;統一資源定位符 URI 格式 登錄信息&#xff08;認證&#xff09;指定用戶名和密碼作為從服務器端獲取資源時必要的登錄信息&a…

MySQL: Invalid use of group function

https://stackoverflow.com/questions/2330840/mysql-invalid-use-of-group-function 出錯SQL: 錯誤原因&#xff1a; 1. 不能在 WHERE 子句中使用聚合&#xff08;或分組&#xff09;函數 2. HAVING 只能篩選分組后的聚合結果或分組字段 # Write your MySQL query statem…

C#財政票查驗接口集成-醫療發票查驗-非稅收入票據查驗接口

財政票據是企事業單位、醫療機構、金融機構等組織的重要報銷憑證&#xff0c;其真實性、完整性和合規性日益受到重視。現如今&#xff0c;為有效防范虛假票據報銷、入賬、資金流失等問題的發生&#xff0c;財政票據查驗接口&#xff0c;結合財政票據識別接口&#xff0c;旨在為…

瀏覽器基礎及緩存

目錄 瀏覽器概述 主流瀏覽器&#xff1a;IE、Chrome、Firefox、Safari Chrome Firefox IE Safari 瀏覽器內核 核心職責 主流瀏覽器內核 JavaScript引擎 主流的JavaScript引擎 瀏覽器兼容性 瀏覽器渲染 渲染引擎的基本流程 DOM和render樹構建 html解析 DOM 渲染…

Ubuntu 安裝Telnet服務

1. 安裝Telnet 客戶端 sudo apt-get install telnet 2. 安裝Telnet 服務器 &#xff08;這樣才能用A電腦的客戶端連接B電腦的Telnet服務&#xff09; sudo apt-get install telnetd 3. 這時候Telnet服務器是無法自我啟動的&#xff0c;需要網絡守護進程服務程序來管理…

AI+預測3D新模型百十個定位預測+膽碼預測+去和尾2025年6月19日第113彈

從今天開始&#xff0c;咱們還是暫時基于舊的模型進行預測&#xff0c;好了&#xff0c;廢話不多說&#xff0c;按照老辦法&#xff0c;重點8-9碼定位&#xff0c;配合三膽下1或下2&#xff0c;殺1-2個和尾&#xff0c;再殺4-5個和值&#xff0c;可以做到100-300注左右。 (1)定…

觀察者模式 vs 發布訂閱模式詳解教程

&#x1f31f;觀察者模式 vs 發布訂閱模式詳解教程 收藏 點贊 關注&#xff0c;持續更新高頻面試知識庫&#xff01;&#x1f680; 一、核心概念&#xff08;總&#xff09; 在軟件開發中&#xff0c;觀察者模式&#xff08;Observer&#xff09; 和 發布訂閱模式&#xff0…

【云馨AI-大模型】MD2Card:從Markdown到知識卡片的完美轉變

Markdown的魅力與挑戰MD2Card的核心功能使用體驗與案例分析總結 在當今這個信息快速傳播的時代&#xff0c;內容創作者們一直在尋找更有效的方式來呈現他們的想法和知識。無論是為了個人學習筆記、團隊內部的知識分享還是對外的內容發布&#xff0c;一個清晰、美觀的展示方式顯…

【實戰教程】OPEN API 雷池社區版自動拉黑IP

老版本使用雷池社區版的時候都需要在界面操作&#xff0c;但是網絡攻擊往往都是無規律的&#xff0c;每次都手動操作非常累 前一段時間雷池社區版剛好開放了OPEN API 功能&#xff0c;可以支持大家使用API的方式進行管理了 但是沒有相關文檔非常難受&#xff0c;一直沒有使用…

Hot100——鏈表專項

目錄 相交鏈表 反轉鏈表 回文鏈表 環形鏈表 合并兩個有序鏈表 相交鏈表 ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {if (headA nullptr || headB nullptr) {return nullptr;}ListNode *pA headA;ListNode *pB headB;while (pA ! pB) {pA (pA…

Java + Spring Boot 后端防抖切面類AOP代碼問題排查分析

需排查分析的防抖切面類 AOP代碼&#xff1a; package com.weiyu.aop;import com.weiyu.anno.Debounce; import com.weiyu.utils.DebounceUtil; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotatio…