Skip to content

Surface hidden failures: out-of-order phonemes and unhandled exceptions - #2404

Merged
stakira merged 4 commits into
openutau:masterfrom
KakaruHayate:fix/surface-hidden-failures
Sep 16, 2026
Merged

stakira merged 4 commits into
openutau:masterfrom
KakaruHayate:fix/surface-hidden-failures

Conversation

@KakaruHayate

Copy link
Copy Markdown
Contributor

Two small, unrelated changes that make hidden failures visible. Kept as two commits so they can be split into two PRs if you prefer.

1. ustx: log when a phonemizer returns out-of-order phonemes

UPart.Update silently repairs out-of-order phoneme positions:

// Safety treatment after phonemizer output and phoneme overrides.
for (int i = phonemes.Count - 2; i >= 0; --i) {
    phonemes[i].position = Math.Min(phonemes[i].position, phonemes[i + 1].position - 10);
}

That repair covers two very different causes:

  • a user dragging a phoneme offset past its neighbour — legitimate, and the reason the code exists;
  • a phonemizer returning positions out of order — a bug.

Because the repair is silent, the second case is invisible: the part just renders slightly odd. No test can see it either, because the test harness calls Phonemizer.Process directly and never reaches this code path.

The patch adds a read-only ordering check on rawPosition, which holds the phonemizer output before user overrides are applied, so only the phonemizer case is reported. One warning per part; the repair itself is unchanged.

I measured the built-in phonemizers through the test harness and found no violations across 598 phonemes / 318 note groups, so this should normally stay quiet — silence is the useful signal here.

2. app: also handle unobserved task and unhandled UI exceptions

Program.InitLogging registers AppDomain.CurrentDomain.UnhandledException, which can log an exception but cannot keep the process alive. Nothing is registered for the two cases users actually hit:

  • TaskScheduler.UnobservedTaskException — a faulted Task that nobody awaited is dropped until finalization, and then ignored.
  • Dispatcher.UIThread.UnhandledException — this code base has 47 async void methods, 31 of them menu/button handlers. An exception escaping one reaches the dispatcher, nothing marks it handled, and the process exits: the window simply disappears.

Both hooks are registered in OnFrameworkInitializationCompleted, where Avalonia is up and we are on the UI thread. Exceptions are logged and surfaced through the existing ErrorMessageNotification path.

This does not fix any root cause. A null reference still has to be fixed where it is thrown (as #2390 did). It only turns "process gone" into "error reported", so an unforeseen exception costs the user a dialog instead of their session.

Verification

dotnet build with both patches applied 0 errors, 0 warnings
dotnet test full suite 238 passed, 0 failed — identical to the baseline before the change

Neither change alters behaviour for the existing suite; the App-level one only changes behaviour in cases that would otherwise terminate the process.

Happy to adjust or drop either part — for example lowering the new log to Debug, or splitting this into two PRs.

`UPart.Update` runs a loop that silently repairs out-of-order phoneme positions
(`Math.Min(position, next.position - 10)`). That safety treatment covers two very
different cases: a user dragging a phoneme offset past its neighbour, which is
legitimate, and a phonemizer that returned positions out of order, which is a bug.

The repair is silent, so the second case is invisible: the part renders "a bit
odd" and no test can see it either, because the test harness calls
`Phonemizer.Process` directly and never goes through this code path.

This adds a read-only ordering check on `rawPosition` -- the phonemizer output
before user overrides are applied -- so only the phonemizer case is reported.
One warning per part, and nothing changes about the repair itself.
`Program.InitLogging` registers `AppDomain.CurrentDomain.UnhandledException`,
which can log an exception but can not keep the process alive. There is nothing
registered for the two cases users actually hit:

* `TaskScheduler.UnobservedTaskException` -- an exception in a `Task` nobody
  awaited is dropped until the finalizer runs, and then ignored by default.
* `Dispatcher.UIThread.UnhandledException` -- the code base has 47 `async void`
  methods, 31 of them menu/button handlers. An exception escaping one of those
  reaches the dispatcher with no handler to mark it handled, so the process
  exits. The user sees the window disappear.

This registers both hooks in `OnFrameworkInitializationCompleted` (where Avalonia
is up and we are on the UI thread). Exceptions are logged and reported through
the existing `ErrorMessageNotification` path, so the app stays alive instead of
vanishing. It does not fix any root cause -- a null reference still has to be
fixed where it is thrown -- it only turns "process gone" into "error reported".
`UPart.Update` runs a loop that silently repairs out-of-order phoneme positions
(`Math.Min(position, next.position - 10)`). That safety treatment covers two very
different cases: a user dragging a phoneme offset past its neighbour, which is
legitimate, and a phonemizer that returned positions out of order, which is a bug.

The repair is silent, so the second case is invisible: the part renders "a bit
odd" and no test can see it either, because the test harness calls
`Phonemizer.Process` directly and never goes through this code path.

This adds a read-only ordering check on `rawPosition` -- the phonemizer output
before user overrides are applied -- so only the phonemizer case is reported.
One warning per part, and nothing changes about the repair itself.
`Program.InitLogging` registers `AppDomain.CurrentDomain.UnhandledException`,
which can log an exception but can not keep the process alive. There is nothing
registered for the two cases users actually hit:

* `TaskScheduler.UnobservedTaskException` -- an exception in a `Task` nobody
  awaited is dropped until the finalizer runs, and then ignored by default.
* `Dispatcher.UIThread.UnhandledException` -- the code base has 47 `async void`
  methods, 31 of them menu/button handlers. An exception escaping one of those
  reaches the dispatcher with no handler to mark it handled, so the process
  exits. The user sees the window disappear.

This registers both hooks in `OnFrameworkInitializationCompleted` (where Avalonia
is up and we are on the UI thread). Exceptions are logged and reported through
the existing `ErrorMessageNotification` path, so the app stays alive instead of
vanishing. It does not fix any root cause -- a null reference still has to be
fixed where it is thrown -- it only turns "process gone" into "error reported".
@stakira
stakira merged commit fe0894d into openutau:master Sep 16, 2026
3 checks passed
@KakaruHayate
KakaruHayate deleted the fix/surface-hidden-failures branch September 16, 2026 15:16
keirokeer added a commit to keirokeer/OpenUtau-lunai that referenced this pull request Sep 16, 2026
…nutau#2408 openutau#2411 openutau#2412 openutau#2414)


Log out-of-order phonemes, clear ClassicSinger loaded on FreeMemory,
add VibratoPreset default ctor, fix G2p lang replacements without IDs,
and drop undefined part curves after load.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants