diff --git a/docs/superpowers/specs/2026-04-10-tolk-nvda-presence-design.md b/docs/superpowers/specs/2026-04-10-tolk-nvda-presence-design.md new file mode 100644 index 0000000..05fe18f --- /dev/null +++ b/docs/superpowers/specs/2026-04-10-tolk-nvda-presence-design.md @@ -0,0 +1,195 @@ +# Tolk NVDA Presence Compatibility Design + +## Goal + +Allow applications running under Wine or Proton to use the official upstream `Tolk.dll` unchanged while routing Tolk speech through the existing Linux NVDA-to-Cthulhu path. + +The compatibility layer must satisfy only the checks that Tolk performs when selecting its NVDA driver. It must not require replacing `Tolk.dll`, patching Tolk, or using a Tolk-specific DLL override. + +## Confirmed Constraints + +- The shipped `Tolk.dll` must remain the official upstream binary. +- The existing `wine2speechd` package already provides replacement `nvdaControllerClient32.dll` and `nvdaControllerClient64.dll` implementations for Linux. +- The current blocker is Tolk detection, not the downstream speech transport. +- Scope is limited to making Tolk believe NVDA is present; broader NVDA emulation is out of scope. + +## Current Tolk Behavior + +From `tolk/src/ScreenReaderDriverNVDA.cpp`, Tolk considers NVDA active only when both of the following succeed: + +1. `nvdaController_testIfRunning() == 0` +2. `FindWindow(L"wxWindowClassNR", L"NVDA")` returns a window handle + +If either check fails, Tolk will not select the NVDA driver and speech output through Tolk will fail. + +## Recommended Approach + +Extend the existing Wine NVDA compatibility stack with a minimal NVDA presence helper. The solution has two parts: + +1. The existing custom NVDA controller DLLs continue handling `nvdaController_*` API calls and forwarding them into the Linux bridge. +2. A lightweight Windows helper process running inside Wine creates the exact window Tolk expects for NVDA detection. + +This keeps the official `Tolk.dll` untouched and confines the compatibility contract to Tolk's actual checks. + +## Architecture + +### 1. NVDA controller DLLs + +The custom `nvdaControllerClient32.dll` and `nvdaControllerClient64.dll` remain the Wine-visible implementation that applications and Tolk load. + +Required behavior: + +- `nvdaController_speakText` forwards speech to the existing Linux bridge. +- `nvdaController_brailleMessage` continues current behavior. +- `nvdaController_cancelSpeech` continues current behavior. +- `nvdaController_testIfRunning` returns success only when the Linux bridge is reachable and the compatibility environment is operational. + +`nvdaController_testIfRunning` must not return success solely because the DLL loaded. It is the main guard against false positive Tolk detection. + +### 2. NVDA presence helper + +A small Windows executable is added to the Wine-side compatibility package. Its only job is to create and maintain a top-level window with: + +- class name: `wxWindowClassNR` +- window title: `NVDA` + +Required behavior: + +- starts quickly and remains idle +- single-instance per Wine prefix or session +- exits cleanly without user interaction +- does not present visible UI unless Wine forces a window surface +- can be launched independently or on demand by the controller DLL + +### 3. Startup coordination + +The presence helper must be running before Tolk calls `FindWindow`, or Tolk detection will fail. + +Acceptable coordination strategies: + +- preferred: launch the helper as part of the existing Wine accessibility startup path +- acceptable: lazily launch the helper the first time the custom NVDA DLL is loaded, then wait briefly for the window to appear + +The preferred strategy is external startup rather than in-DLL process creation because it separates concerns and avoids loader-time side effects. + +## Detection Contract + +Tolk compatibility is considered successful only when all of the following are true: + +- official `Tolk.dll` loads normally +- Tolk loads the custom NVDA controller DLL +- `nvdaController_testIfRunning()` returns `0` +- `FindWindow(L"wxWindowClassNR", L"NVDA")` succeeds +- `Tolk_DetectScreenReader()` returns `NVDA` +- `Tolk_Output()` delivers speech through the existing Linux bridge + +If the Linux bridge is unavailable, the compatibility layer must fail closed: + +- `nvdaController_testIfRunning()` returns failure +- Tolk does not report NVDA as active + +The dummy NVDA window alone must never make Tolk think speech is available. + +## Packaging + +The compatibility feature belongs with the Wine NVDA compatibility stack, not in Tolk itself. + +Expected package contents: + +- `nvdaControllerClient32.dll` +- `nvdaControllerClient64.dll` +- `nvda-presence-helper.exe` or similarly named helper +- startup integration so the helper is available in Wine and Proton environments where Tolk-based games run + +No `Tolk.dll` replacement or override is added. + +## Error Handling + +### Bridge unavailable + +- `nvdaController_testIfRunning` returns failure +- speech-related entry points return the existing failure behavior +- Tolk should not detect NVDA + +### Helper missing or failed to start + +- `FindWindow` fails +- Tolk should not detect NVDA +- logging should identify missing helper startup distinctly from bridge connectivity failures + +### Duplicate helper instances + +- duplicates must resolve harmlessly, preferably by allowing one owner and exiting the rest + +## Logging + +Add targeted logging in the compatibility layer only. Logging should make these states distinguishable: + +- controller DLL loaded +- bridge connectivity check succeeded or failed +- presence helper started +- presence window created +- Tolk compatibility ready + +No Tolk-side logging changes are needed because Tolk is not being modified. + +## Testing + +### Functional test + +Create or reuse a small Wine test application that: + +1. calls `Tolk_Load()` +2. calls `Tolk_DetectScreenReader()` +3. calls `Tolk_Output(L\"test\", false)` + +Expected result with bridge and helper active: + +- `Tolk_DetectScreenReader()` returns `NVDA` +- speech reaches Cthulhu through the existing NVDA path + +### Negative tests + +1. Bridge down, helper up: + `Tolk_DetectScreenReader()` must not return `NVDA` +2. Bridge up, helper down: + `Tolk_DetectScreenReader()` must not return `NVDA` +3. Both down: + `Tolk_DetectScreenReader()` must not return `NVDA` + +### Regression check + +Verify that existing non-Tolk NVDA speech consumers continue using the current `wine2speechd` path without requiring Tolk-specific configuration. + +## Out of Scope + +- adding a plugin mechanism to Tolk +- maintaining a Tolk fork +- broader NVDA desktop emulation beyond what Tolk checks +- compatibility with applications that perform additional NVDA-specific probing outside the current `wine2speechd` contract +- anti-cheat or anti-tamper guarantees beyond avoiding Tolk replacement + +## Risks + +### Wine window behavior + +The helper must create a window that `FindWindow` can discover reliably under Wine and Proton. If Wine normalizes or alters class registration behavior, the helper may need adjustment. + +### Timing + +If a game calls Tolk very early, helper startup races could cause intermittent detection failure. This is why pre-starting the helper is preferred. + +### Split responsibility + +The controller DLL and helper must agree on readiness. If they drift apart, Tolk may see the window but still fail to speak. The fail-closed `testIfRunning` check is the protection against this. + +## Recommendation + +Implement the feature in the existing Wine NVDA compatibility stack, not in Tolk and not in Cthulhu core. + +The smallest correct implementation is: + +1. fix or confirm `nvdaController_testIfRunning()` behavior in the custom DLL +2. add a minimal Wine helper that exposes the NVDA window Tolk checks for +3. wire startup so the helper is available before Tolk detection occurs +4. verify with a small Tolk test program under Wine