Jump to content
IGNORED

The 2600 is truly challenging


johncl

Recommended Posts

Hi, I have a general interest in all retro computing, and although my main focus (and collection) is Commodore 64 I read about the Atari and considering that it was out as early as 1977 with full color graphics I found the whole machine intriguing. Especially the rather complex connection between the TIA and the 6507. I have followed the excellent tutorials in this forum and thought I'd try to code something. The first plan was just to create a nice raster sky/ground and then add a sprite. This is where things get really challenging, and I see that the amount of cycles you have to do stuff is severly limited.

 

I found a pattern though and that was to compute the data needed for the next scanline during the scanline draw so that it was simply a fetch and save into the TIA registers when its retracing for the next scanline. However even this becomes challenging simply because I only got 3 registers (A,X,Y) and these has to be used at its most optimal way so you dont end up shuffling data in and out of memory variables, as there is really no time for this. I see the outlines of serious tweaks here and there just to make things flow nicely. :)

 

Fortunately the X position of sprites could be done e.g. during the vertical blank so the real challenge is to put non-zero data into the sprite at the right place and zero it out at the end. I've never seen such complexity just to move a sprite around, its truly challenging. I cannot imagine how I could ever multiplex sprites in y direction in all of this! And this even before I have thought about playfield data. :)

Link to comment
Share on other sites

Hi, I have a general interest in all retro computing, and although my main focus (and collection) is Commodore 64 I read about the Atari and considering that it was out as early as 1977 with full color graphics I found the whole machine intriguing. Especially the rather complex connection between the TIA and the 6507. I have followed the excellent tutorials in this forum and thought I'd try to code something. The first plan was just to create a nice raster sky/ground and then add a sprite. This is where things get really challenging, and I see that the amount of cycles you have to do stuff is severly limited.

 

I found a pattern though and that was to compute the data needed for the next scanline during the scanline draw so that it was simply a fetch and save into the TIA registers when its retracing for the next scanline. However even this becomes challenging simply because I only got 3 registers (A,X,Y) and these has to be used at its most optimal way so you dont end up shuffling data in and out of memory variables, as there is really no time for this. I see the outlines of serious tweaks here and there just to make things flow nicely. :)

 

Fortunately the X position of sprites could be done e.g. during the vertical blank so the real challenge is to put non-zero data into the sprite at the right place and zero it out at the end. I've never seen such complexity just to move a sprite around, its truly challenging. I cannot imagine how I could ever multiplex sprites in y direction in all of this! And this even before I have thought about playfield data. :)

There are many different learned tricks employed to get around Stella's limitations. After all the thing was meant to be a pong machine! Alot of times people draw sprites with the last byte fetched being zero. This is a simple way to clear the sprite when it is done regardless of what scanline it is on. Sometimes tbe kernal is split into several different vertical zones. Like scanlines 160 to 178 do this subroutine, then 179 to 200 do this subroutine. Then it becomes how clever you design the game, by what type of game it is. Anyhow there are a lot of brilliant subroutines written out there. I'm sure there is a thread or two with them all put together. Things like being able to accurately reposition your sprite with a simple divide by 15 routine, etc...

Link to comment
Share on other sites

Yes I know about splitting up the kernel in several "passes" and thats what I do now. One pass to change background color, one to check if sprites should be drawn, one for setting playfield data and last one for drawing alternate line of sprite. I realised that you could simplify the algorithm a bit by adding some zeros to the top and bottom of the sprite so that you could "slide" its starting position with how it aligns with the rasters that way I can advance the sprite fetch index without checking stuff on all the other raster lines since the actual start/stop of drawing would be issued at every 4th raster.

 

However, I see that multiplexing the second sprite several times down the screen in all of this will be tough. The sprite positioning I use now (found the HMP0, HMOVE routine here) uses two WSYNC's so that ruins the sprite draw routine. One the other hand it seems the only operation it does after the second WSYNC is a sta HMOVE (and then rts) so there should be enough time to store the next sprite data immediately in GRP0. The y register is not used in the sprite positioning subroutine also so I could prefetch that data as well. Except that I also need to get the color information... hmm its going to be challenging... just as my thread title says. :)

 

Its fun to tweak the code and try to find the most optimal way of doing stuff. Sometimes I see that using tables and indirect indexed fetches just takes too much time compared to actually doing some arithmetic work in the code also (if its simple stuff). In other cases you can often shave one or two cycles by precalculating stuff. And like with the sprites you can do things that allow your draw routine to assume it can always fetch some new data although it is fetching zeroes until its ready, so that the actual logic is only done once in a while. Many cool algorithms to be found here.

 

And Jay Miner sure didnt make it easy for us with the playfield thing. One thing is the repeated/mirrored playfield where you have to change the data at the correct spots to get new data on the right hand, another is the rather "backwards" drawing of PF1. Its really no problem if your want to add some graphics to the screen, but if you want a sprite to interact with it, its clear your best option is to have two "versions" of the playfield data, one where the bits are correct from left to right, and another that is used for drawing the screen. Naturally you could create the other on the fly and store that in memory (whats left of it). Simply because in order to figure out where in the playfield the player is you need to rotate a masking test bit based on the X position. Another nice challenge. Btw I am making a platformer and my initial idea was to have the screen scroll, but I have chosen not do that but rather let the player move left and right to reach other screens.

Link to comment
Share on other sites

Hmm, seems I have met a wall here. I just cant seem to find enough cycles to do it all with a full playfield that is updated during the scanline to get different data on both sides. The problem is naturally that since the PF0,PF1,PF2 needs to be updated during the scanline you have to update them twice on every scanline. This severely limits the amount of time for anything else, and I probably have to think differently about how I iterate over the sprite data (consuming too many cycles now).

Link to comment
Share on other sites

Hmm, seems I have met a wall here. I just cant seem to find enough cycles to do it all with a full playfield that is updated during the scanline to get different data on both sides. The problem is naturally that since the PF0,PF1,PF2 needs to be updated during the scanline you have to update them twice on every scanline. This severely limits the amount of time for anything else, and I probably have to think differently about how I iterate over the sprite data (consuming too many cycles now).

 

Have you considered losing PF0, part of it will be obscured if you're using HMOVE's anyway. You can get 4/5ths of the screen just updating PF1 & 2 twice each...

 

P.S. This is my first post, so hello to everyone, excelent forum here....

Link to comment
Share on other sites

Just to ask others before I try it. Is it possible to do asymetrical mirrored playfield (as in they are updated during the drawing) using PF1, PF2 and at the same time draw one sprite, and cycle background, sprite and playfield colors? Seems pretty tight...

 

And yes Eshu, I have limited myself to PF1, PF2 only. Should be enough with a 32 bits wide area for the platforms.

Edited by johncl
Link to comment
Share on other sites

Just to ask others before I try it. Is it possible to do asymetrical mirrored playfield (as in they are updated during the drawing) using PF1, PF2 and at the same time draw one sprite, and cycle background, sprite and playfield colors? Seems pretty tight...

 

It really depends on how you're doing these things for example if the sprite is limited to 128 scanlines vertically it can be drawn in 7 cycles if it is stored with a lot of wasted memory:

 

LDA Sprite,y

STA GRP0

 

11 cycles with less wasted memory, but the sprite data must be placed in the centre of a page.

 

LDA Sprite,y

AND Mask,y

STA GRP0

 

Or more cycles with less restrictions with SkipDraw.

 

 

Also there are a few tricks with colours. If you need complete freedom it should take 7 cycles to do a colour change (No masking required as random colour doesn't matter when theres nothing there):

 

LDA Colour,y

STA COLUP0

 

However, sometimes you can relate the colours to the graphics - something I am working on at the moment uses only 3 extra cycles for the colours:

 

LDA Sprite,y

AND Mask,y

STA GRP0

SAX COLUP0

 

(SAX is an illegal opcode which stores A & X) I have X permenantly set to 0x0F in this kernel.

Link to comment
Share on other sites

Thanks for the tips. That mask looks interesting but how many cycles keeping it up to date ($00 or $ff I assume) during the draw?

 

This is my sprite draw routine now:

 

		ldy SPRYCNT
	beq nosprite; skip when sprite has ended
	dec SPRYPOS
	bne nosprite; until position is at sprite y pos
	lda #1
	sta SPRYPOS
	dey
	sty SPRYCNT

nosprite
	ldx $f900,y
	lda (SPRPTRLO),y

; --- scanline 1 - Player Sprite draw (even line)
	sta WSYNC	  
	sta GRP0
	stx COLUP0

 

The color info is at $f900 and the sprite is at $f800 and I calculate the SPRPTRLO based on which sprite to show. The whole problem is figuring when the sprite is supposed to be drawn and figure a way to know when it is finished. In my case it just fetches a the first entry (zero value) in the sprite data over and over again when the sprite counter has ended. To make sure it iterates over all bytes I need to set a #1 in the counter again so it passes through every time (all which is naturally skipped when it has reached the end). Still a lot of cycles wasted for these checks and iterations.

 

Edit: Reading your code snippets again I now understand that the mask is a table of preceding zeroes and $ff depending on the height of the sprite and then a lot of zeroes after. And your MASK pointer is then adjusted to when it should start drawing, and your sprite pointer is also offset by the same amount. A really smart trick there yes. I guess one could sacrifice some bytes in rom for this table.

Edited by johncl
Link to comment
Share on other sites

Just to ask others before I try it. Is it possible to do asymetrical mirrored playfield (as in they are updated during the drawing) using PF1, PF2 and at the same time draw one sprite, and cycle background, sprite and playfield colors? Seems pretty tight...

 

And yes Eshu, I have limited myself to PF1, PF2 only. Should be enough with a 32 bits wide area for the platforms.

Should be possible - in Stay Frosty I draw a asymmetrical mirrored playfield using just PF1 & PF2 while drawing 2 sprites (and reusing/repositioning one of the sprites), and changing the colors of 1 sprite as well as the playfield.

Link to comment
Share on other sites

Thanks for the tips. That mask looks interesting but how many cycles keeping it up to date ($00 or $ff I assume) during the draw?

 

This is my sprite draw routine now:

 

		ldy SPRYCNT
	beq nosprite; skip when sprite has ended
	dec SPRYPOS
	bne nosprite; until position is at sprite y pos
	lda #1
	sta SPRYPOS
	dey
	sty SPRYCNT

nosprite
	ldx $f900,y
	lda (SPRPTRLO),y

; --- scanline 1 - Player Sprite draw (even line)
	sta WSYNC	  
	sta GRP0
	stx COLUP0

 

The color info is at $f900 and the sprite is at $f800 and I calculate the SPRPTRLO based on which sprite to show. The whole problem is figuring when the sprite is supposed to be drawn and figure a way to know when it is finished. In my case it just fetches a the first entry (zero value) in the sprite data over and over again when the sprite counter has ended. To make sure it iterates over all bytes I need to set a #1 in the counter again so it passes through every time (all which is naturally skipped when it has reached the end). Still a lot of cycles wasted for these checks and iterations.

 

Edit: Reading your code snippets again I now understand that the mask is a table of preceding zeroes and $ff depending on the height of the sprite and then a lot of zeroes after. And your MASK pointer is then adjusted to when it should start drawing, and your sprite pointer is also offset by the same amount. A really smart trick there yes. I guess one could sacrifice some bytes in rom for this table.

 

Have you had a look at the SkipDraw code - skipDraw on MiniDig, think that's the most similar method to yours and is as efficient as possible...

Link to comment
Share on other sites

Well really, the basic idea is to compromise between having enough cycles to work with vs. as many things as you can get on the screen in a single scanline. You could do a selective asymetrical playfield. Remember that if one PF register is loaded you could always leave it as is and not reload it again for the right hand side. I am working on a game platform myself where the game images are done in zones and each zone loads the register is diffrent ways and also the playfield also consists of what appears to be 2 scanline images. You could do like

 

First pass: PF0 PF1 PF2 PF1

second pass: PF1 ~~~~~PF1

 

And the next line would then be new data. You can actualy do several diffrent arangements of register load and stores that can give more detail with out the losses of a full asymentrical playfield. Granted this isn't a ton of savings on cycles but with proper tweaking and some fancy coding along with a compromise here and there, you can get plenty of detail on the screen.

 

I also toyed with the thought that if some registers needed nothing there, then I could do a immediate load and store thereby saving a few cycles. likewise if the register was all on or 11111111.

Link to comment
Share on other sites

:idea: For effecitve sprite drawing search for "mask(ed)draw", "switchdraw" and "skipdraw".

 

For repositioning think about blank PF lines (or a striped PF like in Thrust). Also consider special code for positioning the sprite left or right as this leaves some CPU time during those lines.

Link to comment
Share on other sites

Thank you all for the replies. Will definitely look into other ways of placing sprite. I did try that skipdraw method and it uses 22 cycles at most which is the same that my method already uses. Rewriting my code to the skipdraw gives me:

 

		inc SPRYCNT	; 5
	lda SPRYCNT	; 3
	sec			; 2 
	sbc PlayerY	; 3
	adc #SPR_HEIGHT+1 ; 2
	bcc skipDrawSprite; 2-3
	tay			; 2
	jmp drawSprite	; 3   = 22 cycles (pass through bcc)
skipDrawSprite		
	ldy #0			; 2
drawSprite
	ldx $f900,y
	lda (SPRPTRLO),y

 

Putting the skipDraw outside the kernal and branching back doesnt give any benefits as that would just move the jmp outside and would use 23 cycles in whenever it is being skipped.

 

Naturally the benefit with this method is that you need to have a scanline counter which is of great use to other kernel code. I didnt have that and sorely needed one so I will probably use the skipsprite method for now.

 

About the tip of empty scanlines thats how I intend to reuse the other sprite several times down the screen. Just afraid my kernel will grow very big. :)

 

Edit: I see now that the cycles savings for doing the skipDraw outside the kernel is that I dont need to read the color and indirect adressing of data. Thats naturally a big saving in that case! So it seems the skipdraw will max out at 19 cycles which 3 cycles lower than what I had. Good good. :)

Edited by johncl
Link to comment
Share on other sites

Btw I tried out Stay Frosty, and its a brilliant platformer with some nice challenges as you get into the game. The only thing missing from it is a level number indicator and perhaps a bonus life there now and then (perhaps there is). I love the detail and expression you have gotten into that snowman, I guess you have used the missile and ball a lot on him.

 

I was hoping to give my hero some eyes using the ball and perhaps some other tiny details. But I dont think I have enough cycles to do that. The screen I have now looks fairly nice but with a mirrored playfield. My next challenge is to update it during draw so its asymmetrical and I need to draw one sprite as well in the same scanline but I guess I will try to use some assumptions that the next data is available always so I just pad the end of the sprite with zeros to avoid any sort of checks.

 

At least I have seen that I need to make my kernel quite a bit larger and split the drawing up into sections since there will be whole lines where there is no playfield (but only background color and the main player sprite). These are perfect places to multiplex the other sprite.

 

I guess I should design the game for NTSC and 192 visible lines and just have more border on the PAL versions (or extra decorations).

Link to comment
Share on other sites

Just a quick question. I felt like reinventing the wheel and doing a 6 digit score. I assumed it was done by using the two sprites next to each other, set them to repeat 3 times and then change the GRP0 and GRP1 as it draws over the screen. And I guess the idea is to use the stack register as extra storage since that only uses 2 cycles to transfer the value which is just enough to sqeeze in that last sta GRP1 to set the 6th number. But is this safe?

 

And it works fine for me now in Stella, can I assume its working on real hardware as well? Or are there things you can do in Stella that allows TIA trickery beyond what the real hardware can do?

 

Also one last question. Is it possible to reuse the sprites on the same raster line in any way all? (Even if I cant move it but always relative to previous location?)

Link to comment
Share on other sites

The stack should be safe, as long as you don't have a bunch of return addresses stacked there, and are not close to your used ram. Maybe take a peek with Stella's debugger while your routine is loading. The 6 digit score has been done many times. I don't know what one is the most efficient. I took this one out of Kaboom!

 

 

.loopDraw
   STY	tempOne  ;3
   LDA	($FE),Y  ;5
   STA	tempTwo  ;+3 = 70 cycles
   STA	WSYNC	;3
;----------------------------
   LDA	($F4),Y  ;5
   STA	GRP0	 ;3
   LDA	($F6),Y  ;5
   STA	GRP1	 ;3
   LDA	($F8),Y  ;5
   STA	GRP0	 ;3
   LDA	($FC),Y  ;5
   TAX			 ;2
   LDA	($FA),Y  ;5
   LDY	tempTwo  ;3
   STA	GRP1	 ;3
   STX	GRP0	 ;3
   STY	GRP1	 ;3
   STA	GRP0	 ;3
   LDY	tempOne  ;3
   DEY			 ;+2 = 56 cycles
   BPL	.loopDraw;2/3

Link to comment
Share on other sites

Btw I tried out Stay Frosty, and its a brilliant platformer with some nice challenges as you get into the game. The only thing missing from it is a level number indicator and perhaps a bonus life there now and then (perhaps there is). I love the detail and expression you have gotten into that snowman, I guess you have used the missile and ball a lot on him.

Thanks, Nathan Strum came up with the game idea and did all the graphics. The snowman is only drawn with a single sprite, the fireballs with the other. The ice blocks and platforms are both drawn with the playfield. The snowman's melt trail is drawn with 1 missile, the other missile and ball aren't used. The original design called for a carrot nose to be drawn using the other missile, but there wasn't enough CPU time to do so.

 

I wish I could have put a level number in, I ran out of ROM space due to all the snowman and fireball graphics. There is a bonus life when you complete 32 levels - the game starts over with the temperature increased so everything melts faster.

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...