Hello Scene — Timing, Recording, Keyboarding
If there’s no recording, did it happen? One of the most useful features of the demo scene is the ability to record what’s being generated on the screen. There are plenty of ways to do that, external to the program. At the very least though, you need to be able to save individual frames.
But, I’m getting slightly ahead of myself.
First, we need to revisit the main event loop and see how it is that we get regularly timed ‘frames’ in the first place. Here is the code that’s generating that Bezier animation, bezeeaye.cpp.
#include "gui.h" #include "sampler.h" #include "sampledraw2d.h" int segments = 50; // more segments make smoother curve int dir = 1; // which direction is the animation running RainbowSampler s(1.0); // Sample a rainbow of colors int currentIteration = 1; // Changes during running int iterations = 30; // increase past frame rate to slow down void drawRandomBezier(const PixelRect bounds) { // clear to black gAppSurface->setAllPixels(PixelRGBA(0xff000000)); // draw axis line line(*gAppSurface, bounds.x, bounds.y+bounds.height / 2, bounds.x+bounds.width, bounds.y + bounds.height / 2, PixelRGBA(0xffff0000)); int x1 = bounds.x; int y1 = bounds.y + bounds.height / 2; int x2 = (int)maths::Map(currentIteration,1, iterations, 0, bounds.x + bounds.width - 1); int y2 = bounds.y; int x3 = (int)maths::Map(currentIteration, 1, iterations, bounds.x+bounds.width-1, 0); int y3 = bounds.y+bounds.height-1; int x4 = bounds.x+bounds.width-1; int y4 = bounds.y+bounds.height / 2; sampledBezier(*gAppSurface, x1, y1, x2, y2, x3, y3, x4, y4, segments, s); // Draw control lines line(*gAppSurface, x1, y1, x2, y2, PixelRGBA(0xffffffff)); line(*gAppSurface, x2, y2, x3, y3, PixelRGBA(0xffffffff)); line(*gAppSurface, x3, y3, x4, y4, PixelRGBA(0xffffffff)); currentIteration += dir; // reverse direction if needs be if ((currentIteration >= iterations) || (currentIteration <= 1)) dir = dir < 1 ? 1 : -1; } void setup() { setCanvasSize(800, 600); setFrameRate(15); } void keyReleased(const KeyboardEvent& e) { switch (e.keyCode) { case VK_ESCAPE: halt(); break; case VK_UP: iterations += 1; break; case VK_DOWN: iterations -= 1; if (iterations < 2) iterations = 2; break; case 'R': recordingToggle(); break; } } void onFrame() { drawRandomBezier({ 0,0,canvasWidth,canvasHeight });
It’s a little different than we’ve seen before. First off, at the top, we see ‘include “gui.h“‘. Up to this point, we’ve only been including ‘apphost.h‘, which has served us well in terms of offering just enough to get the job done of putting a window on the screen, and dealing with mouse and keyboard input through a publish/subscribe eventing system. Well, gui.h offers another level of abstraction/simplicity. As we’ve seen before, you are not forced to use this level of convenience, but if you find yourself writing apps that use mouse and keyboard often, and you want a timing system, gui.h makes it much easier.
There are three primary functions being used from gui.h, setup(), keyReleased(), onFrame(). So let’s look a little deeper into gui.h
// // gui.h // // // apphost.h/appmain.cpp give a reasonable core for a windows // based program. It follows a pub/sub paradigm for events, which // is pretty simple to use. // // gui.h/.cpp gives you a function based interface which which // is similar to p5 (processing), or other very simple APIs // If you want something very simple, where you can just implement // the functions that you use, include gui.h in your application. // #include "apphost.h" #include "sampledraw2d.h" #include "recorder.h" #pragma comment (lib, "Synchronization.lib") #ifdef __cplusplus extern "C" { #endif // Application can calls these, they are part of the // gui API APP_EXPORT void fullscreen() noexcept; APP_EXPORT void background(const PixelRGBA &c) noexcept; APP_EXPORT void setFrameRate(const int); // Application can call these to handle recording APP_EXPORT void recordingStart(); APP_EXPORT void recordingStop(); APP_EXPORT void recordingPause(); APP_EXPORT void recordingToggle(); // Application can implement these // Should at least implement setup(), so canvas size // can be set APP_EXPORT void setup(); // If application implements 'onFrame()', it // is called based on the frequency of the // frame rate specified APP_EXPORT void onFrame(); // keyboard event processing // Application can implement these typedef void (*KeyEventHandler)(const KeyboardEvent& e); APP_EXPORT void keyPressed(const KeyboardEvent& e); APP_EXPORT void keyReleased(const KeyboardEvent& e); APP_EXPORT void keyTyped(const KeyboardEvent& e); // mouse event processing // Application can implement these typedef void (*MouseEventHandler)(const MouseEvent& e); APP_EXPORT void mouseClicked(const MouseEvent& e); APP_EXPORT void mouseDragged(const MouseEvent& e); APP_EXPORT void mouseMoved(const MouseEvent& e); APP_EXPORT void mousePressed(const MouseEvent& e); APP_EXPORT void mouseReleased(const MouseEvent& e); APP_EXPORT void mouseWheel(const MouseEvent& e); APP_EXPORT void mouseHWheel(const MouseEvent& e); #ifdef __cplusplus } #endif #ifdef __cplusplus extern "C" { #endif // These are variables available to the application // Size of the application area, set through // setCanvasSize() APP_EXPORT extern int width; APP_EXPORT extern int height; APP_EXPORT extern uint64_t frameCount; APP_EXPORT extern uint64_t droppedFrames; APP_EXPORT extern PixelRGBA* pixels; // Keyboard Globals APP_EXPORT extern int keyCode; APP_EXPORT extern int keyChar; // Mouse Globals APP_EXPORT extern bool mouseIsPressed; // a mouse button is currently pressed APP_EXPORT extern int mouseX; // last reported location of mouse APP_EXPORT extern int mouseY; APP_EXPORT extern int mouseDelta; // last known delta of mouse wheel APP_EXPORT extern int pmouseX; APP_EXPORT extern int pmouseY; #ifdef __cplusplus } #endif
Similar to what we had in apphost.h, there are some functions, that if implemented, will be called at appropriate times, and if they’re not implemented, nothing additional will occur. Additionally, as a convenience, there are some global variables that are available.
So, let’s look at some of the guts in gui.cpp
// Called by the app framework as the first thing // that happens after the app framework has set itself // up. We want to do whatever registrations are required // for the user's app to run inside here. void onLoad() { HMODULE hInst = ::GetModuleHandleA(NULL); setFrameRate(15); // Look for implementation of keyboard events gKeyPressedHandler = (KeyEventHandler)GetProcAddress(hInst, "keyPressed"); gKeyReleasedHandler = (KeyEventHandler)GetProcAddress(hInst, "keyReleased"); gKeyTypedHandler = (KeyEventHandler)GetProcAddress(hInst, "keyTyped"); // Look for implementation of mouse events gMouseMovedHandler = (MouseEventHandler)GetProcAddress(hInst, "mouseMoved"); gMouseClickedHandler = (MouseEventHandler)GetProcAddress(hInst, "mouseClicked"); gMousePressedHandler = (MouseEventHandler)GetProcAddress(hInst, "mousePressed"); gMouseReleasedHandler = (MouseEventHandler)GetProcAddress(hInst, "mouseReleased"); gMouseWheelHandler = (MouseEventHandler)GetProcAddress(hInst, "mouseWheel"); gMouseHWheelHandler = (MouseEventHandler)GetProcAddress(hInst, "mouseHWheel"); gMouseDraggedHandler = (MouseEventHandler)GetProcAddress(hInst, "mouseDragged"); gSetupHandler = (VOIDROUTINE)GetProcAddress(hInst, "setup"); gDrawHandler = (VOIDROUTINE)GetProcAddress(hInst, "onFrame"); subscribe(handleKeyboardEvent); subscribe(handleMouseEvent); // Start with a default background before setup // does something. background(PixelRGBA(0xffffffff)); // Call a setup routine if the user specified one if (gSetupHandler != nullptr) { gSetupHandler(); } // setup the recorder gRecorder.init(&*gAppSurface); // // If there was any drawing done during setup // display that at least once. refreshScreen(); }
Here is an implementation of the ‘onLoad()’ function. When we were just including ‘apphost.h’, our demo code implemented this function to get things started. If you include gui.cpp in your project, it implements ‘onLoad()’, and in turn looks for an additional set of dynamic functions to be loaded. In addition to mouse and keyboard functions, it looks for ‘setup()’ and ‘onFrame()’.
The ‘setup()’ function serves a similar role to ‘onLoad()’. It’s a place where the application has a chance to set the size of the canvas, and do whatever other setup operations it wants.
The ‘onFrame()’ function is where things get really interesting. We want to be able to have an event loop that is something like;
Perform various OS routines that must be performed
Check to see if it’s time to inform the app to draw a frame
Call ‘onFrame()’ if the time is right
Go back to main event loop
This all occurs in the ‘onLoop()’ function, which gui.cpp has an implementation for. Again, in previous examples, the application code itself would implement this function, and it would be called a lot, very rapidly, with no control over timing. By implementing the ‘onLoop()’ here, we can impose some semblance of order on the timing. It looks like this.
// Called by the app framework // This will be called every time through the main app loop, // after the app framework has done whatever processing it // needs to do. // // We deal with frame timing here because it's the only place // we have absolute control of what happens in the user's app // // if we're just past the frame time, then call the 'draw()' // function if there is one. // Otherwise, just let the loop continue // void onLoop() { // We'll wait here until it's time to // signal the frame if (fsw.millis() > fNextMillis) { // WAA - Might also be interesting to get absolute keyboard, mouse, // and joystick positions here. // frameCount += 1; if (gDrawHandler != nullptr) { gDrawHandler(); } gRecorder.saveFrame(); // Since we're on the clock, we will refresh // the screen. refreshScreen(); // catch up to next frame interval // this will possibly result in dropped // frames, but it will ensure we keep up // to speed with the wall clock while (fNextMillis <= fsw.millis()) { fNextMillis += fInterval; } } }
Pretty straight forward. There is an object “StopWatch”, which keeps track of the seconds that have gone by since the app was started. This basic clock can be used to set intervals, and check timing. That’s what’s happening here:
if (fsw.millis() > fNextMillis)
It’s fairly rudimentary, and you’re only going to get millisecond accuracy, at best, but for most demos, that are running at 30 – 60 frames per second, it’s more than adequate. Within this condition, if the user has implemented ‘onFrame()’, that function is called, then ‘refreshScreen()’ is called, and we’re done with our work for this loop iteration.
And that’s how we get timing.
You can set a frame rate with ‘setFrameRate(fps)’, wherein you set the number of frames per second you want your animation to run at. The default is 15 frames per second, which is good enough to get things going. Being able to maintain a certain frame rate is dependent on the amount of time you spend during your ‘onFrame()’ implementation. Your frame drawing is not pre-emted by the runtime, so if you take longer than your allotted time, you’ll begin to drop frames.
What you do within your ‘onFrame()’ is completely up to you. You can do nothing, you can draw a little bit, you can draw a lot. You can maintain your own state information, however you want. You can have a retained drawing context, or you can have a nothing retained context, it’s completely in your control.
On to Recording
So, great, now we have the ability to march to the beat of a drum. How do I record this stuff?
Looking again at gui.h, we see there are some functions related to recording.
// Application can call these to handle recording APP_EXPORT void recordingStart(); APP_EXPORT void recordingStop(); APP_EXPORT void recordingPause(); APP_EXPORT void recordingToggle();
The default recording offered by gui.cpp is extremely simple. The process is to essentially just save a snapshot of the canvas into a file whenver told to. The default recorder uses the very simple and ancient ‘.ppm’ file format. These files have no compression, and are 24-bits per pixel. Extremely wasteful, and extremely big and slow, but super duper simple to generate. Hidden in that ‘onLoop()’ call within gui.cpp is this:
gRecorder.saveFrame();
It will just save the current canvas into a .ppm file, with a name that increments with each frame written.
For the above bezier animation, the partial file list looks likes this:
A bunch of files, each one more than a megabyte in this particular case.
Now that you have a bunch of files, you can use various mechanisms to turn them into an animation. I tend to use this program ffmpeg, because it’s been around forever, it’s free, and it does the job.
And that’s it, the animation file is generated, and you’re a happy camper, able to show off your creations to the world.
Just one small thing, within this particular application, I use the keyboard to turn recording on and off, so when the user presses the ‘R’ key, recording will start or stop. You can make this as fancy as you like, implementing a specific control with a UI button and all that. The bottom line is, you just need to call the ‘recordingToggle()’ function, and the right thing will happen.
Conclusion
At this point, we can create demo apps that do some drawing, keyboard and mouse input, drawing with a specific frame rate, recording and the like. There are a couple more capabilities to explore in the realm of drawing, like text rendering, but we’ve got a fairly complete set. Next time around, we’ll do some fun stuff with screen capturing, possibly throw in some networking, and throw up some text for completeness.
One of the primary considerations here is to consider the design decisions that are being made. There are choices to be made from what language to use, to how memory is managed. In addition, there are considerations for abstractions, how much is too much, and how best to create code through composition. I’ll be highlighting a little more of those choices as I close out this series.
Until then, write yourself some code and join in the fun.