Introduction: C++ OpenGL ES Viewer in C#

Picture of C++ OpenGL ES Viewer in C#

Ok so for this tutorial I will be explaining in detail how one can setup a Xamarin C# application that runs on WP 8.1, Windows 8.1 (store), Android and iOS* and very importantly makes use of OpenGL ES 2.0 views powered by C++ renderers. This is something that I recently needed to implement for a project of my own and I am sure some others might also find it handy. In fact, I already have already found someone on the Xamarin forums who needs this.

The reason one would want to do this is fairly obvious. I mean a lot of OpenGL ES code is written in C++ and all the major platforms now support it through C++. This includes Windows Store and Windows Phone with the recent advances in the ANGLE project. In many cases the performance benefit of C++ over C# would also be very beneficial in this area. OpenTK on all the platforms except the Windows ones (that use ANGLE and do not have OpenTK support yet) seems to be the main way of utilizing OpenGL from C# but unfortunately as I have already mentioned, it is not available on the non-desktop Windows Platforms. Furthermore, as I understand it, OpenTK uses P/Invoke which is fine but with marshaling involved one could see a serious performance decrease when running a renderer in C# vs C++ when there are a lot of OGL calls and a lot of data shifting (and therefore conversion) involved.

Now the obvious question might be, why not just use C++ throughout? The answer to this, as many of you reading this probably already know is because that is not really so straight forward. One of the main advantages of using C# compared to C++ for cross-platform projects is of course Xamarin and Xamarin.Forms** which allows us to create native UIs on all the supported platforms without really having to do much platforms specific work, especially when your only working with standard controls. You will unfortunately have to write the implementations for the GLES view controls separately for each platform, but once you have them we can reuse them as much as we like. Getting back to my story, the only way I know of do achieve something like this in C++ is through Qt and that’s really just creating platform-agnostic applications that are styled to either look like the operating systems at the time of development or just styled with some generic artwork that sort of looked right on the different platforms at the time of creation. Not something that you want… Apart from that I do not know of anything that also supports Windows (store and phone). Also, Qt does not provide any form of native Windows Phone-ish styling at the time of writing… Finally, using C++ for general cross-platform development is also not really as comfortable to use nor as maintainable as C# with Xamarin which provides a very large amount of code sharing. Furthermore I think most of you will agree that using C# is just simply nicer using C++ in quite a few ways. We therefore really just want C++ to do 3D rendering and probably scene construction and manipulation but we don’t want it near any other parts of the application in order to avoid some serious frustration for most.

*Currently I cannot implement the iOS part due to a lack of Mac hardware but you can see the dedicated step page for more details on what you can still try to do.

**Currently Xamarin.Forms does not work for Android on VS 2015 Community RC and I therefore could not implement Xamarin.Forms but you can see the dedicated step page for more details on what you can still try to do.

Step 1: Get Your Environment Ready

Picture of Get Your Environment Ready

Ok enough rambling, let’s get started. Or if you want, just skip to the end where I discuss using my templates. I strongly advise reading the implementation though as some knowledge of how this whole thing works could prove invaluable.

I’m using the Xamarin trial with Visual Studio 2015 Community RC here because it really makes my live easier, especially when working with C++ and C# at the same time on Android. If using Xamarin Studio you will have to make the C++ projects in VS or Xcode or Eclipse or Android Studio or whatever (because Xamarin Studio doesn’t really do C++) and then copy the compiled binaries to your C# project. Setting it up so that it uses the correct DLL for each configuration (Debug or Release) as well as each architecture (ARM vs x86 vs x64) can be tricky. Maybe I’ll discuss how to do this in a later tutorial (if I find a nice way of doing it…). VS 2015 does this automatically for Android. For Windows it is done automatically on any version of VS that supports the platform. For iOS VS 2015 will hopefully support this once released. At the moment it can supposedly sort of do it. Please check the iOS and Xamarin.Forms steps for some broad info on getting this working without VS 2015.

First let’s start by creating a blank solution. I know there is a Xamarin.Forms template but it still uses the old Windows Phone 8.0 Silverlight project that we don’t want. I also just find it slightly full of clutter and frustration on VS 2015 (my new best friend…). You’ll then face something like this the first screenshot.

Next we need to create the individual projects one by one. We’ll start with the Windows projects first and then do the others later once we have the Windows ones working. So we’ll firstly need a blank WP 8.1 and Win 8.1 C# project. Then we will need a C++ WP and Win Runtime Component. Finally we will need a shared C++ project for the Runtime Components. I have added some screenshots of each project being created with an example name below.

Remember to add the shared references by right clicking on the actual projects’ “References” property, selecting “Add Reference…” and selecting the appropriate shared project.

These projects will contain all the platform specific code that we need to get a GLES viewer component and Renderer object exposed to the shared (I guess Xamarin compatible) C# code for the application that we will be creating a little later.

Step 2: Get ANGLE

Picture of Get ANGLE

Before you can go on you will have to install Microsoft’s branch of the ANGLE project (https://github.com/MSOpenTech/angle) which will allow you to access OpenGL ES on all the Windows platforms through a process of intelligently wrapping DirectX. The way I recommend one does this is by adding it as a Git project in Visual Studio. This will allow you to easily sync in any new updates and easily push any modifications you make to your own branch where you can then make a pull request. You could also just download the zip archive and go from there. I believe they document the installation process very themselves but in essence the installation involves that you have to open the project for each platform and then compile it for each configuration and architecture (Debug, Release, x86, x64(on Win Store), ARM). For this tutorial you will only need the Windows 8.1 and Windows Phone 8.1 binaries, not the Desktop or Windows 10 ones. After you have compiled them all you will need to run the install script in the source directory. This will setup your environment variables so that VS knows where to find the ANGLE binaries and headers.

Now we will need to create some Macros that will define where ANGLE is installed. Start by going to the “Property Manager”, you can get there using the search box in the top right corner. So now right-click on the Windows project and select “Add New Property Sheet…” Call it “ANGLE”. Now open it by double-clicking it in any configuration. Finally go to user Macros and add “AngleBinPath” and “AngleIncPath”. The “Bin” one should point to “$(AngleRootPath)\winrt\8.1\windows\src\$(Configuration)_$(Platform)\” and the “Inc” one to “$(AngleRootPath)\include\”. Make sure to enable the Macro for the build environment. Just do the same for the Windows Phone project but this time use “$(AngleRootPath)\winrt\8.1\windowsphone\src\$(Configuration)_$(Platform)\” instead of “$(AngleRootPath)\winrt\8.1\windows\src\$(Configuration)_$(Platform)\”. The include path remains the same.

Now that we have the macros we need to add the include directories to the project as well as the libraries that the linker should link to. So please do the following on the property sheet of both the Win and WP projects. On the left you will see a “Linker” group and on it an “Input” page. Open it and prepend “$(AngleBinPath)lib\libGLESv2.lib;$(AngleBinPath)lib\libEGL.lib;” to the “Additional Dependencies” field. Also go to “C/C++” and then “General”. Here you need to prepend “$(AngleIncPath);” to the “Additional Include Directories” field. As this is one Property page, you only need to do this on one configuration and it will applied throughout. Just remember to do exactly the same thing with the Windows Phone property page.

Next we need to add the DLL files to the C# projects. This is unfortunately working way that I could find to get them copied to the runtime directory and without them there you get a host of errors. So for both the Win and the WP C# projects add libEGL.dll and libGLESv2.dll as existing files. Leave them configured as “Build Action” “Content” and “Copy to Output Directory” “Do not copy”. It does not matter which DLLs you choose now because we will be changing their paths later on to automatically point to the correct files (we need a different one for each configuration). So once you have them, open the directory where your project is. We now want to find the Windows .csproj file and open it with Notepad. Remember to just save everything before doing this. You now have to find the location where the inclusion of the DLLs is declared.

Replace:

<Content Include="libGLESv2.dll" /><br><Content Include="libEGL.dll" />

With:

<Content Condition="'$(Platform)' == 'x86'" Include="$(AngleRootPath)\winrt\8.1\windows\src\$(Configuration)_Win32\libGLESv2.dll" /><br><Content Condition="'$(Platform)' == 'x86'" Include="$(AngleRootPath)\winrt\8.1\windows\src\$(Configuration)_Win32\libEGL.dll" />
<Content Condition="'$(Platform)' != 'x86'" Include="$(AngleRootPath)\winrt\8.1\windows\src\$(Configuration)_$(Platform)\libGLESv2.dll" />
<Content Condition="'$(Platform)' != 'x86'" Include="$(AngleRootPath)\winrt\8.1\windows\src\$(Configuration)_$(Platform)\libEGL.dll" />

The reason we need this complicated Condition based statement is because the platform known to C# as x86 is known to C++ as “Win32”. Luckily the rest of the platforms have the same names...

After doing this, do exactly the same thing for the Windows Phone project (but the “windows” after 8.1 should now be “windowsphone” and then go back to Visual Studio. VS should ask if you want files to be reloaded, just say reload all. And your DLLs should now automatically point to the correct versions at compile time. You can delete the DLLs that were initially copied to your project folders using Explorer as they are now unused.

Next you can just add references to the runtime components in your C# projects. Make sure to use the WP one for the WP project and the Win one for the Win project.

Before going any further I would recommend you check and see if you can still compile and deploy the application. Just remember to choose an architecture like x86 or ARM as “Any CPU” will give errors with the native libraries. If everything is working fine you should see an empty application start up.

Step 3: Create the Renderer

Picture of Create the Renderer

Next we are going to start with the code needed to get something rendering. For this I just modified the GLES code provided in the ANGLE template that draws a spinning cube. So now add a SimpleRenderer class to a new shared C++ project that we will call SharedRenderer. This will later also be shared by the other projects. Also just reference this project in the Runtime Components.

Please just start by turning off precompiled headers because this does not currently play nice with Android shared C++ in VS 2015 RC. This is because a VS shared C++ project can currently not see any files in an Android C++ library project. Here's hoping Microsoft fixes it soon. You can do this by going to your ANGLE property page on both Win and WP and simply selecting “Not Using Precompiled Headers” in the “Precompiled Header” field in the “C/C++” group.

I am not going to discuss what the following GLES code does as it is beyond the scope of the tutorial. So just put the following in the header:

#pragma once
#include <GLES2/gl2.h>
class SimpleRenderer{
public:
    SimpleRenderer();
    ~SimpleRenderer();
    void Draw();
    void UpdateWindowSize(GLsizei width, GLsizei height);
    void Init();
private:
    GLuint mProgram;
    GLsizei mWindowWidth;
    GLsizei mWindowHeight;
    GLint mPositionAttribLocation;
    GLint mColorAttribLocation;
    GLint mModelUniformLocation;
    GLint mViewUniformLocation;
    GLint mProjUniformLocation;
    GLuint mVertexPositionBuffer;
    GLuint mVertexColorBuffer;
    GLuint mIndexBuffer;
    int mDrawCount;
};

And the following in the CPP file:

// This file is used by the template to render a basic scene using GL.
#include "SimpleRenderer.h"
#include "MathHelper.h"
#include <vector>
#include <string>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#if defined(WIN_STORE) || defined(WIN_PHONE)
#include <ppltasks.h>
using namespace Platform;
#endif
#define STRING(s) #s
GLuint CompileShader(GLenum type, const std::string &source)
{
    GLuint shader = glCreateShader(type);
    const char *sourceArray[1] = { source.c_str() };
    glShaderSource(shader, 1, sourceArray, NULL);
    glCompileShader(shader);
    GLint compileResult;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult);
    if (compileResult == 0)
    {
        GLint infoLogLength;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength);
        std::vector infoLog(infoLogLength);
        glGetShaderInfoLog(shader, (GLsizei)infoLog.size(), NULL, infoLog.data());
        std::wstring errorMessage = std::wstring(L"Shader compilation failed: ");
        errorMessage += std::wstring(infoLog.begin(), infoLog.end());
#if defined(WIN_STORE) || defined(WIN_PHONE)
        throw Exception::CreateException(E_FAIL, ref new Platform::String(errorMessage.c_str()));
#endif
    }
    return shader;
}
GLuint CompileProgram(const std::string &vsSource, const std::string &fsSource)
{
    GLuint program = glCreateProgram();
    if (program == 0)
    {
#if defined(WIN_STORE) || defined(WIN_PHONE)
        throw Exception::CreateException(E_FAIL, L"Program creation failed");
#else
        return -1;
#endif
    }
    GLuint vs = CompileShader(GL_VERTEX_SHADER, vsSource);
    GLuint fs = CompileShader(GL_FRAGMENT_SHADER, fsSource);
    if (vs == 0 || fs == 0)
    {
        glDeleteShader(fs);
        glDeleteShader(vs);
        glDeleteProgram(program);
        return 0;
    }
    glAttachShader(program, vs);
    glDeleteShader(vs);
    glAttachShader(program, fs);
    glDeleteShader(fs);
    glLinkProgram(program);
    GLint linkStatus;
    glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
    if (linkStatus == 0)
    {
        GLint infoLogLength;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLogLength);
        std::vector infoLog(infoLogLength);
        glGetProgramInfoLog(program, (GLsizei)infoLog.size(), NULL, infoLog.data());
        std::wstring errorMessage = std::wstring(L"Program link failed: ");
        errorMessage += std::wstring(infoLog.begin(), infoLog.end());
#if defined(WIN_STORE) || defined(WIN_PHONE)
        throw Exception::CreateException(E_FAIL, ref new Platform::String(errorMessage.c_str()));
#else
        return -1;
#endif
    }
    return program;
}
SimpleRenderer::SimpleRenderer() :
    mWindowWidth(0),
    mWindowHeight(0),
    mDrawCount(0)
{
    
}
void SimpleRenderer::Init()
{
    // Vertex Shader source
    const std::string vs = STRING
        (
            uniform mat4 uModelMatrix;
    uniform mat4 uViewMatrix;
    uniform mat4 uProjMatrix;
    attribute vec4 aPosition;
    attribute vec4 aColor;
    varying vec4 vColor;
    void main()
    {
        gl_Position = uProjMatrix * uViewMatrix * uModelMatrix * aPosition;
        vColor = aColor;
    }
    );
    // Fragment Shader source
    const std::string fs = STRING
        (
            precision mediump float;
    varying vec4 vColor;
    void main()
    {
        gl_FragColor = vColor;
    }
    );
    // Set up the shader and its uniform/attribute locations.
    mProgram = CompileProgram(vs, fs);
    mPositionAttribLocation = glGetAttribLocation(mProgram, "aPosition");
    mColorAttribLocation = glGetAttribLocation(mProgram, "aColor");
    mModelUniformLocation = glGetUniformLocation(mProgram, "uModelMatrix");
    mViewUniformLocation = glGetUniformLocation(mProgram, "uViewMatrix");
    mProjUniformLocation = glGetUniformLocation(mProgram, "uProjMatrix");
    // Then set up the cube geometry.
    GLfloat vertexPositions[] =
    {
        -1.0f, -1.0f, -1.0f,
        -1.0f, -1.0f, 1.0f,
        -1.0f, 1.0f, -1.0f,
        -1.0f, 1.0f, 1.0f,
        1.0f, -1.0f, -1.0f,
        1.0f, -1.0f, 1.0f,
        1.0f, 1.0f, -1.0f,
        1.0f, 1.0f, 1.0f,
    };
    glGenBuffers(1, &mVertexPositionBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, mVertexPositionBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);
    GLfloat vertexColors[] =
    {
        0.0f, 0.0f, 0.0f,
        0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f,
        0.0f, 1.0f, 1.0f,
        1.0f, 0.0f, 0.0f,
        1.0f, 0.0f, 1.0f,
        1.0f, 1.0f, 0.0f,
        1.0f, 1.0f, 1.0f,
    };
    glGenBuffers(1, &mVertexColorBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, mVertexColorBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexColors), vertexColors, GL_STATIC_DRAW);
    short indices[] =
    {
        0, 1, 2, // -x
        1, 3, 2,
        4, 6, 5, // +x
        5, 6, 7,
        0, 5, 1, // -y
        0, 4, 5,
        2, 7, 6, // +y
        2, 3, 7,
        0, 6, 4, // -z
        0, 2, 6,
        1, 7, 3, // +z
        1, 5, 7,
    };
    glGenBuffers(1, &mIndexBuffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexBuffer);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
}
SimpleRenderer::~SimpleRenderer()
{
    if (mProgram != 0)
    {
        glDeleteProgram(mProgram);
        mProgram = 0;
    }
    if (mVertexPositionBuffer != 0)
    {
        glDeleteBuffers(1, &mVertexPositionBuffer);
        mVertexPositionBuffer = 0;
    }
    if (mVertexColorBuffer != 0)
    {
        glDeleteBuffers(1, &mVertexColorBuffer);
        mVertexColorBuffer = 0;
    }
    if (mIndexBuffer != 0)
    {
        glDeleteBuffers(1, &mIndexBuffer);
        mIndexBuffer = 0;
    }
}
void SimpleRenderer::Draw()
{
    glEnable(GL_DEPTH_TEST);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    if (mProgram == 0)
        return;
    glUseProgram(mProgram);
    glBindBuffer(GL_ARRAY_BUFFER, mVertexPositionBuffer);
    glEnableVertexAttribArray(mPositionAttribLocation);
    glVertexAttribPointer(mPositionAttribLocation, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glBindBuffer(GL_ARRAY_BUFFER, mVertexColorBuffer);
    glEnableVertexAttribArray(mColorAttribLocation);
    glVertexAttribPointer(mColorAttribLocation, 3, GL_FLOAT, GL_FALSE, 0, 0);
    MathHelper::Matrix4 modelMatrix = MathHelper::SimpleModelMatrix((float)mDrawCount / 50.0f);
    glUniformMatrix4fv(mModelUniformLocation, 1, GL_FALSE, &(modelMatrix.m[0][0]));
    MathHelper::Matrix4 viewMatrix = MathHelper::SimpleViewMatrix();
    glUniformMatrix4fv(mViewUniformLocation, 1, GL_FALSE, &(viewMatrix.m[0][0]));
    MathHelper::Matrix4 projectionMatrix = MathHelper::SimpleProjectionMatrix(float(mWindowWidth) / float(mWindowHeight));
    glUniformMatrix4fv(mProjUniformLocation, 1, GL_FALSE, &(projectionMatrix.m[0][0]));
    // Draw 36 indices: six faces, two triangles per face, 3 indices per triangle
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexBuffer);
    glDrawElements(GL_TRIANGLES, (6 * 2) * 3, GL_UNSIGNED_SHORT, 0);
    mDrawCount += 1;
}
void SimpleRenderer::UpdateWindowSize(GLsizei width, GLsizei height)
{
    glViewport(0, 0, width, height);
    mWindowWidth = width;
    mWindowHeight = height;
}

In the SharedRenderer project you will also need to add a MathHelper.h file with the following content:

#pragma once
// These are some simple math helpers to enable the template to render a spinning cube. It is not a complete math library.
// You can replace this with your favorite math library that's suitable for your target platforms, e.g. DirectXMath or GLM.
#include <math.h>
namespace MathHelper
{
    struct Matrix4
    {
        Matrix4(float m00, float m01, float m02, float m03,
            float m10, float m11, float m12, float m13,
            float m20, float m21, float m22, float m23,
            float m30, float m31, float m32, float m33)
        {
            m[0][0] = m00; m[0][1] = m01; m[0][2] = m02; m[0][3] = m03;
            m[1][0] = m10; m[1][1] = m11; m[1][2] = m12; m[1][3] = m13;
            m[2][0] = m20; m[2][1] = m21; m[2][2] = m22; m[2][3] = m23;
            m[3][0] = m30; m[3][1] = m31; m[3][2] = m32; m[3][3] = m33;
        }
        float m[4][4];
    };
    inline static Matrix4 SimpleModelMatrix(float radians)
    {
        float cosine = cosf(radians);
        float sine = sinf(radians);
        return Matrix4(cosine, 0.0f, -sine, 0.0f,
            0.0f, 1.0f, 0.0f, 0.0f,
            sine, 0.0f, cosine, 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f);
    }
    inline static Matrix4 SimpleViewMatrix()
    {
        // Camera is at 60 degrees to the ground, in the YZ plane.
        // Camera Look-At is hardcoded to (0, 0, 0).
        // Camera Up is hardcoded to (0, 1, 0).
        const float sqrt3over2 = 0.86603f;
        const float cameraDistance = 5.0f;
        return Matrix4(1.0f, 0.0f, 0.0f, 0.0f,
            0.0f, sqrt3over2, 0.5f, 0.0f,
            0.0f, -0.5f, sqrt3over2, 0.0f,
            0.0f, 0.0f, -cameraDistance, 1.0f);
    }
    inline static Matrix4 SimpleProjectionMatrix(float aspectRatio)
    {
        // Far plane is at 50.0f, near plane is at 1.0f.
        // FoV is hardcoded to pi/3.
        const float cotangent = 1 / tanf(3.14159f / 6.0f);
        return Matrix4(cotangent / aspectRatio, 0.0f, 0.0f, 0.0f,
            0.0f, cotangent, 0.0f, 0.0f,
            0.0f, 0.0f, -50.0f / (50.0f - 1.0f), (-50.0f * 1.0f) / (50.0f - 1.0f),
            0.0f, 0.0f, -1.0f, 0.0f);
    }
}

You should be able to compile your project again right now. If not, go back and check what you might have missed. Also, I recommend deleting the “Class1.h” and “Class.cpp” files in the Runtime Components as we won’t need them. Just write down the default namespace used in them first as you'll need it later.

Step 4: Create the Windows Platform(s) Support Layer

Picture of Create the Windows Platform(s) Support Layer

We now need to add Microsoft’s OpenGLES helper class to the Shared C++ bridge project. So please just add a class named OpenGLES to that project.

The header should contain:

#pragma once
#include <EGL/egl.h>
class OpenGLES
{
public:
    OpenGLES();
    ~OpenGLES();
    EGLSurface CreateSurface(Windows::UI::Xaml::Controls::SwapChainPanel^ panel, const Windows::Foundation::Size* renderSurfaceSize);
    void DestroySurface(const EGLSurface surface);
    void MakeCurrent(const EGLSurface surface);
    EGLBoolean SwapBuffers(const EGLSurface surface);
    void Reset();
private:
    void Initialize();
    void Cleanup();
private:
    EGLDisplay mEglDisplay;
    EGLContext mEglContext;
    EGLConfig  mEglConfig;
};

And the cpp file should contain:

#include "OpenGLES.h"
#include <concrt.h> #include <EGL/egl.h> #include <EGL/eglext.h> #include <EGL/eglplatform.h> #include <angle_windowsstore.h>
using namespace Platform;
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
OpenGLES::OpenGLES() :
    mEglConfig(nullptr),
    mEglDisplay(EGL_NO_DISPLAY),
    mEglContext(EGL_NO_CONTEXT)
{
    Initialize();
}
OpenGLES::~OpenGLES()
{
    Cleanup();
}
void OpenGLES::Initialize()
{
    const EGLint configAttributes[] =
    {
        EGL_RED_SIZE, 8,
        EGL_GREEN_SIZE, 8,
        EGL_BLUE_SIZE, 8,
        EGL_ALPHA_SIZE, 8,
        EGL_DEPTH_SIZE, 8,
        EGL_STENCIL_SIZE, 8,
        EGL_NONE
    };
    const EGLint contextAttributes[] =
    {
        EGL_CONTEXT_CLIENT_VERSION, 2,
        EGL_NONE
    };
    const EGLint defaultDisplayAttributes[] =
    {
        // These are the default display attributes, used to request ANGLE's D3D11 renderer.
        // eglInitialize will only succeed with these attributes if the hardware supports D3D11 Feature Level 10_0+.
        EGL_PLATFORM_ANGLE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE,
        // EGL_ANGLE_DISPLAY_ALLOW_RENDER_TO_BACK_BUFFER is an optimization that can have large performance benefits on mobile devices.
        // Its syntax is subject to change, though. Please update your Visual Studio templates if you experience compilation issues with it.
        EGL_ANGLE_DISPLAY_ALLOW_RENDER_TO_BACK_BUFFER, EGL_TRUE,
        // EGL_PLATFORM_ANGLE_ENABLE_AUTOMATIC_TRIM_ANGLE is an option that enables ANGLE to automatically call 
        // the IDXGIDevice3::Trim method on behalf of the application when it gets suspended. 
        // Calling IDXGIDevice3::Trim when an application is suspended is a Windows Store application certification requirement.
        EGL_PLATFORM_ANGLE_ENABLE_AUTOMATIC_TRIM_ANGLE, EGL_TRUE,
        EGL_NONE,
    };
    const EGLint fl9_3DisplayAttributes[] =
    {
        // These can be used to request ANGLE's D3D11 renderer, with D3D11 Feature Level 9_3.
        // These attributes are used if the call to eglInitialize fails with the default display attributes.
        EGL_PLATFORM_ANGLE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE,
        EGL_PLATFORM_ANGLE_MAX_VERSION_MAJOR_ANGLE, 9,
        EGL_PLATFORM_ANGLE_MAX_VERSION_MINOR_ANGLE, 3,
        EGL_ANGLE_DISPLAY_ALLOW_RENDER_TO_BACK_BUFFER, EGL_TRUE,
        EGL_PLATFORM_ANGLE_ENABLE_AUTOMATIC_TRIM_ANGLE, EGL_TRUE,
        EGL_NONE,
    };
    const EGLint warpDisplayAttributes[] =
    {
        // These attributes can be used to request D3D11 WARP.
        // They are used if eglInitialize fails with both the default display attributes and the 9_3 display attributes.
        EGL_PLATFORM_ANGLE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE,
        EGL_PLATFORM_ANGLE_DEVICE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_DEVICE_TYPE_WARP_ANGLE,
        EGL_ANGLE_DISPLAY_ALLOW_RENDER_TO_BACK_BUFFER, EGL_TRUE,
        EGL_PLATFORM_ANGLE_ENABLE_AUTOMATIC_TRIM_ANGLE, EGL_TRUE,
        EGL_NONE,
    };
    EGLConfig config = NULL;
    // eglGetPlatformDisplayEXT is an alternative to eglGetDisplay. It allows us to pass in display attributes, used to configure D3D11.
    PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT = reinterpret_cast
(eglGetProcAddress("eglGetPlatformDisplayEXT"));
    if (!eglGetPlatformDisplayEXT)
    {
        throw Exception::CreateException(E_FAIL, L"Failed to get function eglGetPlatformDisplayEXT");
    }
    //
    // To initialize the display, we make three sets of calls to eglGetPlatformDisplayEXT and eglInitialize, with varying 
    // parameters passed to eglGetPlatformDisplayEXT:
    // 1) The first calls uses "defaultDisplayAttributes" as a parameter. This corresponds to D3D11 Feature Level 10_0+.
    // 2) If eglInitialize fails for step 1 (e.g. because 10_0+ isn't supported by the default GPU), then we try again 
    //    using "fl9_3DisplayAttributes". This corresponds to D3D11 Feature Level 9_3.
    // 3) If eglInitialize fails for step 2 (e.g. because 9_3+ isn't supported by the default GPU), then we try again 
    //    using "warpDisplayAttributes".  This corresponds to D3D11 Feature Level 11_0 on WARP, a D3D11 software rasterizer.
    //
    // Note: On Windows Phone, we #ifdef out the first set of calls to eglPlatformDisplayEXT and eglInitialize.
    //       Windows Phones devices only support D3D11 Feature Level 9_3, but the Windows Phone emulator supports 11_0+.
    //       We use this #ifdef to limit the Phone emulator to Feature Level 9_3, making it behave more like
    //       real Windows Phone devices.
    //       If you wish to test Feature Level 10_0+ in the Windows Phone emulator then you should remove this #ifdef.
    //
#if (WINAPI_FAMILY != WINAPI_FAMILY_PHONE_APP)
    // This tries to initialize EGL to D3D11 Feature Level 10_0+. See above comment for details.
    mEglDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, EGL_DEFAULT_DISPLAY, defaultDisplayAttributes);
    if (mEglDisplay == EGL_NO_DISPLAY)
    {
        throw Exception::CreateException(E_FAIL, L"Failed to get EGL display");
    }
    if (eglInitialize(mEglDisplay, NULL, NULL) == EGL_FALSE)
#endif    
    {
        // This tries to initialize EGL to D3D11 Feature Level 9_3, if 10_0+ is unavailable (e.g. on Windows Phone, or certain Windows tablets).
        mEglDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, EGL_DEFAULT_DISPLAY, fl9_3DisplayAttributes);
        if (mEglDisplay == EGL_NO_DISPLAY)
        {
            throw Exception::CreateException(E_FAIL, L"Failed to get EGL display");
        }
        if (eglInitialize(mEglDisplay, NULL, NULL) == EGL_FALSE)
        {
            // This initializes EGL to D3D11 Feature Level 11_0 on WARP, if 9_3+ is unavailable on the default GPU (e.g. on Surface RT).
            mEglDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, EGL_DEFAULT_DISPLAY, warpDisplayAttributes);
            if (mEglDisplay == EGL_NO_DISPLAY)
            {
                throw Exception::CreateException(E_FAIL, L"Failed to get EGL display");
            }
            if (eglInitialize(mEglDisplay, NULL, NULL) == EGL_FALSE)
            {
                // If all of the calls to eglInitialize returned EGL_FALSE then an error has occurred.
                throw Exception::CreateException(E_FAIL, L"Failed to initialize EGL");
            }
        }
    }
    EGLint numConfigs = 0;
    if ((eglChooseConfig(mEglDisplay, configAttributes, &mEglConfig, 1, &numConfigs) == EGL_FALSE) || (numConfigs == 0))
    {
        throw Exception::CreateException(E_FAIL, L"Failed to choose first EGLConfig");
    }
    mEglContext = eglCreateContext(mEglDisplay, mEglConfig, EGL_NO_CONTEXT, contextAttributes);
    if (mEglContext == EGL_NO_CONTEXT)
    {
        throw Exception::CreateException(E_FAIL, L"Failed to create EGL context");
    }
}
void OpenGLES::Cleanup()
{
    if (mEglDisplay != EGL_NO_DISPLAY && mEglContext != EGL_NO_CONTEXT)
    {
        eglDestroyContext(mEglDisplay, mEglContext);
        mEglContext = EGL_NO_CONTEXT;
    }
    if (mEglDisplay != EGL_NO_DISPLAY)
    {
        eglTerminate(mEglDisplay);
        mEglDisplay = EGL_NO_DISPLAY;
    }
}
void OpenGLES::Reset()
{
    Cleanup();
    Initialize();
}
EGLSurface OpenGLES::CreateSurface(SwapChainPanel^ panel, const Size* renderSurfaceSize)
{
    if (!panel)
    {
        throw Exception::CreateException(E_INVALIDARG, L"SwapChainPanel parameter is invalid");
    }
    EGLSurface surface = EGL_NO_SURFACE;
    const EGLint surfaceAttributes[] =
    {
        // EGL_ANGLE_SURFACE_RENDER_TO_BACK_BUFFER is part of the same optimization as EGL_ANGLE_DISPLAY_ALLOW_RENDER_TO_BACK_BUFFER (see above).
        // If you have compilation issues with it then please update your Visual Studio templates.
        EGL_ANGLE_SURFACE_RENDER_TO_BACK_BUFFER, EGL_TRUE,
        EGL_NONE
    };
    // Create a PropertySet and initialize with the EGLNativeWindowType.
    PropertySet^ surfaceCreationProperties = ref new PropertySet();
    surfaceCreationProperties->Insert(ref new String(EGLNativeWindowTypeProperty), panel);
    // If a render surface size is specified, add it to the surface creation properties
    if (renderSurfaceSize != nullptr)
    {
        surfaceCreationProperties->Insert(ref new String(EGLRenderSurfaceSizeProperty), PropertyValue::CreateSize(*renderSurfaceSize));
    }
    surface = eglCreateWindowSurface(mEglDisplay, mEglConfig, reinterpret_cast(surfaceCreationProperties), surfaceAttributes);
    if (surface == EGL_NO_SURFACE)
    {
        throw Exception::CreateException(E_FAIL, L"Failed to create EGL surface");
    }
    return surface;
}
void OpenGLES::DestroySurface(const EGLSurface surface)
{
    if (mEglDisplay != EGL_NO_DISPLAY && surface != EGL_NO_SURFACE)
    {
        eglDestroySurface(mEglDisplay, surface);
    }
}
void OpenGLES::MakeCurrent(const EGLSurface surface)
{
    if (eglMakeCurrent(mEglDisplay, surface, surface, mEglContext) == EGL_FALSE)
    {
        throw Exception::CreateException(E_FAIL, L"Failed to make EGLSurface current");
    }
}
EGLBoolean OpenGLES::SwapBuffers(const EGLSurface surface)
{
    return (eglSwapBuffers(mEglDisplay, surface));
}

Again, this is taken from the ANGLE template and it’s working need probably not concern you. Again try to see if you can still compile.

Ok so now for the CX (Microsoft’s runtime C++ extensions version of C++ or whatever it’s called)/(The version of C++ with the very funny syntax) wrapper that will provide us with a seamless C# interface. For now we will be using a modified version of the MainPage code found on the ANGLE template. As this page is now actually a C# object, we will have to route the events from C# to C++. The rest of the code can be kept as is in the C++ as to keep things simple.

So now create a class in the shared bridge class and call it GLESSurface. The class will need to be in a namespace with the same name as your project because otherwise you will get errors when trying to compile the C# application. The problem is that this namespace is different for the Windows Store and Windows Phone projects. To fix this we need to add some preprocessor directives. For the store project I added “WIN_STORE” and for the WP one “WIN_PHONE”. Maybe there are already some default ones that would work but I could not find them and just went with these. With these I could then use the following in the header file to define the needed namespace:

#if WIN_STORE
#define MAINNAMESPACE CppGLESBridge_Win
#else if WIN_PHONE
#define MAINNAMESPACE CppGLESBridge_WP
#endif

You will need to change those to whatever your projects are called. Mine are called “CppGLESBridge.Win” and “CppGLESBride.WP”. You should have been able to see what namespace you needed in the default Class1 files. You can also use something like this everywhere in your C++ code where you need to do something slightly differently for WP and Win Store.

Now try compiling to see if everything works. This is also necessary because the Runtime Component needs to be compiled before Intellisense can start to register it when we switch to C#.

If all went well thus far then you will be very close to seeing something about now but we are not there yet. We will only get to Xamarin.Forms at the end of the tutorial. For now we just want to see a coloured spinning cube on all the platforms. So go to your MainPage.xaml files for both Win and WP and add a SwapChainPanel named something like “swapPanel” as can be seen in the screenshots.

Now you need to open the MainPage.xaml.cs for each project. Add the following using statements:

using Windows.UI.Core;
using CppGLESBridge_Win;

The CppGLESBridge_Win is for Win Store and then for WP you should use CppGLESBridge_WP. If you went with some other name then just adapt these appropriately. Next create a private GLESSurface object in both projects. We will be routing the events to this object. You then need to create this object and connect the necessary C# events to the C++ handlers. You want your MainPage() method to eventually contain the following in addition to what was already there:

gs = new GLESSurface(swapPanel);
CoreWindow window = Window.Current.CoreWindow;
window.VisibilityChanged += (CoreWindow sender, VisibilityChangedEventArgs args) => { gs.OnVisibilityChanged(sender, args); };
swapPanel.SizeChanged += (object sender, SizeChangedEventArgs e) => { gs.OnSwapChainPanelSizeChanged(sender, e); };
this.Loaded += (object sender, RoutedEventArgs e) => { gs.OnPageLoaded(sender, e); };

There are screenshots for both the finished Windows Phone and Windows Store files.

If you build and run your projects now you should see a spinning cube.

Step 5: Add Android Platform Support Layer

Picture of Add Android Platform Support Layer

Ok so now we can move on to Android. Start by creating a blank Android C# project. We will also be needing an Android C++ Dynamic Library Project. I would recommend refraining from putting any special characters like a ‘.’ in the name of this project because it might confuse the VS Android toolchain. We then need to reference the Android C++ library in the Android C# application and also add a reference to the Shared C++ renderer project in the Android C++ library project. I would also recommend that you change the API of the C# project to 19 in order to match the C++ one which defaults to 19. You could also just change the C++ project's API level to the newer one but using an older one will give you access to more users if you don’t need any new APIs.

In order to get the shared code working we will need to tell the linker to link to the math and GLESv2 Libraries. In order to do this, add a property page to the Android C++ Library project and call it something like “Universal”. You then need to go to the Input page in the Linker group and prepend “m;GLESv2;” to the “Library Dependencies” field.

You should now be able to get your Android C# application compiled and running, though it won’t show anything fancy just yet. Next we will need to create some C# classes in the Android project. Firstly create a class named MyGLRenderer.

In its own file it should look like this:

using System.Runtime.InteropServices;
using Android.Opengl;
namespace CppGLESXamarin.Android
{
    class MyGLRenderer : Java.Lang.Object, GLSurfaceView.IRenderer
    {
        [DllImport("libAndroidGLESBridge.so")]
        public static extern void on_surface_created();
        [DllImport("libAndroidGLESBridge.so")]
        public static extern void on_surface_changed(int width, int height);
        [DllImport("libAndroidGLESBridge.so")]
        public static extern void on_draw_frame();
        #region IRenderer implementation
        public void OnDrawFrame(Javax.Microedition.Khronos.Opengles.IGL10 gl)
        {
            on_draw_frame();
        }
        public void OnSurfaceChanged(Javax.Microedition.Khronos.Opengles.IGL10 gl, int width, int height)
        {
            on_surface_changed(width, height);
        }
        public void OnSurfaceCreated(Javax.Microedition.Khronos.Opengles.IGL10 gl, Javax.Microedition.Khronos.Egl.EGLConfig config)
        {
            on_surface_created();
        }
        #endregion
    }
}

We will also be needing MyGLSurfaceView which should look like:

using Android.Content;
using Android.Views; using Android.Opengl; using Android.Util;
namespace CppGLESXamarin.Android
{
    public class MyGLSurfaceView : GLSurfaceView
    {
        private MyGLRenderer mRenderer;
        public MyGLSurfaceView(Context context, IAttributeSet attrs) : base(context, attrs)
        {
            //Emulator fix
            SetEGLConfigChooser(8, 8, 8, 8, 16, 0);
            // Create an OpenGL ES 2.0 context.
            SetEGLContextClientVersion(2);
            // Set the Renderer for drawing on the GLSurfaceView
            mRenderer = new MyGLRenderer();
            SetRenderer(mRenderer);
        }
    }
}

Now both of these could be in separate files, one file or even in your main file depending on how you want to structure your code. Please just note that “libAndroidGLESBridge.so” should be the name of the target of your library. You should set this by going to you property page for the Android C++ project and setting the appropriate name in the “Target” field. It should preferably start with “lib”. Next we will need to implement these functions that are being imported into C#.

Go to your C++ Android Library project. Delete the default header because we don’t need it. Replace whatever is in the default .cpp file with the following:

This is just a little piece of C code that allows us to call the necessary methods of a static SimpleRenderer object. Unfortunately it is not possible to access C++ objects directly through P/Invoke (DllImport). You will need a piece of “object agnostic” C code like this to wrap the interface. I recommend that you create these types of wrappers in the Android specific code and not the shared code. Just like you should not put the CX code for Windows in the main shared C++ project. This is just to remove the unnecessary clutter and possible confusion.

Before being able to see anything we will just need to add our new GLES view to the Android application’s UI. So go to “Main.axml” under “layout” under “Resources” in the Android project and add the following bellow the default button in the source view:

<?xml version="1.0" encoding="utf-8"?
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <Button android:id="@+id/MyButton" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/Hello" /> <CppGLESXamarin.Android.MyGLSurfaceView android:layout_width="fill_parent" android:layout_height="fill_parent" android:id="@+id/myGLSurfaceView" /> </LinearLayout>

If you compile and run you should now see a nice spinning Cube.

Step 6: Add IOS Platform Support Layer

Unfortunately I do not own a Mac yet so I cannot actually implement the iOS part of this tutorial but when I do get one I will. It also does not seem as if though Visual Studio has very proper support for iOS C++ library development just yet. It does of course support it as of VS 2015 RC but unfortunately it seems that a lot of functionality, including Xamarin support if I am not mistaken, has not been implemented yet. Anyone who has access to a Mac should of course be able to achieve the end goal already by compiling the C++ library with XCode and then just DllImporting in Xamarin.iOS. You will though through this route not get the advantage of a Visual Studio shared project.For more info on using DllImport on Xamarin.iOS please check:

http://developer.xamarin.com/guides/ios/advanced_t...

I can already tell you in broad terms what you will have to do for iOS. Firstly off course you will need to put the SimpleRenderer class in a static library project in XCode. You will aslo need the little C wrapper like the one that we used for Android. You should then compile this library for all possible configurations and copy the resulting files to your C# project. Finally you will have to set it up to use the correct library for the correct configuration.

You will then need to create a GLKView in the iOS UI, and then give it a simple renderer which forwards the needed calls from C# to C++ in exactly the same way as the Android one does. Please check the following links and try to adapt the necessary code to C#:

https://developer.apple.com/library/ios/documentat...

http://iosapi.xamarin.com/index.aspx?link=T%3AMono...

http://www.learnopengles.com/calling-opengl-from-c...

Step 7: Add Xamarin.Forms Support

Picture of Add Xamarin.Forms Support

So now that we have C++ GLES working in a C# application, we can go on to doing it with Xamarin.Forms. To get started, add the Nuget package for Xamarin.Forms to all 3 yout C# projects. You do this by right-clicking on the project, selecting "Manage Nuget Packages", searching for "Xamarin.Forms" and then installing it.

According to Xamarin, support for Win 8.1 an WP 8.1 is still in its early stages but hopefully they will have it ready soon.

Unfortunately, at the time of writing, the Xamarin.Forms Nuget package for Android does not want to install on Visual Studio 2015 Community Edition with Xamarin Business edition trial. Firstly VS crashes whilst installing and when you reopen the project it appears to be installed but you start getting strange "linking" errors at compile time that can only be fixed by removing all the references to Xamarin.Forms. Once this is fixed I will try to get this project working with Xamarin.Forms and either extend this tutorial or make a new one.

If I forget to add the info myself and this problem gets fixed, please check the following pages as they should give you almost all the info you need to get it working yourself:

http://developer.xamarin.com/guides/cross-platform...

http://developer.xamarin.com/guides/cross-platform...

http://developer.xamarin.com/guides/cross-platform...

Here's hoping it gets fixed and I remember...

Also, if you want to get this working right now, there is another way that I will only discuss in broad terms. You will firstly move this solution to VS 2013 or Xamarin Studio and maybe redo the Android application there. You will then have to move the code for and delete (or just remove) the Android C++ Library project because VS 2013 and Xamarin Studio will not recognize it. You should move the code to something like Android Studio. You can then compile the .so library files there and copy them to your new project. You will probably need to just look online a bit on how to do this properly because you will need a different one for each ABI. I would reccomend reading the following:

http://developer.xamarin.com/guides/android/advanc...

https://richzwaap.wordpress.com/2014/08/07/pinvoke...

For those of you who might be wondering, I have actually even tried installing the bleeding edge pre-release Xamarin components (from base system all the way to the Nuget package) and have had not luck yet.

Step 8: Use Templates

Ok so for the lazy ones amongst you, you can download my template solution on GitHub:

https://github.com/Gerharddc/CppGLESXamarin/tree/m...

This will also work for anyone that has a problem with getting their setup working. You can compare what you currently have to my template. I actually recommend copying the source from these templates as the Instructables editor has proven to be a nightmare when it comes to coping code from Visual Studio. As you might have seen the formatting is quite awkward and I haven't been able to fix it yet. I might also have forgotten to mention something in the tutorial. If you find any problems with it just leave a comment or send me a mail.

I have tried to create a proper VS template for this like you get with ANGLE, trust me I HAVE tried, but unfortunately I have not been able to figure out how to get it working with property pages. I have attached what I currently have for anyone who want's to try and fix it but it does not correctly work for anyone who just want's to use it. Just copy the GitHub project instead if that's the case...

Comments

seamster (author)2015-05-17

Wow, such a thorough first instructable. Well done!

I hope we see more from you soon. Keep up the good work! :)

@seamster Thanks, appreciate the positive feedback :)

About This Instructable

3,995views

14favorites

License:

Bio: I'm a high school student who is very interested in tech. I have been experimenting with the art of programming since the age of ... More »
More by Gerhard de Clercq:C++ OpenGL ES Viewer in C#
Add instructable to: