Dear ImGui, Thanks, From TEMU – The TPU Emulator

This is part of a series of posts detailing the steps and learning undertaken to design and implement a CPU in VHDL. Previous parts are available here, and I’d recommend they are read before continuing.

A few weeks ago I was in San Francisco for the Game Developers Conference (GDC). I decided not to take my MiniSpartan6+ board with me, despite wanting to get more work on TPU completed. Bare circuit boards don’t look good in luggage, etc.

I did however have an idea on the flight over from London Heathrow, so created a new Visual Studio project: temu, the TPU Emulator. I’ve been working on HDMI output of a small framebuffer in the VHDL, so thought I could make a compact emulator which would draw the framebuffer to a window whilst executing some TPU code. This would allow me to get some demos up and running quickly, so that when I went back to the VHDL, I could hit the ground running.

consoleoutAnnoyingly, I forgot to bring the latest TPU ISA document with me on my trip, and the version on GitHub is hilariously out of date. I did, however, have the latest HDL and the TPU assembler, tasm, so I worked backwards from there. The emulator itself is basic – I implemented the most important instructions, ignoring some of the smaller flags (such as signed addition, I don’t need any of the status flags of that for now). The emulator used stdout and just printed the current PC with the instruction it emulated, and in the case of memory operations output the values written along with the locations. I used SDL to open a window, and each frame processed the data in my ‘vram’ memory, writing pixels directly to the surface.

The main function simply had the standard SDL loop, with a WriteSurface() call which trivially wrote pixel values to the window, converting from my 565 16-bit pixel format to what is required of the SDL surface. The emulation happened in another thread. That other thread just loops until exit performing the emulation of current pc, then setting the next pc. There is no synchronization required, as the main thread simply reads the fake vram (simply a char array). The thread is simply spawned, and allowed to emulate alongside the SDL loop.

justvramIt worked well, until it didn’t. It wasn’t a threading issue, it worked great. The issue is I wanted more. I needed a tool, more than simply an emulator. I wanted single step, memory inspection. I wanted a debugger inside my emulator.

Dear ImGui

I had the emulator working by the time GDC began proper, and was wondering what the next steps would be to make it more usable. I knew I needed some sort of real-time interaction, but didn’t want all the hassle of implementing that myself. I was at a GDC breakfast gathering of developers, and Omar (@ocornut) was there, who created a library I’d heard of but never used before: Dear ImGui. It is a cross-platform Immediate Mode GUI library. The answer I was looking for was staring me in the face. Switch SDL to OpenGL mode, and use Dear ImGUI to add an interactive interface to my emulator.

Later in the day when I had some spare time I tried to get things working. Within 15 minutes (and I really do mean only 15 minutes) I’d downloaded the sources required, integrated them into my temu project, and got a window showing FPS and the representation of my VRAM. Most of those 15 minutes, may I add, was looking up how to create and re-upload texture data in OpenGL – its been so long since I’d done any OpenGL coding I’d forgot the basics.

The architecture of the emulator (if you can call it that) is still as before: The SDL OpenGL loop and ‘vram’ texture update remains in the main thread, with the emulation happening in another. I keep to the single producer single consumer with no timing or order constraints, so no synchronization is needed.

vramThe code for my VRAM window is trivial. It’s ever so slightly changed from a sample, used to show drawing an image. It fits my requirements perfectly.

void DrawWindow_Vram()
{
  if (window_vram) {
    ImGui::Begin("VRAM Representation", &window_vram);
    ImGui::Image(ImTextureID(textureID), ImVec2(VIRTUAL_SCREEN_HEIGHT, VIRTUAL_SCREEN_WIDTH));
    ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
    ImGui::End();
  }
}

This code is called each frame. All I need to do is set window_vram to true, and the window is shown. Pressing the close window button will set the bool pointed to in the second argument of Begin() to false, which closes the window. A button elsewhere can set window_vram back to true to show the window again. All ‘widgets’ within the Begin and End calls are ordered sequentially, and there are constructs for ordering things in columns, frames, lines etc. It’s all very straightforward. There is a large demo here which shows the basics and advanced layouts and how they fit together.

This library is brilliant. As someone who has written GUI and UI system code in the past, this has the concept nailed.

Once I realized how easy it was to add different windows, a control window popped up, allowing me to stop execution, resume it, and single-step. It also controlled an arbitrary millisecond delay before each instruction was emulated.

control

void DrawWindow_Control()
{
  if (window_control)
  {
    ImGui::Begin("TPU control", &window_control, ImGuiWindowFlags_MenuBar);
    ImGui::BeginMenuBar();
    ImGui::Text("Status: %s", (gStatePaused) ? ((gStateSingleStepping) ? "Single Stepping":"Stopped") : "Running...");
    ImGui::EndMenuBar();
    		
    if (!gStatePaused)
    {
      if (ImGui::Button("Stop")) 
      {
        gStatePaused = true;
      }
    }
    else
    {
      if (ImGui::Button("Continue")) 
      {
        gStatePaused = false;
      }
    }
    ImGui::SameLine();
    if (ImGui::Button("Single Step"))
    {
      gStateSingleStepping = true;
    }

    ImGui::SliderInt("Cycle Delay (ms)", &gSeepTime, 0, 100);
    ImGui::Checkbox("Status Prints (stdout)", &gStatusPrints);
    ImGui::End();
  }
}

This rocks.

startAfter this, I thought of what else to do. So I added quite a lot of extra functionality. Im just going to go through the windows that now exist in the TPU emulator, giving a bit of blurb about each.

Registers

registersThe registers window is pretty obvious. I’d like to add to it, including some internal registers – like status, which tracks overflow and carry style information.

Memory Map

memory_mapThe memory map/layout is essential information when developing TPU programs, since it can change so often when the top-level VHDL is edited. Things like block rams and memory-mapped registers/signals, like what is used for button inputs and LED output, need to be placed in the correct memory-mapped offsets. This window serves this purpose. At the moment, it’s fixed, but it’s designed to be extensible, in that you can add another block ram at a certain place, define whether its read/write or read and write, and then view how it fits into the whole scheme of things. A further feature I’d like to implement is a generation button which outputs top-level vhdl for a memory management unit, which will take the memory bus signals from TPU and map that to the various areas, providing the relevant clock enables and other state.

Memory Viewer

memoy_viewFrom the Memory Layout window, clicking view on any mapped element opens a memory viewer. This widget is available on the ImGui wiki on github.

Interactive Assembler

assemblerThe interactive assembler allows the TPU code to be updated within the emulator, at runtime. There is a small multi-line text box, containing the contents of an assembly file. There is an assemble button which when clicked invokes TASM on the file, opting to output as a binary blob, rather than the VHDL block-ram initializer format I usually use. This binary is then loaded into ram starting at address 0x0000. This address (at present) is fixed, more due to limitations in TASM than anything else.

This allows some really easy development. I can change constants and swap instructions on the fly without even resetting the emulator state, so in the case of drawing colours to the screen the results are instantly visible. For other changes it’s wise to either pause execution, or reset the TPU (Set the PC to 0 and re-initialize all memories), but it’s still miles away from my previous workflow which was insanely tedious, involving VHDL synthesis steps which take minutes.

Buttons

buttons Buttons! There needed to be input somewhere, and there are 4 input switches on my miniSpartan6+ board. I decided to expand that for the emulator, simply due to the fact that I can add more I/O to the hardware device anyway. The buttons are memory mapped to a single 16-bit word, and there is a window showing a checkbox for each bit.

Leds

ledsAs with the buttons, LEDs provide quick and easy visible status output on my development board, and so they have a window in the emulator. 8 LEDS map to a single byte in the memory space. The code for it, again, is trivial.

void DrawWindow_OutputLeds()
{
  // Storage for the led status values, ImGui expects bools
  static bool ledvals[8];

  if (window_outputleds) {
    // Expand the bit values in the memory_leds mapped area to booleans
    for (uint32_t i = 0; i < 8; i++)
    {
      ledvals[i] = (memory_leds & (1 << i))?true:false;
    }
  
    ImGui::Begin("Output Leds", &window_outputleds);
    ImGui::Text("Mapping: 0x%04X", memory_mapping_leds);
  
    // Arrange the checkboxes and text labels in 8 columns
    ImGui::Columns(8, 0, false);
    // Make the check colour green, a much better LED colour
    ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0, 1, 0, 1));
  
    for (int i = 0; i < 8; i++) {
      ImGui::PushID(i);
      ImGui::Text("%d", 7-i);
      ImGui::Checkbox("", &ledvals[7-i]);
      ImGui::NextColumn();
      ImGui::PopID();
    }
  
    // undo the change to the checkmark colour
    ImGui::PopStyleColor();
    ImGui::End();
  }
}

Interrupt Firer

interrupt_firerThe next window is for testing interrupt routines. You can set what the Interrupt Event Field value will be (what is read externally off the data bus in the hardware on a interrupt ACK) and then signal to the emulator that an interrupt is waiting to be requested. If interrupts are disabled in the current emulator state, this can be forced to enabled from here, and a small status console shows the stage of the interrupt, such as ‘Waiting for ACK’.

Breakpoint Set

breakpointI mentioned debuggers earlier, and this window allows the emulator to get closer to that goal. A single breakpoint can be set by PC value, and enabled. There is nothing more than that just now – although it can be easily expanded to multiple breakpoints. An issue in the emulator at the moment is one which crops up in real debuggers, in that to continue from the breakpoint you need to disable the breakpoint, single step, re-enable the breakpoint, and only then continue. Something to fix later, when it causes me more of a headache than it does now.

PC Hit Counts

histogramAnother window came about from me looking over the ImGui demo, and trying to see what built-in widgets could add some simple functionality. The histogram seemed a winner almost instantly, and it proved itself when I came to try out the interrupt firer feature. The emulator, when executing instructions, grows an std::vector to accommodate the location of the PC. It then increments the value in the bin located at that PC. With this, the histogram is implemented with a single library function:

if (pc_hitcounts.size() > 0) {
  ImGui::PlotHistogram("##histo", &pc_hitcounts[0], pc_hitcounts.size(), 0, nullptr, 0.0f, pc_maxhits / hist_zoom, ImVec2(0, 140));
}

It’s basically a PC hit counter, so is a very simple profiler. Your hot code is the high bars, but you can also use it to identify code coverage issues. For instance, ensuring that the exception handler code you’ve written actually gets executed when you fire an interrupt.

I made a simple video of me using TEMU, showing some of the features and how they work and help things. It’s really helped speed up how quickly I can write TPU assembly, which in turn means I can develop new hardware features quicker. I aim to fix the emulator up (things like project files don’t really exist just no) and upload it to github eventually.

Thanks for reading! Let me know what you think by messaging me on twitter @domipheus, and generally pour praise in the direction of @ocornut for his wonderful library – you can support him via Patreon 🙂