Example programs¶
All thirteen build with ./scripts/build-linux.sh (or build-macos.sh) into build/bin/, and all are checked by ./scripts/check.sh. You can also run any of them straight from its folder with hcc -jit <file>.HC.
| Example | Needs | Shows |
|---|---|---|
| hello-world | nothing | printing, classes, default arguments, loops |
| draw-basic | nothing | software framebuffer, lines, rectangles, circles, triangles, saving an image |
| font-text | nothing | bitmap font as ASCII art, scaled text, centering, multi-line strings |
| image-load | SDL2, SDL2_image | reading PPM in HolyC, PNG through SDL2_image, blitting with transparency |
| sprite-demo | SDL2 | window, frame loop, sprites, animation, keyboard and mouse, collision |
| tilemap | SDL2 | level as a string grid, scrolling camera, gravity and jumping, tile collision |
| audio | SDL2 | audio device without callbacks, square waves, queued sound effects |
| gamepad | SDL2 | game controller buttons, sticks and triggers, plugging in and out |
| templeos-bounce | SDL2 | a TempleOS demo ported: palette, fixed point, range comparisons |
| simple-3d | SDL2 | 3D maths and a software rasterizer: projection, culling, lighting |
| sdl-demo | SDL2 | SDL's own 2D renderer without the canvas, input, timing |
| opengl-demo | SDL2, OpenGL 3.3 | GL context, shaders, vertex buffers, matrices, depth testing |
| gl-texture | SDL2, OpenGL 3.3 | canvas uploaded as a texture each frame, textured quad, nearest filtering |
The shared code they include lives in examples/common/:
| File | Contents |
|---|---|
canvas.HC |
framebuffer and drawing functions, blitting, PPM reader and writer, integer min/max/abs |
font.HC |
5x7 bitmap font: FontDrawText, FontTextWidth |
sdl2.HC |
SDL2 declarations, constants, scancodes, controller buttons and axes, SdlPumpEvents, SdlKeyDown |
audio.HC |
opens an audio device; AudioBeep queues a square wave |
sdl2_image.HC |
IMG_Load; CanvasLoadImage reads PNG, JPEG and more into a canvas |
screen.HC |
shows a canvas in a scaled window; frame timing; mouse in canvas pixels |
gl.HC |
OpenGL 3.3 core declarations |
hello-world¶
// The smallest useful HolyC program, plus a few language features you'll
// see in every other example.
class Player
{
U8 *name;
I64 hp;
I64 x, y;
};
U0 Move(Player *p, I64 dx, I64 dy = 0)
{
p->x += dx;
p->y += dy;
}
U0 Main()
{
// A string literal on its own is a print statement.
"Hello from HolyC!\n";
// With arguments it works like printf.
Player p;
p.name = "Terry";
p.hp = 100;
p.x = 0;
p.y = 0;
for (I64 i = 0; i < 3; i++)
Move(&p, 2);
Move(&p, 0, 5);
"%s has %d hp and stands at (%d, %d)\n", p.name, p.hp, p.x, p.y;
F64 speed = 2.5;
"Floats print with %%f: %f\n", speed * 2;
}
draw-basic¶

Draws a sky gradient, hills, a sun and a house into a 320x200 canvas, then saves it as a PPM. Each shape is a single canvas call. It's the best starting point for experimenting, because there's no window to manage.
draw.HC
// Draws a small scene into a software framebuffer and saves it as a PPM
// image. No window and no libraries beyond HolyC's own runtime.
//
// hcc draw.HC -o draw && ./draw scene.ppm
#include "../common/canvas.HC"
#define WIDTH 320
#define HEIGHT 200
U0 DrawSky(Canvas *c)
{
for (I64 y = 0; y < c->height; y++) {
I64 t = y * 255 / c->height;
U32 color = Rgb(20 + t / 4, 30 + t / 3, 80 + t / 2);
CanvasFillRect(c, 0, y, c->width, 1, color);
}
}
U0 DrawHills(Canvas *c)
{
CanvasFillTriangle(c, -40, 200, 90, 110, 220, 200, Rgb(40, 90, 60));
CanvasFillTriangle(c, 120, 200, 250, 120, 380, 200, Rgb(30, 75, 50));
CanvasFillRect(c, 0, 170, c->width, 30, Rgb(50, 110, 60));
}
U0 DrawHouse(Canvas *c, I64 x, I64 y)
{
CanvasFillRect(c, x, y, 60, 40, Rgb(180, 150, 110));
CanvasRect(c, x, y, 60, 40, Rgb(60, 40, 30));
CanvasFillTriangle(c, x - 6, y, x + 30, y - 26, x + 66, y, Rgb(150, 50, 40));
CanvasFillRect(c, x + 24, y + 18, 12, 22, Rgb(90, 60, 40));
CanvasFillRect(c, x + 8, y + 10, 10, 10, Rgb(250, 230, 140));
CanvasFillRect(c, x + 42, y + 10, 10, 10, Rgb(250, 230, 140));
}
U0 DrawSunRays(Canvas *c, I64 cx, I64 cy)
{
U32 ray = Rgb(255, 220, 120);
CanvasLine(c, cx - 30, cy, cx - 20, cy, ray);
CanvasLine(c, cx + 20, cy, cx + 30, cy, ray);
CanvasLine(c, cx, cy - 30, cx, cy - 20, ray);
CanvasLine(c, cx, cy + 20, cx, cy + 30, ray);
CanvasLine(c, cx - 21, cy - 21, cx - 14, cy - 14, ray);
CanvasLine(c, cx + 14, cy + 14, cx + 21, cy + 21, ray);
CanvasLine(c, cx + 14, cy - 14, cx + 21, cy - 21, ray);
CanvasLine(c, cx - 21, cy + 21, cx - 14, cy + 14, ray);
}
I32 Main(I32 argc, U8 **argv)
{
U8 *path = "draw.ppm";
if (argc > 1)
path = argv[1];
Canvas c;
if (!CanvasInit(&c, WIDTH, HEIGHT)) {
"out of memory\n";
return 1;
}
DrawSky(&c);
CanvasFillCircle(&c, 250, 50, 14, Rgb(255, 210, 90));
DrawSunRays(&c, 250, 50);
DrawHills(&c);
DrawHouse(&c, 60, 132);
if (!CanvasSavePpm(&c, path)) {
"could not write %s\n", path;
CanvasFree(&c);
return 1;
}
"wrote %s (%dx%d)\n", path, c.width, c.height;
CanvasFree(&c);
return 0;
}
font-text¶

Draws a title, the full character set, a score line and a dialogue box into a canvas and saves it. The font lives in common/font.HC, so any canvas program can include it for a score or a menu. Sprites and 2D explains how the glyphs are stored.
text.HC
// Draws text into a canvas with the bitmap font in common/font.HC and
// saves it as a PPM image.
//
// hcc text.HC -o text && ./text text.ppm
#include "../common/canvas.HC"
#include "../common/font.HC"
#define WIDTH 320
#define HEIGHT 200
U0 DrawCentered(Canvas *c, I64 y, U8 *text, U32 color, I64 scale)
{
I64 x = (c->width - FontTextWidth(text, scale)) / 2;
FontDrawText(c, x + scale, y + scale, text, Rgb(0, 0, 0), scale);
FontDrawText(c, x, y, text, color, scale);
}
I32 Main(I32 argc, U8 **argv)
{
U8 *path = "text.ppm";
if (argc > 1)
path = argv[1];
Canvas c;
if (!CanvasInit(&c, WIDTH, HEIGHT)) {
"out of memory\n";
return 1;
}
CanvasClear(&c, Rgb(24, 22, 34));
DrawCentered(&c, 14, "HolyC text", Rgb(240, 200, 90), 3);
FontDrawText(&c, 16, 56, "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n0123456789 .,:!?'-+/()%", Rgb(200, 200, 220));
U8 score[64];
StrPrint(score, "Score: %d Best: %d", 1250, 9001);
FontDrawText(&c, 16, 84, score, Rgb(60, 220, 200), 2);
CanvasFillRect(&c, 16, 120, 288, 44, Rgb(40, 36, 58));
CanvasRect(&c, 16, 120, 288, 44, Rgb(120, 60, 150));
FontDrawText(&c, 26, 130, "A bat blocks the way.\nPress Space to fight,\nor run (it's faster).", Rgb(230, 230, 240));
if (!CanvasSavePpm(&c, path)) {
"could not write %s\n", path;
CanvasFree(&c);
return 1;
}
"wrote %s (%dx%d)\n", path, c.width, c.height;
CanvasFree(&c);
return 0;
}
image-load¶

Reads bricks.ppm with the PPM reader in canvas.HC and slime.png with SDL2_image, tiles the bricks, draws the slime over them with its transparent background, and saves the result. No window. Loading real images explains both loaders.
load.HC
// Loads a PPM with HolyC's own reader and a PNG through SDL2_image, then
// draws them into a canvas and saves it. Run it from this folder so it
// finds bricks.ppm and slime.png.
//
// hcc load.HC -o load && ./load scene.ppm
#include "../common/canvas.HC"
#include "../common/font.HC"
#include "../common/sdl2.HC"
#include "../common/sdl2_image.HC"
#define WIDTH 240
#define HEIGHT 160
I32 Main(I32 argc, U8 **argv)
{
U8 *path = "load.ppm";
if (argc > 1)
path = argv[1];
Canvas bricks;
if (!CanvasLoadPpm(&bricks, "bricks.ppm")) {
"could not read bricks.ppm\n";
return 1;
}
Canvas slime;
if (!CanvasLoadImage(&slime, "slime.png")) {
CanvasFree(&bricks);
return 1;
}
"bricks.ppm %dx%d, slime.png %dx%d\n", bricks.width, bricks.height, slime.width, slime.height;
Canvas c;
if (!CanvasInit(&c, WIDTH, HEIGHT)) {
"out of memory\n";
return 1;
}
CanvasClear(&c, Rgb(24, 22, 34));
for (I64 x = 0; x < WIDTH; x += bricks.width) {
CanvasBlit(&c, &bricks, x, HEIGHT - bricks.height * 2);
CanvasBlit(&c, &bricks, x, HEIGHT - bricks.height);
}
for (I64 i = 0; i < 5; i++)
CanvasBlit(&c, &slime, 30 + i * 40, HEIGHT - bricks.height * 2 - slime.height + 2);
FontDrawText(&c, 8, 8, "bricks.ppm: HolyC PPM reader", Rgb(200, 200, 220));
FontDrawText(&c, 8, 20, "slime.png: SDL2 image", Rgb(60, 220, 200));
Bool ok = CanvasSavePpm(&c, path);
if (ok)
"wrote %s (%dx%d)\n", path, c.width, c.height;
else
"could not write %s\n", path;
CanvasFree(&c);
CanvasFree(&slime);
CanvasFree(&bricks);
IMG_Quit();
if (!ok)
return 1;
return 0;
}
sprite-demo¶
cd examples/sprite-demo && hcc -jit sprite.HC
hcc sprite.HC -o sprite && ./sprite --ppm frame.ppm # no window
![]()
| Input | Action |
|---|---|
| ++arrow-keys++ or W A S D | move |
| left click | release a bat at the cursor |
| Esc | quit |
--ppm plays three scripted seconds with a fixed random seed (it picks up one gem and releases a third bat), then saves the frame. Update takes an Input instead of reading SDL, which is what makes that possible.
Collect gems. Every third gem releases another bat, and touching a bat resets your score. The window title shows your score and best. Sprites and 2D walks through the code.
tilemap¶
cd examples/tilemap && hcc -jit tilemap.HC
hcc tilemap.HC -o tilemap && ./tilemap --ppm frame.ppm # no window

| Input | Action |
|---|---|
| Left Right or A D | run |
| Space, Up or W | jump |
| Esc | quit |
A 100x24 tile level, scrolled by a camera that follows the player. Collect the gems; falling into a pit sends you back to the start. --ppm plays two seconds of running and jumping with no window and saves the last frame, which is how check.sh tests it. Sprites and 2D explains the level format, collision and camera.
audio¶
Plays a short tune, a coin pickup and a falling three-note "hurt" sound, then exits. Nothing is drawn. Sound explains the queue, the square wave and how to use it from a game.
beep.HC
// Plays a few square-wave sounds with no window: a short tune, a coin
// pickup and a falling "hurt" sound. Everything is queued up front, then
// the program waits for the queue to drain.
#include "../common/sdl2.HC"
#include "../common/audio.HC"
#define NOTE_C5 523
#define NOTE_E5 659
#define NOTE_G5 784
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_E6 1319
// Pairs of frequency and length in ms. 0 Hz is a rest.
I64 g_tune[] = {
NOTE_C5, 120, NOTE_E5, 120, NOTE_G5, 120, NOTE_C6, 240,
0, 120,
NOTE_G5, 120, NOTE_C6, 360,
};
U0 PlayCoin(Audio *a)
{
AudioBeep(a, NOTE_B5, 60);
AudioBeep(a, NOTE_E6, 180);
}
U0 PlayHurt(Audio *a)
{
for (I64 freq = 400; freq >= 200; freq -= 100)
AudioBeep(a, freq, 70);
}
I32 Main()
{
Audio audio;
if (!AudioOpen(&audio))
return 1;
I64 notes = sizeof(g_tune) / sizeof(I64) / 2;
for (I64 i = 0; i < notes; i++)
AudioBeep(&audio, g_tune[i * 2], g_tune[i * 2 + 1]);
AudioBeep(&audio, 0, 300);
PlayCoin(&audio);
AudioBeep(&audio, 0, 300);
PlayHurt(&audio);
"queued %d ms of sound\n", AudioQueuedMs(&audio);
AudioWait(&audio);
// The queue empties when SDL takes the last block, which is a little
// before the speaker finishes it.
SDL_Delay(100);
AudioClose(&audio);
SDL_Quit();
"done\n";
return 0;
}
gamepad¶
cd examples/gamepad && hcc -jit pad.HC
hcc pad.HC -o pad && ./pad --ppm frame.ppm # virtual controller, no window

Draws the first connected controller: bumpers and trigger bars at the top, both sticks, the d-pad, Back/Guide/Start and the face buttons, with raw stick values along the bottom. With no controller it says so, and it picks one up when it's plugged in. Esc quits. --ppm needs no hardware: it attaches one of SDL's virtual controllers, sets its sticks and buttons, saves the frame, then unplugs and replugs it, which is how check.sh tests it. Game controllers covers the API.
templeos-bounce¶
cd examples/templeos-bounce && hcc -jit bounce.HC
hcc bounce.HC -o bounce && ./bounce --ppm frame.ppm # no window

TempleOS's own Demo/Graphics/Bounce.HC, ported: sixteen points in the TempleOS palette bounce around and leave trails. Esc quits. Porting from TempleOS shows the original next to the port and explains each change.
simple-3d¶
cd examples/simple-3d && hcc -jit cube.HC
hcc cube.HC -o cube && ./cube --ppm frame.ppm # one frame, no window

A cube and a pyramid rendered on the CPU. Space toggles wireframe. This is a concept demo for learning the 3D pipeline, not a technique to build a game on. 3D overview explains each stage and what to use instead.
sdl-demo¶
cd examples/sdl-demo && hcc -jit sdl_example.HC
hcc sdl_example.HC -o sdl_example && ./sdl_example --ppm frame.ppm # no window

Uses SDL_RenderFillRect and SDL_RenderDrawLine directly instead of a canvas. ++arrow-keys++ move the white box, holding the left mouse button drags the orange one, and the green one bounces. This is the quickest route to a working 2D game when you don't need per-pixel control.
--ppm makes the same drawing calls on SDL's software renderer, which draws into a surface in memory, and saves the frame three seconds in.
opengl-demo¶
cd examples/opengl-demo && hcc -jit opengl_example.HC
# macOS, ahead-of-time:
hcc opengl_example.HC -o opengl_example -clibs "-framework OpenGL"

A spinning, vertex-coloured cube. It prints the OpenGL version and renderer it got, which is the first thing to check when something doesn't draw.
./opengl_example --ppm frame.ppm draws the frame at one second into a hidden window and saves it with glReadPixels (GlSavePpm in gl.HC). Different GPUs and drivers round a few edge pixels differently, so check.sh only compares the result on Mesa's llvmpipe and skips the comparison elsewhere.
opengl_example.HC
// A spinning, vertex-coloured cube on OpenGL 3.3 core.
//
// HolyC owns everything the CPU does: the window, the vertex data, the
// matrices and the frame loop. The two small GLSL shaders below are the
// only non-HolyC code, and they run on the GPU.
//
// Linux: hcc opengl_example.HC -o opengl_example
// macOS: hcc opengl_example.HC -o opengl_example -clibs "-framework OpenGL"
//
// ./opengl_example --ppm a.ppm draw the frame at one second into a
// hidden window and save it
#include "../common/sdl2.HC"
#include "../common/gl.HC"
#define WIDTH 800
#define HEIGHT 600
U8 *g_vertex_src =
"#version 330 core\n"
"layout(location = 0) in vec3 a_pos;\n"
"layout(location = 1) in vec3 a_color;\n"
"uniform mat4 u_mvp;\n"
"out vec3 v_color;\n"
"void main() {\n"
" v_color = a_color;\n"
" gl_Position = u_mvp * vec4(a_pos, 1.0);\n"
"}\n";
U8 *g_fragment_src =
"#version 330 core\n"
"in vec3 v_color;\n"
"out vec4 frag;\n"
"void main() {\n"
" frag = vec4(v_color, 1.0);\n"
"}\n";
// x, y, z, r, g, b per corner.
F32 g_vertices[8 * 6] = {
-1.0, -1.0, -1.0, 0.9, 0.3, 0.2,
1.0, -1.0, -1.0, 0.9, 0.8, 0.2,
1.0, 1.0, -1.0, 0.3, 0.9, 0.4,
-1.0, 1.0, -1.0, 0.2, 0.6, 0.9,
-1.0, -1.0, 1.0, 0.7, 0.3, 0.9,
1.0, -1.0, 1.0, 0.9, 0.9, 0.9,
1.0, 1.0, 1.0, 0.2, 0.9, 0.8,
-1.0, 1.0, 1.0, 0.9, 0.5, 0.6,
};
U32 g_indices[36] = {
0, 2, 1, 0, 3, 2,
4, 5, 6, 4, 6, 7,
0, 1, 5, 0, 5, 4,
3, 6, 2, 3, 7, 6,
0, 4, 7, 0, 7, 3,
1, 2, 6, 1, 6, 5,
};
// Matrices are 16 floats in column-major order, which is what GLSL expects.
U0 Mat4Identity(F64 *m)
{
MemSet(m, 0, 16 * sizeof(F64));
m[0] = 1.0;
m[5] = 1.0;
m[10] = 1.0;
m[15] = 1.0;
}
U0 Mat4Mul(F64 *out, F64 *a, F64 *b)
{
F64 tmp[16];
for (I64 col = 0; col < 4; col++) {
for (I64 row = 0; row < 4; row++) {
F64 sum = 0.0;
for (I64 k = 0; k < 4; k++)
sum += a[k * 4 + row] * b[col * 4 + k];
tmp[col * 4 + row] = sum;
}
}
MemCpy(out, tmp, 16 * sizeof(F64));
}
U0 Mat4Perspective(F64 *m, F64 fov_y, F64 aspect, F64 near, F64 far)
{
F64 f = 1.0 / tan(fov_y / 2.0);
MemSet(m, 0, 16 * sizeof(F64));
m[0] = f / aspect;
m[5] = f;
m[10] = (far + near) / (near - far);
m[11] = -1.0;
m[14] = 2.0 * far * near / (near - far);
}
U0 Mat4RotateY(F64 *m, F64 angle)
{
F64 c = cos(angle), s = sin(angle);
Mat4Identity(m);
m[0] = c;
m[2] = -s;
m[8] = s;
m[10] = c;
}
U0 Mat4RotateX(F64 *m, F64 angle)
{
F64 c = cos(angle), s = sin(angle);
Mat4Identity(m);
m[5] = c;
m[6] = s;
m[9] = -s;
m[10] = c;
}
U0 Mat4Translate(F64 *m, F64 x, F64 y, F64 z)
{
Mat4Identity(m);
m[12] = x;
m[13] = y;
m[14] = z;
}
U32 CompileShader(U32 type, U8 *src)
{
U32 shader = glCreateShader(type);
glShaderSource(shader, 1, &src, NULL);
glCompileShader(shader);
I32 ok;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (!ok) {
U8 log[1024];
glGetShaderInfoLog(shader, 1024, NULL, log);
"shader compile failed:\n%s\n", log;
glDeleteShader(shader);
return 0;
}
return shader;
}
U32 LinkProgram(U8 *vertex_src, U8 *fragment_src)
{
U32 vs = CompileShader(GL_VERTEX_SHADER, vertex_src);
U32 fs = CompileShader(GL_FRAGMENT_SHADER, fragment_src);
if (!vs || !fs)
return 0;
U32 program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
I32 ok;
glGetProgramiv(program, GL_LINK_STATUS, &ok);
if (!ok) {
U8 log[1024];
glGetProgramInfoLog(program, 1024, NULL, log);
"program link failed:\n%s\n", log;
glDeleteProgram(program);
return 0;
}
return program;
}
U0 DrawFrame(U32 program, I32 u_mvp, U32 vao, I64 ms, I32 fb_w, I32 fb_h)
{
F64 proj[16], rot_x[16], rot_y[16], move[16], model[16], mvp[16];
F32 mvp32[16];
glViewport(0, 0, fb_w, fb_h);
F64 t = ms;
t /= 1000.0;
F64 aspect = fb_w;
aspect /= fb_h;
Mat4Perspective(proj, 1.0, aspect, 0.1, 100.0);
Mat4RotateY(rot_y, t * 0.8);
Mat4RotateX(rot_x, t * 0.5);
Mat4Translate(move, 0.0, 0.0, -5.0);
Mat4Mul(model, rot_y, rot_x);
Mat4Mul(model, move, model);
Mat4Mul(mvp, proj, model);
for (I64 i = 0; i < 16; i++)
mvp32[i] = mvp[i];
glClearColor(0.08, 0.09, 0.12, 1.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(program);
glUniformMatrix4fv(u_mvp, 1, GL_FALSE, mvp32);
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
}
I32 Main(I32 argc, U8 **argv)
{
U8 *ppm_path = NULL;
U32 show = SDL_WINDOW_SHOWN;
if (argc > 2 && !StrCmp(argv[1], "--ppm")) {
ppm_path = argv[2];
show = SDL_WINDOW_HIDDEN;
}
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
"SDL_Init failed: %s\n", SDL_GetError();
return 1;
}
// Ask for 3.3 core before the window exists. macOS only hands out a
// modern context when it's core and forward-compatible.
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
U0 *window = SDL_CreateWindow("HolyC + OpenGL", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
WIDTH, HEIGHT, SDL_WINDOW_OPENGL | show);
if (!window) {
"SDL_CreateWindow failed: %s\n", SDL_GetError();
SDL_Quit();
return 1;
}
U0 *context = SDL_GL_CreateContext(window);
if (!context) {
"SDL_GL_CreateContext failed: %s\n", SDL_GetError();
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
SDL_GL_SetSwapInterval(1);
"OpenGL %s on %s\n", glGetString(GL_VERSION), glGetString(GL_RENDERER);
U32 program = LinkProgram(g_vertex_src, g_fragment_src);
if (!program) {
SDL_GL_DeleteContext(context);
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
I32 u_mvp = glGetUniformLocation(program, "u_mvp");
U32 vao, vbo, ebo;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertices), g_vertices, GL_STATIC_DRAW);
glGenBuffers(1, &ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(g_indices), g_indices, GL_STATIC_DRAW);
I32 stride = 6 * sizeof(F32);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, 0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, 3 * sizeof(F32));
glEnable(GL_DEPTH_TEST);
I32 status = 0;
I32 fb_w, fb_h;
if (ppm_path) {
SDL_GL_GetDrawableSize(window, &fb_w, &fb_h);
DrawFrame(program, u_mvp, vao, 1000, fb_w, fb_h);
if (GlSavePpm(ppm_path, fb_w, fb_h)) {
"wrote %s\n", ppm_path;
} else {
"could not write %s\n", ppm_path;
status = 1;
}
} else {
while (SdlPumpEvents() && !SdlKeyDown(SC_ESCAPE)) {
SDL_GL_GetDrawableSize(window, &fb_w, &fb_h);
DrawFrame(program, u_mvp, vao, SDL_GetTicks(), fb_w, fb_h);
SDL_GL_SwapWindow(window);
}
}
glDeleteBuffers(1, &ebo);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
glDeleteProgram(program);
SDL_GL_DeleteContext(context);
SDL_DestroyWindow(window);
SDL_Quit();
return status;
}
gl-texture¶
cd examples/gl-texture && hcc -jit texture.HC
# macOS, ahead-of-time:
hcc texture.HC -o texture -clibs "-framework OpenGL"

Draws a scene with the canvas and font every frame, uploads it into an OpenGL texture and shows it on a quad that swings back and forth in 3D. It's the bridge between the software and GPU halves of this guide: anything drawn with canvas.HC can end up in a GL scene. Graphics and rendering covers the byte order, row order and filtering details.
./texture --ppm frame.ppm saves the frame at 2.5 seconds the same way as opengl-demo, and is checked the same way.
texture.HC
// A software canvas shown through OpenGL. Each frame is drawn on the CPU
// with canvas.HC and font.HC, copied into a GL texture, and drawn on a
// quad that turns slowly in 3D.
//
// Linux: hcc texture.HC -o texture
// macOS: hcc texture.HC -o texture -clibs "-framework OpenGL"
//
// ./texture --ppm a.ppm draw the frame at 2.5 seconds into a hidden
// window and save it
#include "../common/canvas.HC"
#include "../common/font.HC"
#include "../common/sdl2.HC"
#include "../common/gl.HC"
#define WIDTH 800
#define HEIGHT 600
#define CANVAS_W 160
#define CANVAS_H 120
// The quad turns around the y axis, is pushed 3 units away from the
// camera, and is projected with a focal length of 2. One quad needs no
// matrices, so the shader does it directly.
U8 *g_vertex_src =
"#version 330 core\n"
"layout(location = 0) in vec2 a_pos;\n"
"layout(location = 1) in vec2 a_uv;\n"
"uniform float u_angle;\n"
"uniform float u_aspect;\n"
"out vec2 v_uv;\n"
"void main() {\n"
" float c = cos(u_angle);\n"
" float s = sin(u_angle);\n"
" vec3 p = vec3(a_pos.x * c, a_pos.y, a_pos.x * s - 3.0);\n"
" gl_Position = vec4(p.x * 2.0 / u_aspect, p.y * 2.0, 0.0, -p.z);\n"
" v_uv = a_uv;\n"
"}\n";
U8 *g_fragment_src =
"#version 330 core\n"
"uniform sampler2D u_canvas;\n"
"in vec2 v_uv;\n"
"out vec4 frag;\n"
"void main() {\n"
" frag = texture(u_canvas, v_uv);\n"
"}\n";
// x, y, u, v for a triangle strip. The canvas's first row is its top, and
// it's also the first row of the texture (v = 0), so the top corners of
// the quad get v = 0.
F32 g_quad[4 * 4] = {
-1.333, -1.0, 0.0, 1.0,
1.333, -1.0, 1.0, 1.0,
-1.333, 1.0, 0.0, 0.0,
1.333, 1.0, 1.0, 0.0,
};
U32 CompileShader(U32 type, U8 *src)
{
U32 shader = glCreateShader(type);
glShaderSource(shader, 1, &src, NULL);
glCompileShader(shader);
I32 ok;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (!ok) {
U8 log[1024];
glGetShaderInfoLog(shader, 1024, NULL, log);
"shader compile failed:\n%s\n", log;
glDeleteShader(shader);
return 0;
}
return shader;
}
U32 LinkProgram(U8 *vertex_src, U8 *fragment_src)
{
U32 vs = CompileShader(GL_VERTEX_SHADER, vertex_src);
U32 fs = CompileShader(GL_FRAGMENT_SHADER, fragment_src);
if (!vs || !fs)
return 0;
U32 program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
I32 ok;
glGetProgramiv(program, GL_LINK_STATUS, &ok);
if (!ok) {
U8 log[1024];
glGetProgramInfoLog(program, 1024, NULL, log);
"program link failed:\n%s\n", log;
glDeleteProgram(program);
return 0;
}
return program;
}
// Allocates GPU storage for a texture the size of the canvas. Nearest
// filtering keeps the pixels sharp when the quad is scaled up.
U32 CreateCanvasTexture(Canvas *c)
{
U32 texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, c->width, c->height, 0,
GL_BGRA, GL_UNSIGNED_BYTE, NULL);
return texture;
}
// A canvas pixel is the U32 0xAARRGGBB. Stored little-endian, its bytes
// are B, G, R, A, which is exactly GL_BGRA with GL_UNSIGNED_BYTE, so the
// pixels go to the GPU as they are.
U0 UploadCanvas(U32 texture, Canvas *c)
{
glBindTexture(GL_TEXTURE_2D, texture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, c->width, c->height,
GL_BGRA, GL_UNSIGNED_BYTE, c->pixels);
}
U0 DrawScene(Canvas *c, I64 ms)
{
CanvasClear(c, Rgb(24, 22, 34));
for (I64 y = 0; y < c->height; y += 8)
for (I64 x = (y / 8 % 2) * 8; x < c->width; x += 16)
CanvasFillRect(c, x, y, 8, 8, Rgb(30, 28, 42));
CanvasRect(c, 0, 0, c->width, c->height, Rgb(120, 60, 150));
// Back and forth across the canvas, one pixel every 8 ms.
I64 range = c->width - 24;
I64 t = (ms / 8) % (range * 2);
if (t > range)
t = range * 2 - t;
CanvasFillCircle(c, 12 + t, 70, 8, Rgb(240, 200, 90));
FontDrawText(c, 8, 8, "Canvas", Rgb(240, 240, 250), 2);
FontDrawText(c, 8, 28, "on a GL texture", Rgb(60, 220, 200));
U8 clock[32];
StrPrint(clock, "%d ms", ms);
FontDrawText(c, 8, c->height - 14, clock, Rgb(200, 200, 220));
}
U0 DrawFrame(U32 program, I32 u_angle, I32 u_aspect, U32 vao, U32 texture, Canvas *canvas,
I64 ms, I32 fb_w, I32 fb_h)
{
glViewport(0, 0, fb_w, fb_h);
DrawScene(canvas, ms);
UploadCanvas(texture, canvas);
F64 t = ms;
F64 aspect = fb_w;
aspect /= fb_h;
// Swing up to about 35 degrees each way, so the back never shows.
F32 angle = sin(t / 1600.0) * 0.6;
F32 aspect32 = aspect;
glClearColor(0.08, 0.09, 0.12, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(program);
glUniform1f(u_angle, angle);
glUniform1f(u_aspect, aspect32);
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
I32 Main(I32 argc, U8 **argv)
{
U8 *ppm_path = NULL;
U32 show = SDL_WINDOW_SHOWN;
if (argc > 2 && !StrCmp(argv[1], "--ppm")) {
ppm_path = argv[2];
show = SDL_WINDOW_HIDDEN;
}
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
"SDL_Init failed: %s\n", SDL_GetError();
return 1;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
U0 *window = SDL_CreateWindow("HolyC canvas on OpenGL", SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED, WIDTH, HEIGHT,
SDL_WINDOW_OPENGL | show);
if (!window) {
"SDL_CreateWindow failed: %s\n", SDL_GetError();
SDL_Quit();
return 1;
}
U0 *context = SDL_GL_CreateContext(window);
if (!context) {
"SDL_GL_CreateContext failed: %s\n", SDL_GetError();
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
SDL_GL_SetSwapInterval(1);
"OpenGL %s on %s\n", glGetString(GL_VERSION), glGetString(GL_RENDERER);
U32 program = LinkProgram(g_vertex_src, g_fragment_src);
Canvas canvas;
if (!program || !CanvasInit(&canvas, CANVAS_W, CANVAS_H)) {
SDL_GL_DeleteContext(context);
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
I32 u_angle = glGetUniformLocation(program, "u_angle");
I32 u_aspect = glGetUniformLocation(program, "u_aspect");
U32 texture = CreateCanvasTexture(&canvas);
U32 vao, vbo;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(g_quad), g_quad, GL_STATIC_DRAW);
I32 stride = 4 * sizeof(F32);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, stride, 0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, 2 * sizeof(F32));
I32 status = 0;
I32 fb_w, fb_h;
if (ppm_path) {
SDL_GL_GetDrawableSize(window, &fb_w, &fb_h);
DrawFrame(program, u_angle, u_aspect, vao, texture, &canvas, 2500, fb_w, fb_h);
if (GlSavePpm(ppm_path, fb_w, fb_h)) {
"wrote %s\n", ppm_path;
} else {
"could not write %s\n", ppm_path;
status = 1;
}
} else {
while (SdlPumpEvents() && !SdlKeyDown(SC_ESCAPE)) {
SDL_GL_GetDrawableSize(window, &fb_w, &fb_h);
DrawFrame(program, u_angle, u_aspect, vao, texture, &canvas, SDL_GetTicks(), fb_w, fb_h);
SDL_GL_SwapWindow(window);
}
}
glDeleteTextures(1, &texture);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
glDeleteProgram(program);
CanvasFree(&canvas);
SDL_GL_DeleteContext(context);
SDL_DestroyWindow(window);
SDL_Quit();
return status;
}