It should be simple to set a WinForm or Control as render target for your DX11 context thought I didn't do that before. I'm more from the OGL/ Vulkan faction ?. But I guess DX11 also allows to create a render context based on a window handle right?
Every C# WinForms Control, which also the Form inherits from, has a Control.Handle Property which is a usual marshalled to an HWND window pointer. From there, you could create a render context in your C++ code and should be able to render directly to the Form/Control. However, you should inherit a new Control and override some of it's styles to it isn't rendered by GDI anymore (also enabling DoubleBuffer will really help). And you should implement a small message loop to it so it will be frequently updated.
Btw. I wouldn't go for COM but expose some interface methods that could be used from C# p/invoke without it. DllImport is your friend and with CriticalFinalizerObject you can allocate memory in your engine heap and still make sure to free it when GC collects the managed object which is holding a pointer to that memory in C#. I did the same (wrote a code generator in the end that works on some special comments in my C++ source that act like C# attributes) and it worked quite well, also on marshalling data from the engine heap into managed C# and back. As an alternative, you can also pin a managed byte array in the GC and marshal it as pointer into your C++ code but in my opinion it is more efficient if every part of the solution manages it's own memory.
As an alternative to use your engine to render everything, you can get a massive speed improvement by maintaining your own render buffer in C#. You start with a class which manages the data in memory.
GCHandle handle = GCHandle.Alloc(new byte[length], GCHandleType.Pinned);
pixelData = Marshal.UnsafeAddrOfPinnedArrayElement(handle.Target as Array, 0);
memoryHandle = GCHandle.ToIntPtr(handle);
This creates an array of byte in a static memory location and pushes the pointer to the first element into a managed IntPtr struct. I also create a second pointer to the pinned type in order to be able to view and manipulate the array as a managed C# object.
public byte[] ToArray()
{
return GCHandle.FromIntPtr(memoryHandle).Target as byte[];
}
In my render buffer class I then use the pixel data to feed it into a Bitmap object (it is important to use PixelFormat.Format32bppPArgb because GDI renders faster when the pixel data already match the GDI pixel format)
public bool Resize(Size size)
{
int stride = size.Width * 4;
int padding = (stride % 4);
if (padding != 0)
padding = 4 - padding;
stride += padding;
int length = stride * size.Height;
return Resize(size, length, stride);
}
public bool Resize(Size size, int length, int stride)
{
if (pbuffer.Length != length)
{
if (renderTarget != null)
renderTarget.Dispose();
if (graphics != null)
graphics.Dispose();
if (pbuffer.Length < length)
pbuffer.Resize(length);
renderTarget = new Bitmap(size.Width, size.Height, stride, System.Drawing.Imaging.PixelFormat.Format32bppPArgb, pbuffer);
return true;
}
else return false;
}
Now you have a Bitmap which points to the pixel data in memory. What you now can do is to manipulate those data either from hand using bit operations or a Graphics object created from the Bitmap. But you'll ask why not just create a plain Bitmap and that's it, I guess. Simply because you don't have to recreate the pixel data array in memory for as long as your Bitmap partial or entirely covers it. This means that you can offscreen render into areas of your pixel data without the need to reallocate something. So it is quiet easy to use the Graphics object which from the Bitmap to update certain region in your trendered tile data which will tear down the time you spend on render everything while the rest of the buffered data is still intact. Another benefit is that you can update your buffer in multiple threads without issues, for as long as each thread has it's own Bitmap instance pointing to the same pixel data in memory.
Everything you then have to do is overriding the paint event in your Control and instead draw the area of your pixel data to it.
protected virtual void OnPaint (System.Windows.Forms.PaintEventArgs e)
{
e.Graphics.DrawImageUnscaled(buffer.RenderTarget, 0, 0);
}
I use this technique and some other C# sugar like Reactive in my custom WinForms UI Framework in order to render fully customizeable windows and UI components