UE4:UObjectToLua插件简介
简介

该插件继承UBlueprintFunctionLibrary,提供了一些静态函数,可以对UObject类及其子类的对象进行Get和Set操作获取或者修改对应的UPROPERTY的名称或值。

更新

添加了UEnum和TSet的UPROPERTY的独写

提供的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
UFUNCTION(BlueprintCallable, Category = "UTL")
static FString UObjectToLuaString(UObject* Object);

UFUNCTION(BlueprintCallable, Category = "UTL")
static FString UObjectToLuaStringWithIgnore(UObject* Object, FString ignore);

UFUNCTION(BlueprintCallable, Category = "UTL")
static FString UObjectToCSV(UObject* Object);

UFUNCTION(BlueprintCallable, Category = "UTL")
static FString UObjectToCSVwithIgnore(UObject* Object, FString ignore);

UFUNCTION(BlueprintCallable, Category = "UTL")
static FString GetAimValue(UObject* Object, FString Aim);

UFUNCTION(BlueprintCallable, Category = "UTL")
static UObject* SetAimValue(UClass* classNAme, FString Aim, FString nameAndValue);

UFUNCTION(BlueprintCallable, Category = "UTL")
static UObject* LuaStringToUObject(UClass* className, FString nameAndValue);

UFUNCTION(BlueprintCallable, Category = "UTL")
static UObject* LuaStringToSetUObject(UObject* objcet, FString nameAndValue);

UFUNCTION(BlueprintCallable, Category = "UTL")
static UObject* CSVToUObject(UClass* className, FString nameAndValue);

UFUNCTION(BlueprintCallable, Category = "UTL")
static UObject* CSVToSetUObject(UObject* objcet, FString nameAndValue);

UFUNCTION(BlueprintCallable, Category = "UTL")
static UObject* CreateObject(UClass* className);

UFUNCTION(BlueprintCallable, Category = "UTL")
static FString LuaStringToCSV(FString LuaString);
使用举例

用第一个函数举例,该函数需要一个UObject的对象,或者子类的对象,最后返回一个FString变量,该变量是该Object的所有UPROPERTY的名称和值,字符串格式符合Lua的Table格式。

比如我有如下几个Object类类型

MyObject类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
UCLASS(BlueprintType, Blueprintable)
class GAMETEST_API UMyObject : public UObject
{
GENERATED_BODY()

public:

UMyObject();

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GamePlay)
bool baseBool;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
int32 baseInt;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
float baseFloat;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
FString baseString;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
TArray<FString> baseArray;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
TMap<int32, bool> baseMap;
};

MyObjectChild类,继承自MyObject类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
UCLASS(BlueprintType, Blueprintable)
class GAMETEST_API UMyObjectChild : public UMyObject
{
GENERATED_BODY()

public:

UMyObjectChild();

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GamePlay)
bool childBool;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
int32 childInt;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
float childFloat;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
FString childString;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
TArray<bool> childArray;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay)
TMap<int32, int32> childMap;
};

MyAnother类,其中成员变量有一个MyObject类和一个MyObjectChild类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UCLASS(BlueprintType, Blueprintable)
class GAMETEST_API UMyAnotherObject : public UObject
{
GENERATED_BODY()

public:

UMyAnotherObject();

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GamePlay)
UMyObject* a;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GamePlay)
UMyObjectChild* b;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = GamePlay)
bool anotherbool;
};

那么假如以上所有类的成员变量在构造函数中有初始化(应该是必须有),我实例化一个MyAnotherObject的对象,将其传给此函数:

1
2
UMyAnotherObject* another = NewObject<UMyAnotherObject>();
FString getObjectProperties = UObjectToLuaParser::UObjectToLuaString(another);

最后得到的getObjectProperties应该是如下的字符串:

1
return{a={baseBool=false,baseInt=1,baseFloat=1.0,baseString='base string',baseArray={'basearray01','basearray02','basearray03',},baseMap={[1]=true,[2]=false,[3]=true,},},b={childBool=true,childInt=2,childFloat=2.0,childString='child string',childArray={true,false,true,false,true,},childMap={[1]=111,[2]=222,[3]=333,},rotate={Pitch=1.0,Yaw=1.1,Roll=1.2,},vector={X=111.0,Y=222.0,Z=333.0,},baseBool=false,baseInt=1,baseFloat=1.0,baseString='base string',baseArray={'basearray01','basearray02','basearray03',},baseMap={[1]=true,[2]=false,[3]=true,},},anotherbool=false,}

注意这里是没有任何空格和换行符的,最外层的大括号和return是为了Lua拿到后直接就可以dump为table进行处理,此处也可以修改最后输出的地方进行修改。下面是处理过格式的,方便对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
return{
a={
baseBool=false,
baseInt=1,
baseFloat=1.0,
baseString='base string',
baseArray={
'basearray01',
'basearray02',
'basearray03',
},
baseMap={
[1]=true,
[2]=false,
[3]=true,
},
},
b={
childBool=true,
childInt=2,
childFloat=2.0,
childString='child string',
childArray={
true,
false,
true,
false,
true,
},
childMap={
[1]=111,
[2]=222,
[3]=333,
},
rotate={
Pitch=1.0,
Yaw=1.1,
Roll=1.2,
},
vector={
X=111.0,
Y=222.0,
Z=333.0,
},
baseBool=false,
baseInt=1,
baseFloat=1.0,
baseString='base string',
baseArray={
'basearray01',
'basearray02',
'basearray03',
},
baseMap={
[1]=true,
[2]=false,
[3]=true,},
},
}
anotherbool=false,
}
}

需要指出的是目前仅支持基本数据类型,TArray,TMap,FVector,FRotator,不支持不支持不支持自定义结构体。具体原因可以参考大致原理部分。

函数简介
1
static FString UObjectToLuaString(UObject* Object);

上文已经举例,不再赘述。

1
static FString UObjectToLuaStringWithIgnore(UObject* Object, FString ignore);

ignore参数中是用逗号分隔的不需要的UPROPERTY的name,最后一个后面也要有逗号,做成字符串是为了可以直接用Lua调用方便,ignore中的UPROPERTY会在Get信息的过程中跳过

1
static FString UObjectToCSV(UObject* Object);

该函数会最后将字符串修改为符合CSV文件要求的格式,在收集信息到表格的过程中可以使用

1
static FString UObjectToCSVwithIgnore(UObject* Object, FString ignore);

ignore和上面第二个一样,最后的字符串和上面第三个一样符合CSV格式

1
static FString GetAimValue(UObject* Object, FString Aim);

Aim和上文的ignore不同,只能存放一个UPROPERTY的name,不会进一步处理,最后返回的字符串只有这一个UPROPERTY的信息

1
static UObject* SetAimValue(UClass* classNAme, FString Aim, FString nameAndValue);

第一个参数是UCLass*类,函数内部会新建一个该UClass对应的对象实例,Aim依然是一个UPROPERTY的name,会将该UPROPERTY的value设置为第三个参数中的值。注意第三个参数要求写成name=value的形式

1
static UObject* LuaStringToUObject(UClass* className, FString nameAndValue);

这里注意第二个参数,第二个参数的形式和上面第一个函数最后输出的形式相同,但不需要最外层的return和大括号

1
static UObject* LuaStringToSetUObject(UObject* objcet, FString nameAndValue);

和上一个函数的区别在于第一个参数,这个可以理解为给已存在的实例对象的Set方法

1
static UObject* CSVToUObject(UClass* className, FString nameAndValue);

从csv字符串新建一个对象并使用第二个参数进行赋值

1
static UObject* CSVToSetUObject(UObject* objcet, FString nameAndValue);

用csv字符串设置一个已存在的对象

1
static UObject* CreateObject(UClass* className);

自己封装的一个从UClass创建对应对象的函数

1
static FString LuaStringToCSV(FString LuaString);

一个将Lua风格的字符串改为CSV风格的函数

下载和使用

项目地址:https://github.com/muchenhen/UObjectToLua

Git: git@github.com:muchenhen/UObjectToLua.git

将UObjectToLua文件夹复制进项目或者引擎的Plungins文件夹,重新生成项目的解决方案,重新编译项目

在其他插件中使用的话在对应的插件的.build.cs文件包含UObjectToLua模块,运行时使用的话在项目的.build.cs中包含。在要使用的地方包含对应的头文件。

欢迎交流和捉虫,毕竟作者只是一个菜鸡……


UE4:PSO使用指南
完整流程

请按照以下文章的顺序顺序执行。

先来一个一图流指南:

ue4-pso-1

启用PSO

编辑(Edit) > 项目设置(Project Settings) > 打包(Packaging) > 打包(Packaging),按照下图打开选项:

ue4-pso-2.jpg

窗口(Window) > 开发者工具(Developer Tools) > 设备描述(Device Profiles)

ue4-pso-3.jpg

Existing Device Profiles 输入中找到 Android 选项,点击其命名旁边的 省略号 将该设备描述打开:

ue4-pso-4.jpg

控制台变量(Console Variables) 部分中查找 渲染(Rendering),按下 加号 图标公开 Search 输入:

ue4-pso-5.jpg

r.ShaderPipelineCache.Enabled设为1:

ue4-pso-6.jpg

此时完成了PSO收集的启用,判断是否启用可以在游戏启动的log中进行查看,如下图所示则为启动成功:

ue4-pso-7.jpg

可以看到图中r.ShaderPipelineCache.Enabled:1

收集PSO

打包并在目标设备中安装,多次运行游戏的各个流程

请注意结束游戏的时候请使用游戏的退出方式,不要直接用手机结束进程。如果非得用手机杀死进程,请先转到后台片刻,不要在PSO收集过程中强行结束,会导致收集到的PSO信息无法使用

收集到的文件在:

UE4Game\NameOfProject\NameOfProject\Saved\CollectedPSOs

ue4-pso-8.jpg

这里的所有文件下文都用,全部复制走

编译PSO

在C盘根目录创建文件夹名为PSOCaching

然后去这个路径找到两个csv文件:ProjectName\Saved\Cooked\PlatfourmYouCookedFor\ProjectName\Metadata\PipelineCaches

ue4-pso-9.jpg

复制到我们刚创建的文件夹

还有前面提到过后文要用的文件,也粘贴到这个文件夹里

ue4-pso-10.jpg

接下来去打包用的引擎文件夹里Engine\Binaries\Win64找到UE4Editor-Cmd.exe

右键创建该文件的快捷方式,复制走,到上文提到的文件夹里

然后在这个快捷方式邮件属性,目标这一栏里面后面加上:YourGameName -run=ShaderPipelineCacheTools expand C:\PSOCaching*.rec.upipelinecache C:\PSOCaching*.scl.csv YourGameName_GLSL_ES3_1_ANDROID.stablepc.csv

确认关闭

然后直接执行,或者用cmd执行,有错的话可以看到报错

然后在Engine\Binaries\Win64文件夹中寻找这个csv

ue4-pso-11.jpg

使用PSO

将该文将放置到

项目的 Build > Platform Name > PipelineCaches 文件夹,将stablepc.csv文件复制到Pipeline Caches文件夹。

再次进行打包

PSO成功启用的话,设备上第一次启动游戏会有明显的较长的编译shader的时间


DirectX12:4.2 CPU和GPU交互

在图形编程中两个处理:CPU和GPU并行工作,有时需要同步。为了获得最佳性能,要尽可能长时间地保持忙碌,并最小化同步,最好能不同步。需要同步就这意味着GPU或者CPU是空闲的,需要等待另一个完成一些工作,破坏了并行性。并且同步需要开销。

4.2.1命令队列和命令列表

GPU有一个命令队列command queue,本质上是一个环形缓冲区ring buffer,CPU通过使用命令列表command list的Direct3D API向队列提交命令。命令被提交到命令队列不会立即被GPU执行,而是一直排在队列中,直到GPU准备好再进行处理。

在DX11中有两种绘制方式,立即渲染或者延迟渲染,立即渲染会将缓冲区的命令通过驱动层直接发送到GPU执行。

命令队列为空的话GPU无事可做,命令队列太满的话,GPU无法及时处理,则CPU将在某个时间点不得不空闲,这相当于浪费了硬件资源。理想目标是要让CPU和GPU都保持忙碌的状态。

在Direct3D 12中,命令队列由ID3D12CommandQueue接口表示。 需要通过D3D12_COMMAND_QUEUE_DESC结构体描述队列,然后调用ID3D12Device :: CreateCommandQueue方法创建队列。

比如可以采用如下的流程:

1
2
3
4
5
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;  //定义ID3D12CommandQueue的地址
D3D12_COMMAND_QUEUE_DESC queueDesc = {}; //定义CommandQueue的描述符及其一些参数
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue))); //通过地址以及描述符创建命令列表

IID_PPV_ARGS辅助宏的定义为:

1
#define IID_PPV_ARGS(ppType) _uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)

其中__uuof(**(ppType))获取(**(ppType))的COM接口ID(globally unique identifier,GUID,全局唯一标识符),在上面代码段中得到的就是ID3D12CommandQueue接口的COM ID。 IID_PPV_ARGS_Helper函数基本上将ppType转换为void **。

主要方法之一是ExecuteCommandLists方法,它将命令列表中的命令添加到队列中:

1
2
3
void ID3D12CommandQueue::ExecuteCommandLists(
UINT Count,//数组中列出的命令数
ID3D12CommandList *const *ppCommandLists//指向命令列表数组中第一个元素的指针);

命令列表按照从第一个数组元素开始的顺序执行。

正如上面的方法声明所提示的,图形的命令列表由从ID3D12CommandList接口继承的ID3D12GraphicsCommandList接口表示。 ID3D12GraphicsCommandList接口提供了多种向命令列表添加命令的方法。 例如,下面的代码添加了设置窗口,清除渲染目标视图和发出绘制调用的命令:

1
2
3
4
mCommandList pointer to ID3D12CommandList  
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->ClearRenderTargetView(mBackBufferView,Colors::LightSteelBlue, 0, nullptr);
mCommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);

从命令的名字来看是立即执行的命令,但其实不是。上面的这些代码只是把命令添加到了命令列表。ExecuteCommandLists方法将命令添加到命令队列,之后GPU处理命令。当完成向命令列表添加命令时,必须通过调用ID3D12GraphicsCommandList::Close方法来表明已经完成了对命令的记录:

1
mCommandList->Close();//记录命令

在传递给ID3D12CommandQueue :: ExecuteCommandLists之前,命令列表必须关闭。

与命令列表关联的是称为ID3D12CommandAllocator的内存支持类。 当命令被记录到命令列表中时,它们实际上将被存储在关联的命令分配器中。 当通过ID3D12CommandQueue :: ExecuteCommandLists执行命令列表时,命令队列将引用分配器中的命令。 命令分配器是用ID3D12Device接口创建的:

1
2
3
4
HRESULT ID3D12Device::CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE type,
REFIID riid,
void **ppCommandAllocator);
  1. type:可以与此分配器关联的命令列表的类型。
    两种常见类型是:
    1. D3D12_COMMAND_LIST_TYPE_DIRECT:存储GPU直接执行的命令列表(上文到现在描述的命令列表类型)。
    2. D3D12_COMMAND_LIST_TYPE_BUNDLE:指定命令列表表示一个包。在构建命令列表中有一些CPU开销,所以Direct3D 12提供了一种优化,能够将一系列命令记录到捆绑包中。在记录包之后,驱动程序将预处理这些命令以在渲染过程中优化它们的执行。因此,应该在初始化时记录捆绑包。如果分析显示构建特定命令列表需要花费大量时间,则应该将捆绑的使用视为优化。 Direct3D 12绘图API已经非常高效,所以不需要经常使用bundle。
  2. riid:我们想要创建的ID3D12CommandAllocator接口的COM ID
  3. ppCommandAllocator:输出一个指向创建的命令分配器的指针。

命令列表也是从ID3D12Device创建的:

1
2
3
4
5
6
7
HRESULT ID3D12Device::CreateCommandList(  
UINT nodeMask,
D3D12_COMMAND_LIST_TYPE type,
ID3D12CommandAllocator *pCommandAllocator,
ID3D12PipelineState *pInitialState,
REFIID riid,
void **ppCommandList);
  1. nodeMask:为单GPU系统设置为0。 否则,节点掩码将标识与该命令列表关联的物理GPU。假设单GPU系统。
  2. type:命令列表的类型:_COMMAND_LIST_TYPE_DIRECT或D3D12_COMMAND_LIST_TYPE_BUNDLE。
  3. pCommandAllocator:与创建的命令列表关联的分配器。 命令分配器类型必须与命令列表类型匹配。
  4. pInitialState:指定命令列表的初始管道状态。 这对bundle来说可以为null,在特殊情况下,为了初始化而执行命令列表并且不包含任何绘图命令。
  5. riid:要创建的ID3D12CommandList接口的COM ID。
  6. ppCommandList:输出指向创建的命令列表的指针。

可以创建与同一分配器关联的多个命令列表,但不能同时进行记录。

试图用同一个分配器在一行中创建两个命令列表,会错误:

1
2
3
D3D12 ERROR: ID3D12CommandList::

{Create,Reset}CommandList: The command allocator is currently in-use by another command list.

用ID3D12CommandQueue :: ExecuteCommandList(C)之后,通过调用ID3D12CommandList :: Reset方法重用C的内部存储器以记录一组新的命令是安全的。 此方法的参数与ID3D12Device :: CreateCommandList中的匹配参数相同。

1
2
3
HRESULT ID3D12CommandList::Reset(
ID3D12CommandAllocator *pAllocator,
ID3D12PipelineState *pInitialState);

这个方法将命令列表放在与刚刚创建的状态相同的状态,但允许重新使用内部存储器,避免释放旧命令列表并分配新命令列表。 请注意,重置命令列表不会影响命令队列中的命令,因为关联的命令分配器仍然具有命令队列引用的内存中的命令。

在向GPU提交完整帧的渲染命令后,需要在命令分配器中重用下一帧的内存,ID3D12CommandAllocator :: Reset方法可用于此:

1
HRESULT ID3D12CommandAllocator::Reset(void);

类似于调用std :: vector :: clear,它将vector重新调整为零,但保持当前容量不变。 但是,由于命令队列可能引用了分配器中的数据,因此只有在确定GPU已完成执行分配器中的所有命令后,才能重置命令分配器。

4.2.2 CPU / GPU同步

由于有两个处理器并行运行,所以有许多同步问题。

假设,我们有储存了一些我们要绘制的集合体的位置的资源R,CPU更新R的数据去存储位置p1然后将参照r的绘图命令c添加到命令队列中,以便在位置p1处绘制几何图形。向命令队列添加命令不会阻塞CPU,所以cpu可以继续。在gpu执行draw命令c之前,cpu继续运行并覆盖r的数据以存储新的位置p2,会造成错误。

这种情况的一个解决方案是强制CPU等待,直到GPU完成队列中所有命令的处理,直到达到指定的隔断点,将其成为刷新命令队列。可以使用Fence(围栏)来实现。ID3D12Fence接口用于实现CPU和GPU同步:

1
2
3
4
5
6
7
8
9
10
HRESULT ID3D12Device::CreateFence(
UINT64 InitialValue,
D3D12_FENCE_FLAGS Flags,
REFIID riid,
void **ppFence);
// 示例
ThrowIfFailed(md3dDevice->CreateFence(
0,
D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&mFence)));

fence对象维护一个UINT64值,该值只是一个整数,用于及时标识一个fence点。从0开始,每次需要标记一个新的栅栏点时增加1。下面的代码展示了如何使用fence刷新命令队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
UINT64 mCurrentFence = 0;
void D3DApp::FlushCommandQueue()
{
// 增加围栏值,然后将命令标记到此围栏点
mCurrentFence++;
//向命令队列添加一条指令来设置一个新的围栏点
//这条命令交给GPU进行处理,在GPU完成此命令队列中Signal()之前的所有命令前,不会设置新的围栏点
ThrowIfFailed(mCommandQueue->Signal(mFence.Get(),mCurrentFence));
//CPU等到GPU完成到这个围栏点之前的所有命令
if(mFence->GetCompletedValue() < mCurrentFence)
{
HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
//当GPU命中当前的围栏,即执行到Signal指令,修改了围栏值,激发此预定事件
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));
//等待GPU命中围栏激发事件
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
}
}

4.2.3资源转换

为了实现常见的呈现效果,Gpu通常在一个步骤中写入资源r,然后在后面的步骤中读取资源r。但是,如果gpu没有完成对资源r的写入或根本没有开始写入,则从资源r读取将是一种资源危害。要解决此问题,Direct3D将状态与资源关联资源在创建时处于默认状态,由应用程序告诉Direct3D任何状态转换。这使得gpu能够做任何它需要做的工作来进行转换和防止资源危害。例如,如果我们正在写入资源(例如纹理),则将纹理状态设置为渲染目标状态;当需要读取纹理时,将其状态更改为着色器资源状态。通过将转换通知direct3d,gpu可以采取步骤避免危险,例如,在从资源读取之前等待所有写操作完成。由于性能原因,资源转换的负担落在程序员身上。

资源转换是通过在命令列表上设置转换资源屏障的数组来指定的;它是一个数组,以防使用一个api调用转换多个资源。在代码中,资源屏障由d3d12_resource_barrier_desc结构表示。下面的helper函数(在d3dx12.h中定义)返回给定资源的转换资源屏障描述,并指定before和after状态:

struct CD3DX12_RESOURCE_BARRIER : public
D3D12_RESOURCE_BARRIER
{
// […] convenience methods
static inline CD3DX12_RESOURCE_BARRIER Transition(
_In_ ID3D12Resource* pResource,
D3D12_RESOURCE_STATES stateBefore,
D3D12_RESOURCE_STATES stateAfter,
UINT subresource =
D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES,
D3D12_RESOURCE_BARRIER_FLAGS flags =
D3D12_RESOURCE_BARRIER_FLAG_NONE)
{
CD3DX12_RESOURCE_BARRIER result;
ZeroMemory(&result, sizeof(result));
D3D12_RESOURCE_BARRIER &barrier = result;
result.Type =
D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
result.Flags = flags;
barrier.Transition.pResource = pResource;
barrier.Transition.StateBefore = stateBefore;
barrier.Transition.StateAfter = stateAfter;
barrier.Transition.Subresource = subresource;
return result;
}
// […] more convenience methods

};

CD3DX12_RESOURCE_BARRIER扩展了D3D12_RESOURCE_BARRIER_DESC并添加了便利方法。

示例应用程序中该函数的一个示例如下:

mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition( CurrentBackBuffer(), D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET));

此代码将表示在屏幕上显示的图像的纹理从呈现状态转换为呈现目标状态。资源屏障已添加到命令列表中,可以将资源屏障转换视为命令本身,指示GPU正在转换资源的状态,以便可以在执行后续命令时采取必要步骤防止资源危险。

除了过渡类型外,还有其他类型的资源壁垒。

4.2.4多线程命令

direct3d 12是为高效多线程而设计的,命令列表设计是direct3d利用多线程的一种方式。对于包含大量对象的大型场景,构建命令列表以绘制整个场景可能需要CPU时间。因此,并行地构建命令列表是一种可行的办法;例如,您可以生成四个线程,每个线程负责构建一个命令列表来绘制25%的场景对象。

关于命令列表多线程处理,需要注意以下几点:

  1. 命令列表不是自由线程的;也就是说,多个线程不能共享同一个命令列表并同时调用其方法。所以一般来说,每个线程都会得到自己的命令列表。
  2. 命令分配器不是自由线程;也就是说,多个线程不能共享同一个命令分配器并同时调用其方法。所以一般来说,每个线程都有自己的命令分配器。
  3. 命令队列是自由线程的,因此多个线程可以同时访问命令队列和调用其方法。特别是,每个线程都可以同时将生成的命令列表提交给线程队列。
  4. 出于性能原因,应用程序必须在初始化时间指定它们同时记录的命令列表的最大数量。

DirectX12:4.1 初始化预备知识

目的:

  1. 基本了解Direct3D在3D硬件编程中的作用。
  2. 了解COM在Direct3D中扮演的角色。
  3. 学习基本的图形概念,如2D图像如何存储,页面翻转,深度缓冲,多采样,以及CPU和GPU如何交互。
  4. 学习如何使用性能计数器函数获得高分辨率计时器读数。
  5. 了解如何初始化Direct3D。
  6. 熟悉本书所有演示使用的应用程序框架的一般结构。

4.1.1 Direct3D 12 概述

D3D是一个低级图形API,用于控制和编程GPU,用硬件加速渲染。与11对比,除了一些新的渲染特性,还进行了重新设计,性能优化上极大减少了CPU开销,改进了多线程支持,API抽象程度降低。使用更加困难,但是性能更好。

4.1.2 组件对象模型COM

Component Object Model 简称COM,是一种允许DirectX独立于编程语言并具有向后兼容性的技术。通常将COM对象称为接口,可以将其视为c++类来使用。但不能使用new方法来获取一个指向COM对象的指针,需要用特定的函数处理。自身采用引用计数,计数为0会自行释放所占用的内存。

Windows提供Microsoft::WRL::ComPtr类管理COM对象的生命周期,其中三个常用方法为:

  1. Get:返回一个指向此底层COM接口的指针
  2. GetAddressOf:返回一个指向此底层COM接口的指针的地址
  3. Reset:将此ComPtr实例设置为nullptr,直接复制为nullptr效果一样

4.1.3 纹理格式

一般的2D纹理存储图像的对应像素的颜色值。但是纹理可以有更广泛的用途,一维纹理就像数据元素的一维数组,二维纹理就像数据元素的二维数组,三维纹理就相对于三维数组。GPU可以对其进行特殊操作,比如滤波和多重采样。

纹理是以重数据元素构成的矩阵,但只能存储特定格式的数据元素。

注意,纹理不能存储任意类型的数据。只能存储DXGI_FORMAT枚举类型。

下面有几个例子:

  1. DXGI_FORMAT_R32G32B32_FLOAT: 每个元素有三个32位浮点组件。
  2. DXGI_FORMAT_R16G16B16A16_UNORM: 每个元素有四个映射到[0,1]范围的16位组件。
  3. DXGI_FORMAT_R32G32_UINT: 每个元素都有两个32位无符号整数组件。
  4. DXGI_FORMAT_R8G8B8A8_UNORM: 每个元素有四个映射到[0,1]范围的8位无符号组件。
  5. DXGI_FORMAT_R8G8B8A8_SNORM: 每个元素有四个映射到[- 1,1]范围的8位有符号组件。
  6. DXGI_FORMAT_R8G8B8A8_SINT: 每个元素有四个映射到[- 128,127]范围的8位有符号整数组件。
  7. DXGI_FORMAT_R8G8B8A8_UINT: 每个元素有四个映射到[0,255]范围的8位无符号整数组件。

RGBA分别代表红绿蓝,透明度。虽然这些类型名字是和颜色有关的,但是并不意味着必须存储颜色信息。比如:

DXGI_FORMAT_R32G32B32_FLOAT

是可以存储任何3D向量的。

另外有无类型格式的纹理类型,用来预留内存,等纹理被绑定到管线后再解释是什么数据类型,比如

DXGI_FORMAT_R16G16B16A16_TYPELESS

除了上文提到的,DXGI_FORMAT枚举类型还可以用来描述顶点以及索引的数据格式。

4.1.4 The Swap China and Page Flipping

交换链和页面翻转

为了不让玩家看到没渲染完毕的画面,需要在后台渲染好之后在交换指针,后台变前台,前台变后台继续渲染下一帧。前后缓冲区形成了交换链。

D3D中交换链是 IDXGISwapChain 接口。该接口存储前后缓冲区的纹理数据,并提供调整缓冲区大小的方法。

交换操作称为Presenting,呈现、提交、显示。

IDXGISwapChain::ResizeBuffers

IDXGISwapChain::Present

使用两个缓冲区称为双缓冲。可以使用两个以上的缓冲区,使用三个就是三缓冲,一般两个缓冲区就够了。

缓冲区可以提升效率,但也会带来新的问题,画面闪烁、撕裂,掉帧等,解决这些问题就需要垂直同步

4.1.5 深度缓冲

深度缓冲是纹理的一个例子。不存储颜色信息,存储的是特定像素的深度信息。与后缓冲区的像素一一对应。深度信息的0.0表示离查看器最近的,1.0表示最远。

如图一个简单的场景,存在遮挡关系,需要让DirectX确定像素的前后关系,就是用了深度缓冲或Z-buffer的技术。使用深度缓冲时,绘制对象的顺序就不重要了。

再进行任何渲染前,深度缓冲区会被清除为默认值,通常是1.0,即可行的深度最大值。后缓冲区也会重置为默认颜色。

每一个像素点,遇到深度值小于当前深度值的时候会更新,否则不更新。

深度缓冲是一个纹理,需要有特定的存储格式:

  1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT: 指定32位浮点深度缓冲区,8位(无符号整数)预留给模板缓冲区映射到[0,255]范围,24位不用于填充。
  2. DXGI_FORMAT_D32_FLOAT: 指定一个32位浮点深度缓冲区。
  3. DXGI_FORMAT_D24_UNORM_S8_UINT: 指定一个映射到[0,1]范围的无符号24位深度缓冲区,并为模板缓冲区保留8位(无符号整数)映射到[0,255]范围。该缓冲区可以称为模板缓冲区,会在第11章详述。
  4. DXGI_FORMAT_D16_UNORM:指定映射到[0,1]范围的无符号16位深度缓冲区。

4.1.6 Resources and Descriptors

资源和描述符

渲染过程中,GPU会不断的写入和读取资源。比如从缓冲区读取纹理或位置信息,写操作向后台缓冲区或者模板缓冲区写入数据等。

在绘制命令发出之前,需要绑定或者链接资源到即将要渲染的drawcall的渲染管道。有些资源每次渲染会变化,就需要更新绑定信息。但是GPU资源不是直接绑定到渲染管线的。资源是通过描述符对象被间接引用的,可以将其视为向GPU描述资源的轻量级结构,本质上,它是一种间接的层次;给定一个资源描述符,GPU可以获取实际的资源数据,并知道它的必要信息。

使用描述符的原因:

GPU资源,本身是普通的内存块,可以被管线不同阶段使用。并且有时候某个阶段只需要资源中的部分数据被绑定到管线上,需要有方法将部分资源从整块中取出。还有一种情况,就是无类型格式,需要描述符为GPU解释资源类型。

主要作用有,获取局部资源,向管线解释资源类型,告知资源如何使用,用在什么阶段。

4.1.7 多重采样

抗锯齿/多重采样MSAA

显示器像素不是无穷小的,会有走样。

抗锯齿通过采样和相邻像素来生成一个像素的最终颜色,使图像更平滑。

显示器分辨率增加,减小像素点的大小也可以显著减少走样现象。但是好的显示器比较贵。而且硬件总会受限。

所以可以应用超采样技术(SSAA)。原理是让后缓冲区和深度缓冲区为屏幕分辨率的4倍大小,交换前后缓冲时,进行向下采样,每4个像素点的颜色平均成一个。超采样通过软件分辨率来实现。但是超采样对内存和像素处理又多了较高的要求,四倍的内存和像素处理数量,满足条件的硬件也不是很便宜的。

D3D支持一种折中的技术,称为多重采样。这种技术不用对每一个像素进行计算,它计算一次中心像素的颜色,然后基于可视性和覆盖性,将得到的信息分享给其他子像素。比如4X多重采样,1个像素中有4个子像素,后缓冲区和深度缓冲也是分辨率的四倍,但是不用计算四个子像素的颜色,而是计算像素中心的颜色,基于每个子像素被深度/模板测试的结果和子像素中心是否在多边形内部这两个因素,将这个颜色分给四个子像素。

除此之外还有更多的反锯齿技术。

4.1.8 DirectX中的MSAA

DXGI_SAMPLE_DESC

该结构体有两个成员,定义如下:

1
2
3
4
typedef struct DXGI_SAMPLE_DESC {
UINT Count;
UINT Quality;
} DXGI_SAMPLE_DESC;

Count成员指定每像素采样的数量,Quality用于指定所需的质量级别,“质量级别”的含义可能因硬件制造商而异。

两个参数越大,效果越好,但是开销也会更高。

质量级别的范围取决于纹理格式和每像素采样的数量。

可以使用ID3D12Device::CheckFeatureSupport方法查询给定纹理格式的质量级别数量和样本数量。

1
2
3
4
5
6
typedef struct D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS{
_In_ DXGI_FORMAT Format;
_In_ UINT SampleCount;
_In_ D3D12_MULTISAMPLE_QUALITY_LEVEL_FLAGS Flags;
_Out_ UINT NumQualityLevels;
}D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS;
1
2
3
4
5
6
7
8
9
10
11
12
D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels;
msQualityLevels.Format = mBackBufferFormat;
msQualityLevels.SampleCount = 4;
msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
msQualityLevels.NumQualityLevels = 0;
ThrowIfFailed(
md3dDevice->CheckFeatureSupport(
D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
&msQualityLevels,
sizeof(msQualityLevels)
)
);

其中第二个参数兼具输入和输出的属性。

如果不希望使用多重采样,可以将采样数量设置为1,质量级别设置为0。

创建交换链缓冲区和深度缓冲区时都需要填写该结构体,而且设置一定要相同。

4.1.9Feature Levels

功能级别

Direct3D 11引入了特征级别的概念(在代码中由D3D_FEATURE_LEVEL枚举类型表示),它大致对应于从版本9到版本11的各种Direct3D版本:

1
2
3
4
5
6
7
8
9
enum D3D_FEATURE_LEVEL {
D3D_FEATURE_LEVEL_9_1 = 0x9100,
D3D_FEATURE_LEVEL_9_2 = 0x9200,
D3D_FEATURE_LEVEL_9_3 = 0x9300,
D3D_FEATURE_LEVEL_10_0 = 0xa000,
D3D_FEATURE_LEVEL_10_1 = 0xa100,
D3D_FEATURE_LEVEL_11_0 = 0xb000,
D3D_FEATURE_LEVEL_11_1 = 0xb100
}D3D_FEATURE_LEVEL;

一个支持feature level 11的GPU必须支持整个Direct3D 11功能集。如果用户的硬件不支持某个级别,则应用程序可以退回到旧的级别。

4.1.10 DirectX 图形基础结构

DirectX Graphics Infrastructure (DXGI) 是一个与Direct3D一起使用的API。

主要目的是让多种图形API中所共有的底层人物可以借助一些通用API来进行处理。

例如,2D渲染API和3D渲染API一样需要交换链和页面翻转来获得平滑的动画,因此,交换链接口IDXGISwapChain就是DXGI API的一部分。

DXGI处理其他常见的图形功能,如全屏模式转换、枚举图形系统信息(如显示适配器、监视器和支持的显示模式,分辨率、刷新率等。它还定义了各种受支持的表面格式DXGI_FORMAT。

DXGI的一个关键接口:IDXGIFactory。它主要用于创建IDXGISwapChain接口和枚举显示适配器。显示适配器实现图形化功能。显示适配器是一个物理硬件(例如,显卡)或者是系统提供的一个软件显示适配器,用来模拟硬件图形功能。一个系统可以有多个适配器(例如有多个显卡)。适配器由IDXGIAdapter接口表示。可以用以下代码枚举系统上的所有适配器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void D3DApp::LogAdapters()
{
UINT i = 0;
IDXGIAdapter* adapter = nullptr;
std::vector adapterList;
while (mdxgiFactory->EnumAdapters(i, &adapter) != DXGI_ERROR_NOT_FOUND)
{
DXGI_ADAPTER_DESC desc;
adapter->GetDesc(&desc);
std::wstring text = L”***Adapter: “;
text += desc.Description; text += L”\n”;
OutputDebugString(text.c_str());
adapterList.push_back(adapter);
++i;
}
for (size_t i = 0; i < adapterList.size(); ++i)
{
LogAdapterOutputs(adapterList[i]);
ReleaseCom(adapterList[i]);
}
}

下面是这个方法的输出示例:

1
2
***Adapter: NVIDIA GeForce GTX 760
***Adapter: Microsoft Basic Render Driver

Microsoft Basic Render Driver在Win8及以上版本中都有。

一个系统可以有多个显示器。一个显示器就是一个显示输出(display output)。显示输出由IDXGIOutput接口表示。每个适配器都与一个或多个显示输出相关联。例如,考虑一个具有两个显卡和三个显示器的系统,其中两个显示器连接到一个显卡,而第三个显示器连接到另一个显卡。在本例中,一个适配器有两个与之关联的显示输出,而另一个适配器有一个与之关联的显示输出。可以用以下代码枚举与适配器关联的所有输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void D3DApp::LogAdapterOutputs(IDXGIAdapter* adapter)
{
UINT i = 0;
IDXGIOutput* output = nullptr;
while (adapter->EnumOutputs(i, &output) != DXGI_ERROR_NOT_FOUND)
{
DXGI_OUTPUT_DESC desc;
output->GetDesc(&desc);
std::wstring text = L”***Output: “;
text += desc.DeviceName;
text += L”\n”;
OutputDebugString(text.c_str());
LogOutputDisplayModes(output, DXGI_FORMAT_B8G8R8A8_UNORM);
ReleaseCom(output);
++i;
}
}

注意,根据文档,显卡正常的情况下,“Microsoft Basic Render Driver”没有显示输出。

每个监视器都支持一组显示模式。显示模式是指DXGI_MODE_DESC中的以下数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef struct DXGI_MODE_DESC {
UINT Width; // 分辨率宽度
UINT Height;// 分辨率高度
DXGI_RATIONAL RefreshRate; //刷新率
DXGI_FORMAT Format;// 显示格式
DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;// 逐行扫描 隔行扫描
DXGI_MODE_SCALING Scaling;// 图像相对于屏幕拉伸的方式
} DXGI_MODE_DESC;

typedef struct DXGI_RATIONAL {
UINT Numerator;
UINT Denominator;
} DXGI_RATIONAL;

typedef enum DXGI_MODE_SCANLINE_ORDER {
DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED = 0,//未指定
DXGI_MODE_SCANLINE_ORDER_PROGRESSIVE = 1,//逐行扫描
DXGI_MODE_SCANLINE_ORDER_UPPER_FIELD_FIRST = 2,//高场优先
DXGI_MODE_SCANLINE_ORDER_LOWER_FIELD_FIRST = 3//低场优先
} DXGI_MODE_SCANLINE_ORDER;

typedef enum DXGI_MODE_SCALING {
DXGI_MODE_SCALING_UNSPECIFIED = 0,//未指定
DXGI_MODE_SCALING_CENTERED = 1,//不做缩放,将图像显示在屏幕正中
DXGI_MODE_SCALING_STRETCHED = 2//根据屏幕的分辨率对图像进行拉伸缩放
} DXGI_MODE_SCALING;

确定显示模式的具体格式之后,可以通过以下代码获得某个显示输出对改格式所支持的全部显示模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void D3DApp::LogOutputDisplayModes(IDXGIOutput* output, DXGI_FORMAT format)
{
UINT count = 0;
UINT flags = 0;

// 用nullptr作为参数调用此函数,可以获取符合条件的显示模式的个数
output->GetDisplayModeList(format, flags, &count, nullptr);

std::vector<DXGI_MODE_DESC> modeList(count);
output->GetDisplayModeList(format, flags, &count, &modeList[0]);

for (auto& x : modeList)
{
UINT n = x.RefreshRate.Numerator;
UINT d = x.RefreshRate.Denominator;
std::wstring text =
L"Width = " + std::to_wstring(x.Width) + L" " +
L"Height = " + std::to_wstring(x.Height) + L" " +
L"Refresh = " + std::to_wstring(n) + L"/" + std::to_wstring(d) +
L"\n";

::OutputDebugString(text.c_str());
}
}

下面是这段代码的一些输出示例:

1
2
3
4
***Output: [\\.\DISPLAY2](file://./DISPLAY2)

Width = 1920 Height = 1080 Refresh = 59950/1000
Width = 1920 Height = 1200 Refresh = 59950/1000

在进入全屏模式时,枚举显示模式尤为重要。为了获得最佳的全屏性能,指定的显示模式(包括刷新率)必须与显示器支持的显示模式完全匹配。指定枚举显示模式保证了这点。

更多资料:

DXGI Overview:
https://docs.microsoft.com/zh-cn/windows/win32/direct3ddxgi/d3d10-graphics-programming-guide-dxgi?redirectedfrom=MSDN

DirectX Graphics Infrastructure:

Best Improvements:
http://msdn.microsoft.com/en-us/library/windows/desktop/ee417025(v=vs.85).aspx

DXGI 1.4 Improvements:
https://msdn.microsoft.com/en-us/library/windows/desktop/mt427784%28v=vs.85%29.aspx](https://msdn.microsoft.com/en-us/library/windows/desktop/mt427784(v=vs.85).aspx)

4.1.11检查功能支持

之前使用ID3D12Device::CheckFeatureSupport方法来检查当前图形驱动程序的多采样支持。但这只是可以用这个函数检查的其中一个特性支持,该方法的原型如下:

1
2
3
4
HRESULT ID3D12Device::CheckFeatureSupport(
D3D12_FEATURE Feature,
void *pFeatureSupportData,
UINT FeatureSupportDataSize);

译文:

Feature: D3D12_FEATURE枚举类型的一个成员,标识我们想要检查的特性的类型:

  • D3D12_FEATURE_D3D12_OPTIONS:检查当前图形驱动对各种Direct3D 12特性的支持情况。
  • D3D12_FEATURE_ARCHITECTURE:检查图形适配器中GPU的硬件体系架构特性。
  • D3D12_FEATURE_FEATURE_LEVELS:检查功能级别的支持情况。
  • 4D3D12_FEATURE_FORMAT_SUPPORT:检查对于给定纹理格式的支持情况(例如,该格式是否可以用作渲染目标,该格式是否可以与混合一起使用)。
  • D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS:检查对多重采样的支持情况。

pFeatureSupportData:指向某种数据结构的指针,存储了检测到的特定功能支持的信息。具体类型取决于Feature参数,参数与对应的具体数据结构如下:

  • D3D12_FEATURE_D3D12_OPTIONS—>D3D12_FEATURE_DATA_D3D12_OPTIONS
  • D3D12_FEATURE_ARCHITECTURE—>D3D12_FEATURE_DATA_ARCHITECTURE
  • D3D12_FEATURE_FEATURE_LEVELS—>D3D12_FEATURE_DATA_FEATURE_LEVELS
  • D3D12_FEATURE_FORMAT_SUPPORT—>D3D12_FEATURE_DATA_FORMAT_SUPPORT
  • D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS—>D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS

FeatureSupportDataSize:传回pFeatureSupportData参数中的数据结构的大小。

ID3D12Device::CheckFeatureSupport函数检查对许多特性的支持,包括一些高级特性。

有关每种功能结构的数据成员的详细信息,请参阅SDK文档。下面有一个例子展示了如何检查支持的特性级别(§4.1.9):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct D3D12_FEATURE_DATA_FEATURE_LEVELS
{
UINT NumFeatureLevels;
const D3D_FEATURE_LEVEL* pFeatureLevelsRequested;
D3D_FEATURE_LEVEL MaxSupportedFeatureLevel;
} D3D12_FEATURE_DATA_FEATURE_LEVELS;

D3D_FEATURE_LEVEL featureLevels[3] =
{
D3D_FEATURE_LEVEL_11_0, // 首先检查是否支持11
D3D_FEATURE_LEVEL_10_0, // 再检查是否支持10
D3D_FEATURE_LEVEL_9_3 //最后检查9.3
};
D3D12_FEATURE_DATA_FEATURE_LEVELS featureLevelsInfo;
featureLevelsInfo.NumFeatureLevels = 3;
featureLevelsInfo.pFeatureLevelsRequested = featureLevels;
md3dDevice->CheckFeatureSupport(D3D12_FEATURE_FEATURE_LEVELS, &featureLevelsInfo,sizeof(featureLevelsInfo));

需要注意:CheckFeatureSupport方法的第二个参数兼具输入与输出的属性。

作为输入的时候,需要先指定功能级别数组NumFeatureLevels中元素的个数,再将pFeatureSupportData指针指向功能级别数组,最后该函数把MaxSupportedFeatureLevel返回当前硬件可支持的最高功能级别。

4.1.12 Residency

资源驻留

一个复杂的游戏会使用大量的资源,比如纹理和3D网格,但是GPU并不是一直需要这些资源。

在Direct3D 12中,应用程序通过控制资源在显存中的去留,主动管理资源的驻留情况。基本的想法是最小化应用程序使用的GPU内存,因为可能没有足够的内存来存储整个游戏的所有资源,或者用户运行的其他应用程序需要GPU内存。

需要注意,考虑到性能,应用程序应该避免在很短的时间内交换相同的GPU内存中的资源,因为这样做有开销。典型的场景是游戏关卡或者区域的切换。

默认情况下,当一个资源被创建时会驻留在显存中,当资源被销毁时就会GPU中清除。

当然,有手动控制资源常驻的方法:

1
2
HRESULT ID3D12Device::MakeResident(UINT NumObjects, ID3D12Pageable* const* ppObjects);
HRESULT ID3D12Device::Evict(UINT NumObjects, ID3D12Pageable* const* ppObjects);

对于这两种方法,第二个参数是ID3D12Pageable资源的数组,第一个参数是数组中的资源数量。

神秘代码:mt186622


UE4:UE_LOG的使用
宏相关参数
1
UE_LOG(LogTemp, Log, TEXT("I am a log"));

第一个参数是Log的分类Category,可以通过宏进行自定义,不需要的话直接LogTemp即可

第二个参数有三种,Log打出来是灰色,Warning是黄色,Error是红色,可以用来区分不同的消息显示

第三个参数类似于C风格的printf,双引号中是字符串,或者是%d,%s,%f等内容,后续跟第四个参数,比如:

1
2
3
4
int a = 10;
UE_LOG(LogTemp, Log, TEXT(" a = %d"), a);
FString b = "i am log";
UE_LOG(LogTemp, Log, TEXT("%s"), *b);
Category自定义方法

简单的自定义宏的方法:

1
DEFINE_LOG_CATEGORY_STATIC(LogMyCategory, Warning, All);

将这个宏放在要是用这个宏的头文件里即可。

或者可以使用如下方法:

在头文件里:

1
DECLARE_LOG_CATEGORY_EXTERN(YourLog, Log, All);

在对应的源文件中:

1
DEFINE_LOG_CATEGORY(YourLog);
命令行显示
1
2
3
4
[cat] = a category for the command to operate on, or 'global' for all categories.
标签,没有设置就显示所有的Log
[level] = verbosity level, one of: none, error, warning, display, log, verbose, all, default
关卡,显示某某关卡的Log
在当前游戏窗口上打印日志
1
GEngine->AddOnScreenDebugMessage(0, 1.0f, FColor::Red, TEXT("OK"));

第一个参数是为了防止同一个消息被多次添加的key

第二个参数是消息显示的时间,单位是秒

第三个参数是消息的颜色,只接受FColor类型的参数

第四个参数是要显示的消息,使用方法与UE_LOG中相同

还可以有第五个参数,bool值,true的时候最新的显示在最上面,false最新的显示在最下面,可以缺省

第六个参数也可以缺省,是该消息的面积大小,参数类型是FVector2D


UE4:常用FString类型转换方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
FString string = "string";
FName fname = FName(*string);// FString 转 FName
TCHAR tchar = *string;//FString 转 TChar*
FText ftext = FText::FromString(string);//FString 转 FText
std::string cstring(TCHAR_TO_UTF8(*string));//FString 转 C++ string
FString numberString = "1234.123";
int32 strint = FCString::Atoi(*numberString);//FString 转 int32
float strfloat = FCString::Atof(*numberString);//FString 转 float
bool strBool = string.ToBool();//转bool

//FString 转 TArray<uint8>
TArray<uint8> uint8Array;
uint8Array.SetNum(string.len());
memcpy(uint8Array.GetData(), TCHAR_TO_ANSI(*string), string.Len());

//TArray<uint8> 转 FString
TArray<uint8> content;
const std::string cstr(reinterpret_cast<const char*>(content.GetData()), content.Num());
FString frameAsFString = cstr.c_str();

//C++ string 转 FString
std::string cstring = "cstring";
FString fromCstring(cstring.c_str());

FString fromFText = ftext.ToString();//FText 转 FString
FStirng fromFName = fname.ToString();//FName 转 FString
Fstring fromfloat = FString::SanitizeFloat(strfloat);//float 转
FString fromint = FString::FromInt(IntVariable);//int
FString frombool = InBool ? TEXT("true") : TEXT("false");//bool
FString fromFVector = VectorVariable.ToString();//Vector
FString fromFVector2D = Vector2DVariable.ToString();//Vector2D
FString fromFRotator = RotatorVariable.ToString();//FRotator
FString fromFLinerColor = LinearColorVariable.ToString();//FLinerColor
FString fromUObject = (InObj != NULL) ? InObj->GetName() : FString(TEXT("None"));//UObject

代码格式测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "SoundDeviceBase.generated.h"

UCLASS()
class OCTOPUS_API ASoundDeviceBase : public APawn
{
GENERATED_BODY()

public:
// Sets default values for this pawn's properties
ASoundDeviceBase();

UPROPERTY(EditAnywhere)
int32 TestUproperty;

protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;

public:
// Called every frame
virtual void Tick(float DeltaTime) override;

// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

};


UE4:反射系统获取UPROPERTY

读取方法

读取UPROPERTY标记的UObject类类型的属性

UE4提供一个迭代器进行字段遍历

TFieldIterator<T>

for (TFieldIterator<UProperty> i ( GetClass() ); i; ++i)

{

// 进行相关操作

UProperty* prop=*i; //i指向一个UProperty

}

获得了UProperty,如果需要进一步访问该属性的Name和Value

Name = prop.GetName();

UProperty* Property  FString PropertyName  *Propertylterator;  Property- >GetName ( )

获取属性的值则需要ContainerPtrToValuePtr

该函数有很多重载

https://docs.unrealengine.com/en-US/API/Runtime/CoreUObject/UObject/FProperty/ContainerPtrToValuePtr/index.html

如果是要获取一个UObject类类型的一个Uproperty的值,则

假设该对象为Object,该Uproperty为Property,对应类型为float

float* sourceAddr = Proerty->ContainerPtrToValuePtr<float>(Object);

就可以得到对应的value

也可以考虑封装成模板

template <typename ValueType>  EVa1ueType GetPropertyVa1ue(UProperty* Property, void* Object)  ValueType* SourceAddr = Property- ;  if (SourceAddr)  return *SourceAddr;  return *SourceAddr;

类型判断

如果不清楚UProperty对应的类型,可以使用Cast进行判断

template <typename To, typename From>  3FORCEINLINE To* src)  return TCastImp1<From, To>: : DoCast(Src) ;

比如

img

如果该Property是bool类型,那么返回值是true

在使用时需要注意想要获取的目标Property的指针非空,不然会直接触发异常中断,UE4直接崩掉。


UE4:反射系统获取UPROPERTY

读取方法

读取UPROPERTY标记的UObject类类型的属性

UE4提供一个迭代器进行字段遍历

TFieldIterator<T>

for (TFieldIterator<UProperty> i ( GetClass() ); i; ++i)

{

// 进行相关操作

UProperty* prop=*i; //i指向一个UProperty

}

获得了UProperty,如果需要进一步访问该属性的Name和Value

Name = prop.GetName();

UProperty* Property  FString PropertyName  *Propertylterator;  Property- >GetName ( )

获取属性的值则需要ContainerPtrToValuePtr

该函数有很多重载

https://docs.unrealengine.com/en-US/API/Runtime/CoreUObject/UObject/FProperty/ContainerPtrToValuePtr/index.html

如果是要获取一个UObject类类型的一个Uproperty的值,则

假设该对象为Object,该Uproperty为Property,对应类型为float

float* sourceAddr = Proerty->ContainerPtrToValuePtr<float>(Object);

就可以得到对应的value

也可以考虑封装成模板

template <typename ValueType>  EVa1ueType GetPropertyVa1ue(UProperty* Property, void* Object)  ValueType* SourceAddr = Property- ;  if (SourceAddr)  return *SourceAddr;  return *SourceAddr;

类型判断

如果不清楚UProperty对应的类型,可以使用Cast进行判断

template <typename To, typename From>  3FORCEINLINE To* src)  return TCastImp1<From, To>: : DoCast(Src) ;

比如

img

如果该Property是bool类型,那么返回值是true

在使用时需要注意想要获取的目标Property的指针非空,不然会直接触发异常中断,UE4直接崩掉。


Untitled Post - 2

“他们在许多时候,对于小资产阶级出身的知识分子寄予满腔的同情,连他们的缺点也给以同情甚至鼓吹。对于工农兵群众,则缺乏接近,缺乏了解,缺乏研究,缺乏知心朋友,不善于描写他们;倘若描写,也是衣服是劳动人民,面孔却是小资产阶级知识分子。他们在某些方面也爱工农兵,也爱工农兵出身的干部,但有些时候不爱,有些地方不爱,不爱他们的感情,不爱他们的姿态,不爱他们的萌芽状态的文艺(墙报、壁画、民歌、民间故事等)。他们有时也爱这些东西,那是为着猎奇,为着装饰自己的作品,甚至是为着追求其中落后的东西而爱的。有时就公开地鄙弃它们,而偏爱小资产阶级知识分子的乃至资产阶级的东西。这些同志的立足点还是在小资产阶级知识分子方面,或者换句文雅的话说,他们的灵魂深处还是一个小资产阶级知识分子的王国。”

一九四二年五月