
**********************************************************************
files: QuickCopy.TXT, QuickCopy.TAP
       QBcopyDEMO.BAS, QBcopyDEFN.BAS, QBcopyPICT.BAS, QBcopyGSUB.BAS
**********************************************************************


** Quick BASIC RAM copy
   ====================

Here's the short version, describing how to address any contiguous area of memory as though it was a string variable.

[list]
[*]Declare it:
mmmm DEF FN a$(a$)=""
[*]Locate it:
.... RESTORE mmmm: LET da=FN p(23639)+9:
[*]Initialise it:
... POKE da+2,14: POKE da+3,0:
[*]Activate it:
.... RANDOMIZE da: POKE 23563,PEEK 23670: POKE 23564,PEEK 23671
[*]Assign it:
.... RANDOMIZE address: POKE da+4,PEEK 23670: POKE da+5,PEEK 23671:
     RANDOMIZE length : POKE da+6,PEEK 23670: POKE da+7,PEEK 23671:
[*]Use it:
.... LET a$=string expression
.... LET stringvariable=a$
[*]Deactivate it:
.... POKE 23564,0
[/list]


... and here's the long version ...


QBcopy
With a bit of trickery it's possible to copy a SCREEN$ (or any segment of memory) in ZX BASIC without resorting to a tedious POKE loop. This method allows the copy to be done with a single LET statement, which is VERY much faster. In the demo, load a SCREEN$/SCR file when prompted, then press "j", the screen will erase and then will be restored almost as quickly as doing an LDIR in machine code, but using only standard ZX Spectrum BASIC.

QBcopyDEMO.BAS [code]
  10 DEF FN q(a$,s$)=0
  11 DEF FN p(a)=PEEK a+256*PEEK (a+1):
     DEF FN l(v)=v-256*FN h(v): DEF FN h(v)=INT (v/256)
  12 CLEAR 65536-6912-1: RESTORE 10: LET fnq=FN p(23639)+8: LET da=23563:
     REM find FN q() from DATADD
  20 FOR a=1 TO 2: READ o,b:
       POKE o,0:
       POKE o+1,FN l(b): POKE o+2,FN h(b):
       POKE o+3,FN l(6912): POKE o+4,FN h(6912):
     NEXT a
  21 DATA fnq+3,16384,fnq+12,FN p(23730)+1
  35 PRINT #0;"load a SCREEN$ then press ""j"""
  36 IF INKEY$<>"j" THEN PAUSE 0: GO TO 36
  40 POKE da,FN l(fnq): POKE da+1,FN h(fnq): REM set DEFADD for FN q()
  41 LET s$=a$
  50 CLS : PRINT #0;"press ""j"" again"
  51 IF INKEY$<>"j" THEN PAUSE 0: GO TO 51
  60 LET a$=s$: PAUSE 0
  61 POKE da+1,0: REM cancel DEFADD (only hi-byte needs to be reset)
[/code]

It works because the LOOK_VARS routine in the ROM searches the DEFADD area (where FN variables are defined) before searching the VARS area. So it sets up a dummy FN q() to define a$ as starting at 16384 and s$ as starting above RAMTOP, both with a length of 6912. It then POKEs the DEFADD system variable (otherwise zero when not evaluating a user-defined function) to point to the dummy function, and so a$ & s$ can be referenced by BASIC commands, with a$ as the display file and s$ above RAMTOP.

It's not essential for FN q() to specify two string names; only those names being used as place markers for areas in free RAM need to be specified in the function. So if one of the strings is a normal string variable in the VARS area, or the LET is to assign one of such, then that name would not be included in FN q().

   ====================

Here's an example of a function using the QBcopy technique to copy strings to/from free RAM or copy chunks of RAM around quickly without needing to use machine code. Most of the routine is just set up and comments; the actual function call format is only one line of the form:

[code]
DEF FN q(a$,b$,l)=33
...
GO SUB FN q(a$,b$,l)
[/code]

as explained in lines 41-44. The LH-side parameter is the destination and the RH-side parameter is the source, as in LET a$=b$. a$ & b$ contain either a string expression (if referencing a string variable) or an address (if referencing free RAM). Any string names can be used as destination or source (or both; or neither, if copying directly between two different segments of free RAM). Although the routine will copy any length it obviously only makes sense to use it for large chunks where a substantial time saving can be made. Note that if a free RAM address is a calculated value rather than a literal value then the STR$ assignment must be outside the function call, due to the STR$ bug in the ROM, as shown in line 45.

QBcopyDEFN.BAS [code]
  10 DEF FN p(a)=PEEK a+256*PEEK (a+1):
     DEF FN l(v)=v-256*FN h(v): DEF FN h(v)=INT (v/256)
  20 DEF FN q(a$,b$,l)=33: RESTORE 20: LET fnq=FN p(23639)+8:
     LET qlo=FN l(fnq): LET qhi=FN h(fnq): LET da=23563:
     GO TO 40: REM find FN q() from DATADD

  33 POKE da,qlo: POKE da+1,qhi
  34 FOR a=fnq+3 TO fnq+12 STEP 9:
       IF FN p(a+3)=5 THEN
         LET loc=VAL VAL$ (CHR$ PEEK (a-3)+"$"):
         POKE a+1,FN l(loc): POKE a+2,FN h(loc):
         POKE a+3,FN l(l): POKE a+4,FN h(l)
  35 POKE a,0: NEXT a: LET a$=b$: POKE da+1,0: RETURN

  40 REM MAIN PROGRAM
  41 REM TO COPY string to RAM or RAM to string or RAM to RAM
     GO SUB FN q(a$,b$,l)
  42 REM FN q() PARAMETERS
     a$=destin string expression, or destin location if in RAM;
     b$=source string expression, or source location if in RAM;
     l =length to copy
  43 REM eg.: GO SUB FN q("20480","16384",2048)
     copies top 1/3rd screen to bottom
  44 REM eg.: DIM S$(6912): GO SUB FN q(S$,"16384",6912)
     copies all of display file to S$
  45 DIM t$(2048): DIM s$(2048): GO SUB FN q(t$,"16384",2048):
     LIST 45: GO SUB FN q(s$,"16384",2048):
     FOR b=0 TO 0 STEP 0: FOR s=16384 TO 16384+4096 STEP 2048:
       LET c$=STR$ s: GO SUB FN q(c$,s$,2048): GO SUB FN q(c$,t$,2048):
     NEXT s: NEXT b:
     REM bounces around a screen segment
[/code]

The function works with string slicing either side as well.

[code]
  44 LIST 45: DIM S$(6912):
     GO SUB FN q(S$(2049 TO 4096),"16384",2048): CLS : PAUSE 0:
     GO SUB FN q("16384",S$( TO 6144),6144): PAUSE 0: STOP
[/code]

The vertical bars are due to S$() being initialised with spaces ($20) and only the middle segment is being addressed.

   ====================

It becomes more complicated if smaller bit-mapped pictures at various places on the screen are wanted. Eg. a 6x6 character area at the top right could be stored as a single 6x8x6 = 288 byte string, but on the screen it's constituted of 48 distinct non-contiguous 6-byte strings. As the stored picture data has to be divided up into discrete segments in order to build the picture on the screen it's only practical to do this with machine code, unless a tile method is used (ie. a set of pre-defined symbols from which pictures are constructed).

The bigger the picture gets the less efficient a bit-mapped definition becomes; this is why adventure games which store a lot of pictures usually use some form of data definition specifying how to construct the picture rather than storing all the individual bytes. The book [url=ftp://ftp.worldofspectrum.org/pub/sinclair/books/AdvancedGraphicsWithTheSinclairZXSpectrum.pdf]Advanced Graphics with the Sinclair ZX Spectrum[/url] might be of interest, although it probably goes into more of the mathematics than it would be preferred to deal with at the present time.

   ====================

Here's an example using the (very) quick BASIC technique mentioned by Dr.BEEP for restoring SCREEN$, modified to do small pictures. The demo is in three parts: set up a picture; store the picture data & erase it; display the picture. Step 2 also shows how long it would take to display the picture from the data string if it was just using a simple POKE loop, whereas Step 3 uses LET to display the picture, so it takes pretty much the same time whether the picture is 3 characters wide or 30 (variable cc), and does it reasonably quickly despite having to do it in cr*8 pieces.

An important restriction to note is that the picture must fit within a screen 1/3rd, not extending into the next 1/3rd down. In practice the string s$() would contain the data for multiple pictures, and lines 50-51 would be written as a subroutine accepting a picture number parameter. A separate data string would be used as an index into the picture string, specifying the top-left coordinate and row/col dimensions for each picture, plus the location pointer into s$(). Both strings would be set up separately from and loaded as part of the main program, so there would be no setup delay when running the game.

If the picture takes up a full screen 1/3rd then the method can be simplified (ie. without the loop) to copy the whole picture at once in which case it would be displayed instantaneously just using BASIC.

QBcopyPICT.BAS [code]
   2 DEF FN p(a)=PEEK a+256*PEEK (a+1):
     DEF FN l(v)=v-FN h(v)*256: DEF FN h(v)=INT (v/256):
     REM 23684 = DF_CC  = print position address in display file
  10 LET z=0: LET row=9: LET col=10: PRINT AT row,col;:
     LET p=FN p(23684): LET cr=6: LET cc=10
  20 DIM s$(cr*8*cc): FOR a=1 TO cr: FOR b=1 TO cc:
       PRINT CHR$ (a+64);: NEXT b:
       LET row=row+1: PRINT AT row,col;: NEXT a
  30 DEF FN a$(a$)="":
     REM 23639 = DATADD = address of last DATA item (or RESTORE)
     REM Calculate the location of DEF FN a$(a$) & initialise
     REM 23563 = DEFADD = address of DEF FN parameters
  31 RESTORE 30: LET d=FN p(23639)+9:
     POKE 23563,FN l(d): POKE 23564,FN h(d): POKE d+2,14:
     POKE d+3,z: POKE d+4,FN l(p): POKE d+5,FN h(p):
     POKE d+6,cc: POKE d+7,z
  40 LET s=1: FOR a=p TO p+(cr-1)*32 STEP 32:
       FOR b=a TO a+7*256 STEP 256: FOR c=b TO b+cc-1:
         LET s$(s)=CHR$ PEEK c: POKE c,255: LET s=s+1:
       NEXT c: NEXT b: NEXT a
  50 LET s=1-cc: LET l=FN l(p): LET h=FN h(p)
  51 FOR a=l TO l+(cr-1)*32 STEP 32:
       POKE d+4,a: POKE d+5,h:
       FOR b=h+1 TO h+8:
         LET s=s+cc: LET a$=s$(s TO s+cc-1): POKE d+5,b:
       NEXT b: NEXT a
[/code]

   ====================

PS. (cf. Post #14 - note that lines 50-51 have been simplified)
Both the source & destination can be defined in FN a$(), so if using a 128k Spectrum then, instead of the string s$(), one or more RAMpages could be allocated for pictures, the function would become "DEF FN a$(a$,b$)=""", where a$ is in the display file and b$ is in the RAMpage, and the index pointer would be a page number (optional) + address. Changes to the demo would be:

* line  2 : add CLEAR 49151: LET RAM1=49152
* line 20 : replace DIM with LET RAMp=1 (0,1,3,4,6,7; not 5,2)
* line 30 : change DEF FN to DEF FN a$(a$,b$)=""
* line 31 : add POKE d+11,14: POKE d+12,z: POKE d+13,FN l(RAM1): POKE d+14,FN h(RAM1): POKE d+15,cc: POKE d+16,z
* line 40 : change LET s=1 to LET s=RAM1: POKE 23388,16+RAMp
* line 40 : change LET s$(s)=CHR$ PEEK c to POKE s,PEEK c
* line 50 : change LET to LET s=RAM1-cc; add POKE 23388,16+RAMp
* line 51 : after LET s=s+cc add POKE d+13,FN l(s): POKE d+14,FN h(s)
* line 51 : change LET a$ to LET a$=b$

The RAMpage start address (RAM1) can be >= 49152 . If using a page other than 0, ensure that the area selected is not being used by the ROM (eg. for BASIC, RAMdisk or +3DOS).

   ====================

The QBcopy procedure can be run using a simple function call of the form:
	GO SUB FN q(a$,b$,l)
This example also shows a method of passing parameters to subroutines.

The LH-side parameter is the destination and the RH-side parameter is the source, as in LET a$=b$. a$ & b$ contain either a string expression (if referencing a string variable) or an address (if referencing free RAM). Any string names can be used as destination or source (or both; or neither, if copying directly between two different segments of free RAM). Although the routine will copy any length it obviously only makes sense to use it for large chunks where a substantial time saving can be made. Note that if a free RAM address is a calculated value rather than a literal value then the STR$ assignment must be outside the function call, due to the STR$ bug in the ROM, as shown in line 45.

QBcopyGSUB.BAS [code]
  10 DEF FN p(a)=PEEK a+256*PEEK (a+1):
     DEF FN l(v)=v-256*FN h(v): DEF FN h(v)=INT (v/256)
  20 DEF FN q(a$,b$,l)=33: RESTORE 20: LET fnq=FN p(23639)+8:
     LET qlo=FN l(fnq): LET qhi=FN h(fnq): LET da=23563:
     GO TO 40: REM find FN q() from DATADD

  33 POKE da,qlo: POKE da+1,qhi
  34 FOR a=fnq+3 TO fnq+12 STEP 9:
       IF FN p(a+3)=5 THEN
         LET loc=VAL VAL$ (CHR$ PEEK (a-3)+"$"):
         POKE a+1,FN l(loc): POKE a+2,FN h(loc):
         POKE a+3,FN l(l): POKE a+4,FN h(l)
  35 POKE a,0: NEXT a: LET a$=b$: POKE da+1,0: RETURN

  40 REM MAIN PROGRAM
  41 REM TO COPY string to RAM or RAM to string or RAM to RAM
     GO SUB FN q(a$,b$,l)
  42 REM FN q() PARAMETERS
     a$=destin string expression, or destin location if in RAM;
     b$=source string expression, or source location if in RAM;
     l =length to copy
  43 REM eg.: GO SUB FN q("20480","16384",2048)
     copies top 1/3rd screen to bottom
  44 REM eg.: DIM S$(6912): GO SUB FN q(S$,"16384",6912)
     copies all of display file to S$
  45 DIM t$(2048): DIM s$(2048): GO SUB FN q(t$,"16384",2048):
     LIST 45: GO SUB FN q(s$,"16384",2048):
     FOR b=0 TO 0 STEP 0: FOR s=16384 TO 16384+4096 STEP 2048:
       LET c$=STR$ s: GO SUB FN q(c$,s$,2048): GO SUB FN q(c$,t$,2048):
     NEXT s: NEXT b:
     REM bounces around a screen segment
[/code]

   ====================

The function works with string slicing either side as well.

[code]
  44 LIST 45: DIM S$(6912):
     GO SUB FN q(S$(2049 TO 4096),"16384",2048): CLS : PAUSE 0:
     GO SUB FN q("16384",S$( TO 6144),6144): PAUSE 0: STOP
[/code]

The vertical bars are due to S$() being initialised with spaces ($20) and only the middle segment is being addressed.

   ====================

Here's the concise version, describing how to address any contiguous area of memory as though it was a string variable.

[list]
[*]Declare it:
mmmm DEF FN a$(a$)=""
[*]Locate it:
.... RESTORE mmmm: LET da=FN p(23639)+9:
[*]Initialise it:
... POKE da+2,14: POKE da+3,0:
[*]Activate it:
.... RANDOMIZE da: POKE 23563,PEEK 23670: POKE 23564,PEEK 23671
[*]Assign it:
.... RANDOMIZE address: POKE da+4,PEEK 23670: POKE da+5,PEEK 23671:
     RANDOMIZE length : POKE da+6,PEEK 23670: POKE da+7,PEEK 23671:
[*]Use it:
.... LET a$=string expression
.... LET stringvariable=a$
[*]Deactivate it:
.... POKE 23564,0
[/list]

   ====================

Giving this worked example from the general form above, BASin times line 11, which restores the screen from the backup copy, at 6.4 frames, approximately 1/8th second.
[code]
   3 DEF FN a$(a$,s$)="": CLEAR 65535-6912
   4 RESTORE 3: LET da=PEEK 23639+256*PEEK 23640+9
   5 POKE da+2,14: POKE da+3,0: POKE da+11,14: POKE da+12,0
   6 LOAD "screen" SCREEN$
   7 RANDOMIZE da: POKE 23563,PEEK 23670: POKE 23564,PEEK 23671
   8 POKE da+4,0: POKE da+5,229: POKE da+6,0: POKE da+7,27
   9 POKE da+13,0: POKE da+14,64: POKE da+15,0: POKE da+16,27
  10 LET a$=s$: CLS: PAUSE 0
  11 LET s$=a$
  12 POKE 23564,0: STOP
[/code]


**********************************************************************


** Writing Faster BASIC
   ====================

Given that every reference to a variable (or function) involves a sequential search from the beginning of VARS (or PROG) the most frequently accessed items should be located as close to the start of the relevant area as possible. Once the program is written and the frequencies can be evaluated then add a section at the start to declare the most referenced variables first. This applies to simple numeric variables and DIMmed numeric arrays and DIMmed strings/arrays. It's of no use for simple strings as for them a LET always causes the space to be reclaimed and the variable redeclared at the end of VARS, even if the old & new values are the same size. To declare a FOR...NEXT control variable just use "FOR a=SGN PI TO SGN PI: NEXT a", otherwise LET or DIM as appropriate.

For arrays containing small positive integers <= 65535 it's much the same work to reference the elements in a DIMmed string rather than a numeric array and it saves 3 bytes per element. Also, given what I mentioned above, if you're using any strings which don't change much or at all in size then it's better to DIM them as then the redeclaration overhead is avoided. To move chunks of RAM quickly in BASIC using LET - eg. copying to/from the display - use the QuickCopy method described in the Compendium; this is almost as quick as using an LDIR in machine code. Keep numeric variable names as short as possible compatible with clarity, especially those used frequently, as every character of the name has to be checked every time it is referenced.

   ====================

I forgot to mention GOTOs/GOSUBs. Try to organise routines such that those with the most internal GOTOs,and those referenced most frequently via GOSUBs, are placed near the start of the program.

Also, it's much quicker to find a subroutine referenced via a GOSUB than it is a DEF FN on the same line. This is because to find a GOSUB line the ROM just has to check the line number and length of every preceding line, whereas to find a DEF FN it has to search through every statement on every preceding line until it finds it. So depending upon the type of function and how the result is referenced it might be better to write it as a GO SUB routine rather than a DEF FN.

Oh, and to speed up all GOTO/GOSUB line searches, put as many statements on each line as is both practical and consistent with the program logic, as this will minimise the number of steps required to find any particular line.

Similar considerations apply to the location of DATA statements and the use of RESTORE, in that DATA statements should be as close to the start of a line as possible and RESTORE points should be as close to the start of the program as possible.

For timings in BASIC you could use something like this:
[code]
   1 GO SUB 2: LET st=t0: PRINT AT 4,0;"Start    : ";st:
     GO SUB 3:
     GO SUB 2: LET ft=t0: PRINT AT 5,0;"Finish   : ";ft'
                                       "Duration : ";INT ((ft-st)*100)/100:
     STOP
   2 LET t1=FN t(): LET t2=FN t(): LET t0=t1-(t1-t2 AND t1<t2): RETURN:
     DEF FN t()=(65536*PEEK 23674+256*PEEK 23673+PEEK 23672)/50
   3 REM test routine
     ..
9999 RETURN
[/code]
or else use the Profiler in BASin.

   ====================

One other speed consideration I forgot to mention is that recursive DEF FNs are generally, often significantly, slower than the equivalent expanded DEF FN, and always use more memory when being evaluated. It's not always possible to expand a recursive DEF FN (eg. if the terminating iteration level is variable), and the workspace saved during evaluation is offset by the space lost in having a longer DEF FN statement in the BASIC, so it's mainly the comparative timings which would be of interest. So if there are any such functions in the program it's worth doing a timing test loop for comparison. There's a section in the Compendium which describes how to calculate DEF FN memory usage. There's a table of comparative sizes and timings for the DEF FNs at the end of the contents file in the ZIP file.

   ====================

Another significant area in which both space & time can be saved is the declaration of constant data tables, which can be put into a separate program, loaded into the main program and then saved with the BASIC. This cuts out the space overhead caused by duplicating the data in PROG & VARS, and reduces the setup time for those tables to practically zero, as that part of the load time taken up with those tables would be much the same as being loaded in DATA as part of the BASIC, so the program load time wouldn't increase, and there'd be no setup time as the tables would be already set up when they're loaded directly into VARS.


**********************************************************************
