这段内容讲的是离散显卡(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…