From bbf6e3e617d6327a80077b2a9e83554416032246 Mon Sep 17 00:00:00 2001 From: will wade Date: Fri, 21 Aug 2026 21:19:57 +0000 Subject: [PATCH] feat(capi): speed percent honours engine range; resets emit buffer-clear event Two engine-side fixes behind the v6 first-impressions report (see Dasher-GTK #51 and governance RFC 0005/0015 discussion): - dasher_set_speed_percent clamped to a fixed 20-400 (raw 32-640), silently truncating the top of Dasher v5's speed range (v5 allowed raw 10-800, i.e. up to 500 percent). The clamp now derives from the engine's declared LP_MAX_BITRATE bounds (raw 1-1000 -> ~1-625 percent), so frontends using the percent helper can no longer lose v5 speeds. - dasher_reset, dasher_reset_output_text and dasher_set_alphabet_id clear the edit buffer without any insert/delete deltas, forcing every frontend to know this and re-sync shadow buffers by hand (Dasher-GTK's output pane kept stale text after New). They now emit output event type 2 (buffer clear, empty text) so subscribers can clear their copy. Additive: existing callbacks that switch on 0/1 are unaffected. Docs updated in dasher.h and C_API.md; tests extended in test_capi.cpp, test_capi_extended.cpp, and test_parameters.cpp (whose old 20-400 assertions encoded the replaced contract). Signed-off-by: will wade --- docs/C_API.md | 8 +++--- src/CAPI.cpp | 28 ++++++++++++++++++--- src/dasher.h | 16 +++++++++--- tests/test_capi.cpp | 13 ++++++++++ tests/test_capi_extended.cpp | 47 ++++++++++++++++++++++++++++++++++++ tests/test_parameters.cpp | 12 ++++++--- 6 files changed, 112 insertions(+), 12 deletions(-) diff --git a/docs/C_API.md b/docs/C_API.md index e1a6be9f..d153a958 100644 --- a/docs/C_API.md +++ b/docs/C_API.md @@ -347,8 +347,10 @@ Receives real-time text events without polling. Event types: |------|---------| | 0 | Text output (insertion) | | 1 | Text delete (backspace) | +| 2 | Buffer cleared wholesale — `dasher_reset`, `dasher_reset_output_text`, or an alphabet change (which clears the buffer). `text` is empty; shadow buffers must be cleared, not diffed | -The callback fires on the thread calling `dasher_frame()`. +The callback fires on the thread calling `dasher_frame()`. Event type 2 may +also fire on the thread calling the reset function itself. ### Message Callback @@ -421,7 +423,7 @@ int dasher_get_speed_percent(dasher_ctx* ctx); void dasher_set_speed_percent(dasher_ctx* ctx, int percent); ``` -- Range: 20–400 (clamped). Default: 100. +- Range: the engine's declared `LP_MAX_BITRATE` bounds (raw 1–1000 → ~1–625 %, in whole-percent steps of the raw unit). The previous fixed 20–400 clamp truncated Dasher v5's top speeds (v5 allowed raw 10–800 = up to 500 %); values are now clamped to the manifest range instead. Default: 100. - Internally maps to `LP_MAX_BITRATE`: `bitrate = percent / 100.0 * 160` ## Parameters @@ -655,7 +657,7 @@ dasher_frame(ctx, System.currentTimeMillis(), cmds, cmdCount, null, null) 2. **`out_command_count` is total int count, not command count** — divide by 6 for command count. 3. **String pointers are ephemeral** — copy immediately if you need the value beyond the current API call. 4. **Localization state is global** — changing locale in one context affects all contexts (shared static state). -5. **Speed percent mapping** — 100% = `LP_MAX_BITRATE` of 160, range 20–400%. +5. **Speed percent mapping** — 100% = `LP_MAX_BITRATE` of 160, clamped to the engine's declared `LP_MAX_BITRATE` range (not a fixed percent cap). 6. **Language model ID gap** — IDs are 0, 2, 3, 4 (no ID 1). Historical. 7. **No font rendering** — text width is estimated as `characters * fontSize / 2`. Actual font metrics are the frontend's responsibility. 8. **Polygons are decomposed into line segments** — no filled polygon opcode. diff --git a/src/CAPI.cpp b/src/CAPI.cpp index fd0e5319..f895534f 100644 --- a/src/CAPI.cpp +++ b/src/CAPI.cpp @@ -945,11 +945,20 @@ DASHER_API const char* dasher_get_output_text(dasher_ctx* ctx) { return ctx->tlString.c_str(); } +// Notify subscribers that the edit buffer was cleared wholesale (event type +// 2). Insert/delete deltas (0/1) can't express this, and without the event +// every frontend has to know which API calls clear the buffer and re-sync +// manually — Dasher-GTK's stale output pane after "New" was exactly this bug. +static void notify_buffer_cleared(dasher_ctx* ctx) { + if (ctx->outputCb) ctx->outputCb(2, "", ctx->outputCbUserData); +} + DASHER_API void dasher_reset_output_text(dasher_ctx* ctx) { if (!ctx) return; ctx->editBuffer.clear(); ctx->cursorPos = 0; ctx->rateTimestamps.clear(); + notify_buffer_cleared(ctx); } DASHER_API void dasher_reset(dasher_ctx* ctx) { @@ -958,6 +967,7 @@ DASHER_API void dasher_reset(dasher_ctx* ctx) { ctx->cursorPos = 0; ctx->rateTimestamps.clear(); ctx->intf->SetOffset(0, true); + notify_buffer_cleared(ctx); } DASHER_API const char* dasher_get_alphabet_id(dasher_ctx* ctx) { @@ -970,6 +980,7 @@ DASHER_API void dasher_set_alphabet_id(dasher_ctx* ctx, const char* alphabet_id) if (!ctx || !ctx->intf || !alphabet_id) return; ctx->editBuffer.clear(); ctx->cursorPos = 0; + notify_buffer_cleared(ctx); // documented side effect: setting clears the buffer if (!ctx->realized) { ctx->pendingAlphabet = alphabet_id; return; @@ -1049,9 +1060,20 @@ DASHER_API void dasher_set_speed_percent(dasher_ctx* ctx, int percent) { if (!ctx || !ctx->intf) return; try { const double base = 160.0; - const int clamped = (percent < 20) ? 20 : (percent > 400) ? 400 : percent; - long bitrate = static_cast(lround_int(clamped / 100.0 * base)); - if (bitrate < 1) bitrate = 1; + // Clamp to the engine's declared LP_MAX_BITRATE range rather than the + // historic 20–400 %: that cap was raw 32–640, which silently truncated + // the top of Dasher v5's speed range (v5 allowed raw 10–800, i.e. up + // to 500 %). Frontend speed controls should take their bounds from the + // same manifest (dasher_get_parameter_info). + long min_bitrate = 1, max_bitrate = 1000; + auto it = Dasher::Settings::parameter_defaults.find(Dasher::LP_MAX_BITRATE); + if (it != Dasher::Settings::parameter_defaults.end() && it->second.max > 0) { + min_bitrate = it->second.min; + max_bitrate = it->second.max; + } + long bitrate = static_cast(lround_int(percent / 100.0 * base)); + if (bitrate < min_bitrate) bitrate = min_bitrate; + if (bitrate > max_bitrate) bitrate = max_bitrate; ctx->intf->SetLongParameter(Dasher::LP_MAX_BITRATE, bitrate); } catch (const std::exception& e) { log_boundary_error(ctx, "dasher_set_speed_percent", e.what()); diff --git a/src/dasher.h b/src/dasher.h index 52a6e20f..31672dc9 100644 --- a/src/dasher.h +++ b/src/dasher.h @@ -140,7 +140,11 @@ DASHER_API int dasher_get_language_model_param_key(int id, int index); // Returns -1 if not found. DASHER_API int dasher_find_parameter_key(const char* enum_key_name); -// Get/set speed as a percentage (100 = default, range 20-400). +// Get/set speed as a percentage (100 = raw LP_MAX_BITRATE 160). The set +// clamps to the engine's declared LP_MAX_BITRATE range (see +// dasher_get_parameter_info), not a fixed percent cap — Dasher v5 allowed raw +// 10–800 (6–500 %), and the historic 20–400 percent clamp silently truncated +// the top of that range. DASHER_API int dasher_get_speed_percent(dasher_ctx* ctx); DASHER_API void dasher_set_speed_percent(dasher_ctx* ctx, int percent); @@ -341,8 +345,14 @@ DASHER_API void dasher_reset_settings(dasher_ctx* ctx); // Event types: // 0 = text output — text is the string being inserted // 1 = text delete — text is the string being removed (backspace) -// -// The callback fires on the thread that calls dasher_frame(). +// 2 = buffer clear — text is empty; the whole buffer was discarded +// (dasher_reset, dasher_reset_output_text, or an +// alphabet change). Deltas alone cannot express this, +// so subscribers maintaining a shadow buffer must +// treat this as "clear your copy". +// +// The callback fires on the thread that calls dasher_frame(). Event type 2 +// may also fire from the thread calling the reset function itself. typedef void (*dasher_output_callback)(int event_type, const char* text, void* user_data); diff --git a/tests/test_capi.cpp b/tests/test_capi.cpp index 19b9dc8e..fdec3985 100644 --- a/tests/test_capi.cpp +++ b/tests/test_capi.cpp @@ -73,6 +73,19 @@ TEST(parameters) { speed = dasher_get_speed_percent(ctx); ASSERT_EQ(speed, 150); + // The percent setter clamps to the engine's declared LP_MAX_BITRATE range + // (raw 1–1000 → ~1–625 %), not the historic fixed 20–400 cap: Dasher v5 + // allowed raw 10–800 (up to 500 %) and power users rely on the top of it. + dasher_set_speed_percent(ctx, 500); // v5's max, raw 800 + ASSERT_EQ(dasher_get_speed_percent(ctx), 500); + dasher_set_speed_percent(ctx, 100000); // clamped to raw 1000 → 625 % + ASSERT_EQ(dasher_get_speed_percent(ctx), 625); + dasher_set_speed_percent(ctx, 0); // clamped up to raw 1 → 1 % + ASSERT_EQ(dasher_get_speed_percent(ctx), 1); + // Restore a mid value for the rest of the suite. + dasher_set_speed_percent(ctx, 150); + ASSERT_EQ(dasher_get_speed_percent(ctx), 150); + int model = dasher_get_language_model_id(ctx); CHECK(model >= 0); diff --git a/tests/test_capi_extended.cpp b/tests/test_capi_extended.cpp index 9fe23e6c..d5479f96 100644 --- a/tests/test_capi_extended.cpp +++ b/tests/test_capi_extended.cpp @@ -192,6 +192,53 @@ TEST(reset) { dasher_destroy(ctx); } +TEST(reset_emits_buffer_clear_event) { + // Resets clear the edit buffer without insert/delete deltas, so they must + // announce themselves as event type 2 — subscribers keeping a shadow + // buffer cannot reconstruct "everything vanished" from 0/1 events + // (Dasher-GTK's stale output pane after "New" was this bug). + dasher_ctx* ctx = create_isolated_context(); + ASSERT(ctx != nullptr); + dasher_set_screen_size(ctx, 800, 600); + + static int clear_events = 0; + static int other_events = 0; + clear_events = 0; + other_events = 0; + + dasher_set_output_callback( + ctx, + [](int event_type, const char* text, void*) { + if (event_type == 2) { + ASSERT(text != nullptr); // empty string, never null + clear_events++; + } else if (event_type == 0 || event_type == 1) { + other_events++; + } + // Unknown future event types must be ignored, not fatal. + }, + nullptr); + + dasher_reset_output_text(ctx); + ASSERT_EQ(clear_events, 1); + + dasher_reset(ctx); + ASSERT_EQ(clear_events, 2); + + // Alphabet changes clear the buffer as a documented side effect, so they + // fire the event too. + dasher_set_alphabet_id(ctx, "English with numerals and limited punctuation"); + ASSERT_EQ(clear_events, 3); + + // Setting the *same* alphabet still cleared the buffer (historic + // behaviour), so the event fires regardless. + dasher_set_alphabet_id(ctx, "English with numerals and limited punctuation"); + ASSERT_EQ(clear_events, 4); + + (void)other_events; + dasher_destroy(ctx); +} + TEST(save_settings) { static int save_test_counter = 0; char shared_dir[256]; diff --git a/tests/test_parameters.cpp b/tests/test_parameters.cpp index 5481cfcb..3ee5f6c6 100644 --- a/tests/test_parameters.cpp +++ b/tests/test_parameters.cpp @@ -123,17 +123,23 @@ TEST(param_speed_clamping) { ASSERT(ctx != nullptr); dasher_set_screen_size(ctx, 800, 600); + // The percent setter clamps to the engine's declared LP_MAX_BITRATE range + // (raw 1–1000 → ~1–625 %), replacing the historic fixed 20–400 cap that + // truncated Dasher v5's top speeds (v5 allowed raw 10–800 = up to 500 %). dasher_set_speed_percent(ctx, 50); int speed = dasher_get_speed_percent(ctx); - ASSERT(speed >= 20 && speed <= 400); + ASSERT(speed >= 1 && speed <= 625); dasher_set_speed_percent(ctx, 1000); speed = dasher_get_speed_percent(ctx); - ASSERT(speed <= 400); + ASSERT(speed <= 625); // raw clamped to 1000 → 625 % dasher_set_speed_percent(ctx, 1); speed = dasher_get_speed_percent(ctx); - ASSERT(speed >= 20); + ASSERT(speed >= 1); // raw clamped up to the engine minimum + + dasher_set_speed_percent(ctx, 500); // v5's maximum, must survive exactly + ASSERT(dasher_get_speed_percent(ctx) == 500); dasher_destroy(ctx); }