fortrangoingonforty/fortty / 8d13407

Browse files

Add GPU-accelerated text rendering with FreeType atlas

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
8d13407d9b9eb1a270bc24162dc6abe2b2f7699d
Parents
d016cd4
Tree
592e12b

9 changed files

StatusFile+-
M CMakeLists.txt 3 0
M c_src/gl_loader.c 192 0
A shaders/text.frag 13 0
A shaders/text.vert 16 0
M src/fortty.f90 44 1
M src/gl/gl_bindings.f90 290 9
A src/gl/shader.f90 133 0
A src/text/atlas.f90 161 0
A src/text/renderer.f90 331 0
CMakeLists.txtmodified
@@ -33,7 +33,10 @@ set(FORTRAN_SOURCES
3333
     src/core/types.f90
3434
     src/gl/gl_bindings.f90
3535
     src/text/glyph.f90
36
+    src/gl/shader.f90
3637
     src/text/font.f90
38
+    src/text/atlas.f90
39
+    src/text/renderer.f90
3740
     src/window/glfw_bindings.f90
3841
     src/window/window.f90
3942
     src/fortty.f90
c_src/gl_loader.cmodified
@@ -27,3 +27,195 @@ void fortty_glClear(unsigned int mask) {
2727
 void fortty_glClearColor(float red, float green, float blue, float alpha) {
2828
     glClearColor(red, green, blue, alpha);
2929
 }
30
+
31
+/* ============ Texture functions ============ */
32
+
33
+void fortty_glGenTextures(int n, unsigned int *textures) {
34
+    glGenTextures(n, textures);
35
+}
36
+
37
+void fortty_glDeleteTextures(int n, unsigned int *textures) {
38
+    glDeleteTextures(n, textures);
39
+}
40
+
41
+void fortty_glBindTexture(unsigned int target, unsigned int texture) {
42
+    glBindTexture(target, texture);
43
+}
44
+
45
+void fortty_glTexImage2D(unsigned int target, int level, int internalformat,
46
+                         int width, int height, int border,
47
+                         unsigned int format, unsigned int type, const void *data) {
48
+    glTexImage2D(target, level, internalformat, width, height, border, format, type, data);
49
+}
50
+
51
+void fortty_glTexSubImage2D(unsigned int target, int level,
52
+                            int xoffset, int yoffset, int width, int height,
53
+                            unsigned int format, unsigned int type, const void *data) {
54
+    glTexSubImage2D(target, level, xoffset, yoffset, width, height, format, type, data);
55
+}
56
+
57
+void fortty_glTexParameteri(unsigned int target, unsigned int pname, int param) {
58
+    glTexParameteri(target, pname, param);
59
+}
60
+
61
+void fortty_glPixelStorei(unsigned int pname, int param) {
62
+    glPixelStorei(pname, param);
63
+}
64
+
65
+void fortty_glActiveTexture(unsigned int texture) {
66
+    glActiveTexture(texture);
67
+}
68
+
69
+/* ============ Shader functions ============ */
70
+
71
+unsigned int fortty_glCreateShader(unsigned int type) {
72
+    return glCreateShader(type);
73
+}
74
+
75
+void fortty_glDeleteShader(unsigned int shader) {
76
+    glDeleteShader(shader);
77
+}
78
+
79
+void fortty_glShaderSource(unsigned int shader, const char *source) {
80
+    const char *sources[1] = { source };
81
+    glShaderSource(shader, 1, sources, NULL);
82
+}
83
+
84
+void fortty_glCompileShader(unsigned int shader) {
85
+    glCompileShader(shader);
86
+}
87
+
88
+int fortty_glGetShaderCompileStatus(unsigned int shader) {
89
+    int success;
90
+    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
91
+    return success;
92
+}
93
+
94
+void fortty_glGetShaderInfoLog(unsigned int shader, char *log, int maxlen) {
95
+    glGetShaderInfoLog(shader, maxlen, NULL, log);
96
+}
97
+
98
+unsigned int fortty_glCreateProgram(void) {
99
+    return glCreateProgram();
100
+}
101
+
102
+void fortty_glDeleteProgram(unsigned int program) {
103
+    glDeleteProgram(program);
104
+}
105
+
106
+void fortty_glAttachShader(unsigned int program, unsigned int shader) {
107
+    glAttachShader(program, shader);
108
+}
109
+
110
+void fortty_glLinkProgram(unsigned int program) {
111
+    glLinkProgram(program);
112
+}
113
+
114
+int fortty_glGetProgramLinkStatus(unsigned int program) {
115
+    int success;
116
+    glGetProgramiv(program, GL_LINK_STATUS, &success);
117
+    return success;
118
+}
119
+
120
+void fortty_glGetProgramInfoLog(unsigned int program, char *log, int maxlen) {
121
+    glGetProgramInfoLog(program, maxlen, NULL, log);
122
+}
123
+
124
+void fortty_glUseProgram(unsigned int program) {
125
+    glUseProgram(program);
126
+}
127
+
128
+int fortty_glGetUniformLocation(unsigned int program, const char *name) {
129
+    return glGetUniformLocation(program, name);
130
+}
131
+
132
+void fortty_glUniform1i(int location, int value) {
133
+    glUniform1i(location, value);
134
+}
135
+
136
+void fortty_glUniform1f(int location, float value) {
137
+    glUniform1f(location, value);
138
+}
139
+
140
+void fortty_glUniform3f(int location, float v0, float v1, float v2) {
141
+    glUniform3f(location, v0, v1, v2);
142
+}
143
+
144
+void fortty_glUniform4f(int location, float v0, float v1, float v2, float v3) {
145
+    glUniform4f(location, v0, v1, v2, v3);
146
+}
147
+
148
+void fortty_glUniformMatrix4fv(int location, int count, int transpose, const float *value) {
149
+    glUniformMatrix4fv(location, count, transpose, value);
150
+}
151
+
152
+/* ============ VAO/VBO functions ============ */
153
+
154
+void fortty_glGenVertexArrays(int n, unsigned int *arrays) {
155
+    glGenVertexArrays(n, arrays);
156
+}
157
+
158
+void fortty_glDeleteVertexArrays(int n, unsigned int *arrays) {
159
+    glDeleteVertexArrays(n, arrays);
160
+}
161
+
162
+void fortty_glBindVertexArray(unsigned int array) {
163
+    glBindVertexArray(array);
164
+}
165
+
166
+void fortty_glGenBuffers(int n, unsigned int *buffers) {
167
+    glGenBuffers(n, buffers);
168
+}
169
+
170
+void fortty_glDeleteBuffers(int n, unsigned int *buffers) {
171
+    glDeleteBuffers(n, buffers);
172
+}
173
+
174
+void fortty_glBindBuffer(unsigned int target, unsigned int buffer) {
175
+    glBindBuffer(target, buffer);
176
+}
177
+
178
+void fortty_glBufferData(unsigned int target, size_t size, const void *data, unsigned int usage) {
179
+    glBufferData(target, size, data, usage);
180
+}
181
+
182
+void fortty_glBufferSubData(unsigned int target, size_t offset, size_t size, const void *data) {
183
+    glBufferSubData(target, offset, size, data);
184
+}
185
+
186
+void fortty_glBufferSubData_floats(unsigned int target, size_t offset, size_t size, const float *data) {
187
+    glBufferSubData(target, offset, size, data);
188
+}
189
+
190
+void fortty_glVertexAttribPointer(unsigned int index, int size, unsigned int type,
191
+                                   int normalized, int stride, size_t offset) {
192
+    glVertexAttribPointer(index, size, type, normalized, stride, (void*)offset);
193
+}
194
+
195
+void fortty_glEnableVertexAttribArray(unsigned int index) {
196
+    glEnableVertexAttribArray(index);
197
+}
198
+
199
+void fortty_glDisableVertexAttribArray(unsigned int index) {
200
+    glDisableVertexAttribArray(index);
201
+}
202
+
203
+/* ============ Drawing functions ============ */
204
+
205
+void fortty_glDrawArrays(unsigned int mode, int first, int count) {
206
+    glDrawArrays(mode, first, count);
207
+}
208
+
209
+/* ============ State functions ============ */
210
+
211
+void fortty_glEnable(unsigned int cap) {
212
+    glEnable(cap);
213
+}
214
+
215
+void fortty_glDisable(unsigned int cap) {
216
+    glDisable(cap);
217
+}
218
+
219
+void fortty_glBlendFunc(unsigned int sfactor, unsigned int dfactor) {
220
+    glBlendFunc(sfactor, dfactor);
221
+}
shaders/text.fragadded
@@ -0,0 +1,13 @@
1
+#version 330 core
2
+
3
+in vec2 v_texcoord;
4
+in vec4 v_color;
5
+
6
+uniform sampler2D u_atlas;
7
+
8
+out vec4 frag_color;
9
+
10
+void main() {
11
+    float alpha = texture(u_atlas, v_texcoord).r;
12
+    frag_color = vec4(v_color.rgb, v_color.a * alpha);
13
+}
shaders/text.vertadded
@@ -0,0 +1,16 @@
1
+#version 330 core
2
+
3
+layout(location = 0) in vec2 a_position;
4
+layout(location = 1) in vec2 a_texcoord;
5
+layout(location = 2) in vec4 a_color;
6
+
7
+uniform mat4 u_projection;
8
+
9
+out vec2 v_texcoord;
10
+out vec4 v_color;
11
+
12
+void main() {
13
+    gl_Position = u_projection * vec4(a_position, 0.0, 1.0);
14
+    v_texcoord = a_texcoord;
15
+    v_color = a_color;
16
+}
src/fortty.f90modified
@@ -1,12 +1,41 @@
11
 program fortty
22
   use window_mod
33
   use gl_bindings
4
+  use renderer_mod
45
   implicit none
56
 
67
   type(window_t) :: win
8
+  type(renderer_t) :: ren
9
+  integer :: win_width, win_height
10
+  character(len=256) :: font_path
11
+
12
+  ! Window dimensions
13
+  win_width = 800
14
+  win_height = 600
715
 
816
   ! Create window with OpenGL context
9
-  win = window_create(800, 600, "fortty")
17
+  win = window_create(win_width, win_height, "fortty")
18
+
19
+  ! Font path - try common system locations
20
+  font_path = "/usr/share/fonts/TTF/DejaVuSansMono.ttf"
21
+
22
+  ! Create renderer with font
23
+  ren = renderer_create(trim(font_path), 24)
24
+  if (.not. ren%initialized) then
25
+    print *, "Warning: Could not load font, trying alternate path..."
26
+    font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
27
+    ren = renderer_create(trim(font_path), 24)
28
+  end if
29
+
30
+  if (.not. ren%initialized) then
31
+    print *, "Error: Could not initialize renderer"
32
+    print *, "Please ensure DejaVu Sans Mono font is installed"
33
+    call window_destroy(win)
34
+    stop 1
35
+  end if
36
+
37
+  ! Set up projection matrix
38
+  call renderer_set_projection(ren, win_width, win_height)
1039
 
1140
   ! Main event loop
1241
   do while (.not. window_should_close(win))
@@ -14,12 +43,26 @@ program fortty
1443
     call glClearColor(0.1, 0.1, 0.12, 1.0)
1544
     call glClear(GL_COLOR_BUFFER_BIT)
1645
 
46
+    ! Begin new frame
47
+    call renderer_begin(ren)
48
+
49
+    ! Draw test text
50
+    call renderer_draw_string(ren, 50.0, 100.0, "Hello, Fortran!", &
51
+                              1.0, 1.0, 1.0, 1.0)
52
+
53
+    call renderer_draw_string(ren, 50.0, 150.0, "Terminal emulator in progress...", &
54
+                              0.7, 0.7, 0.7, 1.0)
55
+
56
+    ! Flush to GPU
57
+    call renderer_flush(ren)
58
+
1759
     ! Swap buffers and poll events
1860
     call window_swap_buffers(win)
1961
     call window_poll_events()
2062
   end do
2163
 
2264
   ! Cleanup
65
+  call renderer_destroy(ren)
2366
   call window_destroy(win)
2467
 
2568
 end program fortty
src/gl/gl_bindings.f90modified
@@ -6,33 +6,314 @@ module gl_bindings
66
 
77
   ! OpenGL constants
88
   integer(c_int), parameter :: GL_COLOR_BUFFER_BIT = int(Z'00004000', c_int)
9
+  integer(c_int), parameter :: GL_DEPTH_BUFFER_BIT = int(Z'00000100', c_int)
910
 
10
-  ! OpenGL type aliases
11
-  integer, parameter :: GLint = c_int
12
-  integer, parameter :: GLsizei = c_int
13
-  integer, parameter :: GLfloat = c_float
14
-  integer, parameter :: GLbitfield = c_int
11
+  ! Texture constants
12
+  integer(c_int), parameter :: GL_TEXTURE_2D = int(Z'0DE1', c_int)
13
+  integer(c_int), parameter :: GL_TEXTURE0 = int(Z'84C0', c_int)
14
+  integer(c_int), parameter :: GL_TEXTURE_MIN_FILTER = int(Z'2801', c_int)
15
+  integer(c_int), parameter :: GL_TEXTURE_MAG_FILTER = int(Z'2800', c_int)
16
+  integer(c_int), parameter :: GL_TEXTURE_WRAP_S = int(Z'2802', c_int)
17
+  integer(c_int), parameter :: GL_TEXTURE_WRAP_T = int(Z'2803', c_int)
18
+  integer(c_int), parameter :: GL_LINEAR = int(Z'2601', c_int)
19
+  integer(c_int), parameter :: GL_NEAREST = int(Z'2600', c_int)
20
+  integer(c_int), parameter :: GL_CLAMP_TO_EDGE = int(Z'812F', c_int)
21
+  integer(c_int), parameter :: GL_RED = int(Z'1903', c_int)
22
+  integer(c_int), parameter :: GL_UNSIGNED_BYTE = int(Z'1401', c_int)
23
+  integer(c_int), parameter :: GL_UNPACK_ALIGNMENT = int(Z'0CF5', c_int)
24
+
25
+  ! Shader constants
26
+  integer(c_int), parameter :: GL_VERTEX_SHADER = int(Z'8B31', c_int)
27
+  integer(c_int), parameter :: GL_FRAGMENT_SHADER = int(Z'8B30', c_int)
28
+
29
+  ! Buffer constants
30
+  integer(c_int), parameter :: GL_ARRAY_BUFFER = int(Z'8892', c_int)
31
+  integer(c_int), parameter :: GL_STATIC_DRAW = int(Z'88E4', c_int)
32
+  integer(c_int), parameter :: GL_DYNAMIC_DRAW = int(Z'88E8', c_int)
33
+  integer(c_int), parameter :: GL_STREAM_DRAW = int(Z'88E0', c_int)
34
+
35
+  ! Drawing constants
36
+  integer(c_int), parameter :: GL_TRIANGLES = int(Z'0004', c_int)
37
+  integer(c_int), parameter :: GL_FLOAT = int(Z'1406', c_int)
38
+  integer(c_int), parameter :: GL_FALSE = 0
39
+  integer(c_int), parameter :: GL_TRUE = 1
40
+
41
+  ! Blending constants
42
+  integer(c_int), parameter :: GL_BLEND = int(Z'0BE2', c_int)
43
+  integer(c_int), parameter :: GL_SRC_ALPHA = int(Z'0302', c_int)
44
+  integer(c_int), parameter :: GL_ONE_MINUS_SRC_ALPHA = int(Z'0303', c_int)
1545
 
1646
   ! Interface to C wrapper functions (in gl_loader.c)
17
-  ! These wrap the GLAD-loaded OpenGL functions
1847
   interface
19
-    ! void fortty_glViewport(int x, int y, int width, int height)
48
+    ! ============ Basic functions ============
2049
     subroutine glViewport(x, y, width, height) bind(C, name="fortty_glViewport")
2150
       import :: c_int
2251
       integer(c_int), value :: x, y, width, height
2352
     end subroutine glViewport
2453
 
25
-    ! void fortty_glClear(unsigned int mask)
2654
     subroutine glClear(mask) bind(C, name="fortty_glClear")
2755
       import :: c_int
2856
       integer(c_int), value :: mask
2957
     end subroutine glClear
3058
 
31
-    ! void fortty_glClearColor(float red, float green, float blue, float alpha)
3259
     subroutine glClearColor(red, green, blue, alpha) bind(C, name="fortty_glClearColor")
3360
       import :: c_float
3461
       real(c_float), value :: red, green, blue, alpha
3562
     end subroutine glClearColor
63
+
64
+    ! ============ Texture functions ============
65
+    subroutine glGenTextures(n, textures) bind(C, name="fortty_glGenTextures")
66
+      import :: c_int
67
+      integer(c_int), value :: n
68
+      integer(c_int), intent(out) :: textures(*)
69
+    end subroutine glGenTextures
70
+
71
+    subroutine glDeleteTextures(n, textures) bind(C, name="fortty_glDeleteTextures")
72
+      import :: c_int
73
+      integer(c_int), value :: n
74
+      integer(c_int), intent(in) :: textures(*)
75
+    end subroutine glDeleteTextures
76
+
77
+    subroutine glBindTexture(target, texture) bind(C, name="fortty_glBindTexture")
78
+      import :: c_int
79
+      integer(c_int), value :: target, texture
80
+    end subroutine glBindTexture
81
+
82
+    subroutine glTexImage2D(target, level, internalformat, width, height, &
83
+                            border, format, textype, data) bind(C, name="fortty_glTexImage2D")
84
+      import :: c_int, c_ptr
85
+      integer(c_int), value :: target, level, internalformat
86
+      integer(c_int), value :: width, height, border
87
+      integer(c_int), value :: format, textype
88
+      type(c_ptr), value :: data
89
+    end subroutine glTexImage2D
90
+
91
+    subroutine glTexSubImage2D(target, level, xoffset, yoffset, width, height, &
92
+                               format, textype, data) bind(C, name="fortty_glTexSubImage2D")
93
+      import :: c_int, c_ptr
94
+      integer(c_int), value :: target, level, xoffset, yoffset
95
+      integer(c_int), value :: width, height, format, textype
96
+      type(c_ptr), value :: data
97
+    end subroutine glTexSubImage2D
98
+
99
+    subroutine glTexParameteri(target, pname, param) bind(C, name="fortty_glTexParameteri")
100
+      import :: c_int
101
+      integer(c_int), value :: target, pname, param
102
+    end subroutine glTexParameteri
103
+
104
+    subroutine glPixelStorei(pname, param) bind(C, name="fortty_glPixelStorei")
105
+      import :: c_int
106
+      integer(c_int), value :: pname, param
107
+    end subroutine glPixelStorei
108
+
109
+    subroutine glActiveTexture(texture) bind(C, name="fortty_glActiveTexture")
110
+      import :: c_int
111
+      integer(c_int), value :: texture
112
+    end subroutine glActiveTexture
113
+
114
+    ! ============ Shader functions ============
115
+    integer(c_int) function glCreateShader(shadertype) bind(C, name="fortty_glCreateShader")
116
+      import :: c_int
117
+      integer(c_int), value :: shadertype
118
+    end function glCreateShader
119
+
120
+    subroutine glDeleteShader(shader) bind(C, name="fortty_glDeleteShader")
121
+      import :: c_int
122
+      integer(c_int), value :: shader
123
+    end subroutine glDeleteShader
124
+
125
+    subroutine glShaderSource(shader, source) bind(C, name="fortty_glShaderSource")
126
+      import :: c_int, c_char
127
+      integer(c_int), value :: shader
128
+      character(kind=c_char), intent(in) :: source(*)
129
+    end subroutine glShaderSource
130
+
131
+    subroutine glCompileShader(shader) bind(C, name="fortty_glCompileShader")
132
+      import :: c_int
133
+      integer(c_int), value :: shader
134
+    end subroutine glCompileShader
135
+
136
+    integer(c_int) function glGetShaderCompileStatus(shader) &
137
+        bind(C, name="fortty_glGetShaderCompileStatus")
138
+      import :: c_int
139
+      integer(c_int), value :: shader
140
+    end function glGetShaderCompileStatus
141
+
142
+    subroutine glGetShaderInfoLog(shader, log, maxlen) bind(C, name="fortty_glGetShaderInfoLog")
143
+      import :: c_int, c_char
144
+      integer(c_int), value :: shader, maxlen
145
+      character(kind=c_char), intent(out) :: log(*)
146
+    end subroutine glGetShaderInfoLog
147
+
148
+    integer(c_int) function glCreateProgram() bind(C, name="fortty_glCreateProgram")
149
+      import :: c_int
150
+    end function glCreateProgram
151
+
152
+    subroutine glDeleteProgram(program) bind(C, name="fortty_glDeleteProgram")
153
+      import :: c_int
154
+      integer(c_int), value :: program
155
+    end subroutine glDeleteProgram
156
+
157
+    subroutine glAttachShader(program, shader) bind(C, name="fortty_glAttachShader")
158
+      import :: c_int
159
+      integer(c_int), value :: program, shader
160
+    end subroutine glAttachShader
161
+
162
+    subroutine glLinkProgram(program) bind(C, name="fortty_glLinkProgram")
163
+      import :: c_int
164
+      integer(c_int), value :: program
165
+    end subroutine glLinkProgram
166
+
167
+    integer(c_int) function glGetProgramLinkStatus(program) &
168
+        bind(C, name="fortty_glGetProgramLinkStatus")
169
+      import :: c_int
170
+      integer(c_int), value :: program
171
+    end function glGetProgramLinkStatus
172
+
173
+    subroutine glGetProgramInfoLog(program, log, maxlen) bind(C, name="fortty_glGetProgramInfoLog")
174
+      import :: c_int, c_char
175
+      integer(c_int), value :: program, maxlen
176
+      character(kind=c_char), intent(out) :: log(*)
177
+    end subroutine glGetProgramInfoLog
178
+
179
+    subroutine glUseProgram(program) bind(C, name="fortty_glUseProgram")
180
+      import :: c_int
181
+      integer(c_int), value :: program
182
+    end subroutine glUseProgram
183
+
184
+    integer(c_int) function glGetUniformLocation(program, name) &
185
+        bind(C, name="fortty_glGetUniformLocation")
186
+      import :: c_int, c_char
187
+      integer(c_int), value :: program
188
+      character(kind=c_char), intent(in) :: name(*)
189
+    end function glGetUniformLocation
190
+
191
+    subroutine glUniform1i(location, value) bind(C, name="fortty_glUniform1i")
192
+      import :: c_int
193
+      integer(c_int), value :: location, value
194
+    end subroutine glUniform1i
195
+
196
+    subroutine glUniform1f(location, value) bind(C, name="fortty_glUniform1f")
197
+      import :: c_int, c_float
198
+      integer(c_int), value :: location
199
+      real(c_float), value :: value
200
+    end subroutine glUniform1f
201
+
202
+    subroutine glUniform3f(location, v0, v1, v2) bind(C, name="fortty_glUniform3f")
203
+      import :: c_int, c_float
204
+      integer(c_int), value :: location
205
+      real(c_float), value :: v0, v1, v2
206
+    end subroutine glUniform3f
207
+
208
+    subroutine glUniform4f(location, v0, v1, v2, v3) bind(C, name="fortty_glUniform4f")
209
+      import :: c_int, c_float
210
+      integer(c_int), value :: location
211
+      real(c_float), value :: v0, v1, v2, v3
212
+    end subroutine glUniform4f
213
+
214
+    subroutine glUniformMatrix4fv(location, count, transpose, value) &
215
+        bind(C, name="fortty_glUniformMatrix4fv")
216
+      import :: c_int, c_float
217
+      integer(c_int), value :: location, count, transpose
218
+      real(c_float), intent(in) :: value(*)
219
+    end subroutine glUniformMatrix4fv
220
+
221
+    ! ============ VAO/VBO functions ============
222
+    subroutine glGenVertexArrays(n, arrays) bind(C, name="fortty_glGenVertexArrays")
223
+      import :: c_int
224
+      integer(c_int), value :: n
225
+      integer(c_int), intent(out) :: arrays(*)
226
+    end subroutine glGenVertexArrays
227
+
228
+    subroutine glDeleteVertexArrays(n, arrays) bind(C, name="fortty_glDeleteVertexArrays")
229
+      import :: c_int
230
+      integer(c_int), value :: n
231
+      integer(c_int), intent(in) :: arrays(*)
232
+    end subroutine glDeleteVertexArrays
233
+
234
+    subroutine glBindVertexArray(array) bind(C, name="fortty_glBindVertexArray")
235
+      import :: c_int
236
+      integer(c_int), value :: array
237
+    end subroutine glBindVertexArray
238
+
239
+    subroutine glGenBuffers(n, buffers) bind(C, name="fortty_glGenBuffers")
240
+      import :: c_int
241
+      integer(c_int), value :: n
242
+      integer(c_int), intent(out) :: buffers(*)
243
+    end subroutine glGenBuffers
244
+
245
+    subroutine glDeleteBuffers(n, buffers) bind(C, name="fortty_glDeleteBuffers")
246
+      import :: c_int
247
+      integer(c_int), value :: n
248
+      integer(c_int), intent(in) :: buffers(*)
249
+    end subroutine glDeleteBuffers
250
+
251
+    subroutine glBindBuffer(target, buffer) bind(C, name="fortty_glBindBuffer")
252
+      import :: c_int
253
+      integer(c_int), value :: target, buffer
254
+    end subroutine glBindBuffer
255
+
256
+    subroutine glBufferData(target, datasize, data, usage) bind(C, name="fortty_glBufferData")
257
+      import :: c_int, c_size_t, c_ptr
258
+      integer(c_int), value :: target
259
+      integer(c_size_t), value :: datasize
260
+      type(c_ptr), value :: data
261
+      integer(c_int), value :: usage
262
+    end subroutine glBufferData
263
+
264
+    subroutine glBufferSubData(target, offset, datasize, data) bind(C, name="fortty_glBufferSubData")
265
+      import :: c_int, c_size_t, c_ptr
266
+      integer(c_int), value :: target
267
+      integer(c_size_t), value :: offset, datasize
268
+      type(c_ptr), value :: data
269
+    end subroutine glBufferSubData
270
+
271
+    subroutine glBufferSubData_floats(target, offset, datasize, data) &
272
+        bind(C, name="fortty_glBufferSubData_floats")
273
+      import :: c_int, c_size_t, c_float
274
+      integer(c_int), value :: target
275
+      integer(c_size_t), value :: offset, datasize
276
+      real(c_float), intent(in) :: data(*)
277
+    end subroutine glBufferSubData_floats
278
+
279
+    subroutine glVertexAttribPointer(index, vsize, vtype, normalized, stride, offset) &
280
+        bind(C, name="fortty_glVertexAttribPointer")
281
+      import :: c_int, c_size_t
282
+      integer(c_int), value :: index, vsize, vtype, normalized, stride
283
+      integer(c_size_t), value :: offset
284
+    end subroutine glVertexAttribPointer
285
+
286
+    subroutine glEnableVertexAttribArray(index) bind(C, name="fortty_glEnableVertexAttribArray")
287
+      import :: c_int
288
+      integer(c_int), value :: index
289
+    end subroutine glEnableVertexAttribArray
290
+
291
+    subroutine glDisableVertexAttribArray(index) bind(C, name="fortty_glDisableVertexAttribArray")
292
+      import :: c_int
293
+      integer(c_int), value :: index
294
+    end subroutine glDisableVertexAttribArray
295
+
296
+    ! ============ Drawing functions ============
297
+    subroutine glDrawArrays(mode, first, count) bind(C, name="fortty_glDrawArrays")
298
+      import :: c_int
299
+      integer(c_int), value :: mode, first, count
300
+    end subroutine glDrawArrays
301
+
302
+    ! ============ State functions ============
303
+    subroutine glEnable(cap) bind(C, name="fortty_glEnable")
304
+      import :: c_int
305
+      integer(c_int), value :: cap
306
+    end subroutine glEnable
307
+
308
+    subroutine glDisable(cap) bind(C, name="fortty_glDisable")
309
+      import :: c_int
310
+      integer(c_int), value :: cap
311
+    end subroutine glDisable
312
+
313
+    subroutine glBlendFunc(sfactor, dfactor) bind(C, name="fortty_glBlendFunc")
314
+      import :: c_int
315
+      integer(c_int), value :: sfactor, dfactor
316
+    end subroutine glBlendFunc
36317
   end interface
37318
 
38319
 end module gl_bindings
src/gl/shader.f90added
@@ -0,0 +1,133 @@
1
+module shader_mod
2
+  use, intrinsic :: iso_c_binding
3
+  use gl_bindings
4
+  implicit none
5
+  private
6
+
7
+  public :: shader_t
8
+  public :: shader_create, shader_destroy, shader_use
9
+  public :: shader_set_projection, shader_set_int
10
+
11
+  type :: shader_t
12
+    integer :: program_id = 0
13
+    integer :: projection_loc = -1
14
+    integer :: atlas_loc = -1
15
+    logical :: valid = .false.
16
+  end type shader_t
17
+
18
+contains
19
+
20
+  ! Create a shader program from vertex and fragment source
21
+  function shader_create(vert_source, frag_source) result(shader)
22
+    character(len=*), intent(in) :: vert_source, frag_source
23
+    type(shader_t) :: shader
24
+    integer :: vert_id, frag_id
25
+    integer :: success
26
+    character(len=512) :: log_buffer
27
+
28
+    ! Compile vertex shader
29
+    vert_id = glCreateShader(GL_VERTEX_SHADER)
30
+    call glShaderSource(vert_id, vert_source // c_null_char)
31
+    call glCompileShader(vert_id)
32
+
33
+    success = glGetShaderCompileStatus(vert_id)
34
+    if (success == 0) then
35
+      call glGetShaderInfoLog(vert_id, log_buffer, 512)
36
+      print *, "Error: Vertex shader compilation failed:"
37
+      print *, trim(log_buffer)
38
+      call glDeleteShader(vert_id)
39
+      return
40
+    end if
41
+
42
+    ! Compile fragment shader
43
+    frag_id = glCreateShader(GL_FRAGMENT_SHADER)
44
+    call glShaderSource(frag_id, frag_source // c_null_char)
45
+    call glCompileShader(frag_id)
46
+
47
+    success = glGetShaderCompileStatus(frag_id)
48
+    if (success == 0) then
49
+      call glGetShaderInfoLog(frag_id, log_buffer, 512)
50
+      print *, "Error: Fragment shader compilation failed:"
51
+      print *, trim(log_buffer)
52
+      call glDeleteShader(vert_id)
53
+      call glDeleteShader(frag_id)
54
+      return
55
+    end if
56
+
57
+    ! Link program
58
+    shader%program_id = glCreateProgram()
59
+    call glAttachShader(shader%program_id, vert_id)
60
+    call glAttachShader(shader%program_id, frag_id)
61
+    call glLinkProgram(shader%program_id)
62
+
63
+    success = glGetProgramLinkStatus(shader%program_id)
64
+    if (success == 0) then
65
+      call glGetProgramInfoLog(shader%program_id, log_buffer, 512)
66
+      print *, "Error: Shader program linking failed:"
67
+      print *, trim(log_buffer)
68
+      call glDeleteShader(vert_id)
69
+      call glDeleteShader(frag_id)
70
+      call glDeleteProgram(shader%program_id)
71
+      shader%program_id = 0
72
+      return
73
+    end if
74
+
75
+    ! Clean up shaders (they're linked into the program now)
76
+    call glDeleteShader(vert_id)
77
+    call glDeleteShader(frag_id)
78
+
79
+    ! Get uniform locations
80
+    shader%projection_loc = glGetUniformLocation(shader%program_id, &
81
+                                                  "u_projection" // c_null_char)
82
+    shader%atlas_loc = glGetUniformLocation(shader%program_id, &
83
+                                             "u_atlas" // c_null_char)
84
+
85
+    shader%valid = .true.
86
+
87
+  end function shader_create
88
+
89
+  ! Use this shader program
90
+  subroutine shader_use(shader)
91
+    type(shader_t), intent(in) :: shader
92
+    if (shader%valid) then
93
+      call glUseProgram(shader%program_id)
94
+    end if
95
+  end subroutine shader_use
96
+
97
+  ! Set the projection matrix uniform
98
+  subroutine shader_set_projection(shader, matrix)
99
+    type(shader_t), intent(in) :: shader
100
+    real(c_float), intent(in) :: matrix(4,4)
101
+
102
+    if (shader%valid .and. shader%projection_loc >= 0) then
103
+      call glUniformMatrix4fv(shader%projection_loc, 1, GL_FALSE, matrix)
104
+    end if
105
+  end subroutine shader_set_projection
106
+
107
+  ! Set an integer uniform
108
+  subroutine shader_set_int(shader, name, value)
109
+    type(shader_t), intent(in) :: shader
110
+    character(len=*), intent(in) :: name
111
+    integer, intent(in) :: value
112
+    integer :: loc
113
+
114
+    if (shader%valid) then
115
+      loc = glGetUniformLocation(shader%program_id, name // c_null_char)
116
+      if (loc >= 0) then
117
+        call glUniform1i(loc, value)
118
+      end if
119
+    end if
120
+  end subroutine shader_set_int
121
+
122
+  ! Destroy shader program
123
+  subroutine shader_destroy(shader)
124
+    type(shader_t), intent(inout) :: shader
125
+
126
+    if (shader%program_id > 0) then
127
+      call glDeleteProgram(shader%program_id)
128
+      shader%program_id = 0
129
+    end if
130
+    shader%valid = .false.
131
+  end subroutine shader_destroy
132
+
133
+end module shader_mod
src/text/atlas.f90added
@@ -0,0 +1,161 @@
1
+module atlas_mod
2
+  use, intrinsic :: iso_c_binding
3
+  use glyph_mod
4
+  use font_mod
5
+  use gl_bindings
6
+  implicit none
7
+  private
8
+
9
+  public :: atlas_t
10
+  public :: atlas_create, atlas_destroy, atlas_get_glyph
11
+
12
+  integer, parameter :: ATLAS_SIZE = 512
13
+  integer, parameter :: ASCII_START = 32
14
+  integer, parameter :: ASCII_END = 126
15
+
16
+  type :: atlas_t
17
+    integer :: texture_id = 0
18
+    integer :: width = ATLAS_SIZE
19
+    integer :: height = ATLAS_SIZE
20
+    integer :: cursor_x = 0
21
+    integer :: cursor_y = 0
22
+    integer :: row_height = 0
23
+    type(glyph_t) :: glyphs(0:127)
24
+    logical :: initialized = .false.
25
+  end type atlas_t
26
+
27
+contains
28
+
29
+  ! Create a texture atlas from a font, pre-rendering ASCII characters
30
+  function atlas_create(font) result(atlas)
31
+    type(font_t), intent(inout) :: font
32
+    type(atlas_t) :: atlas
33
+    integer :: tex(1)
34
+    integer :: cp
35
+    type(glyph_t) :: g
36
+    type(c_ptr) :: bitmap_ptr
37
+
38
+    if (.not. font%loaded) then
39
+      print *, "Error: Cannot create atlas from unloaded font"
40
+      return
41
+    end if
42
+
43
+    ! Create OpenGL texture
44
+    call glGenTextures(1, tex)
45
+    atlas%texture_id = tex(1)
46
+    call glBindTexture(GL_TEXTURE_2D, atlas%texture_id)
47
+
48
+    ! Set texture parameters
49
+    call glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
50
+    call glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
51
+    call glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
52
+    call glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
53
+
54
+    ! Disable byte-alignment restriction for grayscale textures
55
+    call glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
56
+
57
+    ! Allocate empty texture
58
+    call glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, &
59
+                      atlas%width, atlas%height, 0, &
60
+                      GL_RED, GL_UNSIGNED_BYTE, c_null_ptr)
61
+
62
+    ! Initialize glyph array
63
+    do cp = 0, 127
64
+      call glyph_init(atlas%glyphs(cp))
65
+    end do
66
+
67
+    ! Pre-render ASCII printable characters (32-126)
68
+    atlas%cursor_x = 1
69
+    atlas%cursor_y = 1
70
+    atlas%row_height = 0
71
+
72
+    do cp = ASCII_START, ASCII_END
73
+      g = font_render_glyph(font, cp, bitmap_ptr)
74
+      if (g%valid) then
75
+        call atlas_add_glyph(atlas, g, bitmap_ptr)
76
+        atlas%glyphs(cp) = g
77
+      end if
78
+    end do
79
+
80
+    ! Unbind texture
81
+    call glBindTexture(GL_TEXTURE_2D, 0)
82
+
83
+    atlas%initialized = .true.
84
+
85
+  end function atlas_create
86
+
87
+  ! Add a rendered glyph to the atlas
88
+  subroutine atlas_add_glyph(atlas, g, bitmap_ptr)
89
+    type(atlas_t), intent(inout) :: atlas
90
+    type(glyph_t), intent(inout) :: g
91
+    type(c_ptr), intent(in) :: bitmap_ptr
92
+    integer :: padding
93
+
94
+    padding = 1  ! 1 pixel padding between glyphs
95
+
96
+    ! Check if glyph fits in current row
97
+    if (atlas%cursor_x + g%width + padding > atlas%width) then
98
+      ! Move to next row
99
+      atlas%cursor_x = 1
100
+      atlas%cursor_y = atlas%cursor_y + atlas%row_height + padding
101
+      atlas%row_height = 0
102
+    end if
103
+
104
+    ! Check if glyph fits vertically
105
+    if (atlas%cursor_y + g%height + padding > atlas%height) then
106
+      print *, "Warning: Atlas texture is full, cannot add glyph"
107
+      return
108
+    end if
109
+
110
+    ! Store position in atlas
111
+    g%tex_x = atlas%cursor_x
112
+    g%tex_y = atlas%cursor_y
113
+
114
+    ! Copy bitmap to texture
115
+    if (g%width > 0 .and. g%height > 0 .and. c_associated(bitmap_ptr)) then
116
+      call glTexSubImage2D(GL_TEXTURE_2D, 0, &
117
+                           g%tex_x, g%tex_y, g%width, g%height, &
118
+                           GL_RED, GL_UNSIGNED_BYTE, bitmap_ptr)
119
+    end if
120
+
121
+    ! Calculate UV coordinates (normalized 0-1)
122
+    g%u0 = real(g%tex_x, c_float) / real(atlas%width, c_float)
123
+    g%v0 = real(g%tex_y, c_float) / real(atlas%height, c_float)
124
+    g%u1 = real(g%tex_x + g%width, c_float) / real(atlas%width, c_float)
125
+    g%v1 = real(g%tex_y + g%height, c_float) / real(atlas%height, c_float)
126
+
127
+    ! Update cursor position
128
+    atlas%cursor_x = atlas%cursor_x + g%width + padding
129
+    atlas%row_height = max(atlas%row_height, g%height)
130
+
131
+  end subroutine atlas_add_glyph
132
+
133
+  ! Get glyph information for a codepoint
134
+  function atlas_get_glyph(atlas, codepoint) result(g)
135
+    type(atlas_t), intent(in) :: atlas
136
+    integer, intent(in) :: codepoint
137
+    type(glyph_t) :: g
138
+
139
+    if (codepoint >= 0 .and. codepoint <= 127) then
140
+      g = atlas%glyphs(codepoint)
141
+    else
142
+      ! Return invalid glyph for unsupported characters
143
+      call glyph_init(g)
144
+    end if
145
+  end function atlas_get_glyph
146
+
147
+  ! Destroy atlas and free texture
148
+  subroutine atlas_destroy(atlas)
149
+    type(atlas_t), intent(inout) :: atlas
150
+    integer :: tex(1)
151
+
152
+    if (atlas%texture_id > 0) then
153
+      tex(1) = atlas%texture_id
154
+      call glDeleteTextures(1, tex)
155
+      atlas%texture_id = 0
156
+    end if
157
+
158
+    atlas%initialized = .false.
159
+  end subroutine atlas_destroy
160
+
161
+end module atlas_mod
src/text/renderer.f90added
@@ -0,0 +1,331 @@
1
+module renderer_mod
2
+  use, intrinsic :: iso_c_binding
3
+  use gl_bindings
4
+  use glyph_mod
5
+  use font_mod
6
+  use atlas_mod
7
+  use shader_mod
8
+  implicit none
9
+  private
10
+
11
+  public :: renderer_t
12
+  public :: renderer_create, renderer_destroy
13
+  public :: renderer_begin, renderer_draw_char, renderer_draw_string, renderer_flush
14
+  public :: renderer_set_projection
15
+
16
+  ! Vertex format: position(2) + texcoord(2) + color(4) = 8 floats per vertex
17
+  integer, parameter :: FLOATS_PER_VERTEX = 8
18
+  integer, parameter :: VERTICES_PER_QUAD = 6
19
+  integer, parameter :: FLOATS_PER_QUAD = FLOATS_PER_VERTEX * VERTICES_PER_QUAD
20
+  integer, parameter :: MAX_QUADS = 4096
21
+  integer, parameter :: MAX_VERTICES = MAX_QUADS * VERTICES_PER_QUAD
22
+  integer, parameter :: BUFFER_SIZE = MAX_VERTICES * FLOATS_PER_VERTEX
23
+
24
+  type :: renderer_t
25
+    integer :: vao = 0
26
+    integer :: vbo = 0
27
+    real(c_float), allocatable :: vertices(:)
28
+    integer :: vertex_count = 0
29
+    type(shader_t) :: shader
30
+    type(atlas_t) :: atlas
31
+    type(font_t) :: font
32
+    real(c_float) :: projection(4,4)
33
+    logical :: initialized = .false.
34
+  end type renderer_t
35
+
36
+  ! Embedded shader sources
37
+  character(len=*), parameter :: VERT_SHADER = &
38
+    "#version 330 core" // c_new_line // &
39
+    "layout(location = 0) in vec2 a_position;" // c_new_line // &
40
+    "layout(location = 1) in vec2 a_texcoord;" // c_new_line // &
41
+    "layout(location = 2) in vec4 a_color;" // c_new_line // &
42
+    "uniform mat4 u_projection;" // c_new_line // &
43
+    "out vec2 v_texcoord;" // c_new_line // &
44
+    "out vec4 v_color;" // c_new_line // &
45
+    "void main() {" // c_new_line // &
46
+    "    gl_Position = u_projection * vec4(a_position, 0.0, 1.0);" // c_new_line // &
47
+    "    v_texcoord = a_texcoord;" // c_new_line // &
48
+    "    v_color = a_color;" // c_new_line // &
49
+    "}"
50
+
51
+  character(len=*), parameter :: FRAG_SHADER = &
52
+    "#version 330 core" // c_new_line // &
53
+    "in vec2 v_texcoord;" // c_new_line // &
54
+    "in vec4 v_color;" // c_new_line // &
55
+    "uniform sampler2D u_atlas;" // c_new_line // &
56
+    "out vec4 frag_color;" // c_new_line // &
57
+    "void main() {" // c_new_line // &
58
+    "    float alpha = texture(u_atlas, v_texcoord).r;" // c_new_line // &
59
+    "    frag_color = vec4(v_color.rgb, v_color.a * alpha);" // c_new_line // &
60
+    "}"
61
+
62
+contains
63
+
64
+  ! Create renderer with font
65
+  function renderer_create(font_path, font_size) result(r)
66
+    character(len=*), intent(in) :: font_path
67
+    integer, intent(in) :: font_size
68
+    type(renderer_t) :: r
69
+    integer :: vao(1), vbo(1)
70
+    integer(c_size_t) :: stride, offset
71
+
72
+    ! Load font
73
+    r%font = font_load(font_path, font_size)
74
+    if (.not. r%font%loaded) then
75
+      print *, "Error: Failed to load font"
76
+      return
77
+    end if
78
+
79
+    ! Create atlas from font
80
+    r%atlas = atlas_create(r%font)
81
+    if (.not. r%atlas%initialized) then
82
+      print *, "Error: Failed to create atlas"
83
+      call font_destroy(r%font)
84
+      return
85
+    end if
86
+
87
+    ! Create shader
88
+    r%shader = shader_create(VERT_SHADER, FRAG_SHADER)
89
+    if (.not. r%shader%valid) then
90
+      print *, "Error: Failed to create shader"
91
+      call atlas_destroy(r%atlas)
92
+      call font_destroy(r%font)
93
+      return
94
+    end if
95
+
96
+    ! Create VAO and VBO
97
+    call glGenVertexArrays(1, vao)
98
+    call glGenBuffers(1, vbo)
99
+    r%vao = vao(1)
100
+    r%vbo = vbo(1)
101
+
102
+    call glBindVertexArray(r%vao)
103
+    call glBindBuffer(GL_ARRAY_BUFFER, r%vbo)
104
+
105
+    ! Allocate buffer (empty for now)
106
+    call glBufferData(GL_ARRAY_BUFFER, &
107
+                      int(BUFFER_SIZE * c_sizeof(1.0_c_float), c_size_t), &
108
+                      c_null_ptr, GL_DYNAMIC_DRAW)
109
+
110
+    ! Set up vertex attributes
111
+    stride = int(FLOATS_PER_VERTEX * c_sizeof(1.0_c_float), c_size_t)
112
+
113
+    ! Position: location 0, 2 floats, offset 0
114
+    offset = 0_c_size_t
115
+    call glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, int(stride, c_int), offset)
116
+    call glEnableVertexAttribArray(0)
117
+
118
+    ! Texcoord: location 1, 2 floats, offset 8 bytes
119
+    offset = int(2 * c_sizeof(1.0_c_float), c_size_t)
120
+    call glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, int(stride, c_int), offset)
121
+    call glEnableVertexAttribArray(1)
122
+
123
+    ! Color: location 2, 4 floats, offset 16 bytes
124
+    offset = int(4 * c_sizeof(1.0_c_float), c_size_t)
125
+    call glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, int(stride, c_int), offset)
126
+    call glEnableVertexAttribArray(2)
127
+
128
+    call glBindVertexArray(0)
129
+
130
+    ! Allocate vertex buffer
131
+    allocate(r%vertices(BUFFER_SIZE))
132
+    r%vertices = 0.0_c_float
133
+    r%vertex_count = 0
134
+
135
+    ! Initialize projection to identity
136
+    r%projection = 0.0_c_float
137
+    r%projection(1,1) = 1.0_c_float
138
+    r%projection(2,2) = 1.0_c_float
139
+    r%projection(3,3) = 1.0_c_float
140
+    r%projection(4,4) = 1.0_c_float
141
+
142
+    r%initialized = .true.
143
+
144
+  end function renderer_create
145
+
146
+  ! Destroy renderer
147
+  subroutine renderer_destroy(r)
148
+    type(renderer_t), intent(inout) :: r
149
+    integer :: arr(1)
150
+
151
+    if (r%vbo > 0) then
152
+      arr(1) = r%vbo
153
+      call glDeleteBuffers(1, arr)
154
+      r%vbo = 0
155
+    end if
156
+
157
+    if (r%vao > 0) then
158
+      arr(1) = r%vao
159
+      call glDeleteVertexArrays(1, arr)
160
+      r%vao = 0
161
+    end if
162
+
163
+    call shader_destroy(r%shader)
164
+    call atlas_destroy(r%atlas)
165
+    call font_destroy(r%font)
166
+
167
+    if (allocated(r%vertices)) deallocate(r%vertices)
168
+    r%initialized = .false.
169
+  end subroutine renderer_destroy
170
+
171
+  ! Begin a new frame (reset vertex buffer)
172
+  subroutine renderer_begin(r)
173
+    type(renderer_t), intent(inout) :: r
174
+    r%vertex_count = 0
175
+  end subroutine renderer_begin
176
+
177
+  ! Set orthographic projection matrix
178
+  subroutine renderer_set_projection(r, width, height)
179
+    type(renderer_t), intent(inout) :: r
180
+    integer, intent(in) :: width, height
181
+
182
+    ! Orthographic projection: (0,0) top-left, (width, height) bottom-right
183
+    r%projection = 0.0_c_float
184
+    r%projection(1,1) = 2.0_c_float / real(width, c_float)
185
+    r%projection(2,2) = -2.0_c_float / real(height, c_float)  ! Flip Y
186
+    r%projection(3,3) = -1.0_c_float
187
+    r%projection(4,4) = 1.0_c_float
188
+    r%projection(1,4) = -1.0_c_float
189
+    r%projection(2,4) = 1.0_c_float
190
+  end subroutine renderer_set_projection
191
+
192
+  ! Draw a single character at position (x, y)
193
+  subroutine renderer_draw_char(r, x, y, codepoint, red, green, blue, alpha)
194
+    type(renderer_t), intent(inout) :: r
195
+    real, intent(in) :: x, y
196
+    integer, intent(in) :: codepoint
197
+    real, intent(in) :: red, green, blue, alpha
198
+    type(glyph_t) :: g
199
+    real(c_float) :: x0, y0, x1, y1
200
+    real(c_float) :: u0, v0, u1, v1
201
+    integer :: base
202
+
203
+    if (.not. r%initialized) return
204
+
205
+    g = atlas_get_glyph(r%atlas, codepoint)
206
+    if (.not. g%valid) return
207
+
208
+    ! Check if buffer is full
209
+    if (r%vertex_count + VERTICES_PER_QUAD > MAX_VERTICES) then
210
+      call renderer_flush(r)
211
+    end if
212
+
213
+    ! Calculate quad positions
214
+    ! x, y is the cursor position (baseline)
215
+    x0 = real(x, c_float) + real(g%bearing_x, c_float)
216
+    y0 = real(y, c_float) - real(g%bearing_y, c_float)
217
+    x1 = x0 + real(g%width, c_float)
218
+    y1 = y0 + real(g%height, c_float)
219
+
220
+    u0 = g%u0
221
+    v0 = g%v0
222
+    u1 = g%u1
223
+    v1 = g%v1
224
+
225
+    ! Add 6 vertices (2 triangles) for the quad
226
+    base = r%vertex_count * FLOATS_PER_VERTEX + 1
227
+
228
+    ! Triangle 1: top-left, bottom-left, bottom-right
229
+    ! Vertex 1: top-left
230
+    r%vertices(base:base+7) = [x0, y0, u0, v0, &
231
+                               real(red,c_float), real(green,c_float), &
232
+                               real(blue,c_float), real(alpha,c_float)]
233
+    base = base + 8
234
+
235
+    ! Vertex 2: bottom-left
236
+    r%vertices(base:base+7) = [x0, y1, u0, v1, &
237
+                               real(red,c_float), real(green,c_float), &
238
+                               real(blue,c_float), real(alpha,c_float)]
239
+    base = base + 8
240
+
241
+    ! Vertex 3: bottom-right
242
+    r%vertices(base:base+7) = [x1, y1, u1, v1, &
243
+                               real(red,c_float), real(green,c_float), &
244
+                               real(blue,c_float), real(alpha,c_float)]
245
+    base = base + 8
246
+
247
+    ! Triangle 2: top-left, bottom-right, top-right
248
+    ! Vertex 4: top-left
249
+    r%vertices(base:base+7) = [x0, y0, u0, v0, &
250
+                               real(red,c_float), real(green,c_float), &
251
+                               real(blue,c_float), real(alpha,c_float)]
252
+    base = base + 8
253
+
254
+    ! Vertex 5: bottom-right
255
+    r%vertices(base:base+7) = [x1, y1, u1, v1, &
256
+                               real(red,c_float), real(green,c_float), &
257
+                               real(blue,c_float), real(alpha,c_float)]
258
+    base = base + 8
259
+
260
+    ! Vertex 6: top-right
261
+    r%vertices(base:base+7) = [x1, y0, u1, v0, &
262
+                               real(red,c_float), real(green,c_float), &
263
+                               real(blue,c_float), real(alpha,c_float)]
264
+
265
+    r%vertex_count = r%vertex_count + VERTICES_PER_QUAD
266
+
267
+  end subroutine renderer_draw_char
268
+
269
+  ! Draw a string at position (x, y)
270
+  subroutine renderer_draw_string(r, x, y, text, red, green, blue, alpha)
271
+    type(renderer_t), intent(inout) :: r
272
+    real, intent(in) :: x, y
273
+    character(len=*), intent(in) :: text
274
+    real, intent(in) :: red, green, blue, alpha
275
+    integer :: i, cp
276
+    real :: cursor_x
277
+    type(glyph_t) :: g
278
+
279
+    cursor_x = x
280
+    do i = 1, len_trim(text)
281
+      cp = ichar(text(i:i))
282
+      call renderer_draw_char(r, cursor_x, y, cp, red, green, blue, alpha)
283
+
284
+      ! Advance cursor
285
+      g = atlas_get_glyph(r%atlas, cp)
286
+      if (g%valid) then
287
+        cursor_x = cursor_x + real(g%advance)
288
+      end if
289
+    end do
290
+  end subroutine renderer_draw_string
291
+
292
+  ! Flush the vertex buffer to GPU and draw
293
+  subroutine renderer_flush(r)
294
+    type(renderer_t), intent(inout) :: r
295
+    integer(c_size_t) :: data_size
296
+
297
+    if (r%vertex_count == 0) return
298
+
299
+    ! Enable blending for text
300
+    call glEnable(GL_BLEND)
301
+    call glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
302
+
303
+    ! Bind shader and set uniforms
304
+    call shader_use(r%shader)
305
+    call shader_set_projection(r%shader, r%projection)
306
+
307
+    ! Bind texture
308
+    call glActiveTexture(GL_TEXTURE0)
309
+    call glBindTexture(GL_TEXTURE_2D, r%atlas%texture_id)
310
+    call glUniform1i(r%shader%atlas_loc, 0)
311
+
312
+    ! Upload vertex data
313
+    call glBindVertexArray(r%vao)
314
+    call glBindBuffer(GL_ARRAY_BUFFER, r%vbo)
315
+
316
+    data_size = int(r%vertex_count * FLOATS_PER_VERTEX * c_sizeof(1.0_c_float), c_size_t)
317
+    call glBufferSubData_floats(GL_ARRAY_BUFFER, 0_c_size_t, data_size, r%vertices)
318
+
319
+    ! Draw
320
+    call glDrawArrays(GL_TRIANGLES, 0, r%vertex_count)
321
+
322
+    ! Unbind
323
+    call glBindVertexArray(0)
324
+    call glBindTexture(GL_TEXTURE_2D, 0)
325
+
326
+    ! Reset vertex count
327
+    r%vertex_count = 0
328
+
329
+  end subroutine renderer_flush
330
+
331
+end module renderer_mod