I needed to move a C program to Rust. My first instinct was the easy one: hand it to a model, chunk by chunk, and review the output.
Then I checked whether a tool already existed. c2rust, from Immunant, does exactly this. On the 73,095 lines of doomgeneric it took 23.2 seconds.
The interesting part is not the speed. It is that the model still ended up doing real work, just nowhere near the translation.
Cost was not the argument, and pretending otherwise would be selling smoke
I measured the token cost of translating DOOM with a model using the o200k_base tokenizer: 550,595 input tokens and 1,896,319 output. That is a few dollars. It is not a reason.
What actually decides is three other things:
-
Reproducible. Same input, same output file, byte for byte, every run. You can
diffit. - Systematic errors. The translation produced four bugs. Two were the same bug in 23 places. One class gets fixed once.
- All of them failed loudly. Every one crashed at compile time or on the first frame. A scattered, silent error in one of 143,911 lines costs more to find than the whole translation.
How you prove a translation did not change the program
DOOM ships recorded demos. Every gameplay decision is a fixed-point calculation, so if one of them differs the playback desynchronizes and the tic count stops matching. That is the first gate, and it is free.
The second gate: I hashed every frame the engine hands to the display, in both builds. 11,113 frames, one differed.
I nearly published that one frame as c2rust's single error. The control run is what saved me: I ran the original C binary against itself. It differs from itself too, on the same pixel. Original DOOM reads one pixel of uninitialized memory, and the translation reproduced that faithfully.
If you only ever compare against one run, you will report your own reference's noise as the other side's bug.
The translated Rust is not safe Rust
Measuring lines inside unsafe blocks, the translated zlib puts 47% of the crate in unsafe, against 31% for zlib-rs, a rewrite done by people.
Counting occurrences of the word unsafe gives you the opposite answer, and it is a misleading metric: the translator wraps whole functions in a single block, so it scores better on a count while being worse in reality.
These are not comparable artifacts anyway. zlib-rs is a rewrite, with redesigned structures and the type system doing work. An automatic translation cannot do that. Its goal is to not change the program.
Where the model actually earned its place
Not translating. Generating detail that modulates on top of what the engine already computes.
Every texture goes through an upscaler, then gets stored as a one-channel map whose mean is exactly 128 per texel. The renderer keeps deciding color and lighting; the map only contributes variation within the texel:
color_final = color_doom × detail / 128
Because the mean is anchored at 128, the enhancement provably cannot shift the image. Averaged over any texel, it multiplies by 1.
And that same map is a height map, so its gradient gives per-pixel relief for free, with no extra bytes, baked at pack time.
That constraint, that the engine keeps deciding, is what made the result add instead of ruin.
Both versions are playable in your browser
The 1993 C build and the Rust translation are compiled to WebAssembly and running side by side, plus the enhanced renderer at 640×400 with per-pixel lighting.
The 60-second version of all of this, with the game running behind it:
The full write-up has the four translation bugs, the three portability ones, the memset that silently cleared half the screen after widening a struct field, and how it went from 7215 to 3009 realtics without rewriting the game:
Top comments (0)