IOCCC image by Matt Zucker

The International Obfuscated C Code Contest

2024/endoh1 - Prize in patient pointillism

slow ray tracer in CPP

Author:

To build:

    make all

Try:

    ./try.sh

HINT: You have to be VERY patient while the above script runs to completion.

Judges’ remarks:

When you make all you see what looks like a C program, rt.c. However if you try to compile rt.c, you will fail: even if you try to define the symbols X, Y, W, and H.

Well, the propose of the C program, rt.c is to generate the red, green, and blue colors of a single pixel, in the C Preprocessor!!!

For the patient, a image may be rendered a slow pixel at a time. Think of this as digital Pointillism. :-)

For the truly impatient who wishes to get right to the point without trying to dot around waiting for the try.sh to finish, you may view these images:

FUN FACT: We were able to determine on an “x86-64 processor” running “RHEL 9.6” using “gcc version 11.5.0”, one could theoretically render a 2147483647 x 2147483647 pixel image. At the rate it took to render the 512 x 512 pixel image, and without using any parallelism, such an image would take about 169 billion years to fully render on a single CPU core! Fully rendering such large images is NOT recommended. :-)

Parallel make

SUGGESTION: The 2024/endoh1/Makefile, by default, uses a single CPU core, can make use of parallel operations. If you have access to a “parallel make”, can achieve the above in a shorter around of time. If you have “n” CPU cores available, consider running a parallel make with “n-1” jobs that run simultaneously. For example:

    RESERVE="-2"
    NPROC=$(nproc)
    ((JOBS=NPROC-RESERVE))
    SIZE=512
    make clobber all
    nohup time make -j "$JOBS" out.ppm H="$SIZE" W="$SIZE" > make.out 2>&1 < /dev/null &

Where SIZE is the size of the image to generate.

Where RESERVE is the number of CPU cores to reserve for other things such as the operating system, daemons, terminal window, etc. You can even “oversubscribe” by setting RESERVE to a negative number (e.g., RESERVE="-2").

NOTE: The try.sh script follows the above example for SIZE values of 8, 32, 128, and then 512.

The optimal RESERVE will depend on your hardware, operating system, a C compiler. Making RESERVE too small (i.e., too negative) may slow down the result. We recommend that experiment various values pf RESERVE with moderate SIZE of 32 in order to try to find an optimal JOBS value before attempting to try a larger size such as 512 and beyond.

Be first to build images larger than 512x512

QUESTION: Do you have the patience to build an even larger than a 512x512 image? If you do, using the program as it is written of course, please send the IOCCC the following:

PLEASE do not attempt to optimize the general algorithm used by this entry to build PPM images. Please remain “faithful to the spirit” of this wonderful entry when attempting to build larger PPM images.

The first people to render a new record sized image using the general algorithm used by this entry, will be given credit in the thanks for the help web page.

Author’s remarks:

A Ray Tracer in the C Preprocessor

Ever since the iconic 1988 IOCCC winning entry 1988/applin, bending the C preprocessor to one’s will has been a time-honored tradition in the IOCCC. This entry sets out to be the culmination of that legacy: a ray tracer implemented entirely with C macros!

How to Run

The initial program, ./prog (compiled from prog.c), outputs the main code, rt.c. To get started, compile and execute it:

    make all

Note: rt.c is not a valid C program – it’s written for C preprocessor.

To get the color of a single pixel at position (16, 16) in a 32 x 32 image, run:

    cc -E -P -DX=16 -DY=16 -DW=32 -DH=32 rt.c

This will produce:

    129
    62
    129

These three values represent the red, green, and blue components respectively.

In other words, this indicates that the pixel at position (16, 16) has the color #813e81.

How to Generate the Full Image

To generate the entire image, you need to run the preprocessor for every pixel. For example, to form a 32x32 image:

    mkdir -p pixel
    cc -E -P -DX=0  -DY=0  -DW=32 -DH=32 rt.c > pixel/pixel-00-00.txt
    cc -E -P -DX=1  -DY=0  -DW=32 -DH=32 rt.c > pixel/pixel-00-01.txt
    cc -E -P -DX=2  -DY=0  -DW=32 -DH=32 rt.c > pixel/pixel-00-02.txt
    ...
    cc -E -P -DX=30 -DY=0  -DW=32 -DH=32 rt.c > pixel/pixel-00-30.txt
    cc -E -P -DX=31 -DY=0  -DW=32 -DH=32 rt.c > pixel/pixel-00-31.txt
    cc -E -P -DX=0  -DY=1  -DW=32 -DH=32 rt.c > pixel/pixel-01-00.txt
    cc -E -P -DX=1  -DY=1  -DW=32 -DH=32 rt.c > pixel/pixel-01-01.txt
    cc -E -P -DX=2  -DY=1  -DW=32 -DH=32 rt.c > pixel/pixel-01-02.txt
    ...
    cc -E -P -DX=30 -DY=31 -DW=32 -DH=32 rt.c > pixel/pixel-31-30.txt
    cc -E -P -DX=31 -DY=31 -DW=32 -DH=32 rt.c > pixel/pixel-31-31.txt

Then, concatenate the results with a PPM header to create the image:

    echo P3 32 32 > out.ppm
    echo 255 >> out.ppm
    find pixel -type f -print | LANG=C sort | xargs cat >> out.ppm

Now open out.ppm with any image viewer that supports the PPM format.

Automating with Makefile

To form a 32x32 image:

    make clean targets out.ppm H=32 W=32

Now open out.ppm with any image viewer that supports the PPM format.

Generating a Larger Image

To form a 256x256 image:

    make clean targets out.ppm H=256 W=256

Now open out.ppm with any image viewer that supports the PPM format.

For reference, the 512x512 image took about 10 hours to render.

Technical Challenges

Below is a technical explanation of the implementation.

Copying a Macro Definition

One core challenges was how to “copy” the value of a macro.

For example, in C macros:

    #define X 10
    #define Y X

    #undef X
    #define X 20

    Y // expands to "20", not "10"

The C preprocessor uses lazy evaluation, meaning macro values are always resolved at the time of use. With this observation, some people claim it’s impossible to copy a macro’s value.

One known workaround is encoding one bit by whether or not a macro is defined (as seen in applin.c from IOCCC 1988 and vik2.c from IOCCC 2004). However, this approach is too inefficient for our purposes – ray tracing requires operations like multiplication, and even square root.

Instead, we discovered an efficient way to “copy” a macro’s value. The key observation is that #if eagerly evaluates the definition of a macro at that point in preprocessing. For example:

    #if X % 2
    # define X_b0 1  // The LSB of X is 1
    #else
    # define X_b0 0  // The LSB of X is 0
    #endif

This technique copies the least significant bit of X. By applying this for each bit, we can reconstruct the full value:

    #define X 10

    #if X % 2
    # define X_b0 1
    #else
    # define X_b0 0
    #endif

    #if X / 2 % 2
    # define X_b1 2
    #else
    # define X_b1 0
    #endif

    #if X / 4 % 2
    # define X_b2 4
    #else
    # define X_b2 0
    #endif

    ...

    #define Y (X_b0 + X_b1 + X_b2 + ...)

    #undef X
    #define X 20

    Y // replaced with "0 + 2 + 0 + 8 + ..." = 10

Even if X is later redefined, Y remains unchanged.

This technique resembles “information flow” in the information security. To our best knowledge, there has been no literature that mentions this technique in terms of the C preprocessor programming.

Macro Expansion Explosion

Using macros for complex expressions often causes memory issues.

For instance, consider this Newton’s method implementation for square roots:

    #define sqrt0(x, guess) (guess ? (x/guess+guess)/2 : 0)
    #define sqrt(x) sqrt0(x,sqrt0(x,sqrt0(x,sqrt0(x,sqrt0(x,10000)))))

The subroutine sqrt0 is one step of Newton’s method, and sqrt applies it five times. There are three occurences of the argument guess in the body of sqrt0. Thus, applying sqrt repeats its argument expression at least 5^3 = 243 times. This easily leads to the explosion of macro expansion.

We avoid this issue by using the earlier “copy” trick – reducing the size of macro expansions by materializing their values.

Encoding Vectors

In theory, it is trivial to encode a 3D vector in the C preprocessor: you can just define three macros for x, y, and z components. However, it creates bloated code – unsuitable for IOCCC size limits.

Instead, we encode vectors as simple tuples:

    #define vec 1,2,3

The vector projection can be implemented as follows.

    #define fst(x,y,z) x
    #define snd(x,y,z) y
    #define thd(x,y,z) z
    #define call(f,a) f(a)

Example usage:

    call(fst, vec) // expands to 1
    call(snd, vec) // expands to 2
    call(thd, vec) // expands to 3

The auxiliary macro call is required. This is a hassle of the C preprocessor, but, call(fst, vec) is expanded to fst(1,2,3), which results in 1.

We can define a vector addition by using the projections.

    #define vec3_add(a,b) fst(a)+fst(b),snd(a)+snd(b),thd(a)+thd(b)

Now, we can simply write vec3_add(v1, v2) to add two vectors. One identifier represents one vector. It is also elegantly possible to nest expressions like vec3_add(v1, vec3_add(v1, v2)).

    #define v1 0,1,2
    #define v2 2,1,0

    int main() {
      printf("(%d %d %d)\n", vec3_add(v1, v2));                // prints "(2 2 2)"
      printf("(%d %d %d)\n", vec3_add(v1, vec3_add(v1, v2)));  // prints "(2 3 4)"
    }
Ray Tracing via Self-Inclusion

Ray tracing involves recursive calls for reflection. In rt.c, this is done via:

    #include __FILE__

In the implementation, only spheres reflect rays, and only one sphere exists. Thus, the recursion depth is limited to one.

Theoretically, it is possible to support multiple reflections, but this would require implementing a “stack” to keep “local variables” in a stack frame.

Outputting a Pixel

The preprocessed result of rt.c contains some preprocessor directives (starting with #) and three integers (red, green, and blue).

    cc -E -DX=16 -DY=16 -DW=32 -DH=32 rt.c

This will produce:

    # 0 "rt.c"
    # 0 "<built-in>"
    # 0 "<command-line>"
    # 1 "/usr/include/stdc-predef.h" 1 3 4
    # 0 "<command-line>" 2
    # 1 "rt.c"
    # 3481 "rt.c"
    # 1 "rt.c" 1
    # 2090 "rt.c"
    # 1 "rt.c" 1
    # 2091 "rt.c" 2
    # 3482 "rt.c" 2
    # 3582 "rt.c"
    129
    # 3684 "rt.c"
    62
    # 3786 "rt.c"
    129

Interestingly, these are valid in the PPM format (lines starting with # are commments in PPM). Thus, just concatenating the results for all pixels (and prefixing a small header) will work to create a PPM image file.

When render a large image, we recommend use the -E -P flags as in:

    cc -E -P -DX=16 -DY=16 -DW=32 -DH=32 rt.c

This will only produce:

    129
    62
    129

Note that values appear as expressions like (0 + 2 + 0 + 8 + ...) because we use the “copy” trick. To print them as to ordinary decimal values, we use macro logic like:

    // assume red is (0 + 2 + 0 + 8)

    #if red / 100 % 10 == 0
    #  define digit0(digits) 0##digits
    #elif red / 100 % 10 == 1
    #  define digit0(digits) 1##digits
    #else
    #  define digit0(digits) 2##digits
    #endif

    #if red / 10 % 10 == 0
    #  define digit1(digits) digits0(0##digits)
    #elif red / 10 % 10 == 1
    #  define digit1(digits) digits0(1##digits)
    #elif ...
    ...
    #endif

    #if red % 10 == 0
    #  define digits digit1(0)
    #elif red % 10 == 1
    #  define digits digit1(1)
    #elif ...
    ...
    #endif

    digits // outputs "10"

Customizing the Scene

You can modify the scene by editing the top of rt.c.

Example Defaults
    #define p 0,500,4000            // sphere center position
    #define q 2000                  // sphere radius
    #define r 300,300,300           // sphere color (default: light gray)
    #define s(x) 900,200+K*7100,900 // plane color (default: checkered colors of white for K=1 and purple for K=0)
    #define t -2000,4000,1000       // light position
    #define u 50000,50000,50000     // light color (default: white)
    #define v(x) 500-x,500-x,500    // background color (default: gradation from blue to white)
Variant Example
    #define p 0,0,8000
    #define q 2000
    #define r 300,300,900
    #define s(x) 200,500-x*300,200+x*800
    #define t 2000,4000,3000
    #define u 30000,40000,30000
    #define v(x) 500,x,10

Features:

See ref.512-variant.png for the rendered result.

Limitations

Currently, this program supports:

Adding more lights is relatively simple. Adding multiple spheres would require stack support for recursive ray tracing (see “Ray Tracing by Self-Inclusion” section).

Note: we recommend GCC for performance; Clang is slower for C preprocessor-heavy tasks like this one.

Inventory for 2024/endoh1

Primary files

Secondary files


Jump to: top