diff --git a/cmake/BinHelper.cmake b/cmake/BinHelper.cmake new file mode 100644 index 0000000..8825c40 --- /dev/null +++ b/cmake/BinHelper.cmake @@ -0,0 +1,29 @@ +include(CMakeParseArguments) # 3.4 and lower compatibility +find_package(Python REQUIRED COMPONENTS Interpreter) + +function (bin2h_compile) + set(oneValueArgs OUTPUT) + set(multiValueArgs BIN TXT) + cmake_parse_arguments(ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + find_file(BIN2H_EXECUTABLE bin2h.py PATHS ${CMAKE_SOURCE_DIR}/tools) + set(DEPENDS) + set(COMMAND ${BIN2H_EXECUTABLE}) + + foreach (BIN ${ARGS_BIN}) + set(SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/${BIN}) + list(APPEND DEPENDS ${SOURCE}) + list(APPEND COMMAND "-b" "${SOURCE}") + endforeach() + + foreach (TXT ${ARGS_TXT}) + set(SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/${TXT}) + list(APPEND DEPENDS ${SOURCE}) + list(APPEND COMMAND "-t" "${SOURCE}") + endforeach() + + list(APPEND COMMAND ${ARGS_OUTPUT}) + add_custom_command(COMMAND Python::Interpreter ARGS ${COMMAND} + DEPENDS Python::Interpreter ${BIN2H_EXECUTABLE} ${DEPENDS} + OUTPUT ${ARGS_OUTPUT}) +endfunction() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b18f7da..cd73b0c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,9 +12,14 @@ set(SOURCES if (USE_OPENGL) include(GL3WHelper) add_gl3w(gl3w) + include(BinHelper) + bin2h_compile(OUTPUT glslShaders.h TXT vert.glsl frag.glsl) + list(APPEND SOURCES ${CMAKE_CURRENT_BINARY_DIR}/glslShaders.h) endif() add_executable(${TARGET} ${SOURCES}) +target_include_directories(${TARGET} PRIVATE + $<$:${CMAKE_CURRENT_BINARY_DIR}>) target_link_libraries(${TARGET} $<$:SDL2::SDL2main> SDL2::SDL2 diff --git a/src/draw_opengl_core.c b/src/draw_opengl_core.c index f96af58..e1cbb10 100644 --- a/src/draw_opengl_core.c +++ b/src/draw_opengl_core.c @@ -1,20 +1,47 @@ #include "draw.h" +#include "glslShaders.h" #include "maths.h" #include #include #include +#include + +typedef struct { float x, y; } vertex; + +enum { ATTRIB_VERTPOS, NUM_ATTRIBS }; +static const char* const attribNames[] = +{ + [ATTRIB_VERTPOS] = "inPos", +// [ATTRIB_COLOUR] = "inColour" +}; + #define OPENGL_VERSION_MAJOR 3 #define OPENGL_VERSION_MINOR 3 static SDL_GLContext* ctx = NULL; static SDL_Window* window = NULL; -static uint32_t colour = 0x00000000; -static uint32_t clrColour = 0x00000000; -static bool antialias = false; +static uint32_t + colour = 0x00000000, + drawColour = 0x00000000, + clrColour = 0x00000000; +static bool antialias = false; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-function" +#define DRAWLIST_MAX_SIZE 480 +static vertex drawListVerts[DRAWLIST_MAX_SIZE]; +static uint16_t drawListIndices[DRAWLIST_MAX_SIZE]; +static uint16_t drawListCount = 0, drawListVertNum = 0; +static GLuint vao = 0, drawListVbo = 0, drawListIbo = 0; + +static GLuint program = 0; +static GLint uView, uColour; + + +#if DRAWLIST_MAX_SIZE < 2 || DRAWLIST_MAX_SIZE >= UINT16_MAX + #error DRAWLIST_MAX_SIZE must be larger than 1 and smaller than 65535 +#endif + +#if !defined NDEBUG && !defined __APPLE__ static void GlErrorCb( GLenum source, GLenum type, @@ -27,18 +54,81 @@ static void GlErrorCb( if (severity != GL_DEBUG_SEVERITY_NOTIFICATION) printf("%s\n", message); } -#pragma clang diagnostic pop +#endif void DrawWindowHints(void) { - SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); // Enable MSAA - SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 8); // 8x MSAA + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); // Enable MSAA + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 16); // 16x MSAA - // Legacy OpenGL profile + // Modern OpenGL profile SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, OPENGL_VERSION_MAJOR); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, OPENGL_VERSION_MINOR); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + + SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); +} + +static inline GLuint CompilerShader(const char* src, GLenum type) +{ + GLuint shader = glCreateShader(type); + + glShaderSource(shader, 1, &src, NULL); + glCompileShader(shader); + + int res, logLen; + glGetShaderiv(shader, GL_COMPILE_STATUS, &res); + if (res != GL_TRUE) + { + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLen); + if (logLen > 0) + { + char* log = malloc(logLen); + glGetShaderInfoLog(shader, logLen, NULL, log); + fprintf(stderr, "Shader type %#06X compilation failed:\n%s\n", type, log); + free(log); + } + glDeleteShader(shader); + return 0; + } + + return shader; +} + +static inline GLuint LinkProgram( + GLuint vertShader, GLuint fragShader, + const char* const attrNames[], GLuint attrCount) +{ + GLuint progId = glCreateProgram(); + + // Bind attributes + for (GLuint i = 0; i < attrCount; ++i) + glBindAttribLocation(progId, i, (const GLchar*)attrNames[i]); + + // Attach shaders & link program + glAttachShader(progId, vertShader); + glAttachShader(progId, fragShader); + glLinkProgram(progId); + + int res, logLen; + glGetProgramiv(progId, GL_LINK_STATUS, &res); + if (res != GL_TRUE) + { + glGetProgramiv(progId, GL_INFO_LOG_LENGTH, &logLen); + if (logLen > 0) + { + char* log = malloc(logLen); + glGetProgramInfoLog(progId, logLen, NULL, log); + fprintf(stderr, "Program link failed:\n%s\n", log); + free(log); + } + glDeleteProgram(progId); + return 0; + } + + return progId; } int InitDraw(SDL_Window* _window) @@ -51,6 +141,14 @@ int InitDraw(SDL_Window* _window) return -1; } + SDL_GL_SetSwapInterval(1); // Enable vsync + + // Detect if MSAA is available & active + int res; + if (SDL_GL_GetAttribute(SDL_GL_MULTISAMPLEBUFFERS, &res) == 0 && res == 1) + if (SDL_GL_GetAttribute(SDL_GL_MULTISAMPLESAMPLES, &res) == 0 && res > 0) + antialias = true; + // Load Core profile extensions if (gl3wInit() != GL3W_OK) { @@ -65,23 +163,98 @@ int InitDraw(SDL_Window* _window) // Set debug callback #if !defined NDEBUG && !defined __APPLE__ - glDebugMessageCallback(GlErrorCb, nullptr); + glDebugMessageCallback(GlErrorCb, NULL); glEnable(GL_DEBUG_OUTPUT); #endif - SDL_GL_SetSwapInterval(1); // Enable vsync + // Ensure culling & depth testing are off + glDisable(GL_CULL_FACE); + glDisable(GL_DEPTH_TEST); + //glEnable(GL_BLEND); // These will be relevant if proper anti aliased line drawing is implemented + //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + //glBlendEquation(GL_FUNC_ADD); - // Detect if MSAA is available & active - int res; - if (SDL_GL_GetAttribute(SDL_GL_MULTISAMPLEBUFFERS, &res) == 0 && res == 1) - if (SDL_GL_GetAttribute(SDL_GL_MULTISAMPLESAMPLES, &res) == 0 && res > 0) - antialias = true; + // Compile shaders + GLuint vert = CompilerShader(vert_glsl, GL_VERTEX_SHADER); + if (!vert) + return -1; + GLuint frag = CompilerShader(frag_glsl, GL_FRAGMENT_SHADER); + if (!frag) + { + glDeleteShader(vert); + return -1; + } + + // Link program + program = LinkProgram(vert, frag, attribNames, NUM_ATTRIBS); + glDeleteShader(frag); + glDeleteShader(vert); + if (!program) + return -1; + + // Get uniforms + uView = glGetUniformLocation(program, "uView"); + uColour = glGetUniformLocation(program, "uColour"); + + glUseProgram(program); // Use program + + // Create & bind vertex attribute array + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + // Setup draw list + glGenBuffers(1, &drawListVbo); + glBindBuffer(GL_ARRAY_BUFFER, drawListVbo); + glGenBuffers(1, &drawListIbo); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, drawListIbo); + + glBufferData(GL_ARRAY_BUFFER, DRAWLIST_MAX_SIZE * sizeof(vertex), NULL, GL_DYNAMIC_DRAW); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, DRAWLIST_MAX_SIZE * sizeof(uint16_t), NULL, GL_DYNAMIC_DRAW); + + // Setup draw list vertex attributes + glEnableVertexAttribArray(ATTRIB_VERTPOS); + glVertexAttribPointer(ATTRIB_VERTPOS, 2, GL_FLOAT, GL_FALSE, sizeof(vertex), (GLvoid*)0); + + // Reset viewport & clear + SetDrawViewport(GetDrawSizeInPixels()); + glUniform4f(uColour, 1.0f, 1.0f, 1.0f, 1.0f); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); return 0; } void QuitDraw(void) { + if (drawListVbo) + { + glBindBuffer(GL_ARRAY_BUFFER, 0); + glDeleteBuffers(1, &drawListVbo); + drawListVbo = 0; + } + + if (drawListIbo) + { + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glDeleteBuffers(1, &drawListIbo); + drawListIbo = 0; + } + + if (vao) + { + glDisableVertexAttribArray(ATTRIB_VERTPOS); + glBindVertexArray(0); + glDeleteVertexArrays(1, &vao); + vao = 0; + } + + if (program) + { + glUseProgram(0); + glDeleteProgram(program); + program = 0; + } + SDL_GL_MakeCurrent(window, NULL); SDL_GL_DeleteContext(ctx); ctx = NULL; @@ -99,6 +272,49 @@ size GetDrawSizeInPixels(void) void SetDrawViewport(size size) { glViewport(0, 0, size.w, size.h); + vertex s = (vertex){2.0f / (float)size.w, 2.0f / (float)size.h}; + float mat[16] = { + s.x, 0.0f, 0.0f, 0.0f, + 0.0f, -s.y, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 0.0f, + -1.0f, 1.0f, 0.0f, 1.0f}; + glUniformMatrix4fv(uView, 1, GL_FALSE, mat); +} + + +static int drawCount = 0; +static void FlushDrawBuffers() +{ + if (!drawListCount) + return; + + GLsizeiptr vboSize = drawListVertNum * (GLsizeiptr)sizeof(vertex); + GLsizeiptr iboSize = drawListCount * (GLsizeiptr)sizeof(uint16_t); + glBufferSubData(GL_ARRAY_BUFFER, (GLintptr)0, vboSize, drawListVerts); + glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, (GLintptr)0, iboSize, drawListIndices); + + glDrawElements(GL_LINES, drawListCount, GL_UNSIGNED_SHORT, (GLvoid*)0); + + drawListVertNum = 0; + drawListCount = 0; + ++drawCount; +} + +static void UnpackColour(GLfloat* out) +{ + const float mul = 1.0f / 255.0f; + out[0] = (GLfloat)((colour & 0xFF000000) >> 24) * mul; + out[1] = (GLfloat)((colour & 0x00FF0000) >> 16) * mul; + out[2] = (GLfloat)((colour & 0x0000FF00) >> 8) * mul; + out[3] = (GLfloat)((colour & 0x000000FF)) * mul; +} + +static void UpdateDrawColour() +{ + FlushDrawBuffers(); // Flush anything that might still be in the buffer + GLfloat draw[4]; UnpackColour(draw); // Convert packed RGBA32 to float + glUniform4fv(uColour, 1, draw); // Pass new colour to shader + drawColour = colour; // Mark state as up to date } @@ -111,12 +327,8 @@ void DrawClear(void) { if (clrColour != colour) { - const float mul = 1.0f / 255.0f; - glClearColor( - (GLclampf)((colour & 0xFF000000) >> 24) * mul, - (GLclampf)((colour & 0x00FF0000) >> 16) * mul, - (GLclampf)((colour & 0x0000FF00) >> 8) * mul, - (GLclampf)((colour & 0x000000FF)) * mul); + GLfloat clear[4]; UnpackColour(clear); + glClearColor(clear[0], clear[1], clear[2], clear[3]); clrColour = colour; } glClear(GL_COLOR_BUFFER_BIT); @@ -124,29 +336,162 @@ void DrawClear(void) void DrawPoint(int x, int y) { - + DrawCircleSteps(x, y, 1, 4); } void DrawRect(int x, int y, int w, int h) { +#if 8 <= DRAWLIST_MAX_SIZE + if (drawColour != colour) + UpdateDrawColour(); + else if (drawListCount > DRAWLIST_MAX_SIZE - 8) + FlushDrawBuffers(); + uint16_t base = drawListVertNum; + drawListVerts[drawListVertNum++] = (vertex){(float)x, (float)y}; + drawListVerts[drawListVertNum++] = (vertex){(float)x + (float)w, (float)y}; + drawListVerts[drawListVertNum++] = (vertex){(float)x + (float)w, (float)y + (float)h}; + drawListVerts[drawListVertNum++] = (vertex){(float)x, (float)y + (float)h}; + drawListIndices[drawListCount++] = base; + drawListIndices[drawListCount++] = base + 1; + drawListIndices[drawListCount++] = base + 1; + drawListIndices[drawListCount++] = base + 2; + drawListIndices[drawListCount++] = base + 2; + drawListIndices[drawListCount++] = base + 3; + drawListIndices[drawListCount++] = base + 3; + drawListIndices[drawListCount++] = base; + + if (drawListCount == DRAWLIST_MAX_SIZE) + FlushDrawBuffers(); +#else + #pragma message("DRAWLIST_MAX_SIZE is too low, DrawRect functionality disabled") +#endif } void DrawLine(int x1, int y1, int x2, int y2) { + if (drawColour != colour) + UpdateDrawColour(); + else if (drawListCount > DRAWLIST_MAX_SIZE - 2) + FlushDrawBuffers(); + vertex from = {x1, y1}, to = {x2, y2}; + if (drawListVertNum > 0 && memcmp(&from, &drawListVerts[drawListVertNum - 1], sizeof(vertex)) == 0) + { + // Reuse last vertex + drawListIndices[drawListCount++] = drawListVertNum - 1; + } + else + { + drawListVerts[drawListVertNum] = from; + drawListIndices[drawListCount++] = drawListVertNum++; + } + drawListVerts[drawListVertNum] = to; + drawListIndices[drawListCount++] = drawListVertNum++; } + +static void ArcSubSlice( + float x, float y, + float magx, float magy, + float angle, float stride, + int num) +{ + if (!num) + return; + drawListVerts[drawListVertNum] = (vertex){ + x + cosf(angle) * magx, + y - sinf(angle) * magy}; + for (int i = 1; i <= num; ++i) + { + const float theta = angle + stride * (float)i; + float ofsx = cosf(theta) * magx; + float ofsy = sinf(theta) * magy; + + drawListIndices[drawListCount++] = drawListVertNum++; + drawListVerts[drawListVertNum] = (vertex){x + ofsx, y - ofsy}; + drawListIndices[drawListCount++] = drawListVertNum; + } + drawListVertNum++; +} + +static void ArcSlice( + float x, float y, + float magx, float magy, + float angle, float stride, + int steps) +{ + int i = 0; + while (true) + { + const int subSteps = (MIN(drawListCount + (steps - i) * 2, DRAWLIST_MAX_SIZE) - drawListCount) / 2; + if (subSteps) + ArcSubSlice(x, y, magx, magy, angle, stride, subSteps); + i += subSteps; + if (i < steps) + FlushDrawBuffers(); + else + break; + angle += stride * (float)subSteps; + } +} + void DrawCircleSteps(int x, int y, int r, int steps) { + if (drawColour != colour) + UpdateDrawColour(); + // Circles look better when offset negatively by half a pixel w/o MSAA + const float fx = (antialias ? (float)x : (float)x - 0.5f); + const float fy = (antialias ? (float)y : (float)y - 0.5f); + + const float stepSz = (float)TAU / (float)abs(steps); + const float mag = (float)r; + // Check if whole circle can fit in the buffer + if (drawListCount > DRAWLIST_MAX_SIZE - steps * 2) + { + // Draw circle as segmented arcs + ArcSlice(fx, fy, mag, mag, 0.0f, stepSz, steps); + } + else + { + // Draw whole circle in a single loop + uint16_t base = drawListVertNum; + drawListVerts[drawListVertNum] = (vertex){fx + mag, fy}; + drawListIndices[drawListCount++] = drawListVertNum++; + for (int i = 1; i < steps; ++i) + { + const float theta = stepSz * (float)i; + float ofsx = cosf(theta) * mag; + float ofsy = sinf(theta) * mag; + + drawListVerts[drawListVertNum] = (vertex){fx + ofsx, fy + ofsy}; + drawListIndices[drawListCount++] = drawListVertNum; + drawListIndices[drawListCount++] = drawListVertNum++; + } + drawListIndices[drawListCount++] = base; + } } void DrawArcSteps(int x, int y, int r, int startAng, int endAng, int steps) { + if (drawColour != colour) + UpdateDrawColour(); + // Arcs look better when offset negatively by half a pixel w/o MSAA + const float fx = (antialias ? (float)x : (float)x - 0.5f); + const float fy = (antialias ? (float)y : (float)y - 0.5f); + const float mag = (float)r; + const float fstart = (float)startAng * (float)DEG2RAD; + const float fstepSz = (float)(endAng - startAng) / (float)abs(steps) * (float)DEG2RAD; + ArcSlice(fx, fy, mag, mag, fstart, fstepSz, steps); } void DrawPresent(void) { + FlushDrawBuffers(); SDL_GL_SwapWindow(window); +#ifndef NDEBUG + fprintf(stderr, "%d draw call(s)\n", drawCount); +#endif + drawCount = 0; } diff --git a/src/frag.glsl b/src/frag.glsl new file mode 100644 index 0000000..f9b3027 --- /dev/null +++ b/src/frag.glsl @@ -0,0 +1,10 @@ +#version 330 core + +out vec4 outColour; + +uniform vec4 uColour; + +void main() +{ + outColour = uColour; +} diff --git a/src/vert.glsl b/src/vert.glsl new file mode 100644 index 0000000..f32c019 --- /dev/null +++ b/src/vert.glsl @@ -0,0 +1,10 @@ +#version 330 core + +in vec2 inPos; + +uniform mat4 uView; + +void main() +{ + gl_Position = uView * vec4(inPos, 0.0, 1.0); +} diff --git a/tools/bin2h.py b/tools/bin2h.py new file mode 100644 index 0000000..d79546a --- /dev/null +++ b/tools/bin2h.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +import sys +from pathlib import Path +from typing import BinaryIO, TextIO +import re + + +label_invalid = re.compile(r"\W") # Turn invalid label chars into underscores +label_lstrip = re.compile(r"^[\d_]+") # Trim all underscores or numbers from the beginning + + +def sanitise_label(label: str) -> str: + tmp = label_invalid.sub("_", label, re.I) + return label_lstrip.sub("", tmp, re.I) + + +def bin2h(name: str, binf: BinaryIO, h: TextIO): + raise NotImplementedError + + +def txt2h(name: str, txt: TextIO, h: TextIO): + h.write(f"static const char* const {sanitise_label(name)} = \n") + h.write("\"") + + remap = { + "\a": "\\a", "\b": "\\b", "\f": "\\f", "\n": "\\n", "\r": "\\r", + "\t": "\\t", "\v": "\\v", "\\": "\\\\", "\"": "\\\""} + esc_numeric = False + for idx, c in enumerate(txt.read()): + if m := remap.get(c): + h.write(m) + else: + if (i := ord(c)) < 0x7F: + if c.isprintable() and not (esc_numeric and c.isnumeric()): + h.write(c) + else: + if i < 8: + h.write(f"\\{i:o}") + else: + h.write(f"\\x{i:02X}") + esc_numeric = True + continue + elif i < 0x10000: + h.write(f"\\u{i:04X}") + else: + h.write(f"\\U{i:08X}") + esc_numeric = False + + h.write("\";\n") + + +def main(): + usage = "Usage [-b data.bin] [-t text.txt] " + + binfiles = [] + txtfiles = [] + out = None + opt = None + for arg in sys.argv[1:]: + if opt is not None: + if opt == "b": + binfiles.append(arg) + elif opt == "t": + txtfiles.append(arg) + else: + sys.exit(usage) + opt = None + if arg.startswith("-"): + opt = arg[1:] + else: + out = Path(arg) + + if opt is not None or out is None: + sys.exit(usage) + if len(binfiles) == 0 and len(txtfiles) == 0: + sys.exit(usage) + + with out.open("w") as h: + guard = f"BIN2H_{sanitise_label(out.name).upper()}" + if not guard.endswith("_H"): + guard += "_H" + h.writelines([ + "/*DO NOT EDIT*/\n", + "// Autogenerated by bin2h\n", + "\n", + f"#ifndef {guard}\n", + f"#define {guard}\n", + "\n"]) + + # Write binaries + for i in binfiles: + path = Path(i) + with path.open("rb") as file: + bin2h(path.name, file, h) + + # Write texts + for i in txtfiles: + path = Path(i) + with path.open("r") as file: + txt2h(path.name, file, h) + + h.writelines([ + "\n", + f"#endif//{guard}\n"]) + + +if __name__ == "__main__": + main()