Refactor C module to eliminate duplicate DP logic

Extracted shared DP core algorithm into dp_process_position() function:

1. Added dp_input_t struct - unified input interface for DP algorithm
   - Contains prefix arrays, glue arrays, hyphen info
   - Contains K-P parameters (penalties, ratios)

2. Added dp_is_hyphen() function - unified hyphen position detection
   - Uses O(log n) binary search
   - Replaces is_hyphen_pos() function

3. Added dp_process_position() function - core DP algorithm
   - Processes single start position i, tries all end positions k
   - Contains complete demerits calculation, fitness classification
   - Both entry functions now call this shared core

4. Refactored process_dp_range()
   - Builds temporary glue arrays from ekp_paragraph_t
   - Creates dp_input_t and calls dp_process_position()

5. Refactored ekp_break_with_prefixes()
   - Directly uses input arrays to create dp_input_t
   - Calls dp_process_position() for each position

Benefits:
- Single point of maintenance: DP bugs only need fixing once
- Consistency guaranteed: both paths use identical algorithm
- ~100 lines of duplicate code removed
- Better testability: core algorithm can be tested independently

Co-authored-by: Kinneyzhang <38454496+Kinneyzhang@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-01-25 11:55:27 +00:00
parent 32021fbe35
commit 4a84723181
2 changed files with 276 additions and 226 deletions

1
ekp_c/ekp_c Symbolic link
View File

@ -0,0 +1 @@
./ekp_c

View File

@ -135,134 +135,258 @@ typedef struct {
double last_line_ratio; double last_line_ratio;
} dp_work_t; } dp_work_t;
/*
* Unified DP input structure for shared core algorithm
* This allows both ekp_paragraph_t-based and array-based inputs
* to use the same DP core logic.
*/
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;
/* Dimensions */
size_t n; /* box count */
int32_t line_width;
/* K-P parameters */
int line_penalty;
int hyphen_penalty;
int fitness_penalty;
double last_line_ratio;
} 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;
}
/*
* Core DP algorithm - shared by both entry points
* Processes position i, trying all end positions k.
* Updates output arrays when better solutions found.
*/
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,
int32_t *rest_pixels,
uint8_t *fitness,
int32_t *hyphen_counts,
int32_t *line_counts)
{
size_t n = in->n;
int32_t line_width = in->line_width;
/* Get leading glue for line starting at i */
int32_t lead_ideal = (in->glue_ideals && i < n) ? in->glue_ideals[i] : 0;
int32_t lead_shrink = (in->glue_shrinks && i < n) ? in->glue_shrinks[i] : 0;
int32_t lead_stretch = (in->glue_stretches && i < n) ? in->glue_stretches[i] : 0;
/* Try extending to each position k > i */
for (size_t k = i + 1; k <= n; k++) {
bool is_last = (k == n);
bool end_hyphen = dp_is_hyphen(in, k - 1);
/* Line metrics from i to k (excluding leading glue) */
int32_t ideal = in->ideal_prefix[k] - in->ideal_prefix[i] - lead_ideal;
int32_t min_w = in->min_prefix[k] - in->min_prefix[i] -
(lead_ideal - lead_shrink);
int32_t max_w = in->max_prefix[k] - in->max_prefix[i] -
(lead_ideal + lead_stretch);
/* Add hyphen width if needed */
if (end_hyphen) {
ideal += in->hyphen_width;
min_w += in->hyphen_width;
max_w += in->hyphen_width;
}
/* Too long? Also handle is_last && ideal > line_width. */
if (min_w > line_width || (is_last && ideal > line_width)) {
/* Force break if nothing else found */
if (k > i + 1 && demerits[k - 1] >= EKP_INFINITY) {
int32_t prev_ideal = in->ideal_prefix[k - 1] - in->ideal_prefix[i] - lead_ideal;
int32_t rest = line_width - prev_ideal;
demerits[k - 1] = prev_dem + 10000.0 + (double)rest * rest;
backptrs[k - 1] = i;
rest_pixels[k - 1] = rest;
fitness[k - 1] = FITNESS_VERY_LOOSE;
hyphen_counts[k - 1] = 0;
line_counts[k - 1] = prev_lines + 1;
}
break; /* No point trying longer lines */
}
/* Valid break? */
bool valid = (min_w <= line_width && max_w >= line_width) ||
(is_last && ideal <= line_width);
if (!valid)
continue;
/* Compute demerits */
int32_t adjustment = line_width - ideal;
int32_t flexibility = (adjustment > 0) ?
(max_w - ideal) : (ideal - min_w);
/* Single-box line: use minimum flexibility of 1 */
bool is_single_box = (k == i + 1);
if (is_single_box && flexibility <= 0)
flexibility = 1;
double badness;
uint8_t fit;
double dem;
if (is_last) {
/* Last line: minimal penalty if reasonably filled */
double fill_ratio = (double)ideal / line_width;
if (fill_ratio < in->last_line_ratio) {
badness = 50.0 * (1.0 - fill_ratio);
} else {
badness = 0.0;
}
fit = FITNESS_DECENT;
dem = prev_dem + (in->line_penalty + badness) *
(in->line_penalty + badness);
} else if (is_single_box) {
/* Single-box line: use fixed flexibility=1, fitness=decent */
badness = compute_badness(adjustment, 1);
fit = 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);
} 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);
}
/* 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;
}
}
}
/* /*
* Process a range of candidate breakpoints (for parallel execution) * Process a range of candidate breakpoints (for parallel execution)
* Now uses shared dp_process_position() core.
*/ */
static void process_dp_range(void *arg) static void process_dp_range(void *arg)
{ {
dp_work_t *work = (dp_work_t *)arg; dp_work_t *work = (dp_work_t *)arg;
ekp_paragraph_t *p = work->para; ekp_paragraph_t *p = work->para;
int32_t line_width = work->line_width;
size_t n = p->box_count; size_t n = p->box_count;
/* Build temporary glue arrays from paragraph structure */
int32_t *glue_ideals = malloc(n * sizeof(int32_t));
int32_t *glue_shrinks = malloc(n * sizeof(int32_t));
int32_t *glue_stretches = malloc(n * sizeof(int32_t));
if (!glue_ideals || !glue_shrinks || !glue_stretches) {
free(glue_ideals); free(glue_shrinks); free(glue_stretches);
return;
}
for (size_t i = 0; i < n; i++) {
glue_ideals[i] = p->glues[i].ideal;
glue_shrinks[i] = p->glues[i].shrink;
glue_stretches[i] = p->glues[i].stretch;
}
/* Create unified input structure */
dp_input_t in = {
.ideal_prefix = p->ideal_prefix,
.min_prefix = p->min_prefix,
.max_prefix = p->max_prefix,
.glue_ideals = glue_ideals,
.glue_shrinks = glue_shrinks,
.glue_stretches = glue_stretches,
.hyphen_positions = p->hyphen_positions,
.hyphen_count = p->hyphen_count,
.hyphen_width = p->hyphen_width,
.n = n,
.line_width = work->line_width,
.line_penalty = work->line_penalty,
.hyphen_penalty = work->hyphen_penalty,
.fitness_penalty = work->fitness_penalty,
.last_line_ratio = work->last_line_ratio
};
/* Process each position in range */
for (size_t i = work->start; i < work->end; i++) { for (size_t i = work->start; i < work->end; i++) {
if (work->prev_demerits[i] >= EKP_INFINITY) if (work->prev_demerits[i] >= EKP_INFINITY)
continue; continue;
double prev_dem = work->prev_demerits[i]; dp_process_position(&in, i,
uint8_t prev_fit = work->prev_fitness[i]; work->prev_demerits[i],
int prev_hyph = work->prev_hyphen_counts[i]; work->prev_fitness[i],
int prev_lines = work->prev_line_counts[i]; work->prev_hyphen_counts[i],
work->prev_line_counts[i],
/* Get leading glue for line starting at i */ work->demerits,
int32_t leading_glue_ideal = (i < n) ? p->glues[i].ideal : 0; work->backptrs,
int32_t leading_glue_stretch = (i < n) ? p->glues[i].stretch : 0; work->rest_pixels,
int32_t leading_glue_shrink = (i < n) ? p->glues[i].shrink : 0; work->fitness,
work->hyphen_counts,
/* Try extending to each position k > i */ work->line_counts);
for (size_t k = i + 1; k <= n; k++) {
bool is_last = (k == n);
bool end_hyphen = (k > 0) && is_hyphen_break(p, k - 1);
/* Line metrics from i to k (excluding leading glue) */
int32_t ideal = p->ideal_prefix[k] - p->ideal_prefix[i] - leading_glue_ideal;
int32_t min_w = p->min_prefix[k] - p->min_prefix[i] -
(leading_glue_ideal - leading_glue_shrink);
int32_t max_w = p->max_prefix[k] - p->max_prefix[i] -
(leading_glue_ideal + leading_glue_stretch);
/* Add hyphen width if needed */
if (end_hyphen) {
ideal += p->hyphen_width;
min_w += p->hyphen_width;
max_w += p->hyphen_width;
}
/* Too long? Also handle is_last && ideal > line_width.
* This matches Elisp's ekp--dp-cache-elisp which breaks when
* content won't fit even at the end of a paragraph. */
if (min_w > line_width || (is_last && ideal > line_width)) {
/* Force break if nothing else found */
if (k > 1 && work->demerits[k - 1] >= EKP_INFINITY) {
int32_t rest = line_width - (p->ideal_prefix[k - 1] -
p->ideal_prefix[i] - leading_glue_ideal);
work->demerits[k - 1] = prev_dem + 10000.0 + rest * rest;
work->backptrs[k - 1] = i;
work->rest_pixels[k - 1] = rest;
work->fitness[k - 1] = FITNESS_VERY_LOOSE;
work->hyphen_counts[k - 1] = 0;
work->line_counts[k - 1] = prev_lines + 1;
}
break; /* No point trying longer lines */
}
/* Valid break? */
bool valid = (min_w <= line_width && max_w >= line_width) ||
(is_last && ideal <= line_width);
if (!valid)
continue;
/* Compute demerits */
int32_t adjustment = line_width - ideal;
int32_t flexibility = (adjustment > 0) ?
(max_w - ideal) : (ideal - min_w);
/* Single-box line: use minimum flexibility of 1 to avoid infinite badness
* This matches Elisp's behavior for lines containing only one box */
bool is_single_box = (k == i + 1);
if (is_single_box && flexibility <= 0)
flexibility = 1;
double badness;
uint8_t fit;
double dem;
if (is_last) {
/* Last line: minimal penalty if reasonably filled */
double fill_ratio = (double)ideal / line_width;
if (fill_ratio < work->last_line_ratio) {
badness = 50.0 * (1.0 - fill_ratio);
} else {
badness = 0.0;
}
fit = FITNESS_DECENT;
dem = prev_dem + (work->line_penalty + badness) *
(work->line_penalty + badness);
} else if (is_single_box) {
/* Single-box line: use fixed flexibility=1, fitness=decent */
badness = compute_badness(adjustment, 1);
fit = FITNESS_DECENT;
int penalty = end_hyphen ? work->hyphen_penalty : 0;
dem = prev_dem + compute_demerits(badness, penalty,
prev_fit, fit,
end_hyphen, prev_hyph,
work->line_penalty,
work->fitness_penalty);
} else {
badness = compute_badness(adjustment, flexibility);
fit = compute_fitness(adjustment, flexibility);
int penalty = end_hyphen ? work->hyphen_penalty : 0;
dem = prev_dem + compute_demerits(badness, penalty,
prev_fit, fit,
end_hyphen, prev_hyph,
work->line_penalty,
work->fitness_penalty);
}
/* Update if better */
if (dem < work->demerits[k]) {
work->demerits[k] = dem;
work->backptrs[k] = i;
work->rest_pixels[k] = adjustment;
work->fitness[k] = fit;
work->hyphen_counts[k] = end_hyphen ? prev_hyph + 1 : 0;
work->line_counts[k] = prev_lines + 1;
}
}
} }
free(glue_ideals);
free(glue_shrinks);
free(glue_stretches);
} }
/* /*
@ -426,28 +550,12 @@ void ekp_result_destroy(ekp_result_t *r)
* C module only does: O(n²) DP computation. * C module only does: O(n²) DP computation.
* *
* All font-dependent calculations happen in Elisp. C module is pure algorithm. * All font-dependent calculations happen in Elisp. C module is pure algorithm.
*
* Note: This function now uses dp_process_position() for the core DP logic,
* sharing the same algorithm with process_dp_range(). Any bug fix only needs
* to be made once in dp_process_position().
*/ */
static inline bool is_hyphen_pos(const int32_t *positions, size_t count, int32_t pos)
{
if (count == 0)
return false;
/* Binary search in sorted positions */
size_t lo = 0;
size_t hi = count - 1;
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
if (positions[mid] < pos)
lo = mid + 1;
else
hi = mid;
}
return positions[lo] == pos;
}
ekp_result_t *ekp_break_with_prefixes( ekp_result_t *ekp_break_with_prefixes(
const int32_t *ideal_prefix, const int32_t *ideal_prefix,
const int32_t *min_prefix, const int32_t *min_prefix,
@ -495,100 +603,41 @@ ekp_result_t *ekp_break_with_prefixes(
int fp = ekp_global ? ekp_global->fitness_penalty : 100; int fp = ekp_global ? ekp_global->fitness_penalty : 100;
double last_ratio = ekp_global ? ekp_global->last_line_ratio : 0.5; double last_ratio = ekp_global ? ekp_global->last_line_ratio : 0.5;
/* 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,
.n = n,
.line_width = line_width,
.line_penalty = lp,
.hyphen_penalty = hp,
.fitness_penalty = fp,
.last_line_ratio = last_ratio
};
/* DP: for each valid start, try all ends */ /* DP: for each valid start, try all ends */
for (size_t i = 0; i < n; i++) { for (size_t i = 0; i < n; i++) {
if (demerits[i] >= EKP_INFINITY) if (demerits[i] >= EKP_INFINITY)
continue; continue;
/* Leading glue for line starting at i */ dp_process_position(&in, i,
int32_t lead_ideal = glue_ideals ? glue_ideals[i] : 0; demerits[i],
int32_t lead_shrink = glue_shrinks ? glue_shrinks[i] : 0; fitness[i],
int32_t lead_stretch = glue_stretches ? glue_stretches[i] : 0; hyph_counts[i],
line_counts[i],
for (size_t k = i + 1; k <= n; k++) { demerits,
bool is_last = (k == n); backptrs,
bool end_hyph = hyphen_positions && is_hyphen_pos(hyphen_positions, hyphen_count, k - 1); rest_pixels,
fitness,
/* Line width from i to k (exclude leading glue) */ hyph_counts,
int32_t ideal = ideal_prefix[k] - ideal_prefix[i] - lead_ideal; line_counts);
int32_t min_w = min_prefix[k] - min_prefix[i] - (lead_ideal - lead_shrink);
int32_t max_w = max_prefix[k] - max_prefix[i] - (lead_ideal + lead_stretch);
if (end_hyph) {
ideal += hyphen_width;
min_w += hyphen_width;
max_w += hyphen_width;
}
/* Too long? Also handle is_last && ideal > line_width.
* This matches Elisp's ekp--dp-cache-elisp which breaks when
* content won't fit even at the end of a paragraph. */
if (min_w > line_width || (is_last && ideal > line_width)) {
if (k > i + 1 && demerits[k - 1] >= EKP_INFINITY) {
/* Force break at previous position with high penalty */
int32_t prev_ideal = ideal_prefix[k - 1] - ideal_prefix[i] - lead_ideal;
int32_t rest = line_width - prev_ideal;
double forced_dem = demerits[i] + 10000.0 + (double)rest * rest;
demerits[k - 1] = forced_dem;
backptrs[k - 1] = i;
rest_pixels[k - 1] = rest;
fitness[k - 1] = FITNESS_VERY_LOOSE;
hyph_counts[k - 1] = 0;
line_counts[k - 1] = line_counts[i] + 1;
}
break;
}
/* Valid break? */
bool valid = (min_w <= line_width && max_w >= line_width) ||
(is_last && ideal <= line_width);
if (!valid)
continue;
/* Compute demerits */
int32_t adj = line_width - ideal;
int32_t flex = (adj > 0) ? (max_w - ideal) : (ideal - min_w);
/* Single-box line: use minimum flexibility of 1 to avoid infinite badness
* This matches Elisp's behavior for lines containing only one box */
bool is_single_box = (k == i + 1);
if (is_single_box && flex <= 0)
flex = 1;
double bad;
uint8_t fit;
double dem;
if (is_last) {
double fill = (double)ideal / line_width;
bad = (fill < last_ratio) ? 50.0 * (1.0 - fill) : 0.0;
fit = FITNESS_DECENT;
dem = demerits[i] + (lp + bad) * (lp + bad);
} else if (is_single_box) {
/* Single-box line: use fixed flexibility=1, fitness=decent */
bad = compute_badness(adj, 1);
fit = FITNESS_DECENT;
int pen = end_hyph ? hp : 0;
dem = demerits[i] + compute_demerits(bad, pen, fitness[i], fit,
end_hyph, hyph_counts[i], lp, fp);
} else {
bad = compute_badness(adj, flex);
fit = compute_fitness(adj, flex);
int pen = end_hyph ? hp : 0;
dem = demerits[i] + compute_demerits(bad, pen, fitness[i], fit,
end_hyph, hyph_counts[i], lp, fp);
}
if (dem < demerits[k]) {
demerits[k] = dem;
backptrs[k] = i;
rest_pixels[k] = adj;
fitness[k] = fit;
hyph_counts[k] = end_hyph ? hyph_counts[i] + 1 : 0;
line_counts[k] = line_counts[i] + 1;
}
}
} }
/* If no valid path found to end, return NULL to fallback to Elisp */ /* If no valid path found to end, return NULL to fallback to Elisp */