Directx12 CreateExternalTexture

Hi Unity developers and advanced users!

I’m working on a plugin which uses native rendering and needs to create a texture in C++ code, which then used inside Unity.

For DX11 i’m using ID3D11ShaderResourceView as a parameter for Texture2D.CreateExternalTexture(), however there is no documentation on what should be passed in DX12 case:

Here is my current C++ code:

void* getTextureHandleForUnityDX12(ID3D12Resource* src_tex)
{
      D3D12_RESOURCE_DESC src_tex_desc;
      src_tex_desc = src_tex->GetDesc();

      // Once the texture is created, we must create a shader resource view of it
      // so that shaders may use it.  In general, the view description will match
      // the texture description.
      D3D12_SHADER_RESOURCE_VIEW_DESC textureViewDesc;
      ZeroMemory(&textureViewDesc, sizeof(textureViewDesc));
      textureViewDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
      textureViewDesc.Format = src_tex_desc.Format;
      textureViewDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
      textureViewDesc.Texture2D.MipLevels = src_tex_desc.MipLevels;
      textureViewDesc.Texture2D.MostDetailedMip = 0;

      // create shader resource view and constant buffer view descriptor heap
      D3D12_DESCRIPTOR_HEAP_DESC descHeapCbvSrv = {};
      descHeapCbvSrv.NumDescriptors = 1;
      descHeapCbvSrv.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
      descHeapCbvSrv.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;

      auto hr = m_device->CreateDescriptorHeap(
        &descHeapCbvSrv, __uuidof(ID3D12DescriptorHeap), (void**)&m_descriptorHeap);
      if (FAILED(hr)) { return TranslateReturnCode(hr); }

      // https://software.intel.com/en-us/articles/introduction-to-resource-binding-in-microsoft-directx-12
      CD3DX12_CPU_DESCRIPTOR_HANDLE srvHandle0(m_descriptorHeap->GetCPUDescriptorHandleForHeapStart());
      m_device->CreateShaderResourceView(src_tex, &textureViewDesc, srvHandle0);

      // Doesn't work: the texture in Unity remains black
      return (void*)&srvHandle0;
}

Maybe you could add an example of that to https://bitbucket.org/Unity-Technologies/graphicsdemos as well, I think some people will find it quite useful.

Thanks,
Lev

In the native rendering plugin example it’s not immediately obvious what type to use but if you dig into the implementation of RenderAPI_D3D12 you’ll see the following bit of code in EndModifyTexture:

void RenderAPI_D3D12::EndModifyTexture(void* textureHandle, int textureWidth, int textureHeight, int rowPitch, void* dataPtr)
{
	ID3D12Device* device = s_D3D12->GetDevice();

	const UINT64 kDataSize = textureWidth * textureHeight * 4;
	ID3D12Resource* upload = GetUploadResource(kDataSize);
	upload->Unmap(0, NULL);

	ID3D12Resource* resource = (ID3D12Resource*)textureHandle;
	D3D12_RESOURCE_DESC desc = resource->GetDesc();
	assert(desc.Width == textureWidth);
	assert(desc.Height == textureHeight);

You can see textureHandle is cast to ID3D12Resource. That’s what Unity will return if you call GetNativePtr on a Texture2D in DirectX12, so that’s what you should use to create a Unity texture from scratch.

Hey. I tried passing ID3D12Resource* to Texture2D.CreateExternalTexture(), but it didn’t work at all.

I will post my createTexture2D() method code:

Result createTexture2D(void **dst_tex, int width, int height, TextureFormat format, ResourceFlags flags)
  {
      D3D12_HEAP_PROPERTIES heap  = {};
      heap.Type                   = D3D12_HEAP_TYPE_DEFAULT;
      heap.CPUPageProperty        = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
      heap.MemoryPoolPreference   = D3D12_MEMORY_POOL_UNKNOWN;
      heap.CreationNodeMask       = 1;
      heap.VisibleNodeMask        = 1;

      D3D12_RESOURCE_DESC desc = {};
      desc.Dimension          = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
      desc.Alignment          = 0;
      desc.Width              = (UINT64)width;
      desc.Height             = (UINT)height;
      desc.DepthOrArraySize   = 1;
      desc.MipLevels          = 1;
      desc.Format             = GetDXGIFormat(format);
      desc.SampleDesc.Count   = 1;
      desc.SampleDesc.Quality = 0;
      desc.Layout             = D3D12_TEXTURE_LAYOUT_UNKNOWN;
      desc.Flags              = D3D12_RESOURCE_FLAG_NONE;

      // texture can't be created with D3D12_HEAP_TYPE_UPLOAD / D3D12_HEAP_TYPE_READBACK heap type.
      // ResourceFlags::CPU_Write / CPU_Read flag is ignored.

      ID3D12Resource *tex = nullptr;
      auto hr = m_device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&tex) );
      if ( FAILED( hr ) ) { return TranslateReturnCode(hr); }
      *dst_tex = tex;

      return Result::OK;
  }

Along with writeTexture2D() code:

Result writeTexture2D(void *dst_tex_, int width, int height, TextureFormat format, const void *src, size_t write_size)
  {
      if (write_size == 0) { return Result::OK; }
      if (!dst_tex_ || !src) { return Result::InvalidParameter; }

      auto *dst_tex = (ID3D12Resource*)dst_tex_;

      D3D12_RESOURCE_DESC dst_desc = dst_tex->GetDesc();
      D3D12_PLACED_SUBRESOURCE_FOOTPRINT dst_layout;
      UINT dst_num_rows;
      UINT64 dst_row_size;
      UINT64 dst_required_size;
      m_device->GetCopyableFootprints(&dst_desc, 0, 1, 0, &dst_layout, &dst_num_rows, &dst_row_size, &dst_required_size);


      auto write_proc = [](ID3D12Resource *dst_tex, int width, int height, TextureFormat format, const void *src, size_t write_size, D3D12_PLACED_SUBRESOURCE_FOOTPRINT& dst_layout) {
          void *mapped_data = nullptr;
          auto hr = dst_tex->Map(0, nullptr, &mapped_data);
          if (FAILED(hr)) { return hr; }

          int dst_pitch = dst_layout.Footprint.RowPitch;
          int src_pitch = width * GetTexelSize(format);

          int num_rows = std::min<int>(std::min<int>(height, dst_layout.Footprint.Height),
                                       ceildiv<size_t>(write_size, src_pitch));
		
          CopyRegion(mapped_data, dst_pitch, src, src_pitch, num_rows);
          dst_tex->Unmap(0, nullptr);
          return S_OK;
      };


      // try direct access
      auto hr = write_proc(dst_tex, width, height, format, src, write_size, dst_layout);
      if (SUCCEEDED(hr)) { return Result::OK; }


      // try copy-via-staging
      auto staging = createStagingBuffer(dst_required_size, StagingFlag::Upload);
      if (!staging) { return Result::OutOfMemory; }

      hr = write_proc(staging.Get(), width, height, format, src, write_size, dst_layout);
      if (FAILED(hr)) { return TranslateReturnCode(hr); }

      hr = executeCommands([&](ID3D12GraphicsCommandList *clist) {
          CD3DX12_TEXTURE_COPY_LOCATION dst_region(dst_tex, 0);
          CD3DX12_TEXTURE_COPY_LOCATION src_region(staging.Get(), dst_layout);

          clist->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(dst_tex, D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_DEST));
          clist->CopyTextureRegion(&dst_region, 0, 0, 0, &src_region, nullptr);
          clist->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(dst_tex, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_COMMON));
      });
      if (FAILED(hr)) { return TranslateReturnCode(hr); }

      return Result::OK;
  }

Maybe someone can point me to possible problems with this code.

One good thing is that now I can see that the writeTexture2D() code is rather a DX11 copy-paste with some changes applied to make it compile with DX12 API being used, then a DX12-oriented solution.
Should I try something with GetUploadResource() like GraphicsDemos does ?