When I first got the idea for creating a tiny mandelbrot renderer I knew It would be possible since people having creating tiny demos for a while and that I would probably use a shader. However at that point I had no idea for how to even begin so for a while it was just something that in the back of my head untill recently when I had finaly gotten to learning opengl and realized that I finaly knew how I how do it and as a result I was able create a tiny Mandelbrot renderer. My main questions are in what ways can i improve the code in general and how can make the executable smaller since it is currently 1538 bytes and I don’t know how I can make smaller?
main.c
// standard headers
#include <stdint.h>
#include <stdbool.h>
// windows headers
#define WIN32_LEAN_AND_MEAN
#define WIN32_EXTRA_LEAN
#include <Windows.h>
// opengl headers
#define WGL_WGLEXT_PROTOTYPES
#include <gl/GL.h>
#include "glext.h"
#include "wglext.h"
#include "opengl.h"
// needed when we use floats
extern int _fltused;
int _fltused;
typedef struct Window
{
HDC device_context;
float aspect_ratio;
float scale, pos[2];
float smooth_scale, smooth_pos[2];
int32_t max_iterations;
} Window;
typedef enum Keys
{
KEY_W,
KEY_S,
KEY_A,
KEY_D,
KEY_PLUS1,
KEY_PLUS2,
KEY_MINUS1,
KEY_MINUS2,
KEY_UP,
KEY_DOWN,
KEY_R,
KEY_CTRL,
KEY_LENGTH // needed to keep track of number of keys
} Keys;
// NOTE: we could use GetWindowLongPtr and SetWindowLongPtr
// however this is much more easier
static Window global_window;
static bool keys[KEY_LENGTH];
static LRESULT CALLBACK WinProc(HWND window_handle, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_SYSKEYDOWN:
case WM_SYSCHAR:
case WM_SYSKEYUP:
{
} break;
case WM_SIZE:
{
// the width and height are stored in the low and high word of lParam respectively
LPARAM const width = lParam & 0xFFFF;
LPARAM const height = (lParam >> 16) & 0xFFFF;
// store the aspect ratio
global_window.aspect_ratio = (float)width / (float)height;
glViewport(0, 0, width, height);
} break;
case WM_QUIT:
case WM_CLOSE:
case WM_DESTROY:
{
ExitProcess(0);
}
case WM_KEYUP:
case WM_KEYDOWN:
{
bool const should_flip = ((lParam >> 30) & 0x1) == ((lParam >> 31) & 0x1);
if (should_flip)
{
switch (wParam)
{
case 'W':
{
keys[KEY_W] = !keys[KEY_W];
} break;
case 'S':
{
keys[KEY_S] = !keys[KEY_S];
} break;
case 'A':
{
keys[KEY_A] = !keys[KEY_A];
} break;
case 'D':
{
keys[KEY_D] = !keys[KEY_D];
} break;
case 'R':
{
keys[KEY_R] = !keys[KEY_R];
} break;
case VK_CONTROL:
{
keys[KEY_CTRL] = !keys[KEY_CTRL];
} break;
case VK_OEM_PLUS:
{
keys[KEY_PLUS1] = !keys[KEY_PLUS1];
} break;
case VK_ADD:
{
keys[KEY_PLUS2] = !keys[KEY_PLUS2];
} break;
case VK_OEM_MINUS:
{
keys[KEY_MINUS1] = !keys[KEY_MINUS1];
} break;
case VK_SUBTRACT:
{
keys[KEY_MINUS2] = !keys[KEY_MINUS2];
} break;
case VK_UP:
{
keys[KEY_UP] = !keys[KEY_UP];
} break;
case VK_DOWN:
{
keys[KEY_DOWN] = !keys[KEY_DOWN];
} break;
}
if (wParam == VK_ESCAPE) ExitProcess(0);
}
} break;
default:
{
return DefWindowProc(window_handle, message, wParam, lParam);
}
}
return 0;
}
static void create_opengl_context(HDC const device_context)
{
// same as:
// static PIXELFORMATDESCRIPTOR const pfd = {
// .nSize = sizeof(pfd),
// .dwFlags = PFD_DOUBLEBUFFER | PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW,
// .iPixelType = PFD_TYPE_RGBA,
// .cColorBits = 32,
// .cDepthBits = 32,
// .iLayerType = PFD_MAIN_PLANE
// };
SetPixelFormat(device_context, 9, NULL);
// create an opengl 3.3 context
HGLRC const opengl_context = wglCreateContext(device_context);
// make the new opengl context current and active
wglMakeCurrent(device_context, opengl_context);
PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT = (PFNWGLSWAPINTERVALEXTPROC)
wglGetProcAddress("wglSwapIntervalEXT");
wglSwapIntervalEXT(0);
}
static void create_window(int32_t width, int32_t height)
{
// create a window class
static WNDCLASSA const wndclass = {
.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC,
.lpfnWndProc = &WinProc,
.lpszClassName = "0",
};
RegisterClassA(&wndclass);
// create a window
HWND const window_handle = CreateWindowA(wndclass.lpszClassName,
"",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
width, height, NULL, NULL,
NULL, NULL);
// create a device context
HDC const device_context = GetDC(window_handle);
// create an opengl context
create_opengl_context(device_context);
// load opengl extensions after creating an opengl context
load_extensions();
// setup global window
{
global_window.device_context = device_context;
global_window.aspect_ratio = (float)width / (float)height;
global_window.scale = 1.0f;
global_window.smooth_scale = 0.5f;
global_window.max_iterations = 200;
}
// show the window
ShowWindow(window_handle, SW_SHOWDEFAULT);
}
static unsigned int compile_shaders(char const *vertex_shader_source,
char const *fragment_shader_source)
{
// compile vertex shader
unsigned int vertex_shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex_shader, 1, &vertex_shader_source, NULL);
glCompileShader(vertex_shader);
// compile fragment shader
unsigned int fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL);
glCompileShader(fragment_shader);
// only needed for debugging
#ifdef DEBUG_MODE
{
int success;
char info_log[512];
glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
if(success == GL_FALSE)
{
glGetShaderInfoLog(fragment_shader, sizeof(info_log),
NULL, info_log);
WriteFile((HANDLE)(STD_OUTPUT_HANDLE),
info_log, (DWORD)lstrlenA(info_log),
NULL, NULL);
}
}
#endif
// link the shaders
unsigned int shader_program = glCreateProgram();
glAttachShader(shader_program, vertex_shader);
glAttachShader(shader_program, fragment_shader);
glLinkProgram(shader_program);
return shader_program;
}
static float lerp(float v0, float v1, float t)
{
return (1.0f - t) * v0 + t * v1;
}
__declspec(noreturn) void __stdcall entry(void)
{
create_window(800, 600);
// by making this smaller we can save space at the cost of readabilty
#define VERTEX_SHADER \
"#version 330\n" \
"out vec2 u;void main(){u=vec2[](vec2(0),vec2(1,0),vec2(0,1),vec2(1))[gl_VertexID];" \
"gl_Position=vec4(vec2[](vec2(-1,-1),vec2(1,-1),vec2(-1,1),vec2(1))[gl_VertexID],0,1);}" \
#define FRAGMENT_SHADER \
"#version 330\n" \
"#define B 200000.0\n" \
"out vec4 F;in vec2 u;uniform int I;uniform float A;uniform vec4 D;" \
"void main(){vec2 c=((u*2-1)*vec2(A,1)*D.y-D.zw);vec2 z=vec2(0);int i;" \
"for(i=0;i<I&&dot(z,z)<B;++i)z=vec2(z.x*z.x-z.y*z.y,z.x*z.y*2)+c;" \
"float s=sqrt((i-log2(log(dot(z,z))/log(B)))/float(I));" \
"F=(sin(D.x+20*s*vec4(1.5,1.8,2.1,0))*0.5+0.5)*float(i!=I);}" \
unsigned int const shader_program = compile_shaders(VERTEX_SHADER,
FRAGMENT_SHADER);
glUseProgram(shader_program);
float color_offset = 0.0f;
MSG msg;
for(;;)
{
// process events and messages
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
else
{
// pass uniforms
glUniform1f(glGetUniformLocation(shader_program, "A"),
global_window.aspect_ratio);
glUniform4f(glGetUniformLocation(shader_program, "D"), color_offset, global_window.smooth_scale,
global_window.smooth_pos[0], global_window.smooth_pos[1]);
glUniform1i(glGetUniformLocation(shader_program, "I"),
global_window.max_iterations);
// draw a quad
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// finally draw to the screen
SwapBuffers(global_window.device_context);
// the smooth values will smoothly converge to the real values
{
global_window.smooth_pos[0] = lerp(global_window.smooth_pos[0],
global_window.pos[0], 0.005f);
global_window.smooth_pos[1] = lerp(global_window.smooth_pos[1],
global_window.pos[1], 0.005f);
global_window.smooth_scale = lerp(global_window.smooth_scale,
global_window.scale, 0.005f);
}
color_offset += 0.001f;
}
// handle input
{
// some keyboards have two plus keys(number row and numpad)
if (keys[KEY_PLUS1] || keys[KEY_PLUS2])
{
global_window.scale *= 1.0f - 0.003f;
}
// see the above comment
if(keys[KEY_MINUS1] || keys[KEY_MINUS2])
{
global_window.scale *= 1.0f + 0.003f;
}
if (keys[KEY_W])
{
global_window.pos[1] -= global_window.scale * 0.003f;
}
if (keys[KEY_S])
{
global_window.pos[1] += global_window.scale * 0.003f;
}
if (keys[KEY_A])
{
global_window.pos[0] += global_window.scale * 0.003f;
}
if (keys[KEY_D])
{
global_window.pos[0] -= global_window.scale * 0.003f;
}
// if ctrl-r is pressed reset the scale and pos
if (keys[KEY_CTRL] && keys[KEY_R])
{
global_window.pos[0] = 0.0f;
global_window.pos[1] = 0.0f;
global_window.scale = 1.0f;
}
if (keys[KEY_UP])
{
global_window.max_iterations += 1;
}
if (keys[KEY_DOWN] &&
global_window.max_iterations > 2)
{
global_window.max_iterations -= 1;
}
}
}
}
opengl.h
#ifndef OPENGL_H
#define OPENGL_H
/* from http://dantefalcone.name/tutorials/1a-windows-win32-window-and-3d-context-creation/ */
// Program
static PFNGLCREATEPROGRAMPROC glCreateProgram;
static PFNGLUSEPROGRAMPROC glUseProgram;
static PFNGLATTACHSHADERPROC glAttachShader;
static PFNGLLINKPROGRAMPROC glLinkProgram;
static PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation;
static PFNGLUNIFORM1IPROC glUniform1i;
static PFNGLUNIFORM1FPROC glUniform1f;
static PFNGLUNIFORM4FPROC glUniform4f;
// Shader
static PFNGLCREATESHADERPROC glCreateShader;
static PFNGLSHADERSOURCEPROC glShaderSource;
static PFNGLCOMPILESHADERPROC glCompileShader;
// for debuging
static PFNGLGETSHADERIVPROC glGetShaderiv;
static PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog;
// we need to load opengl extensions from opengl32.dll
static void load_extensions(void)
{
// Program
glCreateProgram = (PFNGLCREATEPROGRAMPROC)wglGetProcAddress("glCreateProgram");
glUseProgram = (PFNGLUSEPROGRAMPROC)wglGetProcAddress("glUseProgram");
glAttachShader = (PFNGLATTACHSHADERPROC)wglGetProcAddress("glAttachShader");
glLinkProgram = (PFNGLLINKPROGRAMPROC)wglGetProcAddress("glLinkProgram");
glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC)wglGetProcAddress("glGetUniformLocation");
glUniform1i = (PFNGLUNIFORM1IPROC)wglGetProcAddress("glUniform1i");
glUniform1f = (PFNGLUNIFORM1FPROC)wglGetProcAddress("glUniform1f");
glUniform4f = (PFNGLUNIFORM4FPROC)wglGetProcAddress("glUniform4f");
// Shader
glCreateShader = (PFNGLCREATESHADERPROC)wglGetProcAddress("glCreateShader");
glShaderSource = (PFNGLSHADERSOURCEPROC)wglGetProcAddress("glShaderSource");
glCompileShader = (PFNGLCOMPILESHADERPROC)wglGetProcAddress("glCompileShader");
#ifdef DEBUG_MODE
glGetShaderInfoLog = (PFNGLGETSHADERINFOLOGPROC)wglGetProcAddress("glGetShaderInfoLog");
glGetShaderiv = (PFNGLGETSHADERIVPROC)wglGetProcAddress("glGetShaderiv");
#endif
}
#endif // OPENGL_H
wglext.h
https://www.khronos.org/registry/OpenGL/api/GL/wglext.h
glext.h
https://www.khronos.org/registry/OpenGL/api/GL/glext.h
to build use:
Makefile
:
NAME = prog
CC = clang-cl # if you don't have clang-cl use cl instead and change FLAGS
FLAGS = -m32 -W4 -c -nologo -GS- -Gr -Ofast -Oi -Gs9999999
LINK_FLAGS = /UNSAFEIMPORT /TRUNCATEFLOATS:18 /HASHSIZE:10 main.obj \
kernel32.lib user32.lib shell32.lib gdi32.lib opengl32.lib \
/SUBSYSTEM:windows /NODEFAULTLIB /ENTRY:entry /OUT:"$(NAME).exe" /STACK:0x100000,0x100000
all: main.c
$(CC) $(FLAGS) main.c && Crinkler $(LINK_FLAGS)
clean:
del $(NAME).exe
to build you will need to also download Crinkler https://github.com/runestubbe/Crinkler
here is what it looks like: enter image description here
1 Answer 1
First, note that significant effort toward hyper-optimizing a program for smallest executable size is effectively the domain of Code Golf; Code Review concerns itself with well-structured, reliable code that follows best practices. If the former gets in the way of the latter, you will tend to find that recommendations on this site bend toward "good code" rather than "small code".
To that end:
Redeclaration
extern int _fltused;
int _fltused;
Why? You haven't marked it static
so it should be exported already. Declaring it extern
would be more useful in a header file that a different translation unit includes, which is not what's happening here; so just int _fltused = 0;
.
Lookup tables
Virtual key codes fit in one byte. Consider replacing your switch (wParam)
with a lookup into a 256-byte-long correspondence table where the index is the virtual key code and the value is your enum Keys
code.
Better yet, make keys
itself 256 bytes long, do away with your enum
altogether, and assume that indexes map to virtual key codes. Be sure to range-check any incoming VKs just to be sure that an out-of-range dereference is not possible.
Strange modifier declaration order
Whereas bool const
is equivalent to const bool
, the latter is much more common.
Header files
The conventional thing to do for load_extensions
is put it in a C file. That will be better-structured and should not affect the compilation size given a correctly-configured compiler and linker.