#include #include #include #include #include #include #include #include "util.h" #include "font.h" #include "ren.h" #define GLERR(x) \ { \ int a = glGetError(); \ if (a != GL_NO_ERROR) \ printf("(%s:%d %s:%s) glError: 0x%x %s\n", \ __FILE__, __LINE__, __func__, x, a, glerr[a&0xff]); \ } #define GL(f) f; GLERR(#f) #define REN_RET(a,b) {ren_errno = b; return a;} enum REN_ERR { REN_SUCCESS = 0, REN_ERRNO, REN_INVAL, REN_VERTEX, REN_FRAGMENT, REN_PROGRAM, REN_COMPILE, REN_LINK, REN_TEXTURE, REN_CONTEXT, REN_GLEW, REN_FONT, REN_BUFFER, REN_UNIFORM, }; // TODO: make a macro for enum-associated string arrays const char * ren_err_msg[] = { [REN_SUCCESS] = "Success", [REN_ERRNO] = "Look at errno", [REN_INVAL] = "Invalid or NULL arguments", [REN_VERTEX] = "Failed to create opengl vertex shader", [REN_FRAGMENT] = "Failed to create opengl fragment shader", [REN_PROGRAM] = "Failed to create opengl program", [REN_COMPILE] = "Failed to compile shaders", [REN_LINK] = "Failed to link shaders", [REN_TEXTURE] = "Failed to create texture", [REN_CONTEXT] = "Failed to create SDL OpenGL context", [REN_GLEW] = "GLEW Error", [REN_FONT] = "Font Error", [REN_BUFFER] = "Failed to create opengl buffer", [REN_UNIFORM] = "Failed to get uniform location", }; #define ELEM(x) [x&0xff] = #x, const char *glerr[] = { ELEM(GL_INVALID_ENUM) ELEM(GL_INVALID_VALUE) ELEM(GL_INVALID_OPERATION) ELEM(GL_OUT_OF_MEMORY) }; #undef ELEM // different stacks #include "generic_stack.h" STACK_DECL(vtstack, struct v_text) STACK_DECL(vcstack, struct v_col) struct ren_font { struct font_atlas *font; GLuint texture; }; struct { SDL_GLContext *gl; struct ren_font *fonts; int fonts_no; GLuint font_prog; GLuint box_prog; GLuint font_buffer; GLuint box_buffer; GLint viewsize_loc; GLint box_viewsize_loc; GLint texturesize_loc; struct vtstack font_stack; struct vcstack box_stack; int width, height; int s_x, s_y, s_w, s_h; int tabsize; } ren = {0}; static int ren_errno; // print shader compilation errors static int shader_compile_error(GLuint shader, const char *path) { GLint status; GL(glGetShaderiv(shader, GL_COMPILE_STATUS, &status)) if (status != GL_FALSE) return 0; GLint log_length; GL(glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length)) GLchar *log_str = emalloc((log_length + 1)*sizeof(GLchar)); GL(glGetShaderInfoLog(shader, log_length, NULL, log_str)) const char *shader_type_str = NULL; GLint shader_type; GL(glGetShaderiv(shader, GL_SHADER_TYPE, &shader_type)) switch(shader_type) { case GL_VERTEX_SHADER: shader_type_str = "vertex"; break; case GL_GEOMETRY_SHADER: shader_type_str = "geometry"; break; case GL_FRAGMENT_SHADER: shader_type_str = "fragment"; break; } fprintf(stderr, "Compile failure in %s shader %s:\n%s\n", shader_type_str, path, log_str); efree(log_str); return -1; } // print shader link errors static int shader_link_error(GLuint prog) { GLint status; GL(glGetProgramiv(prog, GL_LINK_STATUS, &status)) if (status != GL_FALSE) return 0; GLint log_length; GL(glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &log_length)) GLchar *log_str = emalloc((log_length + 1)*sizeof(GLchar)); GL(glGetProgramInfoLog(prog, log_length, NULL, log_str)) fprintf(stderr, "Linker failure: %s\n", log_str); efree(log_str); return -1; } static GLuint shader_compile(const char *path, const char *shader, GLuint type) { GLuint s; // initialize the vertex shader and get the corresponding id s = GL(glCreateShader(type)) if (!s) REN_RET(0, REN_VERTEX) // get the shader into opengl GL(glShaderSource(s, 1, &shader, NULL)) GL(glCompileShader(s)) if (shader_compile_error(s, path)) REN_RET(0, REN_COMPILE) return s; } const char * ren_strerror(void) { return ren_err_msg[ren_errno % (sizeof(ren_err_msg)/sizeof(char *))]; } // compile a vertex shader (vs_path) and a fragment shader (fs_path) into an opengl // program and return it's index static GLuint ren_compile_program(const char *vs_path, const char *fs_path) { GLuint gl_vertshader, gl_fragshader, prog; if (!vs_path || !fs_path) REN_RET(0, REN_INVAL) char *str = NULL; dump_file(vs_path, &str, NULL); if (!str) REN_RET(0, REN_ERRNO) gl_vertshader = shader_compile(vs_path, str, GL_VERTEX_SHADER); efree(str); if (!gl_vertshader) return 0; dump_file(fs_path, &str, NULL); if (!str) REN_RET(0, REN_ERRNO) gl_fragshader = shader_compile(fs_path, str, GL_FRAGMENT_SHADER); efree(str); if (!gl_fragshader) return 0; // create the main program object, it is an amalgamation of all shaders prog = GL(glCreateProgram()) if (!prog) REN_RET(0, REN_PROGRAM) // attach the shaders to the program (set which shaders are present) GL(glAttachShader(prog, gl_vertshader)) GL(glAttachShader(prog, gl_fragshader)) // then link the program (basically the linking stage of the program) GL(glLinkProgram(prog)) if (shader_link_error(prog)) REN_RET(0, REN_LINK) // after linking the shaders can be detached and the source freed from // memory since the program is ready to use GL(glDetachShader(prog, gl_vertshader)) GL(glDetachShader(prog, gl_fragshader)) return prog; } static void set_texture_parameters(GLuint type, GLuint wrap_s, GLuint wrap_t, GLuint upscale, GLuint downscale) { GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap_s)) GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap_t)) GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, upscale)) GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, downscale)) } static GLuint ren_texturergb_2d(const char *buf, int w, int h, int upscale, int downscale) { GLuint t; if (!buf || w <= 0 || h <= 0) REN_RET(0, REN_INVAL) GL(glGenTextures(1, &t)) if (!t) REN_RET(0, REN_TEXTURE) GL(glBindTexture(GL_TEXTURE_2D, t)) GL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, buf)) set_texture_parameters(GL_TEXTURE_2D, GL_REPEAT, GL_REPEAT, upscale, downscale); return t; } static GLuint ren_texturergba_2d(const char *buf, int w, int h, int upscale, int downscale) { GLuint t; if (!buf || w <= 0 || h <= 0) REN_RET(0, REN_INVAL) GL(glGenTextures(1, &t)) if (!t) REN_RET(0, REN_TEXTURE) GL(glBindTexture(GL_TEXTURE_2D, t)) GL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, buf)) set_texture_parameters(GL_TEXTURE_2D, GL_REPEAT, GL_REPEAT, upscale, downscale); return t; } static GLuint ren_texturer_2d(const char *buf, int w, int h, int upscale, int downscale) { GLuint t; if (!buf || w <= 0 || h <= 0) REN_RET(0, REN_INVAL) GL(glGenTextures(1, &t)) if (!t) REN_RET(0, REN_TEXTURE) GL(glBindTexture(GL_TEXTURE_2D, t)) GL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, buf)) set_texture_parameters(GL_TEXTURE_2D, GL_REPEAT, GL_REPEAT, upscale, downscale); return t; } static GLuint ren_texturer_rect(const char *buf, int w, int h, int upscale, int downscale) { GLuint t; if (!buf || w <= 0 || h <= 0) REN_RET(0, REN_INVAL) GL(glGenTextures(1, &t)) if (!t) REN_RET(0, REN_TEXTURE) GL(glBindTexture(GL_TEXTURE_RECTANGLE, t)) GL(glTexImage2D(GL_TEXTURE_RECTANGLE, 0, GL_RED, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, buf)) // a limitation of recatngle textures is that the wrapping mode is limited // to either clamp-to-edge or clamp-to-border set_texture_parameters(GL_TEXTURE_RECTANGLE, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, upscale, downscale); return t; } // FIXME: update only the newly generated character instead of the whole texture static int update_font_texture(int idx) { GL(glUseProgram(ren.font_prog)) GL(glTexSubImage2D( GL_TEXTURE_RECTANGLE, 0, 0, 0, ren.fonts[idx].font->width, ren.fonts[idx].font->height, GL_RED, GL_UNSIGNED_BYTE, ren.fonts[idx].font->atlas)) //font_dump(ren.fonts[idx].font, "./atlas.png"); GL(glUseProgram(0)); return 0; } // loads a font and returns it's index in the fonts array static int ren_load_font(int size, const char *path) { int idx = ren.fonts_no; struct font_atlas *f; ren.fonts = erealloc(ren.fonts, sizeof(struct ren_font)*(idx+1)); ren.fonts[idx].font = font_init(); f = ren.fonts[idx].font; if (!f) REN_RET(-1, REN_FONT) if (!path) path = DEFAULT_FONT; if (font_load(f, path, size)) REN_RET(-1, REN_FONT) //font_dump(f, "./atlas.png"); // load font texture (atlas) ren.fonts[idx].texture = ren_texturer_rect( (const char *)f->atlas, f->width, f->height, GL_LINEAR, GL_LINEAR); if (!ren.fonts[idx].texture) return -1; ren.fonts_no = idx+1; return idx; } // returns the index to the ren.fonts array to the font with the correct size // return -1 on errror static int ren_get_font(int size) { for (int i = 0; i < ren.fonts_no; i++) { if (ren.fonts[i].font->size == size) return i; } // TODO: add a way to change font family return ren_load_font(size, NULL); } int ren_init(SDL_Window *w) { // Initialize OpenGL if (!w) REN_RET(-1, REN_INVAL) // using version 3 does not allow to use glVertexAttribPointer() without // vertex buffer objects, so use compatibility mode SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); ren.gl = SDL_GL_CreateContext(w); if (!ren.gl) { printf("SDL: %s\n", SDL_GetError()); REN_RET(-1, REN_CONTEXT) } GL(glEnable(GL_BLEND)) GL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)) GL(glDisable(GL_CULL_FACE)) GL(glDisable(GL_DEPTH_TEST)) GL(glEnable(GL_SCISSOR_TEST)) GL(glEnable(GL_TEXTURE_2D)) GL(glEnable(GL_TEXTURE_RECTANGLE)) GLenum glew_err = glewInit(); if (glew_err != GLEW_OK) REN_RET(glew_err, REN_GLEW); // Create stacks ren.font_stack = vtstack_init(); ren.box_stack = vcstack_init(); // generate the font buffer object GL(glGenBuffers(1, &ren.font_buffer)) if (!ren.font_buffer) REN_RET(-1, REN_BUFFER) GL(glGenBuffers(1, &ren.box_buffer)) if (!ren.box_buffer) REN_RET(-1, REN_BUFFER) // Compile font shaders ren.font_prog = ren_compile_program(FONT_VERSHADER, FONT_FRAGSHADER); if (!ren.font_prog) return -1; // create the uniforms, if the returned value is -1 then the uniform may have // been optimized away, in any case do not return, just give a warning ren.viewsize_loc = GL(glGetUniformLocation(ren.font_prog, "viewsize")) if (ren.viewsize_loc == -1) printf("uniform %s was optimized away\n", "viewsize"); ren.texturesize_loc = GL(glGetUniformLocation(ren.font_prog, "texturesize")) if (ren.texturesize_loc == -1) printf("uniform %s was optimized away\n", "texturesize"); // Compile box shaders ren.box_prog = ren_compile_program(BOX_VERSHADER, BOX_FRAGSHADER); if (!ren.box_prog) return -1; ren.box_viewsize_loc = GL(glGetUniformLocation(ren.box_prog, "viewsize")) if (ren.box_viewsize_loc == -1) printf("uniform %s was optimized away\n", "viewsize"); // Finishing touches ren.tabsize = REN_TABSIZE; int width, height; SDL_GetWindowSize(w, &width, &height); ren_update_viewport(width, height); GL(glClearColor(0.3f, 0.3f, 0.3f, 0.f)) GL(glClear(GL_COLOR_BUFFER_BIT)) return 0; } int ren_clear(void) { GL(glScissor(0, 0, ren.width, ren.height)) GL(glClear(GL_COLOR_BUFFER_BIT)); return 0; } // idx refers to the fonts array index, this means that when drawing the font stack // only one font size at the time can be used static int ren_draw_font_stack(int idx) { struct font_atlas *font = ren.fonts[idx].font; GLuint font_texture = ren.fonts[idx].texture; GL(glUseProgram(ren.font_prog)) GL(glBindTexture(GL_TEXTURE_RECTANGLE, font_texture)) GL(glViewport(0, 0, ren.width, ren.height)) // this has caused me some trouble, convert from image coordiates to viewport GL(glScissor(ren.s_x, ren.height-ren.s_y-ren.s_h, ren.s_w, ren.s_h)) GL(glUniform2i(ren.viewsize_loc, ren.width, ren.height)) GL(glUniform2i(ren.texturesize_loc, font->width, font->height)) GL(glBindBuffer(GL_ARRAY_BUFFER, ren.font_buffer)) if (vtstack_changed(&ren.font_stack)) { if (vtstack_size_changed(&ren.font_stack)) { GL(glBufferData( GL_ARRAY_BUFFER, ren.font_stack.idx*sizeof(struct v_text), ren.font_stack.items, GL_DYNAMIC_DRAW)) } else { GL(glBufferSubData( GL_ARRAY_BUFFER, 0, ren.font_stack.idx*sizeof(struct v_text), ren.font_stack.items)) } } // when passing ints to glVertexAttribPointer they are automatically // converted to floats GL(glVertexAttribPointer( REN_VERTEX_IDX, 2, GL_INT, GL_FALSE, sizeof(struct v_text), 0)) GL(glVertexAttribPointer( REN_UV_IDX, 2, GL_INT, GL_FALSE, sizeof(struct v_text), (void*)sizeof(vec2_i))) GL(glEnableVertexAttribArray(REN_VERTEX_IDX)) GL(glEnableVertexAttribArray(REN_UV_IDX)) GL(glDrawArrays(GL_TRIANGLES, 0, ren.font_stack.idx)) GL(glDisableVertexAttribArray(REN_VERTEX_IDX)) GL(glDisableVertexAttribArray(REN_UV_IDX)) GL(glBindBuffer(GL_ARRAY_BUFFER, 0)) GL(glBindTexture(GL_TEXTURE_RECTANGLE, 0)) GL(glUseProgram(0)) vtstack_clear(&ren.font_stack); return 0; } static int ren_draw_box_stack(void) { GL(glUseProgram(ren.box_prog)) GL(glViewport(0, 0, ren.width, ren.height)) GL(glScissor(0, 0, ren.width, ren.height)) GL(glUniform2i(ren.box_viewsize_loc, ren.width, ren.height)) GL(glBindBuffer(GL_ARRAY_BUFFER, ren.box_buffer)) if(vcstack_changed(&ren.box_stack)) { if (vcstack_size_changed(&ren.box_stack)) { GL(glBufferData( GL_ARRAY_BUFFER, ren.box_stack.idx*sizeof(struct v_col), ren.box_stack.items, GL_DYNAMIC_DRAW)) } else { GL(glBufferSubData( GL_ARRAY_BUFFER, 0, ren.box_stack.idx*sizeof(struct v_col), ren.box_stack.items)) } } // when passing ints to glVertexAttribPointer they are automatically // converted to floats GL(glVertexAttribPointer( REN_VERTEX_IDX, 2, GL_INT, GL_FALSE, sizeof(struct v_col), 0)) // the color gets normalized GL(glVertexAttribPointer( REN_COLOR_IDX, 4, GL_INT, GL_TRUE, sizeof(struct v_col), (void*)sizeof(vec2_i))) GL(glEnableVertexAttribArray(REN_VERTEX_IDX)) GL(glEnableVertexAttribArray(REN_COLOR_IDX)) GL(glDrawArrays(GL_TRIANGLES, 0, ren.box_stack.idx)) GL(glDisableVertexAttribArray(REN_VERTEX_IDX)) GL(glDisableVertexAttribArray(REN_COLOR_IDX)) GL(glBindBuffer(GL_ARRAY_BUFFER, 0)) GL(glUseProgram(0)) vcstack_clear(&ren.box_stack); return 0; } int ren_update_viewport(int w, int h) { ren.width = w; ren.height = h; return 0; } int ren_set_scissor(int x, int y, int w, int h) { ren.s_x = x; ren.s_y = y; ren.s_w = w; ren.s_h = h; return 0; } static int ren_push_glyph(const struct font_glyph *g, int gx, int gy) { /* x4,y4 x3,y3 * o-------------+ * |(x,y) /| * | / | * | 2 / | * | / | * | / | * | / 1 | * |/ | * +-------------+ * x1,y1 x2,y2 */ struct v_text v; struct font_glyph c; c = *g; //printf("g: u=%d v=%d w=%d h=%d a=%d x=%d y=%d\n", c.u, c.v, c.w, c.h, c.a, c.x, c.y); //printf("v: x=%d y=%d u=%d v=%d\n", v.pos.x, v.pos.y, v.uv.u, v.uv.v); // x1,y1 v = (struct v_text){ .pos = { .x = gx+c.x, .y = gy+c.y+c.h }, .uv = { .u = c.u, .v = c.v+c.h }, }; vtstack_push(&ren.font_stack, &v); // x2,y2 v = (struct v_text){ .pos = { .x = gx+c.x+c.w, .y = gy+c.y+c.h }, .uv = { .u = c.u+c.w, .v = c.v+c.h }, }; vtstack_push(&ren.font_stack, &v); // x3,y3 v = (struct v_text){ .pos = { .x = gx+c.x+c.w, .y = gy+c.y }, .uv = { .u = c.u+c.w, .v = c.v }, }; vtstack_push(&ren.font_stack, &v); // x1,y1 v = (struct v_text){ .pos = { .x = gx+c.x, .y = gy+c.y+c.h }, .uv = { .u = c.u, .v = c.v+c.h }, }; vtstack_push(&ren.font_stack, &v); // x3,y3 v = (struct v_text){ .pos = { .x = gx+c.x+c.w, .y = gy+c.y }, .uv = { .u = c.u+c.w, .v = c.v }, }; vtstack_push(&ren.font_stack, &v); // x4,y4 v = (struct v_text){ .pos = { .x = gx+c.x, .y = gy+c.y }, .uv = { .u = c.u, .v = c.v }, }; vtstack_push(&ren.font_stack, &v); return 0; } static const struct font_glyph * get_glyph(unsigned int code, int idx) { const struct font_glyph *g; int updated; g = font_get_glyph_texture(ren.fonts[idx].font, code, &updated); if (!g) REN_RET(NULL, REN_FONT); if (updated) { if (update_font_texture(idx)) REN_RET(NULL, REN_TEXTURE); } return g; } // TODO: reduce repeating patterns in ren_get_text_box() and ren_render_text() int ren_get_text_box(const char *str, int *rw, int *rh, int size) { int w = 0, h = 0, x = 0, y = 0; const struct font_glyph *g; size_t off, ret; uint32_t cp; int idx = ren_get_font(size); if (idx < 0) return -1; h = y = ren.fonts[idx].font->glyph_max_h; for (off = 0; (ret = grapheme_decode_utf8(str+off, SIZE_MAX, &cp)) > 0 && cp != 0; off += ret) { if (iscntrl(cp)) goto skip_get; if (!(g = get_glyph(cp, idx))) return -1; x += g->x + g->a; // FIXME: generalize this thing skip_get: switch (cp) { case '\t': { const struct font_glyph *sp = get_glyph(' ', idx); if (!sp) return -1; x += (sp->x + sp->a)*ren.tabsize; } break; case '\r': x = 0; break; case '\n': // TODO: encode and/or store line height y += ren.fonts[idx].font->glyph_max_h; x = 0; break; default: break; } if (x > w) w = x; if (y > h) h = y; } if (rw) *rw = w; if (rh) *rh = h; return 0; } int ren_render_text(const char *str, int x, int y, int w, int h, int size) { const struct font_glyph *g; size_t ret, off; uint32_t cp; int gx = x, gy = y; int idx = ren_get_font(size); if (idx < 0) return -1; for (off = 0; (ret = grapheme_decode_utf8(str+off, SIZE_MAX, &cp)) > 0 && cp != 0; off += ret) { // skip special characters that render a box (not present in font) if (iscntrl(cp)) goto skip_render; if (!(g = get_glyph(cp, idx))) return -1; // only push the glyph if it is inside the bounding box if (gx <= x+w && gy <= y+h) ren_push_glyph(g, gx, gy); // TODO: possible kerning needs to be applied here // TODO: handle other unicode control characters such as the // right-to-left isolate (\u2067) gx += g->x + g->a; skip_render: switch (cp) { case '\t': { const struct font_glyph *sp = get_glyph(' ', idx); if (!sp) return -1; gx += (sp->x + sp->a)*ren.tabsize; } break; case '\r': gx = x; break; case '\n': gy += ren.fonts[idx].font->glyph_max_h; gx = x; break; default: break; } } ren_set_scissor(x, y, w, h); ren_draw_font_stack(idx); return 0; } // re-normalize color from 0-255 to 0-0x7fffffff, technically i'm dividing by 256 here #define RENORM(x) (((unsigned long long)(x)*0x7fffffff)>>8) #define R(x) (x&0xff) #define G(x) ((x>>8)&0xff) #define B(x) ((x>>16)&0xff) #define A(x) ((x>>24)&0xff) static int ren_push_box(int x, int y, int w, int h, unsigned int color) { /* x4,y4 x3,y3 * o-------------+ * |(x,y) /| * | / | * | 2 / | * | / | * | / | * | / 1 | * |/ | * +-------------+ * x1,y1 x2,y2 */ struct v_col v; vec4_i c = { .r = RENORM(R(color)), .g = RENORM(G(color)), .b = RENORM(B(color)), .a = RENORM(A(color)), }; // x1, y1 v = (struct v_col){ .pos = { .x = x, .y = y+h }, .col = c }; vcstack_push(&ren.box_stack, &v); // x2, y2 v = (struct v_col){ .pos = { .x = x+w, .y = y+h }, .col = c }; vcstack_push(&ren.box_stack, &v); // x3, y3 v = (struct v_col){ .pos = { .x = x+w, .y = y }, .col = c }; vcstack_push(&ren.box_stack, &v); // x1, y1 v = (struct v_col){ .pos = { .x = x, .y = y+h }, .col = c }; vcstack_push(&ren.box_stack, &v); // x3, y3 v = (struct v_col){ .pos = { .x = x+w, .y = y }, .col = c }; vcstack_push(&ren.box_stack, &v); // x4, y4 v = (struct v_col){ .pos = { .x = x, .y = y }, .col = c }; vcstack_push(&ren.box_stack, &v); return 0; } int ren_render_box(int x, int y, int w, int h, unsigned int color) { ren_push_box(x, y, w, h, color); ren_draw_box_stack(); return 0; } int ren_free(void) { GL(glUseProgram(0)) GL(glBindBuffer(GL_ARRAY_BUFFER, 0)) GL(glDeleteProgram(ren.box_prog)); GL(glDeleteProgram(ren.font_prog)); GL(glDeleteBuffers(1, &ren.font_buffer)) for (int i = 0; i < ren.fonts_no; i++) { GL(glDeleteTextures(1, &ren.fonts[i].texture)) font_free(ren.fonts[i].font); } SDL_GL_DeleteContext(ren.gl); vtstack_free(&ren.font_stack); vcstack_free(&ren.box_stack); return 0; }