====== VBASIC tutorial ====== ===== Foreword ===== In this document we don't want to introduce you to BASIC programming language. We assume that you know it well enough to write more sophisticated programs than the immortal ''10 PRINT "HELLO, WORLD!"\\ 20 GOTO 10'' first programming experience of every BASIC programmer. With this assumption in mind we want to give you a peek into areas where VBASIC can quickly become one of your closest friends: * general BASIC programming at somewhat lower level((Mandatory for sound and graphics)), where the built-in programming aids like full support for haxadecimal numbers, 16-bit PEEKs and POKEs, as well as LO/HI functions come in very much handy. Even without BeamRacer installed * adding stunning visual effects to BASIC programs thanks to full VASYL assembly support((Obviously requires BeamRacer expansion installed in order to see them in action)) * rapid prototyping of timing-sensitive visual effects for award winning demos or games, testing which would normally require a lot of boilerplate code * verification and debugging of VASYL programs thanks to built-in VASYL memory monitor/disassembler Whichever is your cup of tea - create and enjoy! ===== Structuring your programs ===== In most cases the way programs are structured is a result of the programmer's style and his way of thinking/tackling the problem. BASIC as a programming language does not impose many strict rules on how programs have to be structured so we only give you a few hints, which we consider to be good practices. ==== Initialisation ==== Assuming that the machine, with all its components is in a particular state when starting your program is one of the common bad programming practices in general. Unexpected results, which may stem from this assumption are often hard to explain and debug. Do yourself a favour and initialise BeamRacer to a known state at the very beginning of your program: 10 RACER 0: RACER 2 makes sure that BeamRacer hardware is reinitialised into a known, default state and will behave the same every time you ''RUN'' your program. > But why not to simply issue a ''DLOFF'' in the first line instead? Please note that the ''DLOFF''/''DLON'' combination does not reinitialise all of the chip's internal structures. ''DLOFF'' is best suited to //temporarily// stop already running displaylist processing. Like when we need to change displaylists at runtime. Obviously ''DLON'' alone is also indispensable once everything is ready to roll. ==== Parallelism ==== You didn't envision yourself having to deal with parallel computing problems on a C64, did you? ;-) Things changed. Now you have to make sure that both processors (the veteran 6510 and the new kid on the block VASYL) work together as a team rather than against each other. * make sure that whatever you write to from BASIC/6510 is not a subject to writes by VASYL: * VASYL memory * VASYL registers * VIC registers * turn off displaylist processing with a ''DLOFF'' statement whenever you need to modify displaylists, which may also be modified by VASYL from within the displaylist itself * keep a close eye on potential parallel use of VASYL ''PORTx'' (and related ''ADRxL/H'', ''STEPx'') registers! Neglecting this can lead your program into lalaland in no time! The least error-prone approach is to set the displaylist and other VASYL memory data in their entireties, before starting displaylist processing with ''DLON'' statement. But we know that it is often unfeasible so having the above points in mind may save you lots of debugging time. ==== Displaylist programming ==== Another common, yet easy to avoid pitfall is incorrect opening/closing displaylist programming sections. The author's preferred way is to * always configure VBASIC "conveyor" with appropriate ''VCFG'' statement * always close displaylist off with a ''VEND'' statement * start displaylist * ''END'' the program before writing a single line of the actual code: 10 RACER 0:RACER 2 100 VCFG 0,0,1 999 VEND 1000 DLON 1999 END is a typical starting skeleton of a small to medium-sized VBASIC program. In this approach lines * ''10-99'' are used for initialisations / ''GOSUB''s into subroutines delivering whatever may be needed to get pre-initialised, pre-calculated or pre-loaded from external storage * ''100-999'' carry displaylist assembly * ''1000-1999'' hold code that is executed in parallel while the displaylist is already running * ''2000-'' get filled with subroutines as needed This lines numbering scheme is of course just an example of the author's proven preference. The real takeaway here is: - initialise things upfront - never forget to configure the "conveyor(s)" you're going to use - never forget to close the displaylist off with a ''VEND'' - actually starting displaylist processing with a ''DLON'' may help in some "nothing works" cases ;-) ===== Common patterns ===== ==== Transferring displaylists ==== The regular, most common way of storing displaylist programs in VASYL memory is to use the built-in displaylist assembler in conjunction with a preconfigured conveyor pointing to the memory address of choice. In a simple example this will look like: 10 RACER 0:RACER 2 100 VCFG 0,0,1 110 VWAIT $32,0 120 VMOV $20,15 130 VDELAYV 1 140 VMOV $20,6 150 VDELAYV 200 160 VMOV $20,15 170 VDELAYV 1 180 VMOV $20,14 999 VEND 1000 DLON 1999 END Here above lines 110 to 999, when executed, will assemble and store our displaylist in VASYL memory starting at address $0000. The same approach works well for much larger programs too but displaylist programs can also be prepared separately and stored/loaded as needed from storage medium like disk drive for example. We'll cover these more advanced techniques later. ==== Transferring VASYL data ==== The VBASIC built-in displaylist assembler is great for inline assembling and storing even relatively large VASYL programs. But what about other types of data? Transferring the non-executable parts of data to VASYL memory can be tackled in a few ways. The choice of the most suitable one depends on the amount of data, which needs to be transferred. === Direct VPOKE === Small data sets can be easily ''VPOKE''d into VASYL memory 1000 REM ALL 16 COLOURS TO $0100 IN ORDER 1010 FOR I=0 TO 15 1020 VPOKE $100+I,I 1030 NEXT I or 1000 REM ALL 16 COLOURS TO $0100 OUT OF ORDER 1010 FOR I=0 TO 15 1020 READ C 1030 VPOKE $100+I,C 1040 NEXT I 2000 DATA 1,4,3,6,12,2,15,14 2010 DATA 7,9,13,10,0,11,8,5 But you will quickly notice that the larger the dataset, the more visible lack of efficiency of this approach becomes. === VDATA === Step up a notch in efficiency is the use of ''VDATA''. This keyword along with machine code behind it was designed specifically to be far more efficient in transferring medium-sized datasets. 1000 VCFG0,$100 1010 VDATA 0,127,0,1,255,192,3,255,224,3,231,224 1020 VDATA 7,217,240,7,223,240,7,217,240,3,231,224 1030 VDATA 3,255,224,3,255,224,2,255,160,1,127,64 1040 VDATA 1,62,64,0,156,128,0,156,128,0,73,0,0,73,0 1050 VDATA 0,62,0,0,62,0,0,62,0,0,28,0 Transfers quickly the Commodore Air Force One Airship to $0100 in VASYL RAM, while: 5320 VCFG 1,$1F00,1 5330 VDATA "CACACACBCCCDCDCDCDCCCBCA" 5340 VDATA "CACACACBCCCDCDCDCDCCCBCB" 5350 VDATA "CACACACACBCCCDCDCDCDCCCB" 5360 VDATA "CACACACACBCCCDCDCDCDCDCC" 5370 VDATA "CBCACACACACBCCCDCDCDCDCC" 5380 VDATA "CBCACACACACBCCCCCDCDCDCD" 5390 VDATA "CCCBCACACACACBCCCDCD" sends another dataset $1F00 thereto. In order to make ''VDATA'' more versatile than regular ''DATA'', we made it accept binary values in three forms: * decimal numbers ranging from ''0'' to ''255'' * hexadecimal numbers ranging from ''$00'' to ''$FF'' * [[wp>Little-endian|LE]] strings of hexadecimal values (as seen in the last example above) Using ''VDATA'' is an order of magnitude faster than plain ''DATA'' ''READ''ing and ''VPOKE'''ing. It is best suited for up to medium-sized datasets. OTOH there are times when you want to send larger sets of data to VASYL RAM. Typical example would be graphics/bitmap data, which needs to be placed in VASYL memory before it can be used as source for the Programmable Bitmap Sequencer for instance. Storing all those bits in ASCII encoded form of a BASIC program might be((Size dependent, of course)) possible but surely a far cry from efficient use of available resources. Chiefly BASIC RAM but also LOADing and processing time((ASCII encoded binary will be over twice the actual binary size in the **best** case)). === LOAD and COPY === Another notch up, especially useful for dealing with large amounts of data - is to ''LOAD'' the data into a memory buffer available within the main CPU address range and then ''COPY'' it from there to VASYL RAM. This requires more thorough knowledge of the Commodore 64 memory layouts as well as a bit of extra work needed to handle ''LOAD''ing((as opposed to opening the file manually and reading data from it byte by byte)) behaviour when called from within a running BASIC program. Specifically the fact that BASIC does not continue execution of current program once ''LOAD''ing finishes but returns to its beginning and continues from there. The old, plain BASIC pattern for handling this behaviour remains valid with VBASIC: * set a ''LOAD'' tracking variable up * increment it before each ''LOAD'' * skip ''LOAD''ing code sections according to the value of said variable (''IF LD=...'' ''THEN ...'' or ''ON LD GOTO/GOSUB ...'') where ''LD'' is the name of the tracking variable 10 RACER 0:RACER 2 : REM REINIT VASYL 20 DV=PEEK($BA) 30 REM *************************** 40 REM *** LOAD GRAPHICS *** 50 REM *************************** 60 IF LD=0 THEN GOSUB 5000 : REM LOAD 61 PRINT TAB(25);"DONE" 70 IF LD=1 THEN GOSUB 5100 : REM XFER 71 PRINT TAB(25);"DONE" [...] then in the subroutines starting at 5000 and 5100 we load and transfer data respectively: 5000 REM 5001 REM **************************** 5002 REM *** LOAD LOGO TO $C000 *** 5003 REM **************************** 5010 PRINT "LOADING HIRES BITMAP..."; 5020 LD=1 : LOAD"SDLOGO",DV,1 5100 REM 5101 REM **************************** 5102 REM ** XFER LOGO TO VASYL RAM ** 5103 REM **************************** 5110 PRINT "SENDING TO VASYL RAM..."; 5120 LD=2 : REM MARK XFER TO VASYL 5130 DS=DPEEK($AE) - 49152 : REM SIZE 5140 VCFG 2,49152 : REM LOADED ADDRESS 5150 VCFG 0,$1000 : REM DESTINATION 5160 COPY #0,#2,DS 5170 RETURN Easy? I think so! There are a few caveats though. === Memory buffer === First you need to make sure that you have an available memory buffer of the required size. There is 4 KiB of RAM in Commodore 64, which is not normally used by BASIC. This relatively small memory area starting at 49152 ($c000) has been the favourite place for storing transient data or machine language subroutines for decades now. It looks like the first candidate for our buffer === Files preparation === Then all your data file need to be of a ''PRG'' type with two leading bytes pointing to a **correct** memory address it is expected to ''LOAD'' into. Since we use the above mentioned four KiB of RAM to ''LOAD'' as the buffer, the first two bytes of the file need to be $00 and $c0((There are ways to ''LOAD'' files to another address than the one specified by its first two-byte pointer value but we don't go into this territory here)). Make sure that this is in fact the case or you risk crashing the machine. Make also sure that the file is not larger than 4KiB or you //will// crash the machine. > OK but what if my data is larger than the 4KiB available at 49152?? The answer is either you find a buffer large enough to hold all your data at once or you need to ''LOAD'' it in chunks not larger than the available buffer. I would strongly recommend the latter approach - split the file into as many smaller ones as needed and then ''LOAD'' and ''COPY'' them one by one. This will always work regardless of the size of your BASIC program. > But wouldn't it be much better to simply ''OPEN'' the file and read its content byte by byte and use ''VPOKE'' to send it to VASYL? No. This looks like an elegant approach at first but in virtually all cases((Unless your machine configuration includes a very efficient "speeder", which accelerates sequential reading by a large factor. In such case though, LOADing will be most probably accelerated by an even larger factor!)) it will be unbearably slow to read a sizeable amount of data this way. And for small datasets it's easier and faster to use other methods presented above. **Takeaways**: - adapt the way you handle data transfer to VASYL memory to the amount of data you need to move there - it does not pay to ''LOAD'' a few bytes. Using ''DATA'' or ''VDATA'' will be more efficient and less error-prone - when dealing with large amounts of data, the most efficient way is to ''LOAD'' it in chunks not larger than the available buffer size. Up to 4KiB in practice - steer clear of using memory areas other than the 4KiB of free RAM available at $c000. You'll thank me once we meet :-) - reading data from sequential file is not really an option ==== Moving data around ==== We talked about moving data into VASYL accessible memory and outlined a few methods for doing it. How about moving some data within/into the computer's main RAM? An efficient way of doing this might become handy in various situations * moving graphics data (hires, charset, sprite, ...) into their expected locations * clearing/filling memory areas (again - usually graphics or colour RAM related) with predefined values * utilising VASYL RAM as "swap" memory * copying ROM data to RAM for patching * [...] Whoever tried to fill even text screen with a predefined value(s) knows that this is not something Commodore BASIC excels at. OF course instead of clearing the screeen with ''POKE''s we can issue a ''PRINT "{CLEAR}"'' statement and have the screen cleared((technically **filled** with bytes having their value corresponding to ASCII code for the invisible SPACE character)) in an instant. But what if we want to fill it with different value(s)? Try the following to have a fresh impression: 100 FOR I=0 TO 999 110 POKE 1024+I,0 120 NEXT I and now try this for a quick comparison: 100 VPOKE $FFFF,0 110 VCFG 1,$FFFF,0:VCFG 2,1024,1 120 COPY #2,#1,1000 The same technique can be used to fill required Colour RAM areas. Example: 5200 REM 5201 REM **************************** 5202 REM *** FAST COLOUR RAM FILL *** 5203 REM **************************** 5210 PRINT "UPDATING COLOUR RAM..."; 5220 VPOKE $0FFF,14 5230 VCFG 1,$0FFF,0 5240 VCFG 2,$DAA8,1 5250 COPY #2,#1,320 : REM LAST 8 ROWS 5260 RETURN Copying KERNAL ROM to RAM for patching uses a variation of this technique: 5600 REM 5601 REM **************************** 5602 REM *** PATCH KERNAL 17 ROWS *** 5603 REM **************************** 5610 PRINT "PATCHING KERNAL..."; 5620 VCFG 1,$E000,1 : REM $E000 5630 VCFG 2,$E000,1 : REM KERNAL ROM 5640 COPY #1,#2,$2000 5650 VCFG 1,$E000,1 : REM $E000 5660 VCFG 2,$E000,1 : REM KERNAL ROM 5670 COPY #2,#1,$2000 5680 POKE $E882,$11 : REM PATCH SCROLL 5690 POKE 1,53: REM SWITCH TO RAM KERNAL 5700 RETURN It won't be as fast as an optimised machine code (here we do actually two transfers) but surely you can spare yourself checking how long would this take using plain BASIC. === VDATA revisited === In previous sections we discussed how easy and efficient it is to configure a "conveyor" and then use ''VDATA'' statements to quickly move data into VASYL RAM. What we didn't mention though is the fact that we can use the conveyor ''#2'' to make VDATA (and many other "V" statements) talk to computer's main memory instead. Remember this famous example from page 71 of [[http://www.zimmers.net/anonftp/pub/cbm/c64/manuals/C64_Users_Guide.pdf|Commodore 64 Users Guide]]? Yeah - who can forget it, right? 1 REM UP, UP, AND AWAY! 5 PRINT "{CLEAR}" 10 V=53248 : REM START OF DISPLAY CHIP 11 POKE V+21,4 : REM ENABLE SPRITE 2 12 POKE 2042,13 : REM SPRITE 2 DATA FROM 13TH BLK 20 FOR N = 0 TO 62: READ Q : POKE 832 + N, Q : NEXT 30 FOR X = 0 TO 200 40 POKE V+4,X: REM UPDATE X COORDINATES 50 POKE V+5,X: REM UPDATE Y COORDINATES 60 NEXT X 70 GOTO 30 200 DATA 0,127,0,1,255,192,3,255,224,3,231,224 210 DATA 7,217,240,7,223,240,7,217,240,3,231,224 220 DATA 3,255,224,3,255,224,2,255,160,1,127,64 230 DATA 1,62,64,0,156,128,0,156,128,0,73,0,0,73,0 240 DATA 0,62,0,0,62,0,0,62,0,0,28,0 Let's quickly and effortlessly make it use the much faster ''VDATA'' instead 1 REM UP, UP, AND VDATA! 5 PRINT "{CLEAR}" 10 V=53248 : REM START OF DISPLAY CHIP 11 POKE V+21,4 : REM ENABLE SPRITE 2 12 POKE 2042,13 : REM SPRITE 2 DATA FROM 13TH BLK 20 GOSUB 200 30 FOR X = 0 TO 200 40 POKE V+4,X: REM UPDATE X COORDINATES 50 POKE V+5,X: REM UPDATE Y COORDINATES 60 NEXT X 70 GOTO 30 200 VCFG 2,832,1 210 VDATA 0,127,0,1,255,192,3,255,224,3,231,224 220 VDATA 7,217,240,7,223,240,7,217,240,3,231,224 230 VDATA 3,255,224,3,255,224,2,255,160,1,127,64 240 VDATA 1,62,64,0,156,128,0,156,128,0,73,0,0,73,0 250 VDATA 0,62,0,0,62,0,0,62,0,0,28,0 260 RETURN Here we have a relatively small dataset so the difference is adequately small too. But the more data you need to process, the more clearly pronounced the difference becomes. TBC