Friday Feature: In The Beginning… Tesla.Interop

FridayFeature

Allow me to introduce the Friday Feature (yes I know, it is Sunday – I’m late!). Every week, on Friday (or around the weekend), I will try and push myself to write up something about the next incarnation of the engine – Tesla Engine v2. Fancy right? This is the first such in the series and I thought it would be apropos to start with the basis of the new engine.

The engine is now employing some snazzy IL bytecode injection. Its a concept that’s used by the SharpDX developer. So the credit goes entirely to xoofx. The idea is to replace stubs with IL bytecode that is not accessible from C# – memcpy for the biggest example. In the SharpDX’s developer’s words “bring some of C++/CLI bytecode to C#”.

What does this give us? We get fast interop methods that can work on generic structs and copy/clear unmanaged memory. Not just for managed to unmanaged, but unmanaged to unmanaged. All without using the C# Marshal class or C++/CLI. We also get this functionality, while remaining  an AnyCPU .NET assembly.

I’m also employing this technique in AssimpNet. For that project, the IL injection is used for marshaling unmanaged Assimp data structures to managed memory and vice verse for exporting. Not only has the technique made some things easier to develop (greatly simplified managed to unmanaged marshaling), but I’ve found it to be a lot more performant.

Now what about Tesla v2′s requirements? There’s quite a few, which I’ll highlight in a list:

  • The math library has received many new types (vectorized bools, ints, other new structs like a strongly typed Angle). Previously many of the engine interfaces (namely, the content ones) would have a Write/Read method specifically for a Vector3, Vector4, Color…etc. The addition of new types made this pattern undesirable from a maintenance perspective. Clearly, a more generic way was needed.
  • DataBuffer<T> was entirely backed by managed memory. This potentially was a problem with the Large Object Heap due to fragmentation (not so much of an issue in 4.5, but still). It made sense to have some sort of “raw, native buffer” to keep pressure off the GC, as well as speed up the DataBuffer’s core behavior: Treat any type generically as a bunch of bytes (like Stream), and be fast about it.
  • The Effect Framework, or at least the Direct3D11 implementation of it was going to have a similar method-type explosion due to the math library. Also, the new effects framework would be implemented purely by me – no more Microsoft effects. I wanted to be able to have a CPU memory buffer representing a constant buffer, be able to write into that (generically, treating everything as a bunch of bytes), and then blit that to the GPU constant buffer.

Those were some of my biggest use cases, and the common theme was the data buffer case: A uniform and consistent way to treat any type generically as a bunch of bytes (like Stream), and be fast about it. Thus the Tesla.Interop.dll assembly was born. It is a stand-alone assembly intended for general purpose use, all other engine assemblies rely on it. So you can actually use it in your own project, if you so choose without using the engine! I like this a bit better than the approach SharpDX took (its entirely a tool + main project), my approach is using a tool as well as a single, small assembly (separate from the main project) that can be used out of the box.

Of Man and Interop Generators

So at the heart of the interop assembly is an internal class called InternalInterop, it looks a little like this (well, exactly like this):

internal static class InternalInterop {

    public static unsafe void WriteArray<T>(IntPtr pDest, T[] data, int startIndex, int count) where T : struct {
        throw new NotImplementedException();
    }

    public static unsafe void ReadArray<T>(IntPtr pSrc, T[] data, int startIndex, int count) where T : struct {
        throw new NotImplementedException();
    }

    public static unsafe void WriteInline<T>(void* pDest, ref T srcData) where T : struct {
        throw new NotImplementedException();
    }

    public static unsafe T ReadInline<T>(void* pSrc) where T : struct {
        throw new NotImplementedException();
    }

    public static unsafe int SizeOfInline<T>() {
        throw new NotImplementedException();
    }

    public static unsafe void MemCopyInline(void* pDest, void* pSrc, int count) {
        throw new NotImplementedException();
    }

    public static unsafe void MemSetInline(void* pDest, byte value, int count) {
        throw new NotImplementedException();
    }
}

As you can see, there’s two types of stubs – a XXXInline and XXXArray. The inline methods are replaced with the actual IL bytecode at the point where ever the method call is found in the processed assembly. The array ones are slightly trickier where, whatever method body is calling into it is replaced entirely. For example, take this Write<T> method from the MemoryHelper, which you may remember being in the Tesla.Util namespace (it has moved to Tesla.Interop):

public static unsafe void Write<T>(IntPtr pDest, ref T data) where T : struct {
    InternalInterop.WriteInline<T>((void*) pDest, ref data);
}

The generator will replace that entire method body of Write<T>, which is something to be aware of if you’re using the generator yourself. After the processing is complete, the interop stub class is stripped out of the assembly. While the code for Tesla v2 is not yet available, you can actually use this generator as it is now included (source code) in the AssimpNet trunk. The dependency (and magic) behind the IL injection is the Mono.Cecil library, which is perhaps one of the coolest libraries I’ve ever used.

One thing to note, the generator is actually a separate project from Tesla.Interop, specifically its Tesla.Interop.Generator. It runs as a post-process task after Tesla.Interop builds, to patch that assembly with the IL bytecode.

Behind Door Number 2…

The interop generator is only half of the puzzle. The big tamale of the interop assembly isn’t just the memory helper, but two native buffers called RawBuffer and SafeRawBuffer. Very similar to SharpDX’s “DataBuffer” (not to be confused with my DataBuffer<T>), these are wrappers around an unmanaged chunk of memory that allows for generic read/write methods taking in either a struct or an array of structs. It also allows direct access to its memory pointer, and can either use allocated unmanaged memory (AllocHGlobal) or pin a managed array. Fairly straight forward stuff.

The difference between the non-safe and safe buffers is that the safe one provides argument checking to prevent access violation exceptions, where the non-safe version is completely bare bones. That’s because my use case for databuffers/effect constant buffers is to use this raw buffer wrapped inside a higher level object where I’m already doing some other sort of range checking. That will probably be the subject of my next post (friday feature or not), as there are some significant changes to the engine’s most common piece of functionality: The DataBuffer<T>, or really I should say, the IDataBuffer/IDataBuffer<T>.

So there you have it! The work for the interop assembly kicked off the development cycle on Tesla v2 last November (2012), and it’s certainly changed how I’ve been approaching things in the design/implementation of the next engine version dramatically.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Unable to load the Are You a Human PlayThru™. Please contact the site owner to report the problem.