6502.org Forum  Projects  Code  Documents  Tools  Forum
It is currently Tue Sep 24, 2024 4:40 pm

All times are UTC




Post new topic Reply to topic  [ 27 posts ]  Go to page Previous  1, 2
Author Message
 Post subject: Re: Memory Management
PostPosted: Sat Dec 29, 2012 6:25 pm 
Offline

Joined: Sat Dec 13, 2003 3:37 pm
Posts: 1004
There's a couple of ways to look at it, and I think White Flames post is spot on. It's spot on because the technique he demonstrates is not intuitive to someone used to working in a high level language. Its exhibits some architecture dependency as well (as to the reasoning behind choosing the representation).

Here's the two ways to manage this kind of table structure.

The first is more akin to how a C programmer may look at it, as a contiguous block of memory with a structure overlaid on top of it.

Code:
MAX_SPRITES = 32
SPRITE_SIZE = 4

SPRITE_X_OFFSET = 0
SPRITE_CHAR_OFFSET = 1
SPRITE_VID_OFFSET = 2
SPRITE_Y_OFFSET = 3

        .ORG $1000                      ; Sets working address to $1000

SPRITE_X = * + SPRITE_X_OFFSET          ; SPRITE_X = $1000 + 0
SPRITE_CHAR = * + SPRITE_CHAR_OFFSET    ; SPRITE_CHAR = $1000 + 1
SPRITE_VID = * + SPRITE_VID_OFFSET      ; SPRITE_VID = $1000 + 2
SPRITE_Y = * SPRITE_Y_OFFSET            ; SPRITE_Y = $1000 + 3

SPRITE_TAB .ds MAX_SPRITES * SPRITE_SIZE ; Allocates 32 * 4, 128, bytes and
                                        ; bumps working address to $1080


This defines the four fields of the sprite table first as simple offsets, then as we create starting points for each of the fields in the table.

Given something like this, if you wanted to work with a Sprite, you could would put the Sprite number TIMES 4 (SPRITE_SIZE) in to X, and then you can use the index address modes to get the individual elements.

So, something like this:
Code:
MOVE_FORWARD               ; Moves a sprite forward (X = X + 1),
                           ; pass the sprite # in A, destroys A and X
        ASL A              ; Since SPRITE_SIZE is 4, we can simply
        ASL A              ; shift A twice to calculate the proper offset.
                           ; If it was something that was not a power of
                           ; two, we'd have to multiply.
        TAX
        INC SPRITE_X, X    ; Increment the value of X for this sprite.
        RTS


If you were to do this is C code, it would look something like this:
Code:
typedef struct {
    byte x;
    byte chr;
    byte vid;
    byte y;
} sprite;

#define MAX_SPRITES 32

sprite sprite_tab[MAX_SPRITES];

void move_forward(int sprite_id) {
    sprite_tab[sprite_id].x += 1;
}


The basic advantage of this representation is that all of the constructs elements are together, the entire sprite is located in a contiguous block of ram.

The down side is that you have to multiply the offset by the sprite size (here we used shifts), and you are limited to a sprite table that is no larger than 256 bytes total, since the X register is 8 bits. So, that would be a total of 64 sprites.

White Flames technique is essentially like this in C:

Code:
byte sprite_x[MAX_SPRITES];
byte sprite_chr[MAX_SPRITES];
byte sprite_vid[MAX_SPRITES];
byte sprite_y[MAX_SPRITES];

void move_forward(int sprite_id) {
    sprite_x[sprite_id] += 1;
}


In assembly, it looks like this:

Code:
MAX_SPRITES = 32
SPRITE_X    .ds MAX_SPRITES
SPRITE_CHAR .ds MAX_SPRITES
SPRITE_VID  .ds MAX_SPRITES
SPRITE_Y    .ds MAX_SPRITES

MOVE_FORWARD               ; Moves a sprite forward (X = X + 1), pass the sprite # in X
        INC SPRITE_X, X    ; Increment the value of X for this sprite.
        RTS


The primary disadvantage is simply that the sprite values are not contiguous. So, you can't use simple block move logic to move sprites around, you have to move them field by field. But the advantages are that you can have up to 256 sprites, and you can have as many fields as you like (not, simply 4, or powers of two, but you can have 7,19, or 347 fields).

So, different needs have different techniques. Depends on the specific use case of each structure.


Top
 Profile  
Reply with quote  
 Post subject: Re: Memory Management
PostPosted: Sun Dec 30, 2012 5:37 am 
Offline
User avatar

Joined: Thu May 28, 2009 9:46 pm
Posts: 8392
Location: Midwestern USA
whartung wrote:
There's a couple of ways to look at it...

I guess you didn't read the other posts before replying... :D

_________________
x86?  We ain't got no x86.  We don't NEED no stinking x86!


Top
 Profile  
Reply with quote  
 Post subject: Re: Memory Management
PostPosted: Sun Dec 30, 2012 8:10 am 
Offline
User avatar

Joined: Thu May 28, 2009 9:46 pm
Posts: 8392
Location: Midwestern USA
Johnny Starr wrote:
I would like to understand more about ASM tables though.

What do you mean by "ASM tables?" Is ASM an acronym for something? In computing, terminology is critical when trying to learn something new. You shouldn't use acronyms or abbreviations unless you are certain that both the reader and you know what they mean.

Quote:
I sort of get where White Flame was going in his example, but I don't really understand how or where the "tables" are stored...

Okay, Johnny. Here goes...long-winded post to follow. :lol:

At the risk of stating the obvious, data of any kind are stored in memory (RAM, but could be mass storage). A table is a specific type of data that has some kind of programmer-determined structure, which is no different than defining a table in C with the struct keyword. In the abstract, a table is the same whether written on paper or created in RAM: it's organized data with a meaning determined by whomever created it. Your table could be a list of names (e.g., your friends and their phone numbers), a list of numeric constants used in a mathematical function, a list of addresses pointing to objects (e.g., sprite definitions) located somewhere else in RAM, etc. Before you get involved with the programming mechanics of table creation and management, you have to define the size of the table and the nature of the data in it, after which you can concentrate on the required code. This is no different than accomplishing the same task in C: the concept is the same. Only the code syntax differs.

As for where tables are stored, they are stored at a location of your choosing, which you determine either by coding the location into your program, or by allocating memory on the fly at run-time. The latter method would be analogous to calling malloc() in a C program that runs on a system that supports such a function. If you know in advance exactly how much storage will be required for your table you can define the table's extent in code, as well as its starting location. It sounds from what you have already said about what you are doing that static storage will suffice, so it becomes a matter of determining where to put the table and how much room it needs.

To illustrate how to get from concept to code, let's go back to an example I presented a few posts ago: a SCSI device table. First some background information.

SCSI (Small Computer Standard Interface) is a method of attaching various kinds of peripherals to a computer. Depending on the system, either seven or 15 peripherals can be attached to the SCSI bus. My POC unit implements SCSI-2 with an eight bit bus, so up to seven peripherals (aka devices) can be attached. SCSI devices vary widely in characteristics and capabilities, and aren't limited to just mass storage (I have a document scanner in my office that is SCSI). Attached to POC's SCSI port is a 36GB hard drive, a DDS2 cartridge tape drive and a CD-ROM drive. Each device is assigned a unqiue device ID, the hard drive being $00, the CD-ROM being $05 and the tape drive being $06. POC itself is assigned $07, which is something that is set during POST—device numbers on the other hardware are set via jumpers,

At boot time, POC doesn't actually know if anything is connected to the SCSI port. So following POST the BIOS "enumerates" the bus by attempting to contact each of the SCSI IDs that would be valid on the bus, namely $00-$06 (POC doesn't try to talk to itself, unlike its designer). If a device responds to a given ID, it is commanded to identify itself by type, storage capacity (if capable of mass storage), how it handles data (in blocks or in streams), is it ready to accept and/or send data, etc. The information thus gathered is stored in a SCSI device table in RAM, eight bytes per device, and logically organized in a row/column format, with the SCSI ID acting as the row index. In POC, the table starts at $000110 and as there are eight possible devices (including POC), extends to $00014F, 64 bytes in total. The memory dump of the table would look like the following inside POC's machine language monitor:

Code:
.m 110 15F
000110 C3 80 CC DC 45 04 00 02 00 00 00 00 00 00 00 00
000120 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000130 00 00 00 00 00 00 00 00 C2 05 F2 09 00 00 00 08
000140 C2 01 00 00 00 00 00 00 FF FF FF FF FF FF FF FF

The M/L monitor dumps 16 bytes per line, so it isn't immediately obvious from the above that what is being viewed is a data table. However, reformatting the display makes it a little clearer:

Code:
000110 C3 80 CC DC 45 04 00 02
000118 00 00 00 00 00 00 00 00
000120 00 00 00 00 00 00 00 00
000128 00 00 00 00 00 00 00 00
000130 00 00 00 00 00 00 00 00
000138 C2 05 F2 09 00 00 00 08
000140 C2 01 00 00 00 00 00 00
000148 FF FF FF FF FF FF FF FF

Now, if I annotate the above it becomes even clearer:

Code:
 Mem
 Addr                              Assignment
——————————————————————————————————————————————————————————————————————
000110 C3 80 CC DC 45 04 00 02 <—— SCSI device $00 (hard drive)
000118 00 00 00 00 00 00 00 00 <—— SCSI device $01 (not present)
000120 00 00 00 00 00 00 00 00 <—— SCSI device $02 (not present)
000128 00 00 00 00 00 00 00 00 <—— SCSI device $03 (not present)
000130 00 00 00 00 00 00 00 00 <—— SCSI device $04 (not present)
000138 C2 05 F2 09 00 00 00 08 <—— SCSI device $05 (CD-ROM)
000140 C2 01 00 00 00 00 00 00 <—— SCSI device $06 (tape drive)
000148 FF FF FF FF FF FF FF FF <—— SCSI device $07 (POC's host adapter)
———————————————————————————————————————————————————————————————————————
        |  |  |  |  |  |  |  |
        |  |  |  |  |  |  +——+———> Block size (2 bytes)
        |  |  +——+——+——+—————————> Total blocks (4 bytes)
        |  +—————————————————————> Device type bit field (1 byte)
        +————————————————————————> Device flag bit field (1 byte)

It's clear (or should be from the above) that the SCSI device ID can be used as an index into the table, since the ID is zero-based. More on this below.

Now, your next question should be, "What determined the above layout?" The answer is in the assembly language source statements that define field sizes and the relationships of the fields to each other. Obviously, you have to conceptualize the table layout, most likely by doing some scribbling on paper to determine what you need. Some careful thinking can save you a lot of grief in getting it to work. The above annotated memory dump more-or-less is such scribbling. I knew what information I would need to store in the table and knew that I would be accessing the table by SCSI device ID, so it became a matter of mostly determining the order of the fields. As bit 7 in the device flag is used to indicate that a device was found at the corresponding SCSI ID, I place that field first in the row to make testing it convenient. It seemed logical to following the flag with the type, so I made that field the second in the row. The remaining fields, total blocks and block size, are unsigned integers and thus use multiple contiguous bytes, in little-endian order. The order of the fields themselves didn't matter, so I placed the total blocks field first.

The process of determining just how much data each field has to hold is simplified somewhat by the fact that the block size and total blocks fields have to match the size of the data returned from each device when interrogated. POC supports both SCSI-1 and SCSI-2 devices, the latter implementing both larger block size and total blocks fields. So I had to size the fields to accommodate the SCSI-2 standard, in which the block size in bytes is defined by a 16 bit unsigned integer and the total blocks field is defined by a 32 bit unsigned integer (SCSI-1 devices report total blocks in 21 bits). Hence two bytes have to be allocated to store the block size and four to store the total blocks field.

The device flag and type fields are one byte each, something that I defined, as by using each as a bit field, I could fit all desired information into two bytes. When the individual field sizes are added up, the result is eight bytes. Eight bytes times eight devices means the table size is 64 bytes.

Putting this all together resulted in the following assembly language statements:

Code:
;LOGGED SCSI DEVICES TABLE
;
;                 ——————————————————————————————————————
;  row offset ——> $00  $01  $02  $03  $04  $05  $06  $07
;                 ——————————————————————————————————————
;                  |    |    ^——————————————^    ^————^
;                  |    |        capacity         block
;                  |    |                         size
;                  |    +———> type
;                  +————————> flag
;
;
;   field sizes (description order)...
;
s_sdbsiz =s_word               ;block size
s_sdflag =s_byte               ;flag
s_sdcap  =s_dword              ;capacity in blocks
s_sdtype =s_byte               ;type
;
;
;   row & table sizes...
;
sdslotex =ex2_x3               ;row size computation factor...
;
;   ——————————————————————————————————————————————————————————————————————
;   SDSLOTEX is used to atomically define the size of the device table row
;   so bit shifts can be used to compute a row address for a SCSI ID.  The
;   above definition should be changed with caution.  See additional notes
;   in the SSLOTADR function.
;   ——————————————————————————————————————————————————————————————————————
;
s_sdslot =1 << sdslotex        ;row
s_sd_tab =s_sdslot*n_scsiid    ;table
;
;
;   field offsets in row...
;
o_sdflag =0                    ;device flag...
;
;   xx000xxx
;   ||   |||
;   ||   +++———> ANSI version
;   |+—————————> 1: supports sync transfer
;   +——————————> 1: device enumerated
;
o_sdtype =o_sdflag+s_sdflag    ;device type...
;
;   x00xxxxx
;   |  |||||
;   |  +++++———> ANSI device type
;   +——————————> 1: device recognizes 32 bit LBAs
;
o_sdcap  =o_sdtype+s_sdtype    ;capacity in blocks
o_sdbsiz =o_sdcap+s_sdcap      ;block size

"Row offset" is another way of saying "column."

In the above code, the s_byte, s_word and s_dword symbols are atomically defined to be 1, 2 and 4, respectively. The symbol n_scsiid is atomically defined as 8, as that is an immutable SCSI hardware characteristic. "Magic numbers" of this type should be defined in an INCLUDE file to maintain consistency throughout the program. It is bad programming practice to embed constants in program code, as tracking down and changing them, if necessary, becomes an onerous task. Also, it's easier for someone reading the source code to understand the use of constants when they are named. I'm sure you'd agree that LDA #n_scsiid means more to the reader than LDA #8 (from where did that 8 come???).

Note the order in which the definitions are made. First the field sizes are defined. For example, the s_sdcap symbol sets the size of the total blocks field. With all field sizes defined, the assembler can then calculate the row size (eight bytes) and the table size (64 bytes). The expression s_sdslot =1 << sdslotex means s_sdslot is equal to 1 left-shifted sdslotex bits, where sdslotex happens to be 8. Again, this sort of thing is defined in a separate INCLUDE file and never embedded into the code. This somewhat odd way of defining the row size is to assure that it is always a multiple of 2, should I see fit to change the row size. If the row size is a multiple of 2 then the row offset in the table can be computed with some left shifts, rather than by multiplying the row size by the SCSI ID.

The final section of the above code sets the relative offsets in each row to each field, that is, the "column" numbers. Note that the only field that has an actual column number set is the device flag bit field, which is the first field in a row and hence in the zeroth column. All the others are computed at assembly time from the previously defined field sizes. This use of "assembler automation" helps to keep errors from creeping into the code and sabotaging your work.

All of the above defines the table structure but doesn't actually reserve any storage for the table. You can do so by either setting an absolute address, which is, in a roundabout way, how the device table address is set in the POC's BIOS ROM source code, or by having the assembler do it for you. Let's assume the latter.

In MOS Technology convention, the statement *=*+N advances the program counter N bytes without generating any code. In other words, this syntax allocates uninitialized storage, what is often referred to as BSS ("block started by symbol"). In this usage, * represents the current value of the program counter, which is the assembler's notion of where it is in memory as assembly progresses. So, to allocate uninitialized storage for my SCSI device table I would code as follows (again, using MOS Technology convention):

Code:
sd_tab   *=*+s_sd_tab

The above will assign the current value of the program counter to the symbol sd_tab and then advance the program counter by s_sd_tab, hence reserving 64 bytes for the table. Elsewhere in the program, I can refer to the device table with the sd_tab symbol, which will always point to the very first byte of the table. Note how the allocation of storage for the table built upon everything that went before it (field size definitions, etc.). The best part is if I restructure the table in some way the assembler will automatically fix up everything and keep it organized.

To access any given row in the table, knowing the SCSI device ID, the program would execute ID × s_sdslot. Recall that s_sdslot is defined so it is always a power of 2. Hence ID × s_sdslot may be computed by left-shifting instead of outright multiplication, leading to smaller and faster code. Remember the sdslotex symbol? It was used to define the row size as a power of 2, resulting in 8 bytes per row in this case. Here's how sdslotex would be used to compute the row address in the table. In the following code snippet, the SCSI ID is in the accumulator (.A):

Code:
         ldx #sdslotex         ;row size factor (power of 2)
;
.0000010 asl a                 ;SCSI ID × 2
         dex
         bne .0000010          ;loop
;
         ldx #>sd_tab          ;device table base MSB
         adc #<sd_tab          ;device table base LSB
         bcc .0000020
;
         inx                   ;account for carry
;
.0000020 sta zpptr             ;set up zero page pointer LSB
         stx zpptr+s_byte      ;set up zero page pointer MSB
;
         ...program continues...

Upon completion, the address of the device flag in the row corresponding to the SCSI ID will be in zpptr and zpptr+1 (s_byte is atomically defined to be 1). To access any given field in the row, you would use the field size and offset together to, say, copy the field to a work location in RAM. For example, to copy the block size field to somewhere in RAM, the following code would be executed:

Code:
         ldx #0                ;storage offset
         ldy #o_sdbsiz         ;block size field offset in row
;
.0000010 lda (zpptr),y         ;get byte from field &...
         sta faca,x            ;store in RAM
         inx                   ;storage_offset=storage_offset+1
         iny                   ;column=column+1
         cpx #s_sdbsiz         ;get all bytes?
         bne .0000010          ;no, loop
;
         ...program continues...

Note how the previously-defined field size and offset in row values are used.

Quote:
Would I even need indirect addressing to accomplish this? Couldn't I just send the address of the sprites first location and index accordingly?

See above examples. It would be kind of hard to avoid indirection in the case of the SCSI device table, at least if some generality is desired. That's not necessarily true in all cases. You'd have to evaluate your particular situation to determine if indirection is required.

If you use the 65C02 (recommended over the NMOS 6502) you have the availability of true indirection, e.g., LDA (<zp>) instead of only LDA (<zp>),Y. The 65C816 also offers stack indirect addressing:

Code:
;   copying device table block size field via stack indirect addressing...
;   SCSI ID is in 16 bit accumulator
;
         longa                 ;ensure 16 bit accumulator
         shortx                ;8 bit index registers
;
;   —————————————————————————————————————————————————————————————————————————
;   Above instructions are macros that issue the proper machine instructions,
;   as well as tell the assembler how to assemble immediate mode values.
;   —————————————————————————————————————————————————————————————————————————
;
         ldx #sdslotex         ;row size factor (power of 2)
;
.0000010 asl a                 ;SCSI ID × 2
         bcs error             ;SCSI ID way out of range!!!
;
         dex
         bne .0000010          ;loop
;
         adc #sd_tab           ;device table base address (16 bits)
         pha                   ;push row address to stack
         shorta                ;8 bit accumulator (a macro)
         ldx #s_sdbsiz-1       ;storage offset
         ldy #o_sdbsiz+s_sdbsiz-1 ;column offset in row
;
.0000020 lda (1,s),y           ;get byte from field &...
         sta faca,x            ;store in RAM workspace
         dey                   ;column=column-1
         dex                   ;storage_offset=storage_offset-1
         bpl .0000020          ;get some more
;
         pla                   ;clean up stack
         pla
;
         ...program continues...

No zero page is used at all in the above code, which assumes the '816 is operating in native mode.

_________________
x86?  We ain't got no x86.  We don't NEED no stinking x86!


Top
 Profile  
Reply with quote  
 Post subject: Re: Memory Management
PostPosted: Tue Jan 01, 2013 1:35 am 
Offline
User avatar

Joined: Wed May 30, 2012 7:45 pm
Posts: 58
Location: Dallas, TX
BDD,

What I meant by ASM is just assembly. Pardon the newbie slang...

I have spent a great deal of time trying to understand your last post. I feel like I'm getting there.
Here are a few questions I have for you:

When you wrote:
Code:
sdslotex =ex2_x3


What did the ex2_x3 mean exactly? I get that it is a "power of 2" which allows you to ASL,
but what is the literal definition. Is it contingent on the the size of the table?

Also, when you wrote:
Code:
s_sd_tab =s_sdslot*n_scsiid    ;table


Am I to understand that s_sd_tab is the size of the slot times the id? I'm sorry, I don't see where
the n_scsiid was defined in your code.

And finally, when you wrote:
Code:
sd_tab   *=*+s_sd_tab


Is this saying that sd_tab is the initial pointer to the entire table? As long as I know the
exact size of the table in bytes, I can use the *=*+ idiom and reserve that chunk of space?

Thanks so much for your input. I have printed it and I'm sure I will use it for reference in the future.

_________________
http://www.thestarrlab.com


Top
 Profile  
Reply with quote  
 Post subject: Re: Memory Management
PostPosted: Tue Jan 01, 2013 11:20 pm 
Offline
User avatar

Joined: Thu May 28, 2009 9:46 pm
Posts: 8392
Location: Midwestern USA
Johnny Starr wrote:
When you wrote:
Code:
sdslotex =ex2_x3

What did the ex2_x3 mean exactly? I get that it is a "power of 2" which allows you to ASL, but what is the literal definition. Is it contingent on the the size of the table?

Bad monkey for skimming instead of reading! :( You obviously missed this:

Code:
sdslotex =ex2_x3               ;row size computation factor...
;
;   ——————————————————————————————————————————————————————————————————————
;   SDSLOTEX is used to atomically define the size of the device table row
;   so bit shifts can be used to compute a row address for a SCSI ID.  The
;   above definition should be changed with caution.  See additional notes
;   in the SSLOTADR function.
;   ——————————————————————————————————————————————————————————————————————
;
s_sdslot =1 << sdslotex        ;row

And apparently skimmed over this too:

    The expression s_sdslot =1 << sdslotex means s_sdslot is equal to 1 left-shifted sdslotex bits, where sdslotex happens to be 8.

If sdslotex=ex2_x3 and sdslotex=8 then ex2_x3=8 is true, eh?

Quote:
Also, when you wrote:

Code:
s_sd_tab =s_sdslot*n_scsiid    ;table

Am I to understand that s_sd_tab is the size of the slot times the id? I'm sorry, I don't see where the n_scsiid was defined in your code.

From the text in my post:

    The symbol n_scsiid is atomically defined as 8, as that is an immutable SCSI hardware characteristic.

Hence s_sd_tab is equal to the size of one row times the maximum number of SCSI devices.

Quote:
And finally, when you wrote:

Code:
sd_tab   *=*+s_sd_tab

Is this saying that sd_tab is the initial pointer to the entire table? As long as I know the exact size of the table in bytes, I can use the *=*+ idiom and reserve that chunk of space?

Correct. Again, that is spelled out in the code examples that I gave, as well as in the text.

I'm not one to yell "RTFM" at people (although I feel compelled to do so at times) but will admonish you to not skim instead of read. Most everything that you need to know to set up a data table in assembly language was discussed. What wasn't discussed is readily inferred. You have to read all of it though.

_________________
x86?  We ain't got no x86.  We don't NEED no stinking x86!


Top
 Profile  
Reply with quote  
 Post subject: Re: Memory Management
PostPosted: Wed Jan 09, 2013 1:07 am 
Offline

Joined: Tue Jul 24, 2012 2:27 am
Posts: 674
Yeah, I accidentally used the wrong assembler directive in my first example. I generally use ca65, which doesn't seem very well represented in this forum; it uses ".res" for this purpose. Here's a C port of what I was trying to get at, as whartung has described already:

Code:
#define MAX_SPRITES 32
typedef unsigned char byte;

/* Static tables, from which you "allocate" sprites 0 to (MAX_SPRITES - 1)*/
byte spriteX[MAX_SPRITES];
byte spriteTile[MAX_SPRITES];
byte spriteAttrib[MAX_SPRITES];
byte spriteY[MAX_SPRITES];

void moveRight(byte spriteNum)  /* in 6502, spriteNum could be held in .X */
{
  spriteX[spriteNum]++;         /* inc spriteX,x */
}


Here's an example of using a singly-linked free list to do allocation and deallocation. We'll have the spriteX of unused sprites hold a 'next' pointer, and use 255 as the magic end-of-list marker. In 6502, you could also try passing end-of-list in processor flags (C, N, or Z).

Code:
#define END_OF_SPRITES 255
byte firstFreeSprite;

void initSpriteTable()
{
  /* Put all sprites in the free list */
  firstFreeSprite = 0;
  for(int i=0; i < MAX_SPRITES-1; ++i)
    spriteX[i] = i+1;
  spriteX[MAX_SPRITES-1] = END_OF_SPRITES;
}

byte allocateSprite()
{
  /* Return the first entry in the list */
  byte sprite = firstFreeSprite;
  if(sprite != END_OF_SPRITES)  /* don't touch anything if we're empty */
    firstFreeSprite = spriteX[sprite];
  return sprite;
}

void deallocateSprite(byte sprite)
{
  /* Shove to the head of the list */
  spriteX[sprite] = firstFreeSprite;  /* reuse the newly dellocated sprite as a linked list node */
  firstFreeSprite = sprite;
}


I haven't done C in a very long time, so excuse any dumb errors. ;) However, all of this should be very semantically close to the exact steps you'd do in 6502, with all indexing being just 0-31 and fitting nicely in index registers without doing any pointer arithmetic.

There are a few of advantages to laying out the tables in this fashion above ("column-based") as opposed to the SCSI table example ("row-based", analogous to an array of C structs). The primary one is how big the index register needs to be. Using 4 bytes per structure, you'd be constrained to a maximum of 64 entries in your table before you'd need indexing bigger than 0-255; even fewer if your structures got bigger. With column-based tables, you can always go up to 256 entries with just a byte offset for an index. Also is the fact that you can adjust to adjacent entries with INX/INY or DEX/DEY instead of shifting the offset to .A and performing "real" math, though that's not really applicable in a dynamically allocated scenario like this sprite usage.

_________________
WFDis Interactive 6502 Disassembler
AcheronVM: A Reconfigurable 16-bit Virtual CPU for the 6502 Microprocessor


Top
 Profile  
Reply with quote  
 Post subject: Re: Memory Management
PostPosted: Wed Jan 09, 2013 3:43 pm 
Offline
User avatar

Joined: Wed May 30, 2012 7:45 pm
Posts: 58
Location: Dallas, TX
@BDD,

Well I have to admit that I did skim a bit. :)
Your reply was very informative, however a bit pedantic
for my taste. ;)

I think what threw me off was trying to decipher what was SCSI specific.

I would like to ask yet another question:

When using [asl a], it translates to "A = A * 2". As long as 'a' is a power of 2.

But what if each "record" in the table is 8 bytes long, couldn't you just [adc #8]

Meaning, if 'a' was #8 and i did [asl a] 'a' would now = #16. But If i shifted again,
instead of being #24 it would be #32 right? And the next iteration would be #64.

_________________
http://www.thestarrlab.com


Top
 Profile  
Reply with quote  
 Post subject: Re: Memory Management
PostPosted: Wed Jan 09, 2013 6:23 pm 
Offline
User avatar

Joined: Thu May 28, 2009 9:46 pm
Posts: 8392
Location: Midwestern USA
Johnny Starr wrote:
@BDD,

Well I have to admit that I did skim a bit. :)
Your reply was very informative, however a bit pedantic for my taste. ;)

Sorry about the pedantry, but I'm trying to make sure the message comes through loud and clear.

Quote:
When using [asl a], it translates to "A = A * 2". As long as 'a' is a power of 2.

But what if each "record" in the table is 8 bytes long, couldn't you just [adc #8]

Meaning, if 'a' was #8 and i did [asl a] 'a' would now = #16. But If i shifted again,
instead of being #24 it would be #32 right? And the next iteration would be #64.

ASL A is the MOS Technology standard syntax for left-shifting the accumulator. In a compliant assembler, A is interpreted to mean the accumulator, not a symbol named A.

As for adding instead of shifting, the former is a slower operation. In my code I was careful to structure the table so each row (slot) is exactly a power of two, allowing shifts to be used instead of addition.

_________________
x86?  We ain't got no x86.  We don't NEED no stinking x86!


Top
 Profile  
Reply with quote  
 Post subject: Re: Memory Management
PostPosted: Wed Jan 09, 2013 8:30 pm 
Offline
User avatar

Joined: Wed May 30, 2012 7:45 pm
Posts: 58
Location: Dallas, TX
Quote:
In my code I was careful to structure the table so each row (slot) is exactly a power of
two, allowing shifts to be used instead of addition.


I would like to use this approach as well for my sprite table.
Each sprite "record" is 4 bytes long. 4 being a power of two, I could iterate
over the table and access each sprite at the 0th byte.

However, does this mean that I have to store each record exactly in bytes apart
that make up what the ASL would be?

For example:

Code:
sprite1 [$xx04][$xx05][$xx06][$xx07] shift 4  * 2
sprite2 [$xx08][$xx09][$xx0A][$xx0B] shift 8  * 2
sprite3 [$xx10][$xx11][$xx12][$xx13] shift 16 * 2
sprite4 [$xx20][$xx21][$xx22][$xx23]


If that is the right way, what about the data inbetween? How much slower is ADC?

_________________
http://www.thestarrlab.com


Top
 Profile  
Reply with quote  
 Post subject: Re: Memory Management
PostPosted: Wed Jan 09, 2013 9:37 pm 
Offline
User avatar

Joined: Fri Aug 30, 2002 1:09 am
Posts: 8514
Location: Southern California
Most of this topic (C and sprites) is outside my areas of expertise, but once the number is in the accumulator, ADC and ASL both take two clocks. ADC however involves the carry flag; so if you can't be sure it has the right value to start (usually meaning 0), you have to add a CLC in front of ADC, adding another two clocks.

_________________
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?


Top
 Profile  
Reply with quote  
 Post subject: Re: Memory Management
PostPosted: Wed Jan 09, 2013 10:54 pm 
Offline

Joined: Tue Jul 24, 2012 2:27 am
Posts: 674
Johnny: The ASL isn't used to move from one record to another. It's to convert a record number into a pointer offset.

Each record is 4 bytes, so multiply your record number by 4 (ASL twice) to convert to a pointer offset. e.g., if you want to point at entry 10 in the table, you want to use offset 10*4 = (10<<1)<<1 = 40 into the table.

Code:
lda recordNumber
asl
asl
tax
lda table,x

To go from one record to the next, add or subtract 4 from the offset (4x INX or DEX, smaller than transferring to .A and ADC/SBC, using the same number of clock cycles). All 4-byte structures are completely adjacent in memory.

Or, you could use the column-based approach from my example and skip all this hassle. ;)

_________________
WFDis Interactive 6502 Disassembler
AcheronVM: A Reconfigurable 16-bit Virtual CPU for the 6502 Microprocessor


Top
 Profile  
Reply with quote  
 Post subject: Re: Memory Management
PostPosted: Wed Jan 09, 2013 11:57 pm 
Offline

Joined: Sun Nov 08, 2009 1:56 am
Posts: 396
Location: Minnesota
Quote:
However, does this mean that I have to store each record exactly in bytes apart
that make up what the ASL would be?


When you use a row-based approach to array layout every record, no matter what its size, is stored consecutively in memory. That implies that to get the base address of an arbitrary element N, you have perform a multiplication.

ASL is equivalent to multiplying by two, so ASL is a handy way to access elements in row-based arrays whose record size is a power of two. Sometimes it's even worth wasting a little space - if the 'natural' record size is, say, seven bytes, it might pay to start each record on an eight-byte boundary and use three ASL instructions to convert from an element number to an offset. That would be the equivalent of 'structure padding' in C.

For non-power-of-two record sizes which are known to be fixed, it's also reasonably easy to combine ASL and ADC to multiply an index by that fixed value rather than use a slower general-purpose multiplication routine. Using a record size of ten and assuming a zero-based index of no more than 25 records total:

Code:
LDA recordNum
ASL        ; *2
STA temp
ASL        ; *4
ASL        ; *8
ADC temp   ; *10
TAX


Note that each ASL clears the carry bit in this example (top three bits of the index are always clear), so there's no need to set its state explicitly.

Another approach is to have the assembler "pre-calculate" part or all of the possible multiplications and store the results in a table. When accessing a particular bit in the 8K of hi-res graphics memory in a C64 with its unusual cell-based layout, for example, you'd often see code like this:

Code:

; offsets to start of each 320-byte row

vidRowOffset:

]addr = videoBase   ; graphics memory starts on an 8K boundary
repeat 25
    da ]addr        ; store pointer to row
]addr = ]addr+320   ; update address to next row
endr


This stores pointers to the first 8x8 pixel cell in a row of 40 of them (25 rows * 40 cols = 1000 cells of eight bytes each). Obviously this isn't going to fit into a single byte offset in the X- or Y-register!

The table would be used like this:

Code:
LDA y_pos        ; 0->199
LSR              ; 0->98
LSR              ; 0->48
LSR              ; 0->24
TAX
LDA vidRowOffset,X
STA addrPtr
LDA vidRowOffset+1,X
STA addrPtr+1


The code would go on to add the X position to the looked-up start address of any row. The point is the multiplication was done at assembly time, rather than run time.


Top
 Profile  
Reply with quote  
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 27 posts ]  Go to page Previous  1, 2

All times are UTC


Who is online

Users browsing this forum: No registered users and 11 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to: