diff --git a/src/changes.md b/src/changes.md index 0be7c92..8eb4f45 100644 --- a/src/changes.md +++ b/src/changes.md @@ -8,3 +8,6 @@ - [Fix typo: Stack is a LIFO system, not a FIFO](https://github.com/aquova/chip8-book/issues/6) - [Added missing bug fixes from source code into the book](https://github.com/aquova/chip8-book/pull/5) - [Added ePub generation](https://github.com/aquova/chip8-book/pull/4) + +## Version 1.1 +- Updated much of the prose for better flow (special thanks to KDR for editing). diff --git a/src/frontend.md b/src/frontend.md index 8115d29..a130b9d 100644 --- a/src/frontend.md +++ b/src/frontend.md @@ -135,7 +135,7 @@ fn main() { } ``` -This is a lot to take in, but the gist is this. We'll initialize SDL and tell it to create a new window of our scaled up size. We'll also have it be created in the middle of the user's screen. We'll then get the canvas object we'll actually draw to, with VSYNC on. Then go ahead and clear it and show it to the user. +We'll initialize SDL and tell it to create a new window of our scaled up size. We'll also have it be created in the middle of the user's screen. We'll then get the canvas object we'll actually draw to, with VSYNC on. Then go ahead and clear it and show it to the user. If you attempt to run it now (give it a dummy file name to test, like `cargo run test`), you'll see a window pop up for a brief moment before closing. This is because the SDL window is created briefly, but then the program ends and the window closes. We'll need to create our main game loop so that our program doesn't end immediately, and while we're at it, let's add some handling to quit the program if we try to exit out of the window (otherwise you'll have to force quit the program from your task manager). @@ -225,11 +225,11 @@ fn main() { } ``` -A few things to note here. In the event that Rust is unable to open the file from the path the user gave us (likely because it doesn't exist), then the `expect` condition will fail and the program will exit with that message. Secondly, you may be asking why don't we simply give the file path to the backend and load the data in there? Opening and reading a file is in the standard library after all. This is for two reasons. Firstly, reading a file is a more frontend-type behavior and it better fits here. Secondly, and more importantly, our eventual plan is to make this emulator work in a web browser with little to no changes to our backend. How a browser reads a file is very different to how your file system will do it, so we will allow the frontends to handle the reading, and pass in the data once we have it. +A few things to note here. In the event that Rust is unable to open the file from the path the user gave us (likely because it doesn't exist), then the `expect` condition will fail and the program will exit with that message. Secondly, we could give the file path to the backend and load the data there, but reading a file is a more frontend-type behavior and it better fits here. More importantly, our eventual plan is to make this emulator work in a web browser with little to no changes to our backend. How a browser reads a file is very different to how your file system will do it, so we will allow the frontends to handle the reading, and pass in the data once we have it. ## Running the Emulator and Drawing to the Screen -Here's the big moment! The game has been loaded into RAM, our main loop is running, now we just need to tell our backend to begin processing its instructions, and to actually draw to the screen. If you recall, the emulator runs through a clock cycle each time its `tick` function is called, so let's add that to our loop. +The game has been loaded into RAM and our main loop is running. Now we need to tell our backend to begin processing its instructions, and to actually draw to the screen. If you recall, the emulator runs through a clock cycle each time its `tick` function is called, so let's add that to our loop. ```rust fn main() { @@ -254,7 +254,7 @@ use sdl2::render::Canvas; use sdl2::video::Window; ``` -Next, the function, which will take in a reference to our `Emu` object, as well as a mutable reference to our SDL canvas. Drawing the screen requires a few steps. First, we clear the canvas to erase the previous frame. Then, we iterate through the screen buffer, drawing a white rectangle anytime the given value is true. Since Chip-8 only supports black and white, if we clear the screen as black, we only have to worry about drawing the white squares. +Next, the function, which will take in a reference to our `Emu` object, as well as a mutable reference to our SDL canvas. Drawing the screen requires a few steps. First, we clear the canvas to erase the previous frame. Then, we iterate through the screen buffer, drawing a white rectangle anytime the given value is true. Since Chip-8 only supports black and white; if we clear the screen as black, we only have to worry about drawing the white squares. ```rust fn draw_screen(emu: &Emu, canvas: &mut Canvas) { @@ -307,11 +307,11 @@ If you have a Chip-8 game downloaded, go ahead and try running your emulator wit $ cargo run path/to/game ``` -If everything has gone well, you should see the window appear and the game begin to render and play! This is a tremendous step, and you should feel accomplished for getting this far with your very own emulator. +If everything has gone well, you should see the window appear and the game begin to render and play! You should feel accomplished for getting this far with your very own emulator. -As I mentioned previously, the emulation `tick` speed should probably run faster than the canvas refresh rate. If you watch your game run, it might feel a bit sluggish. Right now, we execute one instruction, then draw to the screen, then repeat. As you're aware now, it takes several instructions to be able to do any meaningful changes to the screen. To get around this, we will allow the emulator to tick several times before redrawing. +As I mentioned previously, the emulation `tick` speed should probably run faster than the canvas refresh rate. If you watch your game run, it might feel a bit sluggish. Right now, we execute one instruction, then draw to the screen, then repeat. As you're aware, it takes several instructions to be able to do any meaningful changes to the screen. To get around this, we will allow the emulator to tick several times before redrawing. -Now, this is where things get a bit... experimental. The Chip-8 specification says nothing about how quickly the system should actually run. Even leaving it is now so it runs at 60 Hz is a valid solution (and you're welcome to do so). We'll simply allow our `tick` function to loop several times before moving on to drawing the screen. Personally, I (and other emulators I've looked at) find that 10 times is a nice sweet spot. +Now, this is where things get a bit experimental. The Chip-8 specification says nothing about how quickly the system should actually run. Even leaving it is now so it runs at 60 Hz is a valid solution (and you're welcome to do so). We'll simply allow our `tick` function to loop several times before moving on to drawing the screen. Personally, I (and other emulators I've looked at) find that 10 ticks per frame is a nice sweet spot. ```rust const TICKS_PER_FRAME: usize = 10; @@ -333,7 +333,7 @@ fn main() { } ``` -Some of you might feel this is a bit hackish, and to be honest I somewhat agree with you. However, this is also how more 'sophisticated' systems work, with the exception that those CPUs usually have some way of notifying the screen that it's ready to redraw. Since the Chip-8 has no such mechanism nor any defined clock speed, this is a easier way to accomplish this task. +Some of you might feel this is a bit hackish (I somewhat agree with you). However, this is also how more 'sophisticated' systems work, with the exception that those CPUs usually have some way of notifying the screen that it's ready to redraw. Since the Chip-8 has no such mechanism nor any defined clock speed, this is a easier way to accomplish this task. If you run again, you might notice that it doesn't get very far before pausing. This is likely due to the fact that we never update our two timers, so the emulator has no concept of how long time has passed for its games. I mentioned earlier that the timers run once per frame, rather than at the clock speed, so we can modify the timers at the same point as when we modify the screen. @@ -363,7 +363,7 @@ We can finally render our Chip-8 game to the screen, but we can't get very far i As a refresher, the Chip-8 system supports 16 different keys. These are typically organized in a 4x4 grid, with keys 0-9 organized like a telephone with keys A-F surrounding. While you are welcome to organize the keys in any configuration you like, some game devs assumed they're in the grid pattern when choosing their games inputs, which means it can be awkward to play some games otherwise. For our emulator, we'll use the left-hand keys of the QWERTY keyboard as our inputs, as shown below. -Let's create a helper function to convert SDL's key type into the values that we will send to the emulator. We'll need to bring SDL keyboard support into `main.rs` via: +Let's create a function to convert SDL's key type into the values that we will send to the emulator. We'll need to bring SDL keyboard support into `main.rs` via: ```rust use sdl2::keyboard::Keycode; @@ -397,7 +397,7 @@ fn key2btn(key: Keycode) -> Option { ![Keyboard to Chip-8 key layout](img/input_layout.png) -Next, we'll add two additional events to our main event loop, one for `KeyDown` and the other for `KeyUp`. Each will check if the pressed key gives a `Some` value from our `key2btn` function, and if so pass it to the emulator via the public `keypress` function we defined earlier. The only difference between the two will be if it sets or clears. +Next, we'll add two additional events to our main event loop, one for `KeyDown` and the other for `KeyUp`. Each event will check if the pressed key gives a `Some` value from our `key2btn` function, and if so pass it to the emulator via the public `keypress` function we defined earlier. The only difference between the two will be if it sets or clears. ```rust fn main() { @@ -431,7 +431,7 @@ fn main() { } ``` -If you haven't seen it before, the `if let` statement is only satisfied if the value on the right matches that on the left, namely that `key2btn(key)` returns a `Some` value. The unwrapped value is then stored in `k`. +The `if let` statement is only satisfied if the value on the right matches that on the left, namely that `key2btn(key)` returns a `Some` value. The unwrapped value is then stored in `k`. Let's also add a common emulator ability - quitting the program by pressing Escape. We'll add that alongside our `Quit` event. @@ -468,9 +468,11 @@ fn main() { } ``` -Unlike the other key events, where we would check the found `key` variable, we are simply looking for the Escape key to quit. If you don't want this ability in your emulator, or would like some other key press functionality, you're welcome to do so. +Unlike the other key events, where we would check the found `key` variable, we want to use the Escape key to quit. If you don't want this ability in your emulator, or would like some other key press functionality, you're welcome to do so. -That's it! The desktop frontend of our Chip-8 emulator is now complete. We can specify a game via a command line parameter, load and execute it, display the output to the screen, and handle user input. I hope you were able to get an understanding of how emulation works. Chip-8 is a rather basic system, but the techniques discussed here form the basis for how all emulation works. +That's it! The desktop frontend of our Chip-8 emulator is now complete. We can specify a game via a command line parameter, load and execute it, display the output to the screen, and handle user input. + +I hope you were able to get an understanding of how emulation works. Chip-8 is a rather basic system, but the techniques discussed here form the basis for how all emulation works. However, this guide isn't done! In the next section I will discuss how to build our emulator with WebAssembly and getting it to run in a web browser. diff --git a/src/instr.md b/src/instr.md index 6d4ffb5..cfbce4b 100644 --- a/src/instr.md +++ b/src/instr.md @@ -11,7 +11,7 @@ pub fn tick(&mut self) { } ``` -This implies a bit that decode and execute will be their own separate functions. While they could be, personally for Chip-8 I think it's easier to simply perform the operation as we determine it, rather than involving another function call. Our `tick` function thus becomes this: +This implies that decode and execute will be their own separate functions. While they could be, for Chip-8 it's easier to simply perform the operation as we determine it, rather than involving another function call. Our `tick` function thus becomes this: ```rust pub fn tick(&mut self) { @@ -22,10 +22,11 @@ pub fn tick(&mut self) { } fn execute(&mut self, op: u16) { + // TODO } ``` -Our next step is to *decode*, which is to determine exactly which operation we're dealing with. The [Chip-8 opcode cheatsheet](#ot) has all of the available opcodes, how to interpret their parameters, and some notes on what they mean (if you don't like my table, there are many similar examples online). You will need to reference this often, in order for the emulator to be complete, each and every one of them must be implemented. +Our next step is to *decode*, or determine exactly which operation we're dealing with. The [Chip-8 opcode cheatsheet](#ot) has all of the available opcodes, how to interpret their parameters, and some notes on what they mean. You will need to reference this often. For a complete emulator, each and every one of them must be implemented. ## Pattern Matching @@ -57,13 +58,13 @@ fn execute(&mut self, op: u16) { Rust's `match` statement demands that all possible options be taken into account which is done with the `_` variable, which captures "everything else". Inside, we'll use the `unimplemented!` macro to cause the program to panic if it reaches that point. By the time we finish adding all opcodes, the Rust compiler demands that we still have an "everything else" statement, but we should never hit it. -A brief side note for any developers who might be curious about other systems. While a long `match` statement would certainly work for other architectures, it is usually more common to implement instructions in their own functions, and either use a lookup table or programmatically determine which function is correct. Chip-8 is somewhat unusual as it stores instruction parameters into the opcode itself, meaning we need a lot of wild cards to match the instructions. Since there are a relatively small number of them, a `match` statement works well here. +While a long `match` statement would certainly work for other architectures, it is usually more common to implement instructions in their own functions, and either use a lookup table or programmatically determine which function is correct. Chip-8 is somewhat unusual because it stores instruction parameters into the opcode itself, meaning we need a lot of wild cards to match the instructions. Since there are a relatively small number of them, a `match` statement works well here. With the framework setup, let's dive in! ## Intro to Implementing Opcodes -The following pages individually discuss how all of Chip-8's instructions work, and include code of how to implement them. They are there to assist any programmers who are confused on how to proceed or how to interpret some of the more nuanced behavior of the system. You are welcome to simply follow along and implement instruction by instruction, but before you do that, you may want to look forward to the [next section](#dfe) and begin working on some of the frontend code. Currently we have no way of actually running our emulator, and it may be useful to some to be able to attempt to load and run a game for debugging. However, do remember that the emulator will likely crash rather quickly unless all of the instructions are implemented. Personally, I prefer to work on the instructions first before working on the other moving parts (hence why this guide is laid out the way it is). +The following pages individually discuss how all of Chip-8's instructions work, and include code of how to implement them. You are welcome to simply follow along and implement instruction by instruction, but before you do that, you may want to look forward to the [next section](#dfe) and begin working on some of the frontend code. Currently we have no way of actually running our emulator, and it may be useful to some to be able to attempt to load and run a game for debugging. However, do remember that the emulator will likely crash rather quickly unless all of the instructions are implemented. Personally, I prefer to work on the instructions first before working on the other moving parts (hence why this guide is laid out the way it is). With that disclaimer out of the way, let's proceed to working on each of the Chip-8 instructions in turn. @@ -98,7 +99,7 @@ match (digit1, digit2, digit3, digit4) { ### 00EE - Return from Subroutine -We haven't yet spoken about subroutines (aka functions) and how they work. Entering into a subroutine works in the same way as just a plain jump; we move the PC to the specified address and resume execution from there. Unlike a jump, a subroutine is expected to complete at some point, and we will need to return back to the point where we entered. This is where our stack comes in. When we enter a subroutine, we simply push our address onto the stack, run the routine's code, and when we're ready to return we pop that value off our stack and execute from that point again. A stack also allows us to maintain return addresses for nested subroutines while ensuring they are returned in the correct order. +We haven't yet spoken about subroutines (aka functions) and how they work. Entering into a subroutine works in the same way as a plain jump; we move the PC to the specified address and resume execution from there. Unlike a jump, a subroutine is expected to complete at some point, and we will need to return back to the point where we entered. This is where our stack comes in. When we enter a subroutine, we simply push our address onto the stack, run the routine's code, and when we're ready to return we pop that value off our stack and execute from that point again. A stack also allows us to maintain return addresses for nested subroutines while ensuring they are returned in the correct order. ```rust match (digit1, digit2, digit3, digit4) { @@ -178,7 +179,7 @@ The implementation works like this: since we already have the second digit saved ### 4XNN - Skip next if VX != NN -This opcode is exactly the same as the previous, with the change that we skip if the compared values are not equal. +This opcode is exactly the same as the previous, except we skip if the compared values are not equal. ```rust match (digit1, digit2, digit3, digit4) { @@ -199,7 +200,7 @@ match (digit1, digit2, digit3, digit4) { ### 5XY0 - Skip next if VX == VY -A similar operation again, however we now use the third digit to index into another *V Register*. You will also notice that while the least significant digit is not used in the operation, this opcode requires it to be 0. +A similar operation again, however we now use the third digit to index into another *V Register*. You will also notice that the least significant digit is not used in the operation. This opcode requires it to be 0. ```rust match (digit1, digit2, digit3, digit4) { @@ -220,7 +221,7 @@ match (digit1, digit2, digit3, digit4) { ### 6XNN - VX = NN -This operation does not require too much explanation, set the *V Register* specified by the second digit to the value given. +Set the *V Register* specified by the second digit to the value given. ```rust match (digit1, digit2, digit3, digit4) { @@ -761,7 +762,7 @@ match (digit1, digit2, digit3, digit4) { } ``` -A side note on efficiency here. For this implementation, I converted our VX value first into a `float`, so that I could use division and modulo arithmetic to get each decimal digit. This is not the fastest implementation nor is it probably the shortest. However, it is one of the easiest to understand. I'm sure there are some highly binary-savvy readers who are disgusted that I did it this way, but this solution is not for them. This is for readers who have never seen BCD before where losing some speed for greater understanding is a better trade-off. However, once you have this implemented, I would encourage everyone to go out and look up more efficient BCD algorithms to add a bit of easily optimization into your code. +For this implementation, I converted our VX value first into a `float`, so that I could use division and modulo arithmetic to get each decimal digit. This is not the fastest implementation nor is it probably the shortest. However, it is one of the easiest to understand. I'm sure there are some highly binary-savvy readers who are disgusted that I did it this way, but this solution is not for them. This is for readers who have never seen BCD before, where losing some speed for greater understanding is a better trade-off. However, once you have this implemented, I would encourage everyone to go out and look up more efficient BCD algorithms to add a bit of easy optimization into your code. ### FX55 - Store V0 - VX into I @@ -805,6 +806,6 @@ match (digit1, digit2, digit3, digit4) { ### Final Thoughts -That's it! With this, we now have a fully implemented Chip-8 CPU. You may have noticed through this journey that there are a lot of possible opcode values that are never covered, particularly in the 0x0000, 0xE000, and 0xF000 ranges. This is okay. These opcodes are left as undefined by the original design, and thus if any game attempts to use them it will lead to a `panic!`. If you are still curious following the completion of this emulator, there are a number of Chip-8 extensions which do fill in some of these gaps to add additional functionality, but they will not be covered by this guide. +That's it! With this, we now have a fully implemented Chip-8 CPU. You may have noticed a lot of possible opcode values are never covered, particularly in the 0x0000, 0xE000, and 0xF000 ranges. This is okay. These opcodes are left as undefined by the original design, and thus if any game attempts to use them it will lead to a runtime panic. If you are still curious following the completion of this emulator, there are a number of Chip-8 extensions which do fill in some of these gaps to add additional functionality, but they will not be covered by this guide. \newpage diff --git a/src/metadata.yaml b/src/metadata.yaml index 04ef158..0657f57 100644 --- a/src/metadata.yaml +++ b/src/metadata.yaml @@ -4,7 +4,7 @@ title: - An Introduction to Chip-8 Emulation using the Rust Programming Language author: by aquova date: - - 31 January 2021 + - 15 February 2023 - \newpage keywords: [emulation, chip-8, emudev, rust, webassembly] geometry: margin=0.75in diff --git a/src/methods.md b/src/methods.md index 351d2b8..1222c15 100644 --- a/src/methods.md +++ b/src/methods.md @@ -4,7 +4,7 @@ We have now created our `Emu` struct and defined a number of variables for it to ## Push and Pop -We have added both a `stack` array as well as a pointer `sp` to manage the CPU's stack, however it will be useful to implement both a `push` and `pop` method so we can access it easily. +We have added both a `stack` array, as well as a pointer `sp` to manage the CPU's stack, however it will be useful to implement both a `push` and `pop` method so we can access it easily. ```rust impl Emu { @@ -24,17 +24,19 @@ impl Emu { } ``` -These are pretty straightforward. `push` adds the given 16-bit value to the spot pointed to by the Stack Pointer, then moves the pointer to the next position. `pop` performs this operation in reverse, moving the SP back to the previous value then returning what is there. Note that attempting to pop an empty stack results in an underflow panic. You are welcome to add extra handling here if you like, but in the event this were to occur, that would indicate a bug with either our emulator or the game code, so I feel that a complete panic is acceptable. +These are pretty straightforward. `push` adds the given 16-bit value to the spot pointed to by the Stack Pointer, then moves the pointer to the next position. `pop` performs this operation in reverse, moving the SP back to the previous value then returning what is there. Note that attempting to pop an empty stack results in an underflow panic[^1]. You are welcome to add extra handling here if you like, but in the event this were to occur, that would indicate a bug with either our emulator or the game code, so I feel that a complete panic is acceptable. ## Font Sprites -We haven't yet delved into how the Chip-8 screen display works, but the gist for now is that it renders *sprites* which are stored in memory to the screen, one line at a time. It is up to the game developer to correctly load their sprites in before copying them over. However wouldn't it be nice if the system automatically had sprites for commonly used things, such as numbers? I mentioned earlier that our PC will begin at address 0x200, leaving the first 512 intentionally empty. Most modern emulators will use that space to store the sprite data for font characters of all the hexadecimal digits, that is characters of 0-9 and A-F. Strictly speaking, we could store this data at any fixed position in RAM, but this space is already defined as empty anyway. Each character is five bytes long, with each byte making up a row, thus the characters are 8x5 pixels each. To see how this works, make a note of the following diagram. +We haven't yet delved into how the Chip-8 screen display works, but the gist for now is that it renders *sprites* which are stored in memory to the screen, one line at a time. It is up to the game developer to correctly load their sprites before copying them over. However wouldn't it be nice if the system automatically had sprites for commonly used things, such as numbers? I mentioned earlier that our PC will begin at address 0x200, leaving the first 512 intentionally empty. Most modern emulators will use that space to store the sprite data for font characters of all the hexadecimal digits, that is characters of 0-9 and A-F. We could store this data at any fixed position in RAM, but this space is already defined as empty anyway. Each character is made up of eight rows of five pixels, with each row using a byte of data, meaning that each letter altogether takes up five bytes of data. The following diagram illustrates how a character is stored as bytes. + +[^1] *Underflow* is when the value of an unsigned variable goes from above zero to below zero. In some languages the value would then "roll over" to the highest possible size, but in Rust this leads to a runtime error and needs to be handled differently if desired. The same goes for values exceeding the maximum possible value, known as *overflow*. \newpage ![Chip-8 Font Sprite](img/font_diagram.png) -On the left, we have how the font sprite will appear on screen. On the right, there is each row encoded into binary. Each pixel is assigned a bit, which corresponds to whether that pixel will be white or black. *Every* sprite in Chip-8 is eight pixels wide, which means a pixel row requires 8-bits (1 byte). The above diagram shows the layout of the "1" character sprite. The fonts sprites don't need all 8 bits of width, so they all have black right halves. Sprites have been created for all of the hexadecimal digits, and are required to be present somewhere in RAM for some games to function. Later in this guide we will cover the instruction that handles these sprites, which will show how these are loaded and how the emulator knows where to find them. For now, we simply need to define them. We will do so with a constant array of bytes; at the top of `lib.rs`, add: +On the right, each row is encoded into binary. Each pixel is assigned a bit, which corresponds to whether that pixel will be white or black. *Every* sprite in Chip-8 is eight pixels wide, which means a pixel row requires 8-bits (1 byte). The above diagram shows the layout of the "1" character sprite. The sprites don't need all 8 bits of width, so they all have black right halves. Sprites have been created for all of the hexadecimal digits, and are required to be present somewhere in RAM for some games to function. Later in this guide we will cover the instruction that handles these sprites, which will show how these are loaded and how the emulator knows where to find them. For now, we simply need to define them. We will do so with a constant array of bytes; at the top of `lib.rs`, add: ```rust const FONTSET_SIZE: usize = 80; @@ -124,14 +126,14 @@ pub fn tick(&mut self) { } fn fetch(&mut self) -> u16 { - + // TODO } ``` The `fetch` function will only be called internally as part of our `tick` loop, so it doesn't need to be public. The purpose of this function is to grab the instruction we are about to execute (known as an *opcode*) for use in the next steps of this cycle. If you're unfamiliar with Chip-8's instruction format, I recommend you refresh up with the [overview](#eb) from the earlier chapters. -Fortunately, Chip-8 is easier than many systems in two regards. For one, there's only 35 opcodes to deal with as opposed to the hundreds that many processors support. In addition, many systems store additional parameters for each opcode in subsequent bytes (such as operands for addition), Chip-8 encodes these into the opcode itself. Due to this, Chip-8 opcodes are larger than that of other systems, all of them are 2 bytes, but the entire instruction is stored in those two bytes, while other contemporary system might consume between 1 and 3 bytes per cycle. +Fortunately, Chip-8 is easier than many systems. For one, there's only 35 opcodes to deal with as opposed to the hundreds that many processors support. In addition, many systems store additional parameters for each opcode in subsequent bytes (such as operands for addition), Chip-8 encodes these into the opcode itself. Due to this, all Chip-8 opcodes are exactly 2 bytes, which is larger than some other systems, but the entire instruction is stored in those two bytes, while other contemporary systems might consume between 1 and 3 bytes per cycle. Each opcode is encoded differently, but fortunately since all instructions consume two bytes, the fetch operation is the same for all of them, and implemented as such: @@ -149,7 +151,7 @@ This function fetches the 16-bit opcode stored at our current Program Counter. W ## Timer Tick -The Chip-8 specification also mentions two special purpose *timers*, the Delay Timer and the Sound Timer. While the `tick` function operates once every CPU cycle, these timers are modified instead once every frame, and thus need to be handled in a separate function. Their behavior is rather simple, every frame both decrease by one. If the Sound Timer is set to one, the system will emit a 'beep' noise. If they ever hit zero, they do not automatically reset, they will remain at zero until the game manually resets them to some value. +The Chip-8 specification also mentions two special purpose *timers*, the Delay Timer and the Sound Timer. While the `tick` function operates once every CPU cycle, these timers are modified instead once every frame, and thus need to be handled in a separate function. Their behavior is rather simple, every frame both decrease by one. If the Sound Timer is set to one, the system will emit a 'beep' noise. If the timers ever hit zero, they do not automatically reset; they will remain at zero until the game manually resets them to some value. ```rust pub fn tick_timers(&mut self) { diff --git a/src/wasm.md b/src/wasm.md index a608a46..909c80e 100644 --- a/src/wasm.md +++ b/src/wasm.md @@ -1,6 +1,6 @@ # Introduction to WebAssembly -This section will discuss how to take our finished emulator and configure it to run in a web browser via a relatively new technology called *WebAssembly*. I encourage you to [read more](https://en.wikipedia.org/wiki/WebAssembly) about WebAssembly, as it is an interesting and rapidly growing technology. A brief gist is that it is a format for compiling programs into a binary executable, similar in scope to an .exe, but are meant to be run within a web browser. It is supported by all of the major web browsers, and is a cross-company standard being developed between them. This means that instead of having to write web code in JavaScript or other web-centric languages, you can write it in any language that supports compilation of .wasm files and still be able to run in a browser. At the time of writing, C, C++, and Rust are the major languages which support it, fortunately for us. +This section will discuss how to take our finished emulator and configure it to run in a web browser via a relatively new technology called *WebAssembly*. I encourage you to [read more](https://en.wikipedia.org/wiki/WebAssembly) about WebAssembly. It is a format for compiling programs into a binary executable, similar in scope to an .exe, but are meant to be run within a web browser. It is supported by all of the major web browsers, and is a cross-company standard being developed between them. This means that instead of having to write web code in JavaScript or other web-centric languages, you can write it in any language that supports compilation of .wasm files and still be able to run in a browser. At the time of writing, C, C++, and Rust are the major languages which support it, fortunately for us. ## Setting Up @@ -58,19 +58,19 @@ Now, a big difference between our `desktop` and our new `wasm` is that `desktop` ``` -We'll add more to it later, but for now this will suffice. However, our web program will not run if you simply open the file in a web browser, you will need to start a web server first. If you have Python 3 installed, which all modern Macs and many Linux distributions do, you can simply start a web server via: +We'll add more to it later, but for now this will suffice. Our web program will not run if you simply open the file in a web browser, you will need to start a web server first. If you have Python 3 installed, which all modern Macs and many Linux distributions do, you can simply start a web server via: ``` $ python3 -m http.server ``` -Then navigate to `localhost` in your web browser. If you ran this in the `web` directory, you should see our `index.html` page displayed. Now, I've tried to find a simple, built-in way to start a local web server on Windows, and I haven't really found one. I personally use Python 3, but you are welcome to use any other similar service, such as `npm` or even some Visual Studio Code extensions. It doesn't matter which, just so they can host a local web page. +Navigate to `localhost` in your web browser. If you ran this in the `web` directory, you should see our `index.html` page displayed. I've tried to find a simple, built-in way to start a local web server on Windows, and I haven't really found one. I personally use Python 3, but you are welcome to use any other similar service, such as `npm` or even some Visual Studio Code extensions. It doesn't matter which, just so they can host a local web page. ## Defining our WebAssembly API -Here are the broad steps we are going to take to create our browser emulator. We have our `chip8_core` created already, but we are now missing all of the functionality we added to `desktop`. Loading a file, handling key presses, telling it when to tick, etc. On the other hand, we have a web page that (will) run JavaScript, which needs to handle inputs from the user and display items. Our `wasm` crate is what goes in the middle. It will take inputs from JavaScript and convert them into the data types required by our `chip8_core`. +We have our `chip8_core` created already, but we are now missing all of the functionality we added to `desktop`. Loading a file, handling key presses, telling it when to tick, etc. On the other hand, we have a web page that (will) run JavaScript, which needs to handle inputs from the user and display items. Our `wasm` crate is what goes in the middle. It will take inputs from JavaScript and convert them into the data types required by our `chip8_core`. -Most importantly, we also need to somehow create a `chip8_core::Emu` object and keep it in scope for the entirety of our web page. This is also where that happens. +Most importantly, we also need to somehow create a `chip8_core::Emu` object and keep it in scope for the entirety of our web page. To begin, let's include a few external crates that we will need to allow Rust to interface with JavaScript. Open up `wasm/Cargo.toml` and add the following dependencies: @@ -299,7 +299,7 @@ const input = document.getElementById("fileinput") All of this will look familiar from our `desktop` build. We fetch the HTML canvas and adjust its size to the dimension of our Chip-8 screen, plus scaled up a bit (feel free to adjust this for your preferences). -Now to the meat of it! Let's create a main `run` function that will load our `EmuWasm` object and handle the main emulation. +Let's create a main `run` function that will load our `EmuWasm` object and handle the main emulation. ```js async function run() { @@ -357,7 +357,7 @@ function mainloop(chip8) { This function adds an event listener to our `input` button which is triggered whenever it is clicked. Our `desktop` frontend used SDL to manage not only drawing to a window, but only to ensure that we were running at 60 FPS. The analogous feature for canvases is the "Animation Frames". Anytime we want to render something to the canvas, we request the window to animate a frame, and it will wait until the correct time has elapsed to ensure 60 FPS performance. We'll see how this works in a moment, but for now, we need to tell our program that if we're loading a new game, we need to stop the previous animation. We'll also reset our emulator before we load in the ROM, to ensure everything is just as it started, without having to reload the webpage. -Following that, we look at the file that the user has pointed us to. We don't do any fancy checking to see if it's actually a Chip-8 program, but we do need to make sure that it is a file of some sort. We then read it in and pass it to our backend via our `EmuWasm` object. Once the game is loaded, we can jump into our main emulation loop! +Following that, we look at the file that the user has pointed us to. We don't need to check if it's actually a Chip-8 program, but we do need to make sure that it is a file of some sort. We then read it in and pass it to our backend via our `EmuWasm` object. Once the game is loaded, we can jump into our main emulation loop! ```js function mainloop(chip8) { @@ -380,7 +380,7 @@ function mainloop(chip8) { } ``` -This should look very similar to what we did for our `desktop` frontend, minus the SDL specific stuff, so I'll only briefly mention it. We tick several times before clearing the canvas and telling our `EmuWasm` object to draw the current frame to our canvas. Here is where we tell our window that we would like to render a frame, and we also save its ID for if we need to cancel it above. The `requestAnimationFrame` will wait to ensure 60 FPS performance, and then restart our `mainloop` when it is time, beginning the process all over again. +This should look very similar to what we did for our `desktop` frontend. We tick several times before clearing the canvas and telling our `EmuWasm` object to draw the current frame to our canvas. Here is where we tell our window that we would like to render a frame, and we also save its ID for if we need to cancel it above. The `requestAnimationFrame` will wait to ensure 60 FPS performance, and then restart our `mainloop` when it is time, beginning the process all over again. ## Compiling our WebAssembly binary @@ -396,7 +396,7 @@ Running the page in a local web server should allow you to pick and load a game ## Drawing to the canvas -Here is the final step, rendering to the screen. We created an empty `draw_screen` function in our `EmuWasm` object, and we call it at the right time, but it currently doesn't do anything. Now, there are two ways we could handle this. We could either pass the frame buffer into JavaScript and render it, or we could obtain our canvas in our `EmuWasm` binary and render to it in Rust. Either method would work fine, but personally I found that handling the rendering in Rust is actually easier personally. +Here is the final step, rendering to the screen. We created an empty `draw_screen` function in our `EmuWasm` object, and we call it at the right time, but it currently doesn't do anything. Now, there are two ways we could handle this. We could either pass the frame buffer into JavaScript and render it, or we could obtain our canvas in our `EmuWasm` binary and render to it in Rust. Either method would work fine, but personally I found that handling the rendering in Rust is easier. We've used the `web_sys` crate to handle JavaScript `KeyboardEvents` in Rust, but it has the functionality to manage many more JavaScript elements. Again, the ones we wish to use will need to be defined as features in `wasm/Cargo.toml`.