Alright, so I'm moving past my z80 paradigm of drawing only single dots and characters on the screen at a time...I want to use sprites! My first big question is, quite simply, how does one draw a sprite? I've read multiple tutorials on how to do this, but none seem to give a straightforward answer. I get that you have to define your sprite in a sort of binary matrix, like below:


Code:
smileyFace:
  .db %00000000
  .db %01100110
  .db %01100110
  .db %00000000
  .db %00000000
  .db %10000001
  .db %01000010
  .db %00111100


Derp, I could do that with a matrix in TI-Basic. But how do you then get that "matrix" into plotScreen so as to be drawn. This page, http://z80-heaven.wikidot.com/sprites, seems almost unfinished, because its section "Placing the Sprite on the Screen" has nothing under it. This page, http://z80-heaven.wikidot.com/sprite-routines, shows how to place a sprite but doesn't tell you where or how you should place a matrix like above into the code (a step which I assume is necessary no matter what). And most other tutorials just say, "use Ion PutSprite to do all your sprite stuff." That may be great down the road, but if I can't just put a stationary smiley face on the screen by myself then I'm probably not going to understand Ion, and I won't be a very good programmer in general:P So, in conclusion, I understand the whole binary representation of a sprite, but I don't understand how to get this sequence of bytes onto the display. Any advice will be appreciated. Kthxbai

Edit by Catherine: Added in [ code ] tags. Next time, put it around any bit of code you're typing and it'll format it nicely.
You should always use Ion's putSprite or DCS's iPutSprite when you're using z80, or use another sprite routine that you have obtained. However, you should understand how it works:

Essentially, plotScreen contains 64 sequential rows of pixel data, each of which consists of 12 bytes, each of which define 8 pixels. The sequence of bytes in each row is left to right: you can see this effect by looping through all the bytes in plotScreen, filling each byte with 0xFF and running FastCopy.

When you draw your sprite, you start by determining what row to start in. That's the easy part (it's just the y coordinate), and any sprite routine will move down rows on the graph screen as it moves through bytes in the sprite.

The hard part is determining what column to start in. If your x-coordinate is a multiple of 8, the routine is easy: as you iterate through the rows, simply overwrite, AND, OR, or XOR the current sprite byte into the appropriate byte on the screen. However, if it's not (and you will want to allow that), the routine is more difficult: you must AND, OR, and/or XOR bitshifted parts of the sprite data into two bytes in each row.

I don't have much raw ASM experience, so I can't provide any code to you. But hopefully that should get you on the right foot.
First, here's the ionPutSprite routine:

Code:
ionPutSprite:
    ld    e,l
    ld    h,$00
    ld    d,h
    add    hl,de
    add    hl,de
    add    hl,hl
    add    hl,hl               ;Find the Y displacement offset
    ld    e,a
    and    $07               ;Find the bit number
    ld    c,a
    srl    e
    srl    e
    srl    e
    add    hl,de             ;Find the X displacement offset
    ld    de,gbuf
    add    hl,de
putSpriteLoop1:
sl1:    ld    d,(ix)             ;loads image byte into D
    ld    e,$00
    ld    a,c
    or    a
    jr    z,putSpriteSkip1
putSpriteLoop2:
    srl    d                  ;rotate to give out smooth moving
    rr    e
    dec    a
    jr    nz,putSpriteLoop2
putSpriteSkip1:
    ld    a,(hl)
    xor    d
    ld    (hl),a
    inc    hl
    ld    a,(hl)
    xor    e
    ld    (hl),a              ;copy to buffer using XOR logic
    ld    de,$0B
    add    hl,de
    inc    ix                   ;Set for next byte of image
    djnz    putSpriteLoop1
    ret

Put this somewhere near the end of your source, outside the main body of your program. This is a separate routine which you will call (using call, though it's also possible to use jp/jr but you have to keep in mind what's on the stack). Make sure your data, here your sprite, is also outside the main body of your program. You just want to make sure that the code will not get run unless you specifically call it.

Now, let's look at the inputs/outputs of the routine:
Quote:
ionPutSprite:
Draw a sprite to the graph buffer (XOR).
Input: b=sprite height
a=x coordinate
l=y coordinate
ix->sprite
Output: Sprite is XORed to the graph buffer.
ix->next sprite
Destroys: af bc de hl ix

So to use it, you will need to load the height of the sprite, in your case, 8, into b, before calling the routine. Next you want to load the x position on the screen into a, and the y position into y. Lastly, the label where the sprite is stored should be put into ix. From the outputs, we can see that all registers will be destroyed (so if you need anything to be saved, be sure to push them before calling the routine or have them stored somewhere in RAM/saferam) and that after calling the routine ix will point to the next byte AFTER smileyFace. So, if we wanted to draw the smileyFace to (X,Y) (13,45), our code might look something like this:

Code:
start:
    ld b,8
    ld a,13
    ld l,45
    ld ix,smileyFace
    call ionPutSprite
    call ionFastCopy    ;copy the graph buffer to the LCD. you could also use TI's slower bcall(_GrBufCpy)
    ret

ionPutSprite:
    ld    e,l
    ld    h,$00
    ld    d,h
    add    hl,de
    add    hl,de
    add    hl,hl
    add    hl,hl               ;Find the Y displacement offset
    ld    e,a
    and    $07               ;Find the bit number
    ld    c,a
    srl    e
    srl    e
    srl    e
    add    hl,de             ;Find the X displacement offset
    ld    de,gbuf
    add    hl,de
putSpriteLoop1:
sl1:    ld    d,(ix)             ;loads image byte into D
    ld    e,$00
    ld    a,c
    or    a
    jr    z,putSpriteSkip1
putSpriteLoop2:
    srl    d                  ;rotate to give out smooth moving
    rr    e
    dec    a
    jr    nz,putSpriteLoop2
putSpriteSkip1:
    ld    a,(hl)
    xor    d
    ld    (hl),a
    inc    hl
    ld    a,(hl)
    xor    e
    ld    (hl),a              ;copy to buffer using XOR logic
    ld    de,$0B
    add    hl,de
    inc    ix                   ;Set for next byte of image
    djnz    putSpriteLoop1
    ret

smileyFace:
  .db %00000000
  .db %01100110
  .db %01100110
  .db %00000000
  .db %00000000
  .db %10000001
  .db %01000010
  .db %00111100

It's common practice to put data (sprites, text, etc.) at the end of your program for several reasons. It makes your code look cleaner and the calculators (thank you TI!) are set so that any code that tries to execute at $C000 and beyond will crash the calc. That gives you approximately 8.8k of executable code per program.

If you want, we can go through how the sprite routine works. It's good practice with the shifting instructions.
Thanks chickendude! I will probably want to analyze the routine a little further, but first I have some logistical questions about the above program:
1) You suggested calling ionFastCopy over bcalling _GrBufCpy. Is there a similar ion routine for clearing the graph buffer, that is perhaps faster than _GrBufClr? Also, is it advised to always clear the graph buffer before using ionPutSprite?
2) The Doors SDK that I downloaded came with several include files like ti83plus.inc, one of which was ion.inc. I was thinking that I might be able to leave out the ionPutSprite code that you gave above if I instead #included ion.inc at the beginning of my source. However, ion.inc appears to contain no actual code, just a bunch of defines and equates. Does this mean that I will need to manually copy the above code into every program where I wish to use ionPutSprite? Also, does the program above need ion.inc at all?
3) Again concerning ionFastCopy, I noticed that your code above doesn't actually have any ionFastCopy label, and there is no code for it in ion.inc either. When I try to run the code on an emulator, the output is just a blank screen from which I cannot escape. Did you just forget the ionFastCopy code or is there a step that I'm missing?
4) Why does the ix register shift to the byte after your sprite label, and what would be in that byte? Is that the first byte containing actual sprite data, or does the ix shift just enable a programmer to quickly output the next sprite in the file, perhaps for animations?
5) Lastly, sort of unrelated, I keep hearing people talk about "safeRAM." Does this term refer to specific areas of z80 RAM or is it just a general term for storing variables "safely" in RAM rather than in the stack or a register?
1. To clear the graph buffer, you can use the ldir command:

Code:
 ld hl,gbuf
 ld de,gbuf+1
 ld (hl),0
 ld bc,767
 ldir
This will load 0 into the first byte of the graph buffer (0 = all pixels turned off) then copy the byte at gbuf into the byte at gbuf+1, so the next byte will also be erased. hl and de will be incremented by one, and bc will be decremented by one. So hl now equals gbuf+1, de = gbuf+2, and bc = 766. So the byte at (hl) will get copied into (de), which will clear the next byte. And that repeats 767 times (the screen is 768 pixels large, but the first byte is cleared manually with ld (hl),0 so we only need to copy 767 times.

However, if you redraw the entire screen every time (say you have a tilemapper) then you don't need to clear the graph buffer. Whether or not to clear the graph buffer really depends on how you draw your sprites and whether what's in the buffer will mess with what you want to draw to the buffer. If you XOR or OR your sprites onto the buffer, when you draw the sprite again the previous data might distort your sprite. An interesting thing to note, though, is if you XOR a sprite, XORing it in the same position will erase it without erasing the rest of the buffer Smile ionPutSprite XORs the sprite onto the screen.

2) The include files don't contain any actual code, they just contain equates for memory locations, BCALLs, etc. The ion.inc file contains the equates for ion's special routines. If you use an ion-compatible shell (pretty much any shell you use will support ion, it's kinda the standard), you can make use of the ion libraries without needing to include them in your code. That saves a bit of space in your program. ionFastCopy is one of the routines available to you:

Code:
ionFastCopy:
 di
 ld a,$80
 out ($10),a
 ld hl,gbuf-12-(-(12*64)+1)
 ld a,$20
 ld c,a
 inc hl
 dec hl
fastCopyAgain:
 ld b,64
 inc c
 ld de,-(12*64)+1
 out ($10),a
 add hl,de
 ld de,10
fastCopyLoop:
 add hl,de
 inc hl
 inc hl
 inc de
 ld a,(hl)
 out ($11),a
 dec de
 djnz fastCopyLoop
 ld a,c
 cp $2B+1
 jr nz,fastCopyAgain
 ret
However, i believe you'll need to add more delay for calculators that have bad LCDs. It should work on an emulator, though, i suppose since there's really no way to mimic every LCD.

If you use any of the ion routines (without copying the code into your program) you will need to use a shell. Most people use a shell anyway so it's not a big deal. The program i posted would need ion.inc because i forgot to include the code to ionFastCopy in the program code (sorry!). I usually include ion.inc, not because i use any ion routines, but because i like the equates. It lists saferam areas for you to use, defines the gbuf ($9340: defined as plotSScreen in ti83plus.inc), etc. So if you don't want to include ion.inc, you'll need to define the gbuf (gbuf = $9340 will work) and include ionFastCopy and ionPutSprite in your code. Otherwise, you can leave ionFastCopy and ionPutSprite out of your code (just use the calls) but the program will need the ion header and will need to be run from an ion-compatible shell: ion, MirageOS, CrunchyOS, DoorsCS, etc.

3) I forgot to include ionFastCopy, sorry Razz You can also try with the _GrBufCpy BCALL just to test it out. The ion zip file at ticalc.org contains the source code to the shell as well as the source to all the ion routines available to programmers, if you want to check it out.

4) What i mean by that is that ionPutSprite will draw the first byte in ix, increment ix, check if there are more bytes to draw, then draw the rest. In the end, you will draw 8 bytes and ix will = smileyFace+8 (smileyFace+7 is the last byte in the sprite). So, for example, if you had two consecutive sprites you wanted to draw:

Code:
block1:
.db %11111111
.db %11111111
.db %11111111
.db %11111111
.db %11111111
.db %11111111
.db %11111111
.db %11111111
block2:
.db %11111111
.db %10000001
.db %10000001
.db %10000001
.db %10000001
.db %10000001
.db %10000001
.db %11111111
You could draw block1 and ix would then be pointing to block2. Set the coordinates for block2 then call ionPutSprite without needing to reload block2 into ix. I don't think this will be useful very often, but i thought it was worth mentioning. The TI string routines are mostly like this, too, i believe. It makes menus easy to create, since you don't need to reload the string into HL each time.

5) SafeRAM locations are places in the TI's RAM that the calculator uses as scratch space. The gbuf is an example of saferam, since changing those values won't ruin the functioning of the calculator. They are places you can safely change the values of during the execution of your program. However, some of TI's routines use some of these places, like the OP registers or maybe textShadow. Most places will be fine even using TI's BCALLs. Ion defines these saferam locations:
saferam1 =$86EC ;saveSScreen=768
saferam2 =$8A3A ;statVars=531
saferam3 =$8508 ;textShadow=128
saferam4 =$8478 ;OPs=66
saferam5 =$84D3 ;iMathPtrs=10
saferamp =$9872 ;appBackUpScreen=768
saferamp2 =$8251 ;bootTemp=52
I'd say saferam1 (saveSScreen) and saferamp (appBackUpScreen) are 100% safe, saferamp might give you trouble if the calc APDs (auto-power downs) during program execution, but that will generally never happen. Quite a few BCALLs use the OP space, but you will generally know since you'll be loading values into the OPs anyway. textShadow i believe is just a copy of the homescreen. If you've ever seen weird characters on the homescreen after playing a game, it's because the game used textShadow to store data. I'm not sure what routines modify it, it might just be _PutS/_PutC (and i suppose _DispDone), but i'm not too sure.

So saferam locations are places you can safely store info (in RAM) during the execution of your program. However, you can't expect that data to remain unchanged in between executions of your program. If you want to save data in between runs, either use SMC in your program (most modern shells handle program writeback for you) or create a special variable, an AppVar maybe or a program, to store your data. These days AppVars seem to be a pretty popular choice.
chickendude wrote:
2) The include files don't contain any actual code, they just contain equates for memory locations, BCALLs, etc. The ion.inc file contains the equates for ion's special routines. If you use an ion-compatible shell (pretty much any shell you use will support ion, it's kinda the standard), you can make use of the ion libraries without needing to include them in your code. That saves a bit of space in your program. ionFastCopy is one of the routines available to you

[[code snipped]]

However, i believe you'll need to add more delay for calculators that have bad LCDs. It should work on an emulator, though, i suppose since there's really no way to mimic every LCD.
I of course recommend Doors CS as your shell of choice to pull in iFastCopy, iPutSprite, and friends without taking up space in your program. Other than the fact that I wrote the shell, I recommend my versions of the Ion routines in particular because:
(1) I provide much cleaner interrupt handling around my versions; I properly detect if interrupts are enabled (even with the z80's silly ei-on-check glitch) and then re-enable them if necessary.
(2) My iFastCopy works around all of the possible displays. It is tuned to work on 6MHz and 15MHz calculators, it works on the LCDs with the "bad" display driver, and it properly removes the delay if a TI-Presenter is connected or if the current calculator is an Nspire emulating a TI-84+ (which it can detect).

Of course, you also get all the other Ion routines, the MirageOS routines, and the Doors CS routines if you make your programs DCS programs. Smile
DoorsCS is also the only shell i know of still maintained, though i couldn't tell you how many people use it compared to MirageOS (it's been years since i've been in school Razz). I always go for ion compatibility even if i don't use any of the routines just because pretty much anyone will be able to run the program and people (who don't use zStart/DoorsCS) generally don't care to run a program with the Asm( token.

Quote:
(2) My iFastCopy works around all of the possible displays. It is tuned to work on 6MHz and 15MHz calculators, it works on the LCDs with the "bad" display driver, and it properly removes the delay if a TI-Presenter is connected or if the current calculator is an Nspire emulating a TI-84+ (which it can detect).
Are you then using the in f,(c) instruction? If so, wouldn't that break nSpire compatibility? I don't know what compatibility's like to begin with, so if it does it might not be that huge of a deal Wink
No, I wrote out the use of the in f,(c) instruction. For fun, here's what the prolog to my iFastCopy routine looks like, and how it detects an Nspire:
Code:
iFastCopy:
   call iCheckInts0
   push af
      di
      ld c,$10

      xor a
      ld r,a
      ld a,r
      cp 2                  ;Nspire check
      jr nz,iFastCopyNspire
      bit 3,(iy+41h)            ;PresentationLink
      jr nz,iFastCopyNspire
On the Nspire, the 'r' register advances 1 per instruction, while on normal z80s, it advances 1 per clock cycle.
KermMartian wrote:
On the Nspire, the 'r' register advances 1 per instruction, while on normal z80s, it advances 1 per clock cycle.

That's interesting and potentially quite useful. Weird that they would only bother to implement R half-way. How did anyone ever discover that? Smile

On a real Z80, though, as far as I know, R is incremented not for every clock cycle, but for every M1 machine cycle (i.e., more or less once for every opcode byte.) So instructions with single-byte opcodes (such as "xor a" and "cp 2") increment R once; those with two-byte opcodes (such as "ld a,r") increment R twice. Note that only the opcode bytes count, not operands. DD+CB/FD+CB instructions (such as "bit 3,(iy+41h)") are weird in that only the first two bytes are considered opcode bytes.
Has Doors CS ever run on a calculator with a TI-Presenter attached?
That's fascinating, thanks for sharing. Always nice to get the holes (and inaccuracies) in my knowledge worked out. It sounds like I need to take another pass over the Z80 manual. Elfprince: I believe that BrandonW has.
Yeah, the Z80 manual is quite a good reference. There's also Sean Young's "The Undocumented Z80 Documented" - well worth reading if you haven't seen it. I think that's where I read about all the weird details of the R register.
Alright let me see if I get this straight... Dvery ASM program that I make I first test on my WabbitEmu emulator, which seems to emulate only those parts of a TI-83 plus that would come WITH the calculator; So no pre-installed programs, no apps, and most importantly, no assembly shells. The TI-84 plus calculator that I use, however, does have Mirage OS installed (though I'm definitely considering downloading Doors CS, thanks Kerm). So this is what should happen when I make a program utilizing Ion routines:

1) If I want it to run on my emulator, I MUST include the code for ionPutSprite and ionFastCopy in my source, along with any other ion-based equates I might need, because the emulator does not include a shell that would have these routines pre-implemented.
2) If I want it to run on my actual calculator, the following should happen
a) If I run it through MirageOS or Doors CS, I need not manually type the routines in my source code because the shell will already have them for me
b) If I run it with an Asm( token, I must still manually type the routines because no shell is supplying the code

Is all of that correct? My only remaining question then is, when and why do you type "#include ion.inc" at the start of a program?
You could always send a shell that has those routines to the emulator.
1) Like willrandship said, you can simply send the shell to the emulator. WabbitEmu probably has a "save state" sort of feature that will load a savestate whenever you run it, that way you don't have to send the shell every time you want to test.
2a) Yep.
2b) Yep.

Include ion.inc in your program when a) you want to use the ion routines (in which case you'll also need to add the ion header) or b) you want to use the equates (i like to use the saferam equates). If you don't need either of those things you can just leave it out. If you get an error while compiling saying that something is undefined, then you might need to include ion.inc (or one of the other shell include files). The include files contain no actual code, including them won't make your program any larger or smaller, they simply provide equates so that you don't have to remember RAM locations, key codes, etc.
chickendude wrote:
1) Like willrandship said, you can simply send the shell to the emulator. WabbitEmu probably has a "save state" sort of feature that will load a savestate whenever you run it, that way you don't have to send the shell every time you want to test.

Yes, that is true. Shundra, WabbitEmu is just like a real calculator, so you can send anything you want to it, including a shell like MOS or DCS. If you couldn't do that, you wouldn't be able to send your program either - WabbitEmu would be useless. However, unless you use the "save state" function that chickendude mentioned, anything you install to the calculator won't be there when you quit out.
BTW, the reason you're seeing MirageOS on your TI 84 Plus and not on your TI 83 Plus is because anything in your archive is included in a ROM dump, including any apps you have installed - and when you dumped your ROMs, your 83 Plus was clean while your 84 Plus had MOS on it.
Thanks all, I totally wasn't even aware of WabbitEmu's save state function; this will shepherd in a whole new era of debugging, muahaha! Ahem... Well now that I know about that, I'd kinda like to make this emulator an exact copy of my actual calculator if that's possible. While my actual calculator is a TI-84 plus and the emulator runs a TI-83 plus, I feel like I should still be able to swap OS, installed apps/programs, and pretty much everything else between them. I have TI Connect installed, so is there a quick way for me to do that in like one step?
Also, two final things that are throwing me off about ion:
1) What is the difference between the ion header file and the ion include file?
2) Is there some way for me to easily "include" all the ion code into my code, either with #include or by some other means? What I mean is, the ion.inc file doesn't include any actual code as ChickenDude said, but is there a way for me to reference that code without manually typing it? Is that perhaps what the header file is for?
1) The default ion header goes something like this:

Code:
#include    "ti83plus.inc"
#include    "ion.inc"
#ifdef TI83P
    .org    $9d93-2
    .db    $BB,$6D
#else
    .org    9327h     ;TI-83 compatibility
#endif
    ret                 ;if you don't use libraries, you can put xor a here
    jr    nc,main   ;that way you can still run the program from the homescreen
title:    .db    "Description",0
main:
    ;code starts here
The header is what defines the program as an ion program, in particular the ret/xor a. If your program doesn't have that "header", ion won't detect the file as ion-compatible. Note: if you use the header file above you'll need to put a #define TI83P at the top (or use the ion batch file) to compile for the 83/4+.

2) If you want to include all the ion routines, the source is included with ion. The file ionf.z80 contains all the routines, you just need to include that file. None of the routine names are prefixed with "ion", however, so you'd write "call fastCopy" instead of "call ionFastCopy". Also note that doing this will make the size of your program much larger, and unless you're going to use every single routine in there i'd recommend just copying/pasting the routines you want into your code.

As for making a copy of your calc, i think you can send files from your calc to your computer with TI-Connect (if not, TiLP can). Then just send those files to WabbitEmu. Not one step, but stlil pretty simple.
shundra9 wrote:
Thanks all, I totally wasn't even aware of WabbitEmu's save state function; this will shepherd in a whole new era of debugging, muahaha! Ahem... Well now that I know about that, I'd kinda like to make this emulator an exact copy of my actual calculator if that's possible. While my actual calculator is a TI-84 plus and the emulator runs a TI-83 plus, I feel like I should still be able to swap OS, installed apps/programs, and pretty much everything else between them. I have TI Connect installed, so is there a quick way for me to do that in like one step?
You can dump the ROM off of your actual calculator and use it with any emulator, WabbitEmu and jsTIfied included. Smile I recommend ROM8x.
Quote:
Also, two final things that are throwing me off about ion:
1) What is the difference between the ion header file and the ion include file?
Those are two interchangeable terms for the same thing, the file that defines pointers in memory into your favorite shell's copies of the Ion standard library.
Quote:
2) Is there some way for me to easily "include" all the ion code into my code, either with #include or by some other means?
Yes, as Chickendude said, but why? That sort of defeats the purpose of having shells that hold these common routines for you.
Fair enough, duly noted. Now my ultimate goal here was to create a program that could move a sprite around the screen. Now that I can successfully display a sprite, I figured the next step would be loading a variable that holds the sprite's coordinates into the input registers of ionPutSprite. This is what I attempted with the code below:


Code:

#include    "ti83plus.inc"
.org    $9D93
.db    $BB, $6D

main:       
   bcall(_clrLCDfull)
   bcall(_grbufclr)
   res indicRun, (IY + indicFlags)
   set fullScrnDraw, (IY + apiFlg4)

   ld b,8
   ld a,(X_COORD)
   ld l,(Y_COORD)
   ld ix,smileyFace
   call ionPutSprite
   call ionFastCopy    ;copy the graph buffer to the LCD
   ret

X_COORD:
   .db 17
Y_COORD:
   .db 45

smileyFace:
  .db %00000000
  .db %01100110
  .db %01100110
  .db %00000000
  .db %00000000
  .db %10000001
  .db %01000010
  .db %00111100

.end


Unfortunately, no matter what I initialize Y_COORD with, the smileyFace is always being displayed on the same row, about halfway down the screen. X_COORD seems to be working fine, and I can put literals in the ld a and ld l statements right before the ionPutSprite call to move the smileyFace where I want it, but as written above, I cannot seem to adjust the vertical displacement of the sprite as I wish. What do?
  
Register to Join the Conversation
Have your own thoughts to add to this or any other topic? Want to ask a question, offer a suggestion, share your own programs and projects, upload a file to the file archives, get help with calculator and computer programming, or simply chat with like-minded coders and tech and calculator enthusiasts via the site-wide AJAX SAX widget? Registration for a free Cemetech account only takes a minute.

» Go to Registration page
Page 1 of 2
» All times are UTC - 5 Hours
 
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 vote in polls in this forum

 

Advertisement