Thoughts on sprites

A project log for JJ65C02

Working on my own version of a 65C02-based SBC. Everything is open source and permissively licensed.

jim-jagielskiJim Jagielski 12/28/2023 at 14:140 Comments

Implementation musings

Now that the main details regarding the integration of the 6502 SBC and the Pi Pico I/O support chip are worked out, and working, I find myself mulling over details on how to add in sprite functionality (I know, I know... I should be working on adding audio to the Pico, but that isn't as intriguing right now). On one hand, I again want to resist the urge to make the JJ65C02 effort a Pi Pico-based system, with the 6502 as an afterthought, and instead want to focus on providing just enough capability to make it useful, but still be true to the whole retro vibe. In other words, I don't want this project to turn into another example of the tail wagging the dog.

Due to the limitations imposed, the video setup is a single frame-buffer, bit-mapped display, and so any sprite (or tile) work needs to be able to quickly update the video buffer by drawing the sprite, but also quickly erase the sprite cleanly (restoring the background) as well as handle transparency. By this, I mean that sprites aren't drawn on top of the background (as a layer) but actually drawn IN the background.

Some systems handle this by modifying the actual scanline data before it's rendered on the screen:

  1. Grab the scanline data from the frame buffer (the background)
  2. Draw the corresponding row data for the sprite overtop that scanline data
  3. Push the edited scanline to the VGA rendering system to have it printed out on the screen

The advantage is that the actual frame buffer itself is never directly modified. The disadvantage is that this is pretty complex to fold into the current DMA driven design.

Instead I'm looking into the following flow:

  1. Grab the memory area that the sprite will be written to from the frame buffer (VRAM)
  2. Store that away (backup)
  3. Now using a mask, write the sprite to that area
  4. Store away the x,y coordinates of the recent write
  5. When moving the sprite, restore the original memory area from the stored backup
  6. Jump to 1

Sprite Details

A sprite size of 16x16 fits nicely into a 640x480 screen and provides enough detail and depth to be useful. The actual sprite data itself will use 1byte/pixel, even though we only use 4bits/pixel internally. This is done so we can autogenerate the mask data from the actual sprite itself; by using $FF as code for transparent "color", we can load in the actual sprite data itself, and via shifting a single byte to a nibble, create the real sprite dataset and mask internally to the Pico and store them there, instead of transforming these on the fly, which takes time. In essence, the sprite data is sent from the 6502 to the Pico, which then takes take of converting it to a native format that can then be directly written to VRAM.

There is one catch, and it is bothersome. Because we store (internally, on the Pico) 2 pixels per byte, the byte itself must be aligned with an even X coordinate. That is, if we wanted to draw the sprite starting at, say (1,100), then we would need the 2nd nibble of the 1st byte of that sprite data (the last 4bits) to be written to the 1st nibble of the corresponding VRAM byte (recall that pixel (0,100) and (1,100) are stored in a single byte). A simple hack is to simply force X to be even to ensure byte alignment, and that is likely the 1st route I'll try. Again, this is because we will be writing the sprite data to VRAM directly via DMA, and not pixel-by-pixel. There might be some way around this by extending masking transparency for the special case where X is odd, but it all depends on the performance impact of doing that.

Internal to the Pico, the sprite will be a struct with 5 fields:

  1. the color-encoded bitmap (8x16=128bytes)
  2. the mask (also 128bytes)
  3. the stored background (128bytes again)
  4. Stored X coordinate
  5. Stored Y

Altogether we're looking at ~390bytes per sprite stored on the Pico (padding??). Considering that we're only using ~153k of the Pi Pico RAM, that gives us room for plenty of sprites, though limiting it to something like 32 makes sense. Also, 8 bytes on the Pico corresponds to a long long (or int64_t) so we can use native variables here and bit shifts along with XORs.

I'm sure that there will be other gotchas along the way, but this is a good start for now...