Mode 3 Scrolling Game Engine Demo (WIP)

Use this forum to share and discuss Uzebox games and demos.
User avatar
Artcfox
Posts: 1146
Joined: Thu Jun 04, 2015 5:35 pm
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by Artcfox » Tue Jan 24, 2023 4:12 am

I tried adding back my ram time estimation system to see if I can blit the unicorn late in the process, just before we run out of RAM tiles, and it worked most of the time, except when other entities overlapped and didn't need any more ram tiles to render, then the z-order was wrong.

It used very little CPU, but it wasn't perfect, so I just went whole hog and copied and pasted the BlitSprite() function into my code and added a boolean variable "doblit" that only calls BlitSpritePart() if true.

Now if I want to totally max out my RAM tile usage so the sprite flickering doesn't happen until I truly run out of RAM tiles, but still be able to perfectly control the Z-order of my main character's 2x2, 3x2, or 1x3 megasprite, I can just call MyBlitSprite() with "doblit" set to false to reserve RAM tiles up front without doing any unnecessary blitting. Then I can call the normal BlitSprite() function to draw all of my entities, not stopping or limiting it in any way to take full advantage of free overlapping RAM tiles, and finally I can call MyBlitSprite() with "doblit" set to true to render the main character into the reserved RAM tiles with the highest Z-order when I want to.

That meets all of my goals:
  • Every 8x8 sprite that the main character is composed of must be drawn every frame without flickering
  • Every other visible entity should be blitted without regard for RAM tile usage, so overlapping sprites can happen "for free"
  • Sprite flickering should cost close to nothing, and only happen when absolutely necessary
  • I should have perfect and absolute control of the Z-order of the main character, without wasting any additional CPU
It takes around 6000-10000 clock cycles to double blit, so this seems like a total win. I love it when a plan comes together!

And because I can't help myself... The only thing that might make this better is if the RAM tile allocation wasn't sequential so if sprite flickering does need to happen, it doesn't always happen on entire 8x8 sprites at once, it might only be 1/2 of a sprite if it overlapped 2 RAM tiles, or 1/4 of a sprite if it was overlapping 4 RAM tiles. From what I've seen with my frame-by-frame analysis so far I think flickering just a few pixels of a distributed number of sprites would look way better than losing an entire 8x8 sprite every 1/60th of a second. And it could be combined with the array swizzling technique that I already use to distribute the flicker across all entities. We shall see!

User avatar
Artcfox
Posts: 1146
Joined: Thu Jun 04, 2015 5:35 pm
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by Artcfox » Tue Jan 24, 2023 5:10 am

Ok, so this I would really like some feedback on. I implemented RAM tile striping in the blitter, so instead of just using the next free ram tile, there is a lookup table that is striped:

Code: Select all

for (uint8_t i = 0, j = 0; j < 32; i = (i + 7) % 32, ++j)
    printf("%d, ", i);
The MyBlitSprite function in its entirety now looks like:

Code: Select all

extern void BlitSpritePart(u8 ramtileno, u16 flidx, u16 xy, u16 dxdy);
void MyBlitSprite(u8 flags, u8 sprindex, u8 xpos, u8 ypos, bool doblit)
{
  uint8_t RamTileLUT[] = { 0, 7, 14, 21, 28, 3, 10, 17, 24, 31, 6, 13, 20, 27, 2, 9, 16, 23, 30, 5, 12, 19, 26, 1, 8, 15, 22, 29, 4, 11, 18, 25 };

  extern uint8_t user_ram_tiles_c;
  extern uint8_t free_tile_index;
  extern struct BgRestoreStruct ram_tiles_restore[];
  u8  bx;
  u8  by;
  u8  dx;
  u8  dy;
  u8  bt;
  u8  x;
  u8  y;
  u8  tx;
  u8  ty;
  u8  wx;
  u8  wy;
  u16 ramPtr;
  u8  ssx;
#if (SCROLLING != 0)
  u16 ssy;
#else
  u8  ssy;
#endif

  /* if sprite is off, then don't draw it */

  if ((flags & SPRITE_OFF) != 0U){ return; }

  /* get tile's screen section offsets */

#if (SCROLLING != 0)
  ssx = xpos + Screen.scrollX;
  ssy = ypos + Screen.scrollY;
  if (ypos > (u8)((Screen.scrollHeight << 3) - 1U)){
    ssy += 0xFF00U; /* Sprite should clip on top */
  }
#else
  ssx = xpos;
  ssy = ypos;
#endif

  tx = 1U;
  ty = 1U;

  /* get the BG tiles that are overlapped by the sprite,
  ** supporting wrapping (so sprites located just below zero X
  ** or Y would clip on the left). In a scrolling config. only
  ** TILE_WIDTH = 8 is really supported due to the "weird" VRAM
  ** layout, VRAM_TILES_H is also fixed 32 this case. */

#if ((SCROLLING == 0) && (SCREEN_TILES_H < 32))
  bx = ((u8)((ssx + TILE_WIDTH) & 0xFFU) / TILE_WIDTH) - 1U;
#else
  bx = ssx / TILE_WIDTH;
#endif
  dx = ssx % TILE_WIDTH;
  if (dx != 0U){ tx++; }

#if (SCROLLING == 0)
  by = ((u8)((ssy + TILE_HEIGHT) & 0xFFU) / TILE_HEIGHT) - 1U;
#else
  by = ssy / TILE_HEIGHT;
#endif
  dy = ssy % TILE_HEIGHT;
  if (dy != 0U){ ty++; }

  /* Output sprite tiles */

  for (y = 0U; y < ty; y++){

    wy = by + y;
#if (SCROLLING == 0)
    if (wy < VRAM_TILES_V){
#else
    if ( (Screen.scrollHeight != 0U) &&
         ((u8)((ypos + 7U + (y << 3) - dy) & 0xFFU) < (u8)((Screen.scrollHeight << 3) - 1U)) ){

      while (wy >= Screen.scrollHeight){
        wy -= Screen.scrollHeight;
      }
#endif

      for (x = 0U; x < tx; x++){

        wx = bx + x;

#if (SCROLLING == 0)
        if (wx < VRAM_TILES_H){
#else
        wx = wx % VRAM_TILES_H;
#if (SCREEN_TILES_H < 32U)
        if ((u8)((xpos + 7U + (x << 3) - dx) & 0xFFU) < (((SCREEN_TILES_H + 1U) << 3) - 1U)){
#else
        {
#endif
#endif

#if (SCROLLING == 0)
          ramPtr = (wy * VRAM_TILES_H) +
                   wx;
#else
          ramPtr = ((u16)(wy >> 3) * 256U) +
                   (u8)(wx * 8U) + (u8)(wy & 0x07U);
#endif

          bt = vram[ramPtr];

          if ( ( (bt >= RAM_TILES_COUNT) |
                 (bt < user_ram_tiles_c)) &&
               (free_tile_index < RAM_TILES_COUNT) ){ /* if no ram free ignore tile */

            if (bt >= RAM_TILES_COUNT){
              /* tile is mapped to flash. Copy it to next free RAM tile. */
              CopyFlashTile(bt - RAM_TILES_COUNT, RamTileLUT[free_tile_index]);
            }else if (bt < user_ram_tiles_c){
              /* tile is a user ram tile. Copy it to next free RAM tile. */
              CopyRamTile(bt, RamTileLUT[free_tile_index]);
            }
#if (RTLIST_ENABLE != 0)
            ram_tiles_restore[free_tile_index].addr = (&vram[ramPtr]);
            ram_tiles_restore[free_tile_index].tileIndex = bt;
#endif
            vram[ramPtr] = RamTileLUT[free_tile_index];
            bt = RamTileLUT[free_tile_index];
            free_tile_index++;

          }

          if (doblit && (bt < RAM_TILES_COUNT) &&
               (bt >= user_ram_tiles_c) ){
            BlitSpritePart(bt,
                           ((u16)(flags) << 8) + sprindex,
                           ((u16)(y)     << 8) + x,
                           ((u16)(dy)    << 8) + dx);
          }

        }

      } /* end for X */

    }

  } /* end for Y */

}
You can see where I used the LUT by searching on RamTileLUT, and you can watch it in action in the memory view of cuzebox.
rtlut.png
rtlut.png (74.31 KiB) Viewed 58 times
This, plus a double circular mixing of my entity array every frame that we needed to sprite flicker:

Code: Select all

    if (l->numSpawned >= 4) {
      ENTITY tmp;

      uint8_t llo = 0;
      uint8_t lhi = (l->numSpawned - 1) / 2;
      tmp = l->entity[lhi];
      memmove(&l->entity[llo + 1], &l->entity[llo], sizeof(ENTITY) * (lhi - llo));
      l->entity[llo] = tmp;

      uint8_t rlo = lhi + 1;
      uint8_t rhi = l->numSpawned - 1;
      tmp = l->entity[rlo];
      memmove(&l->entity[rlo], &l->entity[rlo + 1], sizeof(ENTITY) * (rhi - rlo));
      l->entity[rhi] = tmp;
    }
causes smaller portions of sprite flicker to be distributed among all entities. I haven't tried different striping patterns yet, but this one looks very promising. Once I settle on a good one, I'll ditch the array, and just use the math.

Please, please please try the attached .uze file out and let me know what you think.

Edit: WOW! I just tried it on real hardware with a super high quality LCD, and I the only place I can see any sprite flickering is a just a little bit in the turtle if I get the most things on the screen at once (falling spike, turtle, butterfly, all fireballs, misaligned unicorn, jumping and farting). This makes me wish I had a CRT to try this on!
Attachments
unicorn5.uze
(43.69 KiB) Downloaded 1 time

User avatar
Artcfox
Posts: 1146
Joined: Thu Jun 04, 2015 5:35 pm
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by Artcfox » Thu Jan 26, 2023 2:33 am

Sometimes I feel like I'm just talking to myself here, but documenting things as I'm learning them is certainly easier, and if seeing me go through this process helps someone else out, then all the better.

When I ditched the Mode 3 sprite system, it reminded me of the "There is no spoon" scene from The Matrix. The sprites[] array is only there as a temporary holding spot for the information needed for blitting, and that under the hood it all just boils down to blitting stuff into RAM tiles each frame, and keeping track of what was in VRAM before the blit so it can be restored after the pixels are output. "There is no sprite."

So this got me thinking that the blits for a character don't have to be in a tiled grid pattern, they can be disjointed, or on top of each other with transparency, and in the back of my mind I remembered the Partitioned Sprites that Lee had written about many years ago on our wiki. I had never tried that before, and it seemed like a lot of work to implement in the old system, but with blits, it should be really easy, and I would get the benefit of being able to blit things that are different sizes and shapes that share the same origin point, so I no longer needed to translate each individual object that I needed to draw based on how big it is, or which way it is facing, because tile maps always have the origin point set in the top left corner of the megasprite.

So I came up with my own map format, and I've been calling them BlitMaps to differentiate them from the TileMaps from before, and I set about converting all of my tile maps to blit maps, paying attention to minimizing the bounding boxes (and thus RAM tiles needed) as described in Lee's document, and thinking about how I can reuse the same blit for multiple megablits. For instance, now I only have a single head tile that I can use for all of the poses, and I can just blit that at whatever offset it should be drawn at for that pose.

Here is an example of how I carved up the first walking pose:
tilemap_converted_to_blits.png
tilemap_converted_to_blits.png (12.67 KiB) Viewed 37 times
and here is what its BlitMap looks like with the origin point set to 16 pixels above where the front leg starts:

Code: Select all

// This is the starting tile number for the BlitMaps
#define S (20)

const int8_t blitmap_walk0[] PROGMEM =
  {
   3,    // Number of blits (if the high bit is set, all blits in the map should be flipped on the X axis)
   
   S,    // tileIndex
   -2,   // X offset of blit
   0,    // Y offset of blit
   
   S + 1,
   0,
   8,

   S + 2,
   6,
   7,
  };
and I just continued to carve up each of my poses (testing as I went along, because it's easy to mess up!) until everything was now stored as a BlitMap. The function for blitting these BlitMaps is pretty simple, though I wanted to bake x-mirroring into the BlitMap, to save on having to have duplicate tiles that are just mirrored images of each other, and so I don't have to account for mirroring things in the code later. For that I hijacked the top bit of the first element in the BlitMap, the length field since you would run out of CPU if you tried to blit 255 things. If I need y-mirroring later I can just use the second highest bit for that.

And then I wrote the function to churn through a BlitMap:

Code: Select all

void BlitMyBlitMap(uint8_t spriteFlags, const int8_t *blitmap, uint8_t map_x, uint8_t map_y, bool doblit)
{
  uint8_t xflip_len = (uint8_t)pgm_read_byte(&blitmap[0]);
  int8_t len = xflip_len & 0x7F;
  if (xflip_len & 0x80) { // If the high bit of len is set, flip the entire sprite on X
    if (spriteFlags & SPRITE_FLIP_X)
      spriteFlags &= ~SPRITE_FLIP_X;
    else
      spriteFlags |= SPRITE_FLIP_X;
  }

  int8_t xdir;
  int8_t xoff;
  if (spriteFlags & SPRITE_FLIP_X) {
    xdir = -1;
    xoff = TILE_WIDTH * (PLAYER_TILE_WIDTH - 1);
  } else {
    xdir = 1;
    xoff = 0;
  }

  int8_t ydir;
  int8_t yoff;
  if (spriteFlags & SPRITE_FLIP_Y) {
    ydir = -1;
    yoff = TILE_HEIGHT * (PLAYER_TILE_HEIGHT - 1);
  } else {
    ydir = 1;
    yoff = 0;
  }

  for (int8_t i = 0, pos = 1; i < len; ++i, pos += 3)
    MyBlitSprite(spriteFlags,
                 (uint8_t)pgm_read_byte(&blitmap[pos]),
                 map_x + xoff + xdir * (int8_t)pgm_read_byte(&blitmap[pos + 1]),
                 map_y + yoff + ydir * (int8_t)pgm_read_byte(&blitmap[pos + 2]),
                 doblit);
}
PLAYER_TILE_WIDTH and PLAYER_TILE_HEIGHT are #defines of 1 and 2 respectively, and doblit is a boolean that tells the MyBlitSprite() function if we are going to blit the sprite (true) or if we just want to reserve the RAM tiles without doing the blit (false).

This ended up reducing my flash usage by having way fewer sprite tiles, it reduced the CPU time needed for blitting, because there are fewer blits per "megablit", and it reduced the number of RAM tiles needed to display the same sized character unaligned on average. So a triple win!

It also made me think about what can happen next now that I'm using these BlitMaps...

I only have a single unicorn head tile now, what if I add two parameters to the BlitMyBlitMap() function: replaceTile and withTile. Then as it blits the map, any time it sees the head tile (#20) I can replace it with a different head, maybe one where the horn is glowing red because you're using magic, or the tongue is sticking out because you just ate a lollipop, or the eyes blink because you are idle. What if I instead allocate a user RAM tile for this, and now at runtime I can change the way the head looks with special effects, or if it takes damage I can tweak individual pixels.

Or maybe certain powerups can change the look (add wings that get blitted on top in any pose?) Or maybe it can carry objects around the level and have them float nearby with its magic. All I would need to do would be add an extra call to the blitter, I wouldn't need to mess around with trying to find the proper sprite number so I get the z-order correct, and then have to deal with the sprite numbers for everything else getting messed up. Nope, there are no sprites, it's all just blits now, and I can do them whenever I need to, and let the flickering algorithm works its magic drawing the rest of the entities. Exciting times!

User avatar
mapes
Posts: 150
Joined: Sun Feb 13, 2011 7:04 am
Location: Seattle

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by mapes » Fri Jan 27, 2023 7:22 am

Reading your progress on the unicorn game and the sprite/blitting progress you are making is great! I could see this used in the future by the community and could definitely open the doors for more retro game developers for the 644 and bring about more nes like brawlers or similar action games.

One of these days I need to finish my dalaks game that I started along time ago "based on an Dr. Who".

User avatar
Artcfox
Posts: 1146
Joined: Thu Jun 04, 2015 5:35 pm
Contact:

Re: Mode 3 Scrolling Game Engine Demo (WIP)

Post by Artcfox » Fri Jan 27, 2023 1:39 pm

Thanks mapes!

It makes me happy knowing that I'm not just speaking out into the void. :lol:

I am really trying to go all out with this, and I am learning so much in the process. Now when I re-read other posts from people, I understand a lot more what they were talking about with the blitters, and they way the video modes work under the hood. I am hoping that I have learned enough to understand and try out some of the new video modes that Jubatian wrote. The high resolution and 256 color support of Mode 3 is a big draw to me though.

I just wish that mode 3 could access the SD card during HSYNC. I might need to explore adding that in order to not waste so many cycles accessing it during user code, even if it is to just finish off reading the extra junk bytes after I am done reading the data I need.

Ah yes, I do remember you talking about that game before! Now is as good of a time as any to finish it, so go for it! I had started this in 2018, and then got involved in other things, but it is fun to be back and working on Uzebox stuff again.

Post Reply

Who is online

Users browsing this forum: Artcfox and 2 guests