Building an Emulator with WebAssembly and Rust

MAKERbuino Gamebuino

Microcontroller

  • 32-bit ARM Cortex-M0+ @ 48MHz
  • 256KB flash
  • 32KB SRAM
  • 6 serial communication modules (SERCOM)
  • Timers and interrupts
  • Direct memory access (DMA) controller
Instruction set

Screen

  • 160 × 128 pixels
  • 65K colors
  • Communicates over SERCOM4 using SPI

Buttons

  • Communicates over SERCOM4 using SPI
  • D-pad, A, B, Menu, and Home all encoded as a single byte

Ignored

  • Sound
  • SD card
  • Lights
  • USB

                        listen for button input
                        initialize emulator with game ROM
                        call gameLoop()

                        function gameLoop() {
                            tell emulator which buttons are pressed
                            loop for some number of steps {
                                simulate next CPU instruction
                            }
                            draw screen to <canvas>
                            schedule gameLoop() to be called in the future
                        }
                    

Why?


                        (module
                          (func (export "add") (param $lhs i32) (param $rhs i32)
                            (result i32)
                            (i32.add
                              (local.get $lhs)
                              (local.get $rhs)
                            )
                          )
                        )
                    

                        <!DOCTYPE html>
                        <html>
                        <head> … </head>
                        <body>
                          <script>
                            (async function() {
                              const obj = await WebAssembly.instantiateStreaming(
                                fetch("add.wasm")
                              );
                              console.log(obj.instance.exports.add(1, 2)); // 3
                            })();
                          </script>
                        </body>
                        </html>
                    

                    (module
                        (import "js" "mem" (memory 1))
                        (data (i32.const 0) "HAL")
                        (func (export "doSomething")
                            (local $i i32)
                            (loop $loop
                                (i32.store8 
                                    (local.get $i)
                                    (i32.add
                                        (i32.load8_u
                                            (local.get $i)
                                        )
                                        (i32.const 1)
                                    )
                                )
                                (local.set $i
                                    (i32.add
                                        (i32.const 1)
                                        (local.get $i)
                                    )
                                )
                                (br_if $loop
                                    (i32.lt_u
                                        (local.get $i)
                                        (i32.const 3)
                                    )
                                )
                            )
                        )
                    )
                    

Rust + WebAssembly = ❤

  • wasm-pack
  • Small runtime, no garbage collector
  • Low-level control
  • Focus on safety
  • Modern features
  • Error messages and documentation

                        fn fizz_buzz_1() {
                            for i in 1..=30 {
                                if i % 3 == 0 && i % 5 == 0 {
                                    println!("FizzBuzz");
                                } else if i % 3 == 0 {
                                    println!("Fizz");
                                } else if i % 5 == 0 {
                                    println!("Buzz");
                                } else {
                                    println!("{}", i);
                                }
                            }
                        }
                    

                        fn fizz_buzz_2() {
                            (1..=30).for_each(|i| match get_special_message(i) {
                                Some(msg) => println!("{}", msg),
                                None => println!("{}", i),
                            });
                        }
                        
                        fn get_special_message(i: isize) -> Option<&'static str> {
                            if i % 3 == 0 && i % 5 == 0 {
                                Some("FizzBuzz")
                            } else if i % 3 == 0 {
                                Some("Fizz")
                            } else if i % 5 == 0 {
                                Some("Buzz")
                            } else {
                                None
                            }
                        }
                    

                        pub struct St7735 {
                            …
                            image_data: [u32; St7735::WIDTH * St7735::HEIGHT];
                        }
                        
                        impl St7735 {
                            …
                            pub fn byte_received(&mut self, value: u8, 
                                porta: &PortRegisters, portb: &PortRegisters) {
                                …
                                let r = (0b1111100000000000 & pixel_data) >> 8;
                                let g = (0b0000011111100000 & pixel_data) >> 3;
                                let b = (0b0000000000011111 & pixel_data) << 3;
                                let color = (255 << 24) | // alpha
                                            (b   << 16) | // blue
                                            (g   <<  8) | // green
                                             r;           // red
                                …
                                self.image_data[base_index] = color;
                            }
                        }
                    

                        step(timestamp) {
                            … // Snip code to calculate number of iterations
                    
                            // Run number of emulated cycles equal to `iterations`
                            this.gamebuino.run(iterations, this.buttonState);
                    
                            // Draw to canvas
                            let buf8 = new Uint8ClampedArray(
                                memory.buffer,                  // buffer
                                this.gamebuino.image_pointer(), // byteOffset
                                160 * 128 * 4                   // length
                            );
                            this.imageData.data.set(buf8);
                            this.ctx.putImageData(this.imageData, 0, 0);
                            this.ctx.drawImage(this.canvas, 0, 0);
                    
                            // Request next step
                            this.requestId = 
                                requestAnimationFrame(t => this.step(t));
                        }
                    

            fn execute_instruction(&mut self,
                                   instruction: Instruction) {
                match instruction {
                    Instruction::LslImm { rs, rd, offset } => {
                        let original = self.read_register(rs);
                        let result = original << offset;
                        self.set_register(rd, result);
                        self.cond_reg.c = 
                            original & (1 << offset) != 0;
                        self.set_nz(result);
                    }
                    …
                    Instruction::Beq { offset } => {
                        if self.cond_reg.z {
                            self.set_register(PC_INDEX, 
                              self.read_register(PC_INDEX) + offset);
                            self.increment_pc();
                        }
                    }
                    …
                }
            }
                    

Links