精华内容
下载资源
问答
  • 2010-02-21 15:25:00

    *虚拟内存在"我的电脑--属性--高级--性能---虚拟内存"中可以设置

    *电脑上面的虚拟内存实际上是用硬盘来充当的,把内存里面不常用的东西放硬盘里面来节约内存,但是硬盘速度太慢了,根本没办法满足GPU的运算能力!而且按照GPU的计算原理这个东西也根本不现实。

    集成显卡有共享显存技术,可以动态调整系统内存当做现存使用。而且很多BIOS种没有类似的设置默认是自动开启的

    一般情况下进入BIOS后“Advanced chipset setup”-“ONBOARD VGA SHARE MERNORY”应该就是调整显存
    但是各个主板情况是不同的
    显示性能是集成主板发挥性能最主要的瓶径,尤其是在运行3D游戏等考验显卡性能的程序时,集成显卡就会暴露出自己的缺点。而BIOS的设置与集成显卡的性能关系密切。

    更多相关内容
  • 3、多显卡渲染核心原理 3.1、多GPU拓扑模型及工作方式 3.1.1、隐式多显卡系统 3.1.2、显式多显卡系统 3.1.3、链接的多显卡系统 3.1.4、无链接的多显卡系统 3.2、无链接显式多显卡系统的核心关键点——共享资源...

    目录

    1、前言

    2、为什么要多显卡渲染

    3、多显卡渲染核心原理

    3.1、多GPU拓扑模型及工作方式

    3.1.1、隐式多显卡系统

    3.1.2、显式多显卡系统

    3.1.3、链接的多显卡系统

    3.1.4、无链接的多显卡系统

    3.2、无链接显式多显卡系统的核心关键点——共享资源

    4、异构多显卡渲染框架示例——核显+独显方式

    4.1、创建多个设备对象

    4.2、创建交换链及渲染目标

    4.3、创建跨GPU共享资源堆和共享资源

    4.4、创建单位矩形和模拟后处理渲染

    4.5、创建跨GPU的围栏同步对象

    4.6、渲染过程

    4.7、多GPU间同步

    5、性能评估和分析

    6、全部代码和下载链接

    7、后记


    1、前言

    再过几天就是2019年的春节了,在这里先给大家拜个早年,提前祝大家新春吉祥!阖家欢乐!

    按照计划在春节前我终于把D3D12中最重要的基础性话题之一——多显卡渲染搞定了。现在分享出来,算是给大家的一个新年礼物吧。

    按照常理,春节前一般都该是做总结的时候了,可是我觉得至少我永远是在追逐梦想的路上,总结什么呢?不停的做就是了,总结留给生命结束的时候去吧!或者几千年来,其实古圣先贤已经都替我们总结差不多了,最简单的像孔子说的:不贰过,不迁怒!其实人生能做到这简单的两点已经算是很圆满了,而且我一直以:志于道、拘于德、依于仁、游于艺,作为人生的全部法则,或者说座右铭吧。因此我也很少再去过多的总结,只是每日三省吾身尔,当然离古圣先贤的要求还相去甚远,目前也只能勉强算是游于艺吧!

    言归正传,前段时间,我跟几个专业搞游戏开发的朋友聊天,谈到我后面打算写多显卡渲染,尤其是异构多显卡渲染的时候,我发现这个话题居然备受争议,甚至他们认为多显卡渲染其实是个降低性能的骗局。怎么说呢?至少我部分的赞同他们的观点,当然前提就是那是在有D3D12以前的情况了。

    而现在,有了D3D12接口,才能使异构多显卡渲染成为可能,也才能真正使多显卡渲染性能爆棚!当然为什么要追求极致的性能,我想在多线程渲染那一章中我已经说的很明白了,请大家去垂阅。

    本篇教程中,我将先把多显卡渲染,尤其是异构多显卡渲染的基本框架原理讲清楚,至于最终怎么去提高性能,我想留到后面的教程再去啰嗦。这一章我们将实现的效果依然如下:

    OK,不要纠结怎么又是这堆东西,毕竟是研究代码怎么写的,不是研究美工的,请恕我实在是找不到其它的场景物体了,当然也懒得去找了。同时我也认为,过于复杂的场景其实于我们的基础性原理和基本代码编写来说也毫无益处,咱们还是专心的搞明白D3D12接口的方方面面先!

    2、为什么要多显卡渲染

    在我之前的系列文章“D3D11和D3D12多线程渲染框架的比较(1-5)”中我已经提过,在满街智能手机的今天,其实CPU常常和GPU是做在一个芯片上的。而在我们的笔记本、PC,甚至服务器上到处都可见核显,同时很多笔记本、PC、甚至服务器上都还配有独显。有些机器上甚至不止有一块独显,这也很常见。

    但是在有D3D12接口以前,核显和独显的工作方式还是一个干活一个休闲的“倒班制”。而这种方式最常见就在笔记本上。比如我现在用来工作、写代码、写博客文章的这台笔记本,在没有升级Win10升级DirectX12以前,核显G530和独显GTX965m基本就是这样的工作方式。而升级之后这俩货终于有机会可以一起工作了,当然现在仍然需要我自个写程序让它俩一起工作。

    总之这里要说的第一点就是现在多显卡的系统基本已经随处可见了,而这其中异构多显卡是比较常见的情况。同时我估计在不久的将来手机上也可能会出现独显+核显的情况(这个别跟我争,记得当年我说5年内手机上马上会用上多核CPU,结果一哥们不信跟我来抬杠,后来2年时间手机就用上多核CPU了,拭目以待吧)。

    第二点就是现在有了D3D12接口,同时也有了完全支持异步执行渲染命令的显卡驱动支持,那么多显卡同时工作,在软件层面上也就没有什么大的障碍了。而过去只能是硬件层面使用STL或CrossFire,让两个近乎相同的GPU实现协同工作,如果是不同的显卡,哪怕是同一个厂商的不同型号GPU都是没法简单的协同工作的。

    第三点,现在的光栅化渲染中,渲染技术也越来越复杂化,大多数主流的渲染技术已经不是简单的在一遍(Pass)渲染中就能完成的,同时很多渲染技术还有很多后期画面处理运算,这些简称为后处理。后处理其实主要针对一帧固定大小的画面进行,相较于它之前的那些渲染来说,其性能开销是可以预期的,实质往往也就是处理一张固定大小纹理而已,因此使用比如核显来做这些处理也是比较合适的,这样就可以让独显空出后处理的时间去渲染下一帧,而核显就可以完成后处理并且负责最终画面的呈现。甚至对于一些性能较强悍的核显比如AMD的核显,还可以将一些多Pass任务中的若干Pass交给它来完成。总之,也就是说将大的整体串行的渲染过程,拆分在不同的显卡上来完成,形成接力渲染并行执行的效果,从而达到性能的优化效果。

    第四点,现在已经出现的实时光线追踪渲染,可以更简单的利用多显卡渲染来提升性能,甚至是用异构多显卡。这是因为如果你看过我的文章“光线追踪渲染(RayTracing Render)核心原理详解”,你就会明白,发射光线其实可以根据每个不同显卡能力的大小来分配不同数量的光线来完成同一帧画面的渲染的。而且在目前发布的DXR中也是支持这种异构多显卡的实时光追渲染的。因此使用多显卡渲染对实时光追渲染性能的提升也是有益处的。

    总之,也就是说在现代,多核系统随处可见、软硬件也已经做好了充分准备、而渲染过程本身,无论是传统的光栅化渲染还是实时光追渲染理论上都可以在利用多显卡渲染来提升整体性能。

    3、多显卡渲染核心原理

    要能够利用多显卡渲染带来的好处,首先我们就需要深刻的掌握目前GPU的一些拓扑链接方式。正如我在本系列教程的第三篇中所讲的一样,要理解显存管理,就需要我们了解现在GPU存储模型。在这里我们需要更进一步,在当时讲的模型基础上,深刻理解GPU在系统中的拓扑模型。

    3.1、多GPU拓扑模型及工作方式

    在D3D12多显卡渲染的官宣PPT中,对目前多显卡的系统做了一些拓扑上的划分,从大的层面上来讲,就分为隐式和显式多显卡两种模型。下面我就详细的介绍一下。

    3.1.1、隐式多显卡系统

    所谓的隐式多显卡其实就是多GPU显卡,即在一个显卡线路板上集成至少两个以上的GPU,组成一个多GPU显卡。在驱动和D3D12中看到的将是完整的一个适配器。核心的它们基本都是使用AFR(交替渲染并行)工作方式,即一个GPU负责渲染一帧,交替执行。而从其工作方式来看,这种方式并没有实现真正的多个GPU同时渲染一帧的情况,并不是真正意义上的渲染同一帧。因此其性能提升方式比较有限,并且极度依赖于单个GPU的性能。而且这种显卡因其高昂的价格,所以也不是很多见。

    因为在编程上这样的系统与使用单个适配器的情况几乎没什么差别,我们就不多着重介绍了。

    3.1.2、显式多显卡系统

    而与隐式多显卡相对应的就是显示多显卡系统,也就是说我们在编程的过程中可以明显“感知”到有多个GPU可供利用,因此这样的方式就被称为显示多显卡系统。

    这种方式优点比较多,它几乎可以利用所有的硬件资源,也就是说包括完全异构的来自不同厂商的GPU都可以自由的同时工作。它也给我们程序以充分的控制权,可以实现渲染命令级的真正的并行命令执行,即每个GPU都对应一个命令队列。我们可以自行调配每个GPU上的任务负载,同时我们还可以相对独立并主动的控制每个GPU的显存。

    总之显示多显卡系统给我们在编程实现及使用上带来了极大的灵活性。

    具体的显示多显卡系统主要有链接的多显卡系统和离散的多显卡系统。

    3.1.3、链接的多显卡系统

    主要指的就是Nvidia的SLI链接的多个显卡,或用AMD的CrossFire链接的多个显卡。

    这种系统在编程的角度上我们看到的可能是一个适配器,而其上有多个GPU节点(Node),也就是它上面可以有多个3D引擎、复制引擎、计算引擎等。直观的理解上就像使用的是一个多核超线程CPU一样,虽然物理上是一个处理器,但是具体编程上,我们可以使用多线程来充分发挥多核CPU并行执行线程的优势。

    在具体编程上,每个链接的适配器都会被枚举为一个IDXGIAdapter3接口,我们在其上创建一个ID3D12Device接口,然后可以调用ID3D12Device::GetNodeCount方法知道具体有多少个GPU节点在这个适配器上。

    然后我们在创建的一系列围绕设备的相关对象时,在其结构体的NodeMask(GPU节点掩码)参数中指定具体是将对象创建在哪个或哪几个GPU节点上。

    NodeMask参数是一个按照从低位到高位,指定对应GPU序号二进制位为1的一个二进制值,与线程亲缘性参数的CPU Mask掩码类似。当然序号为0的GPU就默认指定第1位Bit位为1即可,即0x1,而序号为1的GPU就指定第2位的Bit位为1,即0x2,以此类推。一般可以使用C++的<<左移位运算符来快速指定NodeMask:NodeMask =1<<GPUIndex。当有多个GPU需要联合指定时,就可以使用C++的位或“|”运算符指定多个GPU节点。

    当然因为NodeMask是一个UINT的32位值,因此我们最多可以索引到第32个GPU或者同时引用32个GPU,但实际上一般也不会有这么多的GPU同时组成这样的适配器节点。目前SLI和CF都最多支持4路GPU。

    而ID3D12Device接口方法创建的各种对象,则根据能否跨GPU节点创建,则被分为单节点对象,和可以跨GPU节点引用的多节点对象。

    其中单节点对象的相关结构体和函数如下:

    类型

    名称

    说明

    结构体

    D3D12_COMMAND_QUEUE_DESC

    有NodeMask成员

    函数

    CreateCommandQueue

    根据D3D12_COMMAND_QUEUE_DESC 结构体指定的GPU节点创建对应GPU节点的命令队列。

    函数

    CreateCommandList

    有NodeMask参数,创建指定GPU节点上的命令列表.

    结构体

    D3D12_DESCRIPTOR_HEAP_DESC

    有NodeMask成员

    函数

    CreateDescriptorHeap

    根据D3D12_DESCRIPTOR_HEAP_DESC结构体指定的NodeMask创建对应GPU节点上的描述符堆

    结构体

    D3D12_QUERY_HEAP_DESC

    有NodeMask成员

    函数

    CreateQueryHeap

    根据D3D12_QUERY_HEAP_DESC结构体指定的NodeMask创建对应GPU节点上的性能参数查询堆

    多节点对象对应的结构体和函数如下:

    类型

    名称

    说明

    结构体

    D3D12_COMMAND_QUEUE_DESC

    有NodeMask成员

    枚举值

    D3D12_CROSS_NODE_SHARING_TIER

    用于确定Tier方式资源可以跨节点支持到什么程度的枚举类型,详细的内容可以看该枚举值的MSDN文档。

    结构体

    D3D12_FEATURE_DATA_D3D12_OPTIONS

     有D3D12_CROSS_NODE_SHARING_TIER枚举类型成员变量

    结构体

    D3D12_FEATURE_DATA_ARCHITECTURE

    包含一个NodeIndex参数,用于查询具体的GPU节点的架构

    结构体

    D3D12_GRAPHICS_PIPELINE_STATE_DESC

    拥有NodeMask成员

    函数

    CreateGraphicsPipelineState

    根据D3D12_GRAPHICS_PIPELINE_STATE_DESC结构体指定的NodeMask创建渲染管线状态对象

    结构体

    D3D12_COMPUTE_PIPELINE_STATE_DESC

    拥有NodeMask成员

    函数

    CreateComputePipelineState

    根据D3D12_COMPUTE_PIPELINE_STATE_DESC结构体指定的NodeMask创建计算管线状态对象

    函数

    CreateRootSignature

    拥有NodeMask参数

    结构体

    D3D12_COMMAND_SIGNATURE_DESC

    拥有NodeMask成员

    函数

    CreateCommandSignature

    根据D3D12_COMMAND_SIGNATURE_DESC结构体指定的NodeMask创建命令标记对象

    其他的一些没有特殊说明的函数和方法一般就不需要再去指定NodeMask参数了,比如各种描述符,因为它们所在的描述符堆已经被指定了GPU NodeMask。

    另外就是作为各种资源,资源堆等就不需要特别指定GPU的NodeMask了,因为作为链接的多显卡系统,这些资源可以映射到其内部的任何一个GPU节点上,或者直白的理解就是所有显存都是对这样的一个SLI或CF系统下的GPU是共享的。

    最终下图形象的展示了各种资源与GPU节点之间的关系(该图来自MSDN):

    其中橙色部分就是指可以跨GPU节点创建并可应用于多个GPU节点的对象。而其他的各种命令队列对象就只能在指定的GPU节点上被创建和使用了。

    需要注意的就是在链接的多显卡系统中,最终交换链、渲染目标甚至深度缓冲等作为可以直接被共享的资源,创建后则可以在所有GPU节点间共享,因此最终如何使用多个3D引擎等来并行执行渲染操作等,就跟多线程编程有些类似了,只是这里我们可以直接利用多个3D命令队列来并行执行不同的命令列表,从而实现真正的并行渲染而已。

    因为我手头没有带有SLI或CF的系统,因此我没法展示这种链接的多显卡系统如何具体的编程,所以只能做下原理性的描述。请各位见谅(实际是因为我太穷,搞不起这样的系统)。

    3.1.4、无链接的多显卡系统

    与使用SLI和CF链接在一起的系统不同,还有一大类系统中多个显卡是完全独立的,这样的系统被D3D12称为无链接的多显卡系统,或者称之为离散多显卡系统。

    这类系统就是比较常见的了,比如我的笔记本电脑中Intel CPU中的核显和来自Nivdia的GPU就组成了一个离散多显卡系统。具体的根据离散显卡的类型,这类系统还可以细分为:多个离散独显的系统和比较常见的核显+独显的系统。当然这类系统的最大特点就是每个显卡的GPU等都可能来自不同的厂商及不同的品牌。

    在没有D3D12之前我们基本没法让这样的不同的多个GPU同时来工作。只能在同一时间为同一任务使用某一个GPU。比如我在写这篇文章时,系统使用的就是集显在工作,而当我玩3D游戏时系统就使用GTX965m独显来工作。

    而本质上,如果你已经深度的学习和掌握了我们说的现代显卡的多引擎架构的话,也就是说现代的GPU上都会有至少一个3D引擎,计算引擎和若干个复制引擎组成,而这些引擎之间是可以并行执行的。更进一步,我们可以想到不同的GPU上的3D引擎之间也是可以并行执行的,这样我们就可以将一些因为只能使用单个3D引擎按渲染命令顺序执行的串行渲染过程,改造为可以使用多个不同的3D引擎来并行执行的过程,从而达到并行优化,并提升性能的效果。

    仔细想想的话,我们会发现其实这是完全可能的,只是说与链接的多显卡系统可以直接共享使用渲染目标、深度缓冲等资源不同,在D3D12以前,可能没有什么直接而简单的方法,在多个离散的显卡间共享资源。这样也就无法简单的去共享渲染目标、深度缓冲等,从而也就无法简单的完成并行渲染的任务。

    从编程角度来分析的话,就是说我们可以为每个离散显卡创建各自的Device对象、Queue对象、Command List对象等,然后我们可以将场景的不同部分创建在不同的Device对象所代表的不同显卡上,但是最终因为我们无法简单的共享资源,从而渲染最终的画面无法简单的统一到一起。

    3.2、无链接显式多显卡系统的核心关键点——共享资源

    那么上述关于离散显卡间共享资源的最终问题,实际在D3D12中为我们提供了一个比较好的解决方案,那就是直接创建共享资源。

    首先,我们需要检测显卡是否支持共享资源,具体方法如下:

    D3D12_FEATURE_DATA_D3D12_OPTIONS stOptions = {};
    // 通过检测带有显示输出的显卡是否支持跨显卡资源来决定跨显卡的资源如何创建
    GRS_THROW_IF_FAILED( stGPUParams[nIDGPUSecondary].m_pID3DDevice->CheckFeatureSupport(
        D3D12_FEATURE_D3D12_OPTIONS, reinterpret_cast<void*>(&stOptions), sizeof(stOptions)));
    bCrossAdapterTextureSupport = stOptions.CrossAdapterRowMajorTextureSupported;

    其次,我们可以使用下面一组函数完成GPU资源堆在多个显卡间甚至多个不同的进程间的共享:

    ID3D12Device::CreateSharedHandle
    ID3D12Device::OpenSharedHandle
    ID3D12Device::OpenSharedHandleByName

    像下面这样调用,就可以完成资源堆的跨显卡共享:

    // 创建跨显卡共享的资源堆
    CD3DX12_HEAP_DESC stCrossHeapDesc(
        n64szTexture * g_nFrameBackBufCount,
        D3D12_HEAP_TYPE_DEFAULT,
        0,
        D3D12_HEAP_FLAG_SHARED | D3D12_HEAP_FLAG_SHARED_CROSS_ADAPTER);
    
    GRS_THROW_IF_FAILED(stGPUParams[nIDGPUMain].m_pID3DDevice->CreateHeap(&stCrossHeapDesc
        , IID_PPV_ARGS(&stGPUParams[nIDGPUMain].m_pICrossAdapterHeap)));
    
    HANDLE hHeap = nullptr;
    GRS_THROW_IF_FAILED(stGPUParams[nIDGPUMain].m_pID3DDevice->CreateSharedHandle(
        stGPUParams[nIDGPUMain].m_pICrossAdapterHeap.Get(),
        nullptr,
        GENERIC_ALL,
        nullptr,
        &hHeap));
    
    HRESULT hrOpenSharedHandle = stGPUParams[nIDGPUSecondary].m_pID3DDevice->OpenSharedHandle(hHeap
        , IID_PPV_ARGS(&stGPUParams[nIDGPUSecondary].m_pICrossAdapterHeap));
    
    // 先关闭句柄,再判定是否共享成功
    ::CloseHandle(hHeap);
    GRS_THROW_IF_FAILED(hrOpenSharedHandle);

    当然如果我们在调用CreateSharedHandle方法时指定了Name名称这个参数,为共享的句柄设置了一个名称,那么在另一个不同的进程中我们就可以使用相同的名字来调用OpenSharedHandleByName从而跨进程打开并共享一个显存堆。当然这就属于更高级的话题了,已经超出了基础教程的范畴。有兴趣的朋友可以自己试一下,在进程间共享显存堆。

    当然被共享的堆是有限制的,一般的隐式堆也可以跨显卡共享,但不能跨进程共享。保留资源和堆也不能跨显卡或跨进程共享。

    有了共享资源堆,我们就可以使用“定位”方式在共享堆上创建共享资源。这时就需要我们为D3D12_RESOURCE_DESC的Flags 参数指定枚举值D3D12_RESOURCE_FLAG_ALLOW_CROSS_ADAPTER,这也是我上一讲之所以特别强调这个结构体及它的这个枚举成员的目的。具体的创建共享资源的代码如下:

    UINT64 n64szTexture = 0;
    D3D12_RESOURCE_DESC stCrossAdapterResDesc = {};
    
    if ( bCrossAdapterTextureSupport )
    {
    	// 如果支持那么直接创建跨显卡资源堆
    	stCrossAdapterResDesc = stRenderTargetDesc;
    	stCrossAdapterResDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_CROSS_ADAPTER;
    	stCrossAdapterResDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
    
    	D3D12_RESOURCE_ALLOCATION_INFO stTextureInfo = stGPUParams[nIDGPUMain].m_pID3DDevice->GetResourceAllocationInfo(0, 1, &stCrossAdapterResDesc);
    	n64szTexture = stTextureInfo.SizeInBytes;
    }
    else
    {
    	// 如果不支持,那么我们就需要先在主显卡上创建用于复制渲染结果的资源堆,然后再共享堆到辅助显卡上
    	D3D12_PLACED_SUBRESOURCE_FOOTPRINT stResLayout = {};
    	stGPUParams[nIDGPUMain].m_pID3DDevice->GetCopyableFootprints(&stRenderTargetDesc, 0, 1, 0, &stResLayout, nullptr, nullptr, nullptr);
    	n64szTexture = GRS_UPPER(stResLayout.Footprint.RowPitch * stResLayout.Footprint.Height, D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT);
    	stCrossAdapterResDesc = CD3DX12_RESOURCE_DESC::Buffer(n64szTexture, D3D12_RESOURCE_FLAG_ALLOW_CROSS_ADAPTER);
    }
    
    // 以定位方式在共享堆上创建每个显卡上的资源
    for ( UINT nFrameIndex = 0; nFrameIndex < g_nFrameBackBufCount; nFrameIndex++ )
    {
    	GRS_THROW_IF_FAILED(stGPUParams[nIDGPUMain].m_pID3DDevice->CreatePlacedResource(
    		stGPUParams[nIDGPUMain].m_pICrossAdapterHeap.Get(),
    		n64szTexture * nFrameIndex,
    		&stCrossAdapterResDesc,
    		D3D12_RESOURCE_STATE_COPY_DEST,
    		nullptr,
    		IID_PPV_ARGS(&stGPUParams[nIDGPUMain].m_pICrossAdapterResPerFrame[nFrameIndex])));
    
    	GRS_THROW_IF_FAILED(stGPUParams[nIDGPUSecondary].m_pID3DDevice->CreatePlacedResource(
    		stGPUParams[nIDGPUSecondary].m_pICrossAdapterHeap.Get(),
    		n64szTexture * nFrameIndex,
    		&stCrossAdapterResDesc,
    		bCrossAdapterTextureSupport ? D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE : D3D12_RESOURCE_STATE_COPY_SOURCE,
    		nullptr,
    		IID_PPV_ARGS(&stGPUParams[nIDGPUSecondary].m_pICrossAdapterResPerFrame[nFrameIndex])));
    
    	if ( ! bCrossAdapterTextureSupport )
    	{
    		// If the primary adapter's render target must be shared as a buffer,
    		// create a texture resource to copy it into on the secondary adapter.
    		GRS_THROW_IF_FAILED(stGPUParams[nIDGPUSecondary].m_pID3DDevice->CreateCommittedResource(
    			&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
    			D3D12_HEAP_FLAG_NONE,
    			&stRenderTargetDesc,
    			D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
    			nullptr,
    			IID_PPV_ARGS(&pISecondaryAdapterTexutrePerFrame[nFrameIndex])));
    	}
    }
    

    这里有个细节需要引起大家注意,就是当我们判断出第二个显卡支持跨显卡共享资源时,其含义并不是说我们能不能创建共享的显存堆,其实从上面代码大家应该看出来,首先无论我们的bCrossAdapterTextureSupport是否为真,显存堆是一定能够共享的,其次这标志是说第二个显卡支不支持Row Major(行主序)排列方式的2D资源,如果支持我们就直接创建成2D Texture即可,否则我们就需要创建成一个Buffer,并且将这个共享的资源作为复制的源,然后我们额外的创建一组对应的2D纹理,用于将共享过来的资源复制到这组纹理中。最终形成如下图所示的共享资源的形式:

    所以最终我们就可以理解之前那个检测显卡是否支持共享资源的判定调用其真实含义只是说能不能以行主序形式直接共享成2D纹理,而不是说不能共享资源或显存堆。这其实也是说共享堆上的资源格式被限定死为Row Major 2D Texture而已。或者用流行的话说,这个判定为我们指定了正确的打开方式!

    综上,其实截止目前,D3D12还只能支持在离散显卡间共享Row Major 形式的2D Texture,还无法直接共享渲染目标纹理和深度缓冲等。要想共享渲染目标和深度缓冲的话,就需要额外创建与Render Target和Depth Buffer格式一致的共享资源,然后当第一个显卡渲染完成后(也就是先渲染到纹理,上一讲已经讲过该技术),通过复制引擎,将渲染结果复制到共享资源中,然后第二个显卡再将共享资源形式的渲染结果Copy件,通过复制引擎复制到第二个显卡的渲染目标纹理和深度缓冲中,然后不Clear就直接接着渲染即可。或者可以使用这个渲染结果的Copy件作为纹理,或做一些其他的后处理等。下面的示意图就形象的演示了最终我们利用离散多显卡渲染形成的渲染过程:

    从上图我们就可以看出来这样至少两个显卡的3D引擎被充分利用了,然后先后接力形成并行运行的状态,这样本来串行的Post Proc(后处理)过程,就可以在第二个GPU的3D引擎上被并行的执行,从而整体上提高了性能。

    4、异构多显卡渲染框架示例——核显+独显方式

    碍于我手头硬件的限制,我没法使用两个差不多的离散显卡,比如一个Nvidia的显卡和一个AMD的差不多类似等级的显卡做接力渲染示例。

    所以我就演示下在像我这样的笔记本上,使用一个Intel的核显+一个Nvidia的独显,如何具体实现异构多显卡渲染的基本框架。

    需要注意的是为了大家能够快速的理解异构多显卡渲染的基本框架,所以我把D3D12微软官方例子中的Blur(运动模糊)特效后处理直接给省略了。只是让第二个显卡简单的使用渲染单位矩形的方式将共享过来的渲染结果最终显示出来即可。这样虽然效果看上去和之前的几章教程没有什么大的差别,但实质的代码中已经是利用了两个显卡进行了接力渲染。大家有兴趣的可以再仿照那个例子(D3D12HeterogeneousMultiadapter),将Blur后处理加回来,就当是练习,当然我想如果你弄明白了本章的内容以及上一章渲染到纹理的内容,那么这个改造其实很简单。

    4.1、创建多个设备对象

    既然要多显卡渲染,那么第一步我们首先要做的就是创建每个显卡的设备对象。在我的笔记本上,因为Intel核显能力较弱,而Nvidia的独显能力相对较强,所以我们选择使用Intel的核显作为辅助显卡,而将Nvidia的显卡作为主显卡。

    在代码中这就需要相对“智能”点的方法来判定主次。与微软官方例子不同,我是提供了另外两种方法,来做这个主次区分,第一种方法是通过看那个显卡带有显示器来判定,因为笔记本上通常显示器是直接接驳在核显上的,这与台式机不同,所以有显示输出的在我的笔记本上就是核显;第二种方法就是使用我们之前教程中讲过的看谁是UMA架构的显卡,谁就是辅助显卡。而在微软官方的例子中它就是使用序号0的显卡作为辅助显卡,因为在系统中一般核显的序号是0。

    具体代码如下:

    D3D12_FEATURE_DATA_ARCHITECTURE stArchitecture = {};
    IDXGIAdapter1*	pIAdapterTmp	= nullptr;
    ID3D12Device4*	pID3DDeviceTmp	= nullptr;
    IDXGIOutput*	pIOutput		= nullptr;
    HRESULT			hrEnumOutput	= S_OK;
    
    for ( UINT nAdapterIndex = 0; DXGI_ERROR_NOT_FOUND != pIDXGIFactory5->EnumAdapters1(nAdapterIndex, &pIAdapterTmp); ++ nAdapterIndex)
    {
    	DXGI_ADAPTER_DESC1 stAdapterDesc = {};
    	pIAdapterTmp->GetDesc1(&stAdapterDesc);
    
    	if (stAdapterDesc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
    	{//跳过软件虚拟适配器设备
    		GRS_SAFE_RELEASE(pIAdapterTmp);
    		continue;
    	}
    
    	//第一种方法,通过判定那个显卡带有输出
    	hrEnumOutput = pIAdapterTmp->EnumOutputs(0, &pIOutput);
    
    	if ( SUCCEEDED(hrEnumOutput) && nullptr != pIOutput)
    	{//该适配器带有显示输出,通常是集显(针对笔记本的情况)
    		//我们将集显称为Main Device,因为用它来后处理和最终输出
    		GRS_THROW_IF_FAILED(D3D12CreateDevice(pIAdapterTmp, D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(&stGPUParams[nIDGPUMain].m_pID3DDevice)));
    	}
    	else
    	{//不带显示输出的,通常是独显(针对笔记本的情况)
    		//我们用独显来完成主场景渲染,当然它就是渲染到纹理,后面会看到我们使用的是共享显存的纹理
    		GRS_THROW_IF_FAILED(D3D12CreateDevice(pIAdapterTmp, D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(&stGPUParams[nIDGPUSecondary].m_pID3DDevice)));
    	}
    
    	GRS_SAFE_RELEASE(pIOutput);
    
    
    	//第二种判定主次显卡的方法,就是看谁是UMA的谁不是,这个在之前的教程示例中已经详细讲解过
    
    	//GRS_THROW_IF_FAILED(D3D12CreateDevice(pIAdapterTmp, D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(&pID3DDeviceTmp)));
    	//GRS_THROW_IF_FAILED(pID3DDeviceTmp->CheckFeatureSupport(D3D12_FEATURE_ARCHITECTURE
    	//	, &stArchitecture, sizeof(D3D12_FEATURE_DATA_ARCHITECTURE)));
    	或者我们通过判定是否UMA架构来决定谁主谁辅
    	//if ( stArchitecture.UMA )
    	//{
    	//	if ( nullptr == pID3DDeviceSecondary.Get() )
    	//	{
    	//		pID3DDeviceSecondary.Attach(pID3DDeviceTmp);
    	//		pID3DDeviceTmp = nullptr;
    	//	}
    	//	else
    	//	{
    	//		//你的显卡太多了,你自己看怎么用吧,方法都告诉你了
    	//	}
    	//}
    	//else
    	//{
    	//	if ( nullptr == pID3DDeviceMain.Get() )
    	//	{
    	//		pID3DDeviceMain.Attach(pID3DDeviceTmp);
    	//		pID3DDeviceTmp = nullptr;
    	//	}
    	//	else
    	//	{
    	//		//你的显卡太多了,你自己看怎么用吧,方法都告诉你了
    	//	}
    
    	//}
    
    	//GRS_SAFE_RELEASE(pID3DDeviceTmp);
    
    	GRS_SAFE_RELEASE(pIAdapterTmp);
    }
    
    //---------------------------------------------------------------------------------------------
    if ( nullptr == stGPUParams[nIDGPUMain].m_pID3DDevice.Get() || nullptr == stGPUParams[nIDGPUSecondary].m_pID3DDevice.Get() )
    {// 可怜的机器上居然没有两个以上的显卡 还是先退出了事 当然你可以使用软适配器凑活看下例子
    	throw CGRSCOMException(E_FAIL);
    }
    
    GRS_SET_D3D12_DEBUGNAME_COMPTR(stGPUParams[nIDGPUMain].m_pID3DDevice);
    GRS_SET_D3D12_DEBUGNAME_COMPTR(stGPUParams[nIDGPUSecondary].m_pID3DDevice);
    

    需要注意的是,为了方便,我将每个GPU单独的设备对象及相关的单节点对象都放在了一个统一的结构体里,如下:

    // 显卡参数集合
    struct ST_GRS_GPU_PARAMS
    {
    	UINT								m_nIndex;
    	UINT								m_nszRTV;
    	UINT								m_nszSRVCBVUAV;
    
    	ComPtr<ID3D12Device4>				m_pID3DDevice;
    	ComPtr<ID3D12CommandQueue>			m_pICmdQueue;
    	ComPtr<ID3D12DescriptorHeap>		m_pIDHRTV;
    	ComPtr<ID3D12Resource>				m_pIRTRes[g_nFrameBackBufCount];
    	ComPtr<ID3D12CommandAllocator>		m_pICmdAllocPerFrame[g_nFrameBackBufCount];
    	ComPtr<ID3D12GraphicsCommandList>	m_pICmdList;
    	ComPtr<ID3D12Heap>					m_pICrossAdapterHeap;
    	ComPtr<ID3D12Resource>				m_pICrossAdapterResPerFrame[g_nFrameBackBufCount];
    	ComPtr<ID3D12Fence>					m_pIFence;
    	ComPtr<ID3D12Fence>					m_pISharedFence;
    	ComPtr<ID3D12Resource>				m_pIDSTex;
    	ComPtr<ID3D12DescriptorHeap>		m_pIDHDSVTex;
    };
    

    代码中的stGPUParams数组变量就是这个结构体类型。

    4.2、创建交换链及渲染目标

    因为笔记本构造的特殊性,通常其显示器都是直接接驳在核显上,所以我们就在核显Intel的G530显卡的命令队列上创建交换链。同时,我们在作为主显卡的Nvidia的独显上创建用作渲染目标的纹理。具体代码如下:

    DXGI_SWAP_CHAIN_DESC1 stSwapChainDesc = {};
    stSwapChainDesc.BufferCount = g_nFrameBackBufCount;
    stSwapChainDesc.Width = iWndWidth;
    stSwapChainDesc.Height = iWndHeight;
    stSwapChainDesc.Format = emRTFmt;
    stSwapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    stSwapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
    stSwapChainDesc.SampleDesc.Count = 1;
    
    GRS_THROW_IF_FAILED(pIDXGIFactory5->CreateSwapChainForHwnd(
    	stGPUParams[nIDGPUSecondary].m_pICmdQueue.Get(),		// 使用接驳了显示器的显卡的命令队列做为交换链的命令队列
    	hWnd,
    	&stSwapChainDesc,
    	nullptr,
    	nullptr,
    	&pISwapChain1
    ));
    GRS_SET_DXGI_DEBUGNAME_COMPTR(pISwapChain1);
    
    //注意此处使用了高版本的SwapChain接口的函数
    GRS_THROW_IF_FAILED(pISwapChain1.As(&pISwapChain3));
    GRS_SET_DXGI_DEBUGNAME_COMPTR(pISwapChain3);
    
    // 获取当前第一个供绘制的后缓冲序号
    nCurrentFrameIndex = pISwapChain3->GetCurrentBackBufferIndex();
    
    //创建RTV(渲染目标视图)描述符堆(这里堆的含义应当理解为数组或者固定大小元素的固定大小显存池)
    D3D12_DESCRIPTOR_HEAP_DESC		stRTVHeapDesc = {};
    stRTVHeapDesc.NumDescriptors	= g_nFrameBackBufCount;
    stRTVHeapDesc.Type				= D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
    stRTVHeapDesc.Flags				= D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
    
    for (int i = 0; i < nMaxGPUParams; i++)
    {
    	GRS_THROW_IF_FAILED(stGPUParams[i].m_pID3DDevice->CreateDescriptorHeap(&stRTVHeapDesc, IID_PPV_ARGS(&stGPUParams[i].m_pIDHRTV)));
    	GRS_SetD3D12DebugNameIndexed(stGPUParams[i].m_pIDHRTV.Get(), _T("m_pIDHRTV"), i);
    }
    CD3DX12_CLEAR_VALUE   stClearValue(stSwapChainDesc.Format, arf4ClearColor);
    stRenderTargetDesc = CD3DX12_RESOURCE_DESC::Tex2D(
    	stSwapChainDesc.Format,
    	stSwapChainDesc.Width,
    	stSwapChainDesc.Height,
    	1u, 1u,
    	stSwapChainDesc.SampleDesc.Count,
    	stSwapChainDesc.SampleDesc.Quality,
    	D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET,
    	D3D12_TEXTURE_LAYOUT_UNKNOWN, 0u);
    
    for ( UINT iGPUIndex = 0; iGPUIndex < nMaxGPUParams; iGPUIndex++ )
    {
    	CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(stGPUParams[iGPUIndex].m_pIDHRTV->GetCPUDescriptorHandleForHeapStart());
    
    	for (UINT j = 0; j < g_nFrameBackBufCount; j++)
    	{
    		if ( iGPUIndex == nIDGPUSecondary )
    		{
    			GRS_THROW_IF_FAILED(pISwapChain3->GetBuffer(j, IID_PPV_ARGS(&stGPUParams[iGPUIndex].m_pIRTRes[j])));
    		}
    		else
    		{
    			GRS_THROW_IF_FAILED(stGPUParams[iGPUIndex].m_pID3DDevice->CreateCommittedResource(
    				&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
    				D3D12_HEAP_FLAG_NONE,
    				&stRenderTargetDesc,
    				D3D12_RESOURCE_STATE_COMMON,
    				&stClearValue,
    				IID_PPV_ARGS(&stGPUParams[iGPUIndex].m_pIRTRes[j])));
    		}
    
    		stGPUParams[iGPUIndex].m_pID3DDevice->CreateRenderTargetView(stGPUParams[iGPUIndex].m_pIRTRes[j].Get(), nullptr, rtvHandle);
    		rtvHandle.Offset(1, stGPUParams[iGPUIndex].m_nszRTV);
    
    		GRS_THROW_IF_FAILED(stGPUParams[iGPUIndex].m_pID3DDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT
    			, IID_PPV_ARGS(&stGPUParams[iGPUIndex].m_pICmdAllocPerFrame[j])));
    
    		if ( iGPUIndex == nIDGPUMain )
    		{ //创建主显卡上的复制命令队列用的命令分配器,每帧使用一个分配器
    			GRS_THROW_IF_FAILED( stGPUParams[iGPUIndex].m_pID3DDevice->CreateCommandAllocator( D3D12_COMMAND_LIST_TYPE_COPY
    				, IID_PPV_ARGS(&pICmdAllocCopyPerFrame[j])));
    		}
    	}
    
    	// 创建每个GPU的命令列表对象
    	GRS_THROW_IF_FAILED(stGPUParams[iGPUIndex].m_pID3DDevice->CreateCommandList(0
    		, D3D12_COMMAND_LIST_TYPE_DIRECT
    		, stGPUParams[iGPUIndex].m_pICmdAllocPerFrame[nCurrentFrameIndex].Get()
    		, nullptr
    		, IID_PPV_ARGS(&stGPUParams[iGPUIndex].m_pICmdList)));
    
    	if ( iGPUIndex == nIDGPUMain )
    	{// 在主显卡上创建复制命令列表对象
    		GRS_THROW_IF_FAILED(stGPUParams[iGPUIndex].m_pID3DDevice->CreateCommandList(0
    			, D3D12_COMMAND_LIST_TYPE_COPY
    			, pICmdAllocCopyPerFrame[nCurrentFrameIndex].Get()
    			, nullptr
    			, IID_PPV_ARGS(&pICmdListCopy)));
    
    	}
    
    	// 创建每个显卡上的深度蜡板缓冲区
    	D3D12_CLEAR_VALUE stDepthOptimizedClearValue = {};
    	stDepthOptimizedClearValue.Format = emDSFmt;
    	stDepthOptimizedClearValue.DepthStencil.Depth = 1.0f;
    	stDepthOptimizedClearValue.DepthStencil.Stencil = 0;
    
    	//使用隐式默认堆创建一个深度蜡板缓冲区,
    	//因为基本上深度缓冲区会一直被使用,重用的意义不大,所以直接使用隐式堆,图方便
    	GRS_THROW_IF_FAILED(stGPUParams[iGPUIndex].m_pID3DDevice->CreateCommittedResource(
    		&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT)
    		, D3D12_HEAP_FLAG_NONE
    		, &CD3DX12_RESOURCE_DESC::Tex2D(
    			emDSFmt
    			, iWndWidth
    			, iWndHeight
    			, 1
    			, 0
    			, 1
    			, 0
    			, D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL)
    		, D3D12_RESOURCE_STATE_DEPTH_WRITE
    		, &stDepthOptimizedClearValue
    		, IID_PPV_ARGS(&stGPUParams[iGPUIndex].m_pIDSTex)
    	));
    	GRS_SetD3D12DebugNameIndexed(stGPUParams[iGPUIndex].m_pIDSTex.Get(), _T("m_pIDSTex"), iGPUIndex);
    
    	D3D12_DEPTH_STENCIL_VIEW_DESC stDepthStencilDesc = {};
    	stDepthStencilDesc.Format = emDSFmt;
    	stDepthStencilDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
    	stDepthStencilDesc.Flags = D3D12_DSV_FLAG_NONE;
    
    	D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
    	dsvHeapDesc.NumDescriptors = 1;
    	dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
    	dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
    	GRS_THROW_IF_FAILED(stGPUParams[iGPUIndex].m_pID3DDevice->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&stGPUParams[iGPUIndex].m_pIDHDSVTex)));
    	GRS_SetD3D12DebugNameIndexed(stGPUParams[iGPUIndex].m_pIDHDSVTex.Get(), _T("m_pIDHDSVTex"), iGPUIndex);
    
    	stGPUParams[iGPUIndex].m_pID3DDevice->CreateDepthStencilView(stGPUParams[iGPUIndex].m_pIDSTex.Get()
    		, &stDepthStencilDesc
    		, stGPUParams[iGPUIndex].m_pIDHDSVTex->GetCPUDescriptorHandleForHeapStart());
    }
    

    这里要注意的就是,在这个例子中我们保留了与微软官方例子一致的创建与后缓冲区数量相同的命令分配器对象,这主要是为了开拓大家的思路,不要局限于一个命令列表就只能配一个命令分配器的思路上。同时这也是说,其实最终的渲染命令是写在命令分配器中的,渲染过程中命令分配器不用动,就可以继续执行,而命令列表在ExecuteCommandList之后就可以使用下一个命令分配器去记录下一轮的命令了。

    最后要注意的是虽然我们引用了链接多显卡系统中的单节点对象的概念,但其实在离散多显卡系统中,不用去指定Node Mask。因为每个独立的显卡都是一个独立的Adapter,然后Device对象也是独立的,而这时每个Device上其实可能只有一个GPU node。当然如果你家里有矿的话,可以组一个带核显和独立SLI显卡的复杂系统出来,然后多显卡渲染走一波试试看。​​​​​​​

    4.3、创建跨GPU共享资源堆和共享资源

    共享堆和共享资源的代码前一节已经讲解并演示了,就不在重复了。

    4.4、创建单位矩形和模拟后处理渲染

    与上一章使用的正交投影渲染矩形不同,我们这一章使用的方法是利用正交标准化渲染一个大小为单位1 的全屏矩形。具体的矩形框的定义如下:

    ST_GRS_VERTEX_QUAD stVertexQuad[] =
    {   //(   x,     y,    z,    w   )  (  u,    v   )
        { { -1.0f,  1.0f, 0.0f, 1.0f }, { 0.0f, 0.0f } },   // Top left.
        { {  1.0f,  1.0f, 0.0f, 1.0f }, { 1.0f, 0.0f } },   // Top right.
        { { -1.0f, -1.0f, 0.0f, 1.0f }, { 0.0f, 1.0f } },   // Bottom left.
        { {  1.0f, -1.0f, 0.0f, 1.0f }, { 1.0f, 1.0f } },   // Bottom right.
    };

    尤其请注意其中的纹理坐标顺序,因为纹理坐标的V轴实质也是竖直向下为正方向,所以如果你使用了微软官方例子中那个矩形的顶点定义的话,你会发现你的画面会上下颠倒。

    对应的shader中也就没有透视变换等过程,shader代码如下:

    struct PSInput
    {
        float4 position : SV_POSITION;
        float2 uv : TEXCOORD;
    };
    
    Texture2D g_texture : register(t0);
    SamplerState g_sampler : register(s0);
    
    PSInput VSMain(float4 position : POSITION, float2 uv : TEXCOORD)
    {
        PSInput result;
        result.position = position;
        result.uv = uv;
        return result;
    }
    
    float4 PSMain(PSInput input) : SV_TARGET
    {
        return g_texture.Sample(g_sampler, input.uv);
    }

    因为在光栅化之后所有的坐标都会标准化到-1至1之间,所以不用额外的变换,我们的矩形就正好贴满了整个窗口。因此这里的渲染全屏矩形的技术与我们在前一章中使用的方法是完全不同的。这里的方法仅适用于类似渲染后处理的全屏纹理方法中,因为它的坐标,大小、位置都是不好控制的。

    在这章的教程中,我特意还保留了将顶点坐标上传到默认堆中的过程,不再使用类似之前教程中的直接使用上传堆的偷懒方法了,算是作为教程完整性的一个补充吧。具体代码如下:

    const UINT nszVBQuad = sizeof(stVertexQuad);
    
    GRS_THROW_IF_FAILED(stGPUParams[nIDGPUSecondary].m_pID3DDevice->CreateCommittedResource(
    	&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
    	D3D12_HEAP_FLAG_NONE,
    	&CD3DX12_RESOURCE_DESC::Buffer(nszVBQuad),
    	D3D12_RESOURCE_STATE_COPY_DEST,
    	nullptr,
    	IID_PPV_ARGS(&pIVBQuad)));
    
    // 这次我们特意演示了如何将顶点缓冲上传到默认堆上的方式,与纹理上传默认堆实际是一样的
    GRS_THROW_IF_FAILED(stGPUParams[nIDGPUSecondary].m_pID3DDevice->CreateCommittedResource(
    	&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
    	D3D12_HEAP_FLAG_NONE,
    	&CD3DX12_RESOURCE_DESC::Buffer(nszVBQuad),
    	D3D12_RESOURCE_STATE_GENERIC_READ,
    	nullptr,
    	IID_PPV_ARGS(&pIVBQuadUpload)));
    
    D3D12_SUBRESOURCE_DATA stVBDataQuad = {};
    stVBDataQuad.pData = reinterpret_cast<UINT8*>(stVertexQuad);
    stVBDataQuad.RowPitch = nszVBQuad;
    stVBDataQuad.SlicePitch = stVBDataQuad.RowPitch;
    
    UpdateSubresources<1>(stGPUParams[nIDGPUSecondary].m_pICmdList.Get(), pIVBQuad.Get(), pIVBQuadUpload.Get(), 0, 0, 1, &stVBDataQuad);
    stGPUParams[nIDGPUSecondary].m_pICmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(pIVBQuad.Get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER));
    
    pstVBVQuad.BufferLocation = pIVBQuad->GetGPUVirtualAddress();
    pstVBVQuad.StrideInBytes = sizeof(ST_GRS_VERTEX_QUAD);
    pstVBVQuad.SizeInBytes = sizeof(stVertexQuad);

    4.5、创建跨GPU的围栏同步对象

    正如前面所提到的,其实多显卡渲染之后,我们本质上利用的是多个显卡上的3D引擎来并行的执行渲染命令的方式来获取性能上的提升。在理解上,其实这时我们可以将3D命令队列,甚至复制命令队列及计算命令队列等想象为与CPU线程类似的等价物,在有些资料上甚至直接将这些队列称之为GPU线程。那么最终的问题其实就剩下我们如何控制这些队列间的同步了,尤其是如何控制多个GPU上的多个GPU线程队列间的同步了。

    这时我们任然需要Fence围栏对象来帮忙。其实在之前我们已经讲过如果使用Fence对象来完成CPU与GPU之间的同步等待的方法。

    那么要使用Fence对象来完成多个GPU线程(命令队列)之间的同步的话,首要的问题就是一个GPU怎么直接知道和理解另一个GPU创建的Fence对象呢?其实这时仍然需要使用CreateSharedHandle- OpenSharedHandle大法,直接在多个GPU之间创建共享围栏对象,具体的代码如下:

    // 在主显卡上创建一个可共享的围栏对象
    GRS_THROW_IF_FAILED(stGPUParams[nIDGPUMain].m_pID3DDevice->CreateFence(0
        , D3D12_FENCE_FLAG_SHARED | D3D12_FENCE_FLAG_SHARED_CROSS_ADAPTER
        , IID_PPV_ARGS(&stGPUParams[nIDGPUMain].m_pISharedFence)));
    
    // 共享这个围栏,通过句柄方式
    HANDLE hFenceShared = nullptr;
    GRS_THROW_IF_FAILED(stGPUParams[nIDGPUMain].m_pID3DDevice->CreateSharedHandle(
        stGPUParams[nIDGPUMain].m_pISharedFence.Get(),
        nullptr,
        GENERIC_ALL,
        nullptr,
        &hFenceShared));
    
    // 在辅助显卡上打开这个围栏对象完成共享
    HRESULT hrOpenSharedHandleResult = stGPUParams[nIDGPUSecondary].m_pID3DDevice->OpenSharedHandle(hFenceShared
        , IID_PPV_ARGS(&stGPUParams[nIDGPUSecondary].m_pISharedFence));
    
    // 先关闭句柄,再判定是否共享成功
    ::CloseHandle(hFenceShared);
    GRS_THROW_IF_FAILED(hrOpenSharedHandleResult);

    上面的代码不难理解,既然到了这里,那么让我们稍微的开一下脑洞。回忆刚才我们已经通过这种方式使显存堆完成了跨显卡共享,那么将来有没有可能很多对象都都能使用这个方法来完成共享呢?我估计有可能,同时未来,为了更充分利用多显卡渲染的优势,尤其是离散多显卡渲染的优势,我估计微软会将共享资源时的一些限制进一步打破,很可能直接可以在多个显卡间共享渲染目标和深度缓冲了。当然也不好说会不会这样,全当瞎猜吧。但是假如真的能这样的话,我想离散多显卡渲染应该很快会流行起来。因为现在AMD的APU中的显卡已经不弱鸡了,而且根据Intel的官宣它家的核显也将大大提升性能,同时它也开始准备研发独显了,所以可以预见的未来,系统中有多个比较强悍的GPU的时代已经不远了。彼时实时光追渲染技术也应该很流行了,为了极致的画面效果,天生就可以多GPU渲染的光追技术又怎会放过系统中那么多GPU呢?​​​​​​​

    4.6、渲染过程

    接下来在渲染时,我们就使用主显卡渲染到纹理中,接着利用主显卡上的复制命令队列将渲染结果复制到共享纹理资源中,再接着辅助显卡就使用渲染单位矩形的技术将这个纹理渲染到交换链的后缓冲中,Present之后,就完成了全部的渲染。当然当之前提到的bCrossAdapterTextureSupport为FALSE时,我们就还需要辅助显卡上的复制引擎将共享的资源复制到纹理中,然后再渲染。具体代码如下(实质是记录渲染命令的过程):

    // 主显卡渲染
    {
    	stGPUParams[nIDGPUMain].m_pICmdList->SetGraphicsRootSignature(pIRSMain.Get());
    	stGPUParams[nIDGPUMain].m_pICmdList->SetPipelineState(pIPSOMain.Get());
    	stGPUParams[nIDGPUMain].m_pICmdList->RSSetViewports(1, &stViewPort);
    	stGPUParams[nIDGPUMain].m_pICmdList->RSSetScissorRects(1, &stScissorRect);
    
    	stGPUParams[nIDGPUMain].m_pICmdList->ResourceBarrier(1
    		, &CD3DX12_RESOURCE_BARRIER::Transition(
    			stGPUParams[nIDGPUMain].m_pIRTRes[nCurrentFrameIndex].Get()
    			, D3D12_RESOURCE_STATE_COMMON
    			, D3D12_RESOURCE_STATE_RENDER_TARGET));
    
    	CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
    		stGPUParams[nIDGPUMain].m_pIDHRTV->GetCPUDescriptorHandleForHeapStart()
    		, nCurrentFrameIndex
    		, stGPUParams[nIDGPUMain].m_nszRTV);
    	CD3DX12_CPU_DESCRIPTOR_HANDLE dsvHandle(stGPUParams[nIDGPUMain].m_pIDHDSVTex->GetCPUDescriptorHandleForHeapStart());
    
    	stGPUParams[nIDGPUMain].m_pICmdList->OMSetRenderTargets(1, &rtvHandle, false, &dsvHandle);
    	stGPUParams[nIDGPUMain].m_pICmdList->ClearRenderTargetView(rtvHandle, arf4ClearColor, 0, nullptr);
    	stGPUParams[nIDGPUMain].m_pICmdList->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
    	//==================================================================================================
    	//执行实际的物体绘制渲染,Draw Call!
    	for (int i = 0; i < nMaxObject; i++)
    	{
    		ID3D12DescriptorHeap* ppHeapsSkybox[] = { stModuleParams[i].pISRVCBVHp.Get(),stModuleParams[i].pISampleHp.Get() };
    		stGPUParams[nIDGPUMain].m_pICmdList->SetDescriptorHeaps(_countof(ppHeapsSkybox), ppHeapsSkybox);
    		stGPUParams[nIDGPUMain].m_pICmdList->ExecuteBundle(stModuleParams[i].pIBundle.Get());
    	}
    	//==================================================================================================
    
    	stGPUParams[nIDGPUMain].m_pICmdList->ResourceBarrier(1
    		, &CD3DX12_RESOURCE_BARRIER::Transition(
    			stGPUParams[nIDGPUMain].m_pIRTRes[nCurrentFrameIndex].Get()
    			, D3D12_RESOURCE_STATE_RENDER_TARGET
    			, D3D12_RESOURCE_STATE_COMMON));
    
    	GRS_THROW_IF_FAILED(stGPUParams[nIDGPUMain].m_pICmdList->Close());
    }
    
    // 将主显卡的渲染结果复制到共享纹理资源中
    {
    	if (bCrossAdapterTextureSupport)
    	{
    		// If cross-adapter row-major textures are supported by the adapter,
    		// simply copy the texture into the cross-adapter texture.
    		pICmdListCopy->CopyResource(stGPUParams[nIDGPUMain].m_pICrossAdapterResPerFrame[nCurrentFrameIndex].Get()
    			, stGPUParams[nIDGPUMain].m_pIRTRes[nCurrentFrameIndex].Get());
    	}
    	else
    	{
    		// If cross-adapter row-major textures are not supported by the adapter,
    		// the texture will be copied over as a buffer so that the texture row
    		// pitch can be explicitly managed.
    
    		// Copy the intermediate render target into the shared buffer using the
    		// memory layout prescribed by the render target.
    		D3D12_RESOURCE_DESC stRenderTargetDesc = stGPUParams[nIDGPUMain].m_pIRTRes[nCurrentFrameIndex]->GetDesc();
    		D3D12_PLACED_SUBRESOURCE_FOOTPRINT stRenderTargetLayout = {};
    
    		stGPUParams[nIDGPUMain].m_pID3DDevice->GetCopyableFootprints(&stRenderTargetDesc, 0, 1, 0, &stRenderTargetLayout, nullptr, nullptr, nullptr);
    
    		CD3DX12_TEXTURE_COPY_LOCATION dest(stGPUParams[nIDGPUMain].m_pICrossAdapterResPerFrame[nCurrentFrameIndex].Get(), stRenderTargetLayout);
    		CD3DX12_TEXTURE_COPY_LOCATION src(stGPUParams[nIDGPUMain].m_pIRTRes[nCurrentFrameIndex].Get(), 0);
    		CD3DX12_BOX box(0, 0, iWndWidth, iWndHeight);
    
    		pICmdListCopy->CopyTextureRegion(&dest, 0, 0, 0, &src, &box);
    	}
    
    	GRS_THROW_IF_FAILED(pICmdListCopy->Close());
    }
    
    // 开始辅助显卡的渲染,通常是后处理,比如运动模糊等,我们这里直接就是把画面绘制出来
    {
    	if (!bCrossAdapterTextureSupport)
    	{
    		// Copy the buffer in the shared heap into a texture that the secondary
    		// adapter can sample from.
    		D3D12_RESOURCE_BARRIER stResBarrier = CD3DX12_RESOURCE_BARRIER::Transition(
    			pISecondaryAdapterTexutrePerFrame[nCurrentFrameIndex].Get(),
    			D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
    			D3D12_RESOURCE_STATE_COPY_DEST);
    
    		stGPUParams[nIDGPUSecondary].m_pICmdList->ResourceBarrier(1, &stResBarrier);
    
    		// Copy the shared buffer contents into the texture using the memory
    		// layout prescribed by the texture.
    		D3D12_RESOURCE_DESC stSecondaryAdapterTexture = pISecondaryAdapterTexutrePerFrame[nCurrentFrameIndex]->GetDesc();
    		D3D12_PLACED_SUBRESOURCE_FOOTPRINT stTextureLayout = {};
    
    		stGPUParams[nIDGPUSecondary].m_pID3DDevice->GetCopyableFootprints(&stSecondaryAdapterTexture, 0, 1, 0, &stTextureLayout, nullptr, nullptr, nullptr);
    
    		CD3DX12_TEXTURE_COPY_LOCATION dest(pISecondaryAdapterTexutrePerFrame[nCurrentFrameIndex].Get(), 0);
    		CD3DX12_TEXTURE_COPY_LOCATION src(stGPUParams[nIDGPUSecondary].m_pICrossAdapterResPerFrame[nCurrentFrameIndex].Get(), stTextureLayout);
    		CD3DX12_BOX box(0, 0, iWndWidth, iWndHeight);
    
    		stGPUParams[nIDGPUSecondary].m_pICmdList->CopyTextureRegion(&dest, 0, 0, 0, &src, &box);
    
    		stResBarrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST;
    		stResBarrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
    		stGPUParams[nIDGPUSecondary].m_pICmdList->ResourceBarrier(1, &stResBarrier);
    	}
    
    	stGPUParams[nIDGPUSecondary].m_pICmdList->SetGraphicsRootSignature(pIRSQuad.Get());
    	stGPUParams[nIDGPUSecondary].m_pICmdList->SetPipelineState(pIPSOQuad.Get());
    	ID3D12DescriptorHeap* ppHeaps[] = { pIDHSRVSecondary.Get(),pIDHSampleSecondary.Get() };
    	stGPUParams[nIDGPUSecondary].m_pICmdList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
    
    	stGPUParams[nIDGPUSecondary].m_pICmdList->RSSetViewports(1, &stViewPort);
    	stGPUParams[nIDGPUSecondary].m_pICmdList->RSSetScissorRects(1, &stScissorRect);
    
    	D3D12_RESOURCE_BARRIER stRTSecondaryBarriers = CD3DX12_RESOURCE_BARRIER::Transition(
    		stGPUParams[nIDGPUSecondary].m_pIRTRes[nCurrentFrameIndex].Get()
    		, D3D12_RESOURCE_STATE_PRESENT
    		, D3D12_RESOURCE_STATE_RENDER_TARGET);
    
    	stGPUParams[nIDGPUSecondary].m_pICmdList->ResourceBarrier(1, &stRTSecondaryBarriers);
    
    	CD3DX12_CPU_DESCRIPTOR_HANDLE rtvSecondaryHandle(
    		stGPUParams[nIDGPUSecondary].m_pIDHRTV->GetCPUDescriptorHandleForHeapStart()
    		, nCurrentFrameIndex
    		, stGPUParams[nIDGPUSecondary].m_nszRTV);
    	stGPUParams[nIDGPUSecondary].m_pICmdList->OMSetRenderTargets(1, &rtvSecondaryHandle, false, nullptr);
    	float f4ClearColor[] = {1.0f,0.0f,0.0f,1.0f}; //故意使用与主显卡渲染目标不同的清除色,查看是否有“露底”的问题
    	stGPUParams[nIDGPUSecondary].m_pICmdList->ClearRenderTargetView(rtvSecondaryHandle, f4ClearColor, 0, nullptr);
    
    	// 开始绘制矩形
    	CD3DX12_GPU_DESCRIPTOR_HANDLE srvHandle(
    		pIDHSRVSecondary->GetGPUDescriptorHandleForHeapStart()
    		, nCurrentFrameIndex
    		, stGPUParams[nIDGPUSecondary].m_nszSRVCBVUAV);
    	stGPUParams[nIDGPUSecondary].m_pICmdList->SetGraphicsRootDescriptorTable(0, srvHandle);
    	stGPUParams[nIDGPUSecondary].m_pICmdList->SetGraphicsRootDescriptorTable(1, pIDHSampleSecondary->GetGPUDescriptorHandleForHeapStart());
    
    	stGPUParams[nIDGPUSecondary].m_pICmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
    	stGPUParams[nIDGPUSecondary].m_pICmdList->IASetVertexBuffers(0, 1, &pstVBVQuad);
    	// Draw Call!
    	stGPUParams[nIDGPUSecondary].m_pICmdList->DrawInstanced(4, 1, 0, 0);
    
    	// 设置好同步围栏
    	stGPUParams[nIDGPUSecondary].m_pICmdList->ResourceBarrier(1
    		, &CD3DX12_RESOURCE_BARRIER::Transition(
    			stGPUParams[nIDGPUSecondary].m_pIRTRes[nCurrentFrameIndex].Get()
    			, D3D12_RESOURCE_STATE_RENDER_TARGET
    			, D3D12_RESOURCE_STATE_PRESENT));
    	GRS_THROW_IF_FAILED(stGPUParams[nIDGPUSecondary].m_pICmdList->Close());
    }
    

     

    4.7、多GPU间同步

    最后所有的命令都记录完成后,我们就使用各自显卡上的命令队列去执行这些命令列表,代码如下:

    {// 第一步:在主显卡的主命令队列上执行主命令列表
    	ID3D12CommandList* ppRenderCommandLists[] = { stGPUParams[nIDGPUMain].m_pICmdList.Get() };
    	stGPUParams[nIDGPUMain].m_pICmdQueue->ExecuteCommandLists(_countof(ppRenderCommandLists), ppRenderCommandLists);
    
    	n64fence = n64FenceValue;
    	GRS_THROW_IF_FAILED(stGPUParams[nIDGPUMain].m_pICmdQueue->Signal(stGPUParams[nIDGPUMain].m_pIFence.Get(), n64fence));
    	n64FenceValue++;
    }
    
    {// 第二步:使用主显卡上的复制命令队列完成渲染目标资源到共享资源间的复制
    	// 通过调用命令队列的Wait命令实现同一个GPU上的各命令队列之间的等待同步 
    	GRS_THROW_IF_FAILED(pICmdQueueCopy->Wait(stGPUParams[nIDGPUMain].m_pIFence.Get(), n64fence));
    
    	ID3D12CommandList* ppCopyCommandLists[] = { pICmdListCopy.Get() };
    	pICmdQueueCopy->ExecuteCommandLists(_countof(ppCopyCommandLists), ppCopyCommandLists);
    
    	n64fence = n64FenceValue;
    	// 复制命令的信号设置在共享的围栏对象上这样使得第二个显卡的命令队列可以在这个围栏上等待
    	// 从而完成不同GPU命令队列间的同步等待
    	GRS_THROW_IF_FAILED(pICmdQueueCopy->Signal(stGPUParams[nIDGPUMain].m_pISharedFence.Get(), n64fence));
    	n64FenceValue++;
    }
    
    {// 第三步:使用辅助显卡上的主命令队列执行命令列表
    	// 辅助显卡上的主命令队列通过等待共享的围栏对象最终完成了与主显卡之间的同步
    	GRS_THROW_IF_FAILED(stGPUParams[nIDGPUSecondary].m_pICmdQueue->Wait(
    		stGPUParams[nIDGPUSecondary].m_pISharedFence.Get()
    		, n64fence));
    	
    	ID3D12CommandList* ppBlurCommandLists[] = { stGPUParams[nIDGPUSecondary].m_pICmdList.Get() };
    	stGPUParams[nIDGPUSecondary].m_pICmdQueue->ExecuteCommandLists(_countof(ppBlurCommandLists), ppBlurCommandLists);
    }
    
    // 执行Present命令最终呈现画面
    GRS_THROW_IF_FAILED(pISwapChain3->Present(1, 0));
    
    n64fence = n64FenceValue;
    GRS_THROW_IF_FAILED(stGPUParams[nIDGPUSecondary].m_pICmdQueue->Signal(stGPUParams[nIDGPUSecondary].m_pIFence.Get(), n64fence));
    n64FenceValue++;
    
    
    // 最后我们只需要在辅助显卡的围栏同步对象上等待即可完成GPU与CPU执行间的同步
    UINT64 u64CompletedFenceValue = stGPUParams[nIDGPUSecondary].m_pIFence->GetCompletedValue();
    if ( u64CompletedFenceValue < n64fence)
    {
    	GRS_THROW_IF_FAILED(stGPUParams[nIDGPUSecondary].m_pIFence->SetEventOnCompletion(
    		n64fence
    		, hFenceEvent));
    	WaitForSingleObject(hFenceEvent, INFINITE);
    }
    // 此时所有GPU上的命令都已经执行完成了,完整的一帧绘制完成了,准备开始下一帧的渲染
    

    仔细看上面的代码,会发现一个新的命令队列函数Wait,其实这个方法本身也是一个命令,是少有的几个不通过命令列表记录的命令之一,这样的命令还有我们常见的Signal和Present等。

    那么命令队列的Wait函数是什么意思呢?其实它就是告诉某个GPU引擎执行到这里时就去等待某个围栏标记值,等到了之后就表示某个围栏标记值之前的命令已经全部完成了,然后再继续执行Wait之后的命令。当然对于一个GPU上的不同引擎之间,我们往往简单实用资源屏障就可以达到这个目的,因为同一个GPU上的不同引擎之间其实往往就是交互操作一些资源而已,所以只需要控制对资源访问的同步即可,而很少有单独命令级的同步需求。

    而在本章例子中,多个GPU之间,就需要命令级的同步了。因为辅助显卡必须要在主显卡的复制命令队列执行完复制之后才能开始执行命令,也就是主显卡要把渲染纹理全部复制到共享资源中之后,辅助显卡才开始工作,此时就需要辅助显卡等待主显卡执行完成。因此我们就调用Wait命令在共享的围栏上完成等待。

    这里需要注意的就是Wait函数是GPU命令队列上的等待命令,并不会影响当前记录这个命令的CPU线程的执行,从CPU调用来看Wait命令也是立即返回的,只是写入了GPU的命令队列而已。或者更直白的理解为CPU线程只是告诉GPU线程去等待,而自己并不需要等待。

    5、性能评估和分析

    整个异构多显卡渲染的基本框架搞明白之后,其实我觉得究竟这种方式会不会提升性能,或者怎样提升性能,可能各位还是比较迷茫的。那么最终让我们来看看实际渲染执行的过程示意图,也许能明白一些:

    其实这幅图是来自微软官宣PPT中说的UE4引擎使用了本章介绍的异构多显卡渲染方式之后,实际测试的性能改进情况。图中横向就是时间轴,上半部分就是传统的只使用独立显卡渲染的时间耗费情况,而下半部分就是使用异构多显卡渲染之后的时间耗费情况。虽然整体来说如果累计每一帧的渲染时间的话,应该是变长了,因为毕竟Intel的核显实在是太弱了,即使是计算量相对固定的后处理,它也还是执行了很长时间,但最终得益于能够跟独显的主渲染过程并行执行,所以从总体上来看时间耗费还是下降了,性能得到了提升。只是目前这种提升还是比较有限的。但未来不好说随着核显性能的提升这种方式在实时光追渲染中会不会给我们带来什么惊喜呢?还是拭目以待吧!或者如果后处理更复杂,我估计性能提升就会更明显。

    6、全部代码和下载链接

    链接:GRSD3D12MultiAdapter.rar

    提取码:zyv5

    二维码:

    7、后记

    至此,本系列教程中我想讲的关于D3D12自身原理、接口、函数,以及基本使用方法等内容算是告一个小段落了。仅这些内容,我基本就耗时半年多,很感慨,一是庆幸于自己基本功还算好,没被D3D12给坑死;二是感觉自己需要学习的东西还有很多很多,照这么个进度,真不知何年何月是个头。而整体上这一系列教程其实还需要进一步加工整理,才算能让大家彻底搞得明白,而这点小事情都不知道什么时候会再有时间。

    最终其实我最感慨的就是时间真的是太快了!人的一生只有短短的三万多天,而我剩下的可能只有一万多天了,愿未来我能跑快点再跑快点,不求跑赢时间,只求在有生之年,能够体验下完成一个梦想的感觉!

    其实按原本的打算还有两个相对高级的话题,即多线程+多趟渲染和异构多显卡渲染+真正后处理的内容,现在不知道还要不要接着弄了,还是说照计划开始准备3D数学之旅了,而这些更高级的话题就放到最后的引擎封装中去呢?暂时我其实也没想明白,先过年吧,一切过完年再说!

    展开全文
  • win10 uwp 渲染原理 DirectComposition 渲染

    千次阅读 2018-05-29 08:54:25
    本文来告诉大家一个新的技术DirectComposition,在 win7 之后(实际上是 vista),微软正在考虑一个新的渲染机制

    本文来告诉大家一个新的技术DirectComposition,在 win7 之后(实际上是 vista),微软正在考虑一个新的渲染机制

    在 Windows Vista 就引入了一个服务,桌面窗口管理器Desktop Window Manager,虽然从借助 C++ 进行 Windows 开发博客可以看到 DWM 不是一个好的方法,但是比之前好。

    在 win8 的时候,微软提出了 DirectComposition ,这是一个新的方法。

    在软件的渲染一直都是两个阵营,一个是使用直接渲染模式。直接渲染的例子是使用 Direct2D 和 Direct3D ,而直接通过 Dx api 的方式当然需要使用 C++ 和底层的 API ,这开发效率比较差。

    在 1511 发布,微软告诉大家可以使用底层的 DirectComposition 接口,这样大家就可以通过 DirectComposition 做出好看的效果

    在原来的 UWP 应用,大家很容易使用 xaml 来写一个界面,但是如果没有 xaml 那么如何创建一个界面。

    我不会告诉大家去 new 一个控件,因为这样和使用之前的方法差不多。我会告诉大家如何从一个 Visual 开始画。

    在 UWP 可以通过下面几个方式显示界面

    • 通过 xaml 或者后台新建控件显示。这是最推荐的方法,本文下面的方法是不推荐的,但是可以让大家知道原理。使用 xaml 显示的元素一般都是继承 UIElement ,创建出来的元素可以带交互。

    • 如果需要高性能的画图,通过 win2d 是一个很好的方法。大家也知道创建的win2d只是显示,不会有交互,如果需要交互需要自己写。虽然写一个交互很简单,但是如果没有使用框架,重复代码很多。

    • 使用 DirectX APIs 来画 3d 的图片,但是现在需要一些 C++ 代码。

    在 UWP 的显示,推荐使用 xaml 来写界面,原因是 xaml 是一个界面无关的代码,也就是无论是 C# 和 C++ 都可以使用。如果使用 C# 来写界面,那么代码就和 C# 合在一起,不能很好在 C++ 运行。而且使用xaml 写简单比使用C#更简单,在 vs 实时编译器可以看到界面效果。

    也许大家会关系 fds 是如何做出来的,对于微软的设计,所有的 xaml 或者 win2d 的显示都是位图。这里的位图不是大家想的 bitmapImage 而是显示的一个说法,微软对所有的位图输出到 DirectComposition 。微软的 DirectComposition 在官方是这样说 “DirectComposition 组件使开发者能够进行高性能的位图合成,并附加变换、特效以及动画等各种效果,以此打造出更为复杂、生动、流畅的用户界面。DirectComposition 利用图形硬件的加速特性可以进行与 UI 线程无关的渲染处理,支持 2D 仿射变换、3D 透视变换等多种变换,以及剪切、不透明等基本特效”。

    翻译参见 Windows Composition API 指南 - 认识 Composition API 感谢大神。

    那么是不是可以通过Composition显示元素,自己来写 UWP 框架。

    在开始告诉大家写 UWP 框架之前,先给大家一个简单的例子,如何应用 DirectComposition 。

    例子

    之前写的一个简单的动画是一个好看效果,请看 win10 uwp 进度条 WaveProgressControl

    下面来通过删除所有 xaml 文件,从头自己写。

    创建工程

    首先创建一个 UWP 项目,注意选择比较高的目标。

    如何写显示

    现在创建项目,删除所有的 app 和 mainpage 类。重新创建一个类。

    只要支持显示,那么就可以完成一半了,因为 UWP 的元素显示都是通过布局找到元素显示的位置。当然这里不会提到 Translate 等。然后元素通过调用DrawContex告诉显卡需要显然的图形。然后在加上用户的输入,就构成了框架。

    虽然一个框架比上面说的复杂很多,但是在写 Avalonial 的时候,大神告诉我,实际上一个界面框架主要的就是显示和交互。本文不会告诉大家如何写交互,只是告诉大家如何显示。

    删除了所有的自动生成的代码,现在创建一个类 View ,用来显示。

    下面代码的意思请看 【Win 10 应用开发】UI Composition 札记(一):视图框架的实现 - 东邪独孤 - 博客园

    using System.Numerics;
    using Windows.ApplicationModel.Core;
    using Windows.UI;
    using Windows.UI.Composition;
    using Windows.UI.Core;
    
    namespace HmeucHsvv
    {
        internal class View : IFrameworkView, IFrameworkViewSource
        {
            public void SetWindow(CoreWindow window)
            {
                _compositor = new Compositor();
    
                _compositionTarget = _compositor.CreateTargetForCurrentView();
    
                // 创建一个容器,用来向他的 Children 添加 Visual 显示复杂的元素
                var container = _compositor.CreateContainerVisual();
                _compositionTarget.Root = container;
    
                // 创建 SpriteVisual ,这个类不仅是一个容器,同时本身也是可以画出来
                var visual = _compositor.CreateSpriteVisual();
    
                // 告诉这个元素的大小和左上角,所以这个就是一个矩形,而且设置颜色
                visual.Size = new Vector2(100, 100);
                visual.Offset = new Vector3(10, 10, 0);
    
                visual.Brush = _compositor.CreateColorBrush(Colors.Red);
    
                // 添加元素,添加进去的元素就会被显示
                container.Children.InsertAtTop(visual);
            }
    
            public void Run()
            {
                //启动窗口需要激活
                var window = CoreWindow.GetForCurrentThread();
                window.Activate();
                //调度方式使用 Dispatcher 通过这个就可以获得消息
                window.Dispatcher.ProcessEvents(CoreProcessEventsOption.ProcessUntilQuit);
            }
    
            public void Initialize(CoreApplicationView applicationView)
            {
            }
    
            public void Load(string entryPoint)
            {
            }
    
            public void Uninitialize()
            {
            }
    
            public IFrameworkView CreateView()
            {
                return this;
            }
    
            private CompositionTarget _compositionTarget;
            private Compositor _compositor;
    
            private static void Main()
            {
                CoreApplication.Run(new View());
            }
        }
    }

    上面代码有一些注释,通过这个方式就可以创建一个显示矩形

    实际上从上面代码很容易就知道,只需要一个类继承IFrameworkView, IFrameworkViewSource,然后使用CreateView返回他自己,这时这个类就可以显示。

    但是还需要使用主函数告诉软件启动的类是哪个,在运行启动窗口,如果注释掉window.Activate那么就会看到只有一个欢迎的图片不会显示矩形。

    那么是什么时候窗口支持渲染的?

    核心代码是CreateTargetForCurrentView这个函数只能调用一次,如果你尝试调用他两次,那么就会出现异常。因为调用这个函数就会告诉 DirectComposition 创建元素。

               _compositor = new Compositor();
    
                _compositionTarget = _compositor.CreateTargetForCurrentView();

    显示的矩形是通过创建 SpriteVisual 来显示。那么下面再写一个 SpriteVisual ,让两个加起来。

            public void SetWindow(CoreWindow window)
            {
                _compositor = new Compositor();
    
                _compositionTarget = _compositor.CreateTargetForCurrentView();
    
                // 创建一个容器,用来向他的 Children 添加 Visual 显示复杂的元素
                var container = _compositor.CreateContainerVisual();
                _compositionTarget.Root = container;
    
                // 创建 SpriteVisual ,这个类不仅是一个容器,同时本身也是可以画出来
                var visual = _compositor.CreateSpriteVisual();
    
                // 告诉这个元素的大小和左上角,所以这个就是一个矩形,而且设置颜色
                visual.Size = new Vector2(100, 100);
                visual.Offset = new Vector3(10, 10, 0);
    
                visual.Brush = _compositor.CreateColorBrush(Colors.Red);
    
                // 添加元素,添加进去的元素就会被显示
                container.Children.InsertAtTop(visual);
    
    
                var visual1 = _compositor.CreateSpriteVisual();
    
                // 创建一个重叠元素
                visual1.Size = new Vector2(100, 100);
                visual1.Offset = new Vector3(20, 20, 0);
    
                visual1.Brush = _compositor.CreateColorBrush(
                    Color.FromArgb(128 /*透明*/, 0, 255, 0));
                container.Children.InsertAtTop(visual1);
            }
    

    使用这个方法就可以创建多个矩形,而且通过指定位置就和大小就可以决定他在哪显示。

    上面用到了三个东西第一个是 Visual ,这是一个基础的类。有 ContainerVisual 继承 Visual ,实际上他只是可以存在子元素。最后一个是 SpriteVisual ,这个类和 ContainerVisual 一样,但是他可以使用笔刷。

    那么 SpriteVisual 设置的笔刷是什么,他可以设置三个不同的笔刷。第一个就是刚才给大家看的 CompositionColorBrush ,这是一个纯色笔刷。 第二个是比较复杂的,可以使用特效的 CompositionEffectBrush 笔刷,最后一个是 CompositionSurfaceBrush 可以和 dx 交互数据。

    从上面代码实际只是画了普通的矩形,如果要写文字,画线,那么怎么办。这时就需要使用 CompositionSurfaceBrush ,这是最复杂的。通过这个类可以使用 d2d 来画,在 UWP 简单使用的方法是 win2d 所以下面告诉大家如何使用 win2d 来画。

    但是 UWP 底层是直接使用d2d没有经过 win2d 的封装。从我的博客WPF 使用 SharpDX 在 D3DImage 显示可以知道,在 WPF 使用 d2d 是比较难的,因为很难集合两个在一个界面。但是 UWP 通过这个类就可以把底层渲染放在指定层级。这就是为什么说 UWP 可以做出比较高性能,因为 WPF 是很难修改他的渲染,即使使用D3DImage也是把渲染位图作为图片显示,需要先在显卡渲染然后把位图复制到内存,让WPF画出图片。但是 UWP 可以直接画出,不需要使用 WPF 这样的方法。我看来 UWP 在这里是很大提升,这就是我看到很多大神说不在 WPF 添加 win2d ,从底层技术实现是不相同。

    CompositionSurfaceBrush

    首先需要安装 win2d ,然后在 SetWindow 使用 CompositionSurfaceBrush 。还是和上面代码一样,但是需要先创建一个函数,用来创建 win2d ,请看下面

            private void GetCanvasAndGraphicsDevices()
            {
                var canvasDevice = CanvasDevice.GetSharedDevice();
    
                _graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(
                    _compositor, canvasDevice);
    
                _graphicsDevice.RenderingDeviceReplaced += OnRenderingDeviceReplaced;
            }

    通过这个方法就可以拿到 graphicsDevice ,这个就是用来做 CompositionSurfaceBrush 。

    如果需要创建 CompositionSurfaceBrush 那么就需要一个 CompositionDrawingSurface ,而 CompositionDrawingSurface 可以通过 graphicsDevice 创建,代码很简单

                _compositor = new Compositor();
    
                _compositionTarget = _compositor.CreateTargetForCurrentView();
    
                // 创建 win2d 用于渲染
                GetCanvasAndGraphicsDevices();
    
                _drawingSurface = _graphicsDevice.CreateDrawingSurface(
                    new Size(600, 600),
                    DirectXPixelFormat.B8G8R8A8UIntNormalized,
                    DirectXAlphaMode.Premultiplied);
    
                var brush = _compositor.CreateSurfaceBrush(
                    _drawingSurface);

    那么创建的 CompositionSurfaceBrush 如何显示?刚才讲到SpriteVisual可以显示笔刷,那么就创建这个类来显示。

    
                var drawingVisual = _compositor.CreateSpriteVisual();
                drawingVisual.Size = new Vector2(600, 600);
    
                drawingVisual.Brush = brush;

    然后把他加入视觉,和上面的代码一样,只是把 Brush 的创建写了其他的代码

                var containerVisual = _compositor.CreateContainerVisual();
                _compositionTarget.Root = containerVisual;
    
                containerVisual.Children.InsertAtTop(drawingVisual);

    下面就是让 win2d 画出矩形。

            private void Redraw()
            {
                using (var drawingSession = CanvasComposition.CreateDrawingSession(
                    _drawingSurface))
                {
                    drawingSession.FillRectangle(
                        new Rect(10, 10, 200, 200),
                        Colors.Red);
    
                    drawingSession.FillRectangle(
                        new Rect(300, 300, 200, 200),
                        Color.FromArgb(255,126,50,50));
                }
            }

    什么时候可以调用这个函数?实际上在刚才的函数最后调用就可以了。

    现在的界面就是两个矩形

    所有的代码

        internal class View : IFrameworkView, IFrameworkViewSource
        {
            public void SetWindow(CoreWindow window)
            {
                _compositor = new Compositor();
    
                _compositionTarget = _compositor.CreateTargetForCurrentView();
    
                // 创建 win2d 用于渲染
                GetCanvasAndGraphicsDevices();
    
                _drawingSurface = _graphicsDevice.CreateDrawingSurface(
                    new Size(600, 600),
                    DirectXPixelFormat.B8G8R8A8UIntNormalized,
                    DirectXAlphaMode.Premultiplied);
    
                var brush = _compositor.CreateSurfaceBrush(
                    _drawingSurface);
    
                var drawingVisual = _compositor.CreateSpriteVisual();
                drawingVisual.Size = new Vector2(600, 600);
    
                drawingVisual.Brush = brush;
    
    
                var containerVisual = _compositor.CreateContainerVisual();
                _compositionTarget.Root = containerVisual;
    
                containerVisual.Children.InsertAtTop(drawingVisual);
    
                Redraw();
            }
    
            public void Run()
            {
                //启动窗口需要激活
                var window = CoreWindow.GetForCurrentThread();
                window.Activate();
                //调度方式使用 Dispatcher 通过这个就可以获得消息
                window.Dispatcher.ProcessEvents(CoreProcessEventsOption.ProcessUntilQuit);
            }
    
            public void Initialize(CoreApplicationView applicationView)
            {
            }
    
            public void Load(string entryPoint)
            {
            }
    
            public void Uninitialize()
            {
            }
    
            public IFrameworkView CreateView()
            {
                return this;
            }
    
            private CompositionTarget _compositionTarget;
            private Compositor _compositor;
            private CompositionGraphicsDevice _graphicsDevice;
            private CompositionDrawingSurface _drawingSurface;
    
            private static void Main()
            {
                CoreApplication.Run(new View());
            }
    
            private void GetCanvasAndGraphicsDevices()
            {
                var canvasDevice = CanvasDevice.GetSharedDevice();
    
                _graphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(
                    _compositor, canvasDevice);
    
                //_graphicsDevice.RenderingDeviceReplaced += OnRenderingDeviceReplaced;
            }
    
            private void OnRenderingDeviceReplaced(
                CompositionGraphicsDevice sender, RenderingDeviceReplacedEventArgs args)
            {
                Redraw();
            }
    
            private void Redraw()
            {
                using (var drawingSession = CanvasComposition.CreateDrawingSession(
                    _drawingSurface))
                {
                    drawingSession.FillRectangle(
                        new Rect(10, 10, 200, 200),
                        Colors.Red);
    
                    drawingSession.FillRectangle(
                        new Rect(300, 300, 200, 200),
                        Color.FromArgb(255,126,50,50));
                }
            }
        }
    

    那么尝试使用 win2d 写文字就请看win10 uwp win2d

    修改函数和普通使用 win2d 没有不同

            using (var drawingSession = CanvasComposition.CreateDrawingSession(
                    _drawingSurface))
                {
                    drawingSession.Clear(Colors.White);
                    drawingSession.DrawText("lindexi", new Vector2(100, 100), Color.FromArgb(0xFF, 100, 100, 100));
                }

    还有如何使用动画和特效,我这里就不说了。

    代码参考 图形和动画 - Windows 组合支持 10 倍缩放

    参考:

    图形和动画 - Windows 组合支持 10 倍缩放

    【Win 10 应用开发】UI Composition 札记(一):视图框架的实现 - 东邪独孤 - 博客园

    借助 C++ 进行 Windows 开发 - 使用 Windows 组合引擎实现高性能窗口分层

    借助 C++ 进行 Windows 开发 - 使用 Windows 组合引擎

    Windows, UI and Composition (the Visual Layer) – Mike Taulty

    Windows with C++ - DirectComposition: A Retained-Mode API to Rule Them All

    我搭建了自己的博客 https://lindexi.gitee.io/ 欢迎大家访问,里面有很多新的博客。只有在我看到博客写成熟之后才会放在csdn或博客园,但是一旦发布了就不再更新

    如果在博客看到有任何不懂的,欢迎交流,我搭建了 dotnet 职业技术学院 欢迎大家加入

    知识共享许可协议
    本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接:http://blog.csdn.net/lindexi_gd ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系

    展开全文
  • 显卡、GPU、CPU、CUDA、显存、RTX/GTX及查看方式,相关学习记录

    学习资源: 

    CPU和GPU到底有什么区别?https://zhuanlan.zhihu.com/p/156171120

    看完就懂:GPU和显卡的关系,GPU和CUDA有什么关系?https://blog.csdn.net/weixin_42306148/article/details/117226047?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1-117226047-blog-52642366.pc_relevant_antiscanv4&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1-117226047-blog-52642366.pc_relevant_antiscanv4&utm_relevant_index=1


    目录

    1 可视化直观记忆理解-视频:CPU和GPU的区别

    2 显卡的概念

    3 GPU的概念

    4 CPU的概念

    5 CUDA的概念

    5 CPU和GPU的关系

    6 CUDA和GPU的关系

    7 显存的概念


    1 可视化直观记忆理解-视频CPU和GPU的区别

    CPU:可以理解为颜料先后发射,连接在一起,给人呈现以笑脸的图像,花费时间较长,可类比CPU的计算原理,一步步迭代计算。

     GPU:多管颜料同时发射,绘制成一幅完整的画像,花费时间是在一瞬间,可类比GPU的计算原理,大量高并行计算。

    因此,在实验中通常希望计算在GPU中进行,常见代码:

    # select device
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    print("device: %s" % device)

    可以发现代码中并未出现GPU,实际上,这暗示了GPU和CUDA之间存在一定的联系,以下进行详细学习记录。

    2 显卡的概念

    显卡(Video card,Graphics card)全称显示接口卡,又称显示适配器,是计算机最基本配置、最重要的配件之一。就像电脑联网需要网卡,主机里的数据要显示在屏幕上就需要显卡。因此,显卡是电脑进行数模信号转换的设备,承担输出显示图形的任务。具体来说, 显卡接在电脑主板上,它将电脑的数字信号转换成模拟信号让显示器显示出来。原始的显卡一般都是集成在主板上,只完成最基本的信号输出工作,并不用来处理数据。随着显卡的迅速发展,就出现了GPU的概念,显卡也分为独立显卡集成显卡。独立显卡和集成显卡两者最大区别在性能和功耗上面。前者性能更强,后者由于集成电路和成本等限制,只有顶级核显的表现能够达到入门独显的性能;而前者拥有独立显存后者采用与内存的共享显存设计,也会影响性能;前者在功耗上面更大,后者的功耗更低。

    • 独立显卡:性能强,独立显存,功耗大。
    • 集成显卡:性能弱,与内存共享显存,功耗低。

    3 GPU的概念

    GPU这个概念是由Nvidia公司于1999年提出的。GPU是显卡上的一块芯片,就像CPU是主板上的一块芯片。那么1999年之前显卡上就没有GPU吗?当然有,只不过那时候没有人给它命名,也没有引起人们足够的重视,发展比较慢。自Nvidia提出GPU这个概念后,GPU就进入了快速发展时期。简单来说,其经过了以下几个阶段的发展:
    仅用于图形渲染,此功能是GPU的初衷,Graphic Processing Unit,图形处理单元;
    ②GPU只用于图形处理太浪费了,它应该用来做更多的工作,例如浮点运算。怎么做呢?直接把浮点运算交给GPU是做不到的,因为它只能用于图形处理(那个时候)。最容易想到的,是把浮点运算做一些处理,包装成图形渲染任务,然后交给GPU来做。这就是 GPGPU(General Purpose GPU)的概念。不过这样做有一个缺点,就是你必须有一定的图形学知识,否则你不知道如何包装。为了让不懂图形学知识的人也能体验到GPU运算的强大,Nvidia公司又提出了CUDA的概念。

    4 CPU的概念

    中央处理器(central processing unit,简称CPU)作为计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单元。

    5 CUDA的概念

    CUDA(Compute Unified Device Architecture),通用并行计算架构,是一种运算平台。它包含CUDA指令集架构以及GPU内部的并行计算引擎。你只要使用一种类似于C语言的 CUDA C语言,就可以开发CUDA程序,从而可以更加方便的利用GPU强大的计算能力,而不是像以前那样先将计算任务包装成图形渲染任务,再交由GPU处理。注意,并不是所有GPU都支持CUDA。

    5 CPU和GPU的关系

    在没有GPU之前,基本上所有的任务都是交给CPU来做的。有GPU之后,二者就进行了分工, CPU负责逻辑性强的事物处理和串行计算,GPU则专注于执行高度线程化的并行处理任务(大规模计算任务)。为什么这么分工?这是由二者的硬件构成决定的。可以看出, CPU是“主(host)”而GPU是“从(device)”,GPU无论发展得多快,都只能是替CPU分担工作,而不是取代CPU

    6 CUDA和GPU的关系

    CUDA是GPU的实现工具;GPU是CUDA的硬件基础。

    7 显存的概念

    显存,也被叫做帧缓存,它的作用是用来存储显卡芯片处理过或者即将提取的渲染数据。如同计算机的内存一样,显存是用来存储要处理的图形信息的部件。也可理解为显卡的内存,电脑查看显卡方法:16G显存 

    还分不清显卡型号RTX和GTX是什么意思?

     

     

     

    展开全文
  • 在d3d11中添加有三个新的阶段,外壳着色阶段,细分阶段以及域阶段,它们是一起工作的以实现细分过程(tesselation)。细分所做的就是将一个图元对象,比如三角形或线条,快速地细分成许多更小的片段以提高模型的细节...
  • How PCI Works | PCI工作原理 Your computer's components work together through a bus. Learn about the PCI bus and PCI card, such as the one above. See more computer hardware pictures. The ....
  • 1.操作系统概述我们从功能、...操作系统把CPU、内存、硬盘抽象成进程、地址空间、文件来供应用程序使用。层次上在硬件和应用程序之间。开机启动的进程称为守护进程,开机不启动的进程需要用户跟操作系统交互,交互...
  • WPF 渲染原理

    2019-09-08 15:01:24
    想用一篇博客就能告诉大家完整的 WPF 渲染原理是不可能的。本文告诉大家 WPF 从开发者告诉如何画图像到在屏幕显示的过程。本文是从一个很高的地方来看渲染的过程,在本文之后会添加很多博客来告诉大家渲染的细节
  • GPU工作原理和应用场景介绍 GPU场景主要应用在CAD设计、大型建模、3D渲染、融媒体、仿真等等 CPU VS GPU架构对比: ALU:用于计算的晶体管,可理解为逻辑运算单元 CPU:中央处理器,巨大的缓存带来最小的指令延迟,...
  • 诺依曼计算机体系1)计算机硬件设备由存储器,运算器,控制器,输入设备和输出设备5部分组成2)采用二进制形式表示数据和指令3)将程序(数据和指令序列)预先存放在主存储器中,是计算机在工作时能够自动高速从存储器中...
  • - Stack Overflow,显卡也全部用重心坐标和 AABB 来填充三角形的。 三角形线框模式和非三角形线框模式(GUI、CAD),也很容易实现,使用 bresenham 中点算法就行了。 点就更简单了,如果没有涉及 Anti A 直接取整...
  • Windows的内存管理机制

    2022-06-27 14:10:04
    内存管理方式建议大家多看看计算机组成原理,回过头来看windows的内存管理机制就能更好的明白windows为啥这么做~而不是直接让你操作物理内存
  • 操作系统原理笔记

    2021-10-20 16:27:44
    1950s有了让多个程序共享一台计算机的需求 计算机非常贵,集中管理、排队使用 (1) 希望程序切换能够尽量无缝 (2) 希望提供一些程序共同需要的功能,例如读写文件 于是有了操作任务的系统 (1) 批处理系统 (2)...
  • CUDA矩阵计算原理和方法

    千次阅读 2019-05-15 11:16:22
    GPU内存函数cudaMalloc()cudaMemcpy()cudaFree()atomicAdd(addr,y)\_\_syncthreads()GPU内存的分类全局内存共享内存常量内存纹理内存固定内存CUDA矩阵乘法的优化过程CPU矩阵乘法CUDA矩阵乘法VS2017...
  • Win32 GDI 学习总结

    千次阅读 2017-03-14 23:18:03
    操作系统:win7 (xp应该可以,win8未测试) 使用工具:visual studio 2010(或更高) 窗口创建 以前代码的前置问题,首先本教程内的 GDI 画图,在最开始部分主要是 在窗口内部绘制 (为避免...
  • 主从复制的作用 1)做数据的热备份,作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作,避免数据丢失。 2)架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低...
  • 【总结】操作系统原理

    千次阅读 2018-12-24 13:21:31
    文章目录操作系统原理基本内容基本特征并发分时(时间片轮转)共享互斥共享交替共享虚拟时分复用(虚拟处理器)空分复用(虚拟存储器,虚拟设备)异步运行机制用户态和内核态时钟管理中断机制内核kernel/壳shell分时...
  • 程序员的成长之路互联网/程序员/技术/资料共享关注阅读本文大概需要 3.5分钟。来自:杨净 发自 凹非寺量子位 报道 |QbitAI2014年,微软发布了win10系统,后来微软号...
  •  Win32:  1. Win32 简介  2. 注册窗口类  3. 创建窗口  4. 注册窗口类时的附加数据缓冲区  5. 显示窗口  6. 消息循环  7. 消息的分类  8. 消息队列  9. 消息  10. 菜单  11. 资源...
  • 硬件虚拟化:是运行在硬件之上的虚拟化技术,可以模拟硬件资源如:CPU、内存显卡等资源,代表如VMWare、Xen、VirtualBox、AWS EC2、微软的Hyper-V等 操作系统虚拟化:是运行在操作系统之上的,它模拟的是操作系统上...
  • 无盘终端网是传统的无盘技术和新兴的终端技术相结合的产物,既具有无盘网络节省网络组建成本、便于管理的优点,又兼有终端网络“零”网络维护成本、“零”网络升级成本的优点,适用于教学和办公等...本文将介绍Win...
  • 看到原版标题就应该明白,这本书是教你程序设计的,目的是使你掌握程序设计原理并付诸实践,而C++是作为教学的工具语言。作者在本书的前言和引言有提到,如果你只是想掌握C++语法的话,那么这本书或许并不合适;如果...
  • 最开始尝试了双系统的方法,但是安装完之后不能wifi上网,显卡MX250不能适配,触控板失效,很麻烦。 找了一大堆方法,但是最后仍然没搞好,卒。 最后尝试虚拟机的方法。小白一个,连怎么安装虚拟机都不懂,搜索之后...
  • pb程序,wiseinstaller打包,自动配置ODBC,自动配置注册表,解决共享文件问题。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,114
精华内容 445
关键字:

win显卡共享内存工作原理