Golden Sun Recompiled: Letting Battles Out of the Box

PROJECT LOG

Golden Sun Recompiled: Letting Battles Out of the Box

After the last post, I moved on to battles. They looked easier: fewer rooms, fewer layers, and everything arranged in a neat little arena.

They turned out to be a useful test of the difference between the renderer cutting something off and the game never drawing it in the first place.

The quick update

The first obvious failure was the enemies. They were still confined to the old 240-pixel-wide screen, even when the expanded renderer had plenty of room beside it. Spell sprites behaved the same way.

That safety check had quietly become a wall. Once it was fixed, monsters and effects could use the margins, the arena could continue sideways, and menus stayed where they belonged.

Then the spells exposed the harder limit: sometimes the game itself had stopped drawing at the original edge. The host-side recovery now keeps the larger spell artwork and its fading trails, so the effect can continue into the extra space at its original scale.

It is still experimental, but all four elemental summons land correctly in the expanded view in my latest testing. That is a satisfying place to be after a lot of work that, when it succeeds, should look completely ordinary.

The deeper battle story

Sprites and the old screen edge

The first battle problem was not the battle artwork itself. Enemies and spell sprites were being kept inside the original 240-pixel-wide screen, even though the expanded renderer had room beside it. The renderer had a safety test that allowed field sprites into the margins only when it could confirm where they belonged. Battles did not provide that same information, so every monster and effect sprite was treated as unsafe.

The useful question was simpler: is this one of the parked sprites Golden Sun hides above the screen? The game uses one very specific position as that sentinel. Only those objects stay confined; everything else is drawn where the game put it, including the battle margins. On one captured damage animation, margin sprite pixels went from none to 360. Other frames stayed at none because those sprites did not reach the margin. A wider view should reveal what the game placed there; it should not invent a monster.

There were two copies of the rule, one in the ordinary renderer and one in the GPU renderer used for play. Fixing the first changed the reference image and did nothing for the screen in front of me. It was the sort of invisible progress that makes a debugger feel accomplished.

Battle layers and live rows

Once the sprites were allowed through, a hard seam remained around several battle effects. The first suspicion was the hardware window, the obvious curtain that decides what is visible. The window was already open at that edge.

The real restriction was in the battle background rule. A summon effect is carried by an affine background layer, and the expanded-view code admitted only one map-size setting into the side margins. Some effects used a smaller, non-wrapping affine map, so the renderer rejected them before the sampler could decide whether a real pixel existed.

The corrected rule is narrower. A wrapping layer still needs its geometry check, because letting it repeat past the wrong edge produces a convincing but false arena. A non-wrapping affine layer can continue until its own map ends. On one Granite frame that changed the margin from zero effect pixels to 1,016. Another frame stayed at zero because no artwork was there. Different background layers can carry the arena depending on what the battle is doing, while menus and command icons keep their original placement.

Golden Sun can rewrite scroll, window, and blend registers during a frame. Reading the register left at the end gives the last row's answer, not every row's. The battle renderer now uses the game's per-row capture, so the arena follows the battle instead of freezing one late-frame decision over the whole screen.

The 128×128 canvas, full-size stamps, and history

The remaining seam was not always the renderer cutting something off. For many summons, the effect stopped at the original screen boundary because that is where the game's own canvas stopped. Golden Sun paints the spell image into a 128×128, 8-bit canvas and displays it through an affine background. The two-times horizontal scale fills the native 240-pixel screen. At the left edge, the first valid source column is zero; there are no pixels just outside it for the widened renderer to discover.

Opening the window wider cannot create those pixels, and sampling the same 128 columns farther away does not help. Widening the game's canvas directly was worse. The stamp routine is generated at runtime and takes one size for both dimensions. Above 128 it starts producing invalid instructions; the next practical size would consume the entire 65,536-byte heap, while VRAM and scratch buffers were already occupied.

At first I thought the only recoverable part was a handful of tiny sparks. That came from an overly strict capture filter which only accepted tiny drawing operations. A later full-size capture intercepted much larger requests, including stamps starting outside the canvas, and kept their requested dimensions. My filter had thrown away the evidence, and I had confidently explained why the evidence could not exist. That was bullshit.

The implementation now intercepts the game's draw requests before its canvas clip. It preserves the full requested stamp, recovers the source artwork, and applies its flips and blend mode on a larger host-side canvas. The result then goes through the same affine mapping and palette compositor as the native layer. The native 128×128 canvas remains authoritative for samples inside its map; when the transformed sample falls outside that map, the host canvas can supply the recovered artwork. The host path supplements the original effect rather than replacing it.

The effect also had memory. I cleared the host canvas at every VBlank, while the game copies its effect to the display and then fades the working canvas. The measured fades are 75 percent and 50 percent. Frames without a copy retain the previous artwork. Clearing our version erased the trails and made a correct capture look incomplete.

The host implementation now keeps that history. On the game's display-copy event it publishes the completed canvas, then applies the matching fade to the working copy. A real clear, a canvas replacement, or loading an earlier state resets the history. In one later capture, the reconstructed image matched all 16,384 intensity values inside the original canvas while also containing artwork beyond its edges.

When replay looked right and live looked wrong

For a while the offline replay looked correct while the running game still showed a missing strip in the widened margin. The centre 240×160 image matched, and the difference in the captured 360×240 presentation was confined to the added margin. That pointed to the live host effect upload rather than the scene state or transforms.

An unusually large request had left the reusable GPU texture oversized. The driver rejected the current allocation, so later frames were trying to draw from a texture with no usable storage. The upload path now sizes from the current frame's bounds, checks the driver's limit and the storage it actually allocated, and abandons only that out-of-range frame while keeping the previous valid texture. A later in-range frame can recover normally. The oversized frame is still omitted; it does not get silently cropped or treated as supported.

In my latest testing, all four elemental summons reach the extra space after the history fix. Heavy effects are substantially better and hard spell clipping appears resolved, but broader checking remains. I am not claiming every spell is guaranteed. More battles and transitions still need checking.

Reacties

Populaire posts van deze blog