Posted in:

Over my career I've done a lot of porting of applications or algorithms from one language to another. Sometimes this has just been as a learning experience, whilst at other times I've wanted to make a DSP algorithm implemented in C++ available in a fully managed .NET environment.

Obviously, porting code is something that in theory LLMs ought to be fairly good at. Its tiresome, repetitive work, with a few nasty gotchas along the way. And with the release of Google Gemini Pro 2.5 this week I thought I'd give it a couple of challenges.

Challenge 1 - Porting a Game

Back in the day I spent many hours playing the classic QBASIC Nibbles game that was bundled with DOS. And I've used it a few times as a good learning exercise, porting it to both WinForms and Silverlight many years back.

Porting a game has unique challenges as you have to find equivalent implementations for handling graphics, timers and keypresses on the new platform you're running on, but the basic game logic is much more straightforward to move across.

The original source code for Nibbles is available.

I tried using Google AI studio with the Gemini 2.5 Pro model and gave it the following prompt:

Here is the original Microsoft QBASIC Nibbles source code. I want the same game with same gameplay and graphics but to run in a browser. Please make this as a single page HTML including CSS and JavaScript.

What it produced after 3 minutes of thinking wasn't exactly polished, but it was a functional game, responding to keypresses, and rendering mostly correctly, albeit with a few visual glitches here and there.

Porting with LLMs

It's quite impressive that you can essentially "one-shot" tasks like this, although clearly it could do with a round of followup fixes. And of course porting this way completely defeats any learning benefits that would come from doing it manually. Unfortunately there's still no replacement from doing things the slow and hard way if you want to learn.

Challenge 2 - Porting some DSP code

I wanted to give it a more difficult challenge. I've ported all kinds of DSP algorithms over the years, such as the NLayer managed MP3 decoder. These projects are not really done as "learning" exercises - they are simply because I want to access a particular piece of code in my language of choice. It would be absolutely great if LLMs could do this for me.

I decided that a good challenge would be to re-port the Cockos WDL Resampler to C# which I did manually many years back as part of NAudio.

I felt that it would be fair to give the LLM a little bit more guidance on what I was expecting so my prompt gave some details of C# capabilities that I wanted. I was particularly interested to see if it would know what to do with the SSE instructions and whether they could get converted into hardware intrinsics.

I want to port this audio resampler from C++ to C#. The resulting C# code should remain as close as possible to the original implementation but use idiomatic C# naming for classes and methods. There should be a focus on performance so use things like Span<T> and hardware intrinsics where necessary. Where it calls into standard library functions - make calls to their C# equivalents or implement yourself if its missing. If it calls any WDL functions that you can't work out what they do, list them along with the output so that I can provide the C++ implementation to be ported.

Again it required several minutes of thinking time, and produced a plausible-looking port of the code.

I liked that it did a good job of understanding what methods it didn't see the source code would do, and implemented them.

// Missing WDL Functions to be potentially implemented or substituted:
// - denormal_filter_double(double val): Filters denormal numbers. Can possibly use a check and set to zero.
// - WDL_denormal_ftz_scope: Manages denormal floating-point behavior (Flush-To-Zero). .NET Core 3.0+ usually has FTZ enabled by default for SSE, but explicit control isn't standard. Might be omitted or use platform invokes if absolutely needed.
// - wdl_max(a, b): Equivalent to Math.Max(a, b).
// - WDL_NOT_NORMALLY(condition): Seems like a hint for unlikely conditions, possibly for compiler optimization. Can be replaced with simple 'if (condition)'.
// - WDL_FIXALIGN: Attribute for alignment. C# struct layout might provide some control, but direct field alignment isn't guaranteed like C++. Often handled implicitly by the runtime.
// - WDL_TypedBuf<T>: A dynamic buffer. We'll replace with T[] and manual resizing (Array.Resize). The GetAligned(16) part is relevant for SSE; we'll use unaligned loads/stores or ensure alignment manually if critical.

Gemini 2.5 Pro is also a thinking model, so reading its explanation of how it went about porting was fascinating. Here's a snippet of its reasoning, explaining how it plans to handle tricky challenges such as C++ templates, and SSE intrinsics.

  • WDL_TypedBuf<T>: This looks like a resizeable buffer, possibly handling alignment. C#'s List<T> or T[] with Array.Resize can work. Since performance is key and we deal with Span<T>, a simple T[] managed manually seems appropriate, similar to how Resize works in the C++ WDL_TypedBuf example. Need to handle the alignment part (GetAligned(16)) if SSE is used.
  • Templates (SincSample*): C# generics are less powerful than C++ templates for this kind of compile-time specialization based on constants (like channel count). We'll likely need separate methods or conditional logic within a single method (SincSample, SincSample1, SincSample2). The template type parameters T1 and T2 seem to be WDL_ResampleSample and WDL_SincFilterSample, respectively. Since these are fixed at compile time in the C# version (likely double and float or double), we don't need generics for the types.
  • SSE Intrinsics: C# has System.Runtime.Intrinsics.X86. We need to map the _mm_* functions. This requires checking for Sse2.IsSupported at runtime. The C++ code uses #ifdef WDL_RESAMPLE_USE_SSE. We'll use runtime checks. The SSE code often processes pairs of samples/coefficients; this structure needs to be replicated using Vector128<double> or Vector128<float>. Mapping specific intrinsics:
    • _mm_setzero_pd() -> Vector128<double>.Zero
    • _mm_loadu_pd() -> Sse2.LoadVector128() (unaligned)
    • _mm_load_pd() -> Sse2.LoadVector128() (aligned - need to ensure alignment or use unaligned load)
    • _mm_set_pd() -> Vector128.Create()
    • _mm_mul_pd() -> Sse2.Multiply()
    • _mm_add_pd() -> Sse2.Add()
    • _mm_unpackhi_pd() -> Sse2.UnpackHigh()
    • _mm_store_sd() -> Sse2.StoreScalar()
    • _mm_cvtps_pd() -> Sse2.ConvertToVector128Double()
    • _mm_load_sd() (loading float, converting) -> Load float scalar, Vector128.CreateScalar().AsDouble(), then maybe Sse2.ConvertToVector128Double() if needed? Or more likely load float pair, convert. Needs careful checking. _mm_castpd_ps is just a type cast, handled by C# types. Let's re-examine _mm_load_sd((double *)fptr) - this loads 64 bits (a double) from a float pointer. It likely intends to load two floats and convert the first. _mm_load_ps seems more appropriate if loading floats. Let's assume the C++ meant to load floats and convert. _mm_load_ps loads 4 floats. _mm_load_ss loads 1 float. The (double*) cast is suspicious. Let's stick to the intent - load floats, convert to doubles for processing. Sse.LoadVector128(float*) followed by Sse2.ConvertToVector128Double().
    • Alignment: The C++ code uses GetAligned(16). For C#, we can try to allocate aligned arrays using OS-specific APIs or rely on the GC potentially aligning larger arrays. For Vector128, unaligned loads (LoadVector128) are often safe and performant enough on modern CPUs, avoiding alignment complexities. Let's default to unaligned loads unless profiling shows a major issue.

I was interested to see if it would compile first time, but there were unfortunately a few errors. It quickly resolved them, but it didn't leave me feeling exactly confident, as just one small mistake with code like this can result in completely useless results, with very little clue as to where the problem is.

Of course, testing the code was another matter. It wasn't quite the drop-in replacement I had hoped for for my original WdlResampler implementation, and after a bit of experimenting, I realised that the LLM had chosen to use doubles rather than floats as the default type which wasn't a good fit for my consuming code. I will need to go back round this process again, and explain to the LLM in more detail that I need 32 bit floating point support.

Despite the first round not getting me all the way there, what I learned from this exercise is how much of a head-start an LLM can give you on a tricky port like this. I certainly wouldn't have felt confident trying to convert the hardware intrinsics myself.

To complete this port, I would almost certainly need a good way of testing it, comparing the behaviour of the original code to the new code. This again is where LLMs can save us a lot of time, by generating a set of tests that can be run against the original and ported code and detecting any discrepancies. If I decide to push through with this to a fully working port, that will definitely be something I try.

Agents to the Rescue

And this brings us to the realization that porting a complex algorithm is rarely a single-step process. It's nice if an LLM can "one-shot" it, but if I was to design an AI "agent" to implement a porting task I'd want it's workflow to be something like this:

  1. Come up with a plan for the port - e.g. which methods or files to do first, what naming and tech changes are required
  2. Define a recipe for how the port should be done - to ensure consistent use of the language features and techniques you want to use. In my case, favouring the use of Span<T> for example
  3. Create a characterization test suite around the original code - capture how it behaves in various scenarios
  4. Actually perform the port of the code, chunking it into right-sized pieces that the LLM can handle in a single shot
  5. Compile the code and fix build errors
  6. Apply the same tests to the ported code and verify that it behaves similarly.

My version of GitHub Copilot doesn't yet have the "agent" mode available, but I expect it won't be long, and I'd like to try this out once it's available.

My quick experiment has clearly shown we are not yet at the place where LLMs can flawlessly convert a source code file from one language to another. But it is still incredible how far we've come - even the rough and ready first-draft of the resampler port would have been a huge timesaver for me back in the day. And perhaps agents iterating with a plan like the one I suggested above will expand the scope of what can be ported even further.

For example, there's an amazing conversion of the Opus codec to .NET, but its out of date now and the original author understandably does not have the time to bring it back into sync with the latest version of the original. Again, this monumental task is something that I can imagine will become possible in the near future.

I'd love to know if any of you have had success porting a non-trivial codebase from one language to another with LLMs. What challenges did you face doing it, or did it go smoothly?