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.
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 bits | Meaning |
---|---|
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:
NW | N | NE |
W | C | E |
SW | S | SE |
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:
; 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.
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
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.