Graphics in the Noble Ape Simulation
IntroductionThis document has been written to demystify the structure of the Noble Ape Simulation graphics and offer methods of integrating the Noble Ape's graphics into your own development. It is important to note, this document is just an introduction to the graphics in the Noble Ape Simulation.
HistoryThe original Noble Ape Simulation (the Nervana Simulation) was written with integrated operating system based graphics. This was reasonable for the early development but it produced a substantial bottleneck for optimisation. When the Simulation was reworked in 2000, the old graphics were replaced with internal graphics that mapped to an offscreen window buffer. These internal graphics routines drew lines, pixels and icons onto the window and were optimised specifically for the graphics requirements of the Simulation.
In late 2001 to early 2002, the internal graphics were reworked to allow for even more optimised low-level routines that could handle multiple window environment. The methodology developed a three layers of graphics approach which is maintained with the current Simulation.
The Ocelot colour interface was added in September 2002. This added a low-level skin drawing routine for displaying landscapes quickly and a low-level colour pixel draw routine.
An important addition to the graphics came with event-driven window-redraw in January 2003. This eliminated a lot of the internal graphics calculations for redrawing windows per simulation cycle.
File Overview and ExampleThis document refers to the four gui files - three source code files and a header file (gui.h). All can be found in the gui directory of the Simulation source. The three source files correspond with the three levels of graphics in the simulation. These are;
Level 1 - display.c - lowest level graphics routines
As an example, to redraw the map window in the simulation, the base function is control_draw.
void control_draw(ng_byte kind, ng_byte render);
This function draws and erases the window or components of the window depending on kind. If kind is zero, then it erases the required windows and if kind is one, it draws the required window. Let us take kind to be one for the initial draw.
render identifies which windows to draw. It is a bit packed flag so each bit corresponds to a window to draw - if needed. Let us assume that render allows the map window to be redrawn in this case.
You may notice something. This function doesn't actually draw the entire map, it just moves the related apes. The function it uses is draw_apeloc. This is one of the oldest functions in the simulation. As drawing the ape location has been central to the simulation since it was first written.
void draw_apeloc(noble_land * local, noble_being * ape, noble_being * a_ape, ng_byte kind);
This function is found in draw.c. The Ocelot and Vector versions of this function are relatively similar. This function handles drawing and erasing the ape's location. There are two kinds of ape in terms of drawing and erasing. The selected ape and the normal ape. The selected ape has an additional box around it, to show it is selected. All the drawing from this function is achieved with na_pset and na_psetc. You might think line drawing would be better (ie na_line) for the box, but historically the na_pset has been used for speed reasons in this instance.
na_pset writes a single monochrome pixel (1-bit) and na_psetc writes a single colour pixel (1-byte). The monochrome value (white, black or checked grey) is governed by na_pen. In addition, you may have noticed in control_draw, a function na_buffer. This is used to locally align the drawing area to each window. For speed, the monochrome na_psetb and na_psetw are used too. These draw black and white pixels respectively and don't go through the pen state.
Graphics EnvironmentsThe Noble Ape Simulation currently has three graphics environments that can be compiled with the gui directory. These are;
Ocelot - the colour, multi window,
These environments are set by two defines at the start of gui.h.
The GPI environment has its own functions from control.c that link into the Generic Platform Interface.
Graphics TypesIn late 2001, I tried to compile the Nervana Simulation (as it was called then) for the old 68000 processor. The original Simulation had been developed on the 68000 which created some esoteric code. The code would compile but wouldn't run correctly on the 68000. Through the development in 2000-2001, new components were introduced and the code was no longer back compatible. After some investigation, the introduction of long types for the core components showed the error. Also the use of char = byte for shorthand coding worked when char = unsigned char but not when char was unique or equal to signed char. These kind of problems had cumulated through the coding development.
The solution was to create limited typing through the program layers. The core has its own type system and the gui graphics layer developed its own type system too. Ideally these types would commute in the future development.
The current types defined in the gui layer are;
typedef unsigned char ng_byte;
typedef short ng_coord;
The strict assignment of type to type maintains the compatibility between Simulation platforms. ng_coord is short to align with the Simulation Core co-ordinates.
Offscreen MemoryThe simulation initially allocates a large array that contains enough memory for all the windows. The colour windows have 64k allocated for them, the monochrome windows have 8k allocated for them, and the meter window has just 1600 bytes allocated for it. These are uncompressed offscreen bitmaps. For monochrome windows, there are eight pixels per byte and for colour windows, there is one pixel per byte.
You pick which section of the offscreen buffer to draw into with na_buffer. Fortunately, as control_draw shows, there are handy macros for assigning each window. These macros can be found in gui.h.
The offscreen memory array handles colour drawing and monochrome drawing with the same basic primitives. The only functions that are used for colour drawing are;
void na_psetc(ng_coord px, ng_coord py, ng_byte col);
The remaining functions cover monochrome drawing;
void na_line(ng_coord x1, ng_coord y1, ng_coord dx, ng_coord dy);
The two exceptions to this are;
void na_buffer(ng_byte* buffer);
Which are used for both monochrome and colour windows. The early development of the system independent graphics was developed to replicate a highly optimised version of Apple's legacy Quickdraw technology with a couple of globals - drawing buffer and pen state. In contrast, na_skin is the Ocelot development.
Ocelot is currently two parts. A y-axis line blit routine that moves up landscape maps and a bilinear interpolator which adds additional smoothness and resolution to the map information.
The bilinear interpolator takes a majority of the time in the function. Interestingly enough, the bilinear interpolator actually takes less time than the memory accessing needed with a larger array. The speed trade-off is that the mathematics are 'cheaper' than the memory accessing.
The current engine also relies on the 8-bit colour to map-height approximation where each height value corresponds to a specific colour. In the future, it will be relatively trivial to include 16 or 24 bit colour information and potentially alpha information as well as multi-bit map information. The current use of the 8-bit colour approximation is a simplification for speed at the very low end.
Redrawing WindowsEach window is only redrawn and erased when it is needed - not once per cycle. The original simulation redrew everything once per simulation cycle. This was done to maintain uniform time steps. It produced a lot of extra processing for no benefit bar a regular timing.
The re-draw every window per cycle paradigm was replaced with an externally driven graphics model. The render value passed into the control.c drawing functions;
void control_state(ng_byte final, ng_byte render);
Is a packed byte of the windows to redraw. The packing is defined in gui.h. Similarly gui.h holds constants for particular redrawing situations like time advancing or a Noble Ape moving. These require different windows to be redrawn. There are three potential causes of windows being redrawn;
user input - mouse or keyboard
A fourth is through the About request being called;
Operating system update information requires the buffers to be redrawn because the offscreen buffer is partially erased after the information is copied to the windows. This enables only minor updates of the map window when an ape is moving. Redrawing directly from this partially erased offscreen buffer would show the map without the ape.
Mid-level Graphics (in draw.c)The graphics called in draw.c is typically well formed. draw.c provides the graphical glue between the very high-level graphics in control.c and the graphics primitives in display.c. Two functions of particular interest are draw_terrain and draw_brain.
draw_terrain is a middle call that formats the height buffer data for na_skin. The information is offset and copied. In contrast, looking at draw_brain, you may wonder why such an optimised function isn't handled in display.c. Whilst it could be possible for draw_brain just to be passed the compressed brain change information from the noble_being pointer, it sits better in draw.c.
The underlying drawing method in draw_brain is optimised from a three dimensional vector rotate with one of the rotation axis removed. This simplifies the equations a little but also allows for the addition simplification. Thus the tight inner loops have no need for multiplication.
There is some potential for functions to be sufficiently optimised to move from draw.c to display.c. There is no hard and fast rule about this transition, bar saying display.c's primary access to the graphics pointer, gives some minor advantages of using the internal macros. In contrast, there is a lot of additional nonsense that needs to be adhered to in display.c that relate to operating system quirks - currently defined explicitly in gui.h.
Code Re-Use and Multiple StatesRemoving unnecessary functions, and replacing many functions of a similar format to a single function with multiple states, was central in the Stockholm re-write. Although such simplification hasn't been seen in the code development since, the gui layer, in particular draw.c contains a number of functions that have either multiple states or have been clumped from a number of similar smaller functions. This can prove confusing to the novice, but where possible comments have been inserted to assist. These will typically occur at the start of a function with multiple states.
In addition to code reuse, states are used with the B&W version of the Simulation to govern what the central window displays. This is mapped into the multiple window Ocelot interface with the selection of windows to draw through control_draw.
High-level Graphics (in control.c)Looking at the code in control.c, you may wonder what the difference is between control_state and control_draw. Why are these two functions separate and why can't the functionality of control_state be included in control_draw. control_state has a very specific purpose. It is fundamentally a legacy function from when the Noble Ape Simulation's graphics were state governed. The user would move between an overview of the island, perhaps to a contour map of the island and then to a biological population map of seed-eating birds, as an example.
control_state was called when the state (relating directly to the view) changed. This removed the contour map etc and replaced the old graphics with the new state graphics. Central to this was the notion of redrawing the graphics in a normal non-state change cycle. As mentioned, the Simulation only alters what needs to be altered in control_draw. It normally wouldn't redraw the contour map or the meter window - in fact these mechanisms don't exist in control_draw. They exist in control_state.
control_draw is called in a very few cases and is specifically optimised to be part of the graphics cycle. In contrast control_state is designed to be called infrequently. Irrespective of the level of optimisation, graphics calls like redrawing the entire map each cycle would slow things down considerably.
Line of SightDrawing line of sight (or LOS) has never been considered in the Simulation prior to 0.663. But as a user requested feature, the Simulation now highlights when the active ape can see other apes. The line of sight is actually calculated in the Simulation Core in the function being_los. This information is called from draw_apeloc at any point of graphics update.
This works remarkably well and highlights how the graphics events don't compromise the speed of graphics updating. An interesting caveat, the being_los uses a Bresenham function to walk down the line of vision. The Bresenham function is central to na_line, in fact the Bresenham function is popularly celebrated as the only line function needed for graphics. In the future it may be practical to have a line function defined with function pointer hooks so the being_los and na_line can use the same code.
Just An OverviewThis document was originally written to be part of a larger treatise on the gui layer of the Noble Ape Simulation. I thought it was important to get the information online, to avoid the information becoming stagnant. I have covered, in introduction, a number of concepts used in the development of the gui layer that should assist your understanding of this component of the Noble Ape Simulation.