SP1: What's the "graphic" argument in sprite creation?

ZX80, ZX 81, ZX Spectrum, TS2068 and other clones
Post Reply
thricenightly
Member
Posts: 28
Joined: Thu Jun 01, 2017 5:46 pm

SP1: What's the "graphic" argument in sprite creation?

Post by thricenightly »

sp1.h says the signature for sp1_CreateSpr is:

Code: Select all

sp1_CreateSpr(void *drawf,uint16_t type,uint16_t height,int graphic,uint16_t plane)
I'm trying to work out what the 'graphic' value is for.

grepping the z88dk codebase finds 150 examples of calls to sp1_CreateSpr() and all but about 3 of them (all in BlackStar) pass this value as zero. There are no comments relating to it.

I looked at the asm source but couldn't work it out. A comment labels it as "graphic definition for column". It's placed in the structure in a field commented "effective offset to add to graphic pointers, equals result of vertical rotation + frame addr". That might make sense if you know what it means! I changed the value in my test program from 0 to a few other values and they all made a mess on screen. It's clearly used for something.

The sp1_AddColSpr() routine has a similar argument, which might be related? I can't find anything on that either.

I give up! What's it for?
alvin
Well known member
Posts: 1872
Joined: Mon Jul 16, 2007 7:39 pm

Post by alvin »

thricenightly wrote:sp1.h says the signature for sp1_CreateSpr is:

Code: Select all

sp1_CreateSpr(void *drawf,uint16_t type,uint16_t height,int graphic,uint16_t plane)
I'm trying to work out what the 'graphic' value is for.
...
The sp1_AddColSpr() routine has a similar argument, which might be related? I can't find anything on that either.

I give up! What's it for?
In both calls, it's for specifying the absolute address of the graphic for that column. In sp1 you're building the sprite up in columns so the first call to sp1_CreateSpr() creates a sprite of width one column and then successive calls to sp1_AddColSpr() add more columns.

The fact that graphic is an int is suggestive and that's because there's a good way to animate sprites if the parameter is used as an offset from the sprite graphic address instead of an absolute address to the graphics. So you'll usually see 0 in there for the sp1_CreateSpr() call and then 0+column displacement for the sp1_AddColSpr() call.

When used in this way, sp1_MoveSpr*()'s frame parameter supplies the absolute address of the graphic frame you want to use so you can animate the sprite at the same time as you move it with sp1_MoveSpr*(). The graphics pointer used is the frame address from sp1_MoveSpr*() plus the graphic parameter in the sp1_CreateSpr() and sp1_AddColSpr() calls.

The other way to go is to give sp1_CreateSpr() and sp1_AddColSpr() the absolute address of frame0 of the sprite and then give sp1_MoveSpr() an offset from the frame in its frame argument to animate the sprite image. But this is less suitable for state machines (eg) controlling the sprite animation sequence where it's easier to specify a sprite image by address instead of by displacement from frame0. If you only have one spite image then giving sp1_CreateSpr() and sp1_AddColSpr() the absolute address of the graphics image does mean you can use 0 in the sp1_MoveSpr*()'s frame parameter to draw the sprite's single frame.

Code: Select all

   defb 0,0,0,0,0,0,0    ; 7 pixel rows of blank, include mask if using masked sprites

spriteframe0:
column0:                 ; offset = 0 from first sprite image frame

    ; definition of first sprite column here

   defb 0,0,0,0,0,0,0,0   ; 8 pixel rows of blank, include mask if using masked sprites

column1:                 ; offset = column1-column0 = 8*row height of sprite

    ; definition of second sprite column here

    defb 0,0,0,0,0,0,0,0

spriteframe1:

(similar to above)
The above is a two-column wide sprite. sp1_CreateSpr() gets 0 for its graphic param. sp1_AddColSpr() gets column1-column0 for its graphic param because its column starts that many bytes away from the first column. The next column gets a larger graphic param because its distance from column0 is larger.

Then the call to sp1_MoveSpr*() gets the absolute address of the sprite frame to use in its frame parameter. If you want to use spriteframe0, you'd pass that address. If you want to use spriteframe1, you'd pass that instead.
thricenightly
Member
Posts: 28
Joined: Thu Jun 01, 2017 5:46 pm

Post by thricenightly »

Right, it's clicked. There are two ways of using the interface. The signatures in header file suggest one way, and the example code pretty much all does it the other way. And mixing the two approaches doesn't work. :)

So the method for my 8x8 circle sprite, as suggested by the function signatures, is this:

Code: Select all

  circle_sprite = sp1_CreateSpr(SP1_DRAW_LOAD1LB, SP1_TYPE_1BYTE, 2, (int)circle, 0);

  sp1_AddColSpr(circle_sprite, SP1_DRAW_LOAD1RB, SP1_TYPE_1BYTE, (int)circle, 0);

  sp1_MoveSprAbs(circle_sprite, &full_screen, 0, 0, 0, 0, 0);
You give the absolute addresses of the graphic data to the Create and AddCol functions, and 0 to the Move function.

The alternative, which the examples all seem to use, is this:

Code: Select all

  circle_sprite = sp1_CreateSpr(SP1_DRAW_LOAD1LB, SP1_TYPE_1BYTE, 2, 0, 0);

  sp1_AddColSpr(circle_sprite, SP1_DRAW_LOAD1RB, 0, 0, 0);

  sp1_MoveSprAbs(circle_sprite, &full_screen, circle, 0, 0, 0, 0);
You give zeroes as the graphic data to the Create and AddCol functions, then the absolute address of the graphic data in the Move function's frame argument.

Right, got it. Until the next thing that stumps me. :)
alvin
Well known member
Posts: 1872
Joined: Mon Jul 16, 2007 7:39 pm

Post by alvin »

thricenightly wrote:You give zeroes as the graphic data to the Create and AddCol functions, then the absolute address of the graphic data in the Move function's frame argument.
Yes that's right. It's not always zeroes though for the columns. The number is an offset from the start of the frame or an absolute address of the column's graphic. You're only defining a single column sprite image so you're not seeing a non-zero offset value for the sp1_AddColSpr() calls. The RB sprite type is for spillover of the graphic into the column to the right when it is pixel shifted horizontally and does not have any graphic associated with it so it can be given any value for its offset, like zero.
derekfountain
Member
Posts: 50
Joined: Mon Mar 26, 2018 1:49 pm

Post by derekfountain »

I post with trepidation. :) I've a feeling this is going to be an embarrassingly obvious mistake, but I just can't see it.

Here's an 8x8 sort-of-sprite made up of numbers, so I can see the frames:

Code: Select all

SECTION rodata_user

PUBLIC _number

        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000

._number
._number_f1
        defb @10010001
        defb @10010001
        defb @10010001
        defb @10010001
        defb @10010001
        defb @10010001
        defb @10010001
        defb @10111001

        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        
._number_f2
        defb @10110001
        defb @10001001
        defb @10000101
        defb @10000101
        defb @10001001
        defb @10010001
        defb @10100001
        defb @10111101

        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        
._number_f3
        defb @10111001
        defb @10000101
        defb @10000101
        defb @10001001
        defb @10110001
        defb @10001001
        defb @10000101
        defb @10111001

        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        
._number_f4
        defb @10100001
        defb @10100001
        defb @10100001
        defb @10101001
        defb @10101001
        defb @10111101
        defb @10001001
        defb @10001001

        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        
._number_f5
        defb @10111101
        defb @10100001
        defb @10100001
        defb @10100001
        defb @10100001
        defb @10011101
        defb @10000101
        defb @10111101

        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
                
._number_f6
        defb @10011101
        defb @10100001
        defb @10100001
        defb @10100001
        defb @10111001
        defb @10100101
        defb @10100101
        defb @10011001

        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
                
._number_f7
        defb @10111101
        defb @10000101
        defb @10000101
        defb @10000101
        defb @10001001
        defb @10010001
        defb @10100001
        defb @10100001

        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
                
._number_f8
        defb @10011001
        defb @10100101
        defb @10100101
        defb @10011001
        defb @10011001
        defb @10100101
        defb @10100101
        defb @10011001

        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
        defb @00000000
Here's a simple animation loop, which doesn't work:

Code: Select all

/*
 * zcc +zx -vn -m -startup=1 -clib=sdcc_iy number_simple.c number_sprite.asm -o number_simple -create-app
 */

#pragma output REGISTER_SP = 0xD000

#include <arch/zx.h>
#include <z80.h>
#include <arch/zx/sp1.h>
#include <stdio.h>

extern unsigned char number[];

struct sp1_Rect full_screen = {0, 0, 32, 24};

int main()
{
  struct sp1_ss  *number_sprite;
  unsigned char   x;
  unsigned char   animation_offset;

  zx_border(INK_BLACK);

  sp1_Initialize( SP1_IFLAG_MAKE_ROTTBL | SP1_IFLAG_OVERWRITE_TILES | SP1_IFLAG_OVERWRITE_DFILE,
                  INK_BLACK | PAPER_WHITE,
                  'X' );
  sp1_Invalidate(&full_screen);
  sp1_UpdateNow();
 
  number_sprite = sp1_CreateSpr(SP1_DRAW_LOAD1LB, SP1_TYPE_1BYTE, 2, (int)number, 0);

  sp1_AddColSpr(number_sprite, SP1_DRAW_LOAD1RB, SP1_TYPE_1BYTE, (int)number, 0);

  x=0;
  animation_offset=0;
  while(1) {
    printf("\x16\x1\x1 offset=%u  ", animation_offset);
    sp1_MoveSprPix(number_sprite, &full_screen, (void*)animation_offset, x++, 80);
    sp1_UpdateNow();

    /*
     * '1'==0, '2'==16, '3'==32, '4'=48, '5'=64, '6'=80, '7'=96, '8'=112
     */
    animation_offset+=16;
    if( animation_offset == 128 )
    {
      zx_border(INK_RED);
      animation_offset = 0;
    }

    z80_delay_ms(3000);
  }
}
When I run this, the sprite starts at '1' and steps one pixel at a time, left to right, through frames '2' thru '8'. That's what I'd expect. When the offset goes past the '8' frame I reset it back to 0, which should be '1'. But it doesn't, it shows the '8' frame again, then goes to '2'. The printed offset looks right, the border switches red at the offset reset point as expected, but something clearly isn't behaving as I expect it to.

I've a feeling it's going to be obvious, but can someone point it out for me?
alvin
Well known member
Posts: 1872
Joined: Mon Jul 16, 2007 7:39 pm

Post by alvin »

This is something I'd completely forgotten... if the animation offset is 0 it means "do not change the graphic". So this is a problem for doing things this way. A little think is in order to figure out if this behaviour should be changed.

In the meantime, there is another way to write the offset directly which I will post next.
alvin
Well known member
Posts: 1872
Joined: Mon Jul 16, 2007 7:39 pm

Post by alvin »

number_simple.c:

Code: Select all

/*
 * zcc +zx -vn -m -startup=1 -clib=sdcc_iy number_simple.c number_sprite.asm -o number_simple -create-app
 */

#pragma output REGISTER_SP = 0xD000

#include <arch/zx.h>
#include <z80.h>
#include <arch/zx/sp1.h>
#include <stdio.h>

extern unsigned char number[];

struct sp1_Rect full_screen = {0, 0, 32, 24};

int main()
{
  struct sp1_ss  *number_sprite;
  unsigned char   x;
  unsigned char   animation_offset;

  zx_border(INK_BLACK);

  sp1_Initialize( SP1_IFLAG_MAKE_ROTTBL | SP1_IFLAG_OVERWRITE_TILES | SP1_IFLAG_OVERWRITE_DFILE,
                  INK_BLACK | PAPER_WHITE,
                  'X' );
  sp1_Invalidate(&full_screen);
  sp1_UpdateNow();
 
  number_sprite = sp1_CreateSpr(SP1_DRAW_LOAD1LB, SP1_TYPE_1BYTE, 2, (int)number, 0);

  sp1_AddColSpr(number_sprite, SP1_DRAW_LOAD1RB, SP1_TYPE_1BYTE, (int)number, 0);

  x=0;
  animation_offset=0;
  while(1) {
    printf("\x16\x1\x1 offset=%u  ", animation_offset);
 
    number_sprite->frame = (void*)animation_offset;
    sp1_MoveSprPix(number_sprite, &full_screen, 0, x++, 80);
         
    sp1_UpdateNow();

    /*
     * '1'==0, '2'==16, '3'==32, '4'=48, '5'=64, '6'=80, '7'=96, '8'=112
     */
    animation_offset+=16;
    if( animation_offset == 128 )
    {
      zx_border(INK_RED);
      animation_offset = 0;
    }

    z80_delay_ms(500);
  }
}
The only changes concern the "sp1_MoveSprPix" call. Just before the call, the frame offset is explicitly written into the sprite structure and then the move sprite call has 0 in *that* parameter so that it does not alter the frame address.
alvin
Well known member
Posts: 1872
Joined: Mon Jul 16, 2007 7:39 pm

Post by alvin »

In retrospect, -1 (0xffff) would have been a better choice to indicate that the sprite image should not be changed but I think this ship has sailed now since 0 is being used in real software. I think for the next version I will change this to a defined constant and probably move away from 0.

This version adds an offset DELTA to the actual sprite graphic location to avoid the 0.

number_simple.c

Code: Select all

/*
 * zcc +zx -vn -m -startup=1 -clib=sdcc_iy number_simple.c number_sprite.asm -o number_simple -create-app
 */

#pragma output REGISTER_SP = 0xD000

#include <arch/zx.h>
#include <z80.h>
#include <arch/zx/sp1.h>
#include <stdio.h>

#define DELTA 1

extern unsigned char number[];

struct sp1_Rect full_screen = {0, 0, 32, 24};

int main()
{
  struct sp1_ss  *number_sprite;
  unsigned char   x;
  unsigned char   animation_offset;

  zx_border(INK_BLACK);

  sp1_Initialize( SP1_IFLAG_MAKE_ROTTBL | SP1_IFLAG_OVERWRITE_TILES | SP1_IFLAG_OVERWRITE_DFILE,
                  INK_BLACK | PAPER_WHITE,
                  'X' );
  sp1_Invalidate(&full_screen);
  sp1_UpdateNow();
 
  number_sprite = sp1_CreateSpr(SP1_DRAW_LOAD1LB, SP1_TYPE_1BYTE | SP1_TYPE_OCCLUDE, 2, (int)number - DELTA, 0);

  sp1_AddColSpr(number_sprite, SP1_DRAW_LOAD1RB, SP1_TYPE_1BYTE | SP1_TYPE_OCCLUDE, 0, 0);

  x=0;
  animation_offset=0;
  while(1) {
    printf("\x16\x1\x1 offset=%u  ", animation_offset);

    sp1_MoveSprPix(number_sprite, &full_screen, (void*)(animation_offset + DELTA), x++, 80);
         
    sp1_UpdateNow();

    /*
     * '1'==0, '2'==16, '3'==32, '4'=48, '5'=64, '6'=80, '7'=96, '8'=112
     */
    animation_offset+=16;
    if( animation_offset == 128 )
    {
      zx_border(INK_RED);
      animation_offset = 0;
    }

    z80_delay_ms(500);
  }
}
I also made the sprites type "SP1_TYPE_1BYTE | SP1_TYPE_OCCLUDE". Occlude means the library doesn't bother drawing anything underneath the sprite, which is a perfect match for load types. You won't see any speed improvement for this program but it may make a small difference when things are busy. For "sp1_AddColSpr" I also took out the "(int)number" to emphasize that there is no graphic associated with an RB draw function.
derekfountain
Member
Posts: 50
Joined: Mon Mar 26, 2018 1:49 pm

Post by derekfountain »

Hmmm, I'm not sure where to go with this re. the getting started guide. Which approach is best for people cutting their teeth and just wanting to move forward?

The first example, where you do this:

Code: Select all

  number_sprite->frame = (void*)animation_offset;
  sp1_MoveSprPix(number_sprite, &full_screen, 0, x++, 80);
is simple, but changing the sprite structure directly goes behind the (presumably opaque) interface. Do you recommend beginners take that approach?

The second example, where, if I read it correctly, you effectively give the create sprite routine the wrong data address, and then correct it in the movement routine, is going to be a tricky thing to explain in a beginner's guide.

I did pick up on what you said above, and what I'm seeing in the examples, where the creation function is given a NULL graphics address and the absolute address of the appropriate animation frame is given to the move routine. Moving forward I'm going to switch my examples to using that approach, but before I do so I'd like to cover animation with the approach I've used in my first two SP1 articles.

My gut feeling is to use the 'number_sprite->frame' assignment in my first animation example of my next article, then explain how the alternative approach (i.e. absolute address in the move call) is frequently better and how it works, then adopt that approach for all future examples.
alvin
Well known member
Posts: 1872
Joined: Mon Jul 16, 2007 7:39 pm

Post by alvin »

is simple, but changing the sprite structure directly goes behind the (presumably opaque) interface. Do you recommend beginners take that approach?
Yeah I would agree this is probably the easiest to talk about. Mentioning that the 0 in the move* functions means it will not alter the frame address you're setting might be ok there too.

It is normal to access some of the sprite structure members directly:
https://github.com/z88dk/z88dk/blob/mas ... /sp1.h#L56

Code: Select all

struct sp1_ss {                       // "sprite structs" - 20 bytes - Every sprite is described by one of these

   uint8_t              row;            // +0  current y tile-coordinate
   uint8_t              col;            // +1  current x tile-coordinate
   uint8_t              width;          // +2  width of sprite in tiles
   uint8_t              height;         // +3  height of sprite in tiles

   uint8_t              vrot;           // +4  bit 7 = 1 for 2-byte graphical definition else 1-byte, bits 2:0 = current vertical rotation (0..7)
   uint8_t              hrot;           // +5  current horizontal rotation (0..7)

   uint8_t             *frame;          // +6  current sprite frame address added to graphic pointers

   uint8_t              res0;           // +8  "LD A,n" opcode
   uint8_t              e_hrot;         // +9  effective horizontal rotation = MSB of rotation table to use
   uint8_t              res1;           // +10 "LD BC,nn" opcode
   uint16_t             e_offset;       // +11 effective offset to add to graphic pointers, equals result of vertical rotation + frame addr
   uint8_t              res2;           // +13 "EX DE,HL" opcode
   uint8_t              res3;           // +14 "JP (HL)" opcode

   struct sp1_cs     *first;          // +15 BIG ENDIAN ; first struct sp1_cs of this sprite

   uint8_t              xthresh;        // +17 hrot must be at least this number of pixels for last column of sprite to be drawn (1 default)
   uint8_t              ythresh;        // +18 vrot must be at least this number of pixels for last row of sprite to be drawn (1 default)
   uint8_t              nactive;        // +19 number of struct sp1_cs cells on display (written by sp1_MoveSpr*)

};
Collision detection will look at "row" through "hrot" to find out where the sprite is. "frame" can be used directly as we see here. "xthresh" and "ythresh" are expected to be set by the user if the default isn't adequate. I haven't found a real use for "nactive" yet but this is intended as read-only data.

You can see "xthresh" and "ythresh" in action in your example program. If the number sprite is printed at an exact character coordinate (x & 0x3 == 0) then you can see the sprite is only 1 character wide and the (empty) second column is not printed. By default sprites are created with "xthresh=1" which means the sprite should be shifted right by at least one pixel for the last column to print. If you have a bullet sprite that is only 3 pixels wide, you'd not want the last column to draw (for performance reasons) unless the bullet is shifted right at least 6 pixels so you'd manually set "xthresh=6" to accomplish that. The same applies to "ythresh" in the vertical direction. In your example you're drawing to an exact vertical character coordinate (y & 0x3 == 0) so the last row is not drawn. If you move down one pixel the load sprites will be two char rows high.


Some of the members in the sprite char struct are also meant to be directly manipulated:
https://github.com/z88dk/z88dk/blob/mas ... /sp1.h#L83

Code: Select all

struct sp1_cs {                       // "char structs" - 24 bytes - Every sprite is broken into pieces fitting into a tile, each of which is described by one of these

   struct sp1_cs     *next_in_spr;    // +0  BIG ENDIAN ; next sprite char within same sprite in row major order (MSB = 0 if none)

   struct sp1_update *update;         // +2  BIG ENDIAN ; tile this sprite char currently occupies (MSB = 0 if none)

   uint8_t              plane;          // +4  plane sprite occupies, 0 = closest to viewer
   uint8_t              type;           // +5  bit 7 = 1 occluding, bit 6 = 1 last column, bit 5 = 1 last row, bit 4 = 1 clear pixelbuffer
   uint8_t              attr_mask;      // +6  attribute mask logically ANDed with underlying attribute, default = 0xff for transparent
   uint8_t              attr;           // +7  sprite colour, logically ORed to form final colour, default = 0 for transparent

   void                *ss_draw;        // +8  struct sp1_ss + 8 bytes ; points at code embedded in sprite struct sp1_ss

   uint8_t              res0;           // +10 typically "LD HL,nn" opcode
   uint8_t             *def;            // +11 graphic definition pointer
   uint8_t              res1;           // +13 typically "LD IX,nn" opcode
   uint8_t              res2;           // +14
   uint8_t             *l_def;          // +15 graphic definition pointer for sprite character to left of this one
   uint8_t              res3;           // +17 typically "CALL nn" opcode
   void                *draw;           // +18 & draw function for this sprite char

   struct sp1_cs       *next_in_upd;    // +20 BIG ENDIAN ; & sp1_cs.attr_mask of next sprite occupying the same tile (MSB = 0 if none)
   struct sp1_cs       *prev_in_upd;    // +22 BIG ENDIAN ; & sp1_cs.next_in_upd of previous sprite occupying the same tile

};
"attr_mask" determines how the sprite char's colour is mixed with the background and "attr" determines the sprite char's colour. "plane" could also be directly modified under program control but that would be more unusual.
alvin
Well known member
Posts: 1872
Joined: Mon Jul 16, 2007 7:39 pm

Post by alvin »

Also I should remind that changes to the sprite do not become visible unless the sprite is redrawn either by invalidating the characters the sprite occupies (there is a "struct sp1_Rect" structure at the top of "struct sp1_ss" so you can in fact "sp1_Invalidate(sprite)") or by using a move sprite function which will invalidated the sprite itself.
I did pick up on what you said above, and what I'm seeing in the examples, where the creation function is given a NULL graphics address and the absolute address of the appropriate animation frame is given to the move routine. Moving forward I'm going to switch my examples to using that approach, but before I do so I'd like to cover animation with the approach I've used in my first two SP1 articles.
Yes the sprite graphic address pointers to "sp1_CreateSpr" and "sp1_AddColSpr" become offsets from the start of the graphic frame.
Post Reply