| 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 = 1024 ! Larger for Unicode support |
| 13 | integer, parameter :: ASCII_START = 32 |
| 14 | integer, parameter :: ASCII_END = 126 |
| 15 | integer, parameter :: EXTENDED_CACHE_SIZE = 256 ! Cache for non-ASCII glyphs |
| 16 | |
| 17 | ! Entry for extended glyph cache (simple hash table) |
| 18 | type :: cache_entry_t |
| 19 | integer :: codepoint = 0 |
| 20 | type(glyph_t) :: glyph |
| 21 | logical :: used = .false. |
| 22 | end type cache_entry_t |
| 23 | |
| 24 | type :: atlas_t |
| 25 | integer :: texture_id = 0 |
| 26 | integer :: width = ATLAS_SIZE |
| 27 | integer :: height = ATLAS_SIZE |
| 28 | integer :: cursor_x = 0 |
| 29 | integer :: cursor_y = 0 |
| 30 | integer :: row_height = 0 |
| 31 | type(glyph_t) :: glyphs(0:127) ! ASCII cache |
| 32 | type(cache_entry_t) :: extended(EXTENDED_CACHE_SIZE) ! Extended glyph cache |
| 33 | type(font_t), pointer :: font => null() ! Reference for on-demand loading |
| 34 | logical :: initialized = .false. |
| 35 | end type atlas_t |
| 36 | |
| 37 | contains |
| 38 | |
| 39 | ! Create a texture atlas from a font, pre-rendering ASCII characters |
| 40 | function atlas_create(font) result(atlas) |
| 41 | type(font_t), intent(inout), target :: font |
| 42 | type(atlas_t) :: atlas |
| 43 | integer :: tex(1) |
| 44 | integer :: cp, i |
| 45 | type(glyph_t) :: g |
| 46 | type(c_ptr) :: bitmap_ptr |
| 47 | integer(c_int8_t), target :: white_pixel(1) |
| 48 | |
| 49 | if (.not. font%loaded) then |
| 50 | print *, "Error: Cannot create atlas from unloaded font" |
| 51 | return |
| 52 | end if |
| 53 | |
| 54 | ! Store font reference for on-demand glyph loading |
| 55 | atlas%font => font |
| 56 | |
| 57 | ! Create OpenGL texture |
| 58 | call glGenTextures(1, tex) |
| 59 | atlas%texture_id = tex(1) |
| 60 | call glBindTexture(GL_TEXTURE_2D, atlas%texture_id) |
| 61 | |
| 62 | ! Set texture parameters |
| 63 | call glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) |
| 64 | call glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) |
| 65 | call glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) |
| 66 | call glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) |
| 67 | |
| 68 | ! Disable byte-alignment restriction for grayscale textures |
| 69 | call glPixelStorei(GL_UNPACK_ALIGNMENT, 1) |
| 70 | |
| 71 | ! Allocate empty texture |
| 72 | call glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, & |
| 73 | atlas%width, atlas%height, 0, & |
| 74 | GL_RED, GL_UNSIGNED_BYTE, c_null_ptr) |
| 75 | |
| 76 | ! Add a solid white pixel at position (0,0) for solid rectangle rendering |
| 77 | ! This allows renderer_draw_rect to sample a non-transparent pixel |
| 78 | white_pixel(1) = -1_c_int8_t ! 255 as signed byte |
| 79 | call glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, & |
| 80 | GL_RED, GL_UNSIGNED_BYTE, c_loc(white_pixel)) |
| 81 | |
| 82 | ! Initialize glyph array |
| 83 | do cp = 0, 127 |
| 84 | call glyph_init(atlas%glyphs(cp)) |
| 85 | end do |
| 86 | |
| 87 | ! Initialize extended cache |
| 88 | do i = 1, EXTENDED_CACHE_SIZE |
| 89 | atlas%extended(i)%used = .false. |
| 90 | atlas%extended(i)%codepoint = 0 |
| 91 | call glyph_init(atlas%extended(i)%glyph) |
| 92 | end do |
| 93 | |
| 94 | ! Pre-render ASCII printable characters (32-126) |
| 95 | atlas%cursor_x = 1 |
| 96 | atlas%cursor_y = 1 |
| 97 | atlas%row_height = 0 |
| 98 | |
| 99 | do cp = ASCII_START, ASCII_END |
| 100 | g = font_render_glyph(font, cp, bitmap_ptr) |
| 101 | if (g%valid) then |
| 102 | call atlas_add_glyph(atlas, g, bitmap_ptr) |
| 103 | atlas%glyphs(cp) = g |
| 104 | end if |
| 105 | end do |
| 106 | |
| 107 | ! Unbind texture |
| 108 | call glBindTexture(GL_TEXTURE_2D, 0) |
| 109 | |
| 110 | atlas%initialized = .true. |
| 111 | |
| 112 | end function atlas_create |
| 113 | |
| 114 | ! Add a rendered glyph to the atlas |
| 115 | subroutine atlas_add_glyph(atlas, g, bitmap_ptr) |
| 116 | type(atlas_t), intent(inout) :: atlas |
| 117 | type(glyph_t), intent(inout) :: g |
| 118 | type(c_ptr), intent(in) :: bitmap_ptr |
| 119 | integer :: padding |
| 120 | |
| 121 | padding = 1 ! 1 pixel padding between glyphs |
| 122 | |
| 123 | ! Check if glyph fits in current row |
| 124 | if (atlas%cursor_x + g%width + padding > atlas%width) then |
| 125 | ! Move to next row |
| 126 | atlas%cursor_x = 1 |
| 127 | atlas%cursor_y = atlas%cursor_y + atlas%row_height + padding |
| 128 | atlas%row_height = 0 |
| 129 | end if |
| 130 | |
| 131 | ! Check if glyph fits vertically |
| 132 | if (atlas%cursor_y + g%height + padding > atlas%height) then |
| 133 | print *, "Warning: Atlas texture is full, cannot add glyph" |
| 134 | return |
| 135 | end if |
| 136 | |
| 137 | ! Store position in atlas |
| 138 | g%tex_x = atlas%cursor_x |
| 139 | g%tex_y = atlas%cursor_y |
| 140 | |
| 141 | ! Copy bitmap to texture |
| 142 | if (g%width > 0 .and. g%height > 0 .and. c_associated(bitmap_ptr)) then |
| 143 | call glTexSubImage2D(GL_TEXTURE_2D, 0, & |
| 144 | g%tex_x, g%tex_y, g%width, g%height, & |
| 145 | GL_RED, GL_UNSIGNED_BYTE, bitmap_ptr) |
| 146 | end if |
| 147 | |
| 148 | ! Calculate UV coordinates (normalized 0-1) |
| 149 | g%u0 = real(g%tex_x, c_float) / real(atlas%width, c_float) |
| 150 | g%v0 = real(g%tex_y, c_float) / real(atlas%height, c_float) |
| 151 | g%u1 = real(g%tex_x + g%width, c_float) / real(atlas%width, c_float) |
| 152 | g%v1 = real(g%tex_y + g%height, c_float) / real(atlas%height, c_float) |
| 153 | |
| 154 | ! Update cursor position |
| 155 | atlas%cursor_x = atlas%cursor_x + g%width + padding |
| 156 | atlas%row_height = max(atlas%row_height, g%height) |
| 157 | |
| 158 | end subroutine atlas_add_glyph |
| 159 | |
| 160 | ! Get glyph information for a codepoint (with on-demand loading for non-ASCII) |
| 161 | function atlas_get_glyph(atlas, codepoint) result(g) |
| 162 | type(atlas_t), intent(inout) :: atlas |
| 163 | integer, intent(in) :: codepoint |
| 164 | type(glyph_t) :: g |
| 165 | |
| 166 | if (codepoint >= 0 .and. codepoint <= 127) then |
| 167 | g = atlas%glyphs(codepoint) |
| 168 | else |
| 169 | ! Look up or load extended glyph |
| 170 | g = atlas_get_extended_glyph(atlas, codepoint) |
| 171 | end if |
| 172 | end function atlas_get_glyph |
| 173 | |
| 174 | ! Get or load an extended (non-ASCII) glyph |
| 175 | function atlas_get_extended_glyph(atlas, codepoint) result(g) |
| 176 | type(atlas_t), intent(inout) :: atlas |
| 177 | integer, intent(in) :: codepoint |
| 178 | type(glyph_t) :: g |
| 179 | integer :: hash_idx, probe, i |
| 180 | type(c_ptr) :: bitmap_ptr |
| 181 | logical :: used_fallback |
| 182 | |
| 183 | call glyph_init(g) |
| 184 | |
| 185 | ! Simple hash function |
| 186 | hash_idx = mod(codepoint, EXTENDED_CACHE_SIZE) + 1 |
| 187 | |
| 188 | ! Linear probe to find existing or empty slot |
| 189 | do i = 0, EXTENDED_CACHE_SIZE - 1 |
| 190 | probe = mod(hash_idx + i - 1, EXTENDED_CACHE_SIZE) + 1 |
| 191 | |
| 192 | if (.not. atlas%extended(probe)%used) then |
| 193 | ! Empty slot - load glyph here |
| 194 | call atlas_load_extended_glyph(atlas, codepoint, probe) |
| 195 | g = atlas%extended(probe)%glyph |
| 196 | return |
| 197 | else if (atlas%extended(probe)%codepoint == codepoint) then |
| 198 | ! Found cached glyph |
| 199 | g = atlas%extended(probe)%glyph |
| 200 | return |
| 201 | end if |
| 202 | end do |
| 203 | |
| 204 | ! Cache is full - just render without caching (fallback behavior) |
| 205 | if (associated(atlas%font)) then |
| 206 | g = font_render_glyph_with_fallback(atlas%font, codepoint, bitmap_ptr, used_fallback) |
| 207 | end if |
| 208 | |
| 209 | end function atlas_get_extended_glyph |
| 210 | |
| 211 | ! Load an extended glyph into the cache at specified slot |
| 212 | subroutine atlas_load_extended_glyph(atlas, codepoint, slot) |
| 213 | type(atlas_t), intent(inout) :: atlas |
| 214 | integer, intent(in) :: codepoint, slot |
| 215 | type(glyph_t) :: g |
| 216 | type(c_ptr) :: bitmap_ptr |
| 217 | logical :: used_fallback |
| 218 | |
| 219 | if (.not. associated(atlas%font)) return |
| 220 | |
| 221 | ! Render using fallback support |
| 222 | g = font_render_glyph_with_fallback(atlas%font, codepoint, bitmap_ptr, used_fallback) |
| 223 | |
| 224 | if (g%valid) then |
| 225 | ! Bind texture and add glyph |
| 226 | call glBindTexture(GL_TEXTURE_2D, atlas%texture_id) |
| 227 | call atlas_add_glyph(atlas, g, bitmap_ptr) |
| 228 | call glBindTexture(GL_TEXTURE_2D, 0) |
| 229 | |
| 230 | ! Store in cache |
| 231 | atlas%extended(slot)%codepoint = codepoint |
| 232 | atlas%extended(slot)%glyph = g |
| 233 | atlas%extended(slot)%used = .true. |
| 234 | end if |
| 235 | |
| 236 | end subroutine atlas_load_extended_glyph |
| 237 | |
| 238 | ! Destroy atlas and free texture |
| 239 | subroutine atlas_destroy(atlas) |
| 240 | type(atlas_t), intent(inout) :: atlas |
| 241 | integer :: tex(1) |
| 242 | |
| 243 | if (atlas%texture_id > 0) then |
| 244 | tex(1) = atlas%texture_id |
| 245 | call glDeleteTextures(1, tex) |
| 246 | atlas%texture_id = 0 |
| 247 | end if |
| 248 | |
| 249 | atlas%initialized = .false. |
| 250 | end subroutine atlas_destroy |
| 251 | |
| 252 | end module atlas_mod |
| 253 |