Final Fantasy VI Ultimate version 2.02

Performance 6

A lot of visual effects in battle slowed down the original game. Large portions of this slowdown are fixed, with the output kept intact.

56.6%less slowdown over 398 different battle animations, each measured as Terra’s spell against six enemies.

The rule for every optimization on this page: the slowdown is removed and nothing else. No graphics are altered and no animation loses a frame of its own; the code is simply optimized.

The biggest gains

1.0 Now

Ten animations with the most slowdown removedLand Slide: 76% less slowdown than in 1.0Land Slide76% lessSpecter: 79% less slowdown than in 1.0Specter79% lessElf Fire: 81% less slowdown than in 1.0Elf Fire81% lessFire Wall: 81% less slowdown than in 1.0Fire Wall81% lessKefka's magicite scene: 77% less slowdown than in 1.0Kefka's magicite scene77% lessRemedy: 68% less slowdown than in 1.0Remedy68% lessX-fer: 72% less slowdown than in 1.0X-fer72% lessPlasma: 56% less slowdown than in 1.0Plasma56% lessRippler: 53% less slowdown than in 1.0Rippler53% lessRasp: 55% less slowdown than in 1.0Rasp55% less0%20%40%60%80%100%slowdown, 1.0 = 100%

392 of the 398 animations slow down less than in 1.0, and none got slower.

Side by side

Terra casts Ultima at six enemies with four party members floating: 1.0 on the left, now on the right, recorded frame by frame. Both play the same animation, but 1.0 slows down while the circle grows and takes almost three seconds longer.

The changes

#548 Battle animations slow down far less, because sprites are drawn faster and the game skips work it does not need.

In 1.0
In heavy battle animations, the game spent most of each frame redrawing every sprite, so the animation fell behind and slowed down.
Now
Animations now slow down far less and look the same.
For ROM hackers Method

Three changes in the battle graphics module. UpdateFrontAnimSprites (C1/2642) and UpdateBackAnimSprites (C1/25E1) call a new pass that walks a target's animation threads once and draws each tile with less setup; the reflect grouping and odd thread counts keep the original pass. ExecAnimScript (C1/C653) sets a flag in one free direct-page byte while a step runs, and BattleNMI (C1/0BA7) then keeps the last finished sprite table instead of rebuilding it, redrawing only characters whose body moves by itself (Float, Magitek armour, running away, a chanting, Muddled or Control pose), at OAM slots DrawCharSprite (C1/34DB) records. DrawDmgNumSprites (C1/2B2A) returns at once when no damage numeral is showing. Every step shows the same frame as in 1.0.

How to read these notes

Credit: my implementation

#549 Ultima, Quake, Meteor, Bio Blaster, Mantra, Bahamut and the other animations with a circle or oval slow down far less.

In 1.0
Animations that open a circle or oval on the screen, such as Ultima, Quake, Meteor, Bio Blaster, Mantra and Bahamut, slowed down.
Now
They now slow down far less and look the same.
For ROM hackers Method

UpdateCircle (C2/D8E1) fills a 256-row buffer, works out every row's half-width with a midpoint loop, writes the circle into the buffer and copies 279 rows into the window table the NMI sends, 63 to 102 scanlines a step. UpdateCircle_far (C2/D8DD) now jumps to a routine that writes each of the table's 151 lines once, with half-widths from tables made in advance by running the original's loops, and hands any unknown shape or oversized size to UpdateCircle: 17 to 26 scanlines a step, the same window line for line. The 28 wait_scanline commands right before update_circle in the attack animation scripts are removed, since the window table is only handed to the screen between frames. The tables take about 25 KB of free space.

How to read these notes

Credit: my implementation

#551 The pause before a battle animation starts is about a third shorter, because its graphics load faster.

In 1.0
Each battle animation started with a slow pause while its graphics loaded.
Now
The pause is now about a third shorter.
For ROM hackers Method

LoadAnimProp (C1/9CB3) unpacks an attack's graphics into a buffer at $7FE400. The tile loops of LoadAnimGfx3bpp (C1/A1FD), LoadAnimGfx2bpp (C1/A306) and LoadBG3AnimGfx2bpp (C1/A3C8) copy a word at a time through pointers they add to after every word and call ApplyTileHFlip (C1/A470) for every word, which reverses a flipped word bit by bit in 16 passes. Each loop becomes a jsl to a routine that tests the flip once a tile, copies an unflipped tile a word at a time with the offsets written into the instructions, and a flipped one a byte at a time through a 256-byte bit-reverse table; a vertical flip only changes the row order. ClearThreadData (C1/9C9E) clears byte by byte; the new one clears one whole thread a pass, in words. About 3 KB of free space.

How to read these notes

Credit: my implementation

#552 Fire Dance and the other animations that fade the battle's colours slow down less.

In 1.0
Animations that fade the battle's colours, such as Fire Dance, slowed down.
Now
They now slow down less, with the same colours.
For ROM hackers 1.0 addresses

IncPal (C1/EB35) and DecPal (C1/EB53) fade a range of colours from $7E7C00 into $7E7E00 by calling IncColor (C1/FD00) or DecColor (C1/FC99) for each, which keep the colour in $2A between channels and test each result against a mask, about 160 cycles a colour. Replace each loop, at C1/EB40 and C1/EB5E (15 bytes each), with a jsl to a routine in free space that keeps the colour in Y and tests each channel by its own carry: for DecPal a borrow gives 0, for IncPal a compare past 31 gives 31, and blue past 31 sets bit 15. About 90 cycles a colour. Both routines take 150 bytes, with a tail that leaves $28, $2A, A and X as the original loop did.

How to read these notes

Credit: my implementation

#553 The Magitek factory mine cart draws 30 frames a second instead of 15, at the same speed, and its frame no longer tears.

In 1.0
The mine cart ride out of the Magitek Factory drew only 15 frames a second, and now and then a frame tore, the lower part refreshed while the upper part lingered on the previous frame.
Now
The ride now shows 30 frames a second at the same speed, and no frame tears.
Twenty seconds of the ride, version 1.0 on the left and now on the right, every frame the console shows.
For ROM hackers Method

The frame is an 80 by 80 byte buffer in work RAM, one Mode 7 tile per pixel. In 1.0, _ee235b (EE/235B) places every tile with hardware multiplies, and UpdateTrainGfx (EE/9F14) clears the buffer a word at a time and copies each pattern a byte at a time. The rewrite: one generated routine per tile pattern and size (319, about 59 KB) storing immediate pixel values; positions from multiply tables; the clear by DMA; TrainMain (EE/04B1) every 2 frames, with a half step between whole ones; one whole upload from TrainIRQ (EE/AA35), and TrainNMI (EE/A94D) no longer resends the palette. Needs about 64 KB of free space.

How to read these notes

Credit: my implementation

#631 Doom's Forcefield and the other attacks that brighten or darken the battle's colours slow down less.

In 1.0
Doom's Forcefield in the final battle, and other attacks that brighten or darken the battle's colours, slowed the game down while the colours changed.
Now
They slow down less, with the same colours on every frame.
For ROM hackers 1.0 addresses

First, the colour loops of #552: work red and blue as one sum (mask $7C1F) and green as another ($03E0). Brightening, add the red and blue amounts as one word; bit 5 or bit 15 set means that channel passed 31, so make it 31; green passed 31 when bit 10 is set. Darkening, add $8020 less both amounts, and $0400 less green's; a guard bit the add left clear means that channel went below 0: clear it. End each loop on a compare of X with the end offset. Second, at _ed18 (C1/ED18, animation commands $FC and $FD) replace the four IncPal calls of 16 colours on palettes 12 to 15 with one of 64, then set $10 to palette 15 as the fourth call left it.

How to read these notes

Credit: my implementation