Skip to content

Releases: qguv/undercooked

Squashing bugs, linking objects, building faster

30 Jul 23:27
Compare
Choose a tag to compare

Play this version directly in your browser

This release fixes some bugs, changes some internals, and totally replaces the build system. There's no new gameplay this time, but the structural changes from this round are necessary to keep things organized as the project gets larger.

Bugs squashed

Garbage tiles

animation showing two gameboy screenshots of kitchen. first frame shows random tiles loaded on the left side of the screen (incorrect), while the second shows the same region black (correct).

Tiles were previously loading in the blank spaces off the map to the left and right. You can see this in this gameplay quote, or you can reproduce this yourself if you play the last release: walk left until you hit the counter, walk right until you hit the other counter, then left again, then right again, then left again. You'll see some garbage tiles loaded. This was such a tricky off-by-one error that I didn't trust myself not to make it again, so I decided to change the way that the loaded parts of the map are represented (now as a signed byte for each horizontal side) so some extra accounting could be eliminated (map_oob, MAP_OOB_LEFT, and MAP_OOB_RIGHT).

Transparent white sprites

animation showing two gameboy screenshots of kitchen with a cat character with white patches. first frame shows the patches transparent (incorrect), while the second shows them opaque (correct).

Sprites with white patches started showing up as transparent shortly after I reworked the build system. I was convinced the issue was related to the way the file was being loaded into memory, but this wasn't true. It turns out I had bypassed a previous image processing step which replaces white pixels, which rgbgfx sees as transparency, with very very light grey pixels, which rgbgfx rounds up to white. The fix was simple, though hard to find.

Animation start/stop

Last but not least, the player character can now stop its walking animation when not moving. Amazing! This sets things up nicely for directional movement, which I hope to implement in the next release.

Internal changes

Linker

The linker is now responsible for bringing all the snippets of code together into the final ROM. Previously, code snippets were all being included into in one big assembly file which was just handed to the assembler, and then on to the linker. Now there are many more, smaller object file targets which are all linked together. If this project were much bigger, this would save some time otherwise spent unnecessarily re-assembling sources that haven't changed, but since this project is so small, this isn't a real consideration. Instead, as available ROM space eventually grows thinner, this will allow better use of the memory map and prepare the migration to banked ROM. Splitting into sections also allows the use of LOAD commands, which simplify loading code into RAM (necessary for DMA and for tricky inner loops that use self-modifying code).

Linker parser spec

Kaitai web IDE parsing main.o

To test and debug the object files passed to the linker, I wrote a kaitai parser spec for the rgbds(5) linker file format. Kaitai is an open-source parser generator, taking in a specification that describes a binary file format and producing code which can parse that format. One cool thing about kaitai is that you can use their Web IDE to play around with the parser and inspect object files without having to set anything up locally.

To try it out:

  1. copy the parser spec I wrote (rgblink.ksy) into the Kaitai web IDE
  2. unzip this rgbds object file and drag the resulting main.o to the kaitai browser tab
  3. poke through the "object tree" below the spec to browse the parsed contents of main.o

Kaitai can even produce graphs showing the structure of the rgblink object file format:

kaitai-generated graphviz graph showing format of rgblink object files

Build system

cropped graph showing build dependencies, jobs, inputs, and outputs

The ninja build system is now building the ROM. This open-source build system is focused on correctness and speed rather than feature completeness, and the build specification it uses is intended to be generated by a script written for the needs of the specific project. In the case of undercooked, it's a simple jinja2 template that becomes a ninja build file after some simple globbing. I've tried to avoid code generation in this project, but ninja's two step build system makes for very fast builds, easy multi-output recipes, easy dynamic dependencies, all without having to hack around the glaring limitations of something like GNU Make. As a bonus, it can spits out pretty build graphs with ninja -t graph in dotviz format (click the cropped image above to see the uncropped version).

RGBDS syntax changes

RGBDS, the open-source gameboy assembler/linker/formatting toolchain we use, pushed a surprise update which changed the syntax of labels: namely, it's a little more strict now in its v0.5.1 release than it was in v0.4.0. This used to be valid:

RUN_DMA_HRAM_SRC
        ldh [c],a

but now a colon is required after the labels:

RUN_DMA_HRAM_SRC:
        ldh [c],a

That's everything that's new in this release. Stay safe, and keep your eyes open for another undercooked update Soon™

PPU timing fixes

04 Jul 15:31
Compare
Choose a tag to compare
PPU timing fixes Pre-release
Pre-release

Play this version directly in your browser

The previous version was broken in some emulators because the game isn't sensitive to the timing of the Pixel Processing Unit (PPU), which is responsible for sending the pixels to the screen. When the LCD is on, the Gameboy PPU is eternally cycling between four different modes:

  1. hblank, a short period between drawing a horizontal line of pixels
  2. vblank, a very long period after the last horizontal line of pixels is drawn
  3. searching objects, a very short period before drawing a line where the PPU is reading sprite RAM (OAM RAM). The CPU can't access sprite RAM when the PPU is in this mode.
  4. drawing, a short period where pixels are being drawn to the screen. The CPU can't access sprite RAM or tile RAM when the PPU is in this mode. The Gameboy Color (CGB) can't access palette data here either.

PPU modes diagram

All the functions of the game were running from the VBLANK interrupt alone; the main loop was simply spinning. This caused several problems:

  1. Operations necessary to write sprites to the Gameboy sprite memory (OAM) trigger the Gameboy Sprite RAM bug, which is a bug in the physical hardware of the Gameboy.
  2. Unfortuitous timing (PPU in mode 2) causes sprite writes to fail. On hardware and bgb this only happens occasionally, but these sort of direct OAM writes aren't supported at all by wasmBoy, the emulator that's used for the undercooked in-browser demo links.
  3. There's inconsistent behavior when the duration of undercooked game and engine logic exceeds the vblank period.

To fix this, several pieces need to be moved around:

  1. All writes to the Gameboy sprite RAM (OAM) are instead directed at a buffer table at $c000, the bottom of WRAM bank 0. Because this region is (almost) always accessible, writes here will (almost) always succeed.
  2. All game and engine logic is moved from the vblank interrupt handler to the main loop.
  3. A 5-byte snippet of code is written to $ffa0, the bottom of HRAM. When this region is called, an accelerated direct memory access (DMA) routine in hardware is activated to copy the OAM buffer table at $c000 into the real OAM RAM. The HRAM subroutine then waits the appropriate amount of time to allow the hardware routine to complete, then returns to the caller. This subroutine must live in HRAM because the CPU can only access HRAM while the hardware DMA routine is active. My sincere thanks to the maintainers of the constantly updated gbdev pandocs for their detailed description of OAM DMA.
  4. The vblank interrupt handler is replaced with register save/restore and a call to the aforementioned OAM DMA subroutine in HRAM. This completes the fix: if we can treat the OAM buffer table at $c000 as the "real" OAM RAM (since we can trust that changes to this region will propagate at the next vblank), then we will never hit the sprite RAM bug, since $c000 is accessible regardless of mode, and we'll never have to edit OAM RAM directly.

You may be thinking:

Whew! That's a lot of core structural changes! So does the game look or behave any differently?

Absolutely not. Aside from slightly better compatibility with naïve emulators and a somewhat lower chance of coincidental sprite errors, it looks pixel-for-pixel identical to the previous version. This is the joy of programming directly in assembly~ "yay"

Gradual VRAM loading

27 May 23:07
Compare
Choose a tag to compare
Gradual VRAM loading Pre-release
Pre-release

Play this version directly in your browser

This version introduces gradual loading of levels that are wider than the Gameboy's VRAM! In the previous version, the level was limited to what would fit in VRAM, which meant you could walk over the edge of the world as it wrapped around. With this version, you can now walk to the end of the level, even though the level is larger then what would fit in memory. The level will continue to load as the Gameboy's display wraps around the edge of its available VRAM.

In the gif below, the left side shows the Gameboy screen, and the right side shows what's actually loaded in the Gameboy VRAM. The moving window on the right represents what the screen is currently displaying. Tiles are loaded from the map into VRAM just before the screen moves to show them.

1590620220_recording

In this release I also got a better handle on how subroutine calling works using unconditional calls (call lbl) and returns (ret), conditional calls (call cc,lbl) and returns (ret cc), and tail-call optimizations using conditional (jp lbl) and unconditional (jp cc,lbl) absolute jumps.

I had some trouble with rgbasm because I was using relative jumps with labels (jr lbl) as a micro-optimization for absolute jumps with labels (jp lbl). If more code is added between the jump and the label being jumped to, such that the label is over one register width (0xff) away from the jump, rgbasm crashes with a syntax error and a less-than-helpful explanation text. Maybe I should submit a patch…

In any case, all relative jumps are replaced with absolute jumps for stability as new code is being written. Maybe I can add an optimization step in the build pipeline that replaces these with relative jumps when possible, repeatedly reassembling to check whether it works. I've found some other easy machine optimizations on the way as well—maybe I should just write an optimizer for rgbds…

It's nice to be writing Gameboy assembly again! I think I'll work on the player character next.

Bonus: I wrote an archlinux package for bgb so that it behaves more like a native binary.

Animation engine, .gif processing pipeline, play as a cat

02 May 23:21
Compare
Choose a tag to compare

Play this version directly in your browser

It might not look like a whole lot of the gameplay has changed, but this is a huge release in sheer hours spent. There are two major features: a new animation engine and the beginnings of a .gif processing pipeline. I also hacked together sprites that use white as a foreground color. Oh yeah, and you can now play as a cat! Thanks as always to Rachel for the lovely art.

screenshot of the new version, showing a pixel cat standing in a pixel kitchen

Animation engine

The Gameboy's sprite memory (OAM) stores x and y position, the currently displayed sprite index, and some flags regarding mirrored sprites and palettes. There's no information on animation frames, speed, or whether a sprite moves with the background or keeps its relative position on the screen. So to animate the sprites, we have to keep track of all this information somewhere. The animation engine is an attempt to pack it into a single place in working RAM so these parameters can be modified live from anywhere in code.

This has several nice consequences, not least of which is that it's much easier to define new sprites in code; you simply add your definitions to the sprite meta-table, and the engine will handle the initialization and frame animation. Final binary asset includes (e.g. 2bpp images) could be further improved.

The data used in the animation engine comes from the sprite metadata table (SMT) which has a ROM and a RAM component. The ROM component is defined using a macro:

SMT_ROM:
	Sprite lstar_sprite,SMTF_ACTIVE|SMTF_ANIMATED|SMTF_WORLD_FIXED,StarBeginIndex,$5d,$2e,0,8,2,0
	Sprite rstar_sprite,SMTF_ACTIVE|SMTF_ANIMATED|SMTF_WORLD_FIXED,StarBeginIndex,$6d,$2e,OAMF_XFLIP,8,2,4
	Sprite playerHL_sprite,SMTF_ACTIVE|SMTF_SCREEN_FIXED,SadcatBeginIndex+0,$50,$4e,OAMF_PAL1,0,0,0 ; head

On boot, the table is copied into memory: some parameters into OAM, others into the (much smaller) RAM component of the SMT. The vblank interrupt code reads the RAM SMT every ~16.75 ms and updates the animations accordingly. This animates sprites and moves them around the screen to match the movement of the background tiles as the viewport scrolls.

.gif processing pipeline

Rachel likes to make pixel art animations in .gif files. This makes them very easy to preview, but it can make them hard to load. After reading through the imagemagick manual, I came up with some additions to the Makefile that should make this possible.

White sprites

The Gameboy screen can produce four colors:

  • very light green / "white" / pixel off
  • light green / "light gray" / pixel 30% on
  • darkish green / "dark gray" / pixel 70% on
  • black / pixel 100% on

In sprites and background tiles, though, the Gameboy doesn't have a concept of color. It stores each pixel with a two-bit associated value: 00, 01, 10, or 11. You then apply a palette to select the behavior of each of these values in the sprite.

All this is dandy, except that sprites reserve the value 00 to represent transparency. The consequence is that, barring some crazy timing-precise hacks, each sprite can only contain three colors. We can choose which three colors they are, but we're still limited to just three.

Usually, we rely on a program called rgbgfx (which comes with the assembler toolchain we're using) to turn our computer-friendly .png files into Gameboy-friendly raw .2bpp image data for sprites and background tiles. The trouble is that rgbgfx always treats white pixels as 00, just as if they were transparent.

The solution is to fiddle with the picture beforehand using imagemagick so that the image is essentially inverted. As you can see in the link, this is not exactly an elegant Make recipe, but it works for now.

Main character

You can now play as a cat! This is the first frame of sadcat.gif, which is being successfully loaded into Gameboy tile memory! This cat is sad, but I'm not!

a sad-looking pixel cat, sighing

Next up: star̀ing͞ ͜in͟to͟ t͌̍̆̃ͩ̀h̗͉̰̤̺ͧẻ̮̃́̚ ̗͙v̈̇ͅö̳̳̩̳́̊ͩͥi̛̩̳͈͖͇͂̈̋͒͂͑ͅd̵͖ͭ͊.͚̙

Character, movement, collision detection

22 Apr 20:43
Compare
Choose a tag to compare

Play this version directly in your browser

To be clear: the fellow on the screen is not the final player sprite, but he does collide appropriately with objects in the environment, and refuses to jump off the ledge! That's a start!

The lovely Rachel is working on some object sprites and a proper character! Stay tuned!

Next up: a new animation engine.

a player character made up of a refrigerator and a stovetop stands in a kitchen with a black edge at the bottom

Background loaded

21 Apr 20:05
Compare
Choose a tag to compare
Background loaded Pre-release
Pre-release

Play this version directly in your browser

I got some art from the amazing courtesy the amazing Rachel! I guess that means I have to start writing some real code!

I've loaded the background tile pixel data into RAM, generated a tilemap and loaded that into RAM, and threw a couple animating sprites in there too! There's some music in there as a proof-of-concept.

picture of it running on hardware

emulator screenshot

vram debugger screenshot