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

Expose high-level APIs to create CanvasSwapChain-s for HWNDs #915

Open
Sergio0694 opened this issue Apr 17, 2023 · 5 comments
Open

Expose high-level APIs to create CanvasSwapChain-s for HWNDs #915

Sergio0694 opened this issue Apr 17, 2023 · 5 comments
Labels
approved The proposal was approved in API review, it can be implemented feature

Comments

@Sergio0694
Copy link
Member

Sergio0694 commented Apr 17, 2023

Overview

As part of an effort to make creating standalone Win2D-powered Win32 apps simpler, we should expose some high-level APIs to easily create a CanvasSwapChain object for HWND. This would allow creating a standalone WASDK Win32 app using Win2D to draw to a top level HWND swap chain very easy, without the need for any other dependency or complicated setup.

It's already possible to achieve this (eg. see https://github.com/Sergio0694/Win2DSample), but this requires a good amount of non trivial setup, doing interop to create all the necessary stuff. In particular, it also requires developers to get the activation factory for CanvasDevice to create the CanvasSwapChain wrapper, which especially in .NET isn't really that nice or intuitive.

API proposal

WinRT doesn't allow having size-dependent parameters (ie. HWND), so the proposal is split into two parts:

  • A new interop interface (in the published header) with the high-level functionality
  • A pair of custom APIs for the .NET projection assembly to easily invoke these APIs with no manual interop needed

The new ICanvasSwapChainFactoryNative interface will be implemented by the CanvasSwapChain activation factory:

namespace ABI {
    namespace Microsoft {
        namespace Graphics {
            namespace Canvas {

              class __declspec(uuid("040AB731-08F1-469F-9BF9-5B1160F27224"))
                  ICanvasSwapChainFactoryNative : public IUnknown
              {
              public:
                  IFACEMETHOD(CreateForHwnd)(
                      ICanvasResourceCreator* resourceCreator,
                      HWND hwnd,
                      uint32_t width,
                      uint32_t height,
                      float dpi,
                      DirectXPixelFormat format,
                      int32_t bufferCount,
                      ICanvasSwapChain** canvasSwapChain) = 0;
              };
} } } }

The .NET projection assembly will then manually implement these two additional APIs on CanvasSwapChain:

namespace Microsoft.Graphics.Canvas;

public sealed class CanvasSwapChain
{
    public static CanvasSwapChain CreateForHwnd(
        IntPtr hwnd,
        uint width,
        uint height);

    public static CanvasSwapChain CreateForHwnd(
        IntPtr hwnd,
        uint width,
        uint height,
        DirectXPixelFormat format,
        int bufferCount);
}

This makes things very easy to use both for C++ devs (which will have C++/WinRT helpers), as well as for C# developers using the projection assembly. Specifically, in this case, the fact these two APIs are not really WinRT APIs is completely transparent.

Example use

// Create a window with basic styling and message loop
IntPtr hwnd = CreateWindow();

// Initialize a swap chain in 1 line!
CanvasSwapChain swapChain = CanvasSwapChain.CreateForHwnd(hwnd, 1920, 1080, 96.0f);

// Draw!
using (CanvasDrawingSession drawingSession = swapChain.CreateDrawingSession(default))
{
    drawingSession.DrawEllipse(155, 115, 80, 30, Color.FromArgb(a: 255, r: 0, g: 148, b: 255), 3);
    drawingSession.DrawText("Hello, world!", 100, 100, Color.FromArgb(a: 255, r: 255, g: 216, b: 0));
}

// Wait for v-sync
this.canvasSwapChain.WaitForVerticalBlank();

// Present the new frame
this.canvasSwapChain.Present(syncInterval: 1);

Open questions

Should we handle DPI changes in some way after a swap chain has been created? Also see conversation below.

@sylveon
Copy link

sylveon commented Apr 17, 2023

Can't you retrieve the window DPI with GetDpiForWindow?

@Sergio0694
Copy link
Member Author

Right, we could use that to automatically get the initial DPIs, and remove the dpi parameter form the static constructors, that'd work nicely. The only thing I'm not exactly sure how to handle is the fact the effective DPI might change after creating the swap chain (eg. if you move the window to another screen with a different DPI, or if you change the DPIs on your screen), but the swap chain wouldn't really respond to that, and its internal DPI value would become "out of sync".

Should we just not worry about this scenario? Users are still able to override the DPI value by calling the Resize overload, is that enough? As in, they could detect a DPI change on their end, and then call Resize with 0 for width and height, and the right value for the new DPI. The swap chain would resize automatically and then update the internal DPI value.

Maybe that's sufficient? 🤔

@sylveon
Copy link

sylveon commented Apr 17, 2023

You could subclass the window to capture the WM_DPICHANGE message. Am I understanding correctly that Win2D tracks its own DPI values?

@Sergio0694
Copy link
Member Author

I guess it does when you use higher level objects that handle that for you (eg. CanvasAnimatedControl, which is a wrapper around CanvasSwapChain, which subscribes to DPI changes automatically). Double checking things, CanvasSwapChain does not in fact handle this automatically, as it's meant to be a lower level object, so I guess that makes sense. If you want to update DPIs you can just monitor DPI change events on your own and call Resize on the panel when needing, and pass the new DPI value from there to update it. It seems like that should be enough. 🤔

@Sergio0694 Sergio0694 added the approved The proposal was approved in API review, it can be implemented label May 9, 2023
@Sergio0694
Copy link
Member Author

Notes from API review

Looks good as proposed.

API breakdown (click to expand):
namespace ABI {
    namespace Microsoft {
        namespace Graphics {
            namespace Canvas {

              class __declspec(uuid("040AB731-08F1-469F-9BF9-5B1160F27224"))
                  ICanvasSwapChainFactoryNative : public IUnknown
              {
              public:
                  IFACEMETHOD(CreateForHwnd)(
                      ICanvasResourceCreator* resourceCreator,
                      HWND hwnd,
                      uint32_t width,
                      uint32_t height,
                      float dpi,
                      DirectXPixelFormat format,
                      int32_t bufferCount,
                      ICanvasSwapChain** canvasSwapChain) = 0;
              };
} } } }
namespace Microsoft.Graphics.Canvas;

public sealed class CanvasSwapChain
{
    public static CanvasSwapChain CreateForHwnd(
        IntPtr hwnd,
        uint width,
        uint height);

    public static CanvasSwapChain CreateForHwnd(
        IntPtr hwnd,
        uint width,
        uint height,
        DirectXPixelFormat format,
        int bufferCount);
}

The C# APIs added to CanvasSwapChain will use GetDpiForWindow to set the initial DPI value. It is up to developers to then monitor DPI changes (through WM_DPICHANGE) and call the appropriate Resize overload to update the DPI value. This matches the general behavior of CanvasSwapChain when used with composition or CoreWindow as well. Additionally, CanvasSwapChain as a whole is meant to be a lower-level helper API anyway. The new APIs introduced in this proposal will already make it significantly easier for developers to get a swapchain running on a raw HWND. For those needing more control over how the swapchain is created, they can keep using the existing approach of manually creating the swapchain and then using GetOrCreate from the CanvasDevice activation factory interop interface to get the WinRT wrapper for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved The proposal was approved in API review, it can be implemented feature
Projects
None yet
Development

No branches or pull requests

2 participants