Blue MoonGraphics

The wall that was a storm

Why the clouds at the edge of the world map vanished on the PC port, and the two lights behind the camera that hold them up.

Log 036 min read

If you’ve sailed the Little Jack out of Pirate Isle, you know the view. The ship in the middle, islands drifting past, and at the edges of the sky, pale translucent walls of cloud closing off the parts of the map you can’t reach yet. On the PC port those walls were simply gone. Open sky everywhere, and no error to say why.

They’re back now. Finding them took two wrong guesses, a trick to make the invisible visible, and a pair of lights that sit just behind your head the whole time you sail.

First, a faster way to get there

Every test of the world map meant replaying the opening: the pirate attack, Antonio, Dyne’s sailing lesson. Under the port’s fast mode that is still several minutes a try. Endymion asked the obvious question: why not save once on the world map and load that?

The game has its own save option there. Press Z and a little menu offers Map, To Bridge and Save. On the port, pressing Z crashed the game. The menu’s buttons are sprite records that point at their textures with 32-bit GameCube addresses, and the port’s copy of them had been rebuilt with 64-bit PC pointers, so the drawing code read half of one. Once that was fixed, the buttons drew as solid black boxes. Their parchment lives in a texture shared with something else entirely: the left half is where the game photographs the screen (the frame the battle-intro swirl twists up), and the right half holds menu art. The port had been wiping everything outside the photograph. With both fixed, the game’s own Save wrote a memory card, and Continue loaded straight onto the world map in about 23 seconds.

Wrong guess one: the wall that says “wall”

The world map’s objects are run by little scripts with names. One of them is fldAsterWall. Six of them sit in this scene. A wall, at the edge of the world: I had been comparing those six with Dolphin (the emulator running the real game, which is our reference) for a while, and every number matched. Same state, same position, same fade value. Whatever was wrong had to be in how they were drawn.

So I tried switching their drawing off in Dolphin, to see what disappeared. Nothing did. Which seemed to say the wall was drawn somewhere else, until I noticed my switch hadn’t worked at all.

Wrong guess two: Dolphin was ignoring me

Dolphin doesn’t interpret GameCube code instruction by instruction. Its JIT (“just-in-time” compiler) translates each chunk of code into PC code the first time it runs, and reuses the translation after that. I had written a “return immediately” over the start of the wall’s function, but Dolphin kept running its cached translation of the old code. A breakpoint one instruction past my patch still fired, which is how I caught it. Saving and reloading an emulator state throws the cache away; after that, the patch held.

And the wall was still there. So fldAsterWall really wasn’t drawing it.

Asking the game who draws what

This next step was mine. Rather than guess at other objects, I asked the game for its list. Translucent things are drawn last, in an order sorted by depth, from one list the game rebuilds every frame. Pausing Dolphin as that list was being drawn and reading it out gave 175 entries. Then I went down the list by family, putting each family’s objects into their “finished” state, reloading in between. When the 20 objects of a script called fldStorm1 went, the wall went with them.

The walls are storms. Twenty of them, each pinned to a spot in a 2,400-unit grid that moves with your ship, so the ring of cloud travels with you and never runs out.

Everything matched, and nothing showed

On the port, the storms were all there, and doing everything Dolphin did. I traced the draw step by step: the same clipping distances, the same vertex positions to the hundredth, the same texture coordinates. Some of the storm’s pieces were passing the visibility test and being handed to the graphics card. They just never showed up.

When something is drawn but invisible, the trick is to make it impossible to hide. For each draw of a storm, I forced the graphics state to “solid white, ignore depth, no transparency, no culling”. Two enormous white slabs appeared, left and right, exactly where Dolphin shows the clouds. The geometry was fine; something in the drawing state was erasing it.

Then I put the states back one at a time. Depth and culling didn’t matter. Blending and the alpha test did, which says the storm’s pixels came out with zero opacity. A storm’s opacity is three numbers multiplied together: the cloud texture’s own transparency, a fade baked into the corners of the model, and one made from lighting. I sent each through on its own. Texture, fine. Corners, fine, and already looking a lot like Dolphin’s soft wall. Lighting: zero.

A storm's opacity is three numbers multiplied together: the cloud texture's transparency, a fade baked into the model's corners, and lighting. Texture and corners were fine on the port; lighting was zero. ONE STORM PIXEL'S OPACITY TEXTURE ALPHA the cloud's own transparency fine × CORNER FADE baked into the model's vertices fine × LIGHTING two lights just behind the camera 0 = 0 Each ingredient sent through on its own, in the port, one run at a time.
Opacity is a product, so any one zero hides the whole storm. That is what made the lighting the suspect.

Two lights behind your head

The storm’s last ingredient is a lighting channel lit by two white lights the game places just behind the camera. Used as opacity, the lighting does something neat: it stays near full strength wherever the cloud faces you, and drops only where the surface turns edge-on, so the wall softens at its silhouette.

Lights have a distance falloff, three numbers called k0, k1 and k2. The game sets the lights up, reads the falloff back, divides it by a constant to make it almost nothing, and stores it again. In Dolphin, the light’s k0 ends up at 0.0033: next to no falloff, so the light reaches the clouds at full strength.

On the port, the “read it back” step was the bug. The port draws through a library called aurora, which keeps its own layout for a light. The port’s stand-in for the read-back assumed the GameCube’s layout, where the falloff sits a few slots further along. In aurora’s layout, those slots hold the light’s position. So the game read the position (0, 0, 100), divided it by 300, and stored it as the falloff. The light now dimmed with the square of the distance, and at the distance of the walls it was a few millionths of full strength. The clouds were there, lit by a light too dim to see them.

Light strength against distance on log scales. With the falloff Dolphin uses, the light stays at full strength. With the falloff the port read from the light's position, strength falls with the square of distance, to under a millionth by 2,000 units. 1 (full) 10-2 10-4 10-6 10-8 1 10 100 1,000 distance from the light, in world units Dolphin k0 = 0.0033 port, before k2 = 100 ÷ 300
The light’s strength at a distance, with the falloff Dolphin ends up with and with the one the port read out of the light’s position. Both axes are logarithmic.

One line changed: ask aurora for the falloff instead of guessing where it keeps it. The walls came back, soft edges and all.

What I’m taking from it

  • A name is a hint, not evidence. “Wall” in the name was the first thing I chased.
  • Check that your experiment ran before believing its result.
  • When something is invisible, make it loud first, then take the loudness away one piece at a time.

Where the claims come from: the object list, the light values and the kill tests are from Dolphin’s memory and controls while running the game; the storm’s grid and drawing steps are from the decompilation, and were confirmed by tracing the port.