fastspin compiler for P2: Assembly, Spin, BASIC, and C in one compiler

18911131432

Comments

  • jmg wrote: »
    ersmith wrote: »
    .... Mainly it's useful because it's written in C and doesn't use much of the standard library, so it was a good platform for testing fastspin's C support and shaking out bugs (which it did a lot of).

    Do you know which C version that Lisp interpreter required ? Maybe that is already a C99 test ?

    I wrote the Lisp interpreter several years ago for P1, and it was generic C89. I have compiled some programs that use C99 features, though (like fft_bench).

    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
  • Another piece that can use some work is the IDE; spin2gui is just a toy that's designed to get people working with fastspin easily. It looks like @Rayman 's SpinEdit program is a great solution for Windows, and I think it's been made to work on Linux and the Mac using Wine. It might also be nice to be able to use fastspin with SimpleIDE and/or PropellerIDE. I think that shouldn't be too hard? But it's not something I have time to look at.
    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
  • ersmith,
    By porting libraries I assume you mean C standard library stuff, right? I found this recently: https://github.com/PetteriAimonen/Baselibc it's BSD license and meant to be very small. Perhaps we can just use it?

    I don't have a lot of time, so I can't promise much, but I will try to use the C stuff in your compiler more. Perhaps take a stab at getting some of the Simple Libraries compiling and working? Maybe, the above libc?
  • Roy Eltham wrote: »
    ersmith,
    By porting libraries I assume you mean C standard library stuff, right? I found this recently: https://github.com/PetteriAimonen/Baselibc it's BSD license and meant to be very small. Perhaps we can just use it?
    Well, I was thinking both of the standard C library and the Simple libraries (and anything else Blockly needs).

    For the standard library I think it makes sense for us to port the PropGCC library,since that already works on the Propeller and it's small and efficient. The stdio interface is probably the only part that'll be tricky, and I'd like to change that anyway (in PropGCC we used FILE handles directly instead of going through Unix like open(), read(), write(), and that ended up being kind of awkward).

    I'm not too familiar with the Simple libraries, so it'd be great if someone else could take a look at those and see what needs to be done to port them.

    Thanks,
    Eric
    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
  • I don't get it. I have not used your tool but I thought you said it does not support linking.

    So does that mean every time I need to create a program everything get compiled again.

    Mike
  • iseries wrote: »
    I don't get it. I have not used your tool but I thought you said it does not support linking.

    So does that mean every time I need to create a program everything get compiled again.

    Yes, everything that's used in the program is compiled together. This allows the compiler to do whole-program optimization, including things like inlining any eligible functions and removing unused functions.

    People have expressed concern that this will lead to slow compile times. I haven't found this to be an issue so far, for two reasons: (1) only the functions that are actually referenced are compiled, and (2) the largest possible program for the P1 is 32KB, and for P2 is 512KB. By today's standards those are miniscule, and modern computers should easily be able to compile such programs quickly.

    If it becomes an issue then I'll revisit the linking question. For now I'm trying to get something that works first, then I'll look at optimizing it later.

    Regards,
    Eric
    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
  • Well with Arduino I wait 5 minutes while it complies everything and it's a pain after a while.

    Also a lot of the custom libraries have test code that is not compiled into the finished library. That will be hard to hide.

    Mike

  • iseries wrote: »
    Well with Arduino I wait 5 minutes while it complies everything and it's a pain after a while.
    I think $THEY rebuild all libraries used in your project even for the slightest change in your sources.
    If you want to avoid this, use AVR-GCC, make and friends without the Arduino-IDEEditor.
    ◁ propeller-wiki ▷ ◁ FastSpin ▷ ◁ Help Spin at RosettaCode.org ▷ ◁ No Source – No Go! ▷ ◁ DK-E ▷ ◁ :-D ▷ ◁ Stay OmmmmmmPtimistic! ▷ ◁ Why Asimov's Laws of Robotics Don't Work ▷ ◁ DNA is a four letter word. ▷ ◁ Stop slavery! Free all mitochondria! ▷
  • iseries wrote: »
    Well with Arduino I wait 5 minutes while it complies everything and it's a pain after a while.

    Also a lot of the custom libraries have test code that is not compiled into the finished library. That will be hard to hide.

    I don't know about Arduino: it sounds like they're doing things wrong. It's a different platform, using a different compiler, and compiling different libraries, so I'd say it may end up performing differently than fastspin.

    I'd rather have a slow C compiler than no C compiler at all, so I'm just going to keep on developing fastspin and fix problems as they occur, rather than trying to forsee every possible issue and spending years trying to plan everything out. I'm not idealogically opposed to a linker, and if we had a standard one for the P2 I could use that. Perhaps you'd be good enough to port GNU binutils to the P2? Both p2gcc and fastspin could benefit from that.

    Regards,
    Eric
    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
  • David BetzDavid Betz Posts: 13,589
    edited 2019-02-04 - 17:09:27
    ersmith wrote: »
    Perhaps you'd be good enough to port GNU binutils to the P2? Both p2gcc and fastspin could benefit from that.
    We have yet to decide if we want to use GAS or if we want to try to use Dave's p2asm assembler or your fastspin assembler. If we're going to use p2asm or fastspin, we need to get whichever one we choose to produce ELF files that are acceptable to the GNU linker and also modify the GCC driver program to invoke it instead of GAS.

    Edit: Well, I guess we don't need to modify the GCC driver program if we're not using the GCC C/C++ compiler.

  • ersmith,
    I'll start looking at the Simple Libraries for porting. I'm going to assume that we want to make them able to work with both P1 and P2, since your compiler targets both.
  • jmgjmg Posts: 14,094
    ersmith wrote: »
    People have expressed concern that this will lead to slow compile times. I haven't found this to be an issue so far, for two reasons: (1) only the functions that are actually referenced are compiled, and (2) the largest possible program for the P1 is 32KB, and for P2 is 512KB. By today's standards those are miniscule, and modern computers should easily be able to compile such programs quickly.

    Yes, PCs are easily fast enough today, to build-all, and that does make for more predictable results and safer archiving and version control.
    The only time linking is useful, is when your compiler either cannot compile all the sources, or cannot access all the sources.
    Even in the latter case, a relocatable object file could be considered to be 'source' so you could 'compile' that to achieve apparent linking ?

  • Roy Eltham wrote: »
    ersmith,
    I'll start looking at the Simple Libraries for porting. I'm going to assume that we want to make them able to work with both P1 and P2, since your compiler targets both.

    That sounds good Roy. Thanks!
    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
  • twm47099twm47099 Posts: 822
    edited 2019-02-08 - 20:30:44
    I've been using ADCs to learn the P2 tools. I've run the MCP3204 in spin2gui basic and TAQOZ, and run ADC using smart pins in spin2+p2asm, and TAQOZ. I've been wanting to do it in basic, but I don't think there are Basic commands for the smart pin functions.

    Looking at ersmiths asm example above, I decided to give basic + p2asm a try. The code below works using pin 22 as a smart pin. I will eventually upgrade it to auto calibrate, but I wanted to show it as is so maybe there will be some others using smart pins with spin2gui basic.

    Any suggestions on how to simplify or improve the code are welcome.

    Tom
    ' Use Smart Pin in ADC Mode
    
    ' clock will be set to 180MHz
    ' set terminal baud to 230400
     const _mode = 0x010c4708
     const _clkfreq = 180_000_000
     clkset(_mode, _clkfreq)
     _setbaud(230400)
    
    ' **** Constants
     const ADC_MODE = 0b0000_0000_000_100011_0000000_00_01111_0
     const ADC_PIN = 22
     const ADC_CYCLES = 16384 
     const valmin = 2850
     const valmax = 13343
    
    '  **** variables for demo (calling) program
     dim raw1
     dim value1
     dim volts#
    
    '  **** variables needed in calling program for function fadc()
     dim stack(64)
     dim shared flag = 0	' 0 = raw data not ready, 1 = ready
    		' flag=0, calling program waits until flag=1, and sets flag=0
    		' when data is taken. 
    
    ' ****************************
    function fadc()		' ***** define function fadc()
    '	returns: raw ADC value 
    '	flag = 0 data not ready, 1 = data ready, wait until taken
    ' ****************************
     dim time, val		' **** define fadc locals
    
    ' **** in fadc use inline asm to setup & start ADC smartpin
    '      ADC_MODE, ADC_PIN, and ADC_CYCLES, and set ADC_PIN dir high
    asm
    	wrpin   ADC_MODE, ADC_PIN
    	wxpin	ADC_CYCLES, ADC_PIN 
    	wypin	0, ADC_PIN  
    	dirh    ADC_PIN
    	getct  time
    	addct1  time, ADC_CYCLES
    end asm
    
    while 1		' **** basic infinite repeat
      pausems 100	'   wait sufficient time for ADC_CYCLES
    
    ' **** get smartpin reading
    ' ***** start 2nd asm, rdpin and put value in local
    asm
    	waitct1
    	addct1	time, ADC_CYCLES
    	rdpin	val, ADC_PIN
    end asm
    
    	' *** put local in global & use flags as needed
      value1 = val
      flag = 1		' tell calling cog data is ready
        while flag = 1	' wait until calling cog takes data
        wend
    wend	' loop back to basic infinite repeat
    end function
    
    ' *************************
    ' ***** Test fadc() function
    
    ' **** call fadc in new cog
     var a = cpu(fadc(), @stack(1))	' run function in new cog, a is cog number
     pausems(20)			' time for cog to start
    while 1	
      while flag = 0		' wait until data ready
      wend			
      raw1 = value1 
      volts# = (raw1 - valmin) * 3300 / (valmax - valmin)
      volts# = volts# / 1000
      flag = 0			' data received
      print "raw1 = "; raw1, "volts = "; volts#
      pausems(300)
    wend
    
    
  • I corrected the code in the post above. I had copied the voltage calculation from my MCP3204 program which used 5v as reference. The smartpin version uses 3.3 v.
    Tom
  • There are new releases of spin2gui and fastspin. This release has Dave Hein's latest loadp2, and a bunch of bug fixes and improvements to fastspin. The fastspin changes are more focused on BASIC and Spin this time around. Highlights are:

    - Support for old school BASIC programs with line numbers. This is still a bit incomplete, but it's pretty easy to port programs from www.classicbasicgames.org, for example

    - Support for shorter BASIC lambda expressions (kind of the opposite end of the pole from the above)

    - A new Spin construct, "PUB FILE", which allows you to import functions from another file and even another language. For example, to add an "atoi" method to your object you can write:
      PUB FILE "libc/stdlib/atoi.c" atoi(x)
    
    This will fetch the definition of atoi from the C library "atoi.c" file and include it as a method of the curent Spin object.

    - Lots of bug fixes
    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
  • ersmith wrote: »
    If it becomes an issue then I'll revisit the linking question. For now I'm trying to get something that works first, then I'll look at optimizing it later.
    ersmith wrote: »
    I'd rather have a slow C compiler than no C compiler at all

    "Premature optimization is the root of all evil" -- Donald Knuth, 1974 Turing Award Lecture.
    (So important that he says it twice)
  • twm47099twm47099 Posts: 822
    edited 2019-02-14 - 05:03:32
    @ersmith
    I am having problems with the latest version of spin2gui (v1.3.7) running the smartpin adc basic program 4 posts above this one. The program runs fine in v1.3.6.

    The errors I get are in the function fadc(). I've copied that section below and marked the lines that are shown as errors with +++++.
    The first error is:
    C:/Users/tandj/Documents/robot/P2ES/Tests/smartpin_adc.bas(46) error: syntax error, unexpected while, expecting end
    

    Then I commented out line 46 and all following lines up to the next +++++ and got this error:
    ... /smartpin_adc.bas(58) error: syntax error, unexpected identifier `value1', expecting end
    
    I'm not sure if there are any other errors.

    **edit** I also tried just commenting out the while 1 on line 46. I got an error referring to the following line
    pausems 100
    
    So it appears that anything after the
    end asm
    
    causes an error.

    Any ideas what is(are) the problem(s)?(is it supposed to only allow 1 in-line asm and then end the function?)


    Tom
    function fadc()		' ***** define function fadc()
    '	returns: raw ADC value 
    '	flag = 0 data not ready, 1 = data ready, wait until taken
    ' ****************************
     dim time, val		' **** define fadc locals
    
    ' **** in fadc use inline asm to setup & start ADC smartpin
    '      ADC_MODE, ADC_PIN, and ADC_CYCLES, and set ADC_PIN dir high
    asm
    	wrpin   ADC_MODE, ADC_PIN
    	wxpin	ADC_CYCLES, ADC_PIN 
    	wypin	0, ADC_PIN  
    	dirh    ADC_PIN
    	getct  time
    	addct1  time, ADC_CYCLES
    end asm
    
    ' +++++ the line below is line (46) referred to in the first error statement.
    while 1		' **** basic infinite repeat
    
      pausems 100	'   wait sufficient time for ADC_CYCLES
    
    ' **** get smartpin reading
    ' ***** start 2nd asm, rdpin and put value in local
    asm
    	waitct1
    	addct1	time, ADC_CYCLES
    	rdpin	val, ADC_PIN
    end asm
    
    	' *** put local in global & use flags as needed
    ' +++++ the next line is line 58 where the 2nd error was reported. 
      value1 = val
    
      flag = 1		' tell calling cog data is ready
        while flag = 1	' wait until calling cog takes data
        wend
    wend	' loop back to basic infinite repeat
    end function
    
    
  • I tried commenting out everything after the
    end asm
    
    for the first asm block and got error messages for each of the asm lines that used constants stating that they had to be immediates. I put # in front of each constant and that error went away.
  • @twm47099 : Your program is (mostly) OK, you just ran into a bug in the new BASIC parsing code. Due to a typo in the grammar the "end asm" has to be the last thing in a function. I didn't notice this because (a) it only affects BASIC and (b) I usually put inline asm into small functions that don't do anything else, and rely on the compiler to inline those. But it should have worked to have multiple inline asms in one function.

    I've attached an updated fastspin.exe to this post; if you replace the one in spin2gui/bin/fastspin.exe with the one from this zip file it should correct that problem.

    The immediates before constants was something that should always have been there, unless you really intended to directly access locations in COG RAM. The old inline assembly parser was kind of lax about this, if it saw a number or a constant it just assumed you wanted an immediate. But in fact PASM does allow you to directly specify a COG memory location, so the new parser does distinguish between these. This was only ever an issue in inline assembly, regular assembly in DAT blocks always required # for immediate constants and could handle COG memory addresses. The inline assembly parser is a bit different because it has to cope with local variables, and that's where the confusion came in.
    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
  • @ersmith,
    Thanks. The fixed fastspin worked.
    Re the use of immediate constants. When I originally wrote the program, I lifted the P2asm from a spin2+asm program. The original had the constants prefaced by ##. I wasn't sure how basic would deal with constants and even if they had to be declared in the function or not. So I intended to just try different ways until it worked. Using v1.3.6 it worked on my first try (in "main" without #). I agree the version 1.3.7 is more consistent (although would it ever require ##)?

    Rereading the Basic documentation, I am confused about a few things.
    I don't understand how shared and member variables work. In the program above I declare value1 as
    dim value1
    
    . That variable is used in bot the function running in the second cog and in the first cog to transfer its value from the function to "main". In my most recent version I have changed it to
    dim shared value1
    

    I originally defined flag the same way, but changed it to
    dim shared flag
    
    . It is used to signal each cog when the other is ready for a transfer.

    In all cases the program ran correctly. When would it make a difference?


    Where are variables stored? (HUB, cog?) How about constants?

    Thanks for your help
    Tom
  • twm47099 wrote: »
    @ersmith,
    Thanks. The fixed fastspin worked.
    Re the use of immediate constants. When I originally wrote the program, I lifted the P2asm from a spin2+asm program. The original had the constants prefaced by ##. I wasn't sure how basic would deal with constants and even if they had to be declared in the function or not. So I intended to just try different ways until it worked. Using v1.3.6 it worked on my first try (in "main" without #). I agree the version 1.3.7 is more consistent (although would it ever require ##)?
    In general you should use the same syntax as in Spin2+PASM. So ## would probably be the best thinig to put in there, although as you've discovered the inline assembler is more forgiving and will accept just # (and convert it to ## if necessary).

    That raises an interesting difference with the inline assembler: it goes through the optimizer, so it does change things in the code (like converting ## to # if it can). In Spin2 DAT sections this is not the case, if you write ## then you'll always get an AUG prefix (so the instruction will really take 8 longs and 4 cycles) even if the immediate would fit in 4 bytes.
    Rereading the Basic documentation, I am confused about a few things.
    I don't understand how shared and member variables work. In the program above I declare value1 as
    dim value1
    
    . That variable is used in bot the function running in the second cog and in the first cog to transfer its value from the function to "main". In my most recent version I have changed it to
    dim shared value1
    

    "dim shared" always puts things in HUB, and there is only one copy shared by all instances of the object. It's like a value in a DAT section in Spin. Just plain "dim" usually goes in HUB (unless you use a special compiler flag) but it's like a member variable in Spin, each copy of the class gets its own copy of the variable. For the top level program this doesn't really matter, both "dim shared" and "dim" will end up being pretty similar. It matters only if you want to write re-usable classes in BASIC.
    Where are variables stored? (HUB, cog?) How about constants?

    Shared variables always go in HUB. "dim" at the top level (outside sub or function declarations) declares a member variable, which goes in HUB. "dim" inside a sub or function creates a local variable, which usually goes in COG memory unless you take its address or there's something else funny going on in the function, in which case it'll go on the stack in HUB memory.

    Constants usually don't end up being "stored" anywhere, they're encoded directly in the instruction as an immediate value.

    Regards,
    Eric
    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
  • Eric,
    Thanks for the info. I am trying to understand more how these compilers work and how to best use them.

    In my program I have 2 in-line asm segments in the function. The first "turns on" the adc smart pin. The second is embedded in a BASIC while loop. The asm reads the value from the smart pin and stores it in a variable and then exits the asm. The BASIC stores the local (adc value) into the shared variable that is then read by a different cog. After testing if the value was retrieved by the cog, the while loop then repeats.

    If I read the documentation correctly (along with what you had posted earlier in the thread) I can't have the asm store the rdpin result into a global. Is that correct. Is there any way to start the smart pin and then loop in asm to directly send the result to the calling cog or is the way I'm doing it reasonable. I like my method because I can see what is happening.

    I looked at using pointers, but just ended up confusing myself.

    Thanks
    Tom
  • twm47099 wrote: »
    In my program I have 2 in-line asm segments in the function. The first "turns on" the adc smart pin. The second is embedded in a BASIC while loop. The asm reads the value from the smart pin and stores it in a variable and then exits the asm. The BASIC stores the local (adc value) into the shared variable that is then read by a different cog. After testing if the value was retrieved by the cog, the while loop then repeats.
    That sounds pretty reasonable.

    In general I like to keep the inline asm to a minimum, because the compiler produces pretty decent code on its own. So usually I just use inline asm to do some instruction that isn't built in to the compiler, and then do all the looping and variable update / manipulation in a high level language.

    You could try to mess around with pointers and having the asm store directly with wrlong, but it's likely you'll end up with the same code anyway and it'll be more confusing :). The compiler will optimize the combination of your code inline code and the code it's creating from BASIC, so for example:
    dim shared pinval
    
    sub getpin(p)
      dim val as integer
      dim dummy as integer
      dim one
      one = 1  ' redundant, we can just use #1 in the rdpin
      asm
        rdpin val, one
      end asm
      dummy = val  ' redundant copy
      pinval = dummy
    end sub
    
    compiles to something like:
    _getpin
            rdpin   _var01, #1
            wrlong  _var01, ptr__dat__
    _getpin_ret
            reta
    
    (Unless you turn off the optimizer, but I wouldn't usually do that unless you're hunting for a bug.)

    Given that, it's probably best to write the code in whatever way is simplest and clearest for you, and not to worry too much about optimizing or about writing as much as possible in assembly.

    Regards,
    Eric
    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
  • cheezuscheezus Posts: 266
    edited 2019-02-16 - 02:32:26
    I think I've found a bug compiling Spin for P1. :open_mouth:
    '' doesn't set clock to output
       outa[clk] := outa[di] := outa[cs] := 1
       dira[clk] := dira[di] := dira[cs] := 1
    
    ' doesn't set di output
       outa[clk] := outa[di] := 1
       outa[cs] := dira[clk] := 1
       dira[di] := dira[cs] := 1
    
    '' but this works
       outa[clk] := 1
       outa[di] :=  1
       outa[cs] :=  1
       dira[clk] := 1
       dira[di] := 1
       dira[cs] := 1
    
    

    * edit - this was with Fastspin V3.9.19, pretty sure that's the latest. I also keep getting "error: Unknown symbol COUNT_" when I try to compile the file in question as the root object?
  • ersmith wrote: »
    twm47099 wrote: »
    @ersmith,

    That raises an interesting difference with the inline assembler: it goes through the optimizer, so it does change things in the code (like converting ## to # if it can). In Spin2 DAT sections this is not the case, if you write ## then you'll always get an AUG prefix (so the instruction will really take 8 longs and 4 cycles) even if the immediate would fit in 4 bytes.

    8 bytes and 4 cycles, right?
  • cheezus wrote: »
    I think I've found a bug compiling Spin for P1. :open_mouth:
    '' doesn't set clock to output
       outa[clk] := outa[di] := outa[cs] := 1
       dira[clk] := dira[di] := dira[cs] := 1
    
    Thanks for the bug report! I think I have a fix for that, and it'll be in the next release (which should be very soon).
    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
  • AJL wrote: »
    ersmith wrote: »
    That raises an interesting difference with the inline assembler: it goes through the optimizer, so it does change things in the code (like converting ## to # if it can). In Spin2 DAT sections this is not the case, if you write ## then you'll always get an AUG prefix (so the instruction will really take 8 longs and 4 cycles) even if the immediate would fit in 4 bytes.

    8 bytes and 4 cycles, right?

    Yes sorry, typo. :)
    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
  • There are new releases of fastspin and spin2gui. The changes are mainly bug fixes to fastspin:

    Version 3.9.20
    - Allow closures to access variables and functions in the enclosing object
    - Fixed some parsing problems with inline assembly
    - Fixed incorrect inlining of some functions in -O2
    - Fixed some C parser errors
    - Fixed some C type conversion errors
    - Fixed encodings of setpiv and setpix instructions
    - Fixed a bug in chained assignments of register ranges
    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
  • There is a new release of spin2gui and of fastspin. The changes are:

    - Increased stack size on Windows to prevent a crash
    - Added if_00, if_same, and other missing P2 conditions
    - Fixed an incorrect result for ">| 0" on P2
    - Added some P2 specific optimizations
    - Added Spin serial drivers to default include directory
    - Rewrote C static and variable handling code to fix a number of bugs and to allow nested variable scopes to work
    - Fixed an optimizer bug where register masks could be created before the variables they dependend on were initialized
    - Fixed a P2 optimizer bug affecting varargs
    - Fixed several C type casting bugs including conversion of arrays and functions to pointers
    - Fixed handling of enums in C
    - Fixed checking for duplicate labels
    - Added some standard Spin serial routines to the include/spin folder
    FlexGUI, a GUI for programming the P1 and P2 in Spin, PASM, BASIC, and C.
    Help support its development at Patreon or PayPal.
Sign In or Register to comment.