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; Arena RenderArena; Rect2 CurrentScissor; Font MainFont; Int FontLineSpacing; Int FontCharSpacing; // ---------- 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 ReportWarningf(const char *fmt, ...); void GLDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *user) { ReportWarningf("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 RenderArena = *AllocArena(GetSystemAllocator(), MiB(64)); #else InitArena(&RenderArena); #endif #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) { Clear(&RenderArena); TotalVertexCount = 0; Vertices.first = NULL; Vertices.last = NULL; CurrentScissor = Rect0Size(wx, wy); } // ---------- EndFrameRender for ES3 ---------- void EndFrameRender(float wx, float wy, Color color) { 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, MainFont.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, float rotation = 0.f, Vec2 rotation_point = {}) { 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 }; if (rotation != 0.f) { float s = sinf(rotation); float c = cosf(rotation); for (int i = 0; i < 6; i += 1) { v[i].pos -= rotation_point; v[i].pos = {v[i].pos.x * c + v[i].pos.y * (-s), v[i].pos.x * s + v[i].pos.y * c}; v[i].pos += rotation_point; } } } void DrawRect(Rect2 rect, Color color) { PushQuad2D(RenderArena, &Vertices, rect, MainFont.white_texture_bounding_box, color); } void DrawRect(Rect2I rect, Color color) { PushQuad2D(RenderArena, &Vertices, ToRect2(rect), MainFont.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, MainFont.white_texture_bounding_box, color); PushQuad2D(RenderArena, &Vertices, b, MainFont.white_texture_bounding_box, color); PushQuad2D(RenderArena, &Vertices, c, MainFont.white_texture_bounding_box, color); PushQuad2D(RenderArena, &Vertices, d, MainFont.white_texture_bounding_box, color); } void DrawRectOutline(Rect2I rect, Color color, Int thickness = 2) { DrawRectOutline(ToRect2(rect), color, (float)thickness); } Int GetCharSpacing(Font *font, int codepoint = '_') { Glyph *g = GetGlyph(font, codepoint); if (g->xadvance) return (Int)g->xadvance; return (Int)g->size.x; } Int GetLineSpacing(Font *font) { Int result = (Int)(font->ascent - font->descent + font->line_gap); return result; } Vec2 DrawString(Font *font, String16 string, Vec2 pos, Color color, bool draw = true) { pos.y += GetLineSpacing(font) + font->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, rect, g->atlas_bounding_box, color); } pos.x += g->xadvance; } Vec2 result = {pos.x - original_pos.x, font->size}; return result; } #define PI32 3.14159265359f 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 = MainFont.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}; } } // ---------- ReloadFont - replace DSA texture calls ---------- void ReloadFont() { Int size = StyleFontSize; size = ClampBottom((Int)2, size); if (MainFont.texture_id) { glDeleteTextures(1, &MainFont.texture_id); Dealloc(&MainFont.glyphs); MainFont = {}; } Scratch scratch; Atlas atlas = CreateAtlas(scratch, {2048, 2048}); MainFont = CreateFont(&atlas, (uint32_t)size, StyleFont); // create and fill texture with ES3-compatible API GLint filter = GL_NEAREST; if (StyleFontFilter == 1) filter = GL_LINEAR; GLuint tex = 0; glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // allocate storage and upload 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); MainFont.texture_id = tex; FontCharSpacing = GetCharSpacing(&MainFont); FontLineSpacing = GetLineSpacing(&MainFont); }