From 89b7801e0e24b2eeac6dc56ad876524efa71093d Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:44:03 +0000 Subject: [PATCH 1/2] display: converge X root on the configure stop/start resize path The batched chromium-configure stop/start path applied the Neko screen reconfig with a bare setResolutionViaNeko and trusted the RPC return, unlike the live-resize path (PatchDisplay), which polls the X root and retries the reconfig when it does not converge. Neko applies screen configuration asynchronously and can silently drop a reconfig that lands while a previous one is still in flight, leaving the X root at the dummy DDX default (3840x2160). When that happens on the configure path, Chromium is then restarted in --kiosk and maximizes onto the stale root, so the live view renders the browser in a small corner of the streamed frame. It is intermittent because it only manifests when the reconfig is dropped, and the bare RPC returns success without confirming it took effect. Extract the apply-poll-retry loop into applyResolutionAndConverge and route both PatchDisplay and the configure stop/start path through it, so the two resize paths share one convergence guarantee instead of the guard living on only one of them (which is how this regression arose). --- server/cmd/api/api/chromium_configure.go | 2 +- server/cmd/api/api/display.go | 157 +++++++++++------------ 2 files changed, 77 insertions(+), 82 deletions(-) diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go index 6fff956f..f1755f44 100644 --- a/server/cmd/api/api/chromium_configure.go +++ b/server/cmd/api/api/chromium_configure.go @@ -690,7 +690,7 @@ func chromiumDisplayApplyWhileStopped(ctx context.Context, s *ApiService, plan * } var err error if s.isNekoEnabled() { - err = s.setResolutionViaNeko(ctx, w, h, rr) + _, _, err = s.applyResolutionAndConverge(ctx, w, h, rr, true) } else { err = s.setResolutionXorgViaXrandr(ctx, w, h, rr) } diff --git a/server/cmd/api/api/display.go b/server/cmd/api/api/display.go index 5e1b3760..6395dedc 100644 --- a/server/cmd/api/api/display.go +++ b/server/cmd/api/api/display.go @@ -115,92 +115,26 @@ func (s *ApiService) PatchDisplay(ctx context.Context, req oapi.PatchDisplayRequ // Route to appropriate resolution change handler if displayMode == "xorg" { - useNeko := s.isNekoEnabled() - if useNeko { - log.Info("using Neko API for Xorg resolution change") - err = s.setResolutionViaNeko(ctx, width, height, refreshRate) - } else { - log.Info("using xrandr for Xorg resolution change (Neko disabled)") - err = s.setResolutionXorgViaXrandr(ctx, width, height, refreshRate) - } - // Re-assert the maximized window state via CDP, then verify the - // X root has reached the requested size. Mutter reflows a - // maximized (or fullscreen) window onto the new root - // automatically, so the CDP call's only job is to make sure the - // window is in the state that triggers the reflow. The X root - // poll is the authoritative post-condition: it's the value the - // server actually set and stays panel-robust if mutter ever - // gains a taskbar (a maximized window would then be smaller - // than the root by the panel's reserved space). - // - // Both are fatal on failure — returning 200 with the X root - // still at the old size, or the window stuck in normal state, - // would leave the caller with no signal of the mismatch. The - // previous approach of restarting chromium so it could re-apply - // --start-maximized had the same effective contract (the - // restart blocked the response) but cost ~9s per resize and - // wiped browser-side state (Emulation.* overrides, devtools - // sessions). The restart_chromium request field is still - // accepted for API compatibility but no longer triggers a - // restart on this path. + var realizedW, realizedH int + realizedW, realizedH, err = s.applyResolutionAndConverge(ctx, width, height, refreshRate, s.isNekoEnabled()) if err == nil { - // Wait for the X root to settle, then use it as the - // realized size in the response. The wait returns early - // when either (a) xrandr reports the requested size — the - // common case — or (b) consecutive reads are stable for a - // short window, capturing libxcvt's rounded size on - // requests it can't honour exactly (CVT 8-pixel grid + - // FWXGA bump for 1360×768 → 1366×768). - // - // The poll absorbs transient X root states — chromium - // running in --kiosk briefly pushes the root to the dummy - // DDX's max mode (3840×2160) while mutter settles on the - // new screen, and a single immediate read would catch that - // transient instead of the steady-state size. - // - // On the Neko path, retry the reconfig RPC when X never - // converges. Neko applies screen configuration asynchronously - // and can drop/clobber a PATCH that lands while a previous - // reconfig (e.g. its boot 1920x1080) is still in flight — - // the new request is silently lost and X stays at the dummy - // DDX default (3840x2160). Polling X harder won't recover; - // only re-issuing the reconfig will. - const maxAttempts = 3 - var realizedW, realizedH int - var converged bool - for attempt := 0; attempt < maxAttempts; attempt++ { - realizedW, realizedH, converged = s.waitForXRootRealized(ctx, width, height, 10*time.Second) - if converged { - break - } - if !useNeko || attempt == maxAttempts-1 { - break - } - log.Warn("X root did not converge after resize, retrying neko reconfig", - "attempt", attempt+1, + if realizedW != width || realizedH != height { + log.Info("X root differs from request after resize", "requested", fmt.Sprintf("%dx%d", width, height), "realized", fmt.Sprintf("%dx%d", realizedW, realizedH)) - if retryErr := s.setResolutionViaNeko(ctx, width, height, refreshRate); retryErr != nil { - err = retryErr - break - } - } - if err == nil { - if !converged { - log.Error("display did not converge to requested mode after retries", - "requested", fmt.Sprintf("%dx%d", width, height), - "realized", fmt.Sprintf("%dx%d", realizedW, realizedH)) - err = fmt.Errorf("display did not converge to %dx%d (X root reports %dx%d)", width, height, realizedW, realizedH) - } else { - if realizedW != width || realizedH != height { - log.Info("X root differs from request after resize", - "requested", fmt.Sprintf("%dx%d", width, height), - "realized", fmt.Sprintf("%dx%d", realizedW, realizedH)) - } - width, height = realizedW, realizedH - } } + width, height = realizedW, realizedH } + // Re-assert the maximized window state via CDP so mutter reflows the + // window onto the new root; applyResolutionAndConverge already verified + // the root size, which is the authoritative post-condition. Fatal on + // failure — returning 200 with the window stuck in its old state would + // leave the caller with no signal of the mismatch. The previous approach + // of restarting chromium so it could re-apply --start-maximized had the + // same effective contract but cost ~9s per resize and wiped browser-side + // state (Emulation.* overrides, devtools sessions); restart_chromium is + // still accepted for API compatibility but no longer triggers a restart + // on this path. if err == nil { if cdpErr := s.setWindowMaximizedViaCDP(ctx); cdpErr != nil { log.Error("CDP maximize re-assert failed after Xorg resolution change", "error", cdpErr) @@ -590,6 +524,67 @@ func abs(x int) int { return x } +// applyResolutionAndConverge sets the Xorg resolution — via Neko when enabled, +// otherwise xrandr — and waits for the X root to actually reach the requested +// size, returning the realized dimensions. +// +// The poll absorbs transient X root states: chromium running in --kiosk briefly +// pushes the root to the dummy DDX's max mode (3840x2160) while mutter settles +// on the new screen, and a single immediate read would catch that transient. It +// also captures libxcvt's rounded size on requests the driver can't honour +// exactly (CVT 8-pixel grid + FWXGA bump for 1360x768 -> 1366x768). +// +// On the Neko path it re-issues the reconfig when the root never converges. +// Neko applies screen configuration asynchronously and can drop/clobber a +// request that lands while a previous reconfig (e.g. its boot 1920x1080) is +// still in flight — the request is silently lost and X stays at the dummy DDX +// default (3840x2160). Polling X harder won't recover it, only re-issuing will. +// setResolutionViaNeko returns as soon as the request is accepted, so every +// caller that needs the resolution to have taken effect — the live-resize path +// before re-asserting the window, and the configure stop/start path before +// relaunching Chromium in --kiosk — must go through here rather than call +// setResolutionViaNeko directly. +func (s *ApiService) applyResolutionAndConverge(ctx context.Context, width, height, refreshRate int, useNeko bool) (int, int, error) { + log := logger.FromContext(ctx) + var err error + if useNeko { + log.Info("using Neko API for Xorg resolution change") + err = s.setResolutionViaNeko(ctx, width, height, refreshRate) + } else { + log.Info("using xrandr for Xorg resolution change (Neko disabled)") + err = s.setResolutionXorgViaXrandr(ctx, width, height, refreshRate) + } + if err != nil { + return 0, 0, err + } + const maxAttempts = 3 + var realizedW, realizedH int + var converged bool + for attempt := 0; attempt < maxAttempts; attempt++ { + realizedW, realizedH, converged = s.waitForXRootRealized(ctx, width, height, 10*time.Second) + if converged { + break + } + if !useNeko || attempt == maxAttempts-1 { + break + } + log.Warn("X root did not converge after resize, retrying neko reconfig", + "attempt", attempt+1, + "requested", fmt.Sprintf("%dx%d", width, height), + "realized", fmt.Sprintf("%dx%d", realizedW, realizedH)) + if retryErr := s.setResolutionViaNeko(ctx, width, height, refreshRate); retryErr != nil { + return realizedW, realizedH, retryErr + } + } + if !converged { + log.Error("display did not converge to requested mode after retries", + "requested", fmt.Sprintf("%dx%d", width, height), + "realized", fmt.Sprintf("%dx%d", realizedW, realizedH)) + return realizedW, realizedH, fmt.Errorf("display did not converge to %dx%d (X root reports %dx%d)", width, height, realizedW, realizedH) + } + return realizedW, realizedH, nil +} + // setViewportViaCDP resizes the browser viewport using the CDP // Emulation.setDeviceMetricsOverride command. This is near-instant and does // not require restarting Chromium or Xvfb. From 189d12dadf03eff4220f215ecd020854f9f8f9e4 Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:12:17 +0000 Subject: [PATCH 2/2] display: route configure xrandr path through applyResolutionAndConverge Collapse the neko/xrandr branch on the configure stop/start path into a single applyResolutionAndConverge call, matching PatchDisplay. Removes the last direct setResolution call so the two mirror paths can no longer drift, and makes the helper's doc comment accurate. --- server/cmd/api/api/chromium_configure.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/server/cmd/api/api/chromium_configure.go b/server/cmd/api/api/chromium_configure.go index f1755f44..7d995314 100644 --- a/server/cmd/api/api/chromium_configure.go +++ b/server/cmd/api/api/chromium_configure.go @@ -688,12 +688,7 @@ func chromiumDisplayApplyWhileStopped(ctx context.Context, s *ApiService, plan * } return nil } - var err error - if s.isNekoEnabled() { - _, _, err = s.applyResolutionAndConverge(ctx, w, h, rr, true) - } else { - err = s.setResolutionXorgViaXrandr(ctx, w, h, rr) - } + _, _, err := s.applyResolutionAndConverge(ctx, w, h, rr, s.isNekoEnabled()) if err != nil { return cfg500ConfigureStep(chromiumConfigureStepDisplay, err.Error()) }