NICE BALL. SHOW ME THE BYTES. — REAGENT TO RUST Worthify / October 14, 2026 00:00 The ball doesn’t care The original Neverball is running inside ReAgent's connected desktop. We're rebuilding its behavior in Rust with the engine source left unopened, and using the running game to catch the details a plausible rewrite would miss. A question in chat leads to the Worthify Decompiler, then to the instructions and memory in the VM. The texture upload exposes a missing pointer. A course reader tells us where materials end and geometry begins. A breakpoint lets us compare the ball before and after one update. Those observations give the Rust version concrete values and rules to implement. It calculates its own state. We give the original and Rust the same keys, then compare their positions, coin counts and completion updates. Worthify Decision One also chooses targets from recovered state during live play, so the controller can work through longer routes. Start at the ball. Its checker pattern is easy to recognise; the useful question is which pixels the renderer actually receives. 01:00 Follow the pixels Neverball's engine source stayed unopened. The executable, compiled course data, and running game supplied the reference. Ask the chat to inspect the texture uploader at 0x104e0. ReAgent fetches the Worthify Decompiler's saved output. Open the linked function, and that output appears on the right. The generated call shows six arguments. OpenGL expects nine. Width and height are visible, but the pixel pointer is missing from the expression. Follow the call-site setup into Assembly. Here is the call at 0x10665. Just above it, three pushes supply the arguments omitted from the generated expression. The executable still passes the pixels. The decompiled call gave us the right place to look, and the instructions complete the picture. 01:47 Follow the pixels The first six arguments travel in registers: target, mip level, internal format, width, height and border. The zeros here are the mip level and border. This value selects the internal format; the external format describes the bytes supplied by the caller. Read the stack backward from the call. The last push supplies that external format. Above it is the unsigned-byte type. Above that is the pixel pointer held in this register. Those are arguments seven, eight and nine. The cleanup removes thirty-two bytes: three eight-byte arguments and alignment space. The call now balances. More usefully, we know which pointer supplies the image. Following that pointer upstream reveals a choice that matters to the capture. 02:32 Follow the pixels The chat answer also links the helper feeding this upload. Open it on the right and look at the loops. The Worthify Decompiler exposes the byte-averaging loops. They resample an existing image. This is already downstream of image decoding. Back in the caller, the branch determines which buffer reaches OpenGL. One path uploads the resized buffer. The other uploads the original buffer. Capturing an allocation earlier in the function could give us the intermediate image instead of the one rendered on screen. Both paths meet the same graphics interface. At that boundary, dimensions, type, format and pixels all describe the selected image. That is the useful place to observe the ball texture. The next step is to instrument that boundary in the connected VM without disturbing the renderer. 03:19 Follow the pixels Ask ReAgent to inspect the capture helper in the VM. The chat reads the file and calls out three things: forwarding the real upload, reading the resulting pixels, and restoring OpenGL state. That is the code review we need before attaching anything to the renderer. Open the wrapper in the connected desktop and follow the two calls in order. 03:42 Follow the pixels This is the capture wrapper in ReAgent's connected desktop. It is instrumentation written for the investigation. The forwarding call runs first, allowing the real graphics implementation to finish the upload. Further down, the readback requests the resulting texture as four-channel bytes. Reading before the forwarding call would inspect the texture before this upload had established it. The resolver locates the next library implementation. That avoids sending the call straight back through our own hook. The guard handles re-entry while capture is active. There is another route into this function: the game can obtain a graphics function pointer through a procedure lookup. The SDL and GLX wrappers cover those lookups too. With the call reaching the capture helper, its remaining responsibility is to leave the graphics state as it found it. 04:30 Follow the pixels The pixel-pack buffer binding is the first trap. If a buffer is bound, OpenGL treats the destination argument as an offset into that buffer. The wrapper saves the binding and unbinds it for this read into CPU memory. Row length, skipped rows, skipped pixels and alignment also affect the output. The helper saves those settings, selects tightly packed rows, reads the image, and restores the settings. Notice that restoration happens before the readback-error branch. A failed capture must not leave different settings for the game's next graphics operation. The actual texture dimensions determine the allocation, with bounds on its size. The context and thread checks keep the read tied to the renderer that owns it. Finally, this queue preserves errors that were already pending. The capture reports its own error separately. That lets us inspect the output without confusing a game error with a readback failure. 05:24 Follow the pixels The resulting event is now open in the VM. Its metadata connects the dimensions and upload format to this pixel file. This texture is five hundred and twelve by two hundred and fifty-six. The upload contains luminance and alpha; the readback requests four unsigned-byte channels. Width times height times four agrees with the file length, and both error fields are zero. Opening the image gives us the ball's checker pattern. These pixels were recovered from the running renderer after it loaded its resources. They were not artwork embedded in the executable. The image viewer makes the pattern obvious. Its background makes transparency much harder to judge. The channel extrema beside it settle that part numerically. 06:08 Follow the pixels Both alpha extrema are one hundred and twenty-seven. Every pixel has the same alpha, just under one half. Making it fully opaque would change the ball even with the correct checker pattern. The helper's luminance branch copies red into green and blue while preserving alpha. The PNG converter carries those channel values into the output image. Its row-flip option handles the orientation once. In the Rust material loader, the ball selects that captured image and passes it through the normal texture-loading path. We now have a concrete link from a call in the executable, to runtime bytes, to an image the replacement actually uses. If its appearance still differs, material alpha, blending and depth behavior are the next things to inspect. Repainting the checker pattern would hide the question. 06:54 Follow the pixels Here it is on the Rust ball. The recovered image supplies the checker pattern, and the alpha survives the conversion. The surface beneath it has a different source: compiled course data. To reuse that data, Rust needs to agree with the original reader about exactly where each record starts and ends. 07:12 Count the bytes Ask ReAgent how the compiled course stores its material records. The answer leads into this reader. The Worthify Decompiler exposes the header and allocation. We follow the byte-consuming calls in Assembly. The file header begins with integer reads for the format signature and version. The unsigned version test accepts six through eight. The small parser used for this inspection selects version eight before interpreting its counts. The count reader describes the arrays that follow. Byte data and dictionary records come before the materials, so their sizes determine where the material walk begins. Skipping that prefix could still produce plausible-looking numbers, just at the wrong addresses. Here is the material allocation. Its stride is useful for understanding the object in memory. The file cursor is a separate question, answered by the calls that actually consume bytes. This distinction is where a straightforward-looking Rust parser can quietly go wrong. 08:10 Count the bytes The allocation stride is one hundred and forty-eight bytes. Count the file reads alongside it. Four loops read four floats each: sixteen floats, or sixty-four bytes. One more float adds four. The flags integer adds another four. The name read consumes sixty-four. That makes a base record of one hundred and thirty-six bytes. The destination offsets describe where those values live in the material object. A gap in that object consumes no bytes from the file. If the Rust parser advanced by the allocation stride, it would skip twelve bytes after every base record and gradually interpret the wrong data. There is still a branch below the name read. The base length is now accounted for; this condition determines whether the record continues. 08:55 Count the bytes The conditional tests the second byte of the flags integer. In this little-endian layout, that bit corresponds to the flag shown here. The byte offset belongs to the in-memory object, rather than an offset to seek to in the file. Inside the selected branch, the integer reader consumes four more bytes. The float reader consumes another four. When the version and flag select this path, the parser must advance by eight extra bytes before beginning the next material. Those two reads explain the variable length. A parser that ignores them can print a convincing first record and lose alignment immediately afterward. The practical check is to print the cursor through the whole material array and inspect what comes next. 09:41 Count the bytes The material walker does that in the VM. After the header, byte data and dictionary pairs, the first material begins at the offset shown here. The first two rows each consume one hundred and thirty-six bytes. The second starts exactly where the first ends. The last row takes us to the next array boundary. All eleven materials in this course use the base path; the optional branch remains a condition derived from the reader. The first three floats at the next boundary are zero, minus a quarter, and minus two. That is a stronger alignment check than readable material names alone. The parser decodes little-endian fields explicitly and checks the remaining slice before reading. This walk recovers the material part of the course layout. Geometry and other records have their own readers; the same byte accounting applies to them. 10:32 Count the bytes The next tables turn that byte stream into a course. Each triangle record contains a material index and three corner indices. The record at byte one hundred and thirty-three thousand eight hundred and forty selects material nine and corners nineteen sixty-two, nineteen sixty-three and nineteen sixty-four. Each corner then selects a texture coordinate, a normal and a vertex. Following those three links recovers where that corner belongs in space and which part of the image lands on it. The course has two thousand three hundred and twenty triangle records. We resolve the same relationships across the table rather than drawing a floor to match a screenshot. The setup records are just as concrete. The ball starts at one, zero point two five zero five, minus one, with a radius of a quarter. The goal is at one, zero, minus seventeen. Eighteen item records supply their positions, types and values. The metadata gives a target of ten coins and a time value of nine thousand. The running game's ninety-second countdown supplies the unit conversion. These decoded records become the Rust course. We adapt the texture coordinate convention in our renderer, while keeping the recovered geometry and setup values. Now we can ask whether the replacement moves through that course like the reference. 11:49 Stop the world Ask the chat to inspect the memory probe. Then take those field offsets back to the update breakpoint. The update routine and global pointer give us two useful addresses. The update routine has an image-relative address in ReAgent. Adding this process's load base turns it into a breakpoint address in the VM. The global ball-pointer slot is relocated the same way. Then we read the pointer stored in that slot. The resulting address identifies the heap object for this run. The slot belongs to the image; the ball itself does not. Continuing reaches the update breakpoint. Printing the pointer here anchors the field reads to the current object. After a restart, reading the slot again avoids carrying an old heap address into a new run. We can now ask a much narrower question: what does one update change in this ball? 12:38 Stop the world The recovered layout puts position, velocity and radius at the offsets shown. Reading position as three floats gives us its coordinates. The next expression reads velocity from the same object. Radius has its own field, and here it is a quarter of a world unit. The labels stay beside the expressions so each value has a location and a meaning. Raw bytes at the same address provide another view if the storage interpretation is in doubt. The update calls into the physics routine. These are the values before it runs. Keeping them in view while finishing the update exposes its effect at this exact boundary. 13:14 Stop the world Finish returns from the update. Reading the same fields again shows horizontal position staying fixed while vertical position changes slightly. Vertical velocity changes with it. The radius stays constant. The ball is settling against the floor. This scripted probe gives us a pair of states around one update: a small change in height, a change in vertical motion, and an address that identifies the code between them. It then closes the process it launched. When Rust disagrees, these are the fields to compare at the first differing step. They turn a vague complaint about movement into a particular state transition. Timing also enters the simulation through the course reader, where a mathematically reasonable expression can still produce different bits. 13:58 One bit out Put the duration conversion back into chat. Follow the answer to the moving-path duration field in the course reader. The assembly shows the conversion from the file value into the runtime duration. First it rounds into integer milliseconds. It converts that integer back to a float, then multiplies by a stored single-precision reciprocal. The two constants are a thousand and the nearest float to one thousandth. That second constant sits slightly above the exact decimal fraction. A Rust division by a thousand looks entirely reasonable. It expresses the same intended unit conversion. But it need not produce the same float as this multiplication sequence. The instruction at this address settles which operation the executable performs. A small calculation in the VM will show whether the distinction survives into a value we can compare. 14:48 One bit out Give both expressions three thousand milliseconds. The first converts to float and multiplies by the reciprocal. The second converts to float and divides by a thousand. Printing the bits makes the difference unambiguous. Division lands exactly on three. Multiplication lands one representable float above three; the last bit changes. That is enough to make an otherwise sensible rewrite disagree with this executable. The Rust duration conversion now preserves the multiplication and single precision. The focused calculation stays beside it, and the physics tests exercise the surrounding implementation. The focused tests pass. This example isolates one operation. Contact, collision and camera behavior still need their own comparisons. The next check joins the pieces together by giving the original and Rust the same inputs and letting each compute its own state. 15:40 Let it play The comparison entry point starts Rust from the compiled course. It applies the key presses and releases delivered to the original, advances the simulation, and only then compares the computed state with the reference. That order matters. Feeding recorded positions into the replacement would make a beautiful replay without testing its physics. Here Rust computes its own position, velocity and moving objects. Each delivered key event is attached to a native update boundary. Rust applies that event at the corresponding boundary in its own run, with ninety updates per second. A held key persists until its release; consuming one event per update would describe a different game. The resulting state samples let us compare a long sequence of decisions one update at a time. The side-by-side view shows the movement, while the report identifies the first numerical disagreement. 16:31 Let it play Launch the live controller from chat and open the connected VM. Worthify Decision One is our public model on Hugging Face, running locally. It receives the recovered game state as text and a menu of possible targets. The native game keeps running while it scores those choices. The controller builds that menu from reachable floor-level coins and teleporter destinations, taking the remaining coin requirement into account. Decision One selects a target; authored pathfinding and steering turn that choice into keys. With only one feasible target, the controller takes it directly. Position and velocity keep arriving from the memory probe. During inference, the controller uses those observations to brake and hold position. A response more than a second out of date is discarded. Once a current choice arrives, steering resumes toward it. Watch the state panel beside the game. The ball keeps moving, new state arrives, and the next target appears. The model is making short, bounded decisions while the controller handles the frequent input updates. That gives us a longer live test without asking a large chat model to narrate every frame. 17:38 Let it play On this live teleporter run, the median model request takes about one hundred and thirty-two milliseconds, including the round trip. Fourteen model calls use just over eight thousand input tokens. The game keeps advancing during inference and finishes with forty-one coins against a goal of forty. Send the delivered-key ledger back through ReAgent's chat. Each event is tied to the native update boundary where it took effect. Rust starts from the compiled course and computes its own state. Both engines reach success at batch four thousand and fifty-four. Across four thousand one hundred and forty-four compared batches, maximum position separation stays below three millionths of a world unit. Coin counts, radius and tilt agree throughout. The images show the route; the per-update comparison tells us how closely this Rust implementation follows the executable on those inputs. 18:29 Let it play Rust gives those recovered details a new implementation. Here it is running on macOS, with the game calculating its own movement and collisions. The keys steer the ball, the camera follows, and the pause menu lets us restart. The result is a replacement we can run, inspect and improve. ReAgent’s code and memory findings give us concrete behavior to compare with the original. REFERENCES AND CREDITS Rust implementation, scope and notices: https://github.com/Worthify-AI/reagent-neverball-rs https://github.com/Worthify-AI/reagent-neverball-rs/blob/main/NOTICE.md Runtime pixel capture helper: https://github.com/Worthify-AI/reagent-neverball-rs/tree/main/experiments/runtime-artwork Neverball: https://neverball.org/ The reference uses the installed executable and compiled resources; the engine implementation source remained withheld. The captured texture comes from runtime pixels after external resources are loaded. It was not embedded in the executable. Neverball resources retain their applicable attribution and copyleft terms. https://github.com/Neverball/neverball/blob/master/LICENSE.md This film uses a fresh, continuous ReAgent recording of the existing investigation. The complete session is retained privately at its original timing. The edited walkthrough selects intervals without cropping the application. Editorial highlights, underlines and cursor guidance point to unchanged application output. New native Worthify Decompiler jobs cover the byte-averaging helper and SOL reader. The texture uploader and camera output were retained results inspected again here; Assembly completes the interpretation where generated C remains incomplete. Existing authored probes and the Rust implementation were reused. This recording does not claim that the whole engine was rediscovered or rewritten during one take. Earlier construction also used Ghidra inside ReAgent, as retained in the research pack. Worthify Decision One: https://huggingface.co/Worthify/Decision-1 The fresh live run keeps the native clock advancing during asynchronous inference. The model classifies supplied coin/teleporter targets from recovered memory state. Authored geometry checks, navigation and steering supply SDL keys. The controller brakes while a choice is pending; a single feasible target is followed automatically. Fourteen model calls used 8,059 input tokens. Median HTTP round trip was 132.402 ms. The independent Rust comparison receives delivered key events aligned to native update boundaries. Reference positions are comparison data, never simulation input. Both engines reached Won with 41 coins at batch 4,054. Across 4,144 compared batches, maximum position difference was 2.86102294921875e-6 world units; coin counts, radius and tilt agreed. This is a measured route result, not a whole-game equivalence claim. The fresh paired route shows the Linux VM reference and independently simulated Rust on Linux. The separate macOS WebKit clip shows actual Rust keyboard play, camera changes, pause and restart. It is not the paired route's reference recording. Original Worthify narration. Locally generated speech: Qwen3-TTS 12Hz 1.7B CustomVoice, built-in Aiden, with conversational delivery instructions and measured pause treatment. Model revision 0c0e3051f131929182e2c023b9537f8b1c68adfe. No individual human voice was cloned or imitated. Apache License 2.0. https://github.com/QwenLM/Qwen3-TTS IBM Plex fonts: SIL Open Font License. https://github.com/IBM/plex Worthify brand artwork supplied by Worthify.