General Vritual Machine architecture

From Final Fantasy XII Wiki
Jump to navigation Jump to search
VM's general specifications
Type stack based
Integer registers 4 (I0 to I3)
Float registers 4 (F0 to F3)
Special registers 3 (X, Y, A)
Number of instructions 100
Maximum stack size unknown
Number of calls ~2000

Final Fantasy XII contains a stack based virtual machine that runs field and event scripts. The implementation seems to be exactly the same starting with Japanese PS2 release, through PS4, PC and Nintendo Switch versions. The only thing that changes are available calls. Each subsequent version supports all previous calls and includes a few dozen of new ones at most.


Even though the VM contains a set of registers, they are used either by compiler or have only quality of life purpose (so it is not needed to declare variables for iterators or temporary storage). All except a few instructions operate solely on stack.

Immediates

Immediates are always 16bit wide integers. In all but one case their values should be treated as unsigned type, the only exception being PUSHII opcode, for which the immediate is always signed 16bit.

If a value greater than 32767 or lower than -32768 is needed, it has to be placed in a special integer table that can be accessed by PUSHI opcode. This means that in a single script file, there can't be more than 65536 unique integer values outside of signed 16bit range. It shouldn't ever become a problem, but there are methods to use more values (arrays with initializer list).

Floating point values are never immediates. Instead, they're stored in similar tables as integers and can be accessed by PUSHF opcode. This means that, just as in case of integers, there cannot be more than 65536 unique floating point values in a single script file. All floating point values are 32bit IEEE754 single precision.

Registers

VM provides the following set of registers:

  • 4 integer registers (I0 to I3, accessed by PUSHI0, PUSHI1, PUSHI2, PUSHI3, POPI0, POPI1, POPI2, POPI3 opcodes)
  • 4 floating point registers (F0 to F3, accessed by PUSHF0, PUSHF1, PUSHF2, PUSHF3, POPF0, POPF1, POPF2, POPF3 opcodes)
  • 3 special purpose registers (X, Y, A accessed by PUSHX, POPX, PUSHY, POPY, PUSHA, POPA and various conditional and calling opcodes)

Special register X is used mostly by if-type conditional jump instructions.

Special register Y is used mostly by switch-case conditional jump instructions.

Special register A is used mostly by call-type instructions.

Because of that, even though it is possible to use special registers directly, it is not recommended.

Variables

All information regarding variables, similarly to integers and floats, is stored in a special variable table. In code, variables are accessed by providing a variable table entry number to instructions like PUSHV, POPV, PUSHP, PUSHDBG.

There are six fundamental variable types:

Name signed/unsigned size
char signed 8bit
u_char unsigned 8bit
short signed 16bit
u_short unsigned 16bit
int signed 32bit
float n/a 32bit

There is no unsigned 32bit integer type, though both assembler and compiler are capable of casting unsigned values.

Once pushed onto stack, all values become 32bit signed integers.

One important thing to note is VM's behavior when assigning values out of range for a given type. All values lower than minimum become the minimum, all values larger than maximum become maximum. In other words: assigning -1 to an unsigned variable type will result in zero being assigned.

Scopes

Virtual Machine supports five variable scopes:

Name placement availability remarks
global anywhere outside script body name available to all scripts within file, value stored in global memory accessible by all scripts import directive
scratch1 anywhere outside script body name and value available to all scripts within file, value stored in special area that is 512 bytes in size import directive
scratch2 anywhere outside script body name and value available to all scripts within file, value stored in special area that is 512 bytes in size import directive
file anywhere outside script body name and value available to all scripts within file, value stored in a memory within the file typical declaration
local anywhere inside script body name and value available to all functions within a script, value stored within script's local frame[1] typical declaration

There is no function local or block local scope.

Pointers and references

VM allows to push a pointer to a variable onto stack by use of PUSHP instruction. It isn't possible to use it as an array. It is considered to be just as any other value.

This instruction is used exclusively to push references onto stack that will be arguments to external calls, that will store values in supplied variable references. Typical usage:

getpos(&posx, &posy, &posz);

You can push a pointer to an array element, but not to the array itself. Pushing pointer to array's first element works as expected.

Note:pointers to local variables return addresses relative to script's frame start, which makes them useless in most cases, including passing as function arguments.

Arrays

All variable types can be declared as either one or two dimensional arrays. For further information read this article.

Initialization and initializer list

Variable tables can store initial values of file scope and script local scope variables and arrays. This feature is very scarcely used by original scripts, which choose to rather initialize them in script's init function by a typical assignment. Both assembler and compiler support typical c++ like initialization.

int variable = 3;
float array[3] = {
  1.3, 2.1,
  3.14
};

Initializers do not count towards integer/float values quota.

REQ tables

TBC

Calls

VM supports ~2000 of predefined call targets. Calls are performed by using CALL, CALLACT, CALLPOPA, CALLACTPOPA instructions.

For further information read this article.

References

  1. Local frames have static size and are stored at static addresses in an area within file.