diff --git a/ekp-utils.el b/ekp-utils.el index b256ad9..137fb1c 100644 --- a/ekp-utils.el +++ b/ekp-utils.el @@ -339,14 +339,10 @@ after CALLBACK returns." (declare-function ekp-c-init "ext:ekp") (declare-function ekp-c-version "ext:ekp") (declare-function ekp-c-thread-count "ext:ekp") -(declare-function ekp-c-load-hyphenator "ext:ekp") (defvar ekp-c-module-loaded nil "Non-nil if C module is loaded.") -(defvar ekp-c-hyphenator-index nil - "Index of the loaded hyphenator in C module.") - (defun ekp-c-module-dir () "Return the C module directory." (when-let ((root-dir (ekp-root-dir))) @@ -389,20 +385,6 @@ Run 'make' in ekp_c/ to rebuild; falling back to Elisp." (ekp-c-version) (ekp-c-thread-count))))) (message "C module not found. Run 'make' in ekp_c/ directory.")))) -(defun ekp-c-load-dictionary (lang) - "Load hyphenation dictionary for LANG into C module." - (when ekp-c-module-loaded - (let* ((root-dir (ekp-root-dir)) - (dict-file (expand-file-name - (format "dictionaries/hyph_%s.dic" lang) - root-dir))) - (when (file-exists-p dict-file) - (setq ekp-c-hyphenator-index - (ekp-c-load-hyphenator dict-file)) - (when ekp-c-hyphenator-index - (message "Loaded hyphenator for %s (index %d)" - lang ekp-c-hyphenator-index)))))) - ;;;###autoload (defun ekp-c-module-build () "Build the C module using make." diff --git a/ekp.el b/ekp.el index 4eac1a1..a84a50c 100644 --- a/ekp.el +++ b/ekp.el @@ -1680,7 +1680,8 @@ the reconstructed rests overfill the indented line." The C module receives all font-dependent data from Elisp; it only runs the pure DP. Falls back to Elisp when the C call fails." (ekp--c-sync-params) - (let* ((result (ekp-c-break-with-arrays + (let* ((result (condition-case nil + (ekp-c-break-with-arrays (ekp-para-ideal-prefixs para) (ekp-para-min-prefixs para) (ekp-para-max-prefixs para) @@ -1695,7 +1696,10 @@ runs the pure DP. Falls back to Elisp when the C call fails." (ekp-para-forbidden-positions para) (ekp-para-tail-protrudes para) (ekp-para-hyphen-protrude para) - (cdr (ekp--line-spec para 0 line-pixel)))) + (cdr (ekp--line-spec para 0 line-pixel))) + ;; A module-level signal must not escape: the + ;; Elisp engine is the fallback for any C failure. + (error nil))) (c-breaks (car result)) (c-cost (cdr result))) (if (null c-breaks) @@ -1723,12 +1727,16 @@ Only computes strings that aren't already cached." (mapcar (lambda (ip) (ekp--prepare-para-for-c (cdr ip) line-pixel)) needs-compute))) - (batch-results (ekp-c-break-batch batch-input))) + ;; nil (whole-batch failure or a signal) falls back to + ;; the Elisp engine per paragraph below. + (batch-results (condition-case nil + (ekp-c-break-batch batch-input) + (error nil)))) (cl-loop for ip in needs-compute for j from 0 for idx = (car ip) for para = (cdr ip) - for res = (aref batch-results j) + for res = (and batch-results (aref batch-results j)) for breaks = (car res) for cost = (cdr res) do (aset results idx diff --git a/ekp_c/Makefile b/ekp_c/Makefile index 9335406..f827cfe 100644 --- a/ekp_c/Makefile +++ b/ekp_c/Makefile @@ -67,7 +67,7 @@ else endif # Source files -SRCS := ekp.c ekp_kp.c ekp_hyphen.c ekp_paragraph.c ekp_thread_pool.c +SRCS := ekp.c ekp_kp.c ekp_thread_pool.c OBJS := $(SRCS:.c=.o) # Output diff --git a/ekp_c/README.md b/ekp_c/README.md index aa69eef..259014d 100644 --- a/ekp_c/README.md +++ b/ekp_c/README.md @@ -1,6 +1,6 @@ # EKP C Dynamic Module -C implementation of the Knuth-Plass DP for emacs-kp (module version 1.4). +C implementation of the Knuth-Plass DP for emacs-kp (module version 1.5). The division of labor: **Elisp owns all font-dependent data** (tokenization, pixel measurement, glue values, prefix sums); the C @@ -15,15 +15,16 @@ ekp_c/ ├── 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. +with zero synchronization in the inner loop. The pool is created +lazily on the first multi-paragraph batch, sized to the machine's +core count; a full queue blocks the submitter instead of dropping +tasks. ## Building @@ -32,10 +33,12 @@ cd ekp_c make # → ekp.dylib (macOS) / ekp.so (Linux) / ekp.dll (Windows) ``` -Requirements: C11 compiler, Emacs 27.1+ headers, pthreads. +Requirements: C11 compiler, Emacs module headers, pthreads. +Windows builds need MinGW-w64 (for pthreads) and +`make EMACS_ROOT=`. ```bash -make DEBUG=1 # Debug build with sanitizers +make DEBUG=1 # Debug build with ASan/UBSan make clean make info ``` @@ -43,29 +46,36 @@ make info ## API (as used by ekp.el) ```elisp -(ekp-c-init) ; init global state + thread pool -(ekp-c-version) ; => "1.4" — checked by ekp-c-module-load -(ekp-c-thread-count) ; => 8 +(ekp-c-init) ; init global state +(ekp-c-version) ; => "1.5" — checked by ekp-c-module-load +(ekp-c-thread-count) ; worker count (created lazily on first batch) (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) + &optional CONSEC-HYPHEN LAST-SHORT EXTRA-STRETCH) -;; Single paragraph (11 args): +;; Single paragraph (15 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) + LEAD-SPACES TRAIL-SPACES FORBIDDEN-POS + TAIL-PROTRUDES HYPHEN-PROTRUDE + FIRST-LINE-WIDTH) ;; => (BREAKS . TOTAL-COST) -;; Many paragraphs in parallel: vector of 11-element vectors +;; Many paragraphs in parallel: vector of 15-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 (since 1.1). +metrics so both layers agree exactly (since 1.1). `FORBIDDEN-POS` +carries the kinsoku / no-break gap indices (since 1.2), +`TAIL-PROTRUDES` / `HYPHEN-PROTRUDE` the right-edge protrusion +allowances (since 1.4), and `FIRST-LINE-WIDTH` the width of line 0 +for first-line indentation (since 1.5; pass the line width or ≤0 +when no indent is active). 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 — @@ -73,19 +83,9 @@ 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. - -```elisp -(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) -``` +Failure behavior: any allocation failure or bad argument makes the +call return nil, and ekp.el falls back to the Elisp engine — the C +module never silently degrades to a subtly different layout. ## Performance @@ -94,13 +94,14 @@ 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 | +| justify text-zh.txt w=200 | 150 ms | 41 ms | +| justify mixed text w=300 | 82 ms | 31 ms | +| range-justify zh 340–380 | 529 ms | 106 ms | +| range-justify mix 280–320 | 762 ms | 52 ms | +| DP only, text-zh w=400 | 30 ms | 2.5 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. +The pure-DP speedup is ~12×; 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. Absolute numbers vary with the machine and +power state; regenerate them with the two commands in DEVELOPER.md §9. diff --git a/ekp_c/ekp.c b/ekp_c/ekp.c index 32a2c7e..6640e96 100644 --- a/ekp_c/ekp.c +++ b/ekp_c/ekp.c @@ -20,6 +20,7 @@ */ #include "ekp_module.h" +#include #include #include #include @@ -27,25 +28,13 @@ /* Required for Emacs modules */ int plugin_is_GPL_compatible; -/* Cached Emacs environment for callbacks */ -static emacs_env *current_env = NULL; -static emacs_value measure_func = NULL; - -/* - * Pixel measurement callback that calls back into Emacs - */ -static int32_t emacs_measure_string(const char *text, size_t len) +/* All pixel quantities travel as int32; clamp instead of silently + * wrapping if Elisp ever hands us something absurd. */ +static inline int32_t clamp32(intmax_t v) { - if (!current_env || !measure_func) - return len * 7; /* fallback: ~7 pixels per char */ - - emacs_value str = current_env->make_string(current_env, text, len); - emacs_value result = current_env->funcall(current_env, measure_func, 1, &str); - - if (current_env->non_local_exit_check(current_env) != emacs_funcall_exit_return) - return len * 7; - - return (int32_t)current_env->extract_integer(current_env, result); + if (v > INT32_MAX) return INT32_MAX; + if (v < INT32_MIN) return INT32_MIN; + return (int32_t)v; } /* @@ -77,72 +66,6 @@ static emacs_value Fekp_c_cleanup(emacs_env *env, ptrdiff_t nargs, return env->intern(env, "t"); } -/* - * ekp-c-load-hyphenator: Load hyphenation dictionary - */ -static emacs_value Fekp_c_load_hyphenator(emacs_env *env, ptrdiff_t nargs, - emacs_value *args, void *data) -{ - (void)nargs; (void)data; - - if (!ekp_global) { - emacs_value signal = env->intern(env, "error"); - emacs_value msg = env->make_string(env, "ekp-c not initialized", 21); - env->non_local_exit_signal(env, signal, msg); - return env->intern(env, "nil"); - } - - /* Get dictionary path */ - ptrdiff_t size = 0; - env->copy_string_contents(env, args[0], NULL, &size); - char *path = malloc(size); - if (!path) - return env->intern(env, "nil"); - - env->copy_string_contents(env, args[0], path, &size); - - /* Load hyphenator */ - ekp_hyphenator_t *h = ekp_hyphen_create(path); - free(path); - - if (!h) - return env->intern(env, "nil"); - - /* Store in global state */ - if (ekp_global->hyphenator_count < 32) { - ekp_global->hyphenators[ekp_global->hyphenator_count++] = h; - return env->make_integer(env, ekp_global->hyphenator_count - 1); - } - - ekp_hyphen_destroy(h); - return env->intern(env, "nil"); -} - -/* - * ekp-c-set-spacing: Set spacing parameters - */ -static emacs_value Fekp_c_set_spacing(emacs_env *env, ptrdiff_t nargs, - emacs_value *args, void *data) -{ - (void)data; - - if (!ekp_global || nargs < 9) { - return env->intern(env, "nil"); - } - - ekp_global->spacing.lws_ideal = env->extract_integer(env, args[0]); - ekp_global->spacing.lws_stretch = env->extract_integer(env, args[1]); - ekp_global->spacing.lws_shrink = env->extract_integer(env, args[2]); - ekp_global->spacing.mws_ideal = env->extract_integer(env, args[3]); - ekp_global->spacing.mws_stretch = env->extract_integer(env, args[4]); - ekp_global->spacing.mws_shrink = env->extract_integer(env, args[5]); - ekp_global->spacing.cws_ideal = env->extract_integer(env, args[6]); - ekp_global->spacing.cws_stretch = env->extract_integer(env, args[7]); - ekp_global->spacing.cws_shrink = env->extract_integer(env, args[8]); - - return env->intern(env, "t"); -} - /* * ekp-c-set-penalties: Set K-P parameters */ @@ -154,144 +77,22 @@ static emacs_value Fekp_c_set_penalties(emacs_env *env, ptrdiff_t nargs, if (!ekp_global || nargs < 4) return env->intern(env, "nil"); - ekp_global->line_penalty = env->extract_integer(env, args[0]); - ekp_global->hyphen_penalty = env->extract_integer(env, args[1]); - ekp_global->fitness_penalty = env->extract_integer(env, args[2]); + ekp_global->line_penalty = clamp32(env->extract_integer(env, args[0])); + ekp_global->hyphen_penalty = clamp32(env->extract_integer(env, args[1])); + ekp_global->fitness_penalty = clamp32(env->extract_integer(env, args[2])); ekp_global->last_line_ratio = env->extract_float(env, args[3]); if (nargs > 4) - ekp_global->consec_hyphen_penalty = env->extract_integer(env, args[4]); + ekp_global->consec_hyphen_penalty = clamp32(env->extract_integer(env, args[4])); if (nargs > 5) ekp_global->last_line_short_penalty = env->extract_float(env, args[5]); /* Per-line extra stretch for non-justify alignment; reset to 0 * when the caller omits it so stale values never leak. */ ekp_global->extra_stretch = - (nargs > 6) ? (int32_t)env->extract_integer(env, args[6]) : 0; + (nargs > 6) ? (int32_t)clamp32(env->extract_integer(env, args[6])) : 0; return env->intern(env, "t"); } -/* - * ekp-c-hyphenate: Get hyphenation positions for a word - */ -static emacs_value Fekp_c_hyphenate(emacs_env *env, ptrdiff_t nargs, - emacs_value *args, void *data) -{ - (void)data; - - if (!ekp_global || nargs < 2) - return env->intern(env, "nil"); - - intmax_t h_idx = env->extract_integer(env, args[0]); - if (h_idx < 0 || (size_t)h_idx >= ekp_global->hyphenator_count) - return env->intern(env, "nil"); - - ekp_hyphenator_t *h = ekp_global->hyphenators[h_idx]; - - /* Get word */ - ptrdiff_t size = 0; - env->copy_string_contents(env, args[1], NULL, &size); - char *word = malloc(size); - if (!word) - return env->intern(env, "nil"); - - env->copy_string_contents(env, args[1], word, &size); - - /* Hyphenate */ - int8_t positions[EKP_MAX_WORD_LEN]; - int count = ekp_hyphen_word(h, word, size - 1, positions, EKP_MAX_WORD_LEN); - free(word); - - /* Build result list */ - emacs_value result = env->intern(env, "nil"); - emacs_value cons_sym = env->intern(env, "cons"); - - for (int i = count - 1; i >= 0; i--) { - emacs_value pos = env->make_integer(env, positions[i]); - emacs_value args2[2] = {pos, result}; - result = env->funcall(env, cons_sym, 2, args2); - } - - return result; -} - -/* - * ekp-c-break-lines: Core line breaking function - * - * Args: (string hyphenator-index line-width measure-func) - * Returns: (breaks . total-cost) where breaks is a list - */ -static emacs_value Fekp_c_break_lines(emacs_env *env, ptrdiff_t nargs, - emacs_value *args, void *data) -{ - (void)data; - - if (!ekp_global || nargs < 4) - return env->intern(env, "nil"); - - /* Get string */ - ptrdiff_t size = 0; - env->copy_string_contents(env, args[0], NULL, &size); - char *text = malloc(size); - if (!text) - return env->intern(env, "nil"); - - env->copy_string_contents(env, args[0], text, &size); - size_t text_len = size - 1; - - /* Get hyphenator */ - intmax_t h_idx = env->extract_integer(env, args[1]); - ekp_hyphenator_t *h = NULL; - if (h_idx >= 0 && (size_t)h_idx < ekp_global->hyphenator_count) - h = ekp_global->hyphenators[h_idx]; - - /* Get line width */ - int32_t line_width = env->extract_integer(env, args[2]); - - /* Get measure function */ - current_env = env; - measure_func = args[3]; - - /* Create paragraph */ - ekp_paragraph_t *para = ekp_para_create(text, text_len, h, emacs_measure_string); - free(text); - - if (!para) { - current_env = NULL; - measure_func = NULL; - return env->intern(env, "nil"); - } - - /* Break lines */ - ekp_result_t *result = ekp_break_lines(para, line_width); - - current_env = NULL; - measure_func = NULL; - - if (!result) { - ekp_para_destroy(para); - return env->intern(env, "nil"); - } - - /* Build result: ((breaks...) . cost) */ - emacs_value breaks_list = env->intern(env, "nil"); - emacs_value cons_sym = env->intern(env, "cons"); - - for (size_t i = result->break_count; i > 0; i--) { - emacs_value brk = env->make_integer(env, result->breaks[i - 1]); - emacs_value args2[2] = {brk, breaks_list}; - breaks_list = env->funcall(env, cons_sym, 2, args2); - } - - emacs_value cost = env->make_float(env, result->total_cost); - emacs_value args2[2] = {breaks_list, cost}; - emacs_value final = env->funcall(env, cons_sym, 2, args2); - - ekp_result_destroy(result); - ekp_para_destroy(para); - - return final; -} - /* * ekp-c-version: Return module version */ @@ -314,7 +115,11 @@ static emacs_value Fekp_c_thread_count(emacs_env *env, ptrdiff_t nargs, emacs_value *args, void *data) { (void)nargs; (void)args; (void)data; - return env->make_integer(env, EKP_THREAD_POOL_SIZE); + /* Pool is created lazily; report its actual size once it exists, + * else the size it will get. */ + if (ekp_global && ekp_global->pool) + return env->make_integer(env, (intmax_t)ekp_global->pool->thread_count); + return env->make_integer(env, (intmax_t)ekp_pool_default_threads()); } /* @@ -370,18 +175,18 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs, /* Extract prefix arrays */ for (ptrdiff_t i = 0; i < prefix_len; i++) { - ideal_prefix[i] = env->extract_integer(env, env->vec_get(env, args[0], i)); - min_prefix[i] = env->extract_integer(env, env->vec_get(env, args[1], i)); - max_prefix[i] = env->extract_integer(env, env->vec_get(env, args[2], i)); - lead_spaces[i] = env->extract_integer(env, env->vec_get(env, args[9], i)); - trail_spaces[i] = env->extract_integer(env, env->vec_get(env, args[10], i)); + ideal_prefix[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[0], i))); + min_prefix[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[1], i))); + max_prefix[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[2], i))); + lead_spaces[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[9], i))); + trail_spaces[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[10], i))); } /* Extract glue arrays */ for (size_t i = 0; i < n; i++) { - glue_ideals[i] = env->extract_integer(env, env->vec_get(env, args[3], i)); - glue_shrinks[i] = env->extract_integer(env, env->vec_get(env, args[4], i)); - glue_stretches[i] = env->extract_integer(env, env->vec_get(env, args[5], i)); + glue_ideals[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[3], i))); + glue_shrinks[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[4], i))); + glue_stretches[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[5], i))); } /* Get hyphen positions vector */ @@ -391,23 +196,23 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs, hyph_pos = malloc(hyph_count * sizeof(int32_t)); if (hyph_pos) { for (ptrdiff_t i = 0; i < hyph_count; i++) { - hyph_pos[i] = env->extract_integer(env, env->vec_get(env, args[6], i)); + hyph_pos[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[6], i))); } } } - int32_t hyph_width = env->extract_integer(env, args[7]); - int32_t line_width = env->extract_integer(env, args[8]); + int32_t hyph_width = clamp32(env->extract_integer(env, args[7])); + int32_t line_width = clamp32(env->extract_integer(env, args[8])); /* Right-edge protrusion: per-gap array (n+1) and hyphen scalar */ int32_t *tail_pro = malloc(prefix_len * sizeof(int32_t)); if (tail_pro) { for (ptrdiff_t i = 0; i < prefix_len; i++) { - tail_pro[i] = env->extract_integer(env, env->vec_get(env, args[12], i)); + tail_pro[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[12], i))); } } - int32_t hyphen_protrude = env->extract_integer(env, args[13]); - int32_t first_line_width = env->extract_integer(env, args[14]); + int32_t hyphen_protrude = clamp32(env->extract_integer(env, args[13])); + int32_t first_line_width = clamp32(env->extract_integer(env, args[14])); /* Forbidden break positions (sorted gap indices, may be empty) */ ptrdiff_t forb_count = env->vec_size(env, args[11]); @@ -416,11 +221,27 @@ static emacs_value Fekp_c_break_with_arrays(emacs_env *env, ptrdiff_t nargs, forb_pos = malloc(forb_count * sizeof(int32_t)); if (forb_pos) { for (ptrdiff_t i = 0; i < forb_count; i++) { - forb_pos[i] = env->extract_integer(env, env->vec_get(env, args[11], i)); + forb_pos[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[11], i))); } } } + /* One consolidated gate: any partial allocation above (silent + * "no kinsoku / no hyphenation" degradation) or a pending Lisp + * signal from a bad element type must fail the whole call — the + * Elisp engine is the correct fallback, not a subtly different + * layout. */ + if ((hyph_count > 0 && !hyph_pos) || + (forb_count > 0 && !forb_pos) || + !tail_pro || + env->non_local_exit_check(env) != emacs_funcall_exit_return) { + free(ideal_prefix); free(min_prefix); free(max_prefix); + free(glue_ideals); free(glue_shrinks); free(glue_stretches); + free(lead_spaces); free(trail_spaces); + free(hyph_pos); free(forb_pos); free(tail_pro); + return env->intern(env, "nil"); + } + /* Call the pure DP function */ ekp_result_t *result = ekp_break_with_prefixes( ideal_prefix, min_prefix, max_prefix, @@ -500,17 +321,17 @@ static bool extract_paragraph_data( } for (ptrdiff_t i = 0; i < prefix_len; i++) { - (*ideal_prefix)[i] = env->extract_integer(env, env->vec_get(env, args[0], i)); - (*min_prefix)[i] = env->extract_integer(env, env->vec_get(env, args[1], i)); - (*max_prefix)[i] = env->extract_integer(env, env->vec_get(env, args[2], i)); - (*lead_spaces)[i] = env->extract_integer(env, env->vec_get(env, args[9], i)); - (*trail_spaces)[i] = env->extract_integer(env, env->vec_get(env, args[10], i)); + (*ideal_prefix)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[0], i))); + (*min_prefix)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[1], i))); + (*max_prefix)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[2], i))); + (*lead_spaces)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[9], i))); + (*trail_spaces)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[10], i))); } for (size_t i = 0; i < *n; i++) { - (*glue_ideals)[i] = env->extract_integer(env, env->vec_get(env, args[3], i)); - (*glue_shrinks)[i] = env->extract_integer(env, env->vec_get(env, args[4], i)); - (*glue_stretches)[i] = env->extract_integer(env, env->vec_get(env, args[5], i)); + (*glue_ideals)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[3], i))); + (*glue_shrinks)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[4], i))); + (*glue_stretches)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[5], i))); } *hyph_count = env->vec_size(env, args[6]); @@ -519,13 +340,13 @@ static bool extract_paragraph_data( *hyph_pos = malloc(*hyph_count * sizeof(int32_t)); if (*hyph_pos) { for (ptrdiff_t i = 0; i < *hyph_count; i++) { - (*hyph_pos)[i] = env->extract_integer(env, env->vec_get(env, args[6], i)); + (*hyph_pos)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[6], i))); } } } - *hyph_width = env->extract_integer(env, args[7]); - *line_width = env->extract_integer(env, args[8]); + *hyph_width = clamp32(env->extract_integer(env, args[7])); + *line_width = clamp32(env->extract_integer(env, args[8])); *forb_count = env->vec_size(env, args[11]); *forb_pos = NULL; @@ -533,7 +354,7 @@ static bool extract_paragraph_data( *forb_pos = malloc(*forb_count * sizeof(int32_t)); if (*forb_pos) { for (ptrdiff_t i = 0; i < *forb_count; i++) { - (*forb_pos)[i] = env->extract_integer(env, env->vec_get(env, args[11], i)); + (*forb_pos)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[11], i))); } } } @@ -541,11 +362,26 @@ static bool extract_paragraph_data( *tail_pro = malloc(prefix_len * sizeof(int32_t)); if (*tail_pro) { for (ptrdiff_t i = 0; i < prefix_len; i++) { - (*tail_pro)[i] = env->extract_integer(env, env->vec_get(env, args[12], i)); + (*tail_pro)[i] = clamp32(env->extract_integer(env, env->vec_get(env, args[12], i))); } } - *hyphen_protrude = env->extract_integer(env, args[13]); - *first_line_width = env->extract_integer(env, args[14]); + *hyphen_protrude = clamp32(env->extract_integer(env, args[13])); + *first_line_width = clamp32(env->extract_integer(env, args[14])); + + if ((*hyph_count > 0 && !*hyph_pos) || + (*forb_count > 0 && !*forb_pos) || + !*tail_pro || + env->non_local_exit_check(env) != emacs_funcall_exit_return) { + free(*ideal_prefix); free(*min_prefix); free(*max_prefix); + free(*glue_ideals); free(*glue_shrinks); free(*glue_stretches); + free(*lead_spaces); free(*trail_spaces); + free(*hyph_pos); free(*forb_pos); free(*tail_pro); + *ideal_prefix = *min_prefix = *max_prefix = NULL; + *glue_ideals = *glue_shrinks = *glue_stretches = NULL; + *lead_spaces = *trail_spaces = NULL; + *hyph_pos = *forb_pos = *tail_pro = NULL; + return false; + } return true; } @@ -740,18 +576,6 @@ int emacs_module_init(struct emacs_runtime *runtime) defun(env, "ekp-c-cleanup", 0, 0, Fekp_c_cleanup, "Cleanup EKP C module resources."); - defun(env, "ekp-c-load-hyphenator", 1, 1, Fekp_c_load_hyphenator, - "Load hyphenation dictionary from PATH.\n\ -Returns hyphenator index or nil on failure.\n\n(fn PATH)"); - - defun(env, "ekp-c-set-spacing", 9, 9, Fekp_c_set_spacing, - "Set spacing parameters (in pixels).\n\n\ -Arguments are: LWS-IDEAL LWS-STRETCH LWS-SHRINK\n\ - MWS-IDEAL MWS-STRETCH MWS-SHRINK\n\ - CWS-IDEAL CWS-STRETCH CWS-SHRINK\n\n\ -LWS = Latin Word Space, MWS = Mixed, CWS = CJK.\n\n\ -(fn LWS-I LWS-+ LWS-- MWS-I MWS-+ MWS-- CWS-I CWS-+ CWS--)"); - defun(env, "ekp-c-set-penalties", 4, 7, Fekp_c_set_penalties, "Set Knuth-Plass algorithm penalties.\n\n\ LINE-PENALTY: base penalty per line break (default 10)\n\ @@ -763,18 +587,6 @@ LAST-LINE-SHORT-PENALTY: multiplier for short last lines (default 50.0)\n\n\ (fn LINE-PENALTY HYPHEN-PENALTY FITNESS-PENALTY LAST-LINE-RATIO \ &optional CONSEC-HYPHEN-PENALTY LAST-LINE-SHORT-PENALTY)"); - defun(env, "ekp-c-hyphenate", 2, 2, Fekp_c_hyphenate, - "Get hyphenation positions for WORD using HYPHENATOR-INDEX.\n\ -Returns list of positions where word can be hyphenated.\n\n(fn HYPHENATOR-INDEX WORD)"); - - defun(env, "ekp-c-break-lines", 4, 4, Fekp_c_break_lines, - "Break STRING into lines of LINE-WIDTH pixels.\n\n\ -Uses Knuth-Plass optimal line breaking with hyphenation.\n\ -HYPHENATOR-INDEX: index from `ekp-c-load-hyphenator', or -1 for none\n\ -MEASURE-FUNC: function that takes a string and returns pixel width\n\n\ -Returns (BREAKS . TOTAL-COST) where BREAKS is list of break positions.\n\n\ -(fn STRING HYPHENATOR-INDEX LINE-WIDTH MEASURE-FUNC)"); - defun(env, "ekp-c-break-with-arrays", 15, 15, Fekp_c_break_with_arrays, "Break lines using Elisp's pre-computed prefix arrays (preferred API).\n\n\ IDEAL-PREFIX: vector of ideal width prefix sums (n+1 elements)\n\ diff --git a/ekp_c/ekp_hyphen.c b/ekp_c/ekp_hyphen.c deleted file mode 100644 index 194ebdb..0000000 --- a/ekp_c/ekp_hyphen.c +++ /dev/null @@ -1,321 +0,0 @@ -/* - * ekp_hyphen.c - Liang hyphenation algorithm implementation - * - * 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. - * - * Fast, thread-safe hyphenation with pattern caching. - * Uses FNV-1a hash for O(1) pattern lookup. - */ - -#include "ekp_module.h" -#include -#include -#include -#include - -/* FNV-1a hash constants */ -#define FNV_OFFSET 14695981039346656037ULL -#define FNV_PRIME 1099511628211ULL - -static uint64_t fnv1a_hash(const char *data, size_t len) -{ - uint64_t hash = FNV_OFFSET; - for (size_t i = 0; i < len; i++) { - hash ^= (uint8_t)data[i]; - hash *= FNV_PRIME; - } - return hash; -} - -/* - * Parse a pattern like "hy3ph" into letters and values. - * Returns true on success. - */ -static bool parse_pattern(const char *pat, ekp_pattern_t *out) -{ - size_t pat_len = strlen(pat); - if (pat_len == 0 || pat_len >= EKP_MAX_PATTERN_LEN * 2) - return false; - - size_t pos = 0; - size_t letter_idx = 0; - size_t value_idx = 0; - - memset(out->values, 0, sizeof(out->values)); - memset(out->letters, 0, sizeof(out->letters)); - - while (pos < pat_len) { - /* Read optional digit */ - uint8_t digit = 0; - if (isdigit((unsigned char)pat[pos])) { - digit = pat[pos] - '0'; - pos++; - } - out->values[value_idx++] = digit; - - /* Read letter if present */ - if (pos < pat_len && !isdigit((unsigned char)pat[pos])) { - out->letters[letter_idx++] = pat[pos]; - pos++; - } - } - - out->len = letter_idx; - - /* Find non-zero range */ - size_t start = 0, end = value_idx; - while (start < end && out->values[start] == 0) start++; - while (end > start && out->values[end - 1] == 0) end--; - - out->offset = start; - - /* Shift values to start */ - if (start > 0) { - memmove(out->values, out->values + start, end - start); - memset(out->values + (end - start), 0, start); - } - - return letter_idx > 0; -} - -/* - * Load patterns from .dic file - */ -ekp_hyphenator_t *ekp_hyphen_create(const char *dict_path) -{ - FILE *fp = fopen(dict_path, "r"); - if (!fp) - return NULL; - - ekp_hyphenator_t *h = calloc(1, sizeof(*h)); - if (!h) { - fclose(fp); - return NULL; - } - - pthread_rwlock_init(&h->lock, NULL); - h->left_min = 2; - h->right_min = 2; - - /* First pass: count patterns */ - char line[256]; - size_t count = 0; - - if (!fgets(line, sizeof(line), fp)) { - /* empty file: no encoding line to skip; count loop sees EOF */ - } - - while (fgets(line, sizeof(line), fp)) { - size_t len = strlen(line); - if (len > 0 && line[len - 1] == '\n') - line[--len] = '\0'; - - /* Skip empty, comments, HYPHENMIN, patterns with / */ - if (len == 0 || line[0] == '%' || line[0] == '#') - continue; - if (strstr(line, "HYPHENMIN") || strchr(line, '/')) - continue; - - count++; - } - - /* Allocate patterns */ - h->patterns = calloc(count, sizeof(ekp_pattern_t)); - h->hash_size = count * 2; /* load factor 0.5 */ - h->hash_table = calloc(h->hash_size, sizeof(uint32_t)); - - if (!h->patterns || !h->hash_table) { - ekp_hyphen_destroy(h); - fclose(fp); - return NULL; - } - - /* Second pass: parse patterns */ - rewind(fp); - if (!fgets(line, sizeof(line), fp)) { - /* empty file: no encoding line to skip; parse loop sees EOF */ - } - - size_t idx = 0; - while (fgets(line, sizeof(line), fp)) { - size_t len = strlen(line); - if (len > 0 && line[len - 1] == '\n') - line[--len] = '\0'; - - if (len == 0 || line[0] == '%' || line[0] == '#') - continue; - if (strstr(line, "HYPHENMIN") || strchr(line, '/')) - continue; - - /* Handle ^^XX hex escapes */ - char decoded[256]; - char *dst = decoded; - const char *src = line; - while (*src) { - if (src[0] == '^' && src[1] == '^' && - isxdigit((unsigned char)src[2]) && - isxdigit((unsigned char)src[3])) { - char hex[3] = {src[2], src[3], 0}; - *dst++ = (char)strtol(hex, NULL, 16); - src += 4; - } else { - *dst++ = *src++; - } - } - *dst = '\0'; - - if (parse_pattern(decoded, &h->patterns[idx])) { - /* Insert into hash table */ - uint64_t hash = fnv1a_hash(h->patterns[idx].letters, - h->patterns[idx].len); - size_t slot = hash % h->hash_size; - - while (h->hash_table[slot] != 0) { - slot = (slot + 1) % h->hash_size; - } - h->hash_table[slot] = idx + 1; /* 1-indexed */ - - if (h->patterns[idx].len > h->max_pattern_len) - h->max_pattern_len = h->patterns[idx].len; - - idx++; - } - } - - h->pattern_count = idx; - fclose(fp); - return h; -} - -void ekp_hyphen_destroy(ekp_hyphenator_t *h) -{ - if (!h) return; - pthread_rwlock_destroy(&h->lock); - free(h->patterns); - free(h->hash_table); - free(h); -} - -/* - * Find pattern by letters (hash table lookup) - */ -static ekp_pattern_t *find_pattern(ekp_hyphenator_t *h, - const char *letters, size_t len) -{ - if (len == 0 || len > h->max_pattern_len) - return NULL; - - uint64_t hash = fnv1a_hash(letters, len); - size_t slot = hash % h->hash_size; - - for (size_t i = 0; i < h->hash_size; i++) { - uint32_t idx = h->hash_table[slot]; - if (idx == 0) - return NULL; - - ekp_pattern_t *p = &h->patterns[idx - 1]; - if (p->len == len && memcmp(p->letters, letters, len) == 0) - return p; - - slot = (slot + 1) % h->hash_size; - } - return NULL; -} - -/* - * Compute hyphenation positions for a word - * Thread-safe (read lock) - */ -int ekp_hyphen_word(ekp_hyphenator_t *h, const char *word, size_t len, - int8_t *positions, size_t max_pos) -{ - if (!h || !word || len == 0 || len > EKP_MAX_WORD_LEN - 2) - return 0; - - /* Check cache first */ - uint64_t word_hash = fnv1a_hash(word, len); - size_t cache_slot = word_hash % EKP_CACHE_SIZE; - - pthread_rwlock_rdlock(&h->lock); - - if (h->cache[cache_slot].hash == word_hash && - strncmp(h->cache[cache_slot].word, word, len) == 0) { - int count = h->cache[cache_slot].pos_count; - if (count <= (int)max_pos) { - memcpy(positions, h->cache[cache_slot].positions, - count * sizeof(int8_t)); - } - pthread_rwlock_unlock(&h->lock); - return count; - } - - pthread_rwlock_unlock(&h->lock); - - /* Compute hyphenation */ - char padded[EKP_MAX_WORD_LEN + 2]; - padded[0] = '.'; - for (size_t i = 0; i < len; i++) - padded[i + 1] = tolower((unsigned char)word[i]); - padded[len + 1] = '.'; - size_t padded_len = len + 2; - - uint8_t prio[EKP_MAX_WORD_LEN + 3]; - memset(prio, 0, sizeof(prio)); - - /* Apply matching patterns */ - pthread_rwlock_rdlock(&h->lock); - - for (size_t i = 0; i < padded_len - 1; i++) { - for (size_t j = i + 1; j <= padded_len && j <= i + h->max_pattern_len; j++) { - ekp_pattern_t *pat = find_pattern(h, padded + i, j - i); - if (pat) { - size_t val_len = pat->len + 1 - pat->offset; - for (size_t k = 0; k < val_len && k < sizeof(pat->values); k++) { - size_t pos = i + pat->offset + k; - if (pos < sizeof(prio) && pat->values[k] > prio[pos]) - prio[pos] = pat->values[k]; - } - } - } - } - - pthread_rwlock_unlock(&h->lock); - - /* Collect odd positions (subtract 1 for padding offset) */ - int8_t result[EKP_MAX_WORD_LEN]; - int count = 0; - - for (size_t i = 1; i < padded_len && count < EKP_MAX_WORD_LEN; i++) { - if (prio[i] & 1) { /* odd = break allowed */ - int pos = (int)i - 1; /* adjust for leading '.' */ - /* Apply margin constraints */ - if (pos >= h->left_min && pos <= (int)len - h->right_min) { - result[count++] = pos; - } - } - } - - /* Update cache */ - pthread_rwlock_wrlock(&h->lock); - - h->cache[cache_slot].hash = word_hash; - strncpy(h->cache[cache_slot].word, word, len); - h->cache[cache_slot].word[len] = '\0'; - memcpy(h->cache[cache_slot].positions, result, count * sizeof(int8_t)); - h->cache[cache_slot].pos_count = count; - - pthread_rwlock_unlock(&h->lock); - - /* Copy to output */ - int out_count = count < (int)max_pos ? count : (int)max_pos; - memcpy(positions, result, out_count * sizeof(int8_t)); - return out_count; -} diff --git a/ekp_c/ekp_kp.c b/ekp_c/ekp_kp.c index 4f8dbd5..fe72186 100644 --- a/ekp_c/ekp_kp.c +++ b/ekp_c/ekp_kp.c @@ -98,39 +98,7 @@ static inline double compute_demerits(double badness, int32_t penalty, } /* - * Parallel work item for demerits computation - */ -typedef struct { - ekp_paragraph_t *para; - int32_t line_width; - size_t start; - size_t end; - - /* Output arrays (pre-allocated) */ - double *demerits; - int32_t *backptrs; - int32_t *rest_pixels; - uint8_t *fitness; - int32_t *hyphen_counts; - int32_t *line_counts; - - /* Shared read-only input */ - const double *prev_demerits; - const uint8_t *prev_fitness; - const int32_t *prev_hyphen_counts; - const int32_t *prev_line_counts; - - /* Parameters */ - int line_penalty; - int hyphen_penalty; - int fitness_penalty; - double last_line_ratio; -} 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. + * Unified DP input structure for the array-based DP core. */ typedef struct { /* Prefix sum arrays */ @@ -418,225 +386,6 @@ static void dp_process_position( } } -/* - * Process a range of candidate breakpoints (for parallel execution) - * Now uses shared dp_process_position() core. - */ -static void process_dp_range(void *arg) -{ - dp_work_t *work = (dp_work_t *)arg; - ekp_paragraph_t *p = work->para; - 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, - .lead_spaces = NULL, - .trail_spaces = NULL, - .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, - .consec_hyphen_penalty = - ekp_global ? ekp_global->consec_hyphen_penalty : 100, - .last_line_short_penalty = - ekp_global ? ekp_global->last_line_short_penalty : 50.0, - .allow_emergency = true - }; - - /* Process each position in range */ - for (size_t i = work->start; i < work->end; i++) { - if (work->prev_demerits[i] >= EKP_INFINITY) - continue; - - dp_process_position(&in, i, - work->prev_demerits[i], - work->prev_fitness[i], - work->prev_hyphen_counts[i], - work->prev_line_counts[i], - work->demerits, - work->backptrs, - work->rest_pixels, - work->fitness, - work->hyphen_counts, - work->line_counts); - } - - free(glue_ideals); - free(glue_shrinks); - free(glue_stretches); -} - -/* - * Main line breaking function - */ -ekp_result_t *ekp_break_lines(ekp_paragraph_t *p, int32_t line_width) -{ - if (!p || p->box_count == 0 || line_width <= 0) - return NULL; - - size_t n = p->box_count; - - /* Allocate DP arrays */ - double *demerits = malloc((n + 1) * sizeof(double)); - int32_t *backptrs = malloc((n + 1) * sizeof(int32_t)); - int32_t *rest_pixels = malloc((n + 1) * sizeof(int32_t)); - uint8_t *fitness = malloc((n + 1) * sizeof(uint8_t)); - int32_t *hyphen_counts = malloc((n + 1) * sizeof(int32_t)); - int32_t *line_counts = malloc((n + 1) * sizeof(int32_t)); - - if (!demerits || !backptrs || !rest_pixels || - !fitness || !hyphen_counts || !line_counts) { - free(demerits); - free(backptrs); - free(rest_pixels); - free(fitness); - free(hyphen_counts); - free(line_counts); - 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; - hyphen_counts[i] = 0; - line_counts[i] = 0; - } - demerits[0] = 0.0; - - /* Get parameters */ - int line_penalty = ekp_global ? ekp_global->line_penalty : 10; - int hyphen_penalty = ekp_global ? ekp_global->hyphen_penalty : 50; - int fitness_penalty = ekp_global ? ekp_global->fitness_penalty : 100; - double last_ratio = ekp_global ? ekp_global->last_line_ratio : 0.5; - - /* - * Single-threaded DP: simple and correct. - * - * Note: Previous "parallel" implementation had data races - multiple - * threads writing to shared demerits[] array without synchronization. - * DP has inherent sequential dependencies (demerits[k] depends on all - * demerits[i] where i < k), making intra-paragraph parallelism complex. - * - * For real parallelism, use ekp_break_batch() to process multiple - * paragraphs concurrently - that's the correct granularity. - */ - dp_work_t work = { - .para = p, - .line_width = line_width, - .start = 0, - .end = n, - .demerits = demerits, - .backptrs = backptrs, - .rest_pixels = rest_pixels, - .fitness = fitness, - .hyphen_counts = hyphen_counts, - .line_counts = line_counts, - .prev_demerits = demerits, - .prev_fitness = fitness, - .prev_hyphen_counts = hyphen_counts, - .prev_line_counts = line_counts, - .line_penalty = line_penalty, - .hyphen_penalty = hyphen_penalty, - .fitness_penalty = fitness_penalty, - .last_line_ratio = last_ratio, - }; - - /* Iterative DP: O(n²) worst case, typically O(n·m) with early termination */ - for (size_t i = 0; i < n; i++) { - if (demerits[i] >= EKP_INFINITY) - continue; - - work.start = i; - work.end = i + 1; - process_dp_range(&work); - } - - /* Trace back optimal path */ - ekp_result_t *result = calloc(1, sizeof(*result)); - if (!result) { - free(demerits); - free(backptrs); - free(rest_pixels); - free(fitness); - free(hyphen_counts); - free(line_counts); - 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(int32_t)); - if (!result->breaks || !result->rest_pixels) { - ekp_result_destroy(result); - free(demerits); - free(backptrs); - free(rest_pixels); - free(fitness); - free(hyphen_counts); - free(line_counts); - 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(hyphen_counts); - free(line_counts); - - return result; -} - void ekp_result_destroy(ekp_result_t *r) { if (!r) @@ -877,6 +626,10 @@ ekp_result_t **ekp_break_batch(ekp_batch_input_t *inputs, size_t count) 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++) { @@ -946,16 +699,6 @@ int ekp_init(void) if (!ekp_global) return -1; - /* Default spacing */ - ekp_global->spacing.lws_ideal = 7; - ekp_global->spacing.lws_stretch = 3; - ekp_global->spacing.lws_shrink = 2; - ekp_global->spacing.mws_ideal = 5; - ekp_global->spacing.mws_stretch = 2; - ekp_global->spacing.mws_shrink = 1; - ekp_global->spacing.cws_ideal = 0; - ekp_global->spacing.cws_stretch = 2; - ekp_global->spacing.cws_shrink = 0; /* Default K-P parameters */ ekp_global->line_penalty = 10; @@ -965,15 +708,9 @@ int ekp_init(void) ekp_global->consec_hyphen_penalty = 100; ekp_global->last_line_short_penalty = 50.0; - /* Create thread pool */ - ekp_global->pool = ekp_pool_create(EKP_THREAD_POOL_SIZE); - if (!ekp_global->pool) { - free(ekp_global); - ekp_global = NULL; - return -1; - } - - pthread_mutex_init(&ekp_global->cache_lock, NULL); + /* 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; } @@ -983,20 +720,6 @@ void ekp_cleanup(void) if (!ekp_global) return; - /* Destroy hyphenators */ - for (size_t i = 0; i < ekp_global->hyphenator_count; i++) { - ekp_hyphen_destroy(ekp_global->hyphenators[i]); - } - - /* Destroy paragraph cache */ - if (ekp_global->para_cache) { - for (size_t i = 0; i < ekp_global->para_cache_size; i++) { - ekp_para_destroy(ekp_global->para_cache[i]); - } - free(ekp_global->para_cache); - } - - pthread_mutex_destroy(&ekp_global->cache_lock); ekp_pool_destroy(ekp_global->pool); free(ekp_global); ekp_global = NULL; diff --git a/ekp_c/ekp_module.h b/ekp_c/ekp_module.h index 0117c08..c6d7ca8 100644 --- a/ekp_c/ekp_module.h +++ b/ekp_c/ekp_module.h @@ -23,123 +23,17 @@ #include #include #include +#include /* Version */ #define EKP_VERSION_MAJOR 1 #define EKP_VERSION_MINOR 5 /* Limits */ -#define EKP_MAX_PATTERN_LEN 64 -#define EKP_MAX_WORD_LEN 256 -#define EKP_CACHE_SIZE 4096 -#define EKP_THREAD_POOL_SIZE 8 +#define EKP_THREAD_POOL_MAX 64 /* Infinity for impossible breaks */ -#define EKP_INFINITY 1e10 - -/* - * Box: indivisible content with fixed width - * Keep it small - we'll have thousands of these - */ -typedef struct { - const char *text; /* UTF-8 string, NOT owned */ - int32_t text_len; /* byte length */ - int32_t pixel_width; /* rendered width in pixels */ - uint8_t box_type; /* 0=latin, 1=cjk, 2=cjk_punct, 3=space */ - uint8_t start_type; /* first char type */ - uint8_t end_type; /* last char type */ -} ekp_box_t; - -/* - * Glue: flexible space between boxes - * The heart of Knuth-Plass: ideal ± stretch/shrink - */ -typedef struct { - int16_t ideal; /* natural width */ - int16_t stretch; /* max stretch */ - int16_t shrink; /* max shrink */ - uint8_t type; /* 0=none, 1=lws, 2=mws, 3=cws */ -} ekp_glue_t; - -/* - * Breakpoint candidate for DP - */ -typedef struct { - int32_t index; /* box index */ - int32_t prev; /* previous breakpoint index */ - double demerits; /* accumulated demerits */ - int32_t line_count; /* lines so far */ - uint8_t fitness; /* 0-3: tight to very-loose */ - uint8_t hyphen_count; /* consecutive hyphens */ - bool is_hyphen; /* ends with hyphen? */ -} ekp_breakpoint_t; - -/* - * Hyphenation pattern (Liang's algorithm) - * Compact representation: letters + priority values - */ -typedef struct { - char letters[EKP_MAX_PATTERN_LEN]; - uint8_t values[EKP_MAX_PATTERN_LEN + 1]; - uint8_t len; - uint8_t offset; /* where values start */ -} ekp_pattern_t; - -/* - * Hyphenator: compiled patterns + cache - * Thread-safe with read-write lock - */ -typedef struct { - ekp_pattern_t *patterns; - size_t pattern_count; - size_t max_pattern_len; - - /* Hash table for O(1) pattern lookup */ - uint32_t *hash_table; - size_t hash_size; - - /* Word cache (LRU) */ - struct { - uint64_t hash; - char word[EKP_MAX_WORD_LEN]; - int8_t positions[EKP_MAX_WORD_LEN]; - int pos_count; - } cache[EKP_CACHE_SIZE]; - size_t cache_head; - - pthread_rwlock_t lock; - - /* Margin constraints */ - int left_min; - int right_min; -} ekp_hyphenator_t; - -/* - * Paragraph: preprocessed text ready for line breaking - * All arrays are parallel: boxes[i] has glues[i], widths[i], etc. - */ -typedef struct { - ekp_box_t *boxes; - ekp_glue_t *glues; - size_t box_count; - - /* Prefix sums for O(1) range queries */ - int32_t *ideal_prefix; - int32_t *min_prefix; - int32_t *max_prefix; - - /* Hyphenation data */ - int32_t *hyphen_positions; - size_t hyphen_count; - int32_t hyphen_width; - - /* Original string (owned) */ - char *text; - size_t text_len; - - /* Hash for cache lookup */ - uint64_t hash; -} ekp_paragraph_t; +#define EKP_INFINITY HUGE_VAL /* unreachable sentinel */ /* * Line break result @@ -151,20 +45,11 @@ typedef struct { double total_cost; } ekp_result_t; -/* - * Global spacing parameters - */ -typedef struct { - int16_t lws_ideal, lws_stretch, lws_shrink; - int16_t mws_ideal, mws_stretch, mws_shrink; - int16_t cws_ideal, cws_stretch, cws_shrink; -} ekp_spacing_t; - /* * Thread pool for parallel computation */ typedef struct { - pthread_t threads[EKP_THREAD_POOL_SIZE]; + pthread_t threads[EKP_THREAD_POOL_MAX]; size_t thread_count; pthread_mutex_t queue_lock; pthread_cond_t queue_cond; @@ -186,15 +71,7 @@ typedef struct { * Global state */ typedef struct { - ekp_hyphenator_t *hyphenators[32]; /* by language */ - size_t hyphenator_count; - - ekp_paragraph_t **para_cache; - size_t para_cache_size; - pthread_mutex_t cache_lock; - - ekp_spacing_t spacing; - ekp_thread_pool_t *pool; + ekp_thread_pool_t *pool; /* created lazily on first batch */ /* K-P parameters */ int line_penalty; @@ -213,23 +90,14 @@ extern ekp_state_t *ekp_global; /* * API: Hyphenation */ -ekp_hyphenator_t *ekp_hyphen_create(const char *dict_path); -void ekp_hyphen_destroy(ekp_hyphenator_t *h); -int ekp_hyphen_word(ekp_hyphenator_t *h, const char *word, size_t len, - int8_t *positions, size_t max_pos); /* * API: Paragraph processing */ -ekp_paragraph_t *ekp_para_create(const char *text, size_t len, - ekp_hyphenator_t *h, - int32_t (*measure_fn)(const char *, size_t)); -void ekp_para_destroy(ekp_paragraph_t *p); /* * API: Line breaking (the main algorithm) */ -ekp_result_t *ekp_break_lines(ekp_paragraph_t *p, int32_t line_width); void ekp_result_destroy(ekp_result_t *r); /* @@ -313,6 +181,7 @@ ekp_result_t **ekp_break_batch( /* * API: Thread pool */ +size_t ekp_pool_default_threads(void); ekp_thread_pool_t *ekp_pool_create(size_t num_threads); void ekp_pool_destroy(ekp_thread_pool_t *pool); void ekp_pool_submit(ekp_thread_pool_t *pool, void (*func)(void *), void *arg); diff --git a/ekp_c/ekp_paragraph.c b/ekp_c/ekp_paragraph.c deleted file mode 100644 index 0aa35e3..0000000 --- a/ekp_c/ekp_paragraph.c +++ /dev/null @@ -1,408 +0,0 @@ -/* - * ekp_paragraph.c - Text preprocessing and box/glue construction - * - * 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 boring part that makes everything else fast. - * Get the data layout right, and the algorithm sings. - */ - -#include "ekp_module.h" -#include -#include - -/* Box types */ -#define BOX_LATIN 0 -#define BOX_CJK 1 -#define BOX_CJK_PUNCT 2 -#define BOX_SPACE 3 - -/* Glue types */ -#define GLUE_NONE 0 -#define GLUE_LWS 1 /* Latin word space */ -#define GLUE_MWS 2 /* Mixed (Latin-CJK) */ -#define GLUE_CWS 3 /* CJK character space */ - -/* UTF-8 helpers */ -static inline uint32_t utf8_decode(const char *s, int *len) -{ - unsigned char c = s[0]; - *len = 1; - - if ((c & 0x80) == 0) - return c; - - if ((c & 0xE0) == 0xC0) { - *len = 2; - return ((c & 0x1F) << 6) | (s[1] & 0x3F); - } - - if ((c & 0xF0) == 0xE0) { - *len = 3; - return ((c & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F); - } - - if ((c & 0xF8) == 0xF0) { - *len = 4; - return ((c & 0x07) << 18) | ((s[1] & 0x3F) << 12) | - ((s[2] & 0x3F) << 6) | (s[3] & 0x3F); - } - - return c; -} - -/* Character classification */ -static inline bool is_cjk(uint32_t cp) -{ - /* CJK Unified Ideographs and related blocks */ - return (cp >= 0x4E00 && cp <= 0x9FFF) || /* CJK Unified */ - (cp >= 0x3400 && cp <= 0x4DBF) || /* CJK Ext A */ - (cp >= 0x20000 && cp <= 0x2A6DF) || /* CJK Ext B */ - (cp >= 0x2A700 && cp <= 0x2B73F) || /* CJK Ext C */ - (cp >= 0x2B740 && cp <= 0x2B81F) || /* CJK Ext D */ - (cp >= 0xF900 && cp <= 0xFAFF) || /* CJK Compat */ - (cp >= 0x3000 && cp <= 0x303F) || /* CJK Symbols */ - (cp >= 0x3040 && cp <= 0x309F) || /* Hiragana */ - (cp >= 0x30A0 && cp <= 0x30FF) || /* Katakana */ - (cp >= 0xAC00 && cp <= 0xD7AF); /* Hangul */ -} - -static inline bool is_cjk_punct(uint32_t cp) -{ - return (cp >= 0x3000 && cp <= 0x303F) || /* CJK Symbols */ - (cp >= 0xFF00 && cp <= 0xFF60) || /* Fullwidth Forms */ - cp == 0x201C || cp == 0x201D || /* " " */ - cp == 0x2018 || cp == 0x2019; /* ' ' */ -} - -static inline bool is_whitespace(uint32_t cp) -{ - return cp == ' ' || cp == '\t' || cp == '\n' || cp == '\r' || - cp == 0x00A0 || cp == 0x3000; /* NBSP, ideographic space */ -} - -/* - * Determine box type from codepoint - */ -static uint8_t classify_char(uint32_t cp) -{ - if (is_whitespace(cp)) - return BOX_SPACE; - if (is_cjk_punct(cp)) - return BOX_CJK_PUNCT; - if (is_cjk(cp)) - return BOX_CJK; - return BOX_LATIN; -} - -/* - * Determine glue type between two boxes - */ -static uint8_t glue_between(uint8_t prev_end, uint8_t curr_start) -{ - if (prev_end == BOX_SPACE || curr_start == BOX_SPACE) - return GLUE_NONE; - - bool prev_latin = (prev_end == BOX_LATIN); - bool curr_latin = (curr_start == BOX_LATIN); - - if (prev_latin && curr_latin) - return GLUE_LWS; - if (!prev_latin && !curr_latin) - return GLUE_CWS; - return GLUE_MWS; -} - -/* - * Split text into boxes with hyphenation - */ -ekp_paragraph_t *ekp_para_create(const char *text, size_t len, - ekp_hyphenator_t *h, - int32_t (*measure_fn)(const char *, size_t)) -{ - if (!text || len == 0) - return NULL; - - ekp_paragraph_t *p = calloc(1, sizeof(*p)); - if (!p) - return NULL; - - /* Copy text */ - p->text = malloc(len + 1); - if (!p->text) { - free(p); - return NULL; - } - memcpy(p->text, text, len); - p->text[len] = '\0'; - p->text_len = len; - - /* Compute hash for caching */ - uint64_t hash = 14695981039346656037ULL; - for (size_t i = 0; i < len; i++) { - hash ^= (uint8_t)text[i]; - hash *= 1099511628211ULL; - } - p->hash = hash; - - /* First pass: count boxes (rough estimate) */ - size_t max_boxes = len + 1; - - /* Temporary arrays for first pass */ - size_t *box_starts = malloc(max_boxes * sizeof(size_t)); - size_t *box_lens = malloc(max_boxes * sizeof(size_t)); - uint8_t *box_types = malloc(max_boxes * sizeof(uint8_t)); - if (!box_starts || !box_lens || !box_types) { - free(box_starts); - free(box_lens); - free(box_types); - ekp_para_destroy(p); - return NULL; - } - - /* Tokenize into boxes */ - size_t box_count = 0; - size_t pos = 0; - size_t word_start = 0; - bool in_latin_word = false; - - while (pos < len) { - int char_len; - uint32_t cp = utf8_decode(text + pos, &char_len); - uint8_t type = classify_char(cp); - - if (in_latin_word) { - if (type != BOX_LATIN) { - /* End Latin word */ - box_starts[box_count] = word_start; - box_lens[box_count] = pos - word_start; - box_types[box_count] = BOX_LATIN; - box_count++; - in_latin_word = false; - } - } - - if (type == BOX_LATIN) { - if (!in_latin_word) { - word_start = pos; - in_latin_word = true; - } - } else { - /* Non-Latin: each character is its own box */ - box_starts[box_count] = pos; - box_lens[box_count] = char_len; - box_types[box_count] = type; - box_count++; - } - - pos += char_len; - } - - /* Flush final Latin word */ - if (in_latin_word) { - box_starts[box_count] = word_start; - box_lens[box_count] = pos - word_start; - box_types[box_count] = BOX_LATIN; - box_count++; - } - - /* Hyphenation: expand Latin words */ - size_t *hyphen_pos = malloc(max_boxes * sizeof(size_t)); - size_t hyphen_count = 0; - - /* Estimate expanded size */ - size_t expanded_boxes = box_count * 2; - ekp_box_t *boxes = calloc(expanded_boxes, sizeof(ekp_box_t)); - if (!boxes || !hyphen_pos) { - free(box_starts); - free(box_lens); - free(box_types); - free(hyphen_pos); - free(boxes); - ekp_para_destroy(p); - return NULL; - } - - size_t final_count = 0; - - for (size_t i = 0; i < box_count; i++) { - const char *box_text = text + box_starts[i]; - size_t box_len = box_lens[i]; - uint8_t type = box_types[i]; - - if (type == BOX_LATIN && h && box_len > 4) { - /* Try hyphenation */ - int8_t positions[EKP_MAX_WORD_LEN]; - int pos_count = ekp_hyphen_word(h, box_text, box_len, - positions, EKP_MAX_WORD_LEN); - - if (pos_count > 0) { - /* Split at hyphenation points */ - size_t prev_split = 0; - for (int j = 0; j < pos_count; j++) { - size_t split = positions[j]; - if (split <= prev_split || split >= box_len) - continue; - - boxes[final_count].text = box_text + prev_split; - boxes[final_count].text_len = split - prev_split; - boxes[final_count].box_type = BOX_LATIN; - boxes[final_count].start_type = BOX_LATIN; - boxes[final_count].end_type = BOX_LATIN; - boxes[final_count].pixel_width = - measure_fn ? measure_fn(boxes[final_count].text, - boxes[final_count].text_len) : 0; - - hyphen_pos[hyphen_count++] = final_count; - final_count++; - prev_split = split; - } - - /* Final segment */ - if (prev_split < box_len) { - boxes[final_count].text = box_text + prev_split; - boxes[final_count].text_len = box_len - prev_split; - boxes[final_count].box_type = BOX_LATIN; - boxes[final_count].start_type = BOX_LATIN; - boxes[final_count].end_type = BOX_LATIN; - boxes[final_count].pixel_width = - measure_fn ? measure_fn(boxes[final_count].text, - boxes[final_count].text_len) : 0; - final_count++; - } - continue; - } - } - - /* No hyphenation */ - boxes[final_count].text = box_text; - boxes[final_count].text_len = box_len; - boxes[final_count].box_type = type; - boxes[final_count].start_type = type; - boxes[final_count].end_type = type; - boxes[final_count].pixel_width = - measure_fn ? measure_fn(box_text, box_len) : 0; - final_count++; - } - - free(box_starts); - free(box_lens); - free(box_types); - - /* Build final arrays */ - p->boxes = boxes; - p->box_count = final_count; - - /* Hyphenation positions */ - p->hyphen_positions = malloc(hyphen_count * sizeof(int32_t)); - if (p->hyphen_positions) { - for (size_t i = 0; i < hyphen_count; i++) { - p->hyphen_positions[i] = hyphen_pos[i]; - } - p->hyphen_count = hyphen_count; - } - free(hyphen_pos); - - /* Hyphen width */ - p->hyphen_width = measure_fn ? measure_fn("-", 1) : 5; - - /* Build glues */ - p->glues = calloc(final_count, sizeof(ekp_glue_t)); - if (!p->glues) { - ekp_para_destroy(p); - return NULL; - } - - if (!ekp_global) { - ekp_para_destroy(p); - return NULL; - } - - ekp_spacing_t *sp = &ekp_global->spacing; - - for (size_t i = 0; i < final_count; i++) { - /* Check if after hyphenation point */ - bool after_hyphen = false; - for (size_t j = 0; j < p->hyphen_count; j++) { - if ((size_t)(p->hyphen_positions[j] + 1) == i) { - after_hyphen = true; - break; - } - } - - if (after_hyphen || i == 0) { - p->glues[i].type = GLUE_NONE; - continue; - } - - uint8_t prev_end = boxes[i - 1].end_type; - uint8_t curr_start = boxes[i].start_type; - uint8_t gtype = glue_between(prev_end, curr_start); - - p->glues[i].type = gtype; - switch (gtype) { - case GLUE_LWS: - p->glues[i].ideal = sp->lws_ideal; - p->glues[i].stretch = sp->lws_stretch; - p->glues[i].shrink = sp->lws_shrink; - break; - case GLUE_MWS: - p->glues[i].ideal = sp->mws_ideal; - p->glues[i].stretch = sp->mws_stretch; - p->glues[i].shrink = sp->mws_shrink; - break; - case GLUE_CWS: - p->glues[i].ideal = sp->cws_ideal; - p->glues[i].stretch = sp->cws_stretch; - p->glues[i].shrink = sp->cws_shrink; - break; - default: - break; - } - } - - /* Build prefix sums for O(1) range queries */ - p->ideal_prefix = calloc(final_count + 1, sizeof(int32_t)); - p->min_prefix = calloc(final_count + 1, sizeof(int32_t)); - p->max_prefix = calloc(final_count + 1, sizeof(int32_t)); - - if (!p->ideal_prefix || !p->min_prefix || !p->max_prefix) { - ekp_para_destroy(p); - return NULL; - } - - for (size_t i = 0; i < final_count; i++) { - int32_t box_w = boxes[i].pixel_width; - int32_t glue_ideal = p->glues[i].ideal; - int32_t glue_stretch = p->glues[i].stretch; - int32_t glue_shrink = p->glues[i].shrink; - - p->ideal_prefix[i + 1] = p->ideal_prefix[i] + box_w + glue_ideal; - p->min_prefix[i + 1] = p->min_prefix[i] + box_w + (glue_ideal - glue_shrink); - p->max_prefix[i + 1] = p->max_prefix[i] + box_w + (glue_ideal + glue_stretch); - } - - return p; -} - -void ekp_para_destroy(ekp_paragraph_t *p) -{ - if (!p) - return; - free(p->text); - free(p->boxes); - free(p->glues); - free(p->hyphen_positions); - free(p->ideal_prefix); - free(p->min_prefix); - free(p->max_prefix); - free(p); -} diff --git a/ekp_c/ekp_thread_pool.c b/ekp_c/ekp_thread_pool.c index 7912c96..f426360 100644 --- a/ekp_c/ekp_thread_pool.c +++ b/ekp_c/ekp_thread_pool.c @@ -18,6 +18,9 @@ #include "ekp_module.h" #include #include +#if !defined(_WIN32) +#include +#endif #define QUEUE_CAPACITY 1024 @@ -41,8 +44,12 @@ static void *worker_thread(void *arg) /* Dequeue work */ void (*func)(void *) = pool->queue[pool->queue_head].func; void *work_arg = pool->queue[pool->queue_head].arg; + bool was_full = + ((pool->queue_tail + 1) % pool->queue_size) == pool->queue_head; pool->queue_head = (pool->queue_head + 1) % pool->queue_size; pool->active_count++; + if (was_full) + pthread_cond_broadcast(&pool->done_cond); pthread_mutex_unlock(&pool->queue_lock); @@ -62,12 +69,25 @@ static void *worker_thread(void *arg) return NULL; } +size_t ekp_pool_default_threads(void) +{ + long n = 0; +#if defined(_SC_NPROCESSORS_ONLN) + n = sysconf(_SC_NPROCESSORS_ONLN); +#endif + if (n <= 0) + n = 4; + if (n > EKP_THREAD_POOL_MAX) + n = EKP_THREAD_POOL_MAX; + return (size_t)n; +} + ekp_thread_pool_t *ekp_pool_create(size_t num_threads) { if (num_threads == 0) - num_threads = EKP_THREAD_POOL_SIZE; - if (num_threads > EKP_THREAD_POOL_SIZE) - num_threads = EKP_THREAD_POOL_SIZE; + num_threads = ekp_pool_default_threads(); + if (num_threads > EKP_THREAD_POOL_MAX) + num_threads = EKP_THREAD_POOL_MAX; ekp_thread_pool_t *pool = calloc(1, sizeof(*pool)); if (!pool) @@ -134,17 +154,21 @@ void ekp_pool_submit(ekp_thread_pool_t *pool, void (*func)(void *), void *arg) pthread_mutex_lock(&pool->queue_lock); - size_t next_tail = (pool->queue_tail + 1) % pool->queue_size; - - /* Queue full - drop task (shouldn't happen with proper sizing) */ - if (next_tail == pool->queue_head) { + /* Queue full: wait for a worker to make room. Dropping the task + * here used to silently degrade the batch to the Elisp fallback + * exactly when parallelism mattered most. */ + while (((pool->queue_tail + 1) % pool->queue_size) == pool->queue_head + && !pool->shutdown) { + pthread_cond_wait(&pool->done_cond, &pool->queue_lock); + } + if (pool->shutdown) { pthread_mutex_unlock(&pool->queue_lock); return; } pool->queue[pool->queue_tail].func = func; pool->queue[pool->queue_tail].arg = arg; - pool->queue_tail = next_tail; + pool->queue_tail = (pool->queue_tail + 1) % pool->queue_size; pthread_cond_signal(&pool->queue_cond); pthread_mutex_unlock(&pool->queue_lock);