Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use DXGI #1

Open
4 of 5 tasks
watfordjc opened this issue Sep 11, 2020 · 2 comments
Open
4 of 5 tasks

Use DXGI #1

watfordjc opened this issue Sep 11, 2020 · 2 comments
Assignees
Labels
enhancement New feature or request
Milestone

Comments

@watfordjc
Copy link
Owner

watfordjc commented Sep 11, 2020

Feature Branch

Current feature branch for this issue: feature/issue-1/d2d-1.

Progress

  • Replace WIC with off-screen DXGI for rendering sans profile and Twitter/Retweet images.
  • Use WIC to save "screenshot" to PNG.
  • Add back WIC rendering of profile and Twitter/Retweet images to Direct2D device (render target is now a DXGI render surface target).
  • Make DXGI output shareable with OBS.
  • Figure out how to make DXGI output directly shareable (i.e. remove the resource copying step).

Background

The current version of the library uses Direct2D and WIC.

In order to interoperate with OBS efficiently, it seems like DXGI is the best option.

Based on Microsoft documentation and a bit of Googling, it looks like I need to insert or replace the functions called up to (and including) CreateRenderTarget(), possibly replacing CreateWICBitmap() with something that creates a DXGI Surface.

@watfordjc watfordjc added the enhancement New feature or request label Sep 11, 2020
@watfordjc watfordjc self-assigned this Sep 11, 2020
@watfordjc
Copy link
Owner Author

watfordjc commented Sep 11, 2020

Function Call Order

It is probably going to be useful to list the current and new order of operations including the prerequisites.

Current Order

  • Included in New Order
    • CreateD2D1Factory (creates ID2D1Factory)
  • Not Initially Included in New Order
    • CreateImagingFactory (creates IWICImagingFactory)
    • CreateDWriteFactory (creates IDWriteFactory7)
    • CreateWICBitmap (creates IWICBitmap; requires IWICImagingFactory)
    • CreateRenderTarget (creates ID2D1RenderTarget; requires ID2D1Factory, IWICBitmap)
    • BeginDraw (requires ID2D1RenderTarget)
    • Geometry drawing functions (requires ID2D1RenderTarget)
    • Layer functions (requires ID2D1RenderTarget, ID2D1RenderTarget as ID2D1DeviceContext, ID2D1Factory)
    • DrawImageFromFilename (requires ID2D1RenderTarget as ID2D1DeviceContext, IWICImagingFactory)
    • CreateTextLayoutFromString (requires ID2D1RenderTarget as ID2D1DeviceContext4, IDWriteFactory7)
    • DrawTextLayout (requires ID2D1DeviceContext as ID2D1DeviceContext4)
    • EndDraw (requires ID2D1RenderTarget)
    • SaveImage (creates .PNG; requires IWICImagingFactory, ID2D1RenderTarget, IWICBitmap)

New Order

  • CreateD2D1Factory (creates ID2D1Factory)
  • CreateD3D11Device (creates ID3D11Device and D3D_FEATURE_LEVEL, ID3D11DeviceContext)
  • CreateDXGIDevice (creates IDXGIDevice, IDXGIFactory2, ID2D1Device, ID2D1DeviceContext; requires ID3D11Device, ID2D1Factory)
  • CreateImagingFactory (creates IWICImagingFactory)
  • CreateDWriteFactory (creates IDWriteFactory7)
  • CreateWICBitmap (creates IWICBitmap; requires IWICImagingFactory)
  • CreateDXGISwapChain(creates IDXGISwapChain1; requires ID2D1Factory, IDXGIDevice)
  • CreateRenderTarget (creates ID2D1RenderTarget; requires ID2D1Factory, IWICBitmap)
  • CreateRenderTarget (creates IDXGISurface2, ID2D1Bitmap1, ID2D1RenderTarget; requires IDXGISwapChain1, ID2D1DeviceContext)
  • BeginDraw (requires ID2D1RenderTarget)
  • Geometry drawing functions (requires ID2D1RenderTarget)
  • Layer functions (requires ID2D1RenderTarget, ID2D1RenderTarget as ID2D1DeviceContext, ID2D1Factory)
  • DrawImageFromFilename (requires ID2D1RenderTarget as ID2D1DeviceContext, IWICImagingFactory)
  • CreateTextLayoutFromString (requires ID2D1RenderTarget as ID2D1DeviceContext4, IDWriteFactory7)
  • DrawTextLayout (requires ID2D1DeviceContext as ID2D1DeviceContext4)
  • EndDraw (requires ID2D1RenderTarget)
  • SaveImage (creates .PNG; requires IWICImagingFactory, ID2D1RenderTarget, IWICBitmap)
  • SaveImage (creates .PNG; requires IDXGISwapChain1, ID3D11DeviceContext)

Thoughts

  1. CreateDXGIDevice() creates an ID2D1DeviceContext. Some existing functions are already casting ID2D1RenderTarget to an ID2D1DeviceContext so casting in the opposite direction should be an option.
  2. CreateRenderTarget() creates an ID2D1RenderTarget using an IWICBitmap. There are alternatives to IWICBitmap but several drawing functions I'm using use IWICImagingFactory.
  3. The documentation for D2D1_RENDER_TARGET_PROPERTIES states the following:

    Note that WIC bitmap render targets do not support hardware rendering.

  4. CreateRenderTarget() calls ID2D1Factory->CreateWicBitmapRenderTarget(). Look at other "Create...RenderTarget()" options available.
    1. CreateDCRenderTarget()

      Creates a render target that draws to a Windows Graphics Device Interface (GDI) device context.

    2. CreateDxgiSurfaceRenderTarget()

      Creates a render target that draws to a DirectX Graphics Infrastructure (DXGI) surface.

    3. CreateHwndRenderTarget()

      Creates an ID2D1HwndRenderTarget, a render target that renders to a window.

    4. CreateWicBitmapRenderTarget()

      Creates a render target that renders to a Microsoft Windows Imaging Component (WIC) bitmap.

  5. Given WIC can't be done in GPU, I'm not writing to a window, and a GDI render target likely has the limitations of GDI (no colour emoji), a DXGI Surface render target looks like the only option.
  6. The following functions currently require WIC or something that requires WIC. I have bolded the ones that will obviously need changing if I change render target type.
    • CreateImagingFactory (creates IWICImagingFactory)
    • CreateWICBitmap (creates IWICBitmap; requires IWICImagingFactory)
    • CreateRenderTarget (creates ID2D1RenderTarget; requires ID2D1Factory, IWICBitmap)
    • BeginDraw (requires ID2D1RenderTarget)
    • Geometry drawing functions (requires ID2D1RenderTarget)
    • Layer functions (requires ID2D1RenderTarget, ID2D1RenderTarget as ID2D1DeviceContext, ID2D1Factory)
    • DrawImageFromFilename (requires ID2D1RenderTarget as ID2D1DeviceContext, IWICImagingFactory)
    • CreateTextLayoutFromString (requires ID2D1RenderTarget as ID2D1DeviceContext4, IDWriteFactory7)
    • DrawTextLayout (requires ID2D1DeviceContext as ID2D1DeviceContext4)
    • EndDraw (requires ID2D1RenderTarget)
    • SaveImage (creates .PNG; requires IWICImagingFactory, ID2D1RenderTarget, IWICBitmap)
  7. I need to investigate each function I currently call and see if there is a way to convert from IWICBitmap to IDXGISurface (or whatever the equivalent is).

CreateRenderTarget()

The current method uses the following function:

HRESULT CreateWicBitmapRenderTarget(
  IWICBitmap                          *target,
  const D2D1_RENDER_TARGET_PROPERTIES *renderTargetProperties,
  ID2D1RenderTarget                   **renderTarget
);

Using DXGI, that will be replaced with… IDXGISwapChain1::GetBuffer(), ID2D1DeviceContext::CreateBitmapFromDxgiSurface(), and ID2D1DeviceContext::SetTarget().

I will still get an ID2D1RenderTarget out, but WIC will no longer be involved.

CreateWICBitmap() and CreateDXGISwapChain()

The current method uses the following function from an instance of IWICImagingFactory:

HRESULT CreateBitmap(
  UINT                       uiWidth,
  UINT                       uiHeight,
  REFWICPixelFormatGUID      pixelFormat,
  WICBitmapCreateCacheOption option,
  IWICBitmap                 **ppIBitmap
);

Using DXGI, that will be replaced with… ???

Well the next thing in the documentation example is dxgiFactory->CreateSwapChainForCoreWindow() but, again, I'm not using a window. CreateSwapChainForComposition() is an option, and a bit of Googling later and I'm at Windows with C++ : High-Performance Window Layering Using the Windows Composition Engine (a useful and relevant MSDN Magazine article from 2014 that Microsoft will undoubtedly eventually move or delete with no redirect, as they have a habit of doing).

The fact the article title has at least one non-Windows "Window" does not make the article irrelevant because it actually covers a lot of what I've already done (and why it has been done) and talks about the Windows Composition Engine (the thing CreateSwapChainForComposition() uses) to render things off-screen.

I’m just going to use it to compose a swap chain that supports transparency with premultiplied alpha values on a per-pixel basis and blend it with the rest of the desktop. Traditional DirectX applications typically create a swap chain with the DXGI factory’s CreateSwapChainForHwnd method. This swap chain is backed by a pair or collection of buffers that effectively would be swapped during presentation, allowing the application to render the next frame while the previous frame is copied. The swap chain surface the application renders to is an opaque off-screen buffer. When the application presents the swap chain, DirectX copies the contents from the swap chain’s back buffer to the window’s redirection surface. As I mentioned earlier, the composition engine eventually composes all of the redirection surfaces together to produce the desktop as a whole.

In this case, the window has no redirection surface, so the DXGI factory’s CreateSwapChainForHwnd method can’t be used. However, I still need a swap chain to support Direct3D and Direct2D rendering. That’s what the DXGI factory’s CreateSwapChainForComposition method is for. I can use this method to create a windowless swap chain, along with its buffers, but presenting this swap chain doesn’t copy the bits to the redirection surface (which doesn’t exist), but instead makes it available to the composition engine directly. The composition engine can then take this surface and use it directly and in lieu of the window’s redirection surface. Because this surface isn’t opaque, but rather its pixel format fully supports per-pixel premultiplied alpha values, the result is pixel-perfect alpha blending on the desktop. It’s also incredibly fast because there’s no unnecessary copying within the GPU and certainly no copies over the bus to system memory.

After a bit of coding, CreateDXGIDevice() also creates an IDXGIFactory2*, CreateDXGISwapChain() creates an IDXGISwapChain1*, and CreateRenderTarget() now instead creates an IDXGISurface2* and an ID2D1Bitmap1*. I'm not sure if I'll split that last one out so that there is a CreateDXGISurface() equivalent to the previous CreateWICBitmap().

After some code commenting, the following are showing errors:

  • DrawImageFromFilename() because WICImagingFactory->CreateDecoderFromFilename() and WICImagingFactory->CreateFormatConverter() do not exist.
  • SaveImage() because WICImagingFactory->CreateStream(), WICImagingFactory->CreateEncoder(), and pWICBitmapFrameEncode->WriteSource() do not exist.

Those are the other two functions I said would have to be changed/replaced.

DrawImageFromFilename()

This function needs to be replaced with something that loads a PNG onto a DXGI surface.

After a lot of research and test code, I reverted all of my changes to this function as I couldn't get images to display.

As loading an image into a 2D texture (or any of the other ways I tried to do it) invariably requires using WIC or DDS, and the effect processing I want to use (resizing with high quality bicubic interpolation) appears to be only available in WIC, I'm not sure why I was trying to change something that was working fine in Direct2D into something else whilst trying to maintain the use of Direct2D.

Everything else was still working because it was still using Direct2D. Reverting the code so the last function was back to the following fixed the display of images:

if (SUCCEEDED(hr))
{
	pD2D1DeviceContext->DrawImage(
		pBitmapSourceEffect,
		originPoint,
		bounds,
		D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC,
		D2D1_COMPOSITE_MODE_SOURCE_OVER
	);
}

SaveImage()

This function needs to be replaced with something that saves a DXGI surface to PNG.

The standalone ScreenGrab from DirextXTex will be utilised for this functionality. After adding ScreenGrab11.h and ScreenGrab11.cpp and #include "DirextXTex/ScreenGrab/ScreenGrab11.h", the contents of the SaveImage() function become succinct:

ID3D11Texture2D* backBuffer;
HRESULT hr = pCanvas->Direct2DPointers.DXGISwapChain->GetBuffer(
	0,
	__uuidof(ID3D11Texture2D),
	reinterpret_cast<void**>(&backBuffer)
);
if (SUCCEEDED(hr))
{
	using namespace DirectX;
	hr = SaveWICTextureToFile(
		pCanvas->Direct2DPointers.Direct3DDeviceContext,
		backBuffer,
		GUID_ContainerFormatPng,
		filename,
		&GUID_WICPixelFormatDontCare,
		nullptr,
		true
	);
}
backBuffer->Release();
return hr;

This is probably not complete though due to the following notes in the ScreenGrab wiki page:

The library assumes that the client code will have already called CoInitialize, CoInitializeEx, or Windows::Foundation::Initialize as needed by the application before calling any Windows Imaging Component functionality.

Looking at my code, I don't see any such calls. This StackOverflow answer to Using CoInitializeEx on WinForms threads says I don't need to call it as I am using .NET and the CLR calls it for me: STA required for all UI threads (cannot be changed), MTA required for all thread-pool threads (cannot be changed), MTA default for all programatically created threads.

If no WIC pixel format GUID is provided as the targetFormat parameter, it will default to a non-alpha format since 'screenshots' usually ignore the alpha channel in render targets.

The alpha channel doesn't currently matter as I call Clear() and set the background to black. If at a later date I start using transparency (e.g. Tweet Treatment rounded corners) I will have to take another look. I probably want GUID_WICPixelFormat32bppBGRA - 32 bits per pixel with 4 channels (BGRA).

New Functions or Functionality

  1. I think some locking will need to be involved when changing the image.
  2. The IDXGISwapChain1::Present() makes changes available to the composition engine, although I'm not sure when I need to call that (or even if I need to).
  3. To start with, I just need to migrate the existing create-a-PNG code over to DXGI.

@watfordjc
Copy link
Owner Author

watfordjc commented Sep 13, 2020

Make DXGI Output Shareable with OBS

This final item on the task list is going to tie together csharp-message-to-image-library, csharp-stream-controller, and obs-shm-image-source.

In obs-shm-image-source I think I am going to want to get a handle to the DXGI surface*.

In csharp-stream-controller I am going to be telling obs-shm-image-source to update the image. I think I am also going to want to relay the DXGI surface pointer to obs-shm-image-source.

In obs-shm-image-source I am going to want to load the DXGI surface as a source and tell OBS when the source has changed. I will need a way of getting the handle to the DXGI surface.

* In the above I say DXGI surface. I am assuming that is what I need, although I might need something else, such as the DXGI swap chain. My SaveImage() function creates a ID3D11Texture2D from the swap chain.

Relevant External Source Code

This section includes source code from elsewhere and may be covered by different licences to this repository. Links to documentation and source files are included where possible.

The majority of OBS Studio source code is licensed GPL version 2 or later.

The inclusion of such source code and documentation here is also likely covered by fair use as I am commenting on it (commentary exception) in order to derive meaning (possible education exception), and am trying to derive meaning in order to inter-operate with it (technically the reverse engineering to provide interoperability exception doesn't apply as you don't need to reverse engineer open source code).

OBS Graphics API Function

The following libobs API function in libobs/graphics/graphics.c is likely going to be called:

gs_texture_t *gs_texture_open_shared(uint32_t handle)

Windows only: Creates a texture from a shared texture handle.

Parameters:

  • handle: Shared texture handle

Returns:

  • A texture object

Texture Functions, Core Graphics API, OBS Studio Documentation

gs_texture_t *gs_texture_open_shared(uint32_t handle)
{
	graphics_t *graphics = thread_graphics;
	if (!gs_valid("gs_texture_open_shared"))
		return NULL;

	if (graphics->exports.device_texture_open_shared)
		return graphics->exports.device_texture_open_shared(
			graphics->device, handle);
	return NULL;
}
  • thread_graphics is NULL if the thread has not entered a graphics context.
  • gs_valid() returns false if (!thread_graphics) (not in a graphics context) otherwise it returns true.
  • device_texture_open_shared() is in d3d11-subsystem.cpp, and tries to return new gs_texture_2d(device, handle);.
  • gs_exports is a struct in libobs/graphics/graphics-internal.h. device_texture_open_shared is inside an #elif _WIN32.

Using graphics functions isn’t possible unless the current thread has entered a graphics context, and the graphics context can only be used by one thread at a time. To enter the graphics context, use obs_enter_graphics(), and to leave the graphics context, use obs_leave_graphics().

Certain callback will automatically be within the graphics context: obs_source_info.video_render, and the draw callback parameter of obs_display_add_draw_callback(), and obs_add_main_render_callback().

The Graphics Context, Rendering Graphics, OBS Studio Documentation

OBS Internal Function

The above API function calls the internal function device_texture_open_shared() which tries to call new gs_texture_2d(device, handle).

libobs-d3d11/d3d11-subsystem.hpp contains a definition of the function inside struct gs_texture_2d : gs_texture { }:

gs_texture_2d(gs_device_t *device, uint32_t handle);

As *device is almost certainly some form of reference to the GPU that OBS is running on (unless using that setting for multiple GPUs that can't be used on my laptop), we just need to dig down into that function to work out what handle is being used for to work out what handle is.

After a few searches, libobs-d3d11/d3d11-texture2d.cpp contains the function we're looking for:

gs_texture_2d::gs_texture_2d(gs_device_t *device, uint32_t handle)
	: gs_texture(device, gs_type::gs_texture_2d, GS_TEXTURE_2D),
	  isShared(true),
	  sharedHandle(handle)
{
	HRESULT hr;
	hr = device->device->OpenSharedResource((HANDLE)(uintptr_t)handle,
						__uuidof(ID3D11Texture2D),
						(void **)texture.Assign());
	if (FAILED(hr))
		throw HRError("Failed to open shared 2D texture", hr);

	texture->GetDesc(&td);

	this->type = GS_TEXTURE_2D;
	this->format = ConvertDXGITextureFormat(td.Format);
	this->levels = 1;
	this->device = device;

	this->width = td.Width;
	this->height = td.Height;
	this->dxgiFormat = td.Format;

	memset(&resourceDesc, 0, sizeof(resourceDesc));
	resourceDesc.Format = td.Format;
	resourceDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
	resourceDesc.Texture2D.MipLevels = 1;

	hr = device->device->CreateShaderResourceView(texture, &resourceDesc,
						      shaderRes.Assign());
	if (FAILED(hr))
		throw HRError("Failed to create shader resource view", hr);
}

This is where a difference in style is going to be useful. OBS Studio and libobs use snake_case whereas Direct3D (and a large amount of Microsoft code and APIs) use PascalCase.

	hr = device->device->OpenSharedResource((HANDLE)(uintptr_t)handle,
						__uuidof(ID3D11Texture2D),
						(void **)texture.Assign());

ID3D11Device::OpenSharedResource() (the source file is in the directory libobs-d3d11, so we know it is Direct3D 11) is the function being called, and we can see that device is probably a struct that contains a device parameter of the type ID3D11Device.

Microsoft's Win32 documentation has the syntax for ID3D11Device::OpenSharedResource method:

HRESULT OpenSharedResource(
  HANDLE hResource,
  REFIID ReturnedInterface,
  void   **ppResource
);

The unique handle of the resource is obtained differently depending on the type of device that originally created the resource.
To share a resource between two Direct3D 11 devices the resource must have been created with the D3D11_RESOURCE_MISC_SHARED flag, if it was created using the ID3D11Device interface. If it was created using a DXGI device interface, then the resource is always shared.
The REFIID, or GUID, of the interface to the resource can be obtained by using the __uuidof() macro. For example, __uuidof(ID3D11Buffer) will get the GUID of the interface to a buffer resource.

I am not only using DXGI, DXGI is based on an ID3D11Device. OBS wants a ID3D11Texture2D, which is exactly the same thing being used in my SaveImage() function.

OBS wants a HANDLE to the buffer of the DXGI swap chain. As I am already using DXGI, I don't believe I have to do anything about making it shareable, I just need to get the HANDLE

After several hours of getting E_INVALIDARG when trying to create a shared handle, I decided to rethink things. I ended up creating a new ID3D11Texture2D with a shared keyed mutex. In the save image function I then switched from using the DGXI buffer contents directly and used ID3D11DeviceContext::CopyResource() to copy from the backBuffer to pCanvas->SharedTexture. Image saving is still working properly.

The next task is working out how to get my does-nothing OBS plugin to load a texture from shared memory and display it. Commit watfordjc/obs-shm-image-source@560a3fc combined with commit 6cefd97 added this functionality, but I still think the copying within the GPU is an unnecessary step that will need looking at again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant