/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.

X Plus: 3,614 of 3,615 game functions matched on both discs. Where it stands ↓

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

TensionDraw: sixteen words out of 379Sixteen words out of 379TensionDraw: our C, the original compiler, and the disc, compared word by wordOur Cfor TensionDrawThe original compilerthe 1999 PS2 GCCOur build379 instruction wordsThe disc379 instruction wordscompareThe whole function on the discone later test of the same valuecallZoomed in: the call that reads the player's Tension, and the 20 words after it.Red differs: the same jobs, done in different registers or a different order.The other 363 words already match the disc exactly.These 16 are all that keep X Plus from 100%.

Open full size

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.

  1. 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.
  2. 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.
  3. 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.

Where it stands

The search is paused, with every attempt written down. It reopens when someone finds the shape of C the original programmers wrote.

← Guilty Gear X Plus