Jump to content
IGNORED

Let's Talk About Sprites


TheBF

Recommended Posts

So in the journey to have some fun with the old TI I finally got to the place with my system where I had to deal with Sprites.
TI Extended Basic does an excellent job of hiding all the details of how these things work making it easy to use.
How it really works was news to me so I had to do some sleuthing around and peeking inside the code of Willsy's TurboForth provided excellent insights as always.
For ease of communication about what I learned I will use XT-BASIC terminology unless there is no alternative.
My hope is this will be interesting to those who like to use sprites in their games and want to "peek behind the curtain"
* For those who really know about Sprites, please jump in and correct the stuff I have misunderstood *
--------
How it works in BASIC
The Sprite control in the VDP chip memory is contained in something called the Sprite Attribute List (SAL).
In BASIC it could be understood like this:
100 DIM SAL$(32)

(The Sprites are indexed from 0 to 31. #1 is actually 0, but we will use option base 1 terminology to avoid confusion)

 

There is a requirement that cannot be defined in BASIC but the SAL$() array MUST begin at a specific address in the VDP memory.

So let's pretend our imaginary BASIC looked after that for us and started the array at HEX 300.

 

Each element in the SAL$() can only be 4 bytes long.
The function of the bytes are as follows:

 byte1: X location
 byte2: Y location
 byte3: ASCII char to use as a pattern descriptor
 byte4: Split into 2 "nibbles": right half clock bit, left half color

Each byte has a specific function in controlling a SPRITE
The only way to express it in BASIC would be to do something like this:

300 SAL$(1)=CHR$(X) & CHR$(Y) & CHR$(PAT) & CHR$(CLR)

So in our imaginary BASIC, if you ran this code:

300 SAL$(1)=CHR$(10) & CHR$(10) & CHR$(65) & CHR$(3)

A green letter "A" sprite would appear at pixels 10,10.

Ya. It's that simple. You put the correct bytes into the right spot in VDP memory and the TMS9918 chip does all the hard work.

Now in fact these data are not strings. They are 8 bit numbers.
So another way to think of this in BASIC is a "byte" array (not really possible but you get what I mean):

310 DIM SAL(32,4)

So in this case our fancy basic would let us put a number in each element this way:

319 REM create Sprite #1 in VDP memory
320 SAL(1,1)=10     ( X coordinate pixel)
330 SAL(1,2)=10     ( Y coordinate pixel)
340 SAL(1,3)=65     ( ASCII char)
350 SAL(1,4)=3      ( foreground color)

Let's stick with this byte array example for the next step.

 

How do Sprites move by themselves?
There is another array that holds the information for MOTION.
It can be understood as a 2 dimensional array:
400 DIM MOTION(32,2)
So when you write CALL MOTION(#1,10,-4) what really happens is:
LET MOTION(1,1)= 10
LET MOTION(1,2)= -4

That's it. CALL MOTION stores the numbers in the MOTION() array.

The part that makes the sprites move is a special fast sub-routine that that does something like this:

490 REM MOVE ALL SPRITES
500 FOR SPRITE=1 TO 32
510    SAL(SPRITE,1)=SAL(SPRITE,1) + MOTION(SPRITE,1)
520    SAL(SPRITE,2)=SAL(SPRITE,2) + MOTION(SPRITE,2)
530 NEXT SPRITE

So as that loop runs, each sprite has the motion value (x,y) added to the LOCATION(x,y) values for the sprite in VDP memory.

So the sprite moves to new location. Simple.

 

ISR is the Magic

The only thing missing is automatic motion. That is done with something called an interrupt and our little program becomes what is called an Interrupt Service Routine (ISR).
This is a fancy name for a program that stops YOUR program every now and then and does something else for a while.
So in the TI-99 there is a timer running at 60 times a second (or 50 in European models) and when it "interrupts" your BASIC program, it runs the "MOVE ALL SPRITES" program and then returns to run BASIC.

So that's the normal way it works.(as far as I can tell)

 

I wondered could it be done differently? Do I need an ISR?

 

I have a multi-tasker in my system.

 

More to come...

  • Like 3
Link to comment
Share on other sites

Sprites are a function of the VDP. For the raw details about sprites the 9918A datasheet is the primary source. The 9918A is a table-driven device, and there are two tables associated with sprites:

1. Sprite Attribute Table (SAT)
2. Sprite Pattern Generator Table (SPGT)

The location of these tables in VRAM are controlled by two of the eight VDP Registers (VR), VR5 (SAT) and VR6 (SPGT). The SAT can be located on 128-byte boundaries in VRAM, and the SPGT can be located on 2K boundaries.

The size of sprites (8x8 or 16x16 pixels) is controlled by bit >02 of VR2, and the magnification of sprites is controlled by bit >01 of VR2.

The original 9918A can only display 4 sprites per horizontal line. Sprites over that limit are not drawn for the given horizontal line. The VDP status register indicates the 5th sprite that would have been drawn if more than 4 sprites are on a line. Also, if any visible sprites have a collision with their '1' pattern pixels (color does not matter), the collision bit in the status register will be set.

Sprites are buffered, so they actually appear one line lower on the screen than their Y position in the SAT.

The SAT consists of 4-bytes per sprite, as you indicated:

      0       1       2       3       4       5       6       7
  +---------------------------------------------------------------+
  |                                                               |
0 |                       Y POSITION                              |
  |                                                               |
  +---------------------------------------------------------------+
  |                                                               |
1 |                       X POSITION                              |
  |                                                               |
  +---------------------------------------------------------------+
  |                                                               |
2 |                          NAME                                 |
  |                                                               |
  +-------+-------+-------+-------+-------------------------------+
  | EARLY |       |       |       |                               |
3 | CLOCK |   0   |   0   |   0   |            COLOR CODE         |
  | BIT   |       |       |       |                               |
  +-------+-------+-------+-------+-------------------------------+


If any sprite has the Y position of 204 (>D0), sprite processing will stop at that sprite. You can disable all sprites by writing >D0 to the Y position of sprite #0.

In XB the auto movement is done by the ISR as you mentioned. Nothing says you have to do it that way, but the ISR makes a nice consistent periodic cycle for such things.

Edited by matthew180
  • Like 2
Link to comment
Share on other sites

The sprite motion table confused me at first because AFAIR the E/A manual is not very clear about the fact that this is not a table of the hardware. It's great for XB, but for assembly, Forth, etc. you can definitely do without it and roll your own more efficient routine. I have never really used the ISR or the automatic sprite motion in any of my stuff because I like to stay in control of both clock cycles and scratchpad RAM.

 

[EDIT: And I since the sprite motion table always starts at >0780 it's not compatible with bitmap mode and restricts your VDP memory layout in general.]

Link to comment
Share on other sites

One other tip, the SPRITE subcommand can do everything that LOCATE, PATTERN, MOTION and COLOR can do. There's no harm in re-running the SPRITE command to move your sprite for example, you just have to provide the same list of variables.

 

I wish the subcommands were better optimized for multiple sprites though. I had to resort to assembly to manage sprites in Wizard's Doom effectively. Even if you use constant values and declare multiple sprites in a single statement, it still seems to write the sprites one at a time to VDP rather than doing them as a block. (Which makes sense, you could declare sprites out of sequence in a statement so it can't assume they are all contiguous.)

Link to comment
Share on other sites

The sprite motion table confused me at first because AFAIR the E/A manual is not very clear about the fact that this is not a table of the hardware. It's great for XB, but for assembly, Forth, etc. you can definitely do without it and roll your own more efficient routine. I have never really used the ISR or the automatic sprite motion in any of my stuff because I like to stay in control of both clock cycles and scratchpad RAM.

 

[EDIT: And I since the sprite motion table always starts at >0780 it's not compatible with bitmap mode and restricts your VDP memory layout in general.]

 

All good reasons to roll your own. And the one I found was if I want to add X and Y in the sprite table (VDP RAM) to 2 other numbers in the motion table (also VDP RAM)

You have to read them both out of VDP ram, then add them, then put the new location x y back into VDP ram. That was pretty slow.

I created the motion table in memory expansion so I can get at the motion vectors faster.

 

Then I made a couple of Assembler routines that can read and write 2 bytes to/from the Sprite table.

Then another one that can add the location and motion vectors together and combine them back into one 16 bit number.

 

Using those little routines, it goes pretty fast and I only update the sprites that I want to update when I want to update.

 

I have found the sprites work very well when I use my multitasker to do the updating as a separate task.

And since it is not on an interrupt, my program gets full control when I need to test for coincidence.

 

Now I need to create something with it.

 

Actually I think these kind of very small assembler routines could be used nicely from XB as well.

Edited by TheBF
Link to comment
Share on other sites

Sprites are a function of the VDP. For the raw details about sprites the 9918A datasheet is the primary source. The 9918A is a table-driven device, and there are two tables associated with sprites:

 

1. Sprite Attribute Table (SAT)

2. Sprite Pattern Generator Table (SPGT)

 

The location of these tables in VRAM are controlled by two of the eight VDP Registers (VR), VR5 (SAT) and VR6 (SPGT). The SAT can be located on 128-byte boundaries in VRAM, and the SPGT can be located on 2K boundaries.

 

The size of sprites (8x8 or 16x16 pixels) is controlled by bit >02 of VR2, and the magnification of sprites is controlled by bit >01 of VR2.

 

The original 9918A can only display 4 sprites per horizontal line. Sprites over that limit are not drawn for the given horizontal line. The VDP status register indicates the 5th sprite that would have been drawn if more than 4 sprites are on a line. Also, if any visible sprites have a collision with their '1' pattern pixels (color does not matter), the collision bit in the status register will be set.

 

Sprites are buffered, so they actually appear one line lower on the screen than their Y position in the SAT.

 

The SAT consists of 4-bytes per sprite, as you indicated:

 

      0       1       2       3       4       5       6       7
  +---------------------------------------------------------------+
  |                                                               |
0 |                       Y POSITION                              |
  |                                                               |
  +---------------------------------------------------------------+
  |                                                               |
1 |                       X POSITION                              |
  |                                                               |
  +---------------------------------------------------------------+
  |                                                               |
2 |                          NAME                                 |
  |                                                               |
  +-------+-------+-------+-------+-------------------------------+
  | EARLY |       |       |       |                               |
3 | CLOCK |   0   |   0   |   0   |            COLOR CODE         |
  | BIT   |       |       |       |                               |
  +-------+-------+-------+-------+-------------------------------+

 

If any sprite has the Y position of 204 (>D0), sprite processing will stop at that sprite. You can disable all sprites by writing >D0 to the Y position of sprite #0.

 

In XB the auto movement is done by the ISR as you mentioned. Nothing says you have to do it that way, but the ISR makes a nice consistent periodic cycle for such things.

In case no one mentioned it to everyone the ISR is in Extended Basic ROM 0:

**************************** EXECG XML ****************************************
*
* EXECUTE A XB STATEMENT IN A PROGRAM *
*
EXECG  EQU  $
* Substack pointer in R9 
* and actual Basic byte in R8
LN6500 BL   @>1E7A                     
LN6504 CLR  @ERRCOD          * Zero out ERRCOD           
LN6508 MOVB @PRGFLG,R0       * Put PRGFLG into R0 (Program Flag byte 0=Edit Mode)             
LN650C JEQ  LN6542           * Equal jump Turn on Interupts, SCAN Keyboard           
LN650E MOVB @FLAG,R0         *#  Put FLAG into R0           
LN6512 SLA  R0,3             * Same as R0 * 8           
LN6514 JLT  LN65D4           *# Lower Than jump           
LN6516 MOV  @EXTRAM,@PGMPTR  * Put EXTRAM into PGMPTR          
LN651C DECT @PGMPTR          * PGMPTR - 2  
* Get a XB token & increment Program Pointer (zero VDP / Any value ERAM)         
LN6520 BL   @LN6C74                     
LN6524 STST R0                          
LN6526 INC  @PGMPTR          * PGMPTR  + 1  
* Get a XB token & increment Program Pointer (zero VDP / Any value ERAM)         
LN652A BL   @LN6C74                     
LN652E SWPB R8               * Switch MSB:LSB of R8
* Get a XB token & increment Program Pointer (zero VDP / Any value ERAM)                         
LN6530 BL   @LN6C74          *            
LN6534 SWPB R8               *            
LN6536 MOV  R8,@PGMPTR       * Put R8 into PGMPTR           
LN653A CLR  R8               * Zero out R8           
LN653C SLA  R0,2             * Same as R0 * 4           
LN653E JLT  LN6542           * Lower Than jump Turn on Interupts, SCAN Keyboard           
LN6540 JNC  LN6636           * No Carry jump           
LN6542 LIMI >0003            *# Turn on Interupts, SCAN Keyboard 
LN6544 EQU  >6544     * <<<<<<<<<<<<<<<<<< Find Another Value Location
LN6545 EQU  >6545     * <<<<<<<<<<<<<<<<<< Fine Another Value Location                
LN6546 LIMI >0000            * Turn off INterupts           
LN654A CLR  @>83D6           * CLEAR VDP SCREEN TIMEOUT
*
* KEY SCAN                        
LN654E BL   @>000E           * KEY SCAN ROUTINE          
LN6552 C    @LN6000,@RKEY    * >0002(FCTN 4 BREAK)= RKEY ?>  (>8375 & >8376)             
LN6558 JEQ  LN65D2           * Equal jump           
LN655A JMP  LN6576           * jump                                                                                                                               655C
LN655C CZC  @LN600A,R0       
LN6560 JNE  LN6576           * Not Equal jump
LN6562 LI   R12,>0024        * Put >0024 (36) into R12
LN6566 LDCR @LN6545,3        * >03
LN656A LI   R12,>0006        * Put >006 into R12   
LN656E STCR R0,8         
LN6570 CZC  @>600A,R0    
LN6574 JEQ  LN65D2           * Equal jump                    
                                                        
LN6576 MOV  @PGMPTR,@SMTSRT   *# Put PGMPTR into SMTSRT            
LN657C INCT R9                * R9 + 2          
LN657E MOV  @LN6466,*R9       * Put >65A6 into Address R9
* Get a XB token & increment Program Pointer (zero VDP / Any value ERAM)             
LN6582 BL   @LN6C74           *           
LN6586 JEQ  LN65C8            * Equal jump          
LN6588 JLT  LN658E            *# Lower Than jump               
LN658A B    @LN6948           * Get Token and check for = or ,
*                       
LN658E MOV  R8,R7             *# Put R8 into R7               
LN6590 INC  @PGMPTR           * PGMPTR + 1         
LN6594 MOVB *R10,R8           * Put ADDRESS MSB R10 into MSB R8          
LN6596 SRL  R7,7                        
LN6598 AI   R7,>FEAC          * Add >FEAC into R7
* PUt >0003 into R0, Return ERROR CODE back to GPL                    
LN659C JGT  LN664E            * Greater Than jump          
LN659E MOV  @LN69FC(R7),R7    * >8010+R7 into R7             
LN65A2 JLT  LN64C2            * Lower Than jump          
LN65A4 B    *R7               * RETURN                 
*
* FLIP ADDRESS >65A6 TO SWAP UPPER ROMS *
*
LN65A6 C    @>0288(R5),R13    *# >0288+R5 = R13 ?
LN65A7 EQU  >65A7    * <<<<<<<<<<<<<<<<<<<<< Find another value          
LN65AA C    R0,R8             * R0 = R8 ?          
LN65AC JEQ  LN6542            * Equal jump Turn on Interupts, SCAN Keyboard          
LN65AE MOVB @PRGFLG,R0        *# Put MSB PRGFLG into MSB R0              
LN65B2 JEQ  LN6656            * jump          
LN65B4 S    @LN6A80,@EXTRAM   * EXTRAM->0004  >>>>>FIND ANOTHER >0004<<<<<          
LN65BA C    @EXTRAM,@STLN     * EXTRAM = STLN ?          
LN65C0 JHE  LN650E            * Higher Equal jump EXECG          
LN65C2 JMP  LN6656            *# jump
*                    
LN65C4 MOVB R8,R8             * Put MSB R8 into MSB R8 (Set Equal Bit)          
LN65C6 JNE  LN6588            * Not Equal jump          
LN65C8 DECT R9                *# R9 - 2                    
LN65CA JMP  LN65AE            * jump     
*                 
***************************** CONTIN XML ********************************
*
* CONTINUE AFTER A BREAK *
* V@SAVEVP = INIT FOR PROGRAM COMPLETION
* FLAG BYTES FOR CONTROL:
* @PRGFLG = PROGRAM MODE
* @RAMFLG = RANFLAG
*
CONTIN EQU  $
* Substack pointer in R9 
* and actual Basic byte in R8
LN65CC BL   @>1E7A                      
LN65D0 JMP  LN6542           *# Turn on Interupts, SCAN Keyboard
*                    
LN65D2 JMP  LN6644           * jump
*                    
LN65D4 JMP  LN6672           * jump
Link to comment
Share on other sites

I didn't bother with interrupt driven sprites in TurboForth. I think XB put me off the notion. When the ISR is used, the movement and animation of your sprites is somewhat divorced from your main logic, and I never quite liked that. You'll see it in various XB games where a bullet hits a character and goes right through it, out the other side, and keeps going, and *then* the logic detects that a hit occurred. Never liked it.

I went for a "Charles Moore" approach WRT sprites (i.e. nice and simple ;-) ). You add "movement vectors" using SPRVEC, and then, each and every time you call SPRMOV the sprites will move according to their vectors. The more often you call SPRMOV, the faster the sprites move. Simple as that. This means your sprite control and other logic are very closely coupled. You can move your sprites, check coordinates etc.

The following code is taken from the sprites tutorial:

hex
: square ( --) \ define sprite pattern 0 as a solid square
  data 4 $FFFF $FFFF $FFFF $FFFF 100 DCHAR ;
decimal
 
\ define colours:
 8 constant red
15 constant white
 5 constant blue
11 constant yellow
 1 constant black
 
: init ( -- )
    1 gmode       \ 32 column mode 
    black screen  \ black screen
    0 magnify     \ single 8x8 sprites
    square        \ define the sprite pattern
   
    \ set up the 4 sprites:
    ( sprite 0) 0 100 100 0 red sprite
    ( sprite 1) 1 110 100 0 white sprite
    ( sprite 2) 2 120 100 0 blue sprite
    ( sprite 3) 3 130 100 0 yellow sprite
   
    \ now define the movement vectors:
    ( sprite 0) 0 0 1 sprvec
    ( sprite 1) 1 0 2 sprvec
    ( sprite 2) 2 0 3 sprvec
    ( sprite 3) 3 0 4 sprvec ;
   
: delay ( n -- ) \ delay loop
    0 do loop ;
   
: moveSprites ( -- ) \ move the sprites 256 times
    init         \ initialise the sprites
    256 0 do 
      0 4 sprmov 
      500 delay  \ we need a delay otherwise its just a blur!
    loop ;

 

SPRVEC glossary entry

SPRMOV glossary entry

Edited by Willsy
Link to comment
Share on other sites

...

You have to read them both out of VDP ram, then add them, then put the new location x y back into VDP ram. That was pretty slow.

 

I created the motion table in memory expansion so I can get at the motion vectors faster.

 

Then I made a couple of Assembler routines that can read and write 2 bytes to/from the Sprite table.

Then another one that can add the location and motion vectors together and combine them back into one 16 bit number.

 

Using those little routines, it goes pretty fast and I only update the sprites that I want to update when I want to update.

...

You should always try to treat VRAM as write-only. Don't store data in VRAM and accessing it won't be slow. A lot of programs store the entire SAT in CPU RAM and just copy all 128 bytes whenever they need to update the sprites. This can be faster than you think because you can take advantage of the VDP Address Register's auto-increment.

 

With your method, you need to realize that for every two bytes your read then write, you have to set up the VDP Address Register twice which adds four more writes. To read/write 2 bytes you actually move 8 bytes of data. If you are only updating a few sprites it might be wroth it, but for a lot of sprites it is probably faster to update in bulk.

Edited by matthew180
Link to comment
Share on other sites

In fbForth 2.0, sprites are handled as they are in TI Forth (pretty much as in XB), except that the code is ALC (Assembly Language Code) rather than TI Forth's high-level Forth. This was done because of my desire to maintain backward compatibility with TI Forth.

 

The programmer can certainly set up sprite motion as in CAMEL99 Forth or TurboForth. I do like the idea of not relying on the Motion table and setting up duplicate tables in RAM to avoid reading VRAM. And of course, the motion table is not used by the console ISR in bitmap mode, so “automotion” can only be effected by the programmer.

 

...lee

Link to comment
Share on other sites

 

Shouldn't that be write-only?

Yes I think it is a typo. Should be write to the VDP is preferred.

 

So I measured what I had and been doing with each sprite motion, 1 at a time, in Forth, with some ALC assistance to add x,y to the motion number and combine to 16 bits.

This took 66 ticks on the 9901 time (1.3mS) for each computation and update.

 

To write an entire sprite table (128 bytes) with VMBW takes 88 ticks or 1.8mS! :-)

 

So Master Lee is right again.

 

I changed everything. I still update each sprite motion in a Forth loop, but then I write it all to VDP at once.

My SPRITE command keeps track of how many sprites are created so I only update as many as needed.

 

Then I wrapped the whole thing up in a background task and it works pretty much like XB now.

 

Here is what the "automotion" task looks like in CAMEL99 Multi-Forth.

(I am going to steal that name for the task Lee. It's a good one.)

 

Compared to the TI ISR that RXB posted it's pretty simple.

CREATE MOVER  USIZE ALLOT  \ create mem block for a task

MOVER FORK                 \ init the mem block to be a task

: SPMOVER                  \ make a Forth word
       BEGIN               \ begin a loop
         SPRITE# @ 1+ 0    \ FOR I=SPRITE#+1 TO  0
         DO                \
            I SP.MOVE PAUSE  \ calc next position of SPRITE(I)
         LOOP                \ & give some time to other tasks
         SP.UPDATE 1 MS    \ write to VDP, wait 10 ms (I will fix this)
       AGAIN ;             \ GOTO BEGIN

' SPMOVER MOVER ASSIGN     \ ASSIGN the address (') of SPMOVER to MOVER
 

To get it going you just say MOVER WAKE.

 

I will get a demo video next week and post the code on GITHUB.

Got a wedding to go to.

Edited by TheBF
Link to comment
Share on other sites

To write an entire sprite table (128 bytes) with VMBW takes 88 ticks or 1.8mS! :-)

 

Since the transfer length is fixed you can easily get more speed out of VMBW by unrolling the loop. And you can also load the VDPWD address into a register for additional speed.

Link to comment
Share on other sites

 

Since the transfer length is fixed you can easily get more speed out of VMBW by unrolling the loop. And you can also load the VDPWD address into a register for additional speed.

 

Yes that is a cool enhancement. I saw a post by someone here, maybe you, about that put it in my VMBW and measure the speed increase at +12.9%.

I am crammed for space right now because my homemade cross-compiler can only make one 8K program image. (gotta fix that one day)

So I can't un-roll the loop in this case.

Edited by TheBF
Link to comment
Share on other sites

Nice. And as you point out, if you have a section like DUMP, not designed to work in a concurrent system, then you notice the biggest problem compared to a pre-emptive one.

But you can't have all advantages without some cons.

 

Ya that's only real downside. And if I REALLY needed a HEX dump in the middle of a video game ( ;) ) I literally just reduce

the priority by inserting the word PAUSE into the loops that are printing out the text and numbers.

For example below is the offending code. Notice the routines called: .CELLS .ASCII and DUMP.

Each routine is a DO/LOOP.

 

To reduce their GOL DARN CPU stealing ways, all you do with this system is put word 'PAUSE' after the word DO in each loop.

I put it in brackets here, but I should add it to the actual code anyway.

 

PAUSE is the actual task switching routine, so you can see how other tasks will get serviced before doing the work inside the loop.

And if you really want to reduce their priority, you add more PAUSEs or you can use MS which will delay but continuously give the CPU to the other tasks

while it is waiting for the timer to expire.

 

I didn't invent this stuff if was perfected in the 1970s but Chuck Moore and Forth Inc. It's pretty cool for lower power computing

because it's lower overhead. This context switch on the 9900 changes in 20uS if it ran proper full speed memory.

On the 8 bit buss it's what? double or so. But still ridiculously fast compared to conventional switchers.

 

By the way, the word TYPE is also a DO/LOOP that prints our 'n' characters at any address. It was made properly for multi-tasking

which is why the sprites didn't stop dead when the DUMP ran on the screen. TYPE was giving away the CPU after each character

was put on the screen.


8 CONSTANT 8  \ no. of bytes to dump for each line

: BOUNDS      ( adr len -- end-adr start-adr  OVER + SWAP ; 
: .####       ( n --)     S>D <# # # # # #> TYPE ;
: .ADR        ( ADR --)   .####  [CHAR] : EMIT ;
: .CELLS      ( ADR N --) BOUNDS DO  ( PAUSE)  SPACE  I @ .####    2 +LOOP ;

: .ASCII      ( adr n --)    \ print ascii values or '.' for non-printable chars
              BOUNDS
              DO  ( PAUSE) 
                 I C@ DUP
                 BL 1-  T[CHAR] ~ WITHIN  \ check for printable char (from SPACE to ASCII '~')
                 0= IF DROP  T[CHAR] .    \ replace unprintable chars with '.'
                 THEN EMIT
              LOOP ;

: DUMP        ( offset n -- )
               BOUNDS
               DO ( PAUSE )                    \ 'I' is the address pointer
                  CR @ I  .ADR        \    print the adr
                  I  8 .CELLS SPACE   \ print 8 bytes of memory
                  I  8 .ASCII         \ print 16 ascii format BYTES
                  KEY? IF LEAVE THEN
               8 +LOOP                \ increment the offset address by 16
               CR ;

Link to comment
Share on other sites

Very nice, indeed!

 

...lee

 

Thanks Lee. That means a lot to me coming from you.

 

If you want this kind of option in FB Forth, it's not hard to add.

I created a weird way to make user variables. I extend the workspace after the registers.

But with TI Forth architecture you have a USER Pointer? or no?

 

Anyway I have this RTWP version which I published here earlier and I also started with this alternative version

that is for a more conventional machine that you could use.

 

Happy to answer any questions should you want to do a port of it.

 

I find it a lot of fun to make the little TI look like a big machine.

 

If I ever get my hardware back up and running, I would want to have a terminal task running on RS232

and the graphics stuff/ games working as it's own job. Utopia...

 

 

 

\ TASKS99.HSF for CAMEL99                               06JAN2017 Brian Fox

\ Loosely derived from COOPTASK.MAX for MaxForth        B Fox 1992

[undefined] XASSEMBLER [IF] ."  **This is for XASM99 cross compiler"
                            cr ." Compile halted."  ABORT [THEN]

\ This is a conventional Forth multi-tasker using a single workspace
\ and stacks. It uses CPU R15 as the USER pointer register.

\ It is interesting to note that the Forth virtual machine uses 3 registers
\ for context,two stack pointers and the instruction pointer and the TMS9900
\ also uses 3 registers for context, WP, PC and ST.

\ =======================================================================
\ CAMEL99 MULTI-TASKING USER AREA
\ -----------------------------------------------------------------------
\ R0   LOCAL general purpose register     ( workspace begins)
\ R1   LOCAL general purpose register
\ R2   LOCAL general purpose register
\ R3   LOCAL general purpose register
\ R4   LOCAL Top of stack cache
\ R5   LOCAL overflow for mult. & div.,       // general purpose register (used by NEXT)
\ R6   LOCAL parameter stack pointer ('SP')
\ R7   LOCAL return stack pointer    ('RP')
\ R8   LOCAL Forth working register  ('W')    // general purpose register in code words
\ R9   LOCAL Forth interpreter pointer ('IP)
\ R10  LOCAL Forth's "NEXT" routine cached in R10
\ R11  LOCAL 9900 sub-routine return register // general purpose register in code words
\ R12  LOCAL 9900 CRU register                // general purpose register in code words
\ R13  LOCAL DO/LOOP index
\ R14  LOCAL DO/LOOP limit
\ **NEW**
\ R15  LOCAL User pointer. (UP) pointer to User area base address, right after workspace

\ ------------------------------------------------------------------------
\ there is apace after the registers for 16 user variables

\ Index      Name
\ ------------------
\  0    USER TFLAG    LOCAL task's awake/asleep flag
\  2    USER TLINK    link to the next task in the round-robin queue
\  4    USER RSAVE    storage for my Return stack pointer
\  6    USER RUN      hold that word that runs in this task
\  8    USER VAR4
\  A    USER VAR5
\  C    USER VAR6
\  E    USER VAR7
\ 10    USER VAR8
\ 12    USER VAR9
\ 16    USER VAR10
\ 18    USER VAR11
\ 1A    USER VAR12
\ 1C    USER VAR13
\ 1E    USER VAR14
\ 1F    USER VAR15
\ -----------------------------------------------------------------------
\  20 CELLS  LOCAL Parameter stack base address (grows downwards)
\  20 CELLS  LOCAL Return stack base address    (grows downwards)
\ =======================================================================


CROSS-ASSEMBLING

CODE: >USER  ( n -- addr)        \ given n calculate a 'local' user-variable address using the USER pointer register
             UP TOS ADD,         \ add UP to TOS
             NEXT,
             END-CODE

\ Coventional Forth Pause
CODE: PAUSE  ( -- )                  \ this is the context switcher
              SP RPUSH,              \ 28
              IP RPUSH,              \ 28
              RP  4 (UP) MOV,        \ 22 save my return stack pointer in RSAVE user-var
              BEGIN,
                 2 (UP) UP MOV,      \ 22 load the next task's UP into CPU UP  (context switch)
                 *UP R0 MOV,         \ 18 test the tlag for zero
              NE UNTIL,              \ 10 loop until it's not zero
              4 (UP) RP MOV,         \ 22 restore local Return stack pointer so I can retrieve IP and SP
              IP RPOP,               \ 22 load this task's IP
              SP RPOP,               \ 22  load this task's SP
              NEXT,               \ = 194 * .333 = 64.6uS context switch
              END-CODE

CODE: MYSELF  ( -- PID-addr)      \ because UP is a register we need code to read/WRITE it
            TOS PUSH,
            UP TOS MOV,
            NEXT,
            END-CODE

CODE: UP!   ( addr -- )
            TOS UP MOV,
            TOS POP,
            NEXT,
            END-CODE

[CC] HEX 8300 20 + [TC] CONSTANT: USER0   \ USER0 is the main task's user-area just above workspace, also called a PID

[CC]
    DECIMAL
    16 cells
    16 cells +
    20 CELLS +
    20 CELLS +
    [TC] CONSTANT: USIZE   \ user-vars. + Pstack + Rstack  = 32+40+40 = 112 bytes per task

TARGET-COMPILING
\ name some user variables without create/does>
: TFLAG  ( -- addr)  0 >user ;  \ this is 1 cell after register 15
: TLINK  ( -- addr)  2 >user ;  \ this is 2 cells after register 15
: RSAVE  ( -- addr)  4 >user ;  \ 4 cells after R15
: RUN    ( -- addr)  6 >user ;  \ local variable hold XT of word to run for this task
: CNT    ( -- addr)  8 >user ;  \ local variable for testing

: LOCAL   ( PID uvar -- addr) MYSELF -  + ;   \ usage:  TASK1 TSP LOCAL @
: SLEEP  ( task -- )  0 SWAP TFLAG LOCAL ! ;
: WAKE   ( task -- ) -1 SWAP TFLAG LOCAL ! ;

( *** YOU  M U S T  call INIT-MULTI ONCE before multi-tasking  ***)
: INIT-MULTI ( -- )
             USER0 UP!          \ set my user-pointer register
             MYSELF TLINK !
             TRUE TFLAG !  ;   \ Set my tlink to my own user-area, mark myself awake

\ these words allow us to push values onto a local return stack
: }RP++ ( task -- )  -2 SWAP RSAVE LOCAL +! ;         \ make space on the local Rstack
: }>R  ( n task -- )  DUP }RP++  RSAVE LOCAL @  ! ;   \ push n onto local Rstack )

[CC] HEX
TARGET-COMPILING
: MAKE-USER ( taskaddr -- )
            >R                                   \ copy taskaddr
            R@ USIZE FF FILL                     \ erase user area
            USER0 R@ 20 CMOVE                    \ copy USER0 vars to taskaddr

            R@ 90 +  R@ RSAVE LOCAL !            \ set Rstack base to this user area

            TLINK @                              \ get copy of current users workspace addr
            R@ TLINK !                           \ store taskaddr in curr. user's tlink
            R@ TLINK LOCAL !                     \ now store curr. user into taskaddr's space

            R> SLEEP  ;                          \ put the new task to sleep

: ASSIGN ( XT task -- )                        \ put stack address and XT onto local task's Rstack
           2dup run local !
           dup 58 +  over }>r                    \ calc local SP base, push to rstack (Pstack is empty)
           dup run local over }>r ;              \ push addr of RUN onto local Rstack (goes into IP when task runs)

TARGET-COMPILING

\ Syntax for setting up 2 tasks:
\ ------------------------------

\ INIT-MULTI              ( setup the root task for mult-tasking)

\ F000 CONSTANT TASK1    ( pointers to some unused memory
\ EF00 CONSTANT TASK2

\ TASK1 MAKE-USER         ( initialize the memory to be a user-area)
\ TASK2 MAKE-USER

\ VARIABLE X1
\ VARIABEL X2

\ : THING1  begin   1 X1 +!  pause again  ;  \ code that needs to run in a task
\ : THING2  begin  -1 X2 +!  pause again ;   \ code that needs to run in a task

\   T' THING1 TASK1 ASSIGN
\   T' THING2 TASK2 ASSIGN

\   TASK1 WAKE
\   TASK2 WAKE


 

 

Link to comment
Share on other sites

Thanks Lee. That means a lot to me coming from you.

 

If you want this kind of option in FB Forth, it's not hard to add.

I created a weird way to make user variables. I extend the workspace after the registers.

But with TI Forth architecture you have a USER Pointer? or no?

 

Anyway I have this RTWP version which I published here earlier and I also started with this alternative version

that is for a more conventional machine that you could use.

 

Happy to answer any questions should you want to do a port of it.

 

I find it a lot of fun to make the little TI look like a big machine.

 

If I ever get my hardware back up and running, I would want to have a terminal task running on RS232

and the graphics stuff/ games working as it's own job. Utopia...

 

 

 

\ TASKS99.HSF for CAMEL99                               06JAN2017 Brian Fox

\ Loosely derived from COOPTASK.MAX for MaxForth        B Fox 1992

[undefined] XASSEMBLER [IF] ."  **This is for XASM99 cross compiler"
                            cr ." Compile halted."  ABORT [THEN]

\ This is a conventional Forth multi-tasker using a single workspace
\ and stacks. It uses CPU R15 as the USER pointer register.

\ It is interesting to note that the Forth virtual machine uses 3 registers
\ for context,two stack pointers and the instruction pointer and the TMS9900
\ also uses 3 registers for context, WP, PC and ST.

\ =======================================================================
\ CAMEL99 MULTI-TASKING USER AREA
\ -----------------------------------------------------------------------
\ R0   LOCAL general purpose register     ( workspace begins)
\ R1   LOCAL general purpose register
\ R2   LOCAL general purpose register
\ R3   LOCAL general purpose register
\ R4   LOCAL Top of stack cache
\ R5   LOCAL overflow for mult. & div.,       // general purpose register (used by NEXT)
\ R6   LOCAL parameter stack pointer ('SP')
\ R7   LOCAL return stack pointer    ('RP')
\ R8   LOCAL Forth working register  ('W')    // general purpose register in code words
\ R9   LOCAL Forth interpreter pointer ('IP)
\ R10  LOCAL Forth's "NEXT" routine cached in R10
\ R11  LOCAL 9900 sub-routine return register // general purpose register in code words
\ R12  LOCAL 9900 CRU register                // general purpose register in code words
\ R13  LOCAL DO/LOOP index
\ R14  LOCAL DO/LOOP limit
\ **NEW**
\ R15  LOCAL User pointer. (UP) pointer to User area base address, right after workspace

\ ------------------------------------------------------------------------
\ there is apace after the registers for 16 user variables

\ Index      Name
\ ------------------
\  0    USER TFLAG    LOCAL task's awake/asleep flag
\  2    USER TLINK    link to the next task in the round-robin queue
\  4    USER RSAVE    storage for my Return stack pointer
\  6    USER RUN      hold that word that runs in this task
\  8    USER VAR4
\  A    USER VAR5
\  C    USER VAR6
\  E    USER VAR7
\ 10    USER VAR8
\ 12    USER VAR9
\ 16    USER VAR10
\ 18    USER VAR11
\ 1A    USER VAR12
\ 1C    USER VAR13
\ 1E    USER VAR14
\ 1F    USER VAR15
\ -----------------------------------------------------------------------
\  20 CELLS  LOCAL Parameter stack base address (grows downwards)
\  20 CELLS  LOCAL Return stack base address    (grows downwards)
\ =======================================================================


CROSS-ASSEMBLING

CODE: >USER  ( n -- addr)        \ given n calculate a 'local' user-variable address using the USER pointer register
             UP TOS ADD,         \ add UP to TOS
             NEXT,
             END-CODE

\ Coventional Forth Pause
CODE: PAUSE  ( -- )                  \ this is the context switcher
              SP RPUSH,              \ 28
              IP RPUSH,              \ 28
              RP  4 (UP) MOV,        \ 22 save my return stack pointer in RSAVE user-var
              BEGIN,
                 2 (UP) UP MOV,      \ 22 load the next task's UP into CPU UP  (context switch)
                 *UP R0 MOV,         \ 18 test the tlag for zero
              NE UNTIL,              \ 10 loop until it's not zero
              4 (UP) RP MOV,         \ 22 restore local Return stack pointer so I can retrieve IP and SP
              IP RPOP,               \ 22 load this task's IP
              SP RPOP,               \ 22  load this task's SP
              NEXT,               \ = 194 * .333 = 64.6uS context switch
              END-CODE

CODE: MYSELF  ( -- PID-addr)      \ because UP is a register we need code to read/WRITE it
            TOS PUSH,
            UP TOS MOV,
            NEXT,
            END-CODE

CODE: UP!   ( addr -- )
            TOS UP MOV,
            TOS POP,
            NEXT,
            END-CODE

[CC] HEX 8300 20 + [TC] CONSTANT: USER0   \ USER0 is the main task's user-area just above workspace, also called a PID

[CC]
    DECIMAL
    16 cells
    16 cells +
    20 CELLS +
    20 CELLS +
    [TC] CONSTANT: USIZE   \ user-vars. + Pstack + Rstack  = 32+40+40 = 112 bytes per task

TARGET-COMPILING
\ name some user variables without create/does>
: TFLAG  ( -- addr)  0 >user ;  \ this is 1 cell after register 15
: TLINK  ( -- addr)  2 >user ;  \ this is 2 cells after register 15
: RSAVE  ( -- addr)  4 >user ;  \ 4 cells after R15
: RUN    ( -- addr)  6 >user ;  \ local variable hold XT of word to run for this task
: CNT    ( -- addr)  8 >user ;  \ local variable for testing

: LOCAL   ( PID uvar -- addr) MYSELF -  + ;   \ usage:  TASK1 TSP LOCAL @
: SLEEP  ( task -- )  0 SWAP TFLAG LOCAL ! ;
: WAKE   ( task -- ) -1 SWAP TFLAG LOCAL ! ;

( *** YOU  M U S T  call INIT-MULTI ONCE before multi-tasking  ***)
: INIT-MULTI ( -- )
             USER0 UP!          \ set my user-pointer register
             MYSELF TLINK !
             TRUE TFLAG !  ;   \ Set my tlink to my own user-area, mark myself awake

\ these words allow us to push values onto a local return stack
: }RP++ ( task -- )  -2 SWAP RSAVE LOCAL +! ;         \ make space on the local Rstack
: }>R  ( n task -- )  DUP }RP++  RSAVE LOCAL @  ! ;   \ push n onto local Rstack )

[CC] HEX
TARGET-COMPILING
: MAKE-USER ( taskaddr -- )
            >R                                   \ copy taskaddr
            R@ USIZE FF FILL                     \ erase user area
            USER0 R@ 20 CMOVE                    \ copy USER0 vars to taskaddr

            R@ 90 +  R@ RSAVE LOCAL !            \ set Rstack base to this user area

            TLINK @                              \ get copy of current users workspace addr
            R@ TLINK !                           \ store taskaddr in curr. user's tlink
            R@ TLINK LOCAL !                     \ now store curr. user into taskaddr's space

            R> SLEEP  ;                          \ put the new task to sleep

: ASSIGN ( XT task -- )                        \ put stack address and XT onto local task's Rstack
           2dup run local !
           dup 58 +  over }>r                    \ calc local SP base, push to rstack (Pstack is empty)
           dup run local over }>r ;              \ push addr of RUN onto local Rstack (goes into IP when task runs)

TARGET-COMPILING

\ Syntax for setting up 2 tasks:
\ ------------------------------

\ INIT-MULTI              ( setup the root task for mult-tasking)

\ F000 CONSTANT TASK1    ( pointers to some unused memory
\ EF00 CONSTANT TASK2

\ TASK1 MAKE-USER         ( initialize the memory to be a user-area)
\ TASK2 MAKE-USER

\ VARIABLE X1
\ VARIABEL X2

\ : THING1  begin   1 X1 +!  pause again  ;  \ code that needs to run in a task
\ : THING2  begin  -1 X2 +!  pause again ;   \ code that needs to run in a task

\   T' THING1 TASK1 ASSIGN
\   T' THING2 TASK2 ASSIGN

\   TASK1 WAKE
\   TASK2 WAKE


 

 

 

The pointer to the base of the User Variable table in the fbForth workspace is UP (R8) in the ASSEMBLER vocabulary and U0 in the FORTH vocabulary. There is currently room for only ten 16-bit User Variables and they are defined with USER .

 

I may, indeed, be interested in porting multitasking to fbForth 2.0 at some point. Right now, I must concentrate on getting the manual published (and my knee rehab, of course).

 

...lee

Link to comment
Share on other sites

 

The pointer to the base of the User Variable table in the fbForth workspace is UP (R8) in the ASSEMBLER vocabulary and U0 in the FORTH vocabulary. There is currently room for only ten 16-bit User Variables and they are defined with USER .

 

I may, indeed, be interested in porting multitasking to fbForth 2.0 at some point. Right now, I must concentrate on getting the manual published (and my knee rehab, of course).

 

...lee

 

Yes DOCs are many times more work than the code.

 

Well in theory the conventional tasker only needs 2 extra USER variables. A flag to say the tasks is awake and a place to store the Rstack pointer.

The RTWP method only really needs the flag, but I stole an extra 1 to hold the XT of the word that will run in that task. Some systems eliminate that variable

by making a task defining word like

 

TASK: MYTASK BEGIN ."Hello world" AGAIN ;

 

And the definition itself holds the XT to run. I was trying to make something that could be more dynamic and change what the task does anytime.

 

Get that knee working well. That is the most important thing for us more "mature" individuals. :-)

 

B

  • Like 2
Link to comment
Share on other sites

While optimizing and debugging my Sprite control code I decided to create something I encountered in an old text book.

It's called Alpha Intelligence. Apparently these days it means something entirely different but that's another matter.

 

For this discussion here is the final definition in Forth.

No multi-tasking used. Just moving a sprite around the screen with less brains than a slime mould.

: RUN      ( -- )
           PAGE ." Alpha Intelligence Demo"
           12 SCREEN
           .BORDER .WALLS
           ALPHA-GUY
           BEGIN
              CLEAR-AHEAD?
              IF   MOVE-FORWARD  \ if true
              ELSE ALPHA-THINK   \ otherwise
              THEN 2 MS          \ then delay a bit
              KEY?
           UNTIL ;

It can be fun to do this in TI BASIC with just characters.

I found that getting my sprite to respect the characters on the screen was more challenging than I thought it would be and it's still got a bug and misses a wall every now and then.

 

The next level is BETA intelligence which asks "Have I been here before?" and if so it reaches into memory and goes the direction that go it free the last time.

If it has not been at the coordinates it reverts to ALPHA intelligence.

 

In case you are interested here is the ALPHA brain code. It loops until it finds a direction that will let it go forward

: ALPHA-THINK   ( -- )
                 BEEP
                 TRYS OFF                   \ reset the trys counter
                 9 ACOLOR                   \ change char color to RED while thinking
                 BEGIN
                    1 TRYS +!               \ count the try
                    NEW-VECTORS 0 MOTION    \ get new rnd vectors into sprite #0 motion
                    CLEAR-AHEAD?
                 UNTIL
                 16 ACOLOR ;                \ restore to color 16

Alpha Intelligence.mov

Edited by TheBF
  • Like 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...