stardance has been extended another month! the new deadline is october 31 :)

You are browsing as a guest. Sign up (or log in) to start making projects!

GBWeb

  • 11 Devlogs
  • 59 Total hours

A Gameboy emulator written in C, localised entirely in your web browser (using WebAssembly / Emscripten)

Open comments for this post

8h 11m 20s logged

Devlog 11 - Input, Interrupts, and (semi-functional) Sprites

  • The next thing I added was joypad input. I had already set up a basic keyboard key checker with SDL events, so I had to connect it to the Gameboy. The joypad in the Gameboy writes to address 0xFF00. The Gameboy uses this as basically a 2x4 matrix. When bit 4 is set, the d-pad buttons are readable, and when bit 5 is set, the A, B, select, and start buttons are readable. I based my implementation of this off of jsGB again, which means I basically just made an array of 2 bytes; one representing the first row of the matrix, and another representing the second. Then when a button is pressed down, I set the corresponding bit to 0, and when it is released, set it back to 1. Then when the Gameboy tries to write to the register, I instead set a column variable, and when it tries to read it, I check the current column and return the correct row.
  • Then, I tried to implement some basic sprite rendering. You can see this in a few of the screenshots below, although my implementation is currently broken. It (should) work like the background renderer, only instead checking each of the 40 sprites in the OAM section of memory, and seeing if they are part of the current line. However currently, I forgot to implement one of the ways to copy sprite data to OAM, meaning that the rendering only actually works in some specific test ROMs, and not Tetris at the moment.
  • Then, I implemented some of the CPU interrupts, namely the VBLANK interrupt, which is the only one Tetris needs to boot. Interrupts have a register (0xFFFF) to enable each one individually, which each bit corresponding to an interrupt. Then, if an interrupt is enabled, when a source triggers it, the corresponding interrupt flag in 0xFF0F is set. On the next CPU step, it checks the interrupt flags, and if one is set, it then calls the specific interrupt handler for that interrupt. Currently, the only fully implemented one is the VBLANK interrupt, which is simply called when the PPU enters the VBLANK mode. This allows Tetris to boot.
  • Unfortunately, I spent a lot of the time in the devlog trying to actually get Tetris in game, as although after implementing interrupts, I could get through the menus, it just spat out a garbled screen when I actually tried to play the game or even watch the title screen demo. At first, I thought this was a rendering bug, so I spend a lot of time scrutinising the PPU code. I used the dmg-acid2 PPU test to try to troubleshoot this (you can see the current, still inaccurate result below), and although I did find out that my placeholder halt code was causing some issues, it didn’t manage to solve the problem. I found a reddit thread that seemed to have the same issue, and a frustratingly deleted comment that seemed to have a solution but obviously I couldn’t use that. However, I managed to find an archiving tool that showed me what the comment was; Tetris incorrectly writes to some area of the ROM, which causes a crash. All I had to do to fix this was block writes to the ROM area in memory, and it finally worked, although the sprites didn’t work as mentioned previously.
  • Next devlog, I want to fix sprite rendering, hopefully get the PPU more accurate, and implement some of the timer functions, as Tetris uses those to get random blocks, and without them, all the block are squares.
0
0
12
Open comments for this post

6h 40m 2s logged

Devlog 10 - (basically) Finishing the CPU

  • After the last devlog, I did some cleaning up with the graphics, which included fixing the last scanline not being rendered, and also adding a screen before the ROM is loaded, telling the user which key to press to load the ROM.
  • I also set up some functions to allow easier access to the I/O registers related to the PPU, such as the x and y scroll and the control flags.
  • Afterwards, I felt a bit burnt out of the PPU, so I went back to finish off most of the CPU.
  • This started with implementing the CFL, CPL, CF, CPL, SCF, SLA, SRA, RL, RLA, RLC, RRC, RLCA, and RRCA instructions, most of which are bit rotations, so once I had a few implemented, it got easier to add the rest. This allowed me to pass CPU test ROM 9.
  • Then, I implemented the rest of the CB prefix instructions, which were all bit related; BIT, RES and SET, which allowed me to pass ROM 10
  • After those, I implemented some stack pointer related instructions; ADD SP,d and LD HL SP+d, which then allowed me to pass ROM 3
  • Finally, I added DAA. This one is a bit more unique, as it is designed to convert the result of an add or subtract with binary coded decimal numbers, into a binary coded decimal number. This gets a bit complicated, but I used these resources (Demystifying the GameBoy/SM83’s DAA Instruction and the RGBDS assembler/linker instruction documentation) to implement this properly, allowing me to also pass test ROM 1
  • With a semi-functioning PPU, and some fixes, I was also able to boot into the copyright screen of Tetris for the first time, although I couldn’t get any further without implementing interrupts. The image below shows that, and my emulator passing all of the previously mentioned test ROMs. The only test ROM I haven’t passed yet is the 2nd one, which also requires interrupts.
0
0
8
Open comments for this post

4h 2m 24s logged

Devlog 9 - Basic graphics (finally)

  • I’ve finally gotten some basic picture processing unit (PPU) functionality implemented into the emulator, which will hopefully make debugging a lot easier going forward.
  • Firstly, to implement this, I had to move the timing logic out of cpu.c and into cycles.c. This was so that, when the loop calls a CPU step, it also calls a PPU step at the same time. I also refactored some of the cycle functions and removed the ones that were no longer being used
  • Then, I implemented the timing code for the PPU. This part and the actual rendering were mostly based on this tutorial series that shows you how to make a GB emulator in javascript, which I based my C code on. Basically, the PPU runs on the clock as the CPU, and a ‘dot’ on the PPU is equal to a T-cycle on the CPU. The PPU renders progressively, like a CRT television, and so also has modes to move the nonexistent electron beam down and back to the start of a scanline before it continutes rendering, known as VBLANK and HBLANK, (vertical and horizontal blank). The Pan Docs were also quite helpful for understanding the basic functionality, although the tutorial was useful to understand the programmatic side. The PPU also has 2 other modes, used for scanning the Object Attribute Memory (OAM) and actually drawing, and all 4 modes have a precise amount of dots that they run for.
  • In terms of actual rendering, the Gameboy doesn’t have a full framebuffer, and instead uses different tiles to create a full image. I implemented a function that, when the CPU writes to VRAM, also updates the data in a separate tileset variables, for easier access in the render pipeline. Each tile is 16 bytes, with 2 bits per pixel colour depth, and 8 x 8 pixels per tile. As the Gameboy renders progressively, I then implemented the function to draw a scanline to the screen, based on the tile data in the background layer. The afformentioned tutorial series helped a lot here, as the tiles need to have a specific offset and mapping applied which would be quite hard to figure out on my own.
  • Then, that function calls a draw pixel function for each each of the 160 pixels in a scanline, which writes the corresponding colour from the palette to an SDL_Surface as the framebuffer. Once the frame is finished, the surface is converted into a texture, which is then scaled to fit the screen size and rendered, with another frame being made afterwards. Below you can see the screen of the previously mentioned blargg’s test ROM 7.
1
0
22
Open comments for this post

6h 31m 22s logged

Devlog 8 - Organisation, and timing, again

  • Ok, so this didn’t exactly go how I wanted (again), but most of this devlog time was spent fixing the past devlog, and organising code in a better way
  • Firstly: I realised that before, my timing code checked based on M-cycles, rather than T-cycles. There’s a specific reason for the distinction, but for now all you need to know is that there’s 4 T-cycles in an M-cycle, so my code was targeting a 4x faster speed. I changed this to be correct, but changed it again later so it doesn’t really matter.
  • Then, I cleaned up a lot of cpu.c, changing functions to return values directly rather than using a local variable first, pre-calculating some variables, et cetera. This was an attempt to speed up the CPU, as I was having a bit of trouble meeting the target speed with debug builds (mentioned in devlog 7), but again, this was a bit pointless because I changed the way the timing worked later. While doing this, I also replaced all of the numerical types with their stdint.h counterparts; for example, an unsigned char became a Uint8 (SDL has macros for the stdint types, so they have a slight naming difference from standard C in my code). This was mainly done for specificity and some Hail Mary for performance (although fruitless), and should make the code at least a bit more portable and easy to maintain.
  • I also implemented a couple of new instructions; RST and SWAP. RST effectively calls from a set of pre-specified addresses , and SWAP just swaps the upper and lower 8 bits in a byte, so these weren’t too hard. I also had to fix some bugs with the flags register´s value after an SBC A instruction, which admittedly did take me a while to fix because I basically banged my head against the wall to fix it.
  • The main change in this devlog was switching the timing system (again). Previously, the CPU ran one instruction for each loop of the SDL_AppIterate function. After reading the documentation and realising that this function is supposed to run each frame, I realised that I was causing all of my speed issues by only doing one cycle per iteration. Instead, the code now does as many instructions as it can until it has run enough T-cycles for a full frame to render (70224). It then renders (to be implemented), and moves on to the next iteration. This is way more performant, and has the added benefit of being more cross-platform, as SDL specifies that sometimes AppIterate may only run at the refresh rate on some devices. Tbh, this took me a while, maybe too long, to conceptualise and implement, and basically made all of the time on the other timing system wasted, but in the end it was worth it for a much better system. You can see the uncapped FPS (in a debug build mind you) here, although when actually playing games, the framerate will be locked to around 60 FPS.
  • Finally, there was a few more miscellaneous changes: copying a ROM file into memory uses memcpy instead of a loop, which may or may not be faster; memory is now a union of smaller arrays of memory banks, for each function, such as work ram (WRAM), high ram (HRAM), ROM banks 0-n and etc; and lastly, some register code has been moved out of cpu.c for organisation.
0
0
9
Open comments for this post

5h 0m 13s logged

Devlog 7 - Timing

  • The next main thing I implemented were some more instructions - namely SRL, RR, RRA and ADD HL. These weren’t too hard, as the first 3 are similar to regular bit shifts, and ADD HL is just adding. While I was working on these, I also fixed some bugs with the existing instructions
  • This allowed me to finally pass the cpu_instrs 6 test ROM, which although not really a massive goal, did help to signify the progress I had made. I then moved onto the 7th test, for which I implemented some load functions for the stack pointer register
  • The biggest improvement of this devlog was getting everything timing correctly. The Gameboy is supposed to run at 4.19 MHz and 59.7 FPS, but at the time, the emulator was just running as fast as possible.
  • To solve this, I first made each instruction call a function to add the amount of cycles it takes to a unwaitedCycles variable.
  • Then, I saved the current time in nanoseconds since the program started (provided by the SDL_GetTicksNS() function) into a variable at the start of an instruction’s execution. After the instruction is executed, a function is then called that finds the expected execution time the by doing the amount of cycles multiplied by the period of 4.19MHz (about 238 nanoseconds). This expected time is then subtracted from the actual time, which is the start time stored previously subtracted from the current time. The program is then delayed by the result, which should keep the CPU at around 4.19MHz, unless it takes too long.
  • I did have some concerns with my emulator’s performance, as when running as a debug build, it wasn’t running nearly fast enough to reach 4.19MHz, but after compiling as a release build with optimisation, it was much faster and ran under the required time. However, I’m not sure how consistent this will be between devices, so I may have to optimise later
  • Finally, I added a similar function to limit the FPS to around 60 (technically 59.7), which just checks the time since the last frame was rendered, and if it hasn’t been long enough, it continues CPU execution and doesn’t render. You can see the frame limit working in the screenshot, where the frametime is a consistent 16.8 ms and the FPS is locked to 60, as opposed to before where it was unlimited. (Ignore the text, it doesn’t change yet to show a ROM has been loaded)
0
0
16
Open comments for this post

5h 29m 35s logged

Devlog 6 - An unfortunate amount of debugging

  • This devlog is probably going to be shorter than the other, simply because I haven’t done as much as I hoped
  • The main reason for this is that I got completely stuck on a bug, and wasted so much time trying to find it. I first noticed this bug when I got this error message (screenshot) in gameboy-doctor, with a mismatch with a load from memory not being the right value. This got me so stuck, because I didn’t know where to find when the memory was supposed to be written to, so I was basically blindly guessing what was wrong with any previous loads or register writes. Eventually, I caved and asked for help online, and I got my answer after asking on the EmuDev subreddit, where I found out it was an issue with my implementation of the LD (FF00+u8), A instructions, where I wrote
    LD_n16_n8((0xFF00 + address + 1), registers.a);
    instead of the correct
    LD_n16_n8((0xFF00 + ReadMemory(address + 1)), registers.a);
    so basically any writes to memory using that function were in the complete wrong location. Stupid mistake on my part, and I wasted way too much time on it.
  • I also did implement some basic instructions, like EI and RETI. EI simply enables the emi register, and RETI basically just does that, then calls RET to return from a subroutine.
  • In addition, I also restructured quite a lot of the code, as too much was piling up in the cpu.c file, and I also renamed quite a few functions to follow a Component_Function syntax, so it was easier to tell where they were located in the code
0
0
22
Open comments for this post

6h 32m 28s logged

Devlog 5 - Adding instructions, the stack and lots of debugging

  • After the last devlog, the first think I did was implement some more instructions, namely the jump instructions, and the increment and decrement functions. Luckily these were pretty simple; all the jumps do are set the program counter (effectively the next instruction) to a specified value, and the increment and decrement are pretty self explanatory, increasing or decreasing a register or memory byte by 1;
  • After this, I started implementing the stack. The stack is a section of memory that is ordered, with values being written to it (pushed) and read off of it (popped). You must read the “top” value, meaning the most recently pushed, which is then removed and the next highest is the next available to read. This wasn’t too hard, although I did have to introduce a new register, the stack pointer (sp). While I was at it, I also implemented load functions, which load data into a specified memory byte or register.
  • After realising it would cause issues down the line, I walked back my previous decision to make the flags register a struct of booleans, and instead made it a single byte with 4 bits representing the 4 flags, making it more accurate. I also made some functions to easily set or reset each flag, rather than use bitwise operations everywhere.
  • Finally, I implemented logging support, so that I could use Gameboy Doctor, a brilliant debugging script that reads a log file from your emulator with a known ROM, and compares it to the correct result. All you have to do is write a line to a log file with all the register values and some memory bytes after each instruction, and it automatically tells you where the first error is, what register or piece of memory is wrong, and even the last instruction to give you a hint as to the problem. This didn’t take much time to implement, but I did spend a considerable amount of time debugging, as previously I hadn’t actually got a way to check that my functions worked correctly (which is why this devlog has quite a few more hours on it). You can see an example of a error in the screenshot; I intentionally changed the SUB instruction to add instead because I forgot to take screenshots of the actual errors 🥀.
  • Next time, I want to implement more of the CPU, especially more complex behaviours like halting and interrupts, and also implement some of the graphics, so that the games can actually be seen visually.
0
0
15
Open comments for this post

5h 21m 18s logged

Devlog 4 - Memory mapping and instruction stepping

  • The next thing I worked on was getting a basic function set up for stepping through instructions and calling the execute function. To do this, I also had to make a program counter, which is a 16 bit register storing the address of the next instruction. Currently, the step function is pretty barebones, but it simply calls the execute function, passes it the PC as a parameter, and then sets the program counter to be the returned address. The execute function has also been modified, meaning it reads the instruction from memory (more on that later) rather than being passed it directly, and return the address plus 1 to 3 bytes, depending on the instruction that was executed.

  • The next bit I made was the Game Boy’s memory. This is simply an char array with 64K values, equivalent to 64KiB of memory. Then, any address can be accessed simply by accessing the array value at an address.

  • After working on the other parts of the CPU, I went back to working on some instructions, implementing the rest of the disassembly tables, including the condition call table, which checks the flags register before executing an instruction. I also implemented some of the instructions that require inputting an address in memory to use instead of a number or register.

  • Finally, I also worked on some quality of life features. First, I made a function to replace missing functions, which simply sends an error message with the function name, and quits with an error code.

  • Secondly, I made / edited some functions to actually move the loaded ROM file into the main GB memory rather than the program memory. I had a little trouble troubleshooting this, as I didn’t at first have a reliable way to verify that the data had been copied successfully, and after I did, I still had trouble with pointers, as I am still pretty new to C’s manual memory management (I usually just keep things in the stack, or have global variables that will persist throughout the program’s lifetime.). I also added basic functionality for loading a ROM on desktop rather than the web, but it doesn’t use the system dialog yet, only loading a rom.gb file in the executable directory. After everything was working, I added a debug message to send the ROM’s header title, showing that it actually works. You can see this in the screenshot. It sends the message twice, once when the file is loaded, and once when it’s moved to GB memory, which I did to make sure that the transfer didn’t mess up the data accidentally.

0
0
4
Open comments for this post

3h 55m 7s logged

Devlog 3 - Algorithmically parsing opcodes

  • After getting things functioning well in the browser, I wanted to move onto the actual emulator bit of the project.
  • Firstly, I looked at a bunch of examples and info, mostly compiled from gbdev.io. At first, I followed both the development blog of Cinoop and the guide DMG-01.
  • I used these guides to first write the register struct, which is basically just a struct of bytes (in char form as I’m using C), as well as the flags register, which I took inspiration from DMG-01 by making it primarily a struct of 4 bools rather than a binary value, for easier setting. I also wrote functions for converting this into binary though so that I didn’t lose compatibility.
  • Next, I used Decoding Gameboy Z80 Opcodes to write an algorithmic way to decide which instruction correlates to an opcode, instead of a large switch case (although admittedly it does have a lot of nested switch cases).
  • Finally, I added in the “alu” table instructions as the first few instructions, which also happened to be quite simple as they were basically just addition, subtraction and some bitwise operations.
  • Next, I plan to fill in the remaining instructions, add them all to the algorithm and get started on the actual control flow for the CPU
0
0
12
Open comments for this post

3h 34m 45s logged

Devlog 2 - SDL3 and File Loading

  • After setting up the project basics, I spent some time on another project, also using SDL, but a desktop app. This used SDL3 instead of SDL2, which is default for Emscripten. I decided to switch this project to SDL3 as well, since I feel more comfortable with it and it has some features like main callbacks that should help with Emscripten. Instead of using the SDL library bundled with Emscripten, I instead added the SDL3 repo as a submodule, so I should be able to make a desktop version of the program later relatively easily.
  • After switching to SDL3, I researched how to load a file from the user in the web browser into the webasm program. I found a StackOverflow thread that helped a lot, which included a link to a more developed header library, however I opted not to use this, as it was C++ based, and I wanted to remain using C. Instead I just implemented the bits I needed from the thread in C, which wasn’t too hard, although included some JS, which I’ve never used and some pointers which I made a few stupid mistakes with, causing me to think it didn’t work for a while.
  • Finally, I also added a keybind to open the file dialog, which currently is tab, and also made sure that the keys only register on the down press, and not the up as well.
0
0
7
Open comments for this post

3h 38m 31s logged

Devlog 1 - Setup and basic input

I took a while to find a toolchain that I liked, but I’ve settled on using Emscripten, and it’s built in version of SDL2. Emscripten is a library and compiler for CXX that allows direct compilation to web assembly and JavaScript, also allowing graphics and input with SDL. This makes it a lot easier for me to write the program, as I don’t really need to know as much JS or find multiple different libraries to use.

Originally I wanted to use SDL3, but I couldn’t get the input to work automatically with Emscripten, and with this being my first major C and web project, I decided to take the easier path and simply use the fully supported SDL2.

I also spent some time setting up a simple CMake system and some scripts for building and running, which took me a while to figure out, especially as I was also learning Emscripten for the first time, however it will hopefully save me time in the long run.

Just to test my implementation, I set up some keybinds for the eventual emulator to come. I just used SDL scancodes to check for the key press, and they printed the corresponding Gameboy button to the output. You can see this in the video below. For simplicity, I decided to just set keys in code, with WASD being the dpad, J being A, K being B, Return being Start and Backspace being select.

0
0
1

Delete project?

Are you sure you want to permanently delete this project? This action cannot be undone.

All devlogs, followers, and associated data will be removed.

Followers

Loading…