ugBASIC User Manual

Expansion memory

In this section we will introduce the concept of "expansion memory", as well as all the features that ugBASIC makes available to increase the memory actually available on 8-bit computers. Particular attention should be paid to the fact that ugBASIC is not intended to provide a way to overcome the physical limitation of CPUs: for most of them, the maximum addressable memory is 64KB, and often even less. However, some of them provide hardware solutions, such as "paged memory" or DMA access, which can help mitigate the limitations.

Definition Banked DMA Other

Types of expansion memory

In ugBASIC there are two main types of memory:

  • resident memory, which is the one that can be addressed directly by the CPU through its instructions;
  • expanded memory, which is not directly addressable by the CPU.
When you program using the available instructions, define a variable, load an image, resize an array, and so on, you are using resident memory. The advantage of resident memory is that it is always available. So you don't have to worry about setting it up to use it. Conversely, it is usually in very limited quantities (and, depending on the target, decreases as the program grows).

Conversely, when you tell ugBASIC that a resource is meant to be loaded into expanded memory, the matter changes. It is still possible to use that resource but, obviously, ugBASIC will perform a series of "maneuvers" to make the data available when you ask for it.

Depending on the target, therefore, we will have a different type of expanded memory, and a different way of use:

  • DMA expanded memory: the c64reu target actually has a very large number of 64KB memory banks, separate from the main one, and not directly accessible from the CPU. The number of banks depends on the size of the expansion memory. However, REU is capable of copying data directly into the processor's memory, thus acting in its place.
  • Banked expanded memory:
    • The pc128op target has three (3) separate memory banks, each one has 16 KB free, which can be accessed with a shared window into resident memory. In this case, depending on the type of resource, it will be copied directly from the memory bank to shared memory, and via the CPU.
    • The coco3 target has at least 32 separate memory banks, each one is 8 KB, which can be accessed with a limited window. In this case, depending on the type of resource, it will be copied directly from the memory bank to shared memory, and via the CPU.
  • Virtual (banked) expanded memory: the atari, atarixl, c128, c128z, c64, coco, d32, d64, mo5, msx1, plus4, vic20, and zx, targets have a "simulated" memory bank: in reality it is a piece of resident memory, which is exploited in this mode. Clearly, there are no particular advantages in using it, except as a support area for compressed data.
  • The coleco, cpc, sc3000, sg1000 and sg1000 have any type of expansion memory.


Further down the page we will give specific indications for the various targets.

BANKED EXPANDED MEMORY

Available on (real, virtual):
atari atarixl c128 c128z c64 coco coco3 d32 d64 mo5 msx1 pc128op plus4 vic20 zx

How it works

Let's take the simplest case, even if perhaps a little useless. We have an image and we want to store it in expanded memory. And when we need it, we want to be able to draw it on the screen.


So, what happens? That ugBASIC will store the image on expanded memory (with the LOAD IMAGE command). When it's time to draw it on the screen, just use the PUT IMAGE command. Behind the scenes, ugBASIC will take the image from the expanded memory and copy it, reading it from the bank, into the resident memory. At this point it will use the data copied in the resident memory to draw on the screen.



What happens if you ask to draw the same image again? Clearly, since the graph data has already been copied from extended memory, it will not be copied again. The previous one will be used. And again, and again, whenever you draw that image.

Shared resident memory



Clearly, you may have more than one image to draw. For example, suppose you have two and want to draw them. The mechanism will be similar to the previous one.


The key difference in this case is that you are drawing two different images. In this case, ugBASIC will not be able to refrain from copying the graphic image from the expanded memory again, even during the second call.

Note that a certain amount of resident memory is reserved to implement this mechanism. How much memory? The one needed to host the largest frame and / or frame of IMAGES and SEQUENCE.

Multiple shared resident memory



Can you avoid copying the graphic resource every time? Can you have multiple resident memory areas? The answer is yes! In that case, the behavior changes again because ugBASIC will be able to avoid copying a given resource that has already been copied previously.

However, attention must be paid to the fact that, for each area dedicated to copying, a certain amount of resident memory will be lost. Also, as we explained in the previous section, the required resident memory area will be sized with the largest of the blocks that can be copied from the expanded memory. It goes without saying, therefore, that it is advisable to combine blocks of memory of similar length in a single sharing space.

Clearly, what is the best strategy between having memory resident zones and shared zones is the full responsibility of the developer.

Loading resource on expansion

To load a resource into an expanded memory bank, and then to use a single shared space, the BANKED keyword must be used. This command tells ugBASIC that the resource should be stored in expanded memory, and retrieved from there if needed.

Example:
player := LOAD IMAGE("player.png") BANKED

This command will load the player image into expanded memory and associate it with the player variable. Notice how we are using the direct assignment syntax (:=). This is needed because, otherwise, the resource will be placed in expanded memory but what will be assigned to the player variable will be a... copy! So we will take up both expanded and extended memory, to no avail.

Of course, we can use the variable as we did before, without any particular restrictions:

PUT IMAGE player AT 40, 40

Loading resource on separate shared memory

To load multiple resource into an expanded memory bank, and then to use different shared space for each, the BANKED keyword must be followed by the number of the shared memory. You can have up to 256 different resident memories.

Example:

CONST playerShared = 1
CONST alienShared = 2
playerIdle := LOAD IMAGE("player_idle.png") BANKED ( playerShared )
playerRun := LOAD IMAGE("player_run.png") BANKED ( playerShared )
alienIdle := LOAD IMAGE("alien_idle.png") BANKED ( alienShared )
alienRun := LOAD IMAGE("player_run.png") BANKED ( alienShared )

Of course, we can use the various variables as we did before, without any particular restrictions:

PUT IMAGE playerIdle AT 40, 40
PUT IMAGE alienIdle AT 0, 0

Defining arrays on expansion memory beta

In addition to graphics resources, entire arrays can also be moved to the memory expansion. The advantage, in this case, is that it does not occupy precious resident memory: with each access, whether reading or writing, the data will be updated on the memory expansion, with minimal occupation of the resident memory.

To define an array on expanded memory simply use the DIM instruction and write:

DIM largeArray(100, 100) AS INTEGER BANKED

Read and write access is the same:
x = largeArray( 42, 42 )
largeArray( 42, 42 ) = 21

If you want to avoid accessing the elements of an array with indices, and work with pre-calculated offsets, then you need to use the BANK READ and BANK WRITE instructions.

offset = 42 * 100 + 42
largeArrayAddress = VARBANKPTR( largeArray ) + offset
BANK READ VARBANK(largeArray) FROM largeArrayAddress TO VARPTR(x) SIZE 2
BANK WRITE VARBANK(largeArray) FROM VARPTR(x) TO largeArrayAddress SIZE 2

DMA EXPANDED MEMORY

Available on:
c64reu

How it works

Let's take the simplest case. We have an image and we want to store it in expanded memory. And when we need it, we want to be able to draw it on the screen.


So, what happens? That ugBASIC will store the image on expanded memory (with the LOAD IMAGE command). When it's time to draw it on the screen, just use the PUT IMAGE command. Behind the scenes, ugBASIC will take the image from the expanded memory and copy it directly into the video memory, to draw on the screen.

Loading resource on expansion

To load a resource into an expanded memory bank, the BANKED keyword must be used. This command tells ugBASIC that the resource should be stored in expanded memory, and retrieved from there if needed.

Example:
player := LOAD IMAGE("player.png") BANKED

This command will load the player image into expanded memory and associate it with the player variable. Notice how we are using the direct assignment syntax (:=). This is needed because, otherwise, the resource will be placed in expanded memory but what will be assigned to the player variable will be a... copy! So we will take up both expanded and extended memory, to no avail.

Of course, we can use the variable as we did before, without any particular restrictions:

PUT IMAGE player AT 40, 40

Defining arrays on expansion memory beta

In addition to graphics resources, entire arrays can also be moved to the memory expansion. The advantage, in this case, is that it does not occupy precious resident memory: with each access, whether reading or writing, the data will be updated on the memory expansion, with minimal occupation of the resident memory.

To define an array on expanded memory simply use the DIM instruction and write:

DIM largeArray(100, 100) AS INTEGER BANKED

Read and write access is the same:
x = largeArray( 42, 42 )
largeArray( 42, 42 ) = 21

If you want to avoid accessing the elements of an array with indices, and work with pre-calculated offsets, then you need to use the BANK READ and BANK WRITE instructions.

offset = 42 * 100 + 42
largeArrayAddress = VARBANKPTR( largeArray ) + offset
BANK READ VARBANK(largeArray) FROM largeArrayAddress TO VARPTR(x) SIZE 2
BANK WRITE VARBANK(largeArray) FROM VARPTR(x) TO largeArrayAddress SIZE 2

Other available primitives

In addition to managing resources, you can directly access and manage the expanded memory. In this case, however, you will be fully responsible for its use and therefore you will have to be careful to avoid creating problems in the automatic management. Also, not all primitives are available or work on all targets, unlike resource management.

To know which resources are available as expanded memory, it is necessary to use the constant BANK COUNT. This contains the number of banks (ie the number of expanded memory areas) that can be used. Each of these areas usually starts from a certain address (BANK ADDRESS()) and it extends for a certain number of bytes (equals to BANK SIZE (…)).

Normally there is always only one bank selected, and its number is returned by the BANK() function.

The following example returns the map of all the available memory banks, and indicates the selected one with an asterisk.

FOR i=0 TO BANK COUNT
   IF i = BANK() THEN
      PRINT "*";
   ELSE
      PRINT " ";
   ENDIF
   PRINT BANK ADDRESS(i);" size = "; BANK SIZE(i)
NEXT

PRINT "* = actual bank selected"
To copy from a memory bank to the resident memory you can use the BANK READ command, with the following syntax:

BANK READ number FROM source TO target SIZE size

Where:

  • number is the number of the bank (from 0 to BANK ACCOUNT-1);
  • source is the source address (zero means start of expansion memory);
  • target is the target address;
  • size is the size of the memory to copy.


On the other and, to copy from resident memory to a memory bank, you can use the BANK WRITE command, with the following syntax:

BANK WRITE number FROM source TO target SIZE size

Where:

  • number is the number of the bank (from 0 to BANK ACCOUNT-1);
  • source is the source address (zero means start of expansion memory);
  • target is the target address;
  • size is the size of the memory to copy.


If desired, it is possible to copy variables in the resident memory. To this end, you can use the VARPTR() function, which allows you to obtain the address (in the resident memory) of the variable.

Any problem?

If you have found a problem, if you think there is a bug or, more simply, you would like something to be improved, write a topic on the official forum, or open an issue on GitHub.

Thank you!