General Vritual Machine architecture
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.
Contents
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
- ↑ Local frames have static size and are stored at static addresses in an area within file.