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

max_silly

@max_silly

Joined June 3rd, 2026

  • 44Devlogs
  • 4Projects
  • 3Ships
  • 46Votes
I'm just a silly human who likes Rust :3. Also a recovering Python user and Zig learner. I also don't know how to Δ my pfp.
Open comments for this post

2h 22m 17s logged

Third Time’s the Charm

Hello everyone! I would have devlogged sooner, but I’ve spent most of the last time between this devlog and the last on a single feature, and the rest on YouTube. Let’s dive right in!

Clock Iiiiit!

After adding MBC2 and No MBC, the natural next step was MBC3.

MBC3 is a lot like MBC1. It uses mostly the same register, many of which are direct copies or basically the same. There are only two key differences: MBC3 supports more RAM and has an RTC (real time clock). The more RAM wasn’t much to deal with, but the RTC kinda was.

I messed up a good deal of the ticking on the irst pass of writing it, and I kept forgetting how if let worked, which did end up slowing me down.

Anyway, it’s well past midnight and I need to smash my face in my pillow. My one regret is that I didn’t listen to enough Jamie Paige while devving.

0
0
37
Open comments for this post

3h 11m 37s logged

Clip It

Hello comrades! We’re back with yet another devlog! Let’s dive in so I can get back to programming!

Memory Control (Not 1984 Style)

A bit ago, I mentioned that since cartridges were physical pieces of hardware, they could have logic chips of their own controlling their own RAM, so I would have to emulate those on top of the functionality of the actual GameBoy’s hardware.

The next MBC (memory bank controller) to emulate was MBC2. MBC2 is weird. It has ROM banks as standard, but it also has 512 half-bytes of RAM built straight into the MBC chip for some reason. Those were a little weird to emulate, but altogether straightforward. I also had duplicated RAM indexing logic for both reading and writing to the cartridge RAM, which I marked todo in my code, so I had an excuse to do a quick refactor.

After handling the weird little freak that is MBC2 was implementing No MBC. You may be asking, V, what’s there to emulate if there is nothing to emulate? And to that I say, it is once again weird and silly, just how we like it. Even if a game has no MBC, it can still have some RAM; it’s just always on and has no logic for accessing it. It ended up with me having to deal with a lil extra changes to handle some things I was not anticipating.

Looks Like You’re Trying to Kill a Stick Figure

A while ago, I watched a prime video (who was also the source of the idea for this project) about the Power of 10. I’ve since thought these rules are pretty neat and can serve to make code better (with possibly the exception of rule 7). I decided that I wanted to follow rule 10 for my code, so I foolishly set all lints to max pedantic, thinking I would have some lints, but I ENDED UP HAVING HUNDREDS OF MOSTLY USELESS LINTS, many of which ended up being me using as for casting causing less readable behaviour. Some of them were useful, such as me allocating MASSIVE arrays on the stack, even if they were boxed. Turns out Box::new([0; 0x20000]) creates the array on the stack first before memcopying to the heap in unoptimised builds, which is completely unnacceptable. Also, may of my other arrays were just straight up massive and unboxed, like the framebuffer or bus memory flat array.

Anyways, after spending the bulk of this devlog’s time dealing with that, I gave up knowing the rest of the lints were harmless and pointless and clogging up important lints.

In other news, despite listening to Jamie Paige for many hours this weekend, I still ended up needing my fren to tell me about it’s new song with Vane Lily and Yuma. I also showed them to Developer theme and it brought them some joy. I’m also very happy I got the thermometer question right, especially since I have an infrared thermometer on my desk and I had to find the correct emissivity setting to use on milk yesterday since my dad was making yoghurt. Anyway, I’ll end this before Grammarly shows me any more lints for this devlog.

0
0
39
Open comments for this post

5h 32m 38s logged

Can It Tetris?

I say hello to you again, my near and dear viewers, as I begin this devlog with a frustratingly long amount of time logged. Let’s dive in so I can put the last few days behind me.

DMing the Memory

Last time I menioned that there was a feature I needed to add that allowed the cpu to write a byte to a register in memory to trigger a transfer of data from the address specified by the written byte to the OAM (object attribute memory — sprites and stuff; this transfer is the DMA OAM tranfer) because that was the main way stuff was written to oam b/c otherwise it would be too slow.
Originally, I wrote a doubly recursive function kinda thing b/c it called read_byte() when it itself was called indirectly by read_byte() and it also called itself, but I scrapped it due to unguarenteed tail recursion and the slowness of repeatedly matching the section of memory being written to/read from, so I just used copy_within(). This also somehow fixed my input issues, so idk.

Double Bug Whammy

This is the part of the last few days that has been a slog and a horror. After adding the DMA OAM transfer, I tried running Tetris, and it worked…until I got into gameplay. The sprite showing the selected player amount on the start screen was working and it would respond to user input. However, when it hit actual gameplay, it wouldn’t render the tetromino sprites until they were baked into the background as part of setting in place. To try and diagnose the problem, I ran blarrg’s big cpu_instrs.gb test ROM, and it stopped rendering frames in the middle of the 3rd test, the standalone ROM for which had been working without a flaw. Testing in mGBA through RetroArch (I don’t have mGBA installed standalone on this device), I found that the test takes a second to run, but doesn’t cause the whole emulator to hang. Also, since new frames weren’t being completed, it wouldn’t poll for input since it only polls on a frame completion, which meant I had to close the terminal session to quit the emulator. I spent hours running tests just to find that it was getting hit by a STOP instruction it couldn’t escape. I checked Pan Docs and found there had been some unimplemented STOP behaviours. After implementing those and it still not working, I set STOP to only stop for a single cycle, which let the test ROM pass all the tests with no timing issues. After that, within 2 minutes, I was able to find an incorrect == (should have been !=) in my sprite attributes bit 7 sprite priority control, allowing Tetris to finally work!!!

Next up is:

  • MBC2
  • MBC3
  • MBC5
  • Saving
  • Audio
  • TUI rather than bare CLI
  • a lib.rs so others can write a frontend on top of Rusty Game Girl
  • A style guide and making my code compliant (hard to do while writing and over a codebase who’s size is at least 4.5x bigger than anything else I’ve ever worked with)
  • Unit tests

also here’s a video of me selling at Tetris b/c I wasn’t listening to Jamie Paige while playing

1
0
135
Open comments for this post

2h 1m 31s logged

RAM or ROM

Hello everyone! I’ve finally logged a normal amount of time again! Let’s dive right in!

MBC Say One!

I managed to bang out my first supported MBC (memory bank controller), a lot of it done while walking on the treadmill this morning. I had already implemented the ROM bank switching by this morning, so next I had to implement switching RAM banks, which has some pretty funky indexing.

Grace Hopper

Naturally, what does one do when they have a hopefully working MBC system, you try a rom!. I only found out tetris doesn’t use an mbc after removing my basic mbc substitute stuff that enabled me to run simple roms.
Speaking of Tetris, the screenshots are me attempting to run tetris iwth different themes (the themes work, tetris doesn’t which we’ll get to).
The themes in order

  • Wisteria
  • GBGRreen
  • Jungle
  • Developer
    (Don’t worry, there’s more themes, and you can always submit an issue or PR with a new theme)

Let’s get back to BUGSSSS!! When I tried to run tetris, it wasn’t accepting input and the arrow showing which options are selected was unpresent. These issues are the next things to resolve.

  1. I found that there was a critical error in the read methods on my button data stuct
    Here is the commit with the changes in case you are curious.

  2. I found out that there is a spot in memory you can write to to load a sprite from some address into OAM. This is the next thing I will be implementing before I try finding the next issue with input so I can at least see some pretty sprites.

Anyway, see y’all tomorrow after I hopefully get tetris working!

0
0
36
Open comments for this post

3h 17m 3s logged

You can switch the banks, switch banks, swi- swi- swi- swi- swi- — until you’re full of life

Hello everyone! I changed the name of my project directory, so I wasn’t seeing my hackatime or had the ability to write a devlog until I realised the problem, so let’s just dive right in!

Bank It

The next thing to deal with was bank switching and better behaviour for handling roms.

Cartridges are physical objects, and thus can have extra RAM and storage for the gameboy. These are controlled by MBCs (memory bank controllers). The most common ones are MBC1, MBC2, MBC3, and MBC5. For this project, for now, I am only emulating 1, 2, and 3. In the future, I will probably add more controllers once the full architecture is done for at least these three so I have an idea of hwo the skeletons should look.

The first step of bank switching was having the rom portion of the memory be read-only and writes to it do certain mbc actions, like enable ram or switch banks or switch ram. I am currently implementing the switching of banks. I need to get back to MBC1-specific flags and writes. See y’all next time!

0
0
22
Open comments for this post

7h 48m 37s logged

Human Error

Hello everyone! I got too much in the flow:3-blahaj-spinning: and in the RAGE, which is why I logged nearly 8 hours since the last devlog! Let’s get in the weeds!

Loading Bay

I added ROM loading!!!!! :3-pet-gabe: I can actually load a test ROM in now!!! Naturally, I immediately loaded one in, and that brings us to our first issue.

Speed? Issues

When I was loading in a test ROM, the text of the name of the test would peek from the bottom of the screen, then jump up and not show passed. I probably spent 2 HOURS on trying to fix this. I thought it was a speed issue, and I ended up making a ton of optimisations and added alternate screen switching.

BUT GUESS WHAT??? I zoomed out my terminal window, and there it was: Passed. I would share a screenshot, but I am currently working on changes to load_rom() that break the emulator currently (bank switching).

Don’t Stop

I mentioned a while ago that I had to emulate a bug in the hardware that caused the next instruction to occur twice if HALT is called when there is an interrupt pending and IME (interrupt master enable) is off. My implementation caused it to infinitely stall instead. So I fixed that bit, but the HALT bug test rom still wasn’t running the tests, so I found that LY couldn’t be read from memory, so fixing that fixed the thing, allowing the tests to run and FAIL. So I had to run a disassembler on the HALT bug test to find the exact mechanics of the HALT bug, then write it. Even though the values for the result were correct, it still wouldn’t pass. This was because of an oversight on my part when I was writing the interrupt handling. The IF byte (0xff0f, pending interrupts, whether or not they’re enabled) in hardware always has its highest 3 bits as 1, and I didn’t think it was necesary to implement this when I was writing interrupt handling, so I ignored it. And finally, fixing that, the HALT bug worked.

The screenshots are:
Gambatte failing the test.
mGBA passing.
RustBoy (May change to GameGirl/RustGirl for uniqueness and searchability) passing.

See y’all next time!

0
0
9
Open comments for this post

4h 58m 15s logged

You’ve Been Selected for a “Random” Screening

Hello Everyone! We are back! I don’t know how it’s already been over 4½ hours! Let’s dive straight into the progress!

They Know How to Push Each Other’s Buttons

After making constructors for the structs and refactoring the button data struct, I added a loop to handle all waiting button presses and set the data in the button data struct. Not particularly complicated, though it was fun to test something that actually did something.

Staaaaahhp

Instructions take a set amount of time, so it is necessary to have a pause, since even a TI-84 (buy the CG-50 or HP Prime G2 instead; don’t get the CG-100, it lacks plugin support, which is why I recommend the CG-50 at all) is faster. Since systems can’t pause for a single tick, it pauses frame-by-frame instead.

How to Exit Neovim

I didn’t take the time to add a break condition, so I added a check to see if the escape key had been pressed, and if it had, it quit the application.

Show Me the Money

The biggest development was the render loop. Now, there is actually a visual to see and play with.

Paint a Pretty Picture

I also felt it would be boring if there was only one colour theme, so I added an enum along with PixelValue => Colour matching so that you can pick a theme! Speaking of picking…

Staring at a Config File

Since the user has to be able to pick a theme, I felt it would be good to have config.toml support for ease of use.

0
0
4
Open comments for this post

5h 35m 6s logged

Building Buttons

Hi everyone!! It seems that time has slipped away from me yet again, so let’s dive straight into your scheduled content!

Pushy Pushy Buttons

After dealing with sprites (oh! the horror! the suffering! the despair!), it was time to deal with a new kind of beast: the joypad. Implementing joypad functionality meant it was time to make a frontend (ew) instead of just building subsystems and cobbling them together later.

This meant it was time to learn stuff about the crossterm crate and make some constructor functions.

The screenshot is my current struct for storing the button push data. It used to be 8 bools, but that was 7 bytes too many, so it’s just a u8 wrapper now.

Cross-multiply Those Terms!

Crossterm is a crate for dealing with terminal keyboard interrupt and such. The first and only thing I’ve done so far was make a wrapper with a custom destructor for Keyboard enhancement flags.

struct TerminalGuard {
    enhancement_pushed: bool,
}

impl TerminalGuard {
    fn new() -> std::io::Result<Self> {
        enable_raw_mode()?;

        let enhancement_pushed = supports_keyboard_enhancement().unwrap_or(false);
        if enhancement_pushed {
            execute!(
                stdout(),
                PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::REPORT_EVENT_TYPES)
            )?;
        }

        Ok(Self { enhancement_pushed })
    }
}

impl Drop for TerminalGuard {
    fn drop(&mut self) {
        if self.enhancement_pushed {
            let _ = execute!(stdout(), PopKeyboardEnhancementFlags);
        }
        let _ = disable_raw_mode();
    }
}

Computah, Make me a New Computah

Since it was time to write a main() fn, it meant I also had to write constructor functions for CPU and all it’s subsidiaries, since I’ve not bothered to do it before.

That’s it folks! See y’all next time!

2
0
20
Open comments for this post

2h 41m 16s logged

Crack Open a Sprite

Hello everyone! This dvelopment took longer than expected, so let’s just dive right in!

County Rogues, Bring me lOAM

After I finished the window layer, it was time to deal with sprites. Where each sprite is on the screen, which tiles to use, and sprite-specific render attributes are stored in a section of memory called OAM. 40 sprites’ data can be stored in OAM, each 4 bytes: the x position, y position, which tile(s) to use, and the attributes of that sprite (height, when to render, etc.)

Unlike the background and window, sprites have a lot more moving parts. Here’s my code for rendering the sprites to the buffer.

    fn render_sprites(&mut self) {
        let lcdc = self.read_byte(0xff40);
        if lcdc >> 1 & 0x1 == 0 {
            return;
        }
        let mut indexes = (0..40).collect::<Vec<u16>>();
        indexes.sort_unstable_by_key(|n| (self.read_byte(0xfe01 + n * 4) as i16 - 8, *n));
        let indexes: Vec<&u16> = indexes.iter().rev().collect();
        let height = if lcdc >> 2 & 0x1 == 0 { 8 } else { 16 };

        for n in indexes {
            let entry = 0xfe00 + n * 4;
            let sprite_y = self.read_byte(entry) as i16 - 16;
            let sprite_x = self.read_byte(entry + 0x1) as i16 - 8;
            let tile_index = self.read_byte(entry + 0x2);
            let attributes = self.read_byte(entry + 0x3);
            let palette = if (attributes >> 4) & 0x1 == 0 {
                0xff48
            } else {
                0xff49
            };

            for row in sprite_y.clamp(0, 144) as u8..(sprite_y + height).clamp(0, 144) as u8 {
                for column in sprite_x.clamp(0, 160) as u8..(sprite_x + 8).clamp(0, 160) as u8 {
                    if (attributes >> 7) & 0x1 == 1 && self.bit_7_check(row, column) {
                        continue;
                    }

                    let row_in_sprite = if (attributes >> 6) & 0x1 == 0 {
                        (row as i16 - sprite_y) as usize
                    } else {
                        height as usize - (row as i16 - sprite_y) as usize - 1
                    };

                    let column_in_sprite = if (attributes >> 5) & 0x1 == 0 {
                        (column as i16 - sprite_x) as usize
                    } else {
                        7 - (column as i16 - sprite_x) as usize
                    };

                    let which_tile = if row_in_sprite < 8 { (0, 0) } else { (1, 8) };

                    let pixel = self.index_to_tile_unsigned(tile_index + which_tile.0)
                        [row_in_sprite - which_tile.1][column_in_sprite];
                    if pixel == TilePixelValue::Zero {
                        continue;
                    }
                    let value = self.palette_lookup(pixel, palette);
                    self.gpu.set_pixel(row, column, value);
                }
            }
        }
    }

    fn bit_7_check(&self, row: u8, column: u8) -> bool {
        if self.window_active_for_pixel(column, row) {
            if self.window_pixel_value(column, row) == TilePixelValue::Zero {
                return false;
            }
            return true;
        }
        if self.background_pixel_value(column, row) == TilePixelValue::Zero {
            return false;
        }
        true
    }

This also excludes any boilerplate for window and background that I had to reuse. The most annoying part was dealing with ordering indexes efficiently, because the x value of the sprite is used as the method for sprite precedence over layers. I didn’t realise that sort_unstable_by_key dealt with like tuples and you could use that for precedence and tiebreaking.

Anyway, that’s all folks! see y’all next time!

0
0
6
Open comments for this post

1h 54m 14s logged

Ogres are like Onions

Hello everyone! This time, I’ve fortunately logged a normal amount of time. Let’s dive in!

Close the Window!

After the background rendering was complete, it was time to work on the next layer: the window. The window is exactly as it sounds — the layer between sprites and the background that can have ui and stuff.

The window starts at a position dictated by wx and wy (0xff4b and 0xff4a in memory), and those two dictate the top-left corner of the window layer (though wx - 7 is the actual top-left x coord). If a game wants to have a window layer that ends, it will switch off the window layer BETWEEN scanlines, which my current implementation cannot do because I render the whole frame at VBlank (after all scanlines) for simplicity, and it’s not strictly necessary.

The code implementation of the window layer was relatively similar to background:

    fn window_active_for_pixel(&self, screen_x: u8, screen_y: u8) -> bool {
        if (self.read_byte(0xff40) >> 5) & 0x1 == 0 {
            return false;
        }
        let wy = self.read_byte(0xff4a);
        let wx = self.read_byte(0xff4b);
        if screen_y >= wy && screen_x >= wx.saturating_sub(7) {
            return true;
        }
        false
    }

    fn window_pixel_value(&self, screen_x: u8, screen_y: u8) -> TilePixelValue {
        let window_y = screen_y - self.read_byte(0xff4a);
        let window_x = screen_x + 7 - self.read_byte(0xff4b);
        let window_tile_map_index = (window_y / 8) as u16 * 32 + (window_x / 8) as u16;
        let window_tile_map_base = if (self.read_byte(0xff40) >> 6) & 0x1 == 0 {
            0x9800
        } else {
            0x9c00
        };

        let tile =
            &self.index_to_tile(self.read_byte(window_tile_map_base + window_tile_map_index));
        tile[(window_y % 8) as usize][(window_x % 8) as usize]
    }

The next step is the final layer: Sprites! Sprites will be more involved and unique compared to background and window. I still can’t believe the Sharp SM83 did all this for developers. See y’all next time!

0
0
51
Open comments for this post

4h 53m 39s logged

No More GPU Crisis

Hello everyone! It seems that, yet again, time has slipped away from me! Let’s dive back in!

STATistics

When the chip swaps rendering modes, depending on whether the STAT register enables that specific interrupt, it will trigger a STAT interrupt (priority level 2). I had to set up a loop to shift modes so that each mode can easily be fully handled (see screenshot). This was just a matter of finding out how long each mode and line lasted, relatively simple.

Render Engines

After this, I had to start adding rendering features. There are three components to the rendering:

  • Background
  • Window
  • Sprites
    The first, and easiest part, was the background. Which tiles compose the background is dictated by a series of bytes after either 0x9800 or 0x9c00, which have references to tiles in memory. Pretty simple.
    fn render_to_buffer(&mut self) {
        for row in 0..144 {
            for column in 0..160 {
                self.gpu.set_pixel(
                    row,
                    column,
                    self.bgp_palette_lookup(self.pixel_value(column, row)),
                );
            }
        }
    }

    fn pixel_value(&self, screen_x: u8, screen_y: u8) -> TilePixelValue {
        let map_y = self.read_byte(0xff42).wrapping_add(screen_y);
        let map_x = self.read_byte(0xff43).wrapping_add(screen_x);
        let tile_map_index = (map_y / 8) as u16 * 32 + (map_x / 8) as u16;
        let tile_map_base = if (self.read_byte(0xff40) >> 3) & 0x1 != 0 {
            0x9c00
        } else {
            0x9800
        };

        let tile: &Tile = &self.index_to_tile(self.read_byte(tile_map_base + tile_map_index));
        tile[(map_y % 8) as usize][(map_x % 8) as usize]
    }

    fn index_to_tile(&self, index: u8) -> Tile {
        let unsigned = (self.read_byte(0xff40) >> 4) & 0x1 != 0;
        if unsigned {
            return self.gpu.tile_set[index as usize];
        }
        let signed_index = (256 + (index as i8 as i16)) as usize;
        self.gpu.tile_set[signed_index]
    }

    fn bgp_palette_lookup(&self, value: TilePixelValue) -> PixelValue {
        let index = value as u8;
        match (self.read_byte(0xff47) >> (index * 2)) & 0b11 {
            0 => PixelValue::White,
            1 => PixelValue::LightGray,
            2 => PixelValue::DarkGray,
            3 => PixelValue::Black,
            _ => panic!("Unknown shade"),
        }
    }

The next step is the window, then the sprites. Sprites will be hard due to their varied size. See y’all next time!

0
0
4
Open comments for this post

2h 0m 27s logged

Div(isions of time)

Hello everyone! This time, I actually remembered to make a devlog! Let’s dive in!

Div(vy up Labour)

Remember how I added the number of ticks each instruction took as a return value from the execute() fn? Well, it actually got put to use now! There is an internal 16-bit timer in the Sharp SM83, the upper 8 bits of which are exposed at memory address 0xff04. Programming this was pretty easy once I realised I could have read_byte() and write_byte() directly access the counter (Originally, I was going to have an update_div thingy probably and have the bte in memory and the accumulator be separate rather than this simpler approach).

    pub fn read_byte(&self, address: u16) -> u8 {
        let address = address as usize;
        match address {
            VRAM_BEGIN..=VRAM_END => self.gpu.read_vram(address - VRAM_BEGIN),
            0xff04 => (self.div_accumulator >> 8) as u8,
            _ => self.memory[address],
        }
    }

    pub fn write_byte(&mut self, address: u16, byte: u8) {
        let address = address as usize;
        match address {
            VRAM_BEGIN..=VRAM_END => self.gpu.write_vram(address - VRAM_BEGIN, byte),
            // Any write to div resets it and its accumulator
            0xff04 => self.div_accumulator = 0,
            _ => self.memory[address] = byte,
        };
    }

Now, last time, I set up interrupts, so with div set up, it was time to set up timer interrupts.

TIMA Time

What actually triggers timer interrupts, rather than div, is a separate timer called TIMA (0xff05 in memory). TIMA has settings set in TAC (0xff07 in memory). Bits 0 and 1 of TAC dictate which bit of the DIV accumulator (let’s call it divacc) to watch to see when TIMA should be incremented. Bit 2 dictates whether TIMA should be incremented at all.

TIMA gets incremented whenever the indicated bit of divacc falls from 1 → 0. This can sometimes be as few as every 16 ticks, and since instructions can take up to 24 ticks, this can cause the bit to go from 1 → 0 → 1, which means that you can’t just check the new divacc against the old one to see whether the bit fell, though I tried this as a temporary solution. This is what I had to do instead (see screenshot). I wish it could be prettier, but what can you do?

Anyways, y’all, see you next time!

0
0
5
Open comments for this post

2h 45m 34s logged

Interrupting Cow

Hello everyone! This took a lot longer than I expected due to a large amount of me getting confused by how interrupts work. Let’s dive in!

Halt!

After dealing with tick returns, the next step was dealing with interrupts. There are two bytes in memory that concern interrupts. One is IE (0xffff), which tells which interrupts to listen for, and the other is IF (0xff0f), which tells which interrupts are currently pending. Dealing with these was easy enough, but there was another thing that needed addressing:

The Halt Bug

The halt bug is a known bug that, should Halt be ran when there are interrupts pending and the IME (interrupt master enable I think) is off, it doesn’t halt and the next instruction runs twice. I was both confused by how this behaved and how it was triggered, which caused issues with my code that had to be addressed.

I wasn’t able to deal with anything besides interrupt handling, so that’s all for now!

0
0
9
Open comments for this post

3h 44m 20s logged

Ticking Time Bomb

Hello everyone! It seems that, yet again, I have forgotten to make a devlog for a few hours, so let’s dive in! Fortunately, not much has happened.

16 GB of VRAM

As I said in my last devlog, it was time to get started on graphical stuff. I set up a GPU struct with a separate VRAM, and set up the memory to write to that VRAM instead of the regular bus when it’s supposed to.

This was also the last part of the docs I’ve been following, so I’ve been on my own since this point. It’s a little pesky that my best resources on how the GB works are now Claude Code and a random opcode table, but what can you do?

Tick-Tock Goes the Clock

Different operations in the GB’s processor take different numbers of clock cycles to do, so I had to add this as a second return value to execute to add slowing in the future. This was the majority of the hours I logged, staring at the opcode table and trying to figure out the patterns in the timings.

Anyway, see y’all next time! These are some silly things that will probably land for at least some of you.

0
0
7
Open comments for this post

1h 18m 56s logged

Where are my keys?

Hello everyone! We are back with a little news, so let’s dive right in!

Hashmap, was not, in fact, the solution

The mapping from opcode to instruction enum (fn from_byte()) is finally done. Not much to say except that it was a very tedious process. While working, I realised I hadn’t implemented ADD SP, i8 (addassign a signed integer to the stack pointer) yet, so I quickly added that.

Unfortunately, that’s all folks! Next, I’m going to start dealing with graphics.

0
0
51
Open comments for this post

1h 54m 50s logged

Miscellany

Hello everyone! Let’s keep this quick because I have much tedium to get through.

What the Hecc is JR?!

I finished most of instructions, so I needed to figure out which ones I forgot. I was looking at the opcode table and I realised I had no idea what JR or DAA did, so I had to figure out what they did. JR is just a relative jump rather than an absolute one, and DAA does some number formatting after an arithmetic op for better display.

Let’s Table this Discussion

Since the instructions that I can implement right now (very nearly all of them, except for stop and interrupts, etc.), it’s been time to update the Instruction::from_byte() fn with all the new instructions. I’m still doing this, and it is purely looking at the opcode table, entering something, looking at the opcode table again, looking at what I wrote, and repeating this over and over and over again. I’m not even done with this!

Anyway, enjoy a hashmap meme that doesn’t apply to me b/c I am using a match

0
0
7
Open comments for this post

1h 38m 33s logged

Pushpops

Hello everyone! This time, I actually logged a normal amount of time! Let’s dive in!

Loading Shipments

I finally finished up the LD instruction! once I got through the weird targets, everything was smooth sailing.

Stack Overflow

Once LD was done, it was time to implement the stack. If you don’t know what (a/the) stack is, reference the wikipedia screenshot. For the GB, the two methods are PUSH and POP, and the stack actually goes down in memory from the end of the memory bus rather than from the beginning.

Dear Functional Bros

When I finished implementing the stack, which I though was going to be a lot harder, it was time to add functions to it. This was pretty simple: a CALL instr. that jumps to a point and puts the place where it left off on the stack and a RET instr. that goes to the last location on the stack.

0
0
46
Open comments for this post

3h 14m 27s logged

Loading Jump Time

Hello everyone! I lost track of time, and it appears I have once more logged too many hours with no devlog, yet no excuses I have. Let’s get right to it!

HL and D8

A lot of Instructions that I had already made have an HL target, in which they use the value in the joint register HL as an address in memory, and perform the operation on that byte. Up until this point, all HL targets were todos, but, since I had created the memory bus, I was able to quickly wire up those targets. A lot of those same instructions could also take the next byte in memory as the input (D8) so I wired those up too (though before HL). I actually wasn’t sure whether HL targets used a different memory bus than the one used for operations.

JP Morgan

The next instruction to deal with was JP (jump). Based on different conditions (the state of the zero flag or carry flag, or always) it would or wouldn’t jump to the address encoded over the next two bytes. Probably one of the easier instructions I’ve made.

Loading…

After JP was load. LD just copies data from one place to another. There are so many variations that I can’t talk about all of them, and there my code is too long for this devlog, so:

Byte: Load a byte from point a to point b
Word: just like the Byte type except with 16-bit valuesAFromIndirect: load the A register with the contents from a value from a memory location whose address is stored in some locationIndirectFromA: load a memory location whose address is stored in some location with the contents of the A registerAFromByteAddress: Just like AFromIndirect except the memory address is some address in the very last byte of memory.ByteAddressFromA: Just like IndirectFromA except the memory address is some address in the very last byte of memory.

This is the opcode table I’ve been using

0
0
44
Open comments for this post

4h 6m 54s logged

Timeliness

Rather unfortunately, a lot of work has been done since the last devlog due to yesterday’s hackatime outage.

Let’s get started!

House of Memories

As I mentioned in the last devlog, the next step was to create a program counter (pc, marks where in the program we are) and the memory bus (with all the instructions we have been emulating for the GB to run, and others)

Bite the Code

The instructions are not stored as plain text of enums, they are stored as bytes, one byte for each combination of instruction and target. I thus have to map each bytecode to each instruction that I have added so far.

Not English Class! Anything but English!

There are much more than 256 different bytecodes; there are actually nearly 512. To accomplish this and still have memory be 1 byte, there is a bytecode prefix, 0xcb, which tells the chip that the real instruction is the next one, and to look it up on a different table.

PCMR

To go through each instruction, I added a step function:

    fn step(&mut self) {
        let (instruction_byte, prefixed) = {
            let temp_byte = self.bus.read_byte(self.pc);
            if temp_byte == 0xcb {
                (self.bus.read_byte(self.pc + 1), true)
            } else {
                (temp_byte, false)
            }
        };

        let next_pc = if let Some(instruction) = Instruction::from_byte(instruction_byte, prefixed)
        {
            self.execute(instruction)
        } else {
            panic!("Unknown instruction found for: 0x{:x}", instruction_byte);
        };

        self.pc = next_pc;
    }

As you can see, execute returns the next pc value, something it didn’t do before. So, I had to go and make all the match arms blocks that do their associated action and also return self.pc.wrapping_add(1) for non-0xcb functions and self.pc.wrapping_add(2) for 0xcb functions, since no function that modifies the pc has been added yet.

Next is added yet more instructions! See y’all next time!

0
0
21
Open comments for this post

2h 20m 41s logged

Registering New Data

Hello Everyone! You know what they say: mo’ code mo’ devlogs.

To the right, to the right, to the right, the right, the right

I finished up all the instructions that act on register data!!! Specifically, these ones:

ADD - Add the value of the target register to the A register.
ADDHL (add to HL) - just like ADD except that the target is added to the HL register
ADC (add with carry) - just like ADD except that the value of the carry flag is also added to the number
SUB (subtract) - subtract the value stored in a specific register with the value in the A register
SBC (subtract with carry) - just like ADD except that the value of the carry flag is also subtracted from the number
AND (logical and) - do a bitwise and on the value in a specific register and the value in the A register
OR (logical or) - do a bitwise or on the value in a specific register and the value in the A register
XOR (logical xor) - do a bitwise xor on the value in a specific register and the value in the A register
CP (compare) - just like SUB except the result of the subtraction is not stored back into A
INC (increment) - increment the value in a specific register by 1
DEC (decrement) - decrement the value in a specific register by 1
CCF (complement carry flag) - toggle the value of the carry flag
SCF (set carry flag) - set the carry flag to true
RRA (rotate right A register) - bit rotate A register right through the carry flag
RLA (rotate left A register) - bit rotate A register left through the carry flag
RRCA (rotate right A register) - bit rotate A register right (not through the carry flag)
RLCA (rotate left A register) - bit rotate A register left (not through the carry flag)
CPL (complement) - toggle every bit of the A registerBIT (bit test) - test to see if a specific bit of a specific register is set
RES (bit reset) - set a specific bit of a specific register to 0
SET (bit set) - set a specific bit of a specific register to 1
SRL (shift right logical) - bit shift a specific register right by 1
RR (rotate right) - bit rotate a specific register right by 1 through the carry flag
RL (rotate left) - bit rotate a specific register left by 1 through the carry flag
RRC (rorate right) - bit rotate a specific register right by 1 (not through the carry flag)
RLC (rorate left) - bit rotate a specific register left by 1 (not through the carry flag)
SRA (shift right arithmetic) - arithmetic shift a specific register right by 1
SLA (shift left arithmetic) - arithmetic shift a specific register left by 1
SWAP (swap nibbles) - switch upper and lower nibble of a specific register

Some of the bitwise ops for rotation were a little difficult, but I was able to troubleshoot.

Next is the game ROM, the program counter, prefix instructions, and jump instructions!

0
0
29
Loading more…

Followers

Loading…