/* * ekp_kp.c - Knuth-Plass line breaking algorithm * * Copyright (C) 2024-2026 Kinney Zhang * SPDX-License-Identifier: GPL-3.0-or-later * * This file is part of emacs-kp, which is free software: you can * redistribute it and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * It is distributed WITHOUT ANY WARRANTY; see the GNU General Public * License (COPYING) for details. * * The heart of the system. O(n²) worst case, but with pruning and * parallel candidate evaluation, typically O(n·m) where m is avg line length. * * Key optimizations: * - Prefix sums for O(1) range queries * - Early termination when line too long * - Parallel demerits computation for large paragraphs */ #include "ekp_module.h" #include #include #include #include /* Global state */ ekp_state_t *ekp_global = NULL; /* Fitness classes */ #define FITNESS_TIGHT 0 #define FITNESS_DECENT 1 #define FITNESS_LOOSE 2 #define FITNESS_VERY_LOOSE 3 /* Infinite badness: capped at 10000 like TeX (and the Elisp engine). * NOT EKP_INFINITY: a badness-10000 line is terrible but still usable, * matching ekp--compute-badness in ekp.el exactly. */ #define EKP_BADNESS_INF 10000.0 /* Badness computation */ static inline double compute_badness(int64_t adjustment, int64_t flexibility) { if (adjustment == 0) return 0.0; if (flexibility <= 0) return EKP_BADNESS_INF; double ratio = (double)adjustment / flexibility; double badness = 100.0 * fabs(ratio * ratio * ratio); return badness > EKP_BADNESS_INF ? EKP_BADNESS_INF : badness; } /* Fitness classification */ static inline uint8_t compute_fitness(int64_t adjustment, int64_t flexibility) { if (flexibility <= 0) return FITNESS_DECENT; double ratio = (double)adjustment / flexibility; if (ratio < -0.5) return FITNESS_TIGHT; if (ratio < 0.5) return FITNESS_DECENT; if (ratio < 1.0) return FITNESS_LOOSE; return FITNESS_VERY_LOOSE; } /* Full demerits computation */ static inline double compute_demerits(double badness, int32_t penalty, uint8_t prev_fitness, uint8_t curr_fitness, bool end_hyphen, int prev_hyphen_count, int line_penalty, int fitness_penalty, int consec_hyphen_penalty) { /* Base: (line_penalty + badness)² */ double base = (line_penalty + badness); base = base * base; /* Add break penalty squared */ base += (double)penalty * penalty; /* Fitness incompatibility */ int delta = abs((int)prev_fitness - (int)curr_fitness); if (delta > 1) base += fitness_penalty; /* Consecutive hyphen penalty (quadratic growth) */ if (end_hyphen) { int count = prev_hyphen_count + 1; base += (double)consec_hyphen_penalty * count * count; } return base; } /* * Unified DP input structure for the array-based DP core. */ typedef struct { /* Prefix sum arrays */ const int32_t *ideal_prefix; const int32_t *min_prefix; const int32_t *max_prefix; /* Glue arrays (nullable) */ const int32_t *glue_ideals; const int32_t *glue_shrinks; const int32_t *glue_stretches; /* Hyphen info */ const int32_t *hyphen_positions; size_t hyphen_count; int32_t hyphen_width; /* Forbidden break positions (kinsoku, no-break spans): sorted gap * indices where a line may NOT end. Nullable. */ const int32_t *forbidden_positions; size_t forbidden_count; /* Right-edge protrusion (nullable, n+1): pixels the line's final * glyph may hang past the flush edge when breaking at gap k; * hyphen_protrude is the same for soft hyphens. */ const int32_t *tail_protrudes; int32_t hyphen_protrude; /* Space-box run widths (nullable, n+1 elements each): * lead_spaces[i] = width of space-box run starting at box i * trail_spaces[k] = width of space-box run ending at box k-1 * These runs are stripped by the renderer, so line metrics * exclude them (matching ekp.el). */ const int32_t *lead_spaces; const int32_t *trail_spaces; /* Dimensions */ size_t n; /* box count */ int32_t line_width; /* Width of line 0 (first-line indent support); equals line_width * when no indent is active. In the forward DP a line starts at * box 0 exactly when i == 0, so this needs no extra state. */ int32_t first_line_width; /* K-P parameters */ int line_penalty; int hyphen_penalty; int fitness_penalty; double last_line_ratio; int consec_hyphen_penalty; double last_line_short_penalty; /* Per-line flexibility for non-justify alignment (0 = justify): * widens max_w, so flexibility = max_w - ideal includes it. */ int32_t extra_stretch; /* Fixed final-pass emergency stretch, matching TeX's emergency pass. */ int32_t emergency_stretch; /* Two-pass strategy: strict K-P first; emergency transitions only * in the second pass (when no valid layout exists). */ bool allow_emergency; } dp_input_t; /* * Shared hyphen position check for dp_input_t */ static inline bool dp_is_hyphen(const dp_input_t *in, size_t pos) { if (!in->hyphen_positions || in->hyphen_count == 0) return false; /* Binary search in sorted positions */ size_t lo = 0; size_t hi = in->hyphen_count - 1; while (lo < hi) { size_t mid = lo + (hi - lo) / 2; if ((size_t)in->hyphen_positions[mid] < pos) lo = mid + 1; else hi = mid; } return (size_t)in->hyphen_positions[lo] == pos; } /* * Shared forbidden-break check for dp_input_t (binary search). */ static inline bool dp_is_forbidden(const dp_input_t *in, size_t pos) { if (!in->forbidden_positions || in->forbidden_count == 0) return false; size_t lo = 0; size_t hi = in->forbidden_count - 1; while (lo < hi) { size_t mid = lo + (hi - lo) / 2; if ((size_t)in->forbidden_positions[mid] < pos) lo = mid + 1; else hi = mid; } return (size_t)in->forbidden_positions[lo] == pos; } /* * Core DP algorithm - shared by both entry points * Processes position i, trying all end positions k. * Updates output arrays when better solutions found. */ typedef struct { bool present; double previous_demerits; size_t start; int previous_hyphens; int previous_lines; int64_t rest; bool end_hyphen; } dp_artificial_candidate_t; static inline void dp_remember_artificial( dp_artificial_candidate_t *candidate, size_t start, double previous_demerits, int previous_hyphens, int previous_lines, int64_t rest, bool end_hyphen) { if (candidate->present && candidate->previous_demerits <= previous_demerits) return; candidate->present = true; candidate->previous_demerits = previous_demerits; candidate->start = start; candidate->previous_hyphens = previous_hyphens; candidate->previous_lines = previous_lines; candidate->rest = rest; candidate->end_hyphen = end_hyphen; } static inline void dp_install_artificial( const dp_artificial_candidate_t *candidate, size_t k, double *demerits, int32_t *backptrs, int64_t *rest_pixels, uint8_t *fitness, int32_t *hyphen_counts, int32_t *line_counts) { demerits[k] = candidate->previous_demerits; backptrs[k] = (int32_t)candidate->start; rest_pixels[k] = candidate->rest; fitness[k] = FITNESS_TIGHT; hyphen_counts[k] = candidate->end_hyphen ? candidate->previous_hyphens + 1 : 0; line_counts[k] = candidate->previous_lines + 1; } static void dp_process_position( const dp_input_t *in, size_t i, /* Input state at position i */ double prev_dem, uint8_t prev_fit, int prev_hyph, int prev_lines, /* Output arrays */ double *demerits, int32_t *backptrs, int64_t *rest_pixels, uint8_t *fitness, int32_t *hyphen_counts, int32_t *line_counts, uint8_t *surviving_candidates, dp_artificial_candidate_t *artificial_candidates) { size_t n = in->n; /* Line 0 (i == 0) may have a different width: first-line indent */ int64_t line_width = (i == 0 && in->first_line_width > 0) ? in->first_line_width : in->line_width; /* Get leading glue for line starting at i */ int64_t lead_ideal = (in->glue_ideals && i < n) ? in->glue_ideals[i] : 0; int64_t lead_shrink = (in->glue_shrinks && i < n) ? in->glue_shrinks[i] : 0; int64_t lead_stretch = (in->glue_stretches && i < n) ? in->glue_stretches[i] : 0; int64_t lead_space = in->lead_spaces ? in->lead_spaces[i] : 0; /* Try extending to each position k > i */ for (size_t k = i + 1; k <= n; k++) { bool is_last = (k == n); /* Break forbidden here (kinsoku, no-break span): not a * candidate; keep extending the line. */ if (!is_last && dp_is_forbidden(in, k)) { continue; } bool is_single_box = (k == i + 1); bool end_hyphen = dp_is_hyphen(in, k - 1); int64_t hyph_w = end_hyphen ? in->hyphen_width : 0; /* Right-edge protrusion widens this candidate's target */ int64_t lw = line_width + (end_hyphen ? in->hyphen_protrude : (in->tail_protrudes ? in->tail_protrudes[k] : 0)); /* Line metrics from i to k, excluding leading glue and the * space-box runs the renderer strips (leading + trailing). */ int64_t raw_ideal = (int64_t)in->ideal_prefix[k] - in->ideal_prefix[i] - lead_ideal; int64_t space_w = lead_space + (in->trail_spaces ? in->trail_spaces[k] : 0); if (space_w > raw_ideal) space_w = raw_ideal; int64_t ideal = raw_ideal - space_w + hyph_w; int64_t min_w = (int64_t)in->min_prefix[k] - in->min_prefix[i] - (lead_ideal - lead_shrink) - space_w + hyph_w; int64_t max_w = (int64_t)in->max_prefix[k] - in->max_prefix[i] - (lead_ideal + lead_stretch) - space_w + hyph_w + in->extra_stretch; int64_t emergency_stretch = in->allow_emergency ? in->emergency_stretch : 0; int64_t effective_max_w = max_w + emergency_stretch; /* Too long? (last line is never shrunk below its ideal) */ if (min_w > lw || (is_last && ideal > lw)) { if (in->allow_emergency) dp_remember_artificial(&artificial_candidates[k], i, prev_dem, prev_hyph, prev_lines, lw - ideal, end_hyphen); break; /* No point trying longer lines */ } if (in->allow_emergency) surviving_candidates[k] = 1; /* Valid break? */ bool valid = (min_w <= lw && effective_max_w >= lw) || (is_last && ideal <= lw); if (!valid) { /* An underfull active path survives to later breakpoints. */ continue; } /* Compute demerits */ int64_t adjustment = lw - ideal; int64_t flexibility = (adjustment > 0) ? (max_w - ideal) : (ideal - min_w); if (adjustment > 0) flexibility += emergency_stretch; double badness; uint8_t fit; double dem; /* Single-box line: use fixed flexibility=1, fitness=decent. * This must come BEFORE is_last check to match Elisp behavior. */ if (is_single_box) { int64_t flexibility = (in->allow_emergency && adjustment > 0) ? emergency_stretch : 1; badness = compute_badness(adjustment, flexibility); fit = (in->allow_emergency && adjustment > 0) ? compute_fitness(adjustment, flexibility) : FITNESS_DECENT; int penalty = end_hyphen ? in->hyphen_penalty : 0; dem = prev_dem + compute_demerits(badness, penalty, prev_fit, fit, end_hyphen, prev_hyph, in->line_penalty, in->fitness_penalty, in->consec_hyphen_penalty); } else if (is_last) { /* Last line: minimal penalty if reasonably filled */ double fill_ratio = (double)ideal / lw; if (fill_ratio < in->last_line_ratio) { badness = in->last_line_short_penalty * (1.0 - fill_ratio); } else { badness = 0.0; } fit = FITNESS_DECENT; dem = prev_dem + (in->line_penalty + badness) * (in->line_penalty + badness); } else { badness = compute_badness(adjustment, flexibility); fit = compute_fitness(adjustment, flexibility); int penalty = end_hyphen ? in->hyphen_penalty : 0; dem = prev_dem + compute_demerits(badness, penalty, prev_fit, fit, end_hyphen, prev_hyph, in->line_penalty, in->fitness_penalty, in->consec_hyphen_penalty); } /* Update if better */ if (dem < demerits[k]) { demerits[k] = dem; backptrs[k] = i; rest_pixels[k] = adjustment; fitness[k] = fit; hyphen_counts[k] = end_hyphen ? prev_hyph + 1 : 0; line_counts[k] = prev_lines + 1; } } } void ekp_result_destroy(ekp_result_t *r) { if (!r) return; free(r->breaks); free(r->rest_pixels); free(r); } /* * Pure DP with pre-computed prefix arrays (for Elisp integration) * * This is the preferred API when Elisp has already computed everything. * Elisp does: tokenization, width measurement, glue computation, prefix sums. * C module only does: O(n²) DP computation. * * All font-dependent calculations happen in Elisp. C module is pure algorithm. * * dp_process_position() owns the per-breakpoint transition logic used by * both strict and final passes, including final-pass active-path preservation. */ ekp_result_t *ekp_break_with_prefixes( const int32_t *ideal_prefix, const int32_t *min_prefix, const int32_t *max_prefix, const int32_t *glue_ideals, const int32_t *glue_shrinks, const int32_t *glue_stretches, size_t n, const int32_t *hyphen_positions, size_t hyphen_count, int32_t hyphen_width, int32_t line_width, const int32_t *lead_spaces, const int32_t *trail_spaces, const int32_t *forbidden_positions, size_t forbidden_count, const int32_t *tail_protrudes, int32_t hyphen_protrude, int32_t first_line_width) { if (!ideal_prefix || !min_prefix || !max_prefix || n == 0 || n > INT32_MAX || line_width <= 0) return NULL; if (first_line_width <= 0) first_line_width = line_width; /* Allocate DP arrays */ double *demerits = malloc((n + 1) * sizeof(double)); int32_t *backptrs = malloc((n + 1) * sizeof(int32_t)); int64_t *rest_pixels = malloc((n + 1) * sizeof(int64_t)); uint8_t *fitness = malloc((n + 1) * sizeof(uint8_t)); int32_t *hyph_counts = malloc((n + 1) * sizeof(int32_t)); int32_t *line_counts = malloc((n + 1) * sizeof(int32_t)); uint8_t *surviving_candidates = calloc(n + 1, sizeof(uint8_t)); dp_artificial_candidate_t *artificial_candidates = calloc(n + 1, sizeof(dp_artificial_candidate_t)); if (!demerits || !backptrs || !rest_pixels || !fitness || !hyph_counts || !line_counts || !surviving_candidates || !artificial_candidates) { free(demerits); free(backptrs); free(rest_pixels); free(fitness); free(hyph_counts); free(line_counts); free(surviving_candidates); free(artificial_candidates); return NULL; } /* Initialize */ for (size_t i = 0; i <= n; i++) { demerits[i] = EKP_INFINITY; backptrs[i] = -1; rest_pixels[i] = 0; fitness[i] = FITNESS_DECENT; hyph_counts[i] = 0; line_counts[i] = 0; } demerits[0] = 0.0; /* Get K-P parameters */ int lp = ekp_global ? ekp_global->line_penalty : 10; int hp = ekp_global ? ekp_global->hyphen_penalty : 50; int fp = ekp_global ? ekp_global->fitness_penalty : 100; double last_ratio = ekp_global ? ekp_global->last_line_ratio : 0.5; int chp = ekp_global ? ekp_global->consec_hyphen_penalty : 100; double llsp = ekp_global ? ekp_global->last_line_short_penalty : 50.0; int32_t xstretch = ekp_global ? ekp_global->extra_stretch : 0; int32_t estretch = ekp_global ? ekp_global->emergency_stretch : 0; /* Create unified input structure */ dp_input_t in = { .ideal_prefix = ideal_prefix, .min_prefix = min_prefix, .max_prefix = max_prefix, .glue_ideals = glue_ideals, .glue_shrinks = glue_shrinks, .glue_stretches = glue_stretches, .hyphen_positions = hyphen_positions, .hyphen_count = hyphen_count, .hyphen_width = hyphen_width, .forbidden_positions = forbidden_positions, .forbidden_count = forbidden_count, .tail_protrudes = tail_protrudes, .hyphen_protrude = hyphen_protrude, .lead_spaces = lead_spaces, .trail_spaces = trail_spaces, .n = n, .line_width = line_width, .first_line_width = first_line_width, .line_penalty = lp, .hyphen_penalty = hp, .fitness_penalty = fp, .last_line_ratio = last_ratio, .consec_hyphen_penalty = chp, .last_line_short_penalty = llsp, .extra_stretch = xstretch, .emergency_stretch = estretch, .allow_emergency = false }; /* Two passes: strict Knuth-Plass first; if the paragraph end is * unreachable, rerun permitting emergency transitions. * Mirrors ekp--dp-cache-elisp. */ for (int pass = 0; pass < 2; pass++) { in.allow_emergency = (pass == 1); for (size_t i = 0; i <= n; i++) { demerits[i] = EKP_INFINITY; backptrs[i] = -1; rest_pixels[i] = 0; fitness[i] = FITNESS_DECENT; hyph_counts[i] = 0; line_counts[i] = 0; } memset(surviving_candidates, 0, (n + 1) * sizeof(uint8_t)); memset(artificial_candidates, 0, (n + 1) * sizeof(dp_artificial_candidate_t)); demerits[0] = 0.0; /* DP: for each valid start, try all ends */ for (size_t i = 0; i < n; i++) { if (in.allow_emergency && demerits[i] >= EKP_INFINITY && !surviving_candidates[i] && artificial_candidates[i].present) dp_install_artificial(&artificial_candidates[i], i, demerits, backptrs, rest_pixels, fitness, hyph_counts, line_counts); if (demerits[i] >= EKP_INFINITY) continue; dp_process_position(&in, i, demerits[i], fitness[i], hyph_counts[i], line_counts[i], demerits, backptrs, rest_pixels, fitness, hyph_counts, line_counts, surviving_candidates, artificial_candidates); } if (in.allow_emergency && demerits[n] >= EKP_INFINITY && !surviving_candidates[n] && artificial_candidates[n].present) dp_install_artificial(&artificial_candidates[n], n, demerits, backptrs, rest_pixels, fitness, hyph_counts, line_counts); if (demerits[n] < EKP_INFINITY) break; } /* Unreachable even with emergency breaks: cannot happen, but be safe */ if (demerits[n] >= EKP_INFINITY) { free(demerits); free(backptrs); free(rest_pixels); free(fitness); free(hyph_counts); free(line_counts); free(surviving_candidates); free(artificial_candidates); return NULL; } /* Build result */ ekp_result_t *result = calloc(1, sizeof(*result)); if (!result) { free(demerits); free(backptrs); free(rest_pixels); free(fitness); free(hyph_counts); free(line_counts); free(surviving_candidates); free(artificial_candidates); return NULL; } /* Count breaks */ size_t break_count = 0; int32_t idx = n; while (idx > 0) { break_count++; idx = backptrs[idx]; if (idx < 0) break; } result->breaks = malloc(break_count * sizeof(int32_t)); result->rest_pixels = malloc(break_count * sizeof(int64_t)); if (!result->breaks || !result->rest_pixels) { ekp_result_destroy(result); free(demerits); free(backptrs); free(rest_pixels); free(fitness); free(hyph_counts); free(line_counts); free(surviving_candidates); free(artificial_candidates); return NULL; } result->break_count = break_count; result->total_cost = demerits[n]; /* Fill in reverse order */ idx = n; for (size_t i = break_count; i > 0; i--) { result->breaks[i - 1] = idx; result->rest_pixels[i - 1] = rest_pixels[idx]; idx = backptrs[idx]; } free(demerits); free(backptrs); free(rest_pixels); free(fitness); free(hyph_counts); free(line_counts); free(surviving_candidates); free(artificial_candidates); return result; } /* * Work item for batch processing */ typedef struct { ekp_batch_input_t *input; ekp_result_t *result; } batch_work_t; static void batch_worker(void *arg) { batch_work_t *work = (batch_work_t *)arg; ekp_batch_input_t *in = work->input; work->result = ekp_break_with_prefixes( in->ideal_prefix, in->min_prefix, in->max_prefix, in->glue_ideals, in->glue_shrinks, in->glue_stretches, in->n, in->hyphen_positions, in->hyphen_count, in->hyphen_width, in->line_width, in->lead_spaces, in->trail_spaces, in->forbidden_positions, in->forbidden_count, in->tail_protrudes, in->hyphen_protrude, in->first_line_width); } /* * Batch line breaking: process multiple paragraphs in parallel * * This is the correct parallelization - each paragraph is completely * independent, so we get linear speedup with zero synchronization overhead. */ ekp_result_t **ekp_break_batch(ekp_batch_input_t *inputs, size_t count) { if (!inputs || count == 0) return NULL; ekp_result_t **results = calloc(count, sizeof(ekp_result_t *)); if (!results) return NULL; /* Create the worker pool on first parallel use */ if (count > 1 && ekp_global && !ekp_global->pool) ekp_global->pool = ekp_pool_create(0); /* Single paragraph: no point using threads */ if (count == 1 || !ekp_global || !ekp_global->pool) { for (size_t i = 0; i < count; i++) { ekp_batch_input_t *in = &inputs[i]; results[i] = ekp_break_with_prefixes( in->ideal_prefix, in->min_prefix, in->max_prefix, in->glue_ideals, in->glue_shrinks, in->glue_stretches, in->n, in->hyphen_positions, in->hyphen_count, in->hyphen_width, in->line_width, in->lead_spaces, in->trail_spaces, in->forbidden_positions, in->forbidden_count, in->tail_protrudes, in->hyphen_protrude, in->first_line_width); } return results; } /* Multiple paragraphs: parallel processing */ batch_work_t *works = malloc(count * sizeof(batch_work_t)); if (!works) { /* Fallback to sequential */ for (size_t i = 0; i < count; i++) { ekp_batch_input_t *in = &inputs[i]; results[i] = ekp_break_with_prefixes( in->ideal_prefix, in->min_prefix, in->max_prefix, in->glue_ideals, in->glue_shrinks, in->glue_stretches, in->n, in->hyphen_positions, in->hyphen_count, in->hyphen_width, in->line_width, in->lead_spaces, in->trail_spaces, in->forbidden_positions, in->forbidden_count, in->tail_protrudes, in->hyphen_protrude, in->first_line_width); } return results; } /* Submit all work items */ for (size_t i = 0; i < count; i++) { works[i].input = &inputs[i]; works[i].result = NULL; ekp_pool_submit(ekp_global->pool, batch_worker, &works[i]); } /* Wait for all to complete */ ekp_pool_wait(ekp_global->pool); /* Collect results */ for (size_t i = 0; i < count; i++) { results[i] = works[i].result; } free(works); return results; } /* * Initialization and cleanup */ int ekp_init(void) { if (ekp_global) return 0; ekp_global = calloc(1, sizeof(*ekp_global)); if (!ekp_global) return -1; /* Default K-P parameters */ ekp_global->line_penalty = 10; ekp_global->hyphen_penalty = 50; ekp_global->fitness_penalty = 100; ekp_global->last_line_ratio = 0.5; ekp_global->consec_hyphen_penalty = 100; ekp_global->last_line_short_penalty = 50.0; ekp_global->extra_stretch = 0; ekp_global->emergency_stretch = 0; /* The thread pool is created lazily by the first batch call: * plain single-paragraph use never starts worker threads. */ ekp_global->pool = NULL; return 0; } void ekp_cleanup(void) { if (!ekp_global) return; ekp_pool_destroy(ekp_global->pool); free(ekp_global); ekp_global = NULL; }