Chapter 9: Hacking OpenTK

This chapter contains instructions for people wishing to modify or extend OpenTK. It describes the project structure, wrapper design, coding style and various caveats and hacks employed by OpenTK to achieve wider platform support.

Project Structure

The OpenTK project consists of a number of managed, cross-platform assemblies:

  • OpenTK: this is the core OpenTK assembly. It provides the Graphics, Audio and Compute APIs, the math toolkit and the platform abstraction layer.
  • OpenTK.Compatibility: this assembly provides an upgrade path for applications compiled against older versions of OpenTK and the Tao Framework. When a deprecated method is removed from core OpenTK, it is added to this dll.
  • OpenTK.GLControl: this assembly provides the GLControl class, which adds OpenGL support to System.Windows.Forms applications.
  • OpenTK.Build: this assembly provides the cross-platform build system for OpenTK. It can be used to generate MSBuild-compatible project files for use with Visual Studio (version 2005 or higher), Sharpdevelop (version 2.0 or higher) and MonoDevelop (version 2.0 or higher).
  • OpenTK.Examples: this assembly provides a number of samples built with OpenTK. It covers topics related to OpenGL, OpenGL|ES, OpenAL, OpenCL and general OpenTK usage.

In addition to these assemblies, OpenTK maintains a custom binding generator which generates the OpenGL, OpenGL|ES and OpenCL bindings. It consists of two assemblies:

  • Converter, which converts the OpenGL|ES and OpenCL C headers to XML files.
  • Bind, which converts the OpenGL .spec files or the Converter XML files into C# code.

Finally, OpenTK provides a QuickStart project, which shows how to setup and build an OpenTK application.

OpenTK Structure

The OpenTK solution provides the following public namespaces:

  • OpenTK: contains classes to create windows (GameWindow, NativeWindow), perform 3d math, interact with the monitor (DisplayDevice, DisplayResolution) as well as query the platform configuration.
  • OpenTK.Graphics: contains bindings for OpenGL and OpenGL|ES.
  • OpenTK.Audio: contains bindings for OpenAL.
  • OpenTK.Compute: contains bindings for OpenCL.
  • OpenTK.Input: contains classes to interact with input devices (Keyboard, Mouse, Joystick).
  • OpenTK.Platform: contains classes to extend OpenTK or interact with the underlying platform.

The public API of OpenTK is completely cross-platform. All platform-specific code is contained in internal interfaces under the OpenTK.Platform namespace. In that sense, most public classes act as fa├žades that forward method calls to the correct platform-specific implementation.

public class Foo : IFoo
{
    IFoo implementation;
 
    public Foo()
    {
        implementation = OpenTK.Platform.Factory.Default.CreateFoo();
    }
 
    #region IFoo Members
 
    public void Bar()
    {
        implementation.Bar();
    }
 
    #endregion
}

This pattern is used in all public OpenTK classes that need platform-specific code to operate: DisplayDevice, DisplayResolution, GraphicsContext, GraphicsMode, NativeWindow and the various input classes.

Classes that do not rely on platform-specific code and classes that contain performance-sensitive code do not use this pattern: the various math classes, the OpenGL, OpenCL and OpenAL bindings, the AudioContext and AudioCapture classes all fall into these categories.

Generating bindings

This is a rough overview of the binding generation process.

  1. Convert.exe converts .spec files into a custom xml file, called signatures.xml.
  2. The generator loads signatures.xml, overrides.xml and gl.tm into a custom in-memory representation.
  3. The generator then applies a number of hard-coded rules. These include stripping the prefixes and suffixes of functions, escaping for tokens that start with digits, generation of safe/unsafe overloads for functions taking pointers and handling of CLS-compliance.
  4. Finally, it generates compilable code for C# or C++.

Several command-line switches can influence the generation process: GL vs ES, namespaces, generated code.

What is the best way for you to proceed depends on (a) the amount of functions you wish to add, and (b) whether these functions are available in the old .spec files or the new xml format.

If you only wish to add a handful of functions, the easiest approach might be to define them by hand at the bottom of the signatures.xml file, which can be found under Source/Bind/Specifications/GL2. The format is relatively simple:

<function name="VertexPointer" extension="Core" category="VERSION_1_1_DEPRECATED" version="1.1" deprecated="3.1">
      <returns type="void" />
      <param name="size" type="Int32" flow="in" />
      <param name="type" type="VertexPointerType" flow="in" />
      <param name="stride" type="SizeI" flow="in" />
      <param name="pointer" type="Void*" flow="in" />
    </function>

In this case, VertexPointerType is an enumeration that must be defined in either signatures.xml or overrides.xml. If this enumeration is not found, the generator will fall back to the "All" enumeration, which is equivalent to the non-typesafe GLenum used in the regular C headers.

If you wish to bind a significant amount of functions, such as a whole new OpenGL version, then your best bet is to execute the Converter utility to generate a new signatures.xml file from the latest spec:

cd Binaries/OpenTK/Debug
wget https://cvs.khronos.org/svn/repos/ogl/trunk/doc/registry/public/oldspecs/gl.spec
Convert.exe -p=gl -v=4.3 -o=../../../Source/Bind/Specifications/GL2 gl.spec
Bind.exe

Now you can recompile OpenTK and take a look at the generated specs - the new functions should be there. If they use enums, these will probably appear as "All". You can use overrides.xml to define type-safe enums and improve the generated API.

gl.tm

File generated by Convert.exe

overrides.xml

Xml file generated by Convert.exe

signatures.xml

The generated XML files follow this schema.

<signatures>
  <add>
    <function name="[function name]" extension="[Core|extension]" profile="[profile name]" category="[category]" version="[1.0|1.1|2.0|...]">
        <returns type="[typename]" />
        <param type="[typename]" name="[parameter name]" />
        <param type="[typename]*" name="[parameter name]" count="[array size]/>
    </function>
    <enum name="[enum name]">
        <token name="[token name]" value="[token value]" />
        <use enum="" token="" />
    </enum>
 </add>
</signatures>
 
<overrides>
    <!-- Todo -->
</overrides>

Notes

Functions:

  • name should not contain a prefix, i.e. BufferData instead of glBufferData.
  • extension must be set to "Core" if this is not an extension method. Otherwise, it must be set to extension name in CamelCase. For example, method MapBufferOES should set extension="Oes".
  • [profile name] can be used to discriminate between different profiles of the same spec (for example "full" and "lite" for ES1.1). This attribute is not used at this point.
  • [category] should be set to the correct function category. This is typically defined for extension methods (e.g. TexImage3DOES belongs to category GL_OES_texture_3D). If the category is unknown, this should be set to the same value as the "version" attribute below.
  • [version] must be set to the correct spec version. OpenGL|ES and OpenCL distribute different header files for each spec version, so this can be set to a constant value (e.g. 1.0 for ES1.0). On the other hand, OpenGL and OpenAL distribute a single file that contains functions from all versions.

The extension attribute is used by the generator to distinguish between core and extension methods (the first use plain DllImports, while the latter are only converted to delegates).

The category and version attributes are used by the generator to match enum parameters. An enum parameter may either define an exact type or may be a generic enum (GLenum). In the last case, category and version are used to find a matching enum. If no match exists, the enum "All" will be used.

Parameter typenames are translated to C# as follows: [typename] -> gl.tm -> csharp.tm. The gl.tm typemap file is shipped by Khronos and matches GL types to C types (this file should not be edited). The csharp.tm typemap file is handwritten and maps C types to C# types (this file may be edited).

Typenames that resolve to csharp strings or string arrays are treated specially by the generator for the purposes of marshalling. For this reason, byte* parameters that contain ASCII strings should be overriden by char* or CharPointer parameters.

Enums:

  • [enum name] should be a valid C# enum name in CamelCase.
  • [token name] should be a valid C# enum token in ALL_CAPS. The generator will translate this to camel case.
  • [enum value] may be a hex or dec number, or a string that refers to a different enum token. The generator will recursively resolve token references and will parse the final values to ensure they are well-formed numbers.

Wrapper Design

OpenTK provides .Net wrappers for a various important native APIs: OpenGL, OpenGL ES, OpenAL and OpenCL (in progress). Unlike similar libraries, OpenTK places an emphasis in usability and developer efficiency, while staying true to the nature of the native interface. To that end, it utilizes a number of .Net constructs that are not available in native C by default:

  • Strongly-typed enum parameters instead of integer constants.
  • Generics instead of void pointers.
  • Namespaces instead of function prefixes ('gl', 'al').
  • Function overloads instead of function suffices (Vector3 instead of Vector3f>, Vector3d, ...).
  • Automatic extension loading.
  • Inline documentation for functions and parameters, accessible through IDE tooltips (intellisense).
  • CLS-compliance, which makes the bindings usable by all .Net languages.
  • Cross-platform support, which allows the bindings to be used by any platform supported by .Net or Mono.

The bindings are generated through an automated binding generator, which converts the official API specifications into C# code. The following pages describe the generation process in detail.

Official API specifications: