Binsk
Member
Alright, so I'm not really sure where to put this since we don't have an "Open Source" section anymore.
If you don't like reading, here is your download.
Optionally, the source for the external runner.
What Is This:
This is my take on having dynamically added code to my projects post-compile. I essentially designed an assembly-like language that you can program in. I also wrote a compiler for the code in GML and an internal runner (in GML) and external multi-threaded runner (in C++) so that the code can actually be executed.
The language is essentially formed out of roughly 50-60 commands, registers, and IO ports. You can read data into registers from a number of IO ports, manipulate it, then push it back out to GameMaker.
The compiler converts it into a single GameMaker buffer which can then be executed manually or through the runner. The internal runner will limit execution times to attempt to keep the framerate at your desired level while the external runner will run tasks on however many threads you specify.
Why Is This:
I have been contemplating an idea like this for some time. I finally had a free day with little work to do so I spent most of the day whipping this up. I have since added a little here and there, but most of this was scrapped together in a day. This in mind, there is a lot here that could be handled cleaner and in a more optimized fashion. I took some short-cuts so that I could finish in the amount of time that I had.
The external runner is a new idea in the hopes of making this more beneficial by actually adding something significant to GameMaker, that being multi-threaded operations.
What Kind Of Functionality:
The language is mostly designed for numbers and math, albeit there are some simple pointer / buffer operations where you can write strings into them.
You have basic math such as addition, division, sqrt, etc. You have bitwise operators, a fractional function as well as several system commands. System commands consist of things like moving data in/out of registers and IOs, storing the current command byte and jump commands. You can do quite a lot with this language and if you want an example then take a look down below for a 2D simplex noise generation script written in this language.
Does This Actually Compile To Native?
No. This does not actually compile down to native computer code. It "compiles" down to a GameMaker buffer where the code is interpreted. It is also not a true assembly language. It does not access actual registers (and in fact things like registers and strings are still handled like a high-level language would), it merely shares the terms. It was a balancing act between simplicity and practicality. I tried to keep it simple to program but still functional enough to be practical to use.
All this said, it is still usable as it remains fairly fast.
Are You Done Yet?
If you made it this far you must at least be somewhat interested. Please, download and take a gander. Play with it, see what you can do. If you have suggestions, additions and/or critiques please let me know! I'd love some feedback.
NOTE: I included a DLL for the external runner for those on Windows. I made sure to only use standard C++ libraries, so if you have a different OS you can compile a version using the provided source code. You shouldn't have to change any code minus perhaps the GMX macro for proper external library creation.
Example Code?
Might be kinda hard to follow, but there are examples of how / when most of the function types are used.
If you don't like reading, here is your download.
Optionally, the source for the external runner.
What Is This:
This is my take on having dynamically added code to my projects post-compile. I essentially designed an assembly-like language that you can program in. I also wrote a compiler for the code in GML and an internal runner (in GML) and external multi-threaded runner (in C++) so that the code can actually be executed.
The language is essentially formed out of roughly 50-60 commands, registers, and IO ports. You can read data into registers from a number of IO ports, manipulate it, then push it back out to GameMaker.
The compiler converts it into a single GameMaker buffer which can then be executed manually or through the runner. The internal runner will limit execution times to attempt to keep the framerate at your desired level while the external runner will run tasks on however many threads you specify.
Why Is This:
I have been contemplating an idea like this for some time. I finally had a free day with little work to do so I spent most of the day whipping this up. I have since added a little here and there, but most of this was scrapped together in a day. This in mind, there is a lot here that could be handled cleaner and in a more optimized fashion. I took some short-cuts so that I could finish in the amount of time that I had.
The external runner is a new idea in the hopes of making this more beneficial by actually adding something significant to GameMaker, that being multi-threaded operations.
What Kind Of Functionality:
The language is mostly designed for numbers and math, albeit there are some simple pointer / buffer operations where you can write strings into them.
You have basic math such as addition, division, sqrt, etc. You have bitwise operators, a fractional function as well as several system commands. System commands consist of things like moving data in/out of registers and IOs, storing the current command byte and jump commands. You can do quite a lot with this language and if you want an example then take a look down below for a 2D simplex noise generation script written in this language.
Does This Actually Compile To Native?
No. This does not actually compile down to native computer code. It "compiles" down to a GameMaker buffer where the code is interpreted. It is also not a true assembly language. It does not access actual registers (and in fact things like registers and strings are still handled like a high-level language would), it merely shares the terms. It was a balancing act between simplicity and practicality. I tried to keep it simple to program but still functional enough to be practical to use.
All this said, it is still usable as it remains fairly fast.
Are You Done Yet?
If you made it this far you must at least be somewhat interested. Please, download and take a gander. Play with it, see what you can do. If you have suggestions, additions and/or critiques please let me know! I'd love some feedback.
NOTE: I included a DLL for the external runner for those on Windows. I made sure to only use standard C++ libraries, so if you have a different OS you can compile a version using the provided source code. You shouldn't have to change any code minus perhaps the GMX macro for proper external library creation.
Example Code?
Code:
; Author: Reuben Shea
; Date: 01-15-2018
;
; Description: This script is written in BIC and will generate
; a block of 2D simplex noise and store the results
; in a buffer as a series of f64s between [-1..1]
;
; Notes:
; - Because values get copied when transfering between
; io and reg ports we reserve some registers for the buffers.
; This prevents slowing down the system with constant re-copying
; of large buffers.
;
; - Macros are, obviously, not required. However, it makes
; things significantly more readable.
; -- REGISTERS -- ;
;
; 0-4: [reserved] mathematical operations
; 5: [reserved] hash buffer
; 6: [reserved] gradient buffer
; 7: [reserved] output buffer
; 8: [float] current x-coord (literal)
; 9: [float] current y-coord (literal)
; 10-18:[misc]
; 19 [int] current array x-coord
; 20 [int] current array y-coord
; -- IO PORTS -- ;
;
; 0: [ptr] Output buffer (as f64s)
; 1: [ptr] Hash buffer (as u8s)
; 2: [ptr] Gradient buffer (as f64s)
; 3: [float] Starting x-coord (literal)
; 4: [float] Starting y-coord (literal)
; 5: [float] Offset scalar (literal, between coords)
; 6: [int] Chunk width (in pixels)
; 7: [int] Chunk height (in pixels)
; -- MACROS -- ;
; IO:
!define io_output 0
!define io_hash 1
!define io_grad 2
!define io_xstart 3
!define io_ystart 4
!define io_scalar 5
!define io_width 6
!define io_height 7
; REG:
; I like defining macros for my math registers
; because you use low numbers often and this helps
; the registers stand out from integers.
!define r0 0
!define r1 1
!define r2 2
!define r3 3
!define r4 4
!define reg_hash 5
!define reg_grad 6
!define reg_output 7
!define reg_x 8
!define reg_y 9
!define reg_ax 19
!define reg_ay 20
; OTHER:
!define f_skew 0.36603 ; Skew factor for the coordinates
!define f_unskew 0.21132
!define count_reg 20 ; Number of registers we will be using
!define count_io 8 ; Number of io ports we will be using
; -- STARTUP CODE -- ;
regioalloc count_reg count_io ; Allocate memory for our registers / io
sinr io_hash reg_hash ; Start loading in our reserved register data
sinr io_grad reg_grad
sinr io_xstart reg_x ; Our current x-location for simplex
sinr io_ystart reg_y ; Our current y-location for simplex
rcpi reg_ax 0
rcpi reg_ay 0
; Allocate memory for our output buffer.
; Instead of passing in a buffer from GameMaker, we just generate it here.
sinr 6 r1 ; Load the width of our chunk into register 1
sinr 7 r2 ; Load in the height to reg 2
rcpi r3 8 ; Write the integer 8 into reg 3
rmul r1 r2 ; Multiply reg1 by reg2 and store in reg0
rmul r0 r3 ; Multiply reg0 by reg3 and now we have the size we need for our buffer!
rpalloc reg_output r0 ; Allocate space for the buffer and store the pointer in reg_output (reg 7)
; -- MAIN LOOP BEGIN -- ;
.MAIN ; We can name our jump-point anything. .MAIN seems appropriate
; NOTES:
; (Keeping notes can help keep track of registers!)
; (These will be important values at the end of this code segment)
; [int] reg12 = x-skew [0..255]
; [int] reg13 = y-skew [0..255]
; [float] reg10 = x-unskew
; [float] reg11 = y-unskew
; [float] reg4 = unskew value
; Calculate skewed coordinates:
radd reg_x reg_y
rcpf r1 f_skew
rmul r0 r1
rcpr r1 r0 ; Store skew value in reg1
radd reg_x r1 ; Calculate skewed x-pos
rftoi r0 ; Simple way to floor a value; convert to int and back!
ritof r0
rcpr r2 r0 ; Store x-result in reg2
radd reg_y r1 ; Calculate skewed y-pos
rftoi r0
ritof r0
rcpr r3 r0 ; Store y-result in reg3
; Calculate unskew values (for later):
radd r2 r3
rcpf r4 f_unskew
rmul r0 r4
rcpr r4 r0 ; Store unskew value in reg4
rsub r2 r4 ; Calculate x-unskew
rcpr 10 r0 ; Store x-unskew in reg10
rsub r3 r4 ; Calculate y-unskew
rcpr 11 r0 ; Store y-unskew in reg11
; Limit our skewed x/y values to [0..255]
rcpi r1 255
rftoi r2 ; Convert our x-value to an int
rand r0 r1 ; Perform a bitwise and
rcpr 12 r0 ; Store the new value
rftoi r3 ; Perform the same with y-value
rand r0 r1
rcpr 13 r0
; -- WORK OUT HASHED GRADIENT INDICES -- ;
; NOTES:
; [float] reg10 = x-distance (from origin)
; [float] reg11 = y-distance (from origin)
; [int] reg14 = hash-value 1
; [int] reg15 = hash-value 2
; [int] reg16 = hash-value 3
; [float] reg17 = x-distance (corner 2)
; [float] reg18 = y-distance (corner 2)
; Calculate distance from cell origin:
rsub reg_x 10 ; Subtract unskew-x from original x
rcpr 10 r0 ; Store result in reg10
rsub reg_y 11 ; Subtract unskew-y from original y
rcpr 11 r0 ; Store result in reg11
; Calculate x vs y based on these distances:
; We get to do an if/else statement here!
rsub 10 11 ; Calculate x - y
jumppn .ifXLY ; If our x - y > 0 then jump to .ifXLY (aka, x > y)
jump .elseXLY ; If we didn't jump then it is false so jump to .elseXLY!
.ifXLY
rcpi r1 1 ; r1 will be our "x", so store a 1!
rcpi r2 0 ; r2 will be our "y", so store a 0!
jump .finXLY ; We want to skip the "else" so we jump past it
.elseXLY
rcpi r1 0 ; Same as above, r1 is our "x" and r2 our "y"!
rcpi r2 1
.finXLY
; Calculate distance for corner 2 down the road since it needs some data from here:
ritof r1
rsub 10 r0
rcpf r3 f_unskew
radd r0 r3
rcpr 17 r0
ritof r2
rsub 11 r0
radd r0 r3
rcpr 18 r0
; Calculate hash 2
; NOTE: We calculate hash 2 first because it is the only one that
; needs the values in r1 and r2 right now. This frees up the
; registers as quickly as possible.
radd r2 13 ; Our "y" to our skewed y
rpgb reg_hash r0 ; Use the result as our hash index (this is not our final hash value)
radd r0 r1 ; Add to our "x"
radd r0 12 ; Add to our skewed x
rpgb reg_hash r0 ; Grab the final hash value using our result as the index
; Now we need to perform hash % 12, this is how we call a function:
rcpr r1 r0 ; Copy the hash into r1 for our function
rcpi r2 12 ; Copy our modulo value into r2 for our function
rcpi r3 6 ; Copy a byte offset to push our byte location past
; our jump function below
rstorloc ; Store our current byte location in the code
radd r0 r3 ; Add the byte offset [radd = 3 bytes, jump = 3 bytes]
jump .funMOD
rcpr 15 r1 ; At this point, our function is done and we store the result for real!
; Calculate hash 1
; This is essentially the same but with slightly simpler math. I
; will skip the comments here.
rpgb reg_hash 13
radd r0 12
rpgb reg_hash r0
rcpr r1 r0
rcpi r2 12
rcpi r3 6
rstorloc
radd r0 r3
jump .funMOD
rcpr 14 r1
; Calculate hash 3
; Same as above, comments shall be skipped.
rcpi r2 1
radd 13 r2
rpgb reg_hash r0
radd r0 r2
radd r0 12
rpgb reg_hash r0
rcpr r1 r0
rcpi r2 12
rcpi r3 6
rstorloc
radd r0 r3
jump .funMOD
rcpr 16 r1
; -- CALCULATE CORNER CONTRIBUTIONS -- ;
; NOTES:
; [float] reg14 = corner-1 contribution
; [float] reg15 = corner-2 contribution
; [float] reg16 = corner-3 contribution
; Calculate corner 1
rcpf r3 0.5
rmul 10 10 ; Square our x-distance
rcpr r1 r0 ; Store result in reg1
rmul 11 11 ; Square our y-distance
rcpr r2 r0 ; Store result in reg2
rsub r3 r1
rsub r0 r2
jumpnn .ifT0L0
jump .elseT0L0
.ifT0L0
; No contribution
rcpf 14 0.0
jump .finT0L0
.elseT0L0
; We are good to contribute. Let's calculate how much:
rmul r0 r0
rmul r0 r0 ; Lots of squaring...
rcpr r4 r0 ; Store this for now in r4
; Now we have to read our gradients, let's get the proper location
rcpi r0 16 ; Buffer of f64 and two values per "section", so compensate for data size
rmul r0 14 ; Multiply by our hash offset
rcpr r1 r0 ; Store our location in r1 (we have to re-use it shortly)
rpgf reg_grad r0
rcpr r2 r0 ; Copy our x-val into r2
rcpi r0 8
radd r1 r0 ; Shift to our next location
rpgf reg_grad r0
rcpr r3 r0 ; Copy our y-val into r3
; Time for a dot product!
rmul r2 10 ; x-coord
rcpr r1 r0 ; Store in reg1
rmul r3 11 ; y-coord
radd r1 r0 ; Add to x-coord
rmul r0 r4 ; Multiply by our stored value from the beginning
rcpr 14 r0 ; And store the result, that's one corner down!
.finT0L0
; Calculate corner 2
rcpf r3 0.5
rmul 17 17
rcpr r1 r0
rmul 18 18
rcpr r2 r0
rsub r3 r1
rsub r0 r2
jumpnn .ifT1L1
jump .elseT1L1
.ifT1L1
; No contribution
rcpf 15 0.0
jump .finT1L1
.elseT1L1
; We are good to contribute. Let's calculate how much:
rmul r0 r0
rmul r0 r0 ; Lots of squaring...
rcpr r4 r0 ; Store this for now in r4
; Now we have to read our gradients, let's get the proper location
rcpi r0 16 ; Buffer of f64, so compensate for data size
rmul r0 15 ; Multiply by our hash offset
rcpr r1 r0 ; Store our location in r1 (we have to re-use it shortly)
rpgf reg_grad r0
rcpr r2 r0 ; Copy our x-val into r2
rcpi r0 8
radd r1 r0 ; Shift to our next location
rpgf reg_grad r0
rcpr r3 r0 ; Copy our y-val into r3
; Time for a dot product!
rmul r2 17 ; x-coord
rcpr r1 r0 ; Store in reg1
rmul r3 18 ; y-coord
radd r1 r0 ; Add to x-coord
rmul r0 r4 ; Multiply by our stored value from the beginning
rcpr 15 r0 ; And store the result, that's one corner down!
.finT1L1
; Calculate corner 3
; We have to calculate the distance for this corner first and store it in r17, r18
rcpf r2 1.0
rcpf r3 2.0
rcpf r4 f_unskew
; Calculate x-distance
rsub 10 r2
rcpr r2 r0
rmul r3 r4
radd r0 r2
rcpr 17 r0
; Calculate y-distance
rcpf r2 1.0
rsub 11 r2
rcpr r2 r0
rmul r3 r4
radd r0 r2
rcpr 18 r0
rcpf r3 0.5
rmul 17 17
rcpr r1 r0
rmul 18 18
rcpr r2 r0
rsub r3 r1
rsub r0 r2
jumpnn .ifT2L2
jump .elseT2L2
.ifT2L2
; No contribution
rcpf 16 0.0
jump .finT2L2
.elseT2L2
; We are good to contribute. Let's calculate how much:
rmul r0 r0
rmul r0 r0 ; Lots of squaring...
rcpr r4 r0 ; Store this for now in r4
; Now we have to read our gradients, let's get the proper location
rcpi r0 16 ; Buffer of f64 and 2 values per "section" so compensate for data size
rmul r0 16 ; Multiply by our hash offset
rcpr r1 r0 ; Store our location in r1 (we have to re-use it shortly)
rpgf reg_grad r0
rcpr r2 r0 ; Copy our x-val into r2
rcpi r0 8
radd r1 r0 ; Shift to our next location
rpgf reg_grad r0
rcpr r3 r0 ; Copy our y-val into r3
; Time for a dot product!
rmul r2 17 ; x-coord
rcpr r1 r0 ; Store in reg1
rmul r3 18 ; y-coord
radd r1 r0 ; Add to x-coord
rmul r0 r4 ; Multiply by our stored value from the beginning
rcpr 16 r0 ; And store the result, that's one corner down!
.finT2L2
; -- SCALE FINAL RESULT -- ;
; Now we can add up and scale the result between [-1..1]
; NOTES:
; [float] reg10 = final result
rcpf r1 70.0
radd 14 15
radd r0 16
rmul r0 r1
rcpr 10 r0
; Store the result in our final buffer:
; Calculate which value in the buffer to write to:
sinr io_width r1
rmul r1 reg_ay
radd r0 reg_ax
rcpi r1 8 ; Values in buffer are floats, so we have to scale by 8 bytes
rmul r1 r0
rcpr r1 r0 ; Our byte location is stored in reg1
rpsf reg_output r1 10 ; Write our final result to the buffer!
; -- DETERMINE LOOP -- ;
; We need to increment our loop bounds and determine if
; we are done with our total calculation.
rcpi r1 1
radd reg_ax r1 ; Add 1 to our x array coord
rcpr reg_ax r0
sinr io_width r2 ; Grab the width of our chunk
rsub r2 reg_ax
jumpp .ifINCY
jump .elseINCY
.ifINCY
; We reached our bounds, so let's loop it around
rcpi reg_ax 0
rcpi r1 1
radd reg_ay r1
rcpr reg_ay r0
; Handle our actual simplex coordinate adjustment:
sinr io_scalar r1
radd reg_y r1
rcpr reg_y r0
sinr io_xstart reg_x
; Since we know we incremented y, let's check if
; we are completely done with our loop:
sinr io_height r1
rsub reg_ay r1
jumpp .FINAL ; Jump to the end if we are ready
jump .finINCY ; If not, just finish the IF loop
.elseINCY
; We DIDN'T reach our bounds, so just modify simplex coord
sinr io_scalar r1
radd reg_x r1
rcpr reg_x r0
.finINCY
jump .MAIN ; We are all set for the next calculation, start over!
; -- OUTPUT FINAL BUFFER -- ;
.FINAL
srout io_output reg_output ; Write it IO port
end ; Terminate code
; -- MODULO FUNCTION -- ;
; INPUTS:
; [int] reg0 = return seek byte
; [int] reg1 = value to operate on
; [int] reg2 = modulo value
;
; MODIFICATIONS:
; [int] reg1 = updated value
; [int] reg3 = misc values
.funMOD
rcpr r3 r0 ; Copy our return location into reg3
ritof r1 ; Convert to floats so we can get decimal points
rcpr r1 r0
ritof r2
rcpr r2 r0
rsub r1 r2 ; Check if our value is larger than the modulo value
jumpnn .funMOD_finVLM
.funMOD_ifVLM ; Just put this here for readability
rdiv r1 r2 ; Divide our value
rfrac r0 ; Grab the fractional result
rmul r0 r2 ; Multiply it by our modulo
rcpr r1 r0 ; Store our final result
.funMOD_finVLM
rftoi r1 ; Convert back to an int
rcpr r1 r0
rcpr r0 r3 ; Copy our byte-value back into r0
jumpr ; Jump to our byte-value
.fun_finMOD ; Again, just here for readability
Might be kinda hard to follow, but there are examples of how / when most of the function types are used.
Last edited: