14 January 2017

OpenGL – Calling version 1.1+ functions from .net under Windows

I learned that the Windows OpenGL DLL only exports OpenGL 1.1 functions. To use newer functions, you must obtain the function pointer first. Conveniently, the following method is provided to do so:

[DllImport("opengl32", EntryPoint = "wglGetProcAddress", SetLastError = true)]
private static extern IntPtr GetProcAddressExtern(string name);
internal static IntPtr GetProcAddress(string name)
{
    var address = GetProcAddressExtern(name);
    if (address == IntPtr.Zero) throw new ApplicationException($@"Get procedure address for ""{name}""");
    return address;
}

To call the method, you must define a delegate for it, and use it as follows:

private delegate void GenBuffersDelegate(int n, uint[] buffers);
internal static void GenBuffers(int n, uint[] buffers)
{
    var funcptr = GetProcAddress("glGenBuffers");
    var func = Marshal.GetDelegateForFunctionPointer(funcptr);
    func(n, buffers);
}

There is a performance cost however. Therefore it's best to store the function once retrieved. I also used a generic method to make the whole thing more convienient.

private static Dictionary<string, object> Functions = new Dictionary<string, object>();
internal static Del Function<del>() where Del : class
{
    var type = typeof(Del).Name;
    if (Functions.ContainsKey(type))
        return Functions[type] as Del;

    IntPtr ptr = GetProcAddress(type);
    if (ptr == null || ptr == IntPtr.Zero)
        throw new ApplicationException($@"Pointer to OpenGL function ""{type}"" could not be retrieved.");
    var function = Marshal.GetDelegateForFunctionPointer<del>(ptr);
    Functions.Add(type, function);
    return function;
}

The same method can now simply be called like this:

private delegate void glGenBuffers(int n, uint[] ids);
public static void GenBuffers(int n, uint[] ids) => Win.GL.Function<glGenBuffers>()(n, ids);

08 January 2017

.net – Running an OpenGL loop written in C# using P/Invoke for Windows

As the title already explains, what follows will be all the parts needed to initiate OpenGL and run a (game) drawing loop. Everything is written in C#, communicating with Windows through P/Invoke, and no additional references are included.

Windows core functions


These are the Windows functions that will be P/Invoked to create the window to draw on. You could simply create a .net form, but somehow .net gets in the way of OpenGL, and it doesn't work properly. The following is all part of a static class called Windows.Core:
internal const int WINDOW_CLASS_STYLE_OWN_DEVICE_CONTEXT = 0x0020;
internal const uint WINDOW_STYLE_VISIBLE = 0x10000000;
internal const uint WINDOW_STYLE_CAPTION = 0x00C00000;
internal const uint WINDOW_STYLE_POPUP_WINDOW = 0x80000000 | 0x00800000 | 0x00080000;

public delegate int WindowProcedure(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam);

[StructLayout(LayoutKind.Sequential)]
private struct WindowClassEx
{
    public uint Size;
    public int Styles;
    [MarshalAs(UnmanagedType.FunctionPtr)]
    public WindowProcedure WindowProcedure;
    public int ClassExtra;
    public int WindowExtra;
    public IntPtr Instance;
    public IntPtr Icon;
    public IntPtr Cursor;
    public IntPtr Background;
    public string MenuName;
    public string ClassName;
    public IntPtr IconSmall;
}

[DllImport("user32", SetLastError = true)]
private static extern IntPtr GetDC(IntPtr hWnd);
internal static IntPtr GetDeviceContext(IntPtr windowHandle)
{
    IntPtr result = GetDC(windowHandle);
    if (result == IntPtr.Zero) throw new ApplicationException("Get device context");
    return result;
}

[DllImport("user32", SetLastError = true)]
private static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC);
internal static void ReleaseDeviceContext(IntPtr windowHandle, IntPtr deviceContext)
{
    if (deviceContext != IntPtr.Zero)
        if (!ReleaseDC(windowHandle, deviceContext))
            throw new ApplicationException("Release device context");
}

[DllImport("kernel32", EntryPoint = "RtlZeroMemory", SetLastError = true)]
internal static extern void ZeroMemory(IntPtr dest, IntPtr size);

[DllImport("user32", EntryPoint = "SetForegroundWindow", SetLastError = true)]
private static extern bool SetForegroundWindowExtern(IntPtr hWnd);
internal static void SetForegroundWindow(IntPtr deviceContext)
{
    if (!SetForegroundWindowExtern(deviceContext))
        Debug.WriteLine("Could not set foreground window.");
}

[DllImport("kernel32", SetLastError = true)]
internal static extern int GetLastError();

[DllImport("user32", SetLastError = true)]
private static extern short RegisterClassEx(ref WindowClassEx windowClass);
internal static short RegisterClass(IntPtr instanceHandle, string className, WindowProcedure windowProcedure)
{
    var windowClass = new Windows.Core.WindowClassEx
    {
        Styles = Windows.Core.WINDOW_CLASS_STYLE_OWN_DEVICE_CONTEXT,
        WindowProcedure = windowProcedure,
        Instance = instanceHandle,
        ClassName = className
    };
    windowClass.Size = (uint)Marshal.SizeOf(windowClass);

    short result = RegisterClassEx(ref windowClass);
    if (result == (short)0) throw new ApplicationException("Register class");
    return result;
}

[DllImport("user32", SetLastError = true)]
private static extern IntPtr CreateWindowEx(uint extendedStyles, string className, string windowName, uint styles, int x, int y,
    int width, int height, IntPtr parent, IntPtr menu, IntPtr instance, IntPtr param);
internal static IntPtr CreateWindow(IntPtr instanceHandle, string className, string title)
{
    IntPtr result = CreateWindowEx(0, className, title,
      Windows.Core.WINDOW_STYLE_CAPTION | Windows.Core.WINDOW_STYLE_POPUP_WINDOW | Windows.Core.WINDOW_STYLE_VISIBLE,
      0, 0, 640, 480, IntPtr.Zero, IntPtr.Zero, instanceHandle, IntPtr.Zero);
    if (result == IntPtr.Zero) throw new ApplicationException("Create window");
    return result;
}

[DllImport("user32", EntryPoint = "DestroyWindow", SetLastError = true)]
private static extern bool DestroyWindowExtern(IntPtr hWnd);
internal static void DestroyWindow(IntPtr deviceContext)
{
    if (deviceContext != IntPtr.Zero)
        if (!DestroyWindowExtern(deviceContext))
            throw new ApplicationException("Destroy window");
}

Windows GDI functions


Some more functions from Windows.GDI:

internal const int PIXEL_FORMAT_DOUBLE_BUFFER = 0x00000001;
internal const int PIXEL_FORMAT_DRAW_TO_WINDOW = 0x00000004;
internal const int PIXEL_FORMAT_SUPPORT_OPENGL = 0x00000020;
internal const int PIXEL_FORMAT_TYPE_RGBA = 0;
internal const int PIXEL_FORMAT_LAYER_MAIN_PLANE = 0;

internal struct PixelFormatDescriptor
{
    public short Size;
    public short Version;
    public int Flags;
    public byte PixelType;
    public byte ColorBits;
    public byte RedBits;
    public byte RedShift;
    public byte GreenBits;
    public byte GreenShift;
    public byte BlueBits;
    public byte BlueShift;
    public byte AlphaBits;
    public byte AlphaShift;
    public byte AccumBits;
    public byte AccumRedBits;
    public byte AccumGreenBits;
    public byte AccumBlueBits;
    public byte AccumAlphaBits;
    public byte DepthBits;
    public byte StencilBits;
    public byte AuxBuffers;
    public byte LayerType;
    public byte Reserved;
    public int LayerMask;
    public int VisibleMask;
    public int DamageMask;
}

internal static PixelFormatDescriptor GetPixelFormatDescriptor(byte colorBits, byte depthBits)
{
    var pixelFormat = new PixelFormatDescriptor();
    unsafe
    {
        Windows.Core.ZeroMemory((IntPtr)(&pixelFormat), (IntPtr)Marshal.SizeOf(pixelFormat));
    }
    pixelFormat.Size = (short)Marshal.SizeOf(pixelFormat);
    pixelFormat.Version = 1;
    pixelFormat.Flags = PIXEL_FORMAT_DOUBLE_BUFFER | PIXEL_FORMAT_DRAW_TO_WINDOW | PIXEL_FORMAT_SUPPORT_OPENGL;
    pixelFormat.PixelType = PIXEL_FORMAT_TYPE_RGBA;
    pixelFormat.ColorBits = colorBits;
    pixelFormat.LayerType = PIXEL_FORMAT_LAYER_MAIN_PLANE;
    pixelFormat.DepthBits = depthBits;
    return pixelFormat;
}

[DllImport("gdi32", EntryPoint = "ChoosePixelFormat", SetLastError = true)]
private static extern int ChoosePixelFormatExtern(IntPtr hDC, ref PixelFormatDescriptor descriptor);
internal static int ChoosePixelFormat(IntPtr deviceContext, ref PixelFormatDescriptor descriptor)
{
    int result = ChoosePixelFormatExtern(deviceContext, ref descriptor);
    if (result == 0) throw new ApplicationException("Choose pixel format");
    return result;
}

[DllImport("gdi32", EntryPoint = "SetPixelFormat", SetLastError = true)]
private static extern bool SetPixelFormatExtern(IntPtr hDC, int format, ref PixelFormatDescriptor descriptor);
internal static void SetPixelFormat(IntPtr deviceContext, int format, ref PixelFormatDescriptor descriptor)
{
    if (!SetPixelFormatExtern(deviceContext, format, ref descriptor))
        throw new ApplicationException("Set pixel format");
}

[DllImport("gdi32", EntryPoint = "SwapBuffers", SetLastError = true)]
private static extern bool SwapBuffersExtern(IntPtr hDC);
internal static void SwapBuffers(IntPtr deviceContext)
{
    if (!SwapBuffersExtern(deviceContext))
        throw new ApplicationException("Swap buffers");
}

Windows OpenGL functions


And some Windows-specific OpenGL functions:

[DllImport("opengl32", EntryPoint = "wglCreateContext", SetLastError = true)]
private static extern IntPtr CreateContextExtern(IntPtr hDC);
internal static IntPtr CreateContext(IntPtr deviceContext)
{
    IntPtr result = CreateContextExtern(deviceContext);
    if (result == IntPtr.Zero) throw new ApplicationException("Create render context");
    return result;
}

[DllImport("opengl32", EntryPoint = "wglMakeCurrent", SetLastError = true)]
private static extern bool MakeCurrentExtern(IntPtr hDC, IntPtr renderContext);
internal static void MakeCurrent(IntPtr deviceContext, IntPtr renderContext)
{
    if (!MakeCurrentExtern(deviceContext, renderContext))
        throw new ApplicationException(renderContext == IntPtr.Zero ? "Make NULL current" : "Make render context current");
}

[DllImport("opengl32", EntryPoint = "wglDeleteContext", SetLastError = true)]
private static extern bool DeleteContextExtern(IntPtr renderContext);
internal static void DeleteContext(IntPtr renderContext)
{
    if (renderContext != IntPtr.Zero)
        if (!DeleteContextExtern(renderContext))
            throw new ApplicationException("Delete render context");
}

OpenGL functions


And finally some actual OpenGL functions, so there's something to see:

internal const int COLOR_BUFFER_BIT = 16384;

[DllImport("opengl32", EntryPoint = "glClear", SetLastError = true)]
internal static extern void Clear(short mask);

[DllImport("opengl32", EntryPoint = "glClearColor", SetLastError = true)]
internal static extern void ClearColor(float red, float green, float blue, float alpha);

[DllImport("opengl32", EntryPoint = "glFinish", SetLastError = true)]
internal static extern void Finish();

[DllImport("opengl32", EntryPoint = "glFlush", SetLastError = true)]
internal static extern void Flush();

Main program


With all of that in place, a fairly simple program can be put together. It's of type "Windows Application", but I removed the auto-created form, as well as the reference to Windows.Forms. It also contains the method that allows Windows to communicate with the program while it's running. This one doesn't really do anything useful for us, but returning that "1" is necessary to get the window created. The following code is in the static Main method:

var windowProcedure = new Windows.Core.WindowProcedure((IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam) =>
{
    return message == 1 || message == 129 || message == 32 ? 1 : 0;
});

IntPtr instanceHandle = Process.GetCurrentProcess().Handle;
string className = "EGSDemolisher3";
Windows.Core.RegisterClass(instanceHandle, className, windowProcedure);
IntPtr windowHandle = Windows.Core.CreateWindow(instanceHandle, className, "Demolisher III");
IntPtr deviceContext = Windows.Core.GetDeviceContext(windowHandle);
Windows.GDI.PixelFormatDescriptor descriptor = Windows.GDI.GetPixelFormatDescriptor(24, 16);
var format = Windows.GDI.ChoosePixelFormat(deviceContext, ref descriptor);
Windows.GDI.SetPixelFormat(deviceContext, format, ref descriptor);
IntPtr renderContext = Windows.OpenGL.CreateContext(deviceContext);
Windows.OpenGL.MakeCurrent(deviceContext, renderContext);
Windows.Core.SetForegroundWindow(deviceContext);

var endTime = DateTime.Now.AddSeconds(5);
int second = DateTime.Now.Second;
int frames = 0;
while (DateTime.Now < endTime)
{
    if (DateTime.Now.Second != second)
    {
        Debug.WriteLine($"FPS {second}': {frames}");
        second = DateTime.Now.Second;
        frames = 0;
    }
    frames++;

    if (second % 2 == 0)
        OpenGL.ClearColor(.5f, .5f, 1, 1);
    else
        OpenGL.ClearColor(1, .5f, 0, 1);

    OpenGL.Clear(OpenGL.COLOR_BUFFER_BIT);
    OpenGL.Flush();
    OpenGL.Finish();

    Windows.GDI.SwapBuffers(deviceContext);
}

Windows.OpenGL.MakeCurrent(IntPtr.Zero, IntPtr.Zero);
Windows.OpenGL.DeleteContext(renderContext);
Windows.Core.ReleaseDeviceContext(windowHandle, deviceContext);
Windows.Core.DestroyWindow(windowHandle);