Writing a C preprocessor for bgfx shader compiler

ibobev1 pts0 comments

Writing a C preprocessor for bgfx shader compiler | Branimir Karadžić's Home Page

Table of Contents

Introduction

For 14+ years, every shader that bgfx compiled, starting from the initial commit, went through a preprocessor written for the Amiga with origins in 1984:

fcpp - Frexx CPP (C Preprocessor)

This is a C preprocessor. It is a project based on public domain code, then forked by Daniel in<br>1993 and future work has been done under a BSD license.

That public domain code is the DECUS cpp, written by Martin Minow in 1984–85. So the preprocessor bgfx shipped in 2026 had a code lineage reaching back forty years, by way of an Amiga port. The sources still carry REG(a0) register-parameter macros and #ifdef AMIGA blocks.

And it worked. This deserves emphasis, because the rest of this post is about replacing it. Over fourteen years fcpp needed only 25 commits&rsquo; worth of attention in the bgfx tree, most of them routine resyncs with upstream. For a dependency you are supposed to forget about, that is close to the ideal outcome.

What made it a good fit specifically was how configurable it was. fcpp exposes 37 FPPTAG_* options through a single entry point:

1int fppPreProcess(struct fppTag *tags);

shaderc used 13 of them: FPPTAG_INPUT and FPPTAG_OUTPUT to route all I/O through bgfx&rsquo;s own file abstraction, FPPTAG_ERROR to capture diagnostics, FPPTAG_DEPENDS for makefile dependency output, plus FPPTAG_LINE, FPPTAG_KEEPCOMMENTS, FPPTAG_RIGHTCONCAT and others to shape the output. Crucially, fcpp keeps no global mutable state , everything lives in a struct Global allocated per call, and it never insists on calling fopen itself. Both properties are rarer than they should be, and both are exactly what an embedder wants. fcpp got the architecture right.

So what&rsquo;s bad about it?

The code is crufty. It is K&R-flavoured C89 with 1980s conventions throughout, spread over six .c files with names that tell you nothing: cpp1.c through cpp6.c. State is threaded through a 250-line struct Global. Making a behavioural change means understanding a program written under constraints 512 KB of RAM, no function prototypes, that stopped applying decades ago.

Upstream is frozen. fcpp is not abandoned ; Daniel Stenberg still merges the occasional PR. But the most recent one, in November 2025, is titled &ldquo;tidy-up whitespace, fix warnings, potential overflow&rdquo;. It is janitorial. There has been no functional development in many years. One detail captures the situation precisely. That November 2025 upstream commit fixes an sprintf buffer overflow in cpp3.c:

cpp3.c:336:28: warning: '%4d' directive writing between 4 and 11 bytes into<br>a region of size between 0 and 6 [-Wformat-overflow=]<br>bgfx had already fixed that exact bug locally in commit ab0a091, on 8 December 2018, seven years earlier . That is what a frozen dependency feels like from the outside: you carry local patches indefinitely, and upstream converges on your fix long after you forgot about it.

Why bgfx shader compiler needs preprocessor?

It is worth being clear about what that preprocessor is actually doing there, because it is not typical role that C preprocessor usually plays in C/C++ code. bgfx targets a dozen graphics APIs, and each comes with its own shading language: HLSL for Direct3D, Metal Shading Language on Apple platforms, GLSL and its embedded variant for OpenGL, SPIR-V for Vulkan, WGSL for WebGPU. They are close relatives, but they disagree constantly on details, how you declare a texture and its sampler, how you bind one to a register, how you multiply a matrix, what an entry point looks like. Rather than ask people to write and maintain one shader per API, bgfx has them write one bgfx&rsquo;s GLSL-inspired style, then include a header at the top of it: bgfx_shader.sh for vertex and fragment shaders, bgfx_compute.sh for compute. Those headers are almost nothing but preprocessor, a few hundred macros and conditional branches that reshape that single source into whatever the target language expects, selected by defines the compiler sets per target. So the preprocessor is not a preliminary pass that strips comments before the real work begins, it is the portability layer, and every shader in every bgfx project goes through it.

What else is out there?

I looked at what else was out there to find a comparable replacement.

fcpp<br>ucpp<br>mcpp<br>tinycpp<br>tcpp<br>Desired

Language<br>C89<br>C89/C99<br>C89<br>C99 + POSIX<br>C++14<br>C99 or C++20 (or below)

Core size<br>5,737 LOC<br>10,725 LOC<br>17,489 LOC<br>2,032 + 624 LOC<br>2,331 LOC<br>~2-3K LOC max

Files<br>6 .c + 7 .h<br>8 .c + 7 .h<br>7 .c + 5 .h<br>2 .c + 2 .h + 4 .h<br>1 .hpp<br>1 .cpp + 1 .h

Licence<br>BSD-3 / MIT<br>BSD-3<br>BSD-2<br>MIT (+ LGPL dep)<br>Apache-2.0<br>Permissive

Last commit<br>2025-11 (janitorial)<br>2015 (archived)<br>2017<br>2020<br>2025-04<br>Active

Third-party deps<br>none<br>none<br>none<br>libulz (LGPL-2.1)<br>none<br>none

C++ STL<br>no<br>no<br>no<br>no<br>heavy<br>no

Custom allocator<br>no<br>no<br>no<br>no<br>no<br>yes

Global mutable state<br>no<br>yes<br>yes<br>no<br>no<br>no

Virtual...

bgfx preprocessor fcpp shader through years

Related Articles