NativeRenderPlugin是Unity直接接触原生图形API,组织渲染指令的方法,本文通过UnityC#层、插件层、Unity源码层角度,解析代码编写方式,给读者提供编写参考。
本文代码解析路径Gfx后端基于DirectX12。工程路径:NativeRenderingPlugin。
Plugin编译
工程ReadMe中说过,DX12编译需要打开宏,在PlatformBase.h
中,将#define SUPPORT_D3D12 0
改为1,此时编译会缺少d3dx12.h
,这个很多人本地都有,没有去Github找个也行,直接拷贝到本地目录然后项目添加现有项。
编译后,将PluginSource\build\x64\Debug\RenderingPlugin.dll
及同目录下的pdb文件拷贝到UnityPlugin\Assets\Plugins\x86_64
中覆盖源文件。
用UnityEditor启动UnityPlugin下的Unity项目,默认可能是DX11后端,运行能看到像波浪一样的面片和不断旋转的三角形。
此时如果切换成DX12后端重启编辑器后运行,波浪和三角形不会运动,可以打开UseRenderingPlugin.cs
,CallPluginAtEndOfFrames
方法中,判断如果是DX12或Switch平台就不会设置时间,所以直接把判断注释掉,在DX12平台下也可以有相同的效果了。
调度流程
对于C#脚本来说,大部分方法是[DllImport("RenderingPlugin")],将dll动态链接到程序中,部分平台只能靠静态链接(例如IOS),所以Attribute是[DllImport("__Internal")]。
初始化
插件会在第一次调用DllImport的方法时通过名称寻找Plugin(重新Start会重新找),然后通过调用(PluginLoadFunc)LoadPluginFunction(pluginHandle, "UnityPluginLoad")
,寻找初始化方法UnityPluginLoad
,假如dll中有这个方法,会调用并将Interfaces作为参数传入。
UnityPluginLoad
需要插件编写者自己编写,在这个示例中,首先获取图形接口IUnityGraphics
:
static IUnityInterfaces* s_UnityInterfaces = NULL;
static IUnityGraphics* s_Graphics = NULL;
extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces)
{
s_UnityInterfaces = unityInterfaces;
s_Graphics = s_UnityInterfaces->Get<IUnityGraphics>();
s_Graphics->RegisterDeviceEventCallback(OnGraphicsDeviceEvent);
OnGraphicsDeviceEvent(kUnityGfxDeviceEventInitialize);
}
这里调用RegisterDeviceEventCallback
向IUnityGraphics
注册了一个回调,这个回调的作用是接收DeviceEvent,执行特定逻辑,Event包括:
typedef enum UnityGfxDeviceEventType
{
kUnityGfxDeviceEventInitialize = 0,
kUnityGfxDeviceEventShutdown = 1,
kUnityGfxDeviceEventBeforeReset = 2,
kUnityGfxDeviceEventAfterReset = 3,
} UnityGfxDeviceEventType;
这个示例只用到初始化kUnityGfxDeviceEventInitialize
和销毁kUnityGfxDeviceEventShutdown
。
这里主动调用了一次OnGraphicsDeviceEvent(kUnityGfxDeviceEventInitialize)
,此外,Unity底层GfxDevice的实现类在构造方法会调用回调方法并传入Event kUnityGfxDeviceEventInitialize
,相应的,析构方法调用回调并传入Event kUnityGfxDeviceEventShutdown
。
回调中,初始化事件创建抽象渲染接口,销毁事件delete掉接口对象,然后将事件分发给底层不同的渲染接口执行逻辑:
static void UNITY_INTERFACE_API OnGraphicsDeviceEvent(UnityGfxDeviceEventType eventType)
{
// Create graphics API implementation upon initialization
if (eventType == kUnityGfxDeviceEventInitialize)
{
assert(s_CurrentAPI == NULL);
s_DeviceType = s_Graphics->GetRenderer();
s_CurrentAPI = CreateRenderAPI(s_DeviceType);
}
// Let the implementation process the device related events
if (s_CurrentAPI)
{
s_CurrentAPI->ProcessDeviceEvent(eventType, s_UnityInterfaces);
}
// Cleanup graphics API implementation upon shutdown
if (eventType == kUnityGfxDeviceEventShutdown)
{
delete s_CurrentAPI;
s_CurrentAPI = NULL;
s_DeviceType = kUnityGfxRendererNull;
}
}
这里直接看RenderAPI_D3D12::ProcessDeviceEvent
:
void RenderAPI_D3D12::ProcessDeviceEvent(UnityGfxDeviceEventType type, IUnityInterfaces* interfaces)
{
switch (type)
{
case kUnityGfxDeviceEventInitialize:
s_d3d12 = interfaces->Get<IUnityGraphicsD3D12v7>();
UnityD3D12PluginEventConfig config_1;
config_1.graphicsQueueAccess = kUnityD3D12GraphicsQueueAccess_DontCare;
config_1.flags = kUnityD3D12EventConfigFlag_SyncWorkerThreads | kUnityD3D12EventConfigFlag_ModifiesCommandBuffersState | kUnityD3D12EventConfigFlag_EnsurePreviousFrameSubmission;
config_1.ensureActiveRenderTextureIsBound = true;
s_d3d12->ConfigureEvent(1, &config_1);
UnityD3D12PluginEventConfig config_2;
config_2.graphicsQueueAccess = kUnityD3D12GraphicsQueueAccess_Allow;
config_2.flags = kUnityD3D12EventConfigFlag_SyncWorkerThreads | kUnityD3D12EventConfigFlag_ModifiesCommandBuffersState | kUnityD3D12EventConfigFlag_EnsurePreviousFrameSubmission;
config_2.ensureActiveRenderTextureIsBound = false;
s_d3d12->ConfigureEvent(2, &config_2);
initialize_and_create_resources();
break;
case kUnityGfxDeviceEventShutdown:
release_resources();
break;
}
}
initialize_and_create_resources
和release_resources
都很简单,是申请和释放当前插件所需的资源,例如顶点索引缓冲区、PSO、RTV、RootSignature等等。
这里说下s_d3d12->ConfigureEvent
的作用,s_d3d12是IUnityGraphicsD3D12v7,作用类似D3D12的接口,例如ID3D12Device7,作用是用于兼容接口,高版本拥有低版本所有的方法,这些方法能获取当前API的一些能力,例如IUnityGraphicsD3D12v7包含如下方法:
UNITY_DECLARE_INTERFACE(IUnityGraphicsD3D12v7)
{
ID3D12Device* (UNITY_INTERFACE_API * GetDevice)();
IDXGISwapChain* (UNITY_INTERFACE_API * GetSwapChain)();
UINT32(UNITY_INTERFACE_API * GetSyncInterval)();
UINT(UNITY_INTERFACE_API * GetPresentFlags)();
ID3D12Fence* (UNITY_INTERFACE_API * GetFrameFence)();
// Returns the value set on the frame fence once the current frame completes or the GPU is flushed
UINT64(UNITY_INTERFACE_API * GetNextFrameFenceValue)();
// Executes a given command list on a worker thread. The command list type must be D3D12_COMMAND_LIST_TYPE_DIRECT.
// [Optional] Declares expected and post-execution resource states.
// Returns the fence value. The value will be set once the current frame completes or the GPU is flushed.
UINT64(UNITY_INTERFACE_API * ExecuteCommandList)(ID3D12GraphicsCommandList * commandList, int stateCount, UnityGraphicsD3D12ResourceState * states);
void(UNITY_INTERFACE_API * SetPhysicalVideoMemoryControlValues)(const UnityGraphicsD3D12PhysicalVideoMemoryControlValues * memInfo);
ID3D12CommandQueue* (UNITY_INTERFACE_API * GetCommandQueue)();
ID3D12Resource* (UNITY_INTERFACE_API * TextureFromRenderBuffer)(UnityRenderBuffer rb);
ID3D12Resource* (UNITY_INTERFACE_API * TextureFromNativeTexture)(UnityTextureID texture);
// Change the precondition for a specific user-defined event
// Should be called during initialization
void(UNITY_INTERFACE_API * ConfigureEvent)(int eventID, const UnityD3D12PluginEventConfig * pluginEventConfig);
bool(UNITY_INTERFACE_API * CommandRecordingState)(UnityGraphicsD3D12RecordingState * outCommandRecordingState);
};
上述接口中包含获取Device、Swapchain、垂直同步、PresentFlag、执行CommandList,获取CommandQueue等方法。
而ConfigureEvent
用于指定事件对当前图形管线的影响,这对不同图形API是不同的,例如对Unity DX12封装来说,kUnityD3D12GraphicsQueueAccess
可以标识当前事件是否需要访问CommandQueue,如果设置为DontCare
,代表不会直接接触CommandQueue,CommandList交给Unity代为上传,这样可以在Gfx工作线程直接提交CmdList;另一个选择是Allow
,对于插件callback来说,会直接获取底层CommandQueue提交CmdList,根据Unity选项的不同,如果有NativeJob,则启动另一个线程执行,否则也是在工作线程现场执行。
此外,其他flag也各有作用,kUnityD3D12EventConfigFlag_SyncWorkerThreads
表示,执行当前Event的callback前,是否需要同步TaskExecutor
所在的线程;
kUnityD3D12EventConfigFlag_ModifiesCommandBuffersState
标识用于声明,当前Event表示的callback,可能会改变CommandList/CommandBuffer的状态,需要让Unity内部无效化对管线状态的记录,防止后续添加命令时,误以为管线状态没变化,导致没有覆盖新状态;
kUnityD3D12EventConfigFlag_EnsurePreviousFrameSubmission
为是否同步上一帧的present;
ensureActiveRenderTextureIsBound
表示是否在执行callback前,自动绑定设置好当前active的RT。
通过上述调用,我们注册了两种event类型以供后续调度。
渲染组织
C#脚本在Start开启了一个协程,在每帧结束时调用:
GL.IssuePluginEvent(GetRenderEventFunc(), 1);
if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Direct3D12)
{
GL.IssuePluginEvent(GetRenderEventFunc(), 2);
}
这里GetRenderEventFunc()返回渲染函数OnRenderEvent
的指针,作为执行函数的callback,第二个传给IssuePluginEvent
的参数就是上文的eventId,看看这个函数的实现:
static void UNITY_INTERFACE_API OnRenderEvent(int eventID)
{
// Unknown / unsupported graphics device type? Do nothing
if (s_CurrentAPI == NULL)
return;
if (eventID == 1)
{
drawToRenderTexture();
DrawColoredTriangle();
ModifyTexturePixels();
ModifyVertexBuffer();
}
if (eventID == 2)
{
drawToPluginTexture();
}
}
drawToRenderTexture
和drawToPluginTexture
大致逻辑相同,甚至可以说干的是同一件事,区别在于前者的RT是在Unity中申请的,后者是在插件中申请的,于是调度上就有不同eventId对应的小差别。
例如Unity的流程,Fence是由Unity内部返回的:
UINT64 RenderAPI_D3D12::submit_cmd_to_unity_worker(ID3D12GraphicsCommandList* cmd, UnityGraphicsD3D12ResourceState* resource_states, int state_count)
{
return s_d3d12->ExecuteCommandList(cmd, state_count, resource_states);
}
void RenderAPI_D3D12::wait_for_unity_frame_fence(UINT64 fence_value)
{
ID3D12Fence* unity_fence = s_d3d12->GetFrameFence();
UINT64 current_fence_value = unity_fence->GetCompletedValue();
if (current_fence_value < fence_value)
{
handle_hr(unity_fence->SetEventOnCompletion(fence_value, m_fence_event), "Failed to set fence event on completion\n");
WaitForSingleObject(m_fence_event, INFINITE);
}
}
void RenderAPI_D3D12::drawToRenderTexture()
{
wait_for_unity_frame_fence(m_render_texture_draw_fence);
// ...
m_render_texture_draw_fence = submit_cmd_to_unity_worker(m_render_texture_cmd_list, &resource_states, 1);
}
而偏向插件的流程,Fence是自己维护的:
void RenderAPI_D3D12::wait_on_fence(UINT64 fence_value, ID3D12Fence* fence, HANDLE fence_event)
{
UINT64 current_fence_value = fence->GetCompletedValue();
if (current_fence_value < fence_value)
{
handle_hr(fence->SetEventOnCompletion(fence_value, fence_event));
WaitForSingleObject(fence_event, INFINITE);
}
}
void RenderAPI_D3D12::drawToPluginTexture()
{
wait_on_fence(m_plugin_texture_fence_value, m_plugin_texture_fence, m_plugin_texture_fence_event);
// ...
ID3D12CommandQueue* unity_command_queue = s_d3d12->GetCommandQueue();
unity_command_queue->ExecuteCommandLists(1, (ID3D12CommandList**)&m_plugin_texture_cmd_list);
unity_command_queue->Signal(m_plugin_texture_fence, ++m_plugin_texture_fence_value);
}
网友评论