Harbor

branch main
showing the latest snapshot on main
packing.odin 17.5 KB · Plain text
packing.odin 0644 Raw
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
}