Bun Runner Devlog -- 2025-02-09

February 9, 2025

This week was all about improving asset loading and drawing some warmup graphics. Before this week, the background graphics – the foreground columns and the background art – were imported directly into the output binary with INCBIN, just for simplicity in those early days when I was still flailing about with Amiga development. I decided to load those in dynamically as XPK compressed assets, much like how I load sprites.

I wanted to add unit tests for this, but ran into an issue with the XPK library and Vamos. Vamos fails to load the XPK library with the error libmgr:WARNING: xpkmaster.library: init resident failed!. This means that if I want to write tests against XPK, I need to run them on a “real” Amiga.

This was easy to set up in the test suite:

  if (strcmp(argv[1], "xpk") == 0) {
    printf("Running XPK tests, these only work on a real Amiga!\n");
    XPKLoaderSuite(suite);
    LevelDataReaderSuite(suite);
  }

It did mean that, if (when!) I wrote code that would crash the Amiga, the dev cycle was a lot slower. But at least I have tests around the loader code now.

The next part is warmup graphics. I want the rabbit to, at the start of the game, stretch a bit before starting to run. That would take about three or four seconds to go through the poses, and it’ll be cute. After the rabbit crashes and the level restarts, I want to pick only one pose, then the crouching pose, then get the game started, so you don’t have to sit through the whole warmup after each death. Here’s the sprites for the poses I have so far:

Sprites of rabbit stretching before running

Up to this point, this game has had no need for randomization, and I actually don’t want it for the level designs. I want them repeatable, to better facilltate muscle memory and speedruns. However, I do want to randomize the poses, so I needed something to help me generate randomness.

Last summer I went through a big FPGA practice experience, pushing my little Go Board as far as I could, then eventually getting a ULX3S to go further. I decided to put that aside to work on this game, but I did learn how FPGAs do randomization, and got reacquainted with it reading the Wikipedia arcicle on Xorshift. I implemented a very simple Xorshift in assembler that overwrites the seed value for later use, much like how you would want to use amiga.lib/FastRand but without needing to link in that library. Here’s the code. It’s super easy:

  MOVE.L #1000,RandomSeed
  MOVE.L #RandomSeed,A0
  BSR randomizeLong ;=> RandomSeed contains new "random" value
  RTS

RandomSeed DS.L 1

; Implement Xorshift LFSR
; https://en.wikipedia.org/wiki/Xorshift
; @inreg A0 uint32_t Memory location to randomize, using itself as the seed
randomizeLong:
  MOVE.L (A0),D0
  MOVE.L D0,D1

  ; 32 bit xorshift
  ; x ^= x << 13;
  LSL.L #8,D0
  LSL.L #5,D0
  EOR.L D0,D1
  MOVE.L D1,D0

  ; x ^= x >> 17;
  LSR.L #8,D0
  LSR.L #8,D0
  LSR.L #1,D0
  EOR.L D0,D1
  MOVE.L D1,D0

  ; x ^= x << 5;
  LSL.L #5,D0
  EOR.L D0,D1

  ; write it back
  MOVE.L D1,(A0)
  RTS

Bun Runner Devlog -- 2025-01-31

January 31, 2025

A bunch of travel slowed down progress, including a very awesome trip to MAGFest 2025. I’m posting pictures on Pixelfed, and those’ll eventually end up on my main site in a more permanent gallery.

This past block of effort has been in getting the head konk animation working:

Rabbit hits their head on a pillar and lands on the ground.

This required a lot of testing to handle all the possible collisions that could happen, but it’s working now and seems relatively stable.

This week is going to be working on the progressive screen rendering. After the initial render, screens are rendered in chunks, one chunk per frame. The screen scrolls at 2 pixels/frame, and columns are 16 pixels wide, so it means we get 8 frames of time to re-render the screen for the next column.

A game screen broken into 8 segments

I started paying more attention to this code and so it’s getting a set of tests and restructuring so it’s easier to reason about. There’s some glitches in this just-in-time rendering that should be easy to squash once all that’s done. There’s one or two other low hanging performance issues I’m going to correct as well, and then it’s on to making this more of a game.

Bun Runner Devlog -- 2025-01-10

January 10, 2025

Not much in the way of visuals to show this week. My focus has been to flesh out more of the unit test suite, allowing me to fix a few bugs in the process as well as perform some nice optimizations on some oft-called code.

I also discovered a good, extensible approach to building switch statements in assember without using a big list of CMP/BEQ statements, which requires writing lots of code and is also costly execution-wise. Any code I’ve run into that uses the CMP/BEQ approach I’ve been converting to this new approach, which is another reason for no real visuals this week.

The approach comes from a post on English Amiga Board: https://eab.abime.net/showpost.php?p=1548231&postcount=7

The example in the post is specific to the original asker’s use case, so I’ll provide a more generic example for DevPac, along with a unit test written in CuTest for SAS/C:

; switch.asm

  ENUM ; MyChoices
  EITEM ItemOne
  EITEM ItemTwo
  EITEM ItemThree

  ; make accessible to C
  XDEF _MySwitch

; @inreg D0 enum MyChoices
; @outreg D0 final value
_MySwitch:
  ADD.W D0,D0                   ; jumpTable entries are word sized
  MOVE.W .jumpTable(pc,D0.W),D0 ; get the offset from .jumpTargets
  JMP .jumpTargets(pc,D0.W)     ; go!

.jumpTable:
  ; these are in enum order
  DC.W .itemOne-.jumpTargets
  DC.W .itemTwo-.jumpTargets
  DC.W .itemThree-.jumpTargets

.jumpTargets:
.itemOne:
  MOVE.L #4,D0
  RTS

.itemTwo:
  MOVE.L #20,D0
  RTS

.itemThree:
  MOVE.L #50,D0
  RTS
// switch_test.c
#include "cutest.h"

enum MyChoices {
  ItemOne,
  ItemTwo,
  ItemThree
};

// this looks for an exported _MySwitch symbol
uint32_t __asm MySwitch(register __d0 uint32_t myChoice);

void T_switch(CuTest *tc) {
  uinit32_t result;

  result = MySwitch(ItemOne);
  CuAssertIntEquals(tc, 4, result);

  result = MySwitch(ItemTwo);
  CuAssertIntEquals(tc, 20, result);

  result = MySwitch(ItemThree);
  CuAssertIntEquals(tc, 50, result);
}

void switchSuite(CuSuite *suite) {
  SUITE_ADD_TEST(suite, T_switch);
}

Bun Runner Devlog -- 2025-01-03

January 3, 2025

Look at that smooth fade in:

Bun runner game map and sprite fade in from black

The palette manager I talked about on 2024-12-22 is finally plugged in at the right place to allow the game to fade in the palette from black (or, really, any color) when the level starts up.

Interfaces, implementations, and dependencies

As I’m writing unit tests for all this, it’s required me to really think a lot about how to structure everything. Originally, I had mashed together a lot of the implementation details into what I call the Game Screen, the main display of the game. This includes:

  • double buffering
  • palette color definitions
  • the Copperlist definition
  • an implementation of “wait for bottom of frame”
  • RastPort building for the lower part of the screen for using AmigaOS graphic routines like text drawing

This is a very shallow interface! And these also bring in a lot of dependencies. This makes unit testing difficult, and would make future reuse challenging as well. I ended up splitting out some of the Copperlist handling and the palette definitions to separate modules. The palettes are currently hardcoded via INCBIN, but they’ll soon enough be loaded dynamically for each level, so they need to be in a separate place anyway.

This allowed me to write some very specific tests for the following:

  • ensuring a palette fade structure could be built for all three palettes: the foreground, background, and sprite palettes. This structure is essentially a multidimensional array built to be easily iterated in assembler.
  • ensuring that I can pick an index in that multidimensional array and set the game screen Copperlist colors from that array data correctly
  • walking through the “level starting up” logic, which includes:
    • prepping a bunch of state and setting default copperlist colors
    • pausing for specific numbers of frames in between palette changes or when about to start the game

Mocking Amiga system libraries in unit tests

Anthropomorphic female squirrel in front of a bulleting board with cards with numbers

Testing that the pauses were working correctly was an additional challenge. I actually built out the ability to mock Library Vector Offset (LVO) routines in libraries so that I can call WaitTOF() directly in the code and not have to deal with:

  • some hacky solution in the assembler code to allow mocking
  • the inability to spy on real library function calls w/o patching the library itself
  • undesired side effects in unit tests like having real frame delays

I covered Library Vector Offsets a little bit in my video about revisiting the AMOS Pro BSDSocket code I wrote 20+ years ago, but never went into precise detail as to what they are. Essentially, they are a list of JMP instructions that live at negative offsets from the library’s base that you get from OpenLibrary(). The stuff actually at the library base varies – for the Graphics library, for example, it includes links to interrupts, fonts, and other data you’d want for OS-compliant graphics routines.

Using an LVO in assembler looks like this. There may be some sugar around the JSR call and how you get the LVO, but it all turns into this eventually:

_LVOWaitTOF EQU -270

  MOVE.L _GfxBase,A6
  JSR _LVOWaitTOF(A6)
  ; the next instruction

A6 is the address register that AmigaOS always uses for LVO-based calls. There’s no technical reason, just convention. JSR is “Jump to Subroutine”, which pushes the memory location of the next instruction onto the stack and changes the code executation location to the provided address. In this case, that address is “whatever is in A6 minus 270”. JSR expects that you’ll eventually RTS, or “Return from Subroutine”, which involves changing the code execution location to the value that’s popped off the stack.

What’s at “whatever is in A6 minus 270”? It’s this:

  JMP <place where the code actually lives>

JMP is “Jump to Location” and it doesn’t expect that you RTS. However, the code that you do jump to does RTS, which means you’ll end up back where you left off in you own code anyway.

LVOs are always 6 bytes away from each other. Why six bytes? Because that’s how many bytes it takes to hold a JMP instruction:

4EF9 FFFF FFFF ; 4EF9 == JMP, FFFF FFFF == 32-bit (4 byte) memory address

Check out the chart here for more opcode details: https://info.sonicretro.org/SCHG:68000_ASM-to-Hex_Code_Reference

This approach means you can change the contents of a library function to your heart’s content and not have to worry about updating function calls when you release a new version. The address of the JSR may change, but the LVO won’t (and can’t!).

Library bases in C and Assembler

_GfxBase is another Amiga library convention. C compilers like SAS/C can automatically open and close libraries for you, if they have the right information. When they do this, they normally place the address of the base of the library in a global variable. The name of that global variable comes from the library’s fd file, in the ##base field. That global variable is then used to JSR to the correct LVO and put the function’s parameter values in the correct registers. Look up pragma libcall in the SAS/C Development System Library Reference for details on how that works.

On the C side, if you do nothing but use Graphics library routines, you get that global _GfxBase variable (seen as GfxBase-no-underscore in C because of how variable and functions are made public in C code). With the right included file setup, your assembler routines that use _GfxBase can use either ones your assembler programs open, say in a system.asm file that opens and closes libraries and has that public variable, or the one that just happens to come along with your C unit tests.

So to get mocked Amiga library functions, I:

  • create a block of memory big enough to cover the size of the largest LVO call I’m going to make
  • place a JMP <address> instruction at each point from the end for each LVO, with the address being the pointer to a mock function in my C test suite
  • set the library base to the end of that memory block
  • call the library like I normally would in assembler or C

This means that my assembler code under test operates as expected, but now the calls to WaitTOF() are logged and I can treat them as you would any other mock in any other testing framework.

Bun Runner Devlog -- 2024-12-22

December 22, 2024

I decided to work on the palette manager, and now I can fade between arbitrary palettes of colors:

Four colored blocks fading between different colors

The fading algorithm is a slightly tweaked implementation of Bresenham’s line algorithm. I know I built paletete fading code back in the AMOS days, very likely using a ratio approach like this:

m% "start color channel value" + "color channel delta" * ("current step" / "step count") %m

The issue is arbitrary multiplications and divisions on the 68000 processor are expensive. Compare the roughly worst-case cycle time cost of MULU and DIVU to MOVE, LSL, and ADD:

  • MULU: 70 cycles
  • DIVU: 140 cycles
  • MOVE.L to/from memory: 8 cycles
  • LSL.L: 8 cycles
  • ADD.L: 12 cycles

(Values from MC68000 Instructions Timing)

If you’re efficient, you can do a lot of stuff in the time it takes one division operation to take place!

The Bresenham implementation is all fixed point with no multiplications or divisions, and it’s pretty fast. This is also an algorithm I’m sure I’ll come back to a lot in retro development, so I wanted to get good with it:

A notebook page filled with Bresenham line drawing attempts

The unit testing I alluded to in my last post helped out a lot with implementing this. Being able to write code like this to test out the results of the assembler-based algorithm made it way easier to find bugs and discover issues in my algorithm:

void T_buildColorTransition(CuTest *tc) {
  uint16_t targetMemory[7] = { 0, 0, 0, 0, 0, 0, 0 };
  struct ColorTransition ct = {
    0x845,
    0xAC0,
    0,
    6
  };
  int i;

  buildColorTransition(
    targetMemory,
    &ct
  );

  CuAssertIntEquals(tc, 0x845, targetMemory[0]);
  CuAssertIntEquals(tc, 0x864, targetMemory[1]);
  CuAssertIntEquals(tc, 0x983, targetMemory[2]);
  CuAssertIntEquals(tc, 0x992, targetMemory[3]);
  CuAssertIntEquals(tc, 0xab1, targetMemory[4]);
  CuAssertIntEquals(tc, 0xac0, targetMemory[5]);
  CuAssertIntEquals(tc, 0, targetMemory[6]);
}

Additionally writing unit tests forced me to build an interface to the palette manager that was reusable. Lots of structures to manage incoming data, and well-defined inputs and outputs means that reusing this code in future programs, like the demo code above, was trivial.

I’ll be getting back to the head collision physics, and probably implementing more unit tests around that, since hand-testing the collision code requires a lot of setup and me jumping really precisely. Tests should speed up that feedback loop a lot.

Bun Runner Devlog -- 2024-12-14

December 14, 2024

All three collision types are now working! (well, mostly working):

Game footage of rabbit tripping, hitting a wall, and hitting their head on a wall

I need to do something about that last one, when you are going to go over an edge. I may just have to extend the physics engine a bit more to make that look good. Thankfully, I have a new tool in my retro development toolkit to make that a lot easier to implement.

I have unit tests with CuTest working with assembler code, so I can write tests for some of the more complicated data transformations (like collision physics) rather than using slow and error-prone hand testing:

Console window showing assembler and C unit test code, as well as test runner results in an emulated Amiga

As I describe on my wiki page about the subject, you have to set the stack up to around 30000 bytes on the Amiga for the test to run. You also have to include the math library. Get those in place, and you can easily run tests against any symbols exported from your assembler code.

Next is fixing that head konk collision ground slide, then implementing a palette manager which can set Copperlist palettes as well as perform transitions between two palette sets, to allow for color fades in and out and other effects.