ekp-alignment selects the paragraph alignment. Non-justify modes keep inter-word glue rigid (arrays and class params zeroed at para build) while the DP gains a per-line flexibility ekp-ragged-stretch-pixel (default ≈2 em): badness = 100·(shortfall/R)³, so Knuth-Plass still globally minimizes raggedness — this is \raggedright with a finite \rightskip stretch, not first-fit. Rendering distributes each line's leftover by mode: trailing (ragged-right), split evenly (center), leading (ragged-left); the spacers are ekp-glue-marked, so buffer roundtrips stay exact. C module 1.3: set-penalties gains an extra-stretch scalar (reset when omitted); max_w widens by it, so the prefix-based flexibility picks it up automatically. Cache keys include alignment and R. Tests: 56 ERT green incl. per-mode invariants (fit, rigid interior glue, flush edges, centered leftover ±1px) and four-mode C parity; fuzz 300/300. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| ekp_hyphen.c | ||
| ekp_kp.c | ||
| ekp_module.h | ||
| ekp_paragraph.c | ||
| ekp_thread_pool.c | ||
| ekp.c | ||
| Makefile | ||
| README.md | ||
EKP C Dynamic Module
C implementation of the Knuth-Plass DP for emacs-kp (module version 1.1).
The division of labor: Elisp owns all font-dependent data (tokenization, pixel measurement, glue values, prefix sums); the C module runs only the O(n²) dynamic program. This keeps the two engines byte-identical in output while making the hot loop native.
Architecture
ekp_c/
├── ekp_module.h # Core data structures and API declarations
├── ekp.c # Emacs module entry point (emacs_module_init)
├── ekp_kp.c # Knuth-Plass DP + two-pass emergency strategy
├── ekp_thread_pool.c # Thread pool (parallelism across paragraphs)
├── ekp_hyphen.c # Liang hyphenation (experimental path only)
├── ekp_paragraph.c # C-side tokenization (experimental path only)
└── Makefile
Parallelism model: the DP for one paragraph is sequential (each
position depends on all earlier ones), so the thread pool parallelizes
across paragraphs via ekp-c-break-batch — the correct granularity,
with zero synchronization in the inner loop.
Building
cd ekp_c
make # → ekp.dylib (macOS) / ekp.so (Linux) / ekp.dll (Windows)
Requirements: C11 compiler, Emacs 27.1+ headers, pthreads.
make DEBUG=1 # Debug build with sanitizers
make clean
make info
API (as used by ekp.el)
(ekp-c-init) ; init global state + thread pool
(ekp-c-version) ; => "1.1" — checked by ekp-c-module-load
(ekp-c-thread-count) ; => 8
(ekp-c-cleanup)
;; Synced automatically by ekp.el before every call:
(ekp-c-set-penalties LINE HYPHEN FITNESS LAST-RATIO
&optional CONSEC-HYPHEN LAST-SHORT)
;; Single paragraph (11 args):
(ekp-c-break-with-arrays IDEAL-PREFIX MIN-PREFIX MAX-PREFIX
GLUE-IDEALS GLUE-SHRINKS GLUE-STRETCHES
HYPHEN-POS HYPHEN-WIDTH LINE-WIDTH
LEAD-SPACES TRAIL-SPACES)
;; => (BREAKS . TOTAL-COST)
;; Many paragraphs in parallel: vector of 11-element vectors
(ekp-c-break-batch PARAGRAPHS) ; => vector of (BREAKS . COST)
LEAD-SPACES / TRAIL-SPACES are the space-box run widths that the
Elisp renderer strips from line edges; the DP excludes them from line
metrics so both layers agree exactly (new in 1.1).
The DP uses the same two-pass strategy as the Elisp engine: a strict Knuth-Plass pass, then — only when the paragraph end is unreachable — a second pass permitting emergency single-box breaks, so overlong unbreakable tokens can never make the result empty. Badness saturates at 10000 exactly like the Elisp side.
Experimental: self-contained C path
ekp-c-break-lines tokenizes and hyphenates in C
(ekp_paragraph.c, ekp_hyphen.c) with a measurement callback into
Emacs. ekp.el does not use this path; its tokenizer is a
simplified approximation of ekp-split-to-boxes. Kept for
experimentation.
(ekp-c-load-hyphenator "/path/to/hyph_en_US.dic") ; => index
(ekp-c-hyphenate 0 "hyphenation") ; => (2 5)
(ekp-c-break-lines "text..." 0 600 #'string-pixel-width)
Performance
Measured with tests/ekp-bench.el (batch Emacs 30.2, Apple Silicon,
byte-compiled Elisp around the C calls, min of 3 cold-cache runs):
| Case | Elisp engine (compiled) | C engine |
|---|---|---|
| justify text-zh.txt w=200 | 96 ms | 57 ms |
| justify mixed text w=300 | 53 ms | 23 ms |
| range-justify zh 340–380 | 294 ms | 75 ms |
| range-justify mix 280–320 | 480 ms | 34 ms |
| DP only, text-zh w=400 | 15 ms | 1.3 ms |
The pure-DP speedup is ~12× (1.3 ms vs 15 ms); end-to-end gains are
smaller because tokenization, measurement and rendering stay in Elisp.
The C engine matters most for range-justify (many widths per text)
and multi-paragraph batches.