Cellular Automata Laboratory


Own Code Evaluators

This section contains quite advanced material and should not be tackled until you a) have a thorough familiarity with CelLab and b) understand assembly language and/or Windows DLL programming.

The section arose because Rudy kept asking me to add new modes to the basic JC program. First he wanted a one-dimensional mode. Then he wanted a mode that can see more than one bit of neighbor state so JC could do the Rug rule in VGA mode. Each of these changes involved adding new code to the main JC program, which meant I had to keep remaking all the rulemakers. And each addition only stimulated more requests. What a drag....

In order to get out of the business of adding custom evaluator after custom evaluator to JC, and to completely open the architecture of the program, rendering it extensible almost without bound, I implemented "user own code evaluators". These allow any user to write his own custom inner loop for any kind of automaton he wishes and have it executed as the update rule by JC, retaining all of the facilities of JC including the lookup table. This facility is intended for experienced assembly language programmers only, but the way in which it is integrated with CelLab allows "aftermarket evaluators" to be coded and run with existing copies of CelLab. Thus CelLab can get smarter after it's shipped without the need for a new release.

We have used this facility already to implement several interesting new evaluators. One performs the optimal solution of Laplace's equation in the plane. A second implements a general-purpose von Neumann neighborhood where each cell can see 3 bits of state from each of its neighbors and four bits of local state. A third implements Langton's self-reproducing creature.

The Windows-based CELLAB simulator uses the same assembly-language own code evaluators as the JC program for DOS and, in addition, allows you to define "custom evaluators" in Windows Dynamic Link Libraries (DLLs), writing them in C (or any other language able to generate suitable DLLs) instead of assembly language. The ability to develop custom evaluators in a high level language and the elimination of the restrictions on program size and memory architecture inherent in the DOS version facilitates the development of much more ambitious custom evaluators. Of course, DLL custom evaluators can be used only with CELLAB for Windows; DOS-based programs such as JC cannot access DLLs.

Defining Own Code Evaluators in Assembly Language

The rules for creating assembly language own code evaluators are detailed and highly rigid, but once you understand them the actual job of coding an evaluator is not all that difficult. First, let's examine it from a rule definition standpoint. Consider the following Pascal rule definition:

PROGRAM Vonpar;
{3 bit parity rule for von Neumann neighborhood implemented
 with the VONN3 own-code evaluator.  }
USES JCmake;
{$F+}     { Required for function argument to genrule. }
FUNCTION JCRule(OldState,nw,n,ne,w,self,
                         e,sw,s,se:integer): integer;
BEGIN
          s := oldstate AND 7;
          e := (oldstate SHR 3) AND 7;
          w := (oldstate SHR 6) AND 7;
          n := (oldstate SHR 9) AND 7;
          self := (oldstate SHR 12) AND 15;
          JCRule := n XOR s XOR w XOR e
END;
BEGIN
          WorldType := 13;    { Own code torus }
          patreq := 'square';
          OCodeReq := 'vonn3';
          genrule(JCRule)
END.

This is a parity rule that works on 3 bits of state in the von Neumann neighborhood. Since this is a neighborhood option not built into CelLab, the rule definition invokes an own code routine called "vonn3" which implements that custom neighborhood. It sets WorldType to 13 to select a toroidal world (it would be 12 if open), and a generic look-up table in which the meaning of the entries is totally up to the own code implementation. The VONN3 own code routine, implemented in the file VONN3.ASM and supplied in ready to use form as VONN3.JCO (the extension stands for "JC Own code"), is requested by setting "OCodeReq" to its file name. Own code evaluators defined with world types 12 and 13 work with rule definition functions called directly with the raw lookup table index in OldState; the rest of the rule definition function arguments are unused and are always zero. The meaning of the 16 bits of OldState is defined by the own code routine itself. For VONN3 the assignments are as follows:

OldState bitsMeaning
2 - 0 South
5 - 3 East
8 - 6 West
11 - 9 North
15 - 12 Self

Thus, as advertised, 3 bits from each neighbor and four local bits are visible (the local bits aren't used in this rule definition). Since only OldState is passed, the JCRule function extracts the neighbors itself from OldState. It calculates the new value and returns it in the conventional manner.

When this rule definition is loaded into JC, it searches for the file VONN3.JCO and loads it as an own code evaluator. To signify that an own code evaluator is in use, the name in the rulebox at the upper right of the status screen is shown with a suffix of +". If you save a rule or experiment within CelLab with Ctrl-F2 or Ctrl-F3, the own code is embedded within the saved rule so it can be automatically loaded when the rule is reloaded. Thus, once you get everything working just right, you can save the rule from within JC as a .JC file, then send it to your friend without his needing a copy of the .JCO file. Similarly, saved experiments are completely self-contained even when own code is involved in them.

So what are these mysterious own code routines, and how do I go about writing one? Listen up, sharpen your coding stick, and get ready to be initiated into the gory details of the innards of CelLab. First of all, an own code routine is mechanically an MS-DOS .COM file (we give it the extension of .JCO so it won't be accidentally executed as a program, which would certainly crash the machine). Own code files are created by writing them in assembly language, assembling them, linking them, and creating a .COM file named .JCO either using EXE2BIN or directly from your linker, if it offers that feature (as does Turbo Assembler's® linker). The own code program is, in essence, the "inner loop" that updates the state of a line of cells in the state map. We have the own code process an entire line rather than just one cell because this reduces the number of times it must be called from 64,000 to 200, which drastically cuts the overhead and increases execution speed. There is no measurable speed penalty when using own code rather than a built-in evaluator of the same complexity. Own code evaluators are called with a precise set of values in registers and a known execution environment. An evaluator must exactly adhere to the coding guidelines below or disaster will ensue, generally manifested as a machine that hangs the first time you try to run the rule.

Let's examine the definition for the VONN3 own code evaluator. In assembly code it's as follows:

;       Own code evaluator for von Neumann neighborhood
;       with 3 bits of state visible from each neighbor and
;       4 bits of local state.

codes   segment byte
        assume  cs:codes

        org     100h

NWEST   equ     [bp-323]
NORTH   equ     [bp-322]
NEAST   equ     [bp-321]
WEST    equ     [bp-1]
SELF    equ     [bp]
EAST    equ     [bp+1]
SWEST   equ     [bp+321]
SOUTH   equ     [bp+322]
SEAST   equ     [bp+323]

vonn3   proc    far

        mov     dx,320   ; load row length counter
        mov     cl,3     ; load shift count

ocbyte: mov     al,SOUTH
        rol     al,1     ; adjust map state
        ror     ax,cl
        mov     al,EAST
        rol     al,1     ; adjust map state
        ror     ax,cl
        mov     al,WEST
        rol     al,1     ; adjust map state
        ror     ax,1
        ror     ax,1
        mov     bl,ah
        ror     ax,1
        mov     al,NORTH
        rol     al,1     ; adjust map state
        ror     ax,cl
        mov     al,SELF
        rol     al,1     ; adjust map state
        ror     ax,cl
        ror     ax,1
        mov     bh,ah

        mov     al,[bx]  ; load new value from lookup table
        inc     bp       ; advance along row
        stosb            ; store into new state table

        dec     dx       ; more to update ?

        jnz     ocbyte   ; yes.  keep on going

        ret
vonn3   endp

codes   ends
        end     vonn3

This code appears puzzling until we explain some things about register contents and the handling of the state map. An own code evaluator is called with a far (intersegment) call, hence the declaration of VONN3 as a FAR procedure. The own code routine must start at address 100h, the very first byte in the file (as is standard for all .COM files). If you have data or other material in the file, it must follow the evaluator procedure. When the procedure receives control, the registers are loaded as follows:

Decrement flag Cleared
SS:BP Old state table
SP At end of 64K containing old state table
DS Lookup table
CS User code segment
FS Writable selector pointing to user code segment. Under Windows, programs cannot modify their code segment. JC own code which kept scratchpad values in the code segment must change these references to be based on FS to work with CELLAB for Windows. To remain compatible, JC loads FS with the user code segment, which is writable under DOS.
ES:DI New state table
AX Scratch
BX Scratch, normally used to index lookup table
CX Scratch, aux value passed in CL
DX Scratch
SI Line counter (200 on first, 1 on last)

The job of each call on the own code function is to update 320 consecutive cells starting at SS:BP, storing their new values in the 320 bytes starting at ES:DI. In the process of performing this update, BP and DI should both be incremented by 320 bytes. The lookup table, if used by the rule, may be found starting at offset zero based on DS, and hence a full 64K of lookup table may be addressed by an index in the BX register. The own code function may use registers AX, BX, CX, and DX as it wishes. Other than incrementing BP and DI, all other registers must be left unchanged by the own code. Own code may assume the decrement flag is cleared when it is called, and it must leave the decrement flag cleared when it returns. Register SI informs the own code which line it is processing--most rules do not need this information, but it's there if you need it (and it must be preserved by the own code). Incredibly sneaky own code can even change SI to update a subset of the state map, but I'll leave that to seriously weird people to figure out (hint: make sure you diddle BP and DI to compensate!).

When SS:BP is pointing to a given cell, its neighbors may be found by the following expressions:

NWNNE
WCE
SWSSE

Neighbor Address expression
NW    [bp-323]
N    [bp-322]
NE    [bp-321]
W    [bp-1]
C    [bp]
E    [bp+1]
SW    [bp+321]
S    [bp+322]
SE    [bp+323]

Symbolic equates for these definitions are given at the top of VONN3.ASM. Now the code in VONN3 should begin to make a little more sense, but what are all those "rol xx,1" instructions with comments that say "adjust map state?" In order to make the updating of built-in neighborhoods run as fast as possible, the internal state map is kept circularly shifted with respect to the normal nomenclature of states--Plane 0 (the public bit for normal rules) is stored as the 27 bit in the internal state map, with planes 1 through 7 stored in the least significant 7 bits of the state map. JC carefully shields the user from knowledge of this, but since own code works directly on the internal state map, it must be cognizant of this fact. Since it takes some time to adjust the state map cells, crafty programming tricks that eliminate this are worth looking for when you design own code routines. (If you only need 7 bits or fewer of state, the simplest expedient is just to ignore Plane 0 and use Planes 1 through 7 for your state. You can extract them simply by loading bytes directly from the state map with no shifting required. The Langton rule uses this trick to run faster than the pure VONN3 definition.) If you want to supply modal information to the rule, you can encode 8 bits of information in the "auxplane" variable in the rule definition function. For own code rules this cell does not cause any special treatment of plane 1, but instead is simply passed to the own code function in register CL each time the function is called. The interpretation of this value is totally up to the own code rule function.

The built-in logic that calls your own code takes care of toroidal wrap around and supplying zero neighbors for open worlds. As long as your own code addresses the neighbors with the expressions given above, it doesn't have to worry about wraparound or world type. When used with own code, the lookup table has no predefined meaning--it's simply 64K of data to which the own code assigns its own interpretation. Consequently, Pascal or C rule definitions which use own code evaluators must be coded with an understanding of what the own code expects to find in the lookup table. (Note that if you really want to go off the deep end, there isn't any reason own code can't change the lookup table as it's running. If you want your own code to do something special at the start of every generation, just test SI equal to 200.) Some own code functions don't even need the lookup table at all. For example, here's own code for LAPLACE.ASM which solves the Laplace equation in the plane using the formula:

New = ((N + S + E + W) × 4 + (NW + NE + SW + SE)) / 20

;       Own code evaluator for optimal solution to Laplace
;       equation in the plane.  The rule is embodied totally
;       in the code--no lookup table is used.

codes   segment byte
        assume  cs:codes

        org     100h

NWEST   equ     [bp-323]
NORTH   equ     [bp-322]
NEAST   equ     [bp-321]
WEST    equ     [bp-1]
SELF    equ     [bp]
EAST    equ     [bp+1]
SWEST   equ     [bp+321]
SOUTH   equ     [bp+322]
SEAST   equ     [bp+323]

laplace proc    far

        mov     dx,320          ; load row length counter
        mov     cl,20           ; load divisor

lbyte:  xor     ah,ah
        mov     bl,WEST
        rol     bl,1
        xor     bh,bh
        mov     al,EAST
        rol     al,1
        add     bx,ax
        mov     al,NORTH
        rol     al,1
        add     bx,ax
        mov     al,SOUTH
        rol     al,1
        add     bx,ax
        add     bx,bx           ; * 2
        add     bx,bx           ; * 4

        mov     al,NWEST
        rol     al,1
        add     bx,ax
        mov     al,NEAST
        rol     al,1
        add     bx,ax
        mov     al,SEAST
        rol     al,1
        add     bx,ax
        mov     al,SWEST
        rol     al,1

        add     ax,bx           ; complete weighted sum
        add     ax,10           ; round up in divide
        div     cl              ; divide to normalize result
        ror     al,1            ; shift into state map order

        inc     bp              ; advance along row
        stosb                   ; store into new state table

        dec     dx              ; more to update ?
        jnz     lbyte           ; yes.  keep on going

        ret
laplace endp

codes   ends
        end     laplace

Since this code computes the new value arithmetically from the neighbor cells, it doesn't bother with the lookup table. A C or Pascal rule definition that called it would just always return zero from the rule definition function.

Own code evaluators should be short, sweet, and simple. Evaluators of the complexity shown here run at speeds comparable to the built in rule evaluators of CelLab (LAPLACE, with all of its shifting and division, runs about half the speed of the standard evaluator). If you need to do lots of computation, try to find a way to reduce it to table lookup or else you're likely to be disappointed at how fast your rule executes.

For an example of what can be done with own code, please refer to the most complicated example to date, the definition of Langton's self-reproducing machine. The own code for this rule (essentially identical to the VONN3 example given above, but using doubled state codes to run faster) is defined in Langton.ASM. The rule definition which generates the complicated lookup table used by the own code is defined in the Pascal file Langton.PAS.

It's worth noting in passing that the place at which user own code is interposed in JC's evaluation loop is precisely the point where one could access a hardware JC evaluation accelerator peripheral, should some crafty hardware maven see fit to build one. Such a device, supplied with a tiny own code driver, would make JC run many times faster.

If you have a lookup table that you'd like to run with several different own code evaluators, you can explicitly load an own code routine by using the L key as for load rule, but entering a file name prefixed with an asterisk. You can see a list of .JCO files in a given directory by entering "*dirname?" to the L command.

As you come to master the craft of own code evaluator design, your horizons will suddenly broaden as you come to realize the extent that JC places you in control. Appropriate own code, written with a thorough understanding of the internal environment seen by the own code, can implement such things as:

Your imagination and assembly language coding skills are truly the only limits to what you can accomplish with own code.

Defining Custom Evaluator DLLs for Windows

The Windows-based CELLAB simulator accepts assembly-language own code evaluators as described above and, in addition, allows you to write custom evaluators in high-level languages such as C. This section explains how to implement custom evaluators in C, and assumes you have a C development environment which allows you to build DLLs in 16-bit mode. 32-bit DLLs are not compatible with CELLAB and, even if they were, would be less efficient than well-optimized 16 bit code.

The following is a custom evaluator program which implements a von Neumann neighborhood where each cell sees three bits of state from each of its neighbors and four bits of its own state. This evaluator is compatible with the assembly language VONN3 discussed above and produces precisely the same results when loaded in conjunction with the Vonpar program. I've interpolated comments in blue type to explain what's going on.

/*

        Evaluator for the von Neumann neighbourhood with three
        bits of state visible from each neighbour and four
        bits of local state.  The value indexing the lookup
        table is:

                    mmmmnnnwwweeesss
        Self _________^  ^  ^  ^  ^ 
        North ___________|  |  |  |
        West _______________|  |  |
        East __________________|  |
        South ____________________|

                    by John Walker  --  March 1997

*/

#include <windows.h>

/*  If POINTERS is defined the evalUpdate function will use
    pointer arithmetic rather than array indexing to access
    the state vectors.  Depending on the compiler's optimiser,
    this may or may not be faster.  */

#define POINTERS

/*  If IULOOK is defined, conversions between internal
    (one bit rotated) and user states are done using a
    lookup table supplied in the file iu.h instead of
    in-line calculation.  Most evaluators run faster
    with IULOOK defined, but this can depend on the
    compiler, and interact with whether POINTERS is
    defined.  Experimentation is the only way to determine
    the optimal settings for your compiler and evalUpdate
    function definition.  */
    
#define IULOOK

#ifdef IULOOK
/*
    The include file iu.h contains lookup tables for translating
    the shifted internal states to and from the external states.
*/
#include "iu.h"
#define ItoU(x)     itou[x]
#else
#define ItoU(x)     ((((x) << 1) | (((x) >> 7) & 1)) & 0xFF)
#endif

/*  Initialisation procedure  */

DWORD FAR PASCAL _export evalInit(HWND parentWindow,
                                  int screenX, int screenY,
                                  DWORD lutSize)
{
    /*
        evalInit is called immediately after the custom evaluator is
        loaded.  It is passed the following arguments:

            parentWindow     Handle to the parent window.  This is
                             useful if the function needs to display a
                             dialogue or message box belonging to the
                             CELLAB main window.

            screenX          Width of lines in the state map, including
                             the two wrap-around bytes.  This is currently
                             always 322, but evaluators should not count on
                             this.

            screenY          Number of lines in the state map, including
                             the two wrap-around lines.  This is currently
                             always 202, but evaluators should not count on
                             this.

            lutSize          Size of the rule lookup table in bytes.
                             This is currently always 65536 (64K) , but
                             evaluators should not count on this.

        If the evaluator requires local storage for configuration parameters
        or information saved from call to call during evaluation, it
        should allocate a buffer of the required size with GlobalAlloc()
        and return its address, cast to a DWORD.  This "context buffer"
        if passed to all subsequent calls to the evaluator, which may
        use it as a scratchpad in any way desired.  If no context
        buffer is required, return NULL.

        If the evaluator does not need the information passed in the
        arguments to evalInit and does not use a context buffer,
        evalInit need not be defined.
    */
    return (DWORD) 0;
}

/*  Configuration procedure  */

void FAR PASCAL _export evalConfig(DWORD context, HWND parentWindow)
{
    /*
        evalConfig is called when a custom evaluator is loaded
        and the user selects the Options/User Evaluator... menu item.
        The evaluator may then display one or more configuration
        dialogues, allowing the user to select modes specific to the
        evaluator.  If the evaluator does not have configurable
        modes, evalConfig may simply return without doing
        anything.  evalConfig is called with the following
        arguments:

            context          Address of the context buffer
                             allocated by evalInit.

            parentWindow     Handle to the parent window.  This is
                             useful if the function needs to display a
                             dialogue or message box belonging to the
                             CELLAB main window.

        A typical evalConfig function will display a configuration
        dialogue initialised from values stored in the context buffer
        and modify values in the context buffer as the user changes
        settings.

        If the rule has no configuration dialogue, evalConfig need not
        be defined.
    */
}                                    

/*  Termination procedure  */

void FAR PASCAL _export evalTerm(DWORD context)
{
    /*
        evalTerm is called when a custom evaluator is unloaded
        due to the selection of a different custom evaluator or loading
        of a rule with no custom evaluator.

            context          Address of the context buffer
                             allocated by evalInit.

        evalTerm should release all resources allocated by the
        evaluator (usually pointed to by items in the context
        buffer) and then release the context buffer itself.

        If the rule allocated no context buffer and does not otherwise
        need notification of termination, evalTerm need not be
        defined.
    */
}                                    

/*  Update map procedure  */

void FAR PASCAL _export evalUpdate(DWORD context,
                                   LPBYTE oldvec, LPBYTE newvec, LPBYTE lut,
                                   int torus, int auxval, long generation)
{
    /*
        evalUpdate is the raison d'ętre for a custom
        evaluator.  It is called on each cycle of the simulator and
        provided pointers to the old and new state maps, the rule
        lookup table, and additional information it may use to
        calculate the next generation.

            context          Address of the context buffer
                             allocated by evalInit.

            oldvec           Pointer to the state vector for the
                             previous generation.  This is a pointer
                             to the start of the screenX+2 by
                             screenY+2 area (referring to the
                             arguments in the call to evalInit)
                             including the wrap-around lines and bytes
                             for each line.  Values in these state maps
                             are kept in "internal bit order", rotated
                             one bit to the right with regard to the
                             usual description of bit planes.  To convert
                             internal bit order to external, use the
                             expression:
                                Ext = ((Int << 1) | ((Int >> 7) & 1)) & 0xFF;
                             For external to internal, use:
                                Int = (((Ext >> 1) & 0x7F) | (Ext << 7)) & 0xFF;

            newvec           Pointer to the state vector for the
                             new generation, to be filled in by the evaluator.
                             This is also a pointer to the start of the
                             screenX+2 by screenY+2 area
                             including the wraparound bytes and lines, but the
                             evaluator need not fill in the wraparound areas.
                             Values in newvec should be stored in
                             internal byte order as described for oldvec.

            lut              Pointer to the rule lookup table, whose
                             length was given by lutSize in the call to 
                             evalInit.  The meaning of the lookup table is
                             entirely up to the evaluator to define; many custom
                             evaluators do not use the lookup table at all.

            auxval           Contains the 8-bit value specified by the
                             rule definition for the "auxiliary plane"
                             confguration.  The interpretation of this value is
                             entirely up to the custom evaluator; it allows
                             rule programs to pass information to custom
                             evaluators without the need for the user to
                             manually configure them.

            generation       The generation number since the rule or pattern
                             was last reloaded.  This allows custom evaluators
                             to behave differently based on the generation
                             number, especially handy for multi-phase rules
                             such as the gas models.
    */
    unsigned int x, y;

#ifdef POINTERS

    /*  Pointer arithmetic version of evaluator.  */
    
    LPBYTE op, np;

#define NORTH   ItoU(*(op - 322))
#define WEST    ItoU(*(op - 1))
#define SELF    ItoU(*op)
#define EAST    ItoU(*(op + 1))   
#define SOUTH   ItoU(*(op + 322))

#define NEWSELF *np
    
    op = oldvec + 323;
    np = newvec + 323;
    for (y = 0; y < 200; y++) {
        for (x = 0; x < 320; x++) {
            unsigned int rv =
                 (SOUTH & 7) |
                ((EAST & 7) << 3) |
                ((WEST & 7) << 6) |
                ((NORTH & 7) << 9) |
                ((SELF & 15) << 12);
            
            /*  Lookup table entries are in internal bit order
                and can be stored directly into NEWSELF.  */
                    
            NEWSELF++ = lut[rv];
            op++;
        }
        op += 2;
        np += 2;
    }

#else

    /* Array indexing version of evaluator.  */

#define NORTH   ItoU(oldvec[y * 322 + x + 1])
#define WEST    ItoU(oldvec[(y + 1) * 322 + x])
#define SELF    ItoU(oldvec[(y + 1) * 322 + x + 1])
#define EAST    ItoU(oldvec[(y + 1) * 322 + x + 2])   
#define SOUTH   ItoU(oldvec[(y + 2) * 322 + x + 1])

#define NEWSELF newvec[(y + 1) * 322 + x + 1]

    for (y = 0; y < 200; y++) {
        for (x = 0; x < 320; x++) {
            unsigned int rv =
                 (SOUTH & 7) |
                ((EAST & 7) << 3) |
                ((WEST & 7) << 6) |
                ((NORTH & 7) << 9) |
                ((SELF & 15) << 12);
            
            /*  Lookup table entries are in internal bit order
                and can be stored directly into NEWSELF.  */
                    
            NEWSELF = lut[rv];
        }
    }
#endif  
}                       

/*  Library initialisation  */

int FAR PASCAL LibMain(HINSTANCE hInstance, WORD wDataSeg, WORD wHeapSize,
                       LPSTR lpszCmdLine)
{
    /*
        This is the standard Windows DLL entry point.  This code should
        not be modified unless you are absolutely certain you know
        what you're doing.
    */
    if (wHeapSize > 0) {
        UnlockData(0);
    }
    return 1;
}                     

/*  Library termination handler  */

int FAR PASCAL _export WEP(int nParam)
{
    /*
        This is the standard Windows DLL termination.  This code should
        not be modified unless you are absolutely certain you know
        what you're doing.
    */
    return 1;
}

A DLL must be linked with a "module definition" (.DEF) file that identifies it as a library and lists the symbols accessible to other program. Our VONN3DLL.DEF file is as follows. You can use this file for any custom evaluator that defines all the eval... functions merely by changing the library name on the first line. If any of the optional functions: evalInit, evalConfig, or evalTerm are not defined, they must not be listed in the EXPORTS section.

LIBRARY   VONN3DLL
EXETYPE   WINDOWS
CODE      PRELOAD MOVEABLE DISCARDABLE
DATA      PRELOAD MOVEABLE SINGLE

SEGMENTS
        _TEXT   FIXED PRELOAD
HEAPSIZE  1024
EXPORTS
          WEP PRIVATE
          evalInit
          evalUpdate
          evalConfig
          evalTerm

After you've successfully compiled and linked your custom evaluator DLL, rename it to have a file type of .jco, identifying it as a custom evaluator, and copy it to the directory containing the .jc file(s) that reference it. You can then load it manually with the File/Load User Evaluator... menu item, or automatically by naming it in an Own Code request in your rule definition. CELLAB determines by examining the file whether it is JC-compatible own code or a custom evaluator DLL and invokes it accordingly. Be careful not to try to use a custom evaluator DLL with JC under DOS; it will probably crash your computer.

Anything that can be done with assembly language own code can be implemented in an evaluator DLL written in C. The ability of evaluator DLLs to allocate memory for local storage, present the user with a configuration dialogue, and access system services allows many things impossible within constraints of JC own code. The ability to program straightforwardly in C makes development of complex evaluators much less intimidating. For example, compare the following evaluator DLL for the Laplace equation in the plane with its assembly language equivalent given earlier.

/*

                Laplace equation in the plane

       by John Walker  --  March 1997

       New = ((N + S + E + W) * 4) + (NW + NE + SW + SE)) / 20

*/

#include <windows.h>

/*  If POINTERS is defined the evalUpdate function will use
    pointer arithmetic rather than array indexing to access
    the state vectors.  Depending on the compiler's optimiser,
    this may or may not be faster.  */

#define POINTERS

/*  If IULOOK is defined, conversions between internal
    (one bit rotated) and user states are done using a
    lookup table supplied in the file iu.h instead of
    in-line calculation.  Most evaluators run faster
    with IULOOK defined, but this can depend on the
    compiler, and interact with whether POINTERS is
    defined.  Experimentation is the only way to determine
    the optimal settings for your compiler and evalUpdate
    function definition.  */
    
#define IULOOK

#ifdef IULOOK
#include "iu.h"
#define UtoI(x)     utoi[x]
#define ItoU(x)     itou[x]
#else
#define UtoI(x)     (((((x) >> 1) & 0x7F) | ((x) << 7)) & 0xFF)
#define ItoU(x)     ((((x) << 1) | (((x) >> 7) & 1)) & 0xFF)
#endif

/*  Update map procedure  */

void FAR PASCAL _export evalUpdate(DWORD context,
                                   LPBYTE oldvec, LPBYTE newvec, LPBYTE lut,
                                   int torus, int auxval, long generation)
{
    unsigned int x, y;
    
#ifdef POINTERS

    /*  Pointer arithmetic version of evaluator.  */
    
    LPBYTE op, np;

#define NWEST   ItoU(*(op - 323))
#define NORTH   ItoU(*(op - 322))
#define NEAST   ItoU(*(op - 321))
#define WEST    ItoU(*(op - 1))
#define SELF    ItoU(*op)
#define EAST    ItoU(*(op + 1))   
#define SWEST   ItoU(*(op + 321))
#define SOUTH   ItoU(*(op + 322))
#define SEAST   ItoU(*(op + 323))

#define NEWSELF *np
    
    op = oldvec + 323;
    np = newvec + 323;
    for (y = 0; y < 200; y++) {
        for (x = 0; x < 320; x++) {
            unsigned int laplace = 
                ((NORTH + SOUTH + EAST + WEST) * 4 +
                  (NWEST + NEAST + SWEST + SEAST) + 10) / 20;
                
            NEWSELF++ = UtoI(laplace);
            op++;
        }
        op += 2;
        np += 2;
    }

#else

    /* Array indexing version of evaluator.  */


#define NWEST   ItoU(oldvec[y * 322 + x])
#define NORTH   ItoU(oldvec[y * 322 + x + 1])
#define NEAST   ItoU(oldvec[y * 322 + x + 2])
#define WEST    ItoU(oldvec[(y + 1) * 322 + x])
#define SELF    ItoU(oldvec[(y + 1) * 322 + x + 1])
#define EAST    ItoU(oldvec[(y + 1) * 322 + x + 2])   
#define SWEST   ItoU(oldvec[(y + 2) * 322 + x])
#define SOUTH   ItoU(oldvec[(y + 2) * 322 + x + 1])
#define SEAST   ItoU(oldvec[(y + 2) * 322 + x + 2])

#define NEWSELF newvec[(y + 1) * 322 + x + 1]

    for (y = 0; y < 200; y++) {
        for (x = 0; x < 320; x++) {
            unsigned int laplace = 
                ((NORTH + SOUTH + EAST + WEST) * 4 +
                  (NWEST + NEAST + SWEST + SEAST) + 10) / 20;
                
            NEWSELF = UtoI(laplace);
        }
    }
#endif  
}                       

/*  Library initialisation  */

int FAR PASCAL LibMain(HINSTANCE hInstance, WORD wDataSeg, WORD wHeapSize,
                       LPSTR lpszCmdLine)
{
    if (wHeapSize > 0) {
        UnlockData(0);
    }
    return 1;
}                     

/*  Library termination handler  */

int FAR PASCAL _export WEP(int nParam)
{
    return 1;
}

This evaluator doesn't need initialization, configuration, nor termination functions, so it omits them. Thus its .def file is simply:

LIBRARY   LAPLDLL
EXETYPE   WINDOWS
CODE      PRELOAD MOVEABLE DISCARDABLE
DATA      PRELOAD MOVEABLE SINGLE

SEGMENTS
        _TEXT   FIXED PRELOAD
HEAPSIZE  1024
EXPORTS
          WEP PRIVATE
          evalUpdate

Need for Speed

When writing an evaluator DLL, keep in mind that your evalUpdate becomes the heart of the inner loop of CelLab, being called once for every generation update. If it does a great deal of computing, especially in its own inner loop that updates each cell, the simulator will run disappointingly slowly. Try to make the lookup table do as much of the work as possible, and include special case tests and lengthy computations on a per-cell basis only as a last resort.

In many programming projects, computation time is an insignificant factor in the performance of the program, with input/output performance or user interface interactions accounting for most of the running time. This is not the case for cellular automata simulations! After you have debugged your evaluator, be sure to compile it with the all of your compiler's optimization switches set for the fastest code possible, and optimised for an 80386 processor (or above, as long as you're sure it will only be used on a machine of the type you're targeting or above). Depending on how effective your compiler is in optimizing various kinds of code, it may be more efficient to use pointers to step through the state maps instead of array arithmetic and to convert to and from internal and external bit order by table lookup rather than shifting and logical operations. The VONN3DLL.C and LAPLDLL.C evaluators contain options for these choices, allowing you to experiment with them on your development system.

Back to Defining Rules

Onward to JC Pattern Design


Next Previous Contents