Computer Emulation: CHIP-8 interpreter, timers and input processing – InformTFB

Computer Emulation: CHIP-8 interpreter, timers and input processing

Computer Emulation: CHIP-8 interpreter, timers and input processing

We have already created a fully working CHIP-8 emulator, but it, unfortunately, turned out to be very slow. Why? If you look at its main loop — you can see that data is displayed on the screen after each step of the loop is completed. When vsync is enabled, SDL tries to link the rendering speed to the display frame refresh rate (possibly 60 Hz). For us, this means that the sdlviewer method::Update, almost every time it is called, will be in a locked state for a long time, waiting for the vsync signal from the monitor.

How fast should our emulator run? The exact answer to this question is not so easy to give. On a real computer, it takes different time to perform operations with different codes, but approximate time indicators for performing various operations are known. You can execute the instruction, find out how much time it should take to execute on real hardware, and then” put to sleep ” the program until the moment when you can continue working. But there is one problem, which is that we, with this approach, do not have access to the temporary parameters of the CPU. Most of these instructions should take a couple of microseconds to complete, but on modern systems, programs can be “put to sleep” for at least one millisecond.

However, we can do otherwise. It is known that an average of 540 operations should be performed within a second. Of course, this will not be the case if each of the instructions is something complex, like the output of graphics, but in real programs this approach is viable. We also know that CHIP-8 computers are designed for a screen refresh rate of 60 Hz. This means that our emulator must wait until the next vsync for as long as it takes to execute 9 (540/60) instructions.

As a result, we need to rewrite the main loop so that before each call to sdlviewer::Update would be carried out 9 of the instructions. And then we will use our own screen refresh rate to adjust the time parameters of the emulator. Let’s try to do this:

// main.cpp

void Run() {
  ...
  while (!quit) {
    for (int i = 0; i < 9; i++) {
      cpu.RunCycle();
    }
    cpu.GetFrame()->CopyToRGB24(rgb24, /*r=*/255, /*g=*/0, /*b=*/0);
    viewer.SetFrameRGB24(rgb24, height);
    auto events = viewer.Update();
    ...
  }
}

However, if you try to run the emulator on a computer whose screen refresh rate is greater than 60 Hz, the hardware characteristics will no longer match the parameters of the emulator. This will cause the emulator to run too fast. If you encounter this problem — you can also emulate vsync. It is known that executing 9 instructions and rendering a frame should take 16.67 ms (1000/60). Therefore, you can perform these operations, measure the time required for their execution, and then “put to sleep” the program for as long as necessary. Since this time can not be calculated accurately enough, you can measure the execution time of 540 operations (60 simulated screen refresh cycles) and” put to sleep ” the program before a new second in order to make the necessary adjustments to the duration of vsync-“sleep”. This is exactly what I did in the original version of the emulator that this series of articles is based on. In that project, in addition, a separate thread was used to emulate the CPU. This is probably not necessary, but it turned out, all the same, very interesting.

Now, when the emulator executes 540 instructions per second, support for CPU timers is implemented quite simply. The CHIP-8 has two timers: a delay timer and a sound timer. The values of both timers are reduced by 1 60 times per second. The computer makes sounds until the sound timer is 0. Now you can simply reduce the values stored in the timers by 1 every time 9 instructions are executed. This will give us the desired speed of the timers:

// cpu_chip8.cpp

void CpuChip8::RunCycle() {
  ... 
  // Обновление значений, хранящихся в таймерах
  num_cycles_++;
  if (num_cycles_ % 9 == 0) {
    if (delay_timer_ > 0) delay_timer_--;
    if (sound_timer_ > 0) {
      std::cout << "BEEPING" << std::endl;
      sound_timer_--;
    }
  }
}

We just need to talk about entering data into the system. This is a simple task that boils down to handling events returned from sdlviewer::Update. As already mentioned, the CHIP-8 uses a 16-key keyboard. These keys can be linked to any buttons on a regular keyboard. Here is the code snippet responsible for processing input:

// main.cpp

...
auto events = viewer.Update();
uint8_t cpu_keypad[16];
for (const auto& e : events) {
  if (e.type == SDL_KEYDOWN || e.type == SDL_KEYUP) {
    if (e.key.keysym.sym == SDLK_1) {                        
      cpu_keypad[0] = e.type == SDL_KEYDOWN;
    } else if (e.key.keysym.sym == SDLK_2) {                        
      cpu_keypad[1] = e.type == SDL_KEYDOWN;
    } else if (e.key.keysym.sym == SDLK_3) {                        
      cpu_keypad[2] = e.type == SDL_KEYDOWN;
    } else if (e.key.keysym.sym == SDLK_4) {                        
      cpu_keypad[3] = e.type == SDL_KEYDOWN;
    } else if (e.key.keysym.sym == SDLK_q) {                        
      cpu_keypad[4] = e.type == SDL_KEYDOWN;
    }
    ...
  }
}
cpu.SetKeypad(cpu_keypad);

// cpu_chip8.cpp
void CpuChip8::SetKeypad(uint8_t* keys) {
  std::memcpy(keypad_state_, keys, 16);
}

Results

Our emulator is ready. We have created a full-fledged CHIP-8 interpreter. It supports a monochrome frame buffer and a system that converts the data stored in this buffer into RGB images that are transmitted to the GPU as textures. And just now we have configured the temporary parameters of the emulator and connected a subsystem to it for processing input.

I don’t know about you, but I can say about myself that I learned a lot while working on this project. I really liked the feeling of creating something from scratch and bringing it to completion. And this project is especially good because, after finishing work on the emulator, I was able to run real programs for CHIP-8 on it. After completing this project, I am one step closer to understanding what is happening in the depths of Super Mario World.

Valery Radokhleb
Valery Radokhleb
Web developer, designer

Leave a Reply

Your email address will not be published. Required fields are marked *