Author:
- Name: Yusuke Endoh
Location: JP - Japan
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:
- Image Height x Width in pixels
- URL of the fully rendered image (in PPM form or better yet converted to PNG)
- How long it took (perhaps using the time(1) command or using the
time
shell builtin) - The starting and ending dates (using
date -u
) of your rendering - Number of CPU cores, Operating system, Compiler, Compiler version, and make version
- The number of jobs you ran in parallel
- Anything else you may have done to optimize
- Your name, (GitHub handle (optional)), (Mastodon handle (optional)), (home page URL (optional)), Country (or XX to be anonymous)
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.
- Red = 129 (0x81)
- Green = 62 (0x3e)
- Blue = 129 (0x81)
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:
- Grounded blue sphere
- Blue and green checkered floor
- Sunset-style lighting
See ref.512-variant.png
for the rendered result.
Limitations
Currently, this program supports:
- One sphere
- One floor
- One light
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
- prog.c - entry source code
- Makefile - entry Makefile
- prog.orig.c - original source code
- ref.8.png - 8x8 image in PNG form
- try.sh - script to try entry
- ref.32.png - 32x32 image in PNG form
- ref.128.png - 128x128 image in PNG form
- ref.512.png - 512x512 image in PNG form
- ref.512-variant.png - variant of 512x512 image in PNG form
Secondary files
- 2024_endoh1.tar.bz2 - download entry tarball
- README.md - markdown source for this web page
- .entry.json - entry summary and manifest in JSON
- .gitignore - list of files that should not be committed under git
- .path - directory path from top level directory
- index.html - this web page