Monday, July 10, 2017

The First Week


One week...

...and a surprising amount of productivity.

I feel like it's been forever since the first post when I actually decided to commit to the project.  Nonetheless, I've begun coding like a crazy person and got pretty far for a week.  In this post I'll discuss the architecture of the animal3D framework and the features that I've implemented so far.  Don't expect any fancy diagrams, you'll get screenshots at least.

Framework & Pipeline Architecture

First, I should mention that I was debating building animal3D for both Windows (Visual Studio) and Mac (Xcode).  Given the time constraints, I decided to stick with Windows for the time being, since that's what my students and I will be using approximately 100% of the time.

That being said, I began with a new solution in Visual Studio 2015 (I like to stay one version behind for compatibility), to which I added 3 projects: 
  1. Static library animal3D.lib, which is where all of the built-in graphics and animation utilities will live; 
  2. Dynamic library animal3D-DemoProject.dll, which is where actual demo code will be developed (e.g. a game or animation concept demo); and 
  3. Win32 application animal3D-LaunchApp.exe, which is the actual windowed application that renders the active demo.
The static library is linked to the dynamic library, which, when compiled, behaves as a free-floating "package" that can be hot-reloaded into the window at run-time.

Windowing & Hot Reloading

I've been interested in learning how hot reloading works for a long time, so this was a perfect opportunity to explore.  I must say, I figured it out faster than I thought I would, and it's super useful.  In short, hot reloading (a.k.a. hot swapping or code injection) is when code is recompiled and linked while an application is still running, thus changing the behavior of the app in real-time.  Unreal Engine and Unity3D are prime examples of engines with this feature.  With this implemented in a C-based framework, the C language effectively becomes a scripting language, only requiring a few seconds to rebuild and inject new code into a running app.

Windowing

My first task was to get a window on the screen.  One of my initial visions for the framework was to not use any other frameworks, so I did this from scratch.  I dove back into one of my older frameworks in C++ and translated its windowing classes into function-based C.  Just regular old Win32 programming.  The result was a window with an OpenGL rendering context and a menu.  There is also a console window so printf can be used.  All of this is implemented as "platform-specific" code in the launcher app project.

The menu is very small and used strictly for debugging.  I tried making a dialog box from scratch but that proved to be overly-complicated.  Besides, a window menu is directly integrated in the window and is always there in case the user wants to change something.  The above screenshot shows all of the window's options: load one of the available pre-compiled demo DLLs (hot loadable but not debuggable); shutdown and immediately reload the current demo; hot load the debuggable demo project DLL with an update build or full rebuild; reload the menu (in case new demo DLLs appear); and exit the current demo or app all together.  Exiting can be programmed into a demo, i.e. using an exit button or a quit key press; this is explained briefly below.

The important thing to note about windowing in Win32 is that there is a "message loop" or "event loop" in which messages from the window are processed.  Messages that should be responded to in some way call external functions, known as callbacks, which give the client a chance to respond to an event, such as a key press or the window being resized.  The demo project is entirely responsible for handling its own callbacks; this is described next.

Demo Information


When the 'load,' 'build and hotload' buttons are used, the first thing that happens is that a text file is loaded.  This text file explains the callbacks that the demo has available, and the name of the function that should be called when a particular callback occurs.  This screenshot shows an example.  Long story short: the name of the demo, the DLL to load, the number of callbacks implemented, and the list of named callbacks with a pre-defined "hook" function that the callback represents.  For example, this screenshot says that a function called "a3test_load" should be used when the 'load' event is triggered, "a3test_unload" should be called when the 'unload' event is triggered, etc.  It's an easy, scriptable way for users to be able to write their own callback functions and tell animal3D which one maps to which callback.  Documentation is provided with the framework that explains how to write the file and what the callbacks should have for their return types and arguments.  The next section explains how this is actually used.

Hot Reload

Believe it or not, this part is actually simpler than it sounds.  After reading a description of the demo to be loaded, the application loads the specified dynamic library (DLL) into process memory and links its functions using function pointers.  Before this happens, the window callbacks just point to dummy functions, but all that changes once the library is loaded is that the window will call user-defined functions.  I also wrote a batch script to run Visual Studio's build tool when this happens so that one could also change the code and re-compile without having to close the window.  The gif at the top of the page shows this in action: the window starts off rendering a black-to-white pulse effect, the user selects the hot reload option from the menu, and after a few seconds the window starts rendering a sweet rainbow.

One might ask, "But Dan, can I still debug my demo after hot loading?"  Yes, thankfully.  One of the main struggles with creating this feature was that Visual Studio locks all PDB files, including those of dynamically-loaded modules, as long as the process is being debugged... even if a module has been released.  To "fix" this, there is an option in Visual Studio's global debugging settings called "Use Native Compatibility Mode" which one must check to bring the debugging format back in time a few years, thereby magically allowing PDBs to be freed when their respective module is released (found out about this here).  I had conceived an over-complicated naming system, but at the end of the day there were still piles of PDBs being activated and locked, which triggered me a little bit.  As long as I have my breakpoints working after reloading, I'm happy!

Pipeline Summary

Long story short, with animal3D you're given a "standardized" render window whose job is to call upon a user-built DLL to do the real-time tasks.  The programmer just fills in a bunch of named callbacks in the DLL, writes up a line of text describing said callbacks, and lets the window figure out the rest.  The whole point of this was to a) get some basic rendering working, and b) streamline development without having to continuously restart the app to change something.

Additional Features

All of the above describes the architecture of and relationship between the dynamic library and the windowing application.  The static library has a few features of its own to start: 
  • Keyboard and mouse input: 
    • Ah yes, the classics.  I built simple state trackers for the keyboard and mouse, which can be modified in the respective callbacks and queried in other functions.
  • Xbox 360 controller input: 
    • A wrapper for XInput so that controllers will work, and states can be tracked and queried with ease.  I deemed this as a priority because, eventually, animation should be controlled using a joystick.  What better way to show off transitioning between walking and running, jumping, attacking, etc.
  • Text rendering: 
    • Simple text drawing within the window for a basic real-time HUD instead of having to rely on the console window.
  • Threading: 
    • A basic thread launcher function and thread descriptor.  The user passes a function pointer and some arguments, which get transformed into a thread.  Animation tasks may be delegated to a separate thread from rendering... which may also have a thread of its own!  An example of threading can be seen in the gif above: the text spewing out to the console is a threaded function in action.
  • High-precision timer: 
    • The cornerstone of any renderer: a decent frame rate.  Here it's as simple as starting a timer with a desired update rate and updating it every time the idle function is called; if it ticks, it's time to do a render.
  • File loading: 
    • A very simple wrapper for reading a file and storing the contents as a string.  This will come in handy for loading things like... shaders!

On to the next round...

Next up: rendering utilities, so that demos can actually be interesting and have stuff showing up.
  • Shaders
  • Vertex buffers and arrays
  • Framebuffers (why not)
  • Textures
  • Procedural geometry
If I can get through all of these this week, I'll be super happy and farther ahead than I expected to be at this point.  I expect to be working on the actual animation demos a couple weeks from now.

Until next time... I'll be programming like an animal!

No comments:

Post a Comment