/x+/tensiondraw
TensionDraw
The last unmatched function in Guilty Gear X Plus Open
Status · 2026-10-25
16 / 379
instruction words in TensionDraw still differ from the disc
Open: one function. Both efforts have reported, and neither has matched it yet.
One function left
X Plus has 3,614 of its 3,615 game functions rebuilt from C, on both the Japanese and the Korean disc, and both full rebuilds come out byte-identical to the discs. This round started at 3,501. The one left is TensionDraw.
What matching means
A PS2 game ships as machine code: the processor's instructions, with the original C source long gone. A matching decompilation writes new C that, run through the same compiler the developers used, produces those instructions again, byte for byte. Close doesn't count. If one instruction differs, the function isn't matched.
That strictness is the point. A match proves the C does exactly what the game does, with nothing guessed.
What TensionDraw does
It draws the Tension gauge at the bottom of the screen: the meter that fills as you press forward and pays for Overdrives, Roman Cancels and Faultless Defense.
The game calls it once for each player, every frame. It asks how much Tension the player has, scales that to the length of the bar, and draws the bar growing toward the middle of the screen, so player 2's runs the other way. The colour changes from blue to red as it fills, and again when it's full. The shimmer along the bar moves at a speed set by the player's hidden Tension balance. A DANGER warning appears when a player is close to a Negative Penalty, and in Instant Kill mode the function draws that mode's red bar instead.
Why one function can hold out
A compiler makes thousands of small decisions: which of the processor's registers holds each value, and in what order to do the work. Those decisions follow from how the C is written, often from details that don't change what the code does: the order of two lines, whether a value gets its own variable, which way round a comparison reads. Usually a few rewrites land on the original. For 3,614 functions, they did.
TensionDraw is about 1.5 KB of code, 379 instruction words on the Japanese disc. Ours matches everywhere except one short block, right after the function asks for the player's Tension. There our instructions do the same jobs, but the compiler put values in different registers and placed two loads later than the original. Sixteen words differ.
Sixteen words out of 379
- Each slot in the long strip is one instruction word of TensionDraw on the Japanese disc. Red marks the 16 that still differ.
- Fifteen sit in the block right after the call that reads the player's Tension. The sixteenth is a later test of the same value, which lands in a different register.
The investigation
The first passes were what usually works: hundreds of rewrites of the C, then a tool that shuffles the code at random and keeps whatever gets closer. The best stayed at 16.
Next we modelled the part of the compiler that hands out registers, in Python, until it reproduced our build's registers exactly. Then we let it try every order the compiler could legally pick for that block, about ten million of them. None gave the disc's registers. Reordering alone can't explain the difference: the original C must have given the compiler something slightly different to work with.
A model is only as good as its assumptions, so the third pass went inside the real compiler. A small add-on, loaded into the original compiler program while it runs, can force an order onto the block or slip in a hidden temporary value, then hand everything back to the compiler's own register allocator to finish. Each experiment takes about 75 milliseconds, and with nothing forced the output is identical to a normal build. Guesses became experiments.
The fourth pass found a combination that gives every one of the nine registers the disc uses in that block: the three conditions below. It also turned up a clue. Two constants that the compiler normally merges into one stayed separate in the original. That only happens if a conditional move, a single instruction that picks one of two values without a branch, was created late, after the compiler's last merging pass.
The missing statement on line 252
The Korean disc kept its debug data, which records the source line every instruction came from. In TensionDraw, line 252 sits between two lines that produce code, and it owns no instructions at all. Something was written there and compiled down to nothing. A statement that leaves no code can still change how the compiler hands out registers before it disappears.
So we tried about 380 candidates for line 252: unused assignments, empty ifs, a variable set to itself, and more. Most change nothing. One family, an unused decimal (floating-point) value set inside an if, leaves no code behind and still moves the registers, the first natural way found to meet one of the conditions. Overall it moves them the wrong way, so the search went on.
The three conditions, in plain words
With all three forced inside the compiler, every register in the block matches the disc.
- The -1 stands on its own. The bar needs a direction: +1 for player 1, -1 for player 2. The -1 has to be its own temporary value, and the choice has to read "if this is player 1, use 1, otherwise -1", not the other way round. Then the compiler sees that a register already holds -1, drops the extra step, and leaves a single conditional move, as on the disc.
- The 1 belongs to the whole function. The compiler hands out registers in two rounds: a quick one for values that live inside one stretch of code, then a function-wide one for the rest. The constant 1 has to go in the second round. That's what puts it in the register the disc uses.
- The copy comes late. The function keeps a copy of the Tension value. In the original, that copy is made after the multiply that scales the bar, not straight away. The register holding the value stays busy longer, which pushes other values elsewhere.
Forcing all three gets the registers right but leaves two instructions in the wrong order. A different forced setup gets the exact order with two registers wrong. What's missing is ordinary C that sets this up on its own, the way the original code did.
The fifth pass: where the conditional move comes from
The fifth pass read the compiler's own source to find out exactly when a conditional move can appear.
- Only one pass of the compiler, jump optimization, creates conditional moves, and it always gives both constants fresh temporaries of their own.
- A later clean-up pass folds that separate -1 straight back into the direction value. That's why every way of writing "if this is player 1" collapses to what we already had.
- The one new finding: asking the same question twice, in a doubled and redundant choice, makes the conditional move appear late, in exactly the three-separate-constants shape the clues point to. Then the compiler's second merging pass folds it back, and it still lands 16 words off.
- The last idea from this pass: write it so the second merging pass, not the first, is the one that changes a jump, leaving nothing to fold the constants back together. GPT-6.1-sol later got the conditional move made that late, but a separate clean-up pass still folds the -1 back in before registers are handed out (see below).
Where it stands
- The Claude effort is closed. Five passes, the last one time-boxed, and TensionDraw is still 16 words off.
- GPT-6.1-sol, running in Codex, ran about 1,800 experiments and checked 45 candidates on both discs. It showed the conditional move can be made after the second merging pass, the case marked untested above, but a later pass still folds the separate -1 away before registers are handed out. Its best stayed at 16.
The search is paused, with every attempt written down. It reopens when someone finds the shape of C the original programmers wrote.