Public Access
Keep drawing the terminal after Android takes the GPU context away
The phone's terminal was blank whenever it was connected. Not slow, not mis-sized, not disconnected: a live session accepting keystrokes, acknowledging output and drawing nothing at all. ◆ THE WEBGL ADDON DOES NOT RECOVER FROM A LOST CONTEXT AND DOES NOT FAIL LOUDLY. It stays loaded over a dead context and renders an empty rectangle, which is xterm's documented behaviour and the reason its guidance is to subscribe to onContextLoss and dispose. This page never did, and until there was a phone there was no reason to notice. Losing the context is ordinary on Android and nearly unheard of on Windows, which is what made this a one-head bug in shared code. Collapsing the renderer sets the native view to GONE — Avalonia's AndroidNativeControlHostImpl.HideWithSize, read out of the assembly rather than guessed at — and a WebView with no surface has no GL context. The shell collapses it every time a tab starts connecting, every time the connect sheet opens and every time the app is backgrounded. Worse, the ordering guarantees it for the first session on every launch: OpenSessionAsync sends SESSION_OPENED before the tab reports a session, so IsTerminalShowing is still false and the pane, the terminal and its GL context are all built inside a collapsed WebView. WebView2 hides a child HWND and keeps rendering throughout, which docs/platform-flags.md measured at length. The addon is not reloaded after a loss. A pane that lost the context once is on a surface that will do it again, and thrashing between renderers is worse than being slow — the DOM renderer is what the existing fallback comment already argues for, because a blank pane is not usable and a slow one is. The comment above MINIMUM_FITTABLE_PIXELS was wrong for this head and is corrected with it. It asserted that collapsing the WebView leaves this page's viewport alone, so no observer fires and the guard protects nothing — true of a hidden child HWND, false of a GONE view, which its parent's layout skips outright. On the phone the guard is the only thing standing between a lock, a connect sheet or a trip to the background and a remote pty reflowed to 2x1. Also on the way past: the renderer-timeout message told phone users to install the Microsoft Edge WebView2 runtime. That is the other blank-terminal failure mode's message, and naming a runtime that cannot exist on the device is worse than saying nothing at the one moment somebody is trying to work out what went wrong. It now names Android's own WebView on that head, as a runtime check for the reason MainWindowViewModel.GestureWait records beside its own. Not verified on a device — there is no handset or emulator here, and no test covers this page. The diagnosis is the decompiled hide path plus xterm's own requirement, not an observation. 523 tests over the shell and the terminal pass, and both heads build.
This commit is contained in:
@@ -11247,10 +11247,7 @@ internal sealed partial class VaultViewModel(
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
Abandon(
|
||||
attempt,
|
||||
"The terminal did not start, so nothing was connected. The Microsoft Edge WebView2 "
|
||||
+ "runtime is probably missing or blocked; install it and try again.");
|
||||
Abandon(attempt, RendererNeverStarted);
|
||||
}
|
||||
catch (SshHostKeyUnknownException exception)
|
||||
{
|
||||
@@ -11275,6 +11272,29 @@ internal sealed partial class VaultViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>What a renderer that never attached is reported as.</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The wait is translated rather than reported for the reason <see cref="OpenSessionAsync"/> gives —
|
||||
/// <see cref="TimeoutException"/> says only "The operation has timed out" — and the whole value of the
|
||||
/// translation is naming where to look. Which is why it cannot be one sentence: the desktop's answer is
|
||||
/// a runtime this application does not install, and the phone has no such runtime and no such answer.
|
||||
/// Telling somebody on a handset to install Microsoft Edge WebView2 is worse than saying nothing, at the
|
||||
/// one moment they are trying to work out what went wrong.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A runtime check rather than a constructor parameter, for the reason
|
||||
/// <c>MainWindowViewModel.GestureWait</c> records at length: which renderer is behind the terminal is a
|
||||
/// fact about the platform this assembly is running on, not about one installation of it.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private static string RendererNeverStarted =>
|
||||
OperatingSystem.IsAndroid()
|
||||
? "The terminal did not start, so nothing was connected. Android's WebView is probably "
|
||||
+ "disabled or updating; check it in Settings and try again."
|
||||
: "The terminal did not start, so nothing was connected. The Microsoft Edge WebView2 "
|
||||
+ "runtime is probably missing or blocked; install it and try again.";
|
||||
|
||||
/// <summary>Says, in one place, that an attempt ended without a session and why.</summary>
|
||||
/// <remarks>
|
||||
/// The reason goes to two places on purpose. The status line is where somebody watching this screen is
|
||||
|
||||
@@ -255,8 +255,31 @@ function createSession(sessionId) {
|
||||
// WebGL where it is available. Falling back rather than failing matters because a software
|
||||
// renderer is slow but usable, whereas a blank pane is not — and remote desktops and VMs
|
||||
// routinely have no usable GPU context.
|
||||
//
|
||||
// ◆ THE CONTEXT-LOSS HANDLER IS THE HALF THAT WAS MISSING, AND ON A PHONE IT IS THE WHOLE THING.
|
||||
//
|
||||
// The addon does not recover from a lost GPU context by itself, and it does not fail loudly either:
|
||||
// it stays loaded over a dead context and draws nothing at all. What that looks like from outside is
|
||||
// a terminal that is connected, still accepting keystrokes, still acknowledging output — and blank.
|
||||
// xterm's own guidance is to dispose the addon and let the DOM renderer take over, which is what this
|
||||
// does; the addon is not reloaded afterwards, because a pane that lost the context once is on a
|
||||
// surface that will do it again and thrashing between renderers is worse than being slow.
|
||||
//
|
||||
// Losing it is ordinary on Android and nearly unheard of on Windows, which is why this went unnoticed
|
||||
// for so long. Collapsing the renderer sets the native view to GONE — see
|
||||
// AndroidNativeControlHostImpl.HideWithSize — and a WebView with no surface has no GL context. The
|
||||
// shell collapses it every time a tab starts connecting, every time the connect sheet opens and every
|
||||
// time the app is backgrounded, so on a phone the first loss arrives within seconds of the first
|
||||
// session. WebView2 hides a child HWND instead and keeps rendering throughout; see
|
||||
// docs/platform-flags.md.
|
||||
try {
|
||||
term.loadAddon(new WebglAddon.WebglAddon());
|
||||
const webgl = new WebglAddon.WebglAddon();
|
||||
|
||||
// Subscribed before loadAddon, because loadAddon is what activates the addon and a context that is
|
||||
// already gone can be reported from inside that call.
|
||||
webgl.onContextLoss(() => webgl.dispose());
|
||||
|
||||
term.loadAddon(webgl);
|
||||
} catch (error) {
|
||||
console.warn('WebGL renderer unavailable; falling back to canvas.', error);
|
||||
}
|
||||
@@ -296,10 +319,17 @@ function activate(sessionId) {
|
||||
// caller, because more than one path reaches here: a minimised window, and a splitter dragged to the edge
|
||||
// once splits land.
|
||||
//
|
||||
// It is *not* what protects the vault's lock screen, which an earlier version of this comment claimed.
|
||||
// Collapsing the host's WebView hides a native child window without resizing it, so this page's viewport
|
||||
// does not change, no observer fires and this function is never called — measured with a live shell, and
|
||||
// confirmed by removing the guard and finding the lock cycle equally clean. See docs/platform-flags.md.
|
||||
// It is *not* what protects the vault's lock screen on the desktop, which an earlier version of this
|
||||
// comment claimed. Collapsing WebView2 hides a native child window without resizing it, so this page's
|
||||
// viewport does not change, no observer fires and this function is never called — measured with a live
|
||||
// shell, and confirmed by removing the guard and finding the lock cycle equally clean. See
|
||||
// docs/platform-flags.md.
|
||||
//
|
||||
// On the phone it *is* load-bearing, and that is the one place the two heads differ here. Android hides a
|
||||
// native child by setting it GONE, and a GONE view is skipped by its parent's layout — so collapsing the
|
||||
// renderer really does take this page's viewport to nothing, the observer really does fire, and without
|
||||
// the guard every lock, every connect sheet and every trip to the background would reflow the remote pty
|
||||
// to 2x1 and mangle the scrollback it wrapped.
|
||||
const MINIMUM_FITTABLE_PIXELS = 40;
|
||||
|
||||
function resize(session, sessionId) {
|
||||
|
||||
Reference in New Issue
Block a user