z88dk / Spec Next sprites

If you like transforming your statements into code, this is the place for you

Moderator: Programming Moderators

Post Reply
stevebrowne
Posts: 4
Joined: Tue May 30, 2017 7:26 pm

z88dk / Spec Next sprites

Post by stevebrowne » Thu Jun 22, 2017 9:56 pm

I've decided to start brushing the dust off my C memories, and have been playing around with getting the Spec Next Sprites working using z88dk and knocked together a little test just to get used to it all. If anyone can help me optimise it more I'd appreciate it! These things will be much easier once there are some easy to use wrappers.

https://github.com/stevebrowne/spectrum ... ritetest.c

stevebrowne
Posts: 4
Joined: Tue May 30, 2017 7:26 pm

Re: z88dk / Spec Next sprites

Post by stevebrowne » Thu Jun 22, 2017 9:59 pm

I should add, this builds in zcc v 20170112 and runs in "ZEsarUX_win-5.0-no-pthreads" version

Alcoholics Anonymous
Posts: 513
Joined: Mon May 29, 2017 7:00 pm

Re: z88dk / Spec Next sprites

Post by Alcoholics Anonymous » Fri Jun 23, 2017 6:32 pm

Looking good - I got it to compile and run with the current z88dk just fine (I think by the date on your build that's 1.99B from sourceforge?)

One trick you're missing is that the compilers can assign i/o ports to "special function registers". In this way, you can write to them as if they were char variables and the compilers will inline i/o instructions. This avoids the call/ret overhead of the method you're using in your code. (Note: sccz80 got this feature after 1.99B so an sccz80 compile may not work with your build).

The other thing I did was create a struct to represent your sprites so the code should be cleaner now.

I'll list it here with a surprise after:

Code: Select all

/*
 * Sprite test
 *  Steve Browne, steveb@quickweb.co.uk
 *
 *  Compile with: 
 *      zcc +zx -vn -startup=5 -clib=new spritetest.c -o spritetest -create-app
 *      zcc +zx -vn -startup=5 -clib=sdcc_iy -SO3 --max-allocs-per-node200000 spritetest.c -o spritetest -create-app
 */

#pragma printf = "%c"   // enable %c only for printf

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

// Define i/o ports

__sfr __at 0x55 IO_55h;
__sfr __at 0x57 IO_57h;

__sfr __banked __at 0x243b IO_243Bh;
__sfr __banked __at 0x253b IO_253Bh;

__sfr __banked __at 0x303b IO_303Bh;

// Printf control codes

#define cls()              putchar(12)
#define printAt(row,col)   printf("\x16%c%c", (col+1), (row+1))

// Sprite information

#define NumberOfSprites  20

#define XboundLow        0
#define XboundHi         319
#define YboundLow        0
#define YboundHi         255

struct sprite_data
{
   unsigned int XCoord;
   unsigned int YCoord;
   int XDir;
   int YDir;
};

struct sprite_data sprites[NumberOfSprites];

// Function prototypes

void showSprites(void);
void hideSprites(void);
void loadPatternData(unsigned char patternId);
void setSpriteStartPositions(unsigned char spriteId, unsigned int x, unsigned int y);
int  getRandomDir(void);


void main(void)
{
    unsigned char l;
    unsigned int  i;

    cls();
    zx_border(INK_RED);
    printf("Z88DK Sprite test\n\n");

    // Load the pattern data into pattern 0
    
    loadPatternData(0);

    // Set each sprite to start at 100,100
    
    for (l=0; l < NumberOfSprites; l++)
        setSpriteStartPositions(l,100,100);

    // Show the sprites
    
    showSprites();

    // Main loop, loop long time
    
    for(i=0; i < 1000; i++) 
    {
        for(l=0; l < NumberOfSprites; l++)
        {
            // Check we're not going off the left or right, and if we do, then bounce
            
            if((int)sprites[l].XCoord + 16 + sprites[l].XDir > XboundHi || (int)sprites[l].XCoord + sprites[l].XDir < XboundLow)
                sprites[l].XDir = -sprites[l].XDir;
            
            // Check we're not going off the top or bottom, and if we do, then bounce
            
            if((int)sprites[l].YCoord + 16 + sprites[l].YDir > YboundHi || (int)sprites[l].YCoord + sprites[l].YDir < YboundLow)
                sprites[l].YDir = -sprites[l].YDir;

            // Update the position in the direction of travel
            
            sprites[l].XCoord += sprites[l].XDir;
            sprites[l].YCoord += sprites[l].YDir;

            // Select sprite
            
            IO_303Bh = l;

            // OUT the 4 bytes related to a sprite
            
            IO_57h = sprites[l].XCoord;
            IO_57h = sprites[l].YCoord;
            IO_57h = (sprites[l].XCoord >> 8) != 0;   // If we have gone over 255 in x then we need to set the overflow flag
            IO_57h = 128;
        }
    }
}

// Show sprites

void showSprites(void)
{
   IO_243Bh = 0x15;
   IO_253Bh = 0x03;   // Change to 0x01 to stay in borders
}

// Hide sprites

void hideSprites(void)
{
   IO_243Bh = 0x15;
   IO_253Bh = 0x00;
}

// ------------ Setup stuff, patterns, start positions etc. --------------

    static unsigned char spriteData[] = {
        0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0x04, 0xFF, 0xFB, 0xFB, 0xFB, 0xFF, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0x04, 0xFF, 0xFB, 0xF5, 0xF5, 0xFB, 0xFF, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0x04, 0xFF, 0xFB, 0xF5, 0xA8, 0xA8, 0xFB, 0xFF, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0x04, 0xFF, 0xFF, 0xFB, 0xA8, 0x44, 0xA8, 0xFB, 0xFF, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0x04, 0x04, 0x04, 0xFF, 0xFB, 0xA8, 0x44, 0xA8, 0xFB, 0xFF, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0xE3, 0xE3, 0xE3, 0x04, 0xFF, 0xFB, 0xA8, 0x44, 0x44, 0xFB, 0xFF, 0x04, 0xE3, 0x04, 0xE3, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0xFF, 0xFB, 0x44, 0x44, 0x44, 0xFB, 0xFF, 0x04, 0x4D, 0x04, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0xFF, 0xFB, 0x44, 0x44, 0x44, 0x44, 0xFA, 0x4D, 0x04, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0xFF, 0xFB, 0x44, 0xFF, 0xF5, 0x44, 0x04, 0xE3, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0xFF, 0x44, 0xF5, 0xA8, 0x04, 0xE3, 0xE3, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0xFA, 0x44, 0x04, 0xA8, 0x04, 0xE3, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0x4D, 0x4D, 0x04, 0xE3, 0x04, 0xF5, 0x04, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0x04, 0xE3, 0xE3, 0xE3, 0x04, 0xFA, 0x04,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0x04,
        0x0
    };
	 
void loadPatternData(unsigned char patternId)
{   
    unsigned int i;
	 
    // Set the pattern ID where this pattern will be loaded
    
    IO_303Bh = patternId;
    
    //Loop through and load the pattern data
    
    // z80_otir(spriteData, 0x55, 0);  // special value len = 0 means 256 in otir instruction

    for (i=0; i < 256; i++)
        IO_55h = spriteData[i];
}

void setSpriteStartPositions(unsigned char spriteId, unsigned int x, unsigned int y)
{
    sprites[spriteId].XCoord = x;
    sprites[spriteId].YCoord = y;
    sprites[spriteId].XDir   = getRandomDir();
    sprites[spriteId].YDir   = getRandomDir();

    IO_303Bh = spriteId;
	 
    IO_57h = x;
    IO_57h = y;
    IO_57h = (x >> 8) != 0;
    IO_57h = 128;
}

// Pick a random direction/speed.
// Change speedVariants to set how many different speeds there are

#define speedVariants  10

int getRandomDir(void)
{
    unsigned int tmp;
    
    tmp = rand() % speedVariants;
    
    if(tmp <= (speedVariants/2)-1)
        return tmp-(speedVariants/2);  // Skip 0 speed
    
    return tmp-(speedVariants/2)-1;
}
Surprise: it doesn't work! When I get some time later today I will try to figure out why or someone here can tell me :P

A couple of things:

I added prototypes for your functions at the top of the source file. C is compiled top to bottom and if the compiler hasn't seen a function your code is calling, it will assume a prototype for it (you must have been getting warnings about this). This is a remnant of the original K&R C and modern code always prototypes (usually via header files) or defines the functions ahead of their use (ie move your subroutines above main). Sometimes the assumptions made by the compiler will be correct and sometimes not so it's best not to leave things to luck.

Try to make variables unsigned and as small as possible (eg unsigned char). Obviously you can't always make things unsigned but if you can, code is improved because the z80 is not too good at dealing with signed quantities. zsdcc is especially good at dealing with unsigned char.

One thing about zsdcc, which I also test complied this program with, is that it is terrible at initializing anything inside functions. For this reason, I moved that static array "spriteData" outside the function to global scope even though it would be better where you put it. zsdcc created a bunch of instructions to initialize that array that was inserted before main() was called and that added several hundred bytes to the binary size. sccz80 is much more sensible, leaving it to be done by the linker (ie memory starts with the array data set) and this is also what happens when that array is in global scope. I've complained before at sdcc about this and maybe it's time to remind them about this problem.
Last edited by Alcoholics Anonymous on Sat Jun 24, 2017 1:52 am, edited 1 time in total.

User avatar
ckirby101
Posts: 73
Joined: Mon May 29, 2017 7:09 pm

Re: z88dk / Spec Next sprites

Post by ckirby101 » Fri Jun 23, 2017 9:33 pm

The way i'm handling it is to make a struct for all the sprite data.. then use __FASTCALL__ and send the pointer to array of structs to a function that's 100% asm.. it then updates all the sprites & does the animation update.

this is the update function i'm using, its not optimised at all yet!

Code: Select all


void __FASTCALL__ UpdateSprites(spriteData* data)
{
#asm
	ld d,64
	push iy
	push ix          
	push hl       ;push passed in value
	pop ix         ;ix points at sprite struct


	//hl sprite data
UpdateSpritesLoop:
	push de

	//is sprite used?
	ld a,(ix+6)
	cp 0
	jr z,UpdateSpritesNotUsed


	ld a,(ix+5)                   ;iy points to animation data
	ld iyh,a
	ld a,(ix+4)
	ld iyl,a

    ld bc,Sprite_Index_Port       	;Set the sprite index
    dec d                         ; do a dec becuase we start at 64 i can remove this later!
    out (c),d                     ; (0 to 63)

    ld bc,Sprite_Sprite_Port
    ld a,(ix+0)						;get x low byte
    out (c),a                     ; X (7..0)

    ld a,(ix+2)						;get y 
    out (c),a                     ; T

    ld a,(ix+1)						;X high bit
    and %00000001
    or (iy+1)						;or the flip bits
	out (c),a






	ld a,(ix+3)					;frameindex
	inc a

	ld l,(iy+3)				;animation num anim frames
	cp l
	jr nz,ok

	ld a,0					;wrap frameindex


ok:
	ld (ix+3),a 				;store frameindex back


	ld h,(iy+2)					;get pattern


	add 6						;jump to anim table
	ld d,0
	ld e,a
	add iy,de 					;iy now points to animindex



	ld a,h 						;pattern now in a
	add (iy+0)					;;add index



	and %00111111					;0-63
	or %10000000					;visable flag
	out (c),a 

UpdateSpritesNotUsed:

	ld bc,SpriteDataSize                         ;size of the struct
	add ix,bc

	pop de
	dec d
	jr nz,UpdateSpritesLoop


	pop ix
	pop iy

#endasm
}

Alcoholics Anonymous
Posts: 513
Joined: Mon May 29, 2017 7:00 pm

Re: z88dk / Spec Next sprites

Post by Alcoholics Anonymous » Sun Jun 25, 2017 1:00 am

Ok so I've found the problem - there are possibly problems in the emulators. I've run this program on both ZEsarUX and Zeus and they both fail in the same way -- they treat the 8-bit ports 0x55 and 0x57 as 16-bit only and will not accept writes to those ports unless they are written to as 0x0055 and 0x0057. I do not believe the Next completely decodes 16 bits for those port addresses so I think I'll ask the Next people and then get the emulators fixed up.

Updated source code with compile lines for the current version of z88dk:

Code: Select all

/*
 * Sprite test
 *  Steve Browne, steveb@quickweb.co.uk
 *
 *  Compile with: 
 *      zcc +zx -vn -startup=5 -clib=new spritetest.c -o spritetest -create-app
 *      zcc +zx -vn -startup=5 -clib=sdcc_iy -SO3 --max-allocs-per-node200000 spritetest.c -o spritetest -create-app
 */

#pragma printf = "%c %u"  // only enable these converters

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

// Define i/o ports

__sfr __banked __at 0x55   IO_55h;
__sfr __banked __at 0x57   IO_57h;

__sfr __banked __at 0x243b IO_243Bh;
__sfr __banked __at 0x253b IO_253Bh;

__sfr __banked __at 0x303b IO_303Bh;

// Printf control codes

#define cls()              putchar(12)
#define printAt(row,col)   printf("\x16%c%c", (col+1), (row+1))

// Sprite information

#define NumberOfSprites  20

#define XboundLow        0
#define XboundHi         319
#define YboundLow        0
#define YboundHi         255

struct sprite_data
{
   unsigned int XCoord;
   unsigned int YCoord;
   int XDir;
   int YDir;
};

struct sprite_data sprites[NumberOfSprites];

// Function prototypes

void showSprites(void);
void hideSprites(void);
void loadPatternData(unsigned char patternId);
void setSpriteStartPositions(unsigned char spriteId, unsigned int x, unsigned int y);
int  getRandomDir(void);


void main(void)
{
    unsigned char l;
    unsigned int  i;

    cls();
    zx_border(INK_RED);  // z88dk keeps track of border colour so that audio function use the same colour
    printf("Z88DK Sprite test\n\n");

    // Load the pattern data into pattern 0

    loadPatternData(0);

    // Set each sprite to start at 100,100
    
    for (l=0; l < NumberOfSprites; l++)
        setSpriteStartPositions(l,100,100);

    // Show the sprites
    
    showSprites();

    // Main loop, loop long time
    
    for(i=0; i < 1000; i++) 
    {
        for(l=0; l < NumberOfSprites; l++)
        {
            // Check we're not going off the left or right, and if we do, then bounce
            
            if((int)sprites[l].XCoord + 16 + sprites[l].XDir > XboundHi || (int)sprites[l].XCoord + sprites[l].XDir < XboundLow)
                sprites[l].XDir = -sprites[l].XDir;
            
            // Check we're not going off the top or bottom, and if we do, then bounce
            
            if((int)sprites[l].YCoord + 16 + sprites[l].YDir > YboundHi || (int)sprites[l].YCoord + sprites[l].YDir < YboundLow)
                sprites[l].YDir = -sprites[l].YDir;

            // Update the position in the direction of travel
            
            sprites[l].XCoord += sprites[l].XDir;
            sprites[l].YCoord += sprites[l].YDir;

            // Select sprite
            
            IO_303Bh = l;

            // OUT the 4 bytes related to a sprite

            IO_57h = sprites[l].XCoord;
            IO_57h = sprites[l].YCoord;
            IO_57h = (sprites[l].XCoord >> 8) != 0;   // bit 8 of XCoord
            IO_57h = 128;
        }
    }
}

// Show sprites

void showSprites(void)
{
   IO_243Bh = 0x15;
   IO_253Bh = 0x03;   // Change to 0x01 to stay in borders
}

// Hide sprites

void hideSprites(void)
{
   IO_243Bh = 0x15;
   IO_253Bh = 0x00;
}

// ------------ Setup stuff, patterns, start positions etc. --------------

    static unsigned char spriteData[] = {
        0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0x04, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0x04, 0xFF, 0xFB, 0xFB, 0xFB, 0xFF, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0x04, 0xFF, 0xFB, 0xF5, 0xF5, 0xFB, 0xFF, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0x04, 0xFF, 0xFB, 0xF5, 0xA8, 0xA8, 0xFB, 0xFF, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0x04, 0xFF, 0xFF, 0xFB, 0xA8, 0x44, 0xA8, 0xFB, 0xFF, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0x04, 0x04, 0x04, 0xFF, 0xFB, 0xA8, 0x44, 0xA8, 0xFB, 0xFF, 0x04, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3,
        0xE3, 0xE3, 0xE3, 0x04, 0xFF, 0xFB, 0xA8, 0x44, 0x44, 0xFB, 0xFF, 0x04, 0xE3, 0x04, 0xE3, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0xFF, 0xFB, 0x44, 0x44, 0x44, 0xFB, 0xFF, 0x04, 0x4D, 0x04, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0xFF, 0xFB, 0x44, 0x44, 0x44, 0x44, 0xFA, 0x4D, 0x04, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0xFF, 0xFB, 0x44, 0xFF, 0xF5, 0x44, 0x04, 0xE3, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0xFF, 0x44, 0xF5, 0xA8, 0x04, 0xE3, 0xE3, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0xFA, 0x44, 0x04, 0xA8, 0x04, 0xE3, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0x4D, 0x4D, 0x04, 0xE3, 0x04, 0xF5, 0x04, 0xE3,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0x04, 0xE3, 0xE3, 0xE3, 0x04, 0xFA, 0x04,
        0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0x04, 0x04,
        0x0
    };
	 
void loadPatternData(unsigned char patternId)
{   
    unsigned int i;
	 
    // Set the pattern ID where this pattern will be loaded
    
    IO_303Bh = patternId;
    
    //Loop through and load the pattern data
    
    // z80_otir(spriteData, 0x55, 0);  // special value len = 0 means 256 in otir instruction

    for (i=0; i < 256; i++)
        IO_55h = spriteData[i];
}

void setSpriteStartPositions(unsigned char spriteId, unsigned int x, unsigned int y)
{
    sprites[spriteId].XCoord = x;
    sprites[spriteId].YCoord = y;
    sprites[spriteId].XDir   = getRandomDir();
    sprites[spriteId].YDir   = getRandomDir();

    IO_303Bh = spriteId;
	 
    IO_57h = x;
    IO_57h = y;
    IO_57h = (x >> 8) != 0;
    IO_57h = 128;
}

// Pick a random direction/speed.
// Change speedVariants to set how many different speeds there are

#define speedVariants  10

int getRandomDir(void)
{
    unsigned int tmp;
    
    tmp = rand() % speedVariants;
    
    if(tmp <= (speedVariants/2)-1)
        return tmp-(speedVariants/2);  // Skip 0 speed
    
    return tmp-(speedVariants/2)-1;
}
The zsdcc compile is a couple hundred bytes smaller and a bit faster for this program.

Download source, tap and binary for address 32768. (zsdcc compile, the binary is for zeus).

An IO port defined like this defines a 16-bit port so that all io will be done with 16 bit ports 0x0055 and 0x0057:

Code: Select all

__sfr __banked __at 0x55   IO_55h;
__sfr __banked __at 0x57   IO_57h;
An IO port defined like this without the banked keyword defines an 8-bit port so that io will be done in 8-bits with unknown value in the top 8 bits:

Code: Select all

__sfr __at 0x55   IO_55h;
__sfr __at 0x57   IO_57h;
Thanks for the example Steve! This is the first program I've used to test sprites with z88dk.

I hope you don't mind I've made these suggestions -- early on I'd like to make sure people know the ins and outs to get the best performance out of the compilers. This version is noticeably quicker and part of that has to do with the special IO port definitions that inline io instructions rather than using a call to a library function as your original code does and I think another part is by moving to an array of struct we got rid of some multiplications.

BTW if you increase the number of sprites to the maximum 64, you'll see the 12 per scan line limit in action.

Alcoholics Anonymous
Posts: 513
Joined: Mon May 29, 2017 7:00 pm

Re: z88dk / Spec Next sprites

Post by Alcoholics Anonymous » Sun Jun 25, 2017 5:16 pm

The Next team has confirmed that ports 0x55 and 0x57 are 8-bit so they should be declared as 8-bit ports and it is safe to use, eg, the otir instruction to transfer sprite data.

We'll just have to wait for an update to the emulators to implement this.

stevebrowne
Posts: 4
Joined: Tue May 30, 2017 7:26 pm

Re: z88dk / Spec Next sprites

Post by stevebrowne » Mon Jun 26, 2017 5:14 pm

Cool! thanks for the input guys. I will digest, incorporate and update. thankyou!

Post Reply