package font import "core:mem" import "core:math" // ============================================================================ // SIMPLE FONT BAKING (Phase 2) // ============================================================================ // Get scaled vertical metrics for text layout // size > 0: pixel height, size < 0: em size (unscaled) get_scaled_font_vmetrics :: proc(fontdata: [^]u8, index: i32, size: f32, ascent: ^f32, descent: ^f32, line_gap: ^f32) { info: Font_Info offset := get_font_offset_for_index(fontdata, index) init_font(&info, fontdata, offset) scale: f32 if size > 0 { scale = scale_for_pixel_height(&info, size) } else { scale = scale_for_mapping_em_to_pixels(&info, -size) } i_ascent, i_descent, i_line_gap: i32 get_font_vmetrics(&info, &i_ascent, &i_descent, &i_line_gap) ascent^ = f32(i_ascent) * scale descent^ = f32(i_descent) * scale line_gap^ = f32(i_line_gap) * scale } // Bake a range of characters into a bitmap texture atlas // Returns bottom_y on success, or -i on failure where i is the character that didn't fit bake_font_bitmap :: proc(data: [^]u8, offset: i32, pixel_height: f32, pixels: [^]u8, pw: i32, ph: i32, first_char: i32, num_chars: i32, chardata: [^]Baked_Char) -> i32 { f: Font_Info f.userdata = nil if !init_font(&f, data, offset) { return -1 } // Clear bitmap to 0 (background) for i: i32 = 0; i < pw * ph; i += 1 { pixels[i] = 0 } x: i32 = 1 y: i32 = 1 bottom_y: i32 = 1 scale := scale_for_pixel_height(&f, pixel_height) for i: i32 = 0; i < num_chars; i += 1 { advance, lsb: i32 x0, y0, x1, y1: i32 g := find_glyph_index(&f, first_char + i) get_glyph_hmetrics(&f, g, &advance, &lsb) get_glyph_bitmap_box(&f, g, scale, scale, &x0, &y0, &x1, &y1) gw := x1 - x0 gh := y1 - y0 if x + gw + 1 >= pw { y = bottom_y x = 1 // advance to next row } if y + gh + 1 >= ph { // check if it fits vertically AFTER potentially moving to next row return -i } make_glyph_bitmap(&f, pixels[x + y * pw:], gw, gh, pw, scale, scale, g) chardata[i].x0 = u16(x) chardata[i].y0 = u16(y) chardata[i].x1 = u16(x + gw) chardata[i].y1 = u16(y + gh) chardata[i].xadvance = scale * f32(advance) chardata[i].xoff = f32(x0) chardata[i].yoff = f32(y0) x = x + gw + 1 if y + gh + 1 > bottom_y { bottom_y = y + gh + 1 } } return bottom_y } // Get screen and texture coordinates for rendering a baked character // opengl_fillrule: 1 for OpenGL/D3D10+, 0 for D3D9 get_baked_quad :: proc(chardata: [^]Baked_Char, pw: i32, ph: i32, char_index: i32, xpos: ^f32, ypos: ^f32, q: ^Aligned_Quad, opengl_fillrule: i32) { d3d_bias: f32 = 0 if opengl_fillrule != 0 else -0.5 ipw: f32 = 1.0 / f32(pw) iph: f32 = 1.0 / f32(ph) b := &chardata[char_index] round_x := ifloor((xpos^ + b.xoff) + 0.5) round_y := ifloor((ypos^ + b.yoff) + 0.5) q.x0 = f32(round_x) + d3d_bias q.y0 = f32(round_y) + d3d_bias q.x1 = f32(round_x) + f32(b.x1 - b.x0) + d3d_bias q.y1 = f32(round_y) + f32(b.y1 - b.y0) + d3d_bias q.s0 = f32(b.x0) * ipw q.t0 = f32(b.y0) * iph q.s1 = f32(b.x1) * ipw q.t1 = f32(b.y1) * iph xpos^ += b.xadvance } // ============================================================================ // ADVANCED FONT PACKING // ============================================================================ // Internal: mask for oversample circular buffer @(private) OVER_MASK :: MAX_OVERSAMPLE - 1 // Internal: Initialize rect packing target @(private) rp_init_target :: proc(ctx: ^Rp_Context, pw: i32, ph: i32, nodes: ^Rp_Node, num_nodes: i32) { ctx.width = pw ctx.height = ph ctx.x = 0 ctx.y = 0 ctx.bottom_y = 0 // nodes and num_nodes are unused in simple packer } // Internal: Pack rectangles using simple row-by-row algorithm @(private) rp_pack_rects :: proc(ctx: ^Rp_Context, rects: [^]Rp_Rect, num_rects: i32) { i: i32 for i = 0; i < num_rects; i += 1 { if ctx.x + rects[i].w > ctx.width { ctx.x = 0 ctx.y = ctx.bottom_y } if ctx.y + rects[i].h > ctx.height { break } rects[i].x = ctx.x rects[i].y = ctx.y rects[i].was_packed = 1 ctx.x += rects[i].w if ctx.y + rects[i].h > ctx.bottom_y { ctx.bottom_y = ctx.y + rects[i].h } } for ; i < num_rects; i += 1 { rects[i].was_packed = 0 } } // Internal: Calculate oversample phase shift @(private) oversample_shift :: proc(oversample: i32) -> f32 { if oversample == 0 { return 0.0 } return f32(-(oversample - 1)) / (2.0 * f32(oversample)) } // Internal: Horizontal prefilter for oversampling @(private) h_prefilter :: proc(pixels: [^]u8, w: i32, h: i32, stride_in_bytes: i32, kernel_width: u32) { buffer: [MAX_OVERSAMPLE]u8 safe_w := w - i32(kernel_width) for j: i32 = 0; j < h; j += 1 { for b: u32 = 0; b < kernel_width; b += 1 { buffer[b] = 0 } total: u32 = 0 row := pixels[j * stride_in_bytes:] // Process with specific kernel width divisions for optimization switch kernel_width { case 2: #no_bounds_check for i: i32 = 0; i <= safe_w; i += 1 { total += u32(row[i]) - u32(buffer[u32(i) & OVER_MASK]) buffer[(u32(i) + kernel_width) & OVER_MASK] = row[i] row[i] = u8(total / 2) } #no_bounds_check for i := safe_w + 1; i < w; i += 1 { total -= u32(buffer[u32(i) & OVER_MASK]) row[i] = u8(total / 2) } case 3: #no_bounds_check for i: i32 = 0; i <= safe_w; i += 1 { total += u32(row[i]) - u32(buffer[u32(i) & OVER_MASK]) buffer[(u32(i) + kernel_width) & OVER_MASK] = row[i] row[i] = u8(total / 3) } #no_bounds_check for i := safe_w + 1; i < w; i += 1 { total -= u32(buffer[u32(i) & OVER_MASK]) row[i] = u8(total / 3) } case 4: #no_bounds_check for i: i32 = 0; i <= safe_w; i += 1 { total += u32(row[i]) - u32(buffer[u32(i) & OVER_MASK]) buffer[(u32(i) + kernel_width) & OVER_MASK] = row[i] row[i] = u8(total / 4) } #no_bounds_check for i := safe_w + 1; i < w; i += 1 { total -= u32(buffer[u32(i) & OVER_MASK]) row[i] = u8(total / 4) } case 5: #no_bounds_check for i: i32 = 0; i <= safe_w; i += 1 { total += u32(row[i]) - u32(buffer[u32(i) & OVER_MASK]) buffer[(u32(i) + kernel_width) & OVER_MASK] = row[i] row[i] = u8(total / 5) } #no_bounds_check for i := safe_w + 1; i < w; i += 1 { total -= u32(buffer[u32(i) & OVER_MASK]) row[i] = u8(total / 5) } case: #no_bounds_check for i: i32 = 0; i <= safe_w; i += 1 { total += u32(row[i]) - u32(buffer[u32(i) & OVER_MASK]) buffer[(u32(i) + kernel_width) & OVER_MASK] = row[i] row[i] = u8(total / kernel_width) } #no_bounds_check for i := safe_w + 1; i < w; i += 1 { total -= u32(buffer[u32(i) & OVER_MASK]) row[i] = u8(total / kernel_width) } } } } // Internal: Vertical prefilter for oversampling @(private) v_prefilter :: proc(pixels: [^]u8, w: i32, h: i32, stride_in_bytes: i32, kernel_width: u32) { buffer: [MAX_OVERSAMPLE]u8 safe_h := h - i32(kernel_width) for j: i32 = 0; j < w; j += 1 { for b: u32 = 0; b < kernel_width; b += 1 { buffer[b] = 0 } total: u32 = 0 col := pixels[j:] switch kernel_width { case 2: #no_bounds_check for i: i32 = 0; i <= safe_h; i += 1 { total += u32(col[i * stride_in_bytes]) - u32(buffer[u32(i) & OVER_MASK]) buffer[(u32(i) + kernel_width) & OVER_MASK] = col[i * stride_in_bytes] col[i * stride_in_bytes] = u8(total / 2) } #no_bounds_check for i := safe_h + 1; i < h; i += 1 { total -= u32(buffer[u32(i) & OVER_MASK]) col[i * stride_in_bytes] = u8(total / 2) } case 3: #no_bounds_check for i: i32 = 0; i <= safe_h; i += 1 { total += u32(col[i * stride_in_bytes]) - u32(buffer[u32(i) & OVER_MASK]) buffer[(u32(i) + kernel_width) & OVER_MASK] = col[i * stride_in_bytes] col[i * stride_in_bytes] = u8(total / 3) } #no_bounds_check for i := safe_h + 1; i < h; i += 1 { total -= u32(buffer[u32(i) & OVER_MASK]) col[i * stride_in_bytes] = u8(total / 3) } case 4: #no_bounds_check for i: i32 = 0; i <= safe_h; i += 1 { total += u32(col[i * stride_in_bytes]) - u32(buffer[u32(i) & OVER_MASK]) buffer[(u32(i) + kernel_width) & OVER_MASK] = col[i * stride_in_bytes] col[i * stride_in_bytes] = u8(total / 4) } #no_bounds_check for i := safe_h + 1; i < h; i += 1 { total -= u32(buffer[u32(i) & OVER_MASK]) col[i * stride_in_bytes] = u8(total / 4) } case 5: #no_bounds_check for i: i32 = 0; i <= safe_h; i += 1 { total += u32(col[i * stride_in_bytes]) - u32(buffer[u32(i) & OVER_MASK]) buffer[(u32(i) + kernel_width) & OVER_MASK] = col[i * stride_in_bytes] col[i * stride_in_bytes] = u8(total / 5) } #no_bounds_check for i := safe_h + 1; i < h; i += 1 { total -= u32(buffer[u32(i) & OVER_MASK]) col[i * stride_in_bytes] = u8(total / 5) } case: #no_bounds_check for i: i32 = 0; i <= safe_h; i += 1 { total += u32(col[i * stride_in_bytes]) - u32(buffer[u32(i) & OVER_MASK]) buffer[(u32(i) + kernel_width) & OVER_MASK] = col[i * stride_in_bytes] col[i * stride_in_bytes] = u8(total / kernel_width) } #no_bounds_check for i := safe_h + 1; i < h; i += 1 { total -= u32(buffer[u32(i) & OVER_MASK]) col[i * stride_in_bytes] = u8(total / kernel_width) } } } } // Initialize a packing context // Returns 1 on success, 0 on allocation failure pack_begin :: proc(spc: ^Pack_Context, pixels: [^]u8, pw: i32, ph: i32, stride_in_bytes: i32, padding: i32, alloc_context: rawptr = nil) -> i32 { num_nodes := pw - padding context_ptr, _ := mem.alloc(size_of(Rp_Context)) nodes_ptr, _ := mem.alloc(int(num_nodes) * size_of(Rp_Node)) if context_ptr == nil || nodes_ptr == nil { if context_ptr != nil { mem.free(context_ptr) } if nodes_ptr != nil { mem.free(nodes_ptr) } return 0 } spc.user_allocator_context = alloc_context spc.width = pw spc.height = ph spc.pixels = pixels spc.pack_info = cast(^Rp_Context)context_ptr spc.nodes = cast(^Rp_Node)nodes_ptr spc.padding = padding spc.stride_in_bytes = stride_in_bytes if stride_in_bytes != 0 else pw spc.h_oversample = 1 spc.v_oversample = 1 spc.skip_missing = 0 rp_init_target(spc.pack_info, pw - padding, ph - padding, spc.nodes, num_nodes) if pixels != nil { for i: i32 = 0; i < pw * ph; i += 1 { pixels[i] = 0 } } return 1 } // Clean up packing context pack_end :: proc(spc: ^Pack_Context) { mem.free(spc.nodes) mem.free(spc.pack_info) } // Set oversampling (must call before packing) pack_set_oversampling :: proc(spc: ^Pack_Context, h_oversample: u32, v_oversample: u32) { if h_oversample <= MAX_OVERSAMPLE { spc.h_oversample = h_oversample } if v_oversample <= MAX_OVERSAMPLE { spc.v_oversample = v_oversample } } // Set skip missing codepoints behavior pack_set_skip_missing_codepoints :: proc(spc: ^Pack_Context, skip: i32) { spc.skip_missing = skip } // Gather rectangle sizes for all characters across all ranges // Returns number of rects filled pack_font_ranges_gather_rects :: proc(spc: ^Pack_Context, info: ^Font_Info, ranges: [^]Pack_Range, num_ranges: i32, rects: [^]Rp_Rect) -> i32 { missing_glyph_added := false k: i32 = 0 for i: i32 = 0; i < num_ranges; i += 1 { fh := ranges[i].font_size scale := scale_for_pixel_height(info, fh) if fh > 0 else scale_for_mapping_em_to_pixels(info, -fh) ranges[i].h_oversample = u8(spc.h_oversample) ranges[i].v_oversample = u8(spc.v_oversample) for j: i32 = 0; j < ranges[i].num_chars; j += 1 { codepoint := ranges[i].first_unicode_codepoint_in_range + j if ranges[i].array_of_unicode_codepoints == nil else ranges[i].array_of_unicode_codepoints[j] glyph := find_glyph_index(info, codepoint) if glyph == 0 && (spc.skip_missing != 0 || missing_glyph_added) { rects[k].w = 0 rects[k].h = 0 } else { x0, y0, x1, y1: i32 get_glyph_bitmap_box_subpixel(info, glyph, scale * f32(spc.h_oversample), scale * f32(spc.v_oversample), 0, 0, &x0, &y0, &x1, &y1) rects[k].w = (x1 - x0) + spc.padding + i32(spc.h_oversample) - 1 rects[k].h = (y1 - y0) + spc.padding + i32(spc.v_oversample) - 1 if glyph == 0 { missing_glyph_added = true } } k += 1 } } return k } // Pack rectangles into atlas pack_font_ranges_pack_rects :: proc(spc: ^Pack_Context, rects: [^]Rp_Rect, num_rects: i32) { rp_pack_rects(spc.pack_info, rects, num_rects) } // Render glyphs into packed positions // Returns 1 on success, 0 if any character failed pack_font_ranges_render_into_rects :: proc(spc: ^Pack_Context, info: ^Font_Info, ranges: [^]Pack_Range, num_ranges: i32, rects: [^]Rp_Rect) -> i32 { missing_glyph: i32 = -1 return_value: i32 = 1 // Save current values old_h_over := spc.h_oversample old_v_over := spc.v_oversample k: i32 = 0 for i: i32 = 0; i < num_ranges; i += 1 { fh := ranges[i].font_size scale := scale_for_pixel_height(info, fh) if fh > 0 else scale_for_mapping_em_to_pixels(info, -fh) spc.h_oversample = u32(ranges[i].h_oversample) spc.v_oversample = u32(ranges[i].v_oversample) recip_h := 1.0 / f32(spc.h_oversample) recip_v := 1.0 / f32(spc.v_oversample) sub_x := oversample_shift(i32(spc.h_oversample)) sub_y := oversample_shift(i32(spc.v_oversample)) for j: i32 = 0; j < ranges[i].num_chars; j += 1 { r := &rects[k] if r.was_packed != 0 && r.w != 0 && r.h != 0 { bc := &ranges[i].chardata_for_range[j] advance, lsb: i32 x0, y0, x1, y1: i32 codepoint := ranges[i].first_unicode_codepoint_in_range + j if ranges[i].array_of_unicode_codepoints == nil else ranges[i].array_of_unicode_codepoints[j] glyph := find_glyph_index(info, codepoint) pad := spc.padding // Pad on left and top r.x += pad r.y += pad r.w -= pad r.h -= pad get_glyph_hmetrics(info, glyph, &advance, &lsb) get_glyph_bitmap_box(info, glyph, scale * f32(spc.h_oversample), scale * f32(spc.v_oversample), &x0, &y0, &x1, &y1) make_glyph_bitmap_subpixel(info, spc.pixels[r.x + r.y * spc.stride_in_bytes:], r.w - i32(spc.h_oversample) + 1, r.h - i32(spc.v_oversample) + 1, spc.stride_in_bytes, scale * f32(spc.h_oversample), scale * f32(spc.v_oversample), 0, 0, glyph) if spc.h_oversample > 1 { h_prefilter(spc.pixels[r.x + r.y * spc.stride_in_bytes:], r.w, r.h, spc.stride_in_bytes, spc.h_oversample) } if spc.v_oversample > 1 { v_prefilter(spc.pixels[r.x + r.y * spc.stride_in_bytes:], r.w, r.h, spc.stride_in_bytes, spc.v_oversample) } bc.x0 = u16(r.x) bc.y0 = u16(r.y) bc.x1 = u16(r.x + r.w) bc.y1 = u16(r.y + r.h) bc.xadvance = scale * f32(advance) bc.xoff = f32(x0) * recip_h + sub_x bc.yoff = f32(y0) * recip_v + sub_y bc.xoff2 = f32(x0 + r.w) * recip_h + sub_x bc.yoff2 = f32(y0 + r.h) * recip_v + sub_y if glyph == 0 { missing_glyph = j } } else if spc.skip_missing != 0 { return_value = 0 } else if r.was_packed != 0 && r.w == 0 && r.h == 0 && missing_glyph >= 0 { ranges[i].chardata_for_range[j] = ranges[i].chardata_for_range[missing_glyph] } else { return_value = 0 } k += 1 } } // Restore original values spc.h_oversample = old_h_over spc.v_oversample = old_v_over return return_value } // Pack multiple font ranges into the atlas // Returns 1 on success, 0 if any character failed to pack pack_font_ranges :: proc(spc: ^Pack_Context, fontdata: [^]u8, font_index: i32, ranges: [^]Pack_Range, num_ranges: i32) -> i32 { info: Font_Info return_value: i32 = 1 // Flag all characters as NOT packed for i: i32 = 0; i < num_ranges; i += 1 { for j: i32 = 0; j < ranges[i].num_chars; j += 1 { ranges[i].chardata_for_range[j].x0 = 0 ranges[i].chardata_for_range[j].y0 = 0 ranges[i].chardata_for_range[j].x1 = 0 ranges[i].chardata_for_range[j].y1 = 0 } } // Count total characters n: i32 = 0 for i: i32 = 0; i < num_ranges; i += 1 { n += ranges[i].num_chars } // Allocate rects rects_ptr, _ := mem.alloc(int(n) * size_of(Rp_Rect)) if rects_ptr == nil { return 0 } rects := cast([^]Rp_Rect)rects_ptr info.userdata = spc.user_allocator_context if !init_font(&info, fontdata, get_font_offset_for_index(fontdata, font_index)) { mem.free(rects_ptr) return 0 } n = pack_font_ranges_gather_rects(spc, &info, ranges, num_ranges, rects) pack_font_ranges_pack_rects(spc, rects, n) return_value = pack_font_ranges_render_into_rects(spc, &info, ranges, num_ranges, rects) mem.free(rects_ptr) return return_value } // Pack a single contiguous range of characters // Returns 1 on success, 0 on failure pack_font_range :: proc(spc: ^Pack_Context, fontdata: [^]u8, font_index: i32, font_size: f32, first_unicode_codepoint_in_range: i32, num_chars_in_range: i32, chardata_for_range: [^]Packed_Char) -> i32 { range := Pack_Range{ first_unicode_codepoint_in_range = first_unicode_codepoint_in_range, array_of_unicode_codepoints = nil, num_chars = num_chars_in_range, chardata_for_range = chardata_for_range, font_size = font_size, } return pack_font_ranges(spc, fontdata, font_index, &range, 1) } // Get rendering quad for a packed character // align_to_integer: non-zero to snap to pixel, 0 for subpixel positioning get_packed_quad :: proc(chardata: [^]Packed_Char, pw: i32, ph: i32, char_index: i32, xpos: ^f32, ypos: ^f32, q: ^Aligned_Quad, align_to_integer: i32) { ipw := 1.0 / f32(pw) iph := 1.0 / f32(ph) b := &chardata[char_index] if align_to_integer != 0 { x := f32(ifloor((xpos^ + b.xoff) + 0.5)) y := f32(ifloor((ypos^ + b.yoff) + 0.5)) q.x0 = x q.y0 = y q.x1 = x + b.xoff2 - b.xoff q.y1 = y + b.yoff2 - b.yoff } else { q.x0 = xpos^ + b.xoff q.y0 = ypos^ + b.yoff q.x1 = xpos^ + b.xoff2 q.y1 = ypos^ + b.yoff2 } q.s0 = f32(b.x0) * ipw q.t0 = f32(b.y0) * iph q.s1 = f32(b.x1) * ipw q.t1 = f32(b.y1) * iph xpos^ += b.xadvance }