Getting Started with Udon Assembly

Looking to dig a little deeper? Want to try building VRChat Udon out of assembly, or even build your own compiler? Here’s some first steps you should look into.

IMPORTANT NOTE: If you’re just getting started with Udon, this probably isn’t what you want! Check out our thread on Getting Started with Udon.

How Udon Works

Udon has many layers: a nodegraph, which compiles down to UAssembly, which assembles down to bytecode, which gets run by the UdonVM.

In addition to building graphs, users can also write in assembly directly.

Writing UAssembly Quickstart

  • Create a new scene
  • Add a new gameobject to your scene.
  • Add an UdonBehaviour component to the gameobject.
  • Set the dropdown to “UdonAssemblyProgramAsset”
  • Click the “New Program” button on the UdonBehaviour.
    • This will create an assembly program in your assets folder, instead of a graph program.
  • Type or paste your assembly into the text field, and it’ll automatically compile and load any public variable fields you’ve defined.
    • You can also create assembly by dropping a text file with the extension “.uasm” into your assets folder, it’ll automatically get imported as an assembly program.

UAssembly Instruction Set

  • NOP
    • Does nothing
  • PUSH, Address
    • Pushes an address onto the stack
  • POP
    • Pops an address off the stack
  • JUMP_IF_FALSE, Address
    • Pops an address off the stack, and gets a bool ‘jumpCondition’ from the heap at that address.
    • if(jumpCondition) , sets the program counter to an address
    • else , does nothing
  • JUMP, Address
    • sets the program counter to an address
  • EXTERN, ExternMethodSignature
    • Gets a method matching this signature (internally the assembler maps an address to this signature and stores the string value on the heap, which the vm fetches and does some caching on to retrieve the actual method signature)
    • Pops off as many addresses from the stack as there are parameters for the method
    • Invokes the method with the parameters from the heap
  • ANNOTATION
    • Does nothing
  • JUMP_INDIRECT, Address
    • Pops an address off the stack, and gets a uint ‘jumpAddress’ from the heap at that address.
    • Sets the program counter to jumpAddress
  • COPY
    • Pops two addresses off the stack, copies reference value from the first to the second on the heap

Learn X in Y where X = UAssembly

Note: This is incomplete! It is missing more advanced symbols and sync syntax.

# Comments are denoted with a '#'

# Everything that occurs after a '#' will be ignored by the assembler's lexer.

# Programs typically contain .data_ and .code_ sections

.data_start # Section where variables get defined and set

	.export Target # Set a variable as public, this exposes it
                    # in the udonBehaviour

	angle_0: %SystemSingle, null # Define variables with this syntax: 
                                  # name: $TypeString, defaultValue
	axis_0: %UnityEngineVector3, null # Note: currently, the only
        # default values you can set from assembly are null (will 
        #set to default(type) if a value type), or `this`
	instance_0: %UnityEngineTransform, this # Use `this` on 
       #transforms, gameobjects, and udonBehaviours to set the default 
       #reference to the object that your program is attached to
	Target: %UnityEngineTransform, this
	axisName_0: %SystemString, null
	Single_0: %SystemSingle, null

.data_end # End your datablock like so

.code_start # Section where VM instructions are defined

	.export _update # Export an event to expose it to the  
          #UdonBehaviour. Events like update and start get called 
          #automatically if named with the _eventName syntax, but you 
          #can also define and call custom events with any name
    
	_update: # Start of the instruction block for a given event
    
    	PUSH, Target # Push a variable onto the stack. You can also use a 
        #raw address pointer
    	PUSH, instance_0
    	COPY # Copy takes the last two items on the stack, and copies the 
         #reference value from the first to the 2nd
    	PUSH, axisName_0 #Push variables onto the stack for a method call
    	PUSH, Single_0 #in the order that the signature defines
        # The order is “instance, properties, return type”
    	EXTERN, "UnityEngineInput.__GetAxis__SystemString__SystemSingle" 
        # Extern calls a method from the UdonWrapper with a matching  
        #signature, using the values that were pushed onto the stack
    	PUSH, instance_0
    	PUSH, axis_0
    	PUSH, Single_0
    	EXTERN, "UnityEngineTransform.__Rotate__UnityEngineVector3_SystemSingle__SystemVoid"
    	JUMP, 0xFFFFFF # You can jump to any address, which is how you do 
         #loops and such. It’s convention to jump to max address when 
         #your instruction block is done, to avoid running into 
         #subsequent code blocks
    
.code_end # End your instruction section like so

So you want to write a compiler?

VRChat Udon natively supports custom program types, with hooks for displaying custom UI in the inspector and for loading a program from any valid source.

If you’re interested in building a custom program source for your favorite language, take a look at UdonAssemblyProgramAsset.cs and UdonGraphProgramAsset.cs.

UdonBehaviours can load any ScriptableObject that inherits from AbstractUdonProgramSource, although for most applications, you’ll want to just inherit from UdonAssemblyProgramAsset, and build upon that.

The simplest way to get going is to override RefreshProgram, and insert a custom compilation step before AssembleProgram, just like GraphProgramAsset does.

FYI, your custom program source will automatically appear as an option in the UdonBehaviour’s “Create New Program” dropdown.

6 Likes