struct Vertex2D { Vec2 pos; Vec2 tex; Color color; }; struct VertexNode2D { VertexNode2D *next; int count; Vertex2D vertices[1024 * 16]; Rect2 scissor; }; struct VertexList2D { VertexNode2D *first; VertexNode2D *last; }; struct Shader { GLuint program; // linked program (vertex+fragment) GLint uni_invHalf; // uniform location for inv half-screen size GLint uni_texture; // sampler location }; VertexList2D Vertices; int64_t TotalVertexCount; unsigned VBO, VAO; Shader Shader2D; BlockArena RenderArena; Rect2 CurrentScissor; Font PrimaryFont; Font SecondaryFont; // ---------- shaders (ES3 / WebGL2) ---------- static const char *glsl_vshader_es3 = R"==(#version 300 es precision highp float; uniform vec2 U_InvHalfScreenSize; // same meaning as before layout(location = 0) in vec2 aPos; layout(location = 1) in vec2 aTex; layout(location = 2) in vec4 aColor; // normalized unsigned byte will map to 0..1 if we use normalized=true out vec2 vUV; out vec4 vColor; void main() { vec2 pos = aPos * U_InvHalfScreenSize; pos.y = 2.0 - pos.y; // invert Y the same way you had pos -= vec2(1.0, 1.0); gl_Position = vec4(pos, 0.0, 1.0); vUV = aTex; vColor = aColor; // already 0..1 } )=="; static const char *glsl_fshader_es3 = R"==(#version 300 es precision mediump float; in vec2 vUV; in vec4 vColor; uniform sampler2D S_Texture; out vec4 OutColor; void main() { float a = texture(S_Texture, vUV).r; vec4 c = vColor; c.a *= a; OutColor = c; } )=="; void ReportErrorf(const char *fmt, ...); void GLDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *user) { Unused(source); Unused(type); Unused(id); Unused(length); Unused(user); ReportErrorf("OpenGL message: %s", message); if (severity == GL_DEBUG_SEVERITY_HIGH || severity == GL_DEBUG_SEVERITY_MEDIUM) { SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "OpenGL error", message, NULL); } } // ---------- helper: compile/link ---------- static GLuint CompileShaderSrc(GLenum kind, const char *src) { GLuint s = glCreateShader(kind); glShaderSource(s, 1, &src, NULL); glCompileShader(s); GLint ok = 0; glGetShaderiv(s, GL_COMPILE_STATUS, &ok); if (!ok) { char buf[1024]; glGetShaderInfoLog(s, sizeof(buf), NULL, buf); SDL_Log("%s", buf); SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, (kind == GL_VERTEX_SHADER ? "Vertex shader compile error" : "Fragment shader compile error"), buf, NULL); } return s; } Shader CreateShaderES3(const char *vsrc, const char *fsrc) { Shader out = {}; GLuint vs = CompileShaderSrc(GL_VERTEX_SHADER, vsrc); GLuint fs = CompileShaderSrc(GL_FRAGMENT_SHADER, fsrc); GLuint prog = glCreateProgram(); glAttachShader(prog, vs); glAttachShader(prog, fs); // Bind attribute locations to match layout locations (optional because we use layout(location=...)) // glBindAttribLocation(prog, 0, "aPos"); // glBindAttribLocation(prog, 1, "aTex"); // glBindAttribLocation(prog, 2, "aColor"); glLinkProgram(prog); GLint linked = 0; glGetProgramiv(prog, GL_LINK_STATUS, &linked); if (!linked) { char buf[1024]; glGetProgramInfoLog(prog, sizeof(buf), NULL, buf); SDL_Log("%s", buf); SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Program link error", buf, NULL); } // We can detach/delete shaders after linking glDetachShader(prog, vs); glDetachShader(prog, fs); glDeleteShader(vs); glDeleteShader(fs); out.program = prog; out.uni_invHalf = glGetUniformLocation(prog, "U_InvHalfScreenSize"); out.uni_texture = glGetUniformLocation(prog, "S_Texture"); return out; } // ---------- InitRender for ES3 ---------- void InitRender() { #if !OS_WASM glDebugMessageCallback(&GLDebugCallback, NULL); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); #endif // Create VBO (non-DSA path) glGenBuffers(1, &VBO); // reserve buffer big enough for one VertexNode2D->vertices array (same as original intent) GLsizeiptr vbo_size = (GLsizeiptr) (Lengthof(((VertexNode2D*)0)->vertices) * sizeof(Vertex2D)); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, vbo_size, NULL, GL_DYNAMIC_DRAW); // NULL initial, dynamic usage // Create and setup VAO glGenVertexArrays(1, &VAO); glBindVertexArray(VAO); glBindBuffer(GL_ARRAY_BUFFER, VBO); // attribute 0 : pos (vec2, floats) glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), (const void*)offsetof(Vertex2D, pos)); // attribute 1 : tex (vec2) glEnableVertexAttribArray(1); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), (const void*)offsetof(Vertex2D, tex)); // attribute 2 : color (vec4) -- stored as 4 unsigned bytes; we want normalized floats 0..1 glEnableVertexAttribArray(2); // Using normalized GL_TRUE to map 0..255 -> 0..1 directly in shader glVertexAttribPointer(2, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Vertex2D), (const void*)offsetof(Vertex2D, color)); // Unbind VAO and VBO glBindVertexArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); // Create shader program (ES3) Shader2D = CreateShaderES3(glsl_vshader_es3, glsl_fshader_es3); // other initializations like fonts will be done elsewhere (ReloadFont) } void BeginFrameRender(float wx, float wy) { Release(&RenderArena); TotalVertexCount = 0; Vertices.first = NULL; Vertices.last = NULL; CurrentScissor = Rect0Size(wx, wy); } // ---------- EndFrameRender for ES3 ---------- void EndFrameRender(float wx, float wy, Color color) { ProfileFunction(); glEnable(GL_BLEND); glEnable(GL_SCISSOR_TEST); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); glViewport(0, 0, (GLsizei)wx, (GLsizei)wy); glScissor(0, 0, (GLsizei)wx, (GLsizei)wy); glClearColor(color.r, color.g, color.b, color.a); glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // Use program and set uniforms glUseProgram(Shader2D.program); float xinverse = 1.f / (wx / 2.f); float yinverse = 1.f / (wy / 2.f); // set uniform (U_InvHalfScreenSize) if (Shader2D.uni_invHalf >= 0) { glUniform2f(Shader2D.uni_invHalf, xinverse, yinverse); } // set sampler to texture unit 0 if (Shader2D.uni_texture >= 0) { glUniform1i(Shader2D.uni_texture, 0); } glBindVertexArray(VAO); for (VertexNode2D *it = Vertices.first; it; it = it->next) { Rect2 rect = it->scissor; GLint x = (GLint)rect.min.x; GLint y = (GLint)rect.min.y; GLsizei w = (GLsizei)(rect.max.x - x); GLsizei h = (GLsizei)(rect.max.y - y); // convert scissor Y to OpenGL bottom-left origin like before glScissor(x, (GLint)wy - (GLint)rect.max.y, w, h); // upload vertex data into VBO at offset 0 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferSubData(GL_ARRAY_BUFFER, 0, it->count * sizeof(Vertex2D), it->vertices); // bind texture unit 0 and the font texture glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, PrimaryFont.texture_id); // draw glDrawArrays(GL_TRIANGLES, 0, it->count); // unbind VBO (optional) glBindBuffer(GL_ARRAY_BUFFER, 0); } glBindVertexArray(0); glUseProgram(0); } VertexNode2D *AllocVertexNode2D(Allocator allocator, VertexList2D *list) { VertexNode2D *node = AllocType(allocator, VertexNode2D); SLL_QUEUE_ADD(list->first, list->last, node); return node; } void SetScissor(Rect2 rect) { CurrentScissor = rect; VertexNode2D *node = Vertices.last; if (!node) { node = AllocVertexNode2D(RenderArena, &Vertices); node->scissor = rect; return; } if (node->scissor != rect && node->count == 0) { node->scissor = rect; return; } if (node->scissor != rect) { node = AllocVertexNode2D(RenderArena, &Vertices); node->scissor = rect; return; } } void SetScissor(Rect2I rect) { SetScissor(ToRect2(rect)); } Vertex2D *AllocVertex2D(Allocator allocator, VertexList2D *list, int count) { VertexNode2D *node = list->last; if (node == 0 || node->count + count > Lengthof(node->vertices)) { node = AllocVertexNode2D(allocator, list); node->scissor = CurrentScissor; } TotalVertexCount += count; Vertex2D *result = node->vertices + node->count; node->count += count; return result; } void PushVertex2D(Allocator allocator, VertexList2D *list, Vertex2D *vertices, int count) { Vertex2D *result = AllocVertex2D(allocator, list, count); for (int i = 0; i < count; i += 1) result[i] = vertices[i]; } void PushQuad2D(Allocator arena, VertexList2D *list, Rect2 rect, Rect2 tex, Color color) { Vertex2D *v = AllocVertex2D(arena, list, 6); v[0] = { {rect.min.x, rect.max.y}, { tex.min.x, tex.max.y}, color }; v[1] = { {rect.max.x, rect.max.y}, { tex.max.x, tex.max.y}, color }; v[2] = { {rect.min.x, rect.min.y}, { tex.min.x, tex.min.y}, color }; v[3] = { {rect.min.x, rect.min.y}, { tex.min.x, tex.min.y}, color }; v[4] = { {rect.max.x, rect.max.y}, { tex.max.x, tex.max.y}, color }; v[5] = { {rect.max.x, rect.min.y}, { tex.max.x, tex.min.y}, color }; } void DrawRect(Rect2 rect, Color color) { PushQuad2D(RenderArena, &Vertices, rect, PrimaryFont.white_texture_bounding_box, color); } void DrawRect(Rect2I rect, Color color) { PushQuad2D(RenderArena, &Vertices, ToRect2(rect), PrimaryFont.white_texture_bounding_box, color); } void DrawRectOutline(Rect2 rect, Color color, float thickness = 2) { Rect2 a = CutLeft(&rect, thickness); Rect2 b = CutRight(&rect, thickness); Rect2 c = CutTop(&rect, thickness); Rect2 d = CutBottom(&rect, thickness); PushQuad2D(RenderArena, &Vertices, a, PrimaryFont.white_texture_bounding_box, color); PushQuad2D(RenderArena, &Vertices, b, PrimaryFont.white_texture_bounding_box, color); PushQuad2D(RenderArena, &Vertices, c, PrimaryFont.white_texture_bounding_box, color); PushQuad2D(RenderArena, &Vertices, d, PrimaryFont.white_texture_bounding_box, color); } void DrawRectOutline(Rect2I rect, Color color, Int thickness = 2) { DrawRectOutline(ToRect2(rect), color, (float)thickness); } Vec2 DrawString(Font *font, String16 string, Vec2 pos, Color color, bool draw = true) { pos.y += font->line_spacing + font->descent; // TODO: descent? Vec2 original_pos = pos; For(string) { Glyph *g = GetGlyph(font, it); Rect2 rect = Rect2FromSize(pos + g->offset, g->size); if (draw && it != '\n' && it != ' ' && it != '\t') { PushQuad2D(RenderArena, &Vertices, Round(rect), g->atlas_bounding_box, color); } pos.x += g->xadvance; } Vec2 result = {pos.x - original_pos.x, font->size}; return result; } void DrawCircle(Vec2 pos, float radius, Color color) { const int segment_count = 16; const int vertex_count = segment_count * 3; Vec2 points[segment_count + 1]; for (int i = 0; i < Lengthof(points); i += 1) { float radians = 2.0f * PI32 * float(i) / float(segment_count); float x = radius * cosf(radians); float y = radius * sinf(radians); points[i] = {x, y}; } points[segment_count] = points[0]; // wrap around Vertex2D *vertices = AllocVertex2D(RenderArena, &Vertices, vertex_count); int point_i = 0; int segment_i = 0; for (; segment_i < vertex_count; segment_i += 3, point_i += 1) { Rect2 tex = PrimaryFont.white_texture_bounding_box; Vertex2D *it = vertices + segment_i; it[0].color = color; it[1].color = color; it[2].color = color; it[0].tex = {tex.min.x, tex.max.y}; it[1].tex = {tex.max.x, tex.max.y}; it[2].tex = {tex.min.x, tex.min.y}; it[0].pos = {pos + points[point_i]}; it[1].pos = {pos + points[point_i + 1]}; it[2].pos = {pos}; } } GLuint UploadAtlas(Atlas *atlas) { GLuint tex = 0; glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); 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_R8, (GLsizei)atlas->size.x, (GLsizei)atlas->size.y, 0, GL_RED, GL_UNSIGNED_BYTE, atlas->bitmap); glBindTexture(GL_TEXTURE_2D, 0); atlas->texture_id = tex; return tex; } void ReloadFont(String path, U32 size) { if (PrimaryFont.texture_id) { glDeleteTextures(1, &PrimaryFont.texture_id); Dealloc(&PrimaryFont.glyphs); PrimaryFont = {}; } Scratch scratch; Atlas atlas = CreateAtlas(scratch, {2048, 2048}); PrimaryFont = CreateFont(&atlas, (uint32_t)ClampBottom(2u, (U32)size), path); SecondaryFont = CreateFont(&atlas, 12, path); SecondaryFont.texture_id = PrimaryFont.texture_id = UploadAtlas(&atlas); }