美文网首页
【Unity3D】NativeRenderPlugin编写方法

【Unity3D】NativeRenderPlugin编写方法

作者: crossous | 来源:发表于2024-06-10 20:58 被阅读0次

  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.csCallPluginAtEndOfFrames方法中,判断如果是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);
}

  这里调用RegisterDeviceEventCallbackIUnityGraphics注册了一个回调,这个回调的作用是接收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_resourcesrelease_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();
    }
}

  drawToRenderTexturedrawToPluginTexture大致逻辑相同,甚至可以说干的是同一件事,区别在于前者的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);
}

相关文章

  • 无标题文章

    刚刚发现了一个UNITY3D中物体移动比较齐全的方法,借鉴的,希望对大家都有所帮 unity3d中控制物体移动方法...

  • Android 与 unity3d 基于微信授权、支付、分享,Q

    前文: unity3d项目需要调用到android原生的方法,而unity3d不提供这些类型的api,所以就需要a...

  • android与Unity3D之间的相爱相杀

    正题:android打jar包到Unity3D的方法及遇到的问题 最近公司在做unity3d游戏方面的业务,从而就...

  • Unity3d常用两种加载资源方案:Resources.Load

    初步整理并且学习unity3d资源加载方法,预计用时两天完成入门学习Unity3d常用两种加载资源方案:Resou...

  • 提取王者荣耀的模型和资源

    王者荣耀使用的是【Unity3D 5.X】开发,可以使用Unity3D手游通用的提取方法提取。本文以安卓为例,IO...

  • 引言

    Unity3D的学习已有半年多了, 在中途也曾在其他网站博客上写过文章, 都是极少的编写, 最近感觉很多的学...

  • 关于Invoke

    Invoke() 方法是 Unity3D 的一种委托机制如: Invoke("SendMsg", 5); 它的...

  • Unity -invoke

    Invoke() 方法是 Unity3D的一种委托机制如: Invoke("SendMsg", 5); 它的意...

  • 编写代码方法

    在类的头文件中尽量少引用其他头文件 多用字面量,少用与之等价的方法 多用类型常量,少用#define预处理指令 并...

  • linux学习-week14--综合架构批量管理服务/网站web

    综合架构知识概述说明剧本编写扩展说明剧本整合功能说明方法一: 编写整合剧本信息方法二: 编写剧本角色信息 ???网...

网友评论

      本文标题:【Unity3D】NativeRenderPlugin编写方法

      本文链接:https://www.haomeiwen.com/subject/fmoiqjtx.html