From 46845bce49d289144d31c6d584027ab6c5da3d98 Mon Sep 17 00:00:00 2001 From: std-external <201375710+std-external@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:09:00 +0800 Subject: [PATCH 1/5] modoptions: keep the key config screen visible above the settings page The Everest keyboard/button config UI is an entity added at depth 0 while the material mod options page is drawn on top of it, and the acrylic pass renders the whole scene into a blurred snapshot before drawing the page again. Suppress acrylic while the config screen is open, freeze the page's input, and lift the config screen to a foreground depth instead. That also fixes the white flash on Esc: the old fix pushed the page to depth 1, which put it behind depth-0 Overworld entities (mountain, snow), so the bright world leaked through while the config screen faded out. --- Source/MaterialModOptions.cs | 32 +++++++++++++++++++++++++++++++- Source/QolSettingsOverlay.cs | 8 +++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Source/MaterialModOptions.cs b/Source/MaterialModOptions.cs index 56f3ed8..ff8c036 100644 --- a/Source/MaterialModOptions.cs +++ b/Source/MaterialModOptions.cs @@ -90,6 +90,7 @@ public sealed class MaterialModOptions : Oui, IMaterialAcrylicPage { private string settingSearchText = ""; private string imeText = ""; private SearchTarget searchTarget; + private bool externalOverlayOpen; public bool SuppressNormalRender { get; set; } @@ -98,7 +99,9 @@ internal static MaterialModOptions? ActivePage { if (Engine.Scene is not Overworld overworld) return null; MaterialModOptions? page = overworld.Current as MaterialModOptions ?? overworld.Next as MaterialModOptions; - return page is { Visible: true } ? page : null; + // The key/button config screen uses the normal composition path; feeding the + // blurred acrylic snapshot from this page would hide it behind the settings UI. + return page is { Visible: true, externalOverlayOpen: false } ? page : null; } } @@ -206,6 +209,12 @@ public override void Update() { } if (menu is null) return; + RefreshExternalOverlay(); + if (externalOverlayOpen) { + // The key/button config screen owns input while it is open. + menu.Focused = false; + return; + } bool acceptsInput = pauseLevel is not null || Selected && Focused; if (acceptsInput && inputDelay <= 0f && dropdownItem is null && compositePopupItem is null && UpdateSearchInput(layout)) { @@ -1004,6 +1013,21 @@ private void EnsureCompositePopupSelectionVisible(ModOptionsLayout layout, TextM } } + private void RefreshExternalOverlay() { + bool open = false; + if (Scene is { } scene) { + foreach (Entity entity in scene.Entities) { + if (entity is not (KeyboardConfigUI or ButtonConfigUI)) continue; + open = true; + // Keep the page at its normal depth and lift the config screen instead. + // Moving the page behind depth-0 scene entities made the bright world + // leak through for a frame when the config screen faded out (white flash). + if (entity.Depth > -100_000) entity.Depth = -100_000; + } + } + externalOverlayOpen = open; + } + private void SelectItem(TextMenu.Item item) { if (menu is null || menu.Current == item) return; menu.Current?.OnLeave?.Invoke(); @@ -1034,6 +1058,12 @@ private void EnsureTabVisible(ModOptionsLayout layout) { } private void UpdateInteractions(ModOptionsLayout layout) { + if (externalOverlayOpen) { + // Do not leave hidden setting rows alive as touch targets under the config UI. + MaterialTouch.Publish(this, [], TouchContext); + motion.Update([]); + return; + } List targets = [ new MaterialInteractionTarget("mod-options.search.tabs", layout.TabSearch, Focused: searchTarget == SearchTarget.Tabs, TouchKind: "text", TouchText: tabSearchText, diff --git a/Source/QolSettingsOverlay.cs b/Source/QolSettingsOverlay.cs index 45b072a..41a9a86 100644 --- a/Source/QolSettingsOverlay.cs +++ b/Source/QolSettingsOverlay.cs @@ -108,7 +108,13 @@ public override void Update() { recordingRefreshTimer -= Engine.RawDeltaTime; if (IsRecorderTab && IsRecorderLibrary && recordingRefreshTimer <= 0f) RefreshRecordingFiles(); - if (bindingConfig is not null) return; + if (bindingConfig is not null) { + // The key/button config screen handles its own input; do not leave the hidden + // setting rows alive as touch targets underneath it. + MaterialTouch.Publish(this, [], TouchContext); + motion.Update([]); + return; + } OverlayLayout layout = OverlayLayout.Create(1f - Ease.CubeOut(ease)); rowScroll.Update(MaxRowScroll(layout)); From 507ddf43100da8ff0950809381dd88276e8e3b21 Mon Sep 17 00:00:00 2001 From: std-external <201375710+std-external@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:10:58 +0800 Subject: [PATCH 2/5] modoptions: lift the config screen just above the settings page --- Source/MaterialModOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/MaterialModOptions.cs b/Source/MaterialModOptions.cs index ff8c036..a04dee9 100644 --- a/Source/MaterialModOptions.cs +++ b/Source/MaterialModOptions.cs @@ -1022,7 +1022,7 @@ private void RefreshExternalOverlay() { // Keep the page at its normal depth and lift the config screen instead. // Moving the page behind depth-0 scene entities made the bright world // leak through for a frame when the config screen faded out (white flash). - if (entity.Depth > -100_000) entity.Depth = -100_000; + if (entity.Depth >= Depth) entity.Depth = Depth - 1; } } externalOverlayOpen = open; From 5f93de6af231ffdc400e2cc87e4ff88c6d1afef8 Mon Sep 17 00:00:00 2001 From: Dilant Date: Thu, 22 Oct 2026 23:39:16 +0800 Subject: [PATCH 3/5] modoptions: keep the acrylic composition stable around config screens --- Source/IMaterialAcrylicPage.cs | 6 ++++++ Source/MaterialAcrylicRenderer.cs | 28 ++++++++++++++++++++++-- Source/MaterialModOptions.cs | 36 ++++++++++++++++++++++--------- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/Source/IMaterialAcrylicPage.cs b/Source/IMaterialAcrylicPage.cs index cd88a20..815480f 100644 --- a/Source/IMaterialAcrylicPage.cs +++ b/Source/IMaterialAcrylicPage.cs @@ -4,3 +4,9 @@ internal interface IMaterialAcrylicPage { bool SuppressNormalRender { get; set; } void RenderMaterialContent(bool acrylicActive); } + +internal interface IMaterialAcrylicOverlay { + void HideOverlaysForAcrylicCapture(); + void RestoreOverlaysAfterAcrylicCapture(); + void RenderOverlaysAboveAcrylic(); +} diff --git a/Source/MaterialAcrylicRenderer.cs b/Source/MaterialAcrylicRenderer.cs index 2683bf7..9ffe4d7 100644 --- a/Source/MaterialAcrylicRenderer.cs +++ b/Source/MaterialAcrylicRenderer.cs @@ -86,6 +86,7 @@ private static void RenderScene(On.Monocle.Scene.orig_Render orig, Scene self) { orig(self); return; } + IMaterialAcrylicOverlay? overlay = page as IMaterialAcrylicOverlay; GraphicsDevice graphics = Engine.Graphics.GraphicsDevice; RenderTargetBinding[] originalTargets = graphics.GetRenderTargets(); @@ -103,7 +104,19 @@ private static void RenderScene(On.Monocle.Scene.orig_Render orig, Scene self) { graphics.SetRenderTarget(sceneTarget); graphics.Viewport = new Viewport(0, 0, sceneTarget.Width, sceneTarget.Height); graphics.Clear(Engine.ClearColor); - orig(self); + HiresSnow? snow = (self as Overworld)?.Snow; + bool renderSnow = snow is { Visible: true }; + if (renderSnow) snow!.Visible = false; + try { + overlay?.HideOverlaysForAcrylicCapture(); + try { + orig(self); + } finally { + overlay?.RestoreOverlaysAfterAcrylicCapture(); + } + } finally { + if (renderSnow) snow!.Visible = true; + } sceneRendered = true; float sampleScale = MathHelper.Lerp(0.8f, 3.1f, @@ -123,6 +136,8 @@ private static void RenderScene(On.Monocle.Scene.orig_Render orig, Scene self) { page.SuppressNormalRender = false; DrawMaterialPage(page); + DrawOverlays(overlay); + if (renderSnow) snow!.Render(self); successfulFrames++; } catch (Exception exception) { page.SuppressNormalRender = false; @@ -163,6 +178,15 @@ private static void DrawTexture( } private static void DrawMaterialPage(IMaterialAcrylicPage page) { + DrawScreenBatch(() => page.RenderMaterialContent(acrylicActive: true)); + } + + private static void DrawOverlays(IMaterialAcrylicOverlay? overlay) { + if (overlay is null) return; + DrawScreenBatch(() => overlay.RenderOverlaysAboveAcrylic()); + } + + private static void DrawScreenBatch(Action render) { bool begun = false; try { Draw.SpriteBatch.Begin( @@ -175,7 +199,7 @@ private static void DrawMaterialPage(IMaterialAcrylicPage page) { Engine.ScreenMatrix ); begun = true; - page.RenderMaterialContent(acrylicActive: true); + render(); } finally { if (begun) Draw.SpriteBatch.End(); } diff --git a/Source/MaterialModOptions.cs b/Source/MaterialModOptions.cs index a04dee9..e919371 100644 --- a/Source/MaterialModOptions.cs +++ b/Source/MaterialModOptions.cs @@ -14,7 +14,7 @@ namespace Celeste.Mod.MicroblocksQolUtils; /// TextMenu, so custom entries and binding/config sub-pages keep their original behavior; /// this class only owns grouping, navigation, layout and mouse interaction. /// -public sealed class MaterialModOptions : Oui, IMaterialAcrylicPage { +public sealed class MaterialModOptions : Oui, IMaterialAcrylicPage, IMaterialAcrylicOverlay { private const float ScreenWidth = 1920f; private const float ScreenHeight = 1080f; private const float TabHeight = MaterialSplitPageLayout.TabHeight; @@ -91,6 +91,8 @@ public sealed class MaterialModOptions : Oui, IMaterialAcrylicPage { private string imeText = ""; private SearchTarget searchTarget; private bool externalOverlayOpen; + private readonly List externalOverlays = []; + private readonly List<(Entity Entity, bool Visible)> hiddenOverlays = []; public bool SuppressNormalRender { get; set; } @@ -99,9 +101,7 @@ internal static MaterialModOptions? ActivePage { if (Engine.Scene is not Overworld overworld) return null; MaterialModOptions? page = overworld.Current as MaterialModOptions ?? overworld.Next as MaterialModOptions; - // The key/button config screen uses the normal composition path; feeding the - // blurred acrylic snapshot from this page would hide it behind the settings UI. - return page is { Visible: true, externalOverlayOpen: false } ? page : null; + return page is { Visible: true } ? page : null; } } @@ -1014,18 +1014,34 @@ private void EnsureCompositePopupSelectionVisible(ModOptionsLayout layout, TextM } private void RefreshExternalOverlay() { - bool open = false; + externalOverlays.Clear(); if (Scene is { } scene) { foreach (Entity entity in scene.Entities) { if (entity is not (KeyboardConfigUI or ButtonConfigUI)) continue; - open = true; - // Keep the page at its normal depth and lift the config screen instead. - // Moving the page behind depth-0 scene entities made the bright world - // leak through for a frame when the config screen faded out (white flash). + externalOverlays.Add(entity); if (entity.Depth >= Depth) entity.Depth = Depth - 1; } } - externalOverlayOpen = open; + externalOverlayOpen = externalOverlays.Count > 0; + } + + void IMaterialAcrylicOverlay.HideOverlaysForAcrylicCapture() { + hiddenOverlays.Clear(); + foreach (Entity entity in externalOverlays) { + hiddenOverlays.Add((entity, entity.Visible)); + entity.Visible = false; + } + } + + void IMaterialAcrylicOverlay.RestoreOverlaysAfterAcrylicCapture() { + foreach ((Entity entity, bool visible) in hiddenOverlays) entity.Visible = visible; + hiddenOverlays.Clear(); + } + + void IMaterialAcrylicOverlay.RenderOverlaysAboveAcrylic() { + foreach (Entity entity in externalOverlays) { + if (entity.Visible) entity.Render(); + } } private void SelectItem(TextMenu.Item item) { From 8c6d0c2866e2cd584d588cb2091a6734b01c41b4 Mon Sep 17 00:00:00 2001 From: Dilant Date: Thu, 22 Oct 2026 23:49:48 +0800 Subject: [PATCH 4/5] modoptions: redraw scene HUD overlays above the acrylic page --- Source/MaterialModOptions.cs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Source/MaterialModOptions.cs b/Source/MaterialModOptions.cs index e919371..2e8c4c5 100644 --- a/Source/MaterialModOptions.cs +++ b/Source/MaterialModOptions.cs @@ -90,7 +90,7 @@ public sealed class MaterialModOptions : Oui, IMaterialAcrylicPage, IMaterialAcr private string settingSearchText = ""; private string imeText = ""; private SearchTarget searchTarget; - private bool externalOverlayOpen; + private bool configOverlayOpen; private readonly List externalOverlays = []; private readonly List<(Entity Entity, bool Visible)> hiddenOverlays = []; @@ -192,6 +192,7 @@ public override void Removed(Scene scene) { public override void Update() { MaterialTouch.BeginUpdate(this); base.Update(); + RefreshExternalOverlays(); ModOptionsLayout layout = ModOptionsLayout.Create(1f - Ease.CubeOut(ease)); tabScroll.Update(MaxTabScroll(layout)); rowScroll.Update(MaxRowScroll(layout)); @@ -209,8 +210,7 @@ public override void Update() { } if (menu is null) return; - RefreshExternalOverlay(); - if (externalOverlayOpen) { + if (configOverlayOpen) { // The key/button config screen owns input while it is open. menu.Focused = false; return; @@ -1013,16 +1013,18 @@ private void EnsureCompositePopupSelectionVisible(ModOptionsLayout layout, TextM } } - private void RefreshExternalOverlay() { + private void RefreshExternalOverlays() { externalOverlays.Clear(); - if (Scene is { } scene) { - foreach (Entity entity in scene.Entities) { - if (entity is not (KeyboardConfigUI or ButtonConfigUI)) continue; - externalOverlays.Add(entity); + configOverlayOpen = false; + if (Scene is not { } scene) return; + foreach (Entity entity in scene.Entities) { + if (entity is KeyboardConfigUI or ButtonConfigUI) { + configOverlayOpen = true; if (entity.Depth >= Depth) entity.Depth = Depth - 1; } + if (!entity.Visible || !entity.TagCheck(Tags.HUD) || entity.Depth >= Depth) continue; + externalOverlays.Add(entity); } - externalOverlayOpen = externalOverlays.Count > 0; } void IMaterialAcrylicOverlay.HideOverlaysForAcrylicCapture() { @@ -1074,7 +1076,7 @@ private void EnsureTabVisible(ModOptionsLayout layout) { } private void UpdateInteractions(ModOptionsLayout layout) { - if (externalOverlayOpen) { + if (configOverlayOpen) { // Do not leave hidden setting rows alive as touch targets under the config UI. MaterialTouch.Publish(this, [], TouchContext); motion.Update([]); From e1feb5a82f913719c5bb435bcdca78bb641a6a10 Mon Sep 17 00:00:00 2001 From: Dilant Date: Fri, 23 Oct 2026 00:35:36 +0800 Subject: [PATCH 5/5] modoptions: keep acrylic overlays out of the HUD buffer --- Source/IMaterialAcrylicPage.cs | 1 + Source/MaterialAcrylicRenderer.cs | 23 +++++++++++++++-------- Source/MaterialModOptions.cs | 14 ++++++++++---- Source/MaterialTouch.cs | 5 +++++ Source/QolSettingsOverlay.cs | 3 +-- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/Source/IMaterialAcrylicPage.cs b/Source/IMaterialAcrylicPage.cs index 815480f..4cda439 100644 --- a/Source/IMaterialAcrylicPage.cs +++ b/Source/IMaterialAcrylicPage.cs @@ -6,6 +6,7 @@ internal interface IMaterialAcrylicPage { } internal interface IMaterialAcrylicOverlay { + bool HasOverlaysForAcrylic { get; } void HideOverlaysForAcrylicCapture(); void RestoreOverlaysAfterAcrylicCapture(); void RenderOverlaysAboveAcrylic(); diff --git a/Source/MaterialAcrylicRenderer.cs b/Source/MaterialAcrylicRenderer.cs index 9ffe4d7..49bd947 100644 --- a/Source/MaterialAcrylicRenderer.cs +++ b/Source/MaterialAcrylicRenderer.cs @@ -67,13 +67,16 @@ private static void RenderCore(On.Monocle.Engine.orig_RenderCore orig, Engine se rendering = true; activePage = page; + IMaterialAcrylicOverlay? overlay = page as IMaterialAcrylicOverlay; page.SuppressNormalRender = true; try { + overlay?.HideOverlaysForAcrylicCapture(); // Keep the normal Engine render lifecycle intact. In particular, the // Overworld must run BeforeRender exactly once so its mountain and HUD // buffers are ready before RenderScene redirects the final composition. orig(self); } finally { + overlay?.RestoreOverlaysAfterAcrylicCapture(); page.SuppressNormalRender = false; activePage = null; rendering = false; @@ -95,6 +98,7 @@ private static void RenderScene(On.Monocle.Scene.orig_Render orig, Scene self) { try { EnsureTargets(Math.Max(1, originalViewport.Width), Math.Max(1, originalViewport.Height)); if (sceneTarget is null || blurTemporary is null || blurredTarget is null) { + overlay?.RestoreOverlaysAfterAcrylicCapture(); RestoreOutput(graphics, originalTargets, originalViewport); page.SuppressNormalRender = false; orig(self); @@ -108,13 +112,9 @@ private static void RenderScene(On.Monocle.Scene.orig_Render orig, Scene self) { bool renderSnow = snow is { Visible: true }; if (renderSnow) snow!.Visible = false; try { - overlay?.HideOverlaysForAcrylicCapture(); - try { - orig(self); - } finally { - overlay?.RestoreOverlaysAfterAcrylicCapture(); - } + orig(self); } finally { + overlay?.RestoreOverlaysAfterAcrylicCapture(); if (renderSnow) snow!.Visible = true; } sceneRendered = true; @@ -141,6 +141,7 @@ private static void RenderScene(On.Monocle.Scene.orig_Render orig, Scene self) { successfulFrames++; } catch (Exception exception) { page.SuppressNormalRender = false; + overlay?.RestoreOverlaysAfterAcrylicCapture(); RestoreOutput(graphics, originalTargets, originalViewport); failed = true; Logger.LogDetailed(exception, "MicroblocksQolUtils/MaterialAcrylic"); @@ -182,7 +183,7 @@ private static void DrawMaterialPage(IMaterialAcrylicPage page) { } private static void DrawOverlays(IMaterialAcrylicOverlay? overlay) { - if (overlay is null) return; + if (overlay is null || !overlay.HasOverlaysForAcrylic) return; DrawScreenBatch(() => overlay.RenderOverlaysAboveAcrylic()); } @@ -201,7 +202,13 @@ private static void DrawScreenBatch(Action render) { begun = true; render(); } finally { - if (begun) Draw.SpriteBatch.End(); + if (begun) { + try { + Draw.SpriteBatch.End(); + } catch (Exception exception) { + Logger.LogDetailed(exception, "MicroblocksQolUtils/MaterialAcrylic"); + } + } } } diff --git a/Source/MaterialModOptions.cs b/Source/MaterialModOptions.cs index 2e8c4c5..6b635c9 100644 --- a/Source/MaterialModOptions.cs +++ b/Source/MaterialModOptions.cs @@ -1016,7 +1016,7 @@ private void EnsureCompositePopupSelectionVisible(ModOptionsLayout layout, TextM private void RefreshExternalOverlays() { externalOverlays.Clear(); configOverlayOpen = false; - if (Scene is not { } scene) return; + if (!Visible || Scene is not { } scene) return; foreach (Entity entity in scene.Entities) { if (entity is KeyboardConfigUI or ButtonConfigUI) { configOverlayOpen = true; @@ -1027,6 +1027,8 @@ private void RefreshExternalOverlays() { } } + bool IMaterialAcrylicOverlay.HasOverlaysForAcrylic => externalOverlays.Count > 0; + void IMaterialAcrylicOverlay.HideOverlaysForAcrylicCapture() { hiddenOverlays.Clear(); foreach (Entity entity in externalOverlays) { @@ -1042,7 +1044,12 @@ void IMaterialAcrylicOverlay.RestoreOverlaysAfterAcrylicCapture() { void IMaterialAcrylicOverlay.RenderOverlaysAboveAcrylic() { foreach (Entity entity in externalOverlays) { - if (entity.Visible) entity.Render(); + if (!entity.Visible) continue; + try { + entity.Render(); + } catch (Exception exception) { + Logger.LogDetailed(exception, "MicroblocksQolUtils/MaterialAcrylic"); + } } } @@ -1078,8 +1085,7 @@ private void EnsureTabVisible(ModOptionsLayout layout) { private void UpdateInteractions(ModOptionsLayout layout) { if (configOverlayOpen) { // Do not leave hidden setting rows alive as touch targets under the config UI. - MaterialTouch.Publish(this, [], TouchContext); - motion.Update([]); + MaterialTouch.Suppress(this, motion, TouchContext); return; } List targets = [ diff --git a/Source/MaterialTouch.cs b/Source/MaterialTouch.cs index 3df90d9..efb9f5e 100644 --- a/Source/MaterialTouch.cs +++ b/Source/MaterialTouch.cs @@ -96,6 +96,11 @@ internal static void Publish(Entity owner, IEnumerable orig, string mode) { Entity? owner = ActiveOwner(); if (owner is null || !surfaces.TryGetValue(owner, out Surface? surface)) { orig(mode); return; } diff --git a/Source/QolSettingsOverlay.cs b/Source/QolSettingsOverlay.cs index 41a9a86..ceeebe9 100644 --- a/Source/QolSettingsOverlay.cs +++ b/Source/QolSettingsOverlay.cs @@ -111,8 +111,7 @@ public override void Update() { if (bindingConfig is not null) { // The key/button config screen handles its own input; do not leave the hidden // setting rows alive as touch targets underneath it. - MaterialTouch.Publish(this, [], TouchContext); - motion.Update([]); + MaterialTouch.Suppress(this, motion, TouchContext); return; }