From 23da1ea674b1341e9fc341d7e4abd2a5bede2138 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 24 Aug 2026 18:45:10 -0400 Subject: [PATCH 01/26] working --- Core/Layer/Worlds/WorldLayer.Render.Hud.cs | 18 ++++- Core/Layer/Worlds/WorldLayer.cs | 1 + .../Renderers/Legacy/World/IBspHeuristics.cs | 13 ++++ .../Legacy/World/LegacyWorldRenderer.cs | 46 ++++++++++++- Core/World/IWorld.cs | 3 +- Core/World/Impl/SinglePlayer/AutomapMarker.cs | 66 ++++++++++++++++--- .../Impl/SinglePlayer/SinglePlayerWorld.cs | 3 + Core/World/WorldBase.cs | 2 + Core/World/WorldStatic.cs | 3 + 9 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs diff --git a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs index 78e841b68..e1f3421c2 100644 --- a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs +++ b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs @@ -77,11 +77,13 @@ public partial class WorldLayer private readonly SpanString m_fpsMaxString = new(); private readonly SpanString m_timeString = new(); private readonly SpanString m_renderMessageSpan = new(128); + private readonly SpanString m_bspString = new(); private readonly RenderableString m_renderFpsString; private readonly RenderableString m_renderFpsMinString; private readonly RenderableString m_renderFpsMaxString; private readonly RenderableString m_renderTimeString; + private readonly RenderableString m_renderBspString; private readonly RenderStat[] m_renderStats; @@ -258,6 +260,7 @@ private void DrawStatInfo(IHudRenderContext hud, bool automapVisible, Vec2I star if (!m_config.Hud.ShowStats && (!automapVisible || !m_config.Hud.AutoMap.ShowStats)) return; + int labelX = 0; start.X = -m_padding - m_hudPaddingX; Vec2I labelPos = start; @@ -287,8 +290,8 @@ private void DrawStatInfo(IHudRenderContext hud, bool automapVisible, Vec2I star maxLabelWidth = Math.Max(renderStat.RenderLabel.DrawArea.Width, maxLabelWidth); maxValueWidth = Math.Max(renderStat.RenderValue.DrawArea.Width, maxValueWidth); } - - labelPos.X = -(maxValueWidth + m_padding + m_hudPaddingX); + labelX = -(maxValueWidth + m_padding + m_hudPaddingX); + labelPos.X = labelX; for (int i = 0; i < m_renderStats.Length; i++) { var renderStat = m_renderStats[i]; @@ -328,6 +331,17 @@ private void DrawStatInfo(IHudRenderContext hud, bool automapVisible, Vec2I star hud.Text(m_renderTimeString, labelPos, both: Align.TopRight, alpha: m_hudAlpha); labelPos.Y += m_renderTimeString.DrawArea.Height; + + m_bspString.Clear(); + m_bspString.Append(WorldStatic.Bsp ? "BSP (" : "Static ("); + m_bspString.Append(WorldStatic.BspSegCount); + m_bspString.Append("-"); + m_bspString.Append(WorldStatic.BspLineCount); + m_bspString.Append(')'); + labelPos.X = labelX; + SetRenderableString(m_bspString.AsSpan(), m_renderBspString, FixedNumberFont, m_infoFontSize, useDoomScale: false); + hud.Text(m_renderBspString, labelPos, Align.TopRight, alpha: m_hudAlpha); + labelPos.Y += m_renderBspString.DrawArea.Height; } topRightY = labelPos.Y; diff --git a/Core/Layer/Worlds/WorldLayer.cs b/Core/Layer/Worlds/WorldLayer.cs index 518865112..8076747e2 100644 --- a/Core/Layer/Worlds/WorldLayer.cs +++ b/Core/Layer/Worlds/WorldLayer.cs @@ -122,6 +122,7 @@ public WorldLayer(GameLayerManager parent, IConfig config, HelionConsole console m_renderFpsMinString = InitRenderableString(TextAlign.Right); m_renderFpsMaxString = InitRenderableString(TextAlign.Right); m_renderTimeString = InitRenderableString(TextAlign.Right); + m_renderBspString = InitRenderableString(TextAlign.Right); World.LevelExiting += World_LevelExiting; World.WorldPaused += World_WorldPaused; diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs new file mode 100644 index 000000000..1295edf41 --- /dev/null +++ b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Helion.Render.OpenGL.Renderers.Legacy.World; + +public interface IBspHeuristics +{ + public float SubsectorVisibility { get; } + public float SegVisibility { get; } + public int SubsectorCount { get; } + public int SegCount { get; } + public int LineCount { get; } + public int LastProcessedId { get; } +} diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs index e469e1ac6..8816d8d9d 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs @@ -15,6 +15,7 @@ using Helion.Resources.Definitions.Decorate.Properties.Enums; using Helion.Util; using Helion.Util.Configs; +using Helion.Util.Loggers; using Helion.World; using Helion.World.Entities; using Helion.World.Geometry.Sectors; @@ -69,6 +70,7 @@ public partial class LegacyWorldRenderer : WorldRenderer private TransferHeightView m_lastTransferHeightsView; private PlaneClipFrameBuffer? m_planeClipFrameBuffer; private PlaneClipFrameBuffer? m_wallClipFrameBuffer; + private IBspHeuristics? m_bspHeuristics; public LegacyWorldRenderer(IConfig config, ArchiveCollection archiveCollection, LegacyGLTextureManager textureManager) { @@ -128,6 +130,7 @@ public override void UpdateToNewWorld(IWorld world) m_lastTicker = -1; m_pixelGapCorrection = m_config.Render.PixelGapCorrection.Value; m_lastTransferHeightsView = TransferHeightView.Middle; + m_bspHeuristics = world.GetBspHeuristics(); m_stopwatch.Stop(); Log.Info($"Completed level geometry {m_stopwatch.Elapsed}"); @@ -291,14 +294,33 @@ void RenderEntity(IWorld world, Entity entity, int renderIndex) m_entityRenderer.RenderEntity(entity, m_renderData.ViewPosInterpolated, renderIndex); } + private bool m_lastUseBsp; + protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFramebuffer framebuffer) { // If the transfer height view is not the middle then the cached static geometry cannot be used. // Render all sectors dynamically instead. m_lastRenderStatic = m_renderStatic; - m_renderStatic = !m_config.Developer.ForceBsp.Value && renderInfo.TransferHeightView == TransferHeightView.Middle; + m_renderStatic = !m_config.Developer.ForceBsp.Value && renderInfo.TransferHeightView == TransferHeightView.Middle && !m_lastUseBsp; m_postProcessingEffects = m_config.Render.PostProcessingEffects; + if (world.GameTicker != m_lastTicker && renderInfo.TransferHeightView == TransferHeightView.Middle) + { + m_lastUseBsp = UseBspBasedOnHeuristic(world); + WorldStatic.Bsp = m_lastUseBsp; + if (m_bspHeuristics != null) + { + WorldStatic.BspSegCount = m_bspHeuristics.SegCount; + WorldStatic.BspLineCount = m_bspHeuristics.LineCount; + } + + if (m_lastUseBsp && m_lastRenderStatic) + HelionLog.Info("Swapped to BSP based on heuristic"); + else if (!m_lastUseBsp && !m_lastRenderStatic) + HelionLog.Info("Swapped to static based on heuristic"); + m_renderStatic = !m_lastUseBsp; + } + var renderTickChange = !m_config.Developer.LockRender.Value && NeedsRenderTickChange(world, renderInfo.TransferHeightView); m_lastTransferHeightsView = renderInfo.TransferHeightView; @@ -416,6 +438,28 @@ protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFra RenderTransparent(renderInfo, framebuffer); } + private bool UseBspBasedOnHeuristic(IWorld world) + { + if (m_config.Developer.ForceBsp.Value) + return true; + + if (m_bspHeuristics == null) + return false; + + m_stopwatch.Restart(); + + while (m_bspHeuristics.LastProcessedId != world.GameTicker - 1 && m_stopwatch.ElapsedMilliseconds < 3) ; + + if (m_bspHeuristics.LastProcessedId != world.GameTicker - 1) + { + HelionLog.Info("Fell behind"); + return false; + } + + var use = m_bspHeuristics.LineCount < 2000; + return use; + } + private void RenderFloodFill(RenderInfo renderInfo) { // Doom would draw middle textures over flood fill. diff --git a/Core/World/IWorld.cs b/Core/World/IWorld.cs index 47b7bc3a4..b0d7cfb1e 100644 --- a/Core/World/IWorld.cs +++ b/Core/World/IWorld.cs @@ -7,9 +7,9 @@ using Helion.Maps.Specials; using Helion.Maps.Specials.ZDoom; using Helion.Models; +using Helion.Render.OpenGL.Renderers.Legacy.World; using Helion.Resources; using Helion.Resources.Archives.Collection; -using Helion.Resources.Archives.Entries; using Helion.Resources.Definitions.Compatibility; using Helion.Resources.Definitions.MapInfo; using Helion.Util; @@ -231,6 +231,7 @@ void FirePlayerHitscanBullets(Player shooter, int bulletCount, double spreadAngl bool SectorReturnStop(); IEnumerable GetPreCacheTextureNames(); IEnumerable GetPreCacheSoundNames(); + IBspHeuristics? GetBspHeuristics(); WorldModel ToWorldModel(); GameFilesModel GetGameFilesModel(); diff --git a/Core/World/Impl/SinglePlayer/AutomapMarker.cs b/Core/World/Impl/SinglePlayer/AutomapMarker.cs index a67fa7d72..ba13187c0 100644 --- a/Core/World/Impl/SinglePlayer/AutomapMarker.cs +++ b/Core/World/Impl/SinglePlayer/AutomapMarker.cs @@ -3,10 +3,11 @@ using Helion.Geometry.Vectors; using Helion.Render; using Helion.Render.Common.Shared; +using Helion.Render.OpenGL.Renderers.Legacy.World; using Helion.Render.OpenGL.Shared; using Helion.Render.OpenGL.Shared.World.ViewClipping; -using Helion.Resources.Archives.Collection; using Helion.Util; +using Helion.Util.Container; using Helion.World.Bsp; using Helion.World.Entities; using Helion.World.Entities.Definition; @@ -15,13 +16,14 @@ using System; using System.Collections; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace Helion.World.Impl.SinglePlayer; -public class AutomapMarker +public class AutomapMarker : IBspHeuristics { private BitArray m_hitLines = new(0); private readonly Stopwatch m_stopwatch = new(); @@ -29,16 +31,31 @@ public class AutomapMarker private readonly RenderInfo m_renderInfo = new(); private readonly OldCamera m_camera = new(default, default, 0, 0); private readonly Entity m_dummyEntity = new(); + private readonly HashSet m_visibleTextures = new(256); private Task? m_task; private CancellationTokenSource m_cancelTasks = new(); private IWorld m_world = null!; private FrustumPlanes m_frustumPlanes; + private int m_subsectorCount; + private int m_segCount; + private int m_lineCount; + private int m_lastSubsectorCount; + private int m_lastSegCount; + private int m_lastLineCount; + private float m_subsectorVisibility; + private float m_segVisibility; private readonly ConcurrentQueue m_positions = new(); - - public int LastProcessedId; + + public int LastProcessedId { get; private set; } public event EventHandler? PositionProcessed; + public float SubsectorVisibility => m_subsectorVisibility; + public float SegVisibility => m_segVisibility; + public int SubsectorCount => m_lastSubsectorCount; + public int SegCount => m_lastSegCount; + public int LineCount => m_lastLineCount; + public void Start(IWorld world) { if (m_task != null) @@ -109,29 +126,55 @@ private void AutomapTask(CancellationToken token) { // Don't let the queue fill up indefinitely when processing too slowly if (m_positions.Count > ClearCount) + { + MaxHeuristics(); m_positions.Clear(); + } if (token.IsCancellationRequested) return; + m_subsectorCount = 0; + m_segCount = 0; + m_lineCount = 0; m_viewClipper.Clear(); m_viewClipper.Center = pos.Position.XY; m_hitLines.SetAll(false); + m_visibleTextures.Clear(); SetFrustum(viewport, pos); MarkBspLineClips((uint)m_world.BspTree.Nodes.Length - 1, pos.Position.XY, m_world, token); LastProcessedId = pos.Id; PositionProcessed?.Invoke(this, pos); + + SetHeuristics(); } m_stopwatch.Stop(); if (m_stopwatch.ElapsedMilliseconds >= ticks) continue; - Thread.Sleep(Math.Max(ticks - (int)m_stopwatch.ElapsedMilliseconds, 0)); + //Thread.Sleep(Math.Max(ticks - (int)m_stopwatch.ElapsedMilliseconds, 0)); } } + private void MaxHeuristics() + { + m_subsectorVisibility = 1; + m_segVisibility = 1; + m_lastSegCount = int.MaxValue; + m_lastLineCount = int.MaxValue; + } + + private void SetHeuristics() + { + m_lastSubsectorCount = m_subsectorCount; + m_lastSegCount = m_segCount; + m_lastLineCount = m_lineCount; + m_segVisibility = m_segCount / (float)m_world.BspTree.Segments.Length; + m_subsectorVisibility = m_subsectorCount / (float)m_world.BspTree.Subsectors.Length; + } + private void SetFrustum(Rectangle viewport, PlayerPosition pos) { var viewPosition = pos.Position.Float; @@ -168,6 +211,7 @@ private unsafe void MarkBspLineClips(uint nodeIndex, in Vec2D position, IWorld w return; } + m_subsectorCount++; var subsector = world.BspTree.Subsectors[nodeIndex & BspNodeCompact.SubsectorMask]; var lineArray = world.StructLines.Data; uint smallerAngle; @@ -193,18 +237,24 @@ private unsafe void MarkBspLineClips(uint nodeIndex, in Vec2D position, IWorld w if (edge.BackSectorId == -1 || RenderBlock.IsBlocked(side, m_world.Sectors[edge.FrontSectorId], m_world.Sectors[edge.BackSectorId])) m_viewClipper.AddLine(smallerAngle, largerAngle); + m_segCount++; + if (m_hitLines.Get(edge.LineId)) continue; + m_hitLines.Set(edge.LineId, true); + m_lineCount++; ref var line = ref lineArray[edge.LineId]; - if ((line.Flags & StructLineFlags.SeenForAutomap) != 0) - continue; + //if ((line.Flags & StructLineFlags.SeenForAutomap) != 0) + // continue; if (!m_frustumPlanes.PointInFrustum(line.Segment.Start.X, line.Segment.Start.Y) && !m_frustumPlanes.PointInFrustum(line.Segment.End.X, line.Segment.End.Y)) continue; - m_hitLines.Set(line.Id, true); + if ((line.Flags & StructLineFlags.SeenForAutomap) != 0) + continue; + line.Flags |= StructLineFlags.SeenForAutomap; line.Line.DataChanges |= LineDataTypes.Automap; } diff --git a/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs b/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs index f636ddb8e..db4812133 100644 --- a/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs +++ b/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs @@ -2,6 +2,7 @@ using Helion.Geometry.Vectors; using Helion.Maps; using Helion.Models; +using Helion.Render.OpenGL.Renderers.Legacy.World; using Helion.Resources.Archives.Collection; using Helion.Resources.Archives.Entries; using Helion.Resources.Definitions.MapInfo; @@ -613,4 +614,6 @@ private void HandleMouseLook(IConsumableInput input) input.Manager.AnalogAdapter.ZeroGyroAbsolute(); } } + + public override IBspHeuristics? GetBspHeuristics() => m_automapMarker; } diff --git a/Core/World/WorldBase.cs b/Core/World/WorldBase.cs index 5ae8138a3..11487d00d 100644 --- a/Core/World/WorldBase.cs +++ b/Core/World/WorldBase.cs @@ -14,6 +14,7 @@ using Helion.Maps.Specials.Vanilla; using Helion.Maps.Specials.ZDoom; using Helion.Models; +using Helion.Render.OpenGL.Renderers.Legacy.World; using Helion.Render.OpenGL.Renderers.Legacy.World.Primitives; using Helion.Resources; using Helion.Resources.Archives.Collection; @@ -4817,6 +4818,7 @@ public void AddEntityScrollAccumulator(Entity entity, double x, double y) public bool UseAverageScrollCarry() => m_averageScrollCarry; public bool SectorReturnStop() => m_sectorReturnStop; + public virtual IBspHeuristics? GetBspHeuristics() => null; public IEnumerable GetPreCacheTextureNames() => MapInfo.PrecacheTextures.Union(GetFilteredAcsStrings(), StringComparer.OrdinalIgnoreCase); diff --git a/Core/World/WorldStatic.cs b/Core/World/WorldStatic.cs index 791943f52..d34e778fc 100644 --- a/Core/World/WorldStatic.cs +++ b/Core/World/WorldStatic.cs @@ -61,6 +61,9 @@ public static class WorldStatic public static float DamageApplyMultiplier = 1; public static float DamageReceiveMultiplier = 1; public static int MaxSoulsphere = 200; + public static bool Bsp; + public static int BspLineCount; + public static int BspSegCount; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool HasCustomBlood() => BloodColor || AutoColoredBlood || FuzzBlood; From 7a900a2b938a522c90e942504f255e4ec1a7fb57 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 25 Aug 2026 06:34:58 -0400 Subject: [PATCH 02/26] start config cleanup --- Core/Layer/Worlds/WorldLayer.Render.Hud.cs | 36 +++++++++++-------- .../Renderers/Legacy/World/IBspHeuristics.cs | 5 ++- .../Legacy/World/LegacyWorldRenderer.cs | 19 +++++----- .../Configs/Components/ConfigDeveloper.cs | 3 ++ Core/Util/Configs/Components/ConfigRender.cs | 13 +++++++ Core/Util/Configs/ConfigEnums.cs | 2 ++ Core/World/Impl/SinglePlayer/AutomapMarker.cs | 33 +++++++++++------ .../Impl/SinglePlayer/SinglePlayerWorld.cs | 3 +- Core/World/WorldStatic.cs | 1 + 9 files changed, 76 insertions(+), 39 deletions(-) diff --git a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs index e1f3421c2..49a952cc4 100644 --- a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs +++ b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs @@ -127,6 +127,8 @@ private void DrawHud(HudRenderContext hudContext, IHudRenderContext hud, bool au suppressStats: (sbarCoverage & StatusBarCoverage.Stats) != 0, suppressTime: (sbarCoverage & StatusBarCoverage.Time) != 0); + DrawBspStats(hud); + DrawBottomHud(hud, automapVisible, activeSbarLayout); DrawHudEffects(hud); @@ -260,7 +262,6 @@ private void DrawStatInfo(IHudRenderContext hud, bool automapVisible, Vec2I star if (!m_config.Hud.ShowStats && (!automapVisible || !m_config.Hud.AutoMap.ShowStats)) return; - int labelX = 0; start.X = -m_padding - m_hudPaddingX; Vec2I labelPos = start; @@ -290,8 +291,7 @@ private void DrawStatInfo(IHudRenderContext hud, bool automapVisible, Vec2I star maxLabelWidth = Math.Max(renderStat.RenderLabel.DrawArea.Width, maxLabelWidth); maxValueWidth = Math.Max(renderStat.RenderValue.DrawArea.Width, maxValueWidth); } - labelX = -(maxValueWidth + m_padding + m_hudPaddingX); - labelPos.X = labelX; + labelPos.X = -(maxValueWidth + m_padding + m_hudPaddingX); for (int i = 0; i < m_renderStats.Length; i++) { var renderStat = m_renderStats[i]; @@ -331,22 +331,30 @@ private void DrawStatInfo(IHudRenderContext hud, bool automapVisible, Vec2I star hud.Text(m_renderTimeString, labelPos, both: Align.TopRight, alpha: m_hudAlpha); labelPos.Y += m_renderTimeString.DrawArea.Height; - - m_bspString.Clear(); - m_bspString.Append(WorldStatic.Bsp ? "BSP (" : "Static ("); - m_bspString.Append(WorldStatic.BspSegCount); - m_bspString.Append("-"); - m_bspString.Append(WorldStatic.BspLineCount); - m_bspString.Append(')'); - labelPos.X = labelX; - SetRenderableString(m_bspString.AsSpan(), m_renderBspString, FixedNumberFont, m_infoFontSize, useDoomScale: false); - hud.Text(m_renderBspString, labelPos, Align.TopRight, alpha: m_hudAlpha); - labelPos.Y += m_renderBspString.DrawArea.Height; } topRightY = labelPos.Y; } + private void DrawBspStats(IHudRenderContext hud) + { + if (!m_config.Developer.DebugAdaptiveRenderMode.Value) + return; + + m_bspString.Clear(); + var x = hud.MeasureText(" ", FixedNumberFont, m_infoFontSize).Width; + + m_bspString.Append(WorldStatic.Bsp ? "BSP (" : "Static ("); + m_bspString.Append(WorldStatic.BspSegCount); + m_bspString.Append('/'); + m_bspString.Append(WorldStatic.BspLineCount); + m_bspString.Append(" "); + m_bspString.Append(WorldStatic.BspMicroseconds); + m_bspString.Append(')'); + SetRenderableString(m_bspString.AsSpan(), m_renderBspString, FixedNumberFont, m_infoFontSize, useDoomScale: false); + hud.Text(m_renderBspString, (-x, m_padding / 2), Align.TopMiddle, alpha: m_hudAlpha); + } + private static SpanString AppendStatString(SpanString str, int current, int max) { str.Append(current); diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs index 1295edf41..9bacea87e 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Helion.Render.OpenGL.Renderers.Legacy.World; +namespace Helion.Render.OpenGL.Renderers.Legacy.World; public interface IBspHeuristics { @@ -10,4 +8,5 @@ public interface IBspHeuristics public int SegCount { get; } public int LineCount { get; } public int LastProcessedId { get; } + public int Microseconds { get; } } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs index 8816d8d9d..2b9cdb0e4 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs @@ -15,6 +15,7 @@ using Helion.Resources.Definitions.Decorate.Properties.Enums; using Helion.Util; using Helion.Util.Configs; +using Helion.Util.Configs.Components; using Helion.Util.Loggers; using Helion.World; using Helion.World.Entities; @@ -63,6 +64,7 @@ public partial class LegacyWorldRenderer : WorldRenderer private bool m_pixelGapCorrection; private bool m_downscaleVanillaBuffer; private bool m_postProcessingEffects; + private bool m_lastUseBsp; private int m_lastTicker = -1; private Entity? m_viewerEntity; private IWorld? m_previousWorld; @@ -294,8 +296,6 @@ void RenderEntity(IWorld world, Entity entity, int renderIndex) m_entityRenderer.RenderEntity(entity, m_renderData.ViewPosInterpolated, renderIndex); } - private bool m_lastUseBsp; - protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFramebuffer framebuffer) { // If the transfer height view is not the middle then the cached static geometry cannot be used. @@ -312,12 +312,9 @@ protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFra { WorldStatic.BspSegCount = m_bspHeuristics.SegCount; WorldStatic.BspLineCount = m_bspHeuristics.LineCount; + WorldStatic.BspMicroseconds = m_bspHeuristics.Microseconds; } - if (m_lastUseBsp && m_lastRenderStatic) - HelionLog.Info("Swapped to BSP based on heuristic"); - else if (!m_lastUseBsp && !m_lastRenderStatic) - HelionLog.Info("Swapped to static based on heuristic"); m_renderStatic = !m_lastUseBsp; } @@ -440,15 +437,18 @@ protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFra private bool UseBspBasedOnHeuristic(IWorld world) { - if (m_config.Developer.ForceBsp.Value) + if (m_config.Render.Mode.Value == AdaptiveRenderMode.Bsp) return true; + if (m_config.Render.Mode.Value == AdaptiveRenderMode.Static) + return false; + if (m_bspHeuristics == null) return false; m_stopwatch.Restart(); - while (m_bspHeuristics.LastProcessedId != world.GameTicker - 1 && m_stopwatch.ElapsedMilliseconds < 3) ; + while (m_bspHeuristics.LastProcessedId != world.GameTicker - 1 && m_stopwatch.ElapsedMilliseconds < 4) ; if (m_bspHeuristics.LastProcessedId != world.GameTicker - 1) { @@ -456,8 +456,7 @@ private bool UseBspBasedOnHeuristic(IWorld world) return false; } - var use = m_bspHeuristics.LineCount < 2000; - return use; + return m_bspHeuristics.LineCount < m_config.Render.AdaptiveBspThreshold; } private void RenderFloodFill(RenderInfo renderInfo) diff --git a/Core/Util/Configs/Components/ConfigDeveloper.cs b/Core/Util/Configs/Components/ConfigDeveloper.cs index e77cbe44e..32868ebc4 100644 --- a/Core/Util/Configs/Components/ConfigDeveloper.cs +++ b/Core/Util/Configs/Components/ConfigDeveloper.cs @@ -42,4 +42,7 @@ public class ConfigDeveloper: ConfigElement [ConfigInfo("Locks rendering to current state.", save: false)] public readonly ConfigValue LockRender = new(false); + + [ConfigInfo("Shows values for adaptive render mode.", save: false)] + public readonly ConfigValue DebugAdaptiveRenderMode = new(false); } diff --git a/Core/Util/Configs/Components/ConfigRender.cs b/Core/Util/Configs/Components/ConfigRender.cs index edd16b726..6dcf436ee 100644 --- a/Core/Util/Configs/Components/ConfigRender.cs +++ b/Core/Util/Configs/Components/ConfigRender.cs @@ -49,6 +49,13 @@ public enum RenderContrastMode Smooth } +public enum AdaptiveRenderMode +{ + Static, + Bsp, + Adaptive +} + public class ConfigRenderFilter : ConfigElement { [ConfigInfo("Filter applied to fonts.")] @@ -223,4 +230,10 @@ public class ConfigRender: ConfigElement // This option is a hacked test that writes everything directly to the default backbuffer. Relies on undefined behavior since certain rendering functions need the depth texture. [ConfigInfo("Disables post processing effects like spectre fuzz refraction and skips FBO. Can have rendering defects.", restartRequired: true)] public readonly ConfigValue PostProcessingEffects = new(true); + + [ConfigInfo("Changes the render mode.")] + public readonly ConfigValue Mode = new(AdaptiveRenderMode.Static); + + [ConfigInfo("The number of visible lines until the mode is switched to static when using adapative.")] + public readonly ConfigValue AdaptiveBspThreshold = new(2000); } diff --git a/Core/Util/Configs/ConfigEnums.cs b/Core/Util/Configs/ConfigEnums.cs index 1fba40cc3..e2a56335e 100644 --- a/Core/Util/Configs/ConfigEnums.cs +++ b/Core/Util/Configs/ConfigEnums.cs @@ -52,6 +52,7 @@ public static class ConfigEnums { typeof(ConfigRenderMode), Enum.GetValues() }, { typeof(CompatSetting), Enum.GetValues() }, { typeof(Id24TrackInfoType), Enum.GetValues() }, + { typeof(AdaptiveRenderMode), Enum.GetValues() }, }; public static Dictionary> KnownEnumLabels { get; } = new Dictionary>() @@ -81,6 +82,7 @@ public static class ConfigEnums { typeof(ConfigRenderMode), GetDescriptions()}, { typeof(CompatSetting), GetDescriptions() }, { typeof(Id24TrackInfoType), GetDescriptions() }, + { typeof(AdaptiveRenderMode), GetDescriptions() }, }; private static Dictionary GetDescriptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>() where T : struct, Enum diff --git a/Core/World/Impl/SinglePlayer/AutomapMarker.cs b/Core/World/Impl/SinglePlayer/AutomapMarker.cs index ba13187c0..4451d78f9 100644 --- a/Core/World/Impl/SinglePlayer/AutomapMarker.cs +++ b/Core/World/Impl/SinglePlayer/AutomapMarker.cs @@ -7,6 +7,8 @@ using Helion.Render.OpenGL.Shared; using Helion.Render.OpenGL.Shared.World.ViewClipping; using Helion.Util; +using Helion.Util.Configs; +using Helion.Util.Configs.Components; using Helion.Util.Container; using Helion.World.Bsp; using Helion.World.Entities; @@ -19,11 +21,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; -using System.Threading.Tasks; namespace Helion.World.Impl.SinglePlayer; -public class AutomapMarker : IBspHeuristics +public class AutomapMarker(IConfig config) : IBspHeuristics { private BitArray m_hitLines = new(0); private readonly Stopwatch m_stopwatch = new(); @@ -32,7 +33,7 @@ public class AutomapMarker : IBspHeuristics private readonly OldCamera m_camera = new(default, default, 0, 0); private readonly Entity m_dummyEntity = new(); private readonly HashSet m_visibleTextures = new(256); - private Task? m_task; + private Thread? m_thread; private CancellationTokenSource m_cancelTasks = new(); private IWorld m_world = null!; private FrustumPlanes m_frustumPlanes; @@ -42,9 +43,11 @@ public class AutomapMarker : IBspHeuristics private int m_lastSubsectorCount; private int m_lastSegCount; private int m_lastLineCount; + private int m_lastMicroseconds; private float m_subsectorVisibility; private float m_segVisibility; + private readonly IConfig m_config = config; private readonly ConcurrentQueue m_positions = new(); public int LastProcessedId { get; private set; } @@ -55,10 +58,11 @@ public class AutomapMarker : IBspHeuristics public int SubsectorCount => m_lastSubsectorCount; public int SegCount => m_lastSegCount; public int LineCount => m_lastLineCount; + public int Microseconds => m_lastMicroseconds; public void Start(IWorld world) { - if (m_task != null) + if (m_thread != null) return; ClearData(); @@ -70,8 +74,12 @@ public void Start(IWorld world) m_dummyEntity.Set(0, 0, 0, EntityDefinition.Default, default, 0, m_world.Sectors[0], m_world, default); - m_task = Task.Factory.StartNew(() => AutomapTask(m_cancelTasks.Token), m_cancelTasks.Token, - TaskCreationOptions.LongRunning, TaskScheduler.Default); + m_thread = new Thread(() => AutomapTask(m_cancelTasks.Token)) + { + IsBackground = true, + Priority = ThreadPriority.AboveNormal + }; + m_thread.Start(); } private void World_OnDestroying(object? sender, EventArgs e) @@ -86,17 +94,17 @@ private void World_OnDestroying(object? sender, EventArgs e) public void Stop() { - if (m_task == null) + if (m_thread == null) return; m_cancelTasks.Cancel(); m_cancelTasks.Dispose(); - m_task.Wait(); + m_thread.Join(); ClearData(); m_cancelTasks = new CancellationTokenSource(); - m_task = null; + m_thread = null; } private void ClearData() @@ -107,7 +115,8 @@ private void ClearData() public void AddPosition(Vec3D pos, Vec3D viewDirection, double angleRadians, double pitchRadians, int id) { - m_positions.Enqueue(new PlayerPosition(pos, viewDirection, angleRadians, pitchRadians, id)); + if (m_config.Render.Mode.Value != AdaptiveRenderMode.Adaptive || m_positions.Count == 0) + m_positions.Enqueue(new PlayerPosition(pos, viewDirection, angleRadians, pitchRadians, id)); } private void AutomapTask(CancellationToken token) @@ -154,7 +163,8 @@ private void AutomapTask(CancellationToken token) if (m_stopwatch.ElapsedMilliseconds >= ticks) continue; - //Thread.Sleep(Math.Max(ticks - (int)m_stopwatch.ElapsedMilliseconds, 0)); + if (m_config.Render.Mode.Value != AdaptiveRenderMode.Adaptive) + Thread.Sleep(Math.Max(ticks - (int)m_stopwatch.ElapsedMilliseconds, 0)); } } @@ -168,6 +178,7 @@ private void MaxHeuristics() private void SetHeuristics() { + m_lastMicroseconds = (int)m_stopwatch.Elapsed.TotalMicroseconds; m_lastSubsectorCount = m_subsectorCount; m_lastSegCount = m_segCount; m_lastLineCount = m_lineCount; diff --git a/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs b/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs index db4812133..21276dbe6 100644 --- a/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs +++ b/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs @@ -35,7 +35,7 @@ public class SinglePlayerWorld : WorldBase private static bool SoundsCached; private static readonly Logger Log = LogManager.GetCurrentClassLogger(); private static readonly CheatType[] ChaseCameraCheats = [CheatType.AutoMapModeShowAllLines, CheatType.AutoMapModeShowAllLinesAndThings]; - private readonly AutomapMarker m_automapMarker = new(); + private readonly AutomapMarker m_automapMarker; private readonly HashSet m_renderDistanceOverrideTags = []; private bool m_chaseCamMode; private WorldType m_worldType = WorldType.SinglePlayer; @@ -60,6 +60,7 @@ public SinglePlayerWorld(GlobalData globalData, IConfig config, ArchiveCollectio IMap map, bool sameAsPreviousMap, Player? existingPlayer = null, WorldModel? worldModel = null, IRandom? random = null, bool reuse = true, int playerSpawnArg0 = 0) : base(globalData, config, archiveCollection, audioSystem, profiler, geometry, mapDef, skillDef, map, worldModel, random, sameAsPreviousMap, reuse) { + m_automapMarker = new(config); m_worldType = config.Game.SoloNet ? WorldType.Cooperative : WorldType.SinglePlayer; if (worldModel == null) diff --git a/Core/World/WorldStatic.cs b/Core/World/WorldStatic.cs index d34e778fc..ca9db70da 100644 --- a/Core/World/WorldStatic.cs +++ b/Core/World/WorldStatic.cs @@ -64,6 +64,7 @@ public static class WorldStatic public static bool Bsp; public static int BspLineCount; public static int BspSegCount; + public static int BspMicroseconds; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool HasCustomBlood() => BloodColor || AutoColoredBlood || FuzzBlood; From 3c105e0d1d603a57f9d0bfe980f409f17728b0db Mon Sep 17 00:00:00 2001 From: Nick Date: Wed, 26 Aug 2026 07:28:23 -0400 Subject: [PATCH 03/26] update processing for adaptive and remove automap bsp config --- Core/Layer/Worlds/WorldLayer.Render.Hud.cs | 16 ++++--- .../World/Automap/LegacyAutomapRenderer.cs | 11 +++-- .../Legacy/World/Geometry/GeometryRenderer.cs | 2 +- .../Renderers/Legacy/World/IBspHeuristics.cs | 5 ++- .../Legacy/World/LegacyWorldRenderer.cs | 45 +++++++++++-------- Core/Render/Renderer.cs | 5 --- .../Configs/Components/ConfigDeveloper.cs | 15 +++---- Core/Util/Configs/Components/ConfigRender.cs | 7 +-- Core/World/Geometry/Sectors/SectorPlane.cs | 2 - Core/World/Impl/SinglePlayer/AutomapMarker.cs | 19 +++----- .../Impl/SinglePlayer/SinglePlayerWorld.cs | 22 ++------- Core/World/WorldStatic.cs | 4 -- 12 files changed, 63 insertions(+), 90 deletions(-) diff --git a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs index 49a952cc4..1368250cc 100644 --- a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs +++ b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs @@ -338,18 +338,22 @@ private void DrawStatInfo(IHudRenderContext hud, bool automapVisible, Vec2I star private void DrawBspStats(IHudRenderContext hud) { - if (!m_config.Developer.DebugAdaptiveRenderMode.Value) + if (!m_config.Developer.Render.DebugAdaptiveMode.Value) + return; + + var bspHeuristics = World.GetBspHeuristics(); + if (bspHeuristics == null) return; m_bspString.Clear(); var x = hud.MeasureText(" ", FixedNumberFont, m_infoFontSize).Width; - m_bspString.Append(WorldStatic.Bsp ? "BSP (" : "Static ("); - m_bspString.Append(WorldStatic.BspSegCount); + m_bspString.Append(bspHeuristics.LastBspSetting ? "BSP (" : "Static ("); + m_bspString.Append(bspHeuristics.LineCount); + m_bspString.Append('/'); + m_bspString.Append(bspHeuristics.SegCount); m_bspString.Append('/'); - m_bspString.Append(WorldStatic.BspLineCount); - m_bspString.Append(" "); - m_bspString.Append(WorldStatic.BspMicroseconds); + m_bspString.Append(bspHeuristics.Microseconds); m_bspString.Append(')'); SetRenderableString(m_bspString.AsSpan(), m_renderBspString, FixedNumberFont, m_infoFontSize, useDoomScale: false); hud.Text(m_renderBspString, (-x, m_padding / 2), Align.TopMiddle, alpha: m_hudAlpha); diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Automap/LegacyAutomapRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Automap/LegacyAutomapRenderer.cs index e38912d02..3a515d076 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Automap/LegacyAutomapRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Automap/LegacyAutomapRenderer.cs @@ -324,7 +324,6 @@ private void PopulateColoredLines(IWorld world, Player? player) player.Cheats.IsCheatActive(CheatType.AutoMapModeShowAllLinesAndThings); } - bool forceDraw = !world.Config.Render.AutomapBspThread; bool markSecrets = world.Config.Game.MarkSecrets; bool markFlood = world.Config.Developer.MarkFlood; bool checkMarkedSectors = markSecrets || markFlood || world.Config.Game.MarkSpecials; @@ -340,7 +339,7 @@ private void PopulateColoredLines(IWorld world, Player? player) continue; bool markedLine = IsLineMarked(ref line, markSecrets, markFlood, checkMarkedSectors); - if (!forceDraw && !line.AutomapFlags.AlwaysDraw && !markedLine && (!allMap && !line.SeenForAutomap() || line.AutomapFlags.NeverDraw)) + if (!line.AutomapFlags.AlwaysDraw && !markedLine && (!allMap && !line.SeenForAutomap() || line.AutomapFlags.NeverDraw)) continue; if (!markedLine && line.LockKey != -1) @@ -351,11 +350,11 @@ private void PopulateColoredLines(IWorld world, Player? player) if (line.BackSector == null || line.Secret() || line.AutomapFlags.AlwaysDraw) { - AddLine(GetLineColor(ref line, m_wallColor, m_unseenWallColor, forceDraw, markedLine, allMap, out _), start, end); + AddLine(GetLineColor(ref line, m_wallColor, m_unseenWallColor, markedLine, allMap, out _), start, end); continue; } - var color = GetLineColor(ref line, m_twoSidedWallColor, m_unseenWallColor, forceDraw, markedLine, allMap, out var specialColor); + var color = GetLineColor(ref line, m_twoSidedWallColor, m_unseenWallColor, markedLine, allMap, out var specialColor); if (!allMap && !specialColor && line.BackFloorPlane != null && line.BackCeilingPlane != null && line.FrontFloorPlane.Z == line.BackFloorPlane.Z && line.FrontCeilingPlane.Z == line.BackCeilingPlane.Z) continue; @@ -364,7 +363,7 @@ private void PopulateColoredLines(IWorld world, Player? player) } } - private Color GetLineColor(ref StructLine line, Color seenColor, Color unseenColor, bool forceDraw, bool marked, bool allMap, out bool specialColor) + private Color GetLineColor(ref StructLine line, Color seenColor, Color unseenColor, bool marked, bool allMap, out bool specialColor) { specialColor = false; @@ -374,7 +373,7 @@ private Color GetLineColor(ref StructLine line, Color seenColor, Color unseenCol return GetMarkedColor(); } - if (line.SeenForAutomap() || forceDraw || allMap) + if (line.SeenForAutomap() || allMap) { if (line.IsTeleportSpecial()) { diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs index 584daa132..7ad933039 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs @@ -1662,7 +1662,7 @@ public void SetRenderMode(GeometryRenderMode renderMode, TransferHeightView view m_ceilingVertexLookupInvalidated.SetAll(true); } - var clearFloodVertices = !m_config.Developer.LockRender; + var clearFloodVertices = !m_config.Developer.Render.Lock.Value; if (clearFloodVertices && !newTick) clearFloodVertices = false; diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs index 9bacea87e..76ff81917 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs @@ -2,11 +2,12 @@ public interface IBspHeuristics { - public float SubsectorVisibility { get; } - public float SegVisibility { get; } public int SubsectorCount { get; } public int SegCount { get; } public int LineCount { get; } public int LastProcessedId { get; } public int Microseconds { get; } + public long LastProcessedTimeStamp { get; } + + public bool LastBspSetting { get; set; } } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs index 2b9cdb0e4..28232f275 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs @@ -301,27 +301,19 @@ protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFra // If the transfer height view is not the middle then the cached static geometry cannot be used. // Render all sectors dynamically instead. m_lastRenderStatic = m_renderStatic; - m_renderStatic = !m_config.Developer.ForceBsp.Value && renderInfo.TransferHeightView == TransferHeightView.Middle && !m_lastUseBsp; + m_renderStatic = m_config.Render.Mode.Value != AdaptiveRenderMode.Bsp && renderInfo.TransferHeightView == TransferHeightView.Middle && !m_lastUseBsp; m_postProcessingEffects = m_config.Render.PostProcessingEffects; if (world.GameTicker != m_lastTicker && renderInfo.TransferHeightView == TransferHeightView.Middle) { m_lastUseBsp = UseBspBasedOnHeuristic(world); - WorldStatic.Bsp = m_lastUseBsp; - if (m_bspHeuristics != null) - { - WorldStatic.BspSegCount = m_bspHeuristics.SegCount; - WorldStatic.BspLineCount = m_bspHeuristics.LineCount; - WorldStatic.BspMicroseconds = m_bspHeuristics.Microseconds; - } - m_renderStatic = !m_lastUseBsp; } - var renderTickChange = !m_config.Developer.LockRender.Value && NeedsRenderTickChange(world, renderInfo.TransferHeightView); + var renderTickChange = !m_config.Developer.Render.Lock.Value && NeedsRenderTickChange(world, renderInfo.TransferHeightView); m_lastTransferHeightsView = renderInfo.TransferHeightView; - if (!m_config.Developer.LockRender.Value) + if (!m_config.Developer.Render.Lock.Value) Clear(world, renderInfo); m_geometryRenderer.SetRenderMode(m_renderStatic ? GeometryRenderMode.Dynamic : GeometryRenderMode.All, renderInfo.TransferHeightView, renderTickChange); @@ -336,7 +328,7 @@ protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFra m_downscaleVanillaBuffer = m_config.Render.DownScaleVanillaRenderSampleBuffer.Value > 1; SetupClipBuffers(framebuffer, dimension, prevDownscale != m_downscaleVanillaBuffer); - if (!m_config.Developer.LockRender.Value && renderTickChange) + if (!m_config.Developer.Render.Lock.Value && renderTickChange) m_entityRenderer.Start(renderInfo); SetOccludePosition(renderInfo.Camera.PositionInterpolated.Double, renderInfo.Camera.YawRadians, renderInfo.Camera.PitchRadians, @@ -446,17 +438,34 @@ private bool UseBspBasedOnHeuristic(IWorld world) if (m_bspHeuristics == null) return false; - m_stopwatch.Restart(); + var now = Stopwatch.GetTimestamp(); + var ageTicks = now - m_bspHeuristics.LastProcessedTimeStamp; + double ageMicroseconds = ageTicks * (1_000_000.0 / Stopwatch.Frequency); - while (m_bspHeuristics.LastProcessedId != world.GameTicker - 1 && m_stopwatch.ElapsedMilliseconds < 4) ; + const double ProcessWindowUs = 500.0; + if (ageMicroseconds <= ProcessWindowUs) + return m_bspHeuristics.LastBspSetting; - if (m_bspHeuristics.LastProcessedId != world.GameTicker - 1) + var threshold = m_config.Render.AdaptiveBspThreshold.Value; + var highRange = threshold * 1.15f; + var lowRange = threshold * 0.85f; + + var shouldUseBsp = m_bspHeuristics.Microseconds < threshold; + + if (shouldUseBsp != m_lastUseBsp) { - HelionLog.Info("Fell behind"); - return false; + if (!shouldUseBsp && m_bspHeuristics.Microseconds < highRange) + shouldUseBsp = true; + else if (shouldUseBsp && m_bspHeuristics.Microseconds > lowRange) + shouldUseBsp = false; } - return m_bspHeuristics.LineCount < m_config.Render.AdaptiveBspThreshold; + // Maybe add to config. Don't let fast CPUs switch to BSP when it's likely not beneficial. Maybe should be seg count? + if (m_bspHeuristics.LineCount > 2000) + shouldUseBsp = false; + + m_bspHeuristics.LastBspSetting = shouldUseBsp; + return m_bspHeuristics.LastBspSetting; } private void RenderFloodFill(RenderInfo renderInfo) diff --git a/Core/Render/Renderer.cs b/Core/Render/Renderer.cs index 1096ffdac..349a4d46b 100644 --- a/Core/Render/Renderer.cs +++ b/Core/Render/Renderer.cs @@ -17,7 +17,6 @@ using Helion.Render.OpenGL.Renderers.Legacy.World.Automap; using Helion.Render.OpenGL.Renderers.Legacy.World.Shader; using Helion.Render.OpenGL.Shared; -using Helion.Render.OpenGL.Texture.Fonts; using Helion.Render.OpenGL.Texture.Legacy; using Helion.Render.OpenGL.Util; using Helion.Resources.Archives.Collection; @@ -32,7 +31,6 @@ using NLog; using OpenTK.Graphics.OpenGL; using System; -using System.Diagnostics.CodeAnalysis; using static Helion.Util.Assertion.Assert; namespace Helion.Render; @@ -115,9 +113,6 @@ public Renderer(IWindow window, IConfig config, ArchiveCollection archiveCollect PrintGLInfo(); SetGLStates(); - - if (m_config.Developer.ForceBsp) - Log.Error("Developer.ForceBsp enabled!"); } private mat4 CalculateVirtualMvp(GLFramebuffer buffer, Dimension bufferDimension) diff --git a/Core/Util/Configs/Components/ConfigDeveloper.cs b/Core/Util/Configs/Components/ConfigDeveloper.cs index 32868ebc4..97c90f5fd 100644 --- a/Core/Util/Configs/Components/ConfigDeveloper.cs +++ b/Core/Util/Configs/Components/ConfigDeveloper.cs @@ -10,6 +10,12 @@ public class ConfigDeveloperRender: ConfigElement [ConfigInfo("Draw the tracers from autoaim and shooting for the player.", save: false)] public readonly ConfigValue Tracers = new(false); + + [ConfigInfo("Locks rendering to current state.", save: false)] + public readonly ConfigValue Lock = new(false); + + [ConfigInfo("Shows values for adaptive render mode.")] + public readonly ConfigValue DebugAdaptiveMode = new(false); } public class ConfigDeveloper: ConfigElement @@ -36,13 +42,4 @@ public class ConfigDeveloper: ConfigElement [ConfigInfo("Adds debug labels to GL objects.", save: true, restartRequired: true)] public readonly ConfigValue DebugLabel = new(false); - - [ConfigInfo("Forces renderer to use BSP rendering.")] - public readonly ConfigValue ForceBsp = new(false); - - [ConfigInfo("Locks rendering to current state.", save: false)] - public readonly ConfigValue LockRender = new(false); - - [ConfigInfo("Shows values for adaptive render mode.", save: false)] - public readonly ConfigValue DebugAdaptiveRenderMode = new(false); } diff --git a/Core/Util/Configs/Components/ConfigRender.cs b/Core/Util/Configs/Components/ConfigRender.cs index 6dcf436ee..e476ae172 100644 --- a/Core/Util/Configs/Components/ConfigRender.cs +++ b/Core/Util/Configs/Components/ConfigRender.cs @@ -224,9 +224,6 @@ public class ConfigRender: ConfigElement [ConfigInfo("Enable texture transparency.")] public readonly ConfigValue TextureTransparency = new(true); - [ConfigInfo("Traverse the BSP tree in a separate thread to mark lines seen for automap. If disabled, automap always shows all lines.")] - public readonly ConfigValue AutomapBspThread = new(true); - // This option is a hacked test that writes everything directly to the default backbuffer. Relies on undefined behavior since certain rendering functions need the depth texture. [ConfigInfo("Disables post processing effects like spectre fuzz refraction and skips FBO. Can have rendering defects.", restartRequired: true)] public readonly ConfigValue PostProcessingEffects = new(true); @@ -234,6 +231,6 @@ public class ConfigRender: ConfigElement [ConfigInfo("Changes the render mode.")] public readonly ConfigValue Mode = new(AdaptiveRenderMode.Static); - [ConfigInfo("The number of visible lines until the mode is switched to static when using adapative.")] - public readonly ConfigValue AdaptiveBspThreshold = new(2000); + [ConfigInfo("The number microseconds until the mode is switched to static when using adapative.")] + public readonly ConfigValue AdaptiveBspThreshold = new(1500); } diff --git a/Core/World/Geometry/Sectors/SectorPlane.cs b/Core/World/Geometry/Sectors/SectorPlane.cs index 15217e0bb..f38ff908a 100644 --- a/Core/World/Geometry/Sectors/SectorPlane.cs +++ b/Core/World/Geometry/Sectors/SectorPlane.cs @@ -104,7 +104,5 @@ public void SetTexture(int texture, int gametick) LastRenderChangeGametick = gametick; } - - public override string ToString() => $"Id={Id} Z={Z} Face={Facing} Texture={TextureHandle}"; } diff --git a/Core/World/Impl/SinglePlayer/AutomapMarker.cs b/Core/World/Impl/SinglePlayer/AutomapMarker.cs index 4451d78f9..f6274471c 100644 --- a/Core/World/Impl/SinglePlayer/AutomapMarker.cs +++ b/Core/World/Impl/SinglePlayer/AutomapMarker.cs @@ -44,8 +44,6 @@ public class AutomapMarker(IConfig config) : IBspHeuristics private int m_lastSegCount; private int m_lastLineCount; private int m_lastMicroseconds; - private float m_subsectorVisibility; - private float m_segVisibility; private readonly IConfig m_config = config; private readonly ConcurrentQueue m_positions = new(); @@ -53,12 +51,12 @@ public class AutomapMarker(IConfig config) : IBspHeuristics public int LastProcessedId { get; private set; } public event EventHandler? PositionProcessed; - public float SubsectorVisibility => m_subsectorVisibility; - public float SegVisibility => m_segVisibility; public int SubsectorCount => m_lastSubsectorCount; public int SegCount => m_lastSegCount; public int LineCount => m_lastLineCount; public int Microseconds => m_lastMicroseconds; + public bool LastBspSetting { get; set; } + public long LastProcessedTimeStamp { get; private set; } public void Start(IWorld world) { @@ -115,7 +113,7 @@ private void ClearData() public void AddPosition(Vec3D pos, Vec3D viewDirection, double angleRadians, double pitchRadians, int id) { - if (m_config.Render.Mode.Value != AdaptiveRenderMode.Adaptive || m_positions.Count == 0) + if (m_config.Render.Mode.Value != AdaptiveRenderMode.Adaptive || m_positions.IsEmpty) m_positions.Enqueue(new PlayerPosition(pos, viewDirection, angleRadians, pitchRadians, id)); } @@ -170,20 +168,17 @@ private void AutomapTask(CancellationToken token) private void MaxHeuristics() { - m_subsectorVisibility = 1; - m_segVisibility = 1; m_lastSegCount = int.MaxValue; m_lastLineCount = int.MaxValue; } private void SetHeuristics() { + LastProcessedTimeStamp = Stopwatch.GetTimestamp(); m_lastMicroseconds = (int)m_stopwatch.Elapsed.TotalMicroseconds; m_lastSubsectorCount = m_subsectorCount; m_lastSegCount = m_segCount; m_lastLineCount = m_lineCount; - m_segVisibility = m_segCount / (float)m_world.BspTree.Segments.Length; - m_subsectorVisibility = m_subsectorCount / (float)m_world.BspTree.Subsectors.Length; } private void SetFrustum(Rectangle viewport, PlayerPosition pos) @@ -254,15 +249,13 @@ private unsafe void MarkBspLineClips(uint nodeIndex, in Vec2D position, IWorld w continue; m_hitLines.Set(edge.LineId, true); - m_lineCount++; - ref var line = ref lineArray[edge.LineId]; - //if ((line.Flags & StructLineFlags.SeenForAutomap) != 0) - // continue; + ref var line = ref lineArray[edge.LineId]; if (!m_frustumPlanes.PointInFrustum(line.Segment.Start.X, line.Segment.Start.Y) && !m_frustumPlanes.PointInFrustum(line.Segment.End.X, line.Segment.End.Y)) continue; + m_lineCount++; if ((line.Flags & StructLineFlags.SeenForAutomap) != 0) continue; diff --git a/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs b/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs index 21276dbe6..3d8d3cf27 100644 --- a/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs +++ b/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs @@ -140,7 +140,6 @@ public SinglePlayerWorld(GlobalData globalData, IConfig config, ArchiveCollectio config.Player.Name.OnChanged += PlayerName_OnChanged; config.Player.Gender.OnChanged += PlayerGender_OnChanged; - config.Render.AutomapBspThread.OnChanged += AutomapBspThread_OnChanged; config.Game.MarkSpecials.OnChanged += MarkSpecials_OnChanged; ChaseCamPlayer = CreateChaseCamPlayer(); @@ -208,16 +207,6 @@ private void MarkSpecials_OnChanged(object? sender, bool e) MarkSpecials.Clear(this, Player); } - private void AutomapBspThread_OnChanged(object? sender, bool set) - { - m_automapMarker.Stop(); - - if (!set) - return; - - m_automapMarker.Start(this); - } - public override ListenerParams GetListener() { var player = GetCameraPlayer(); @@ -226,11 +215,8 @@ public override ListenerParams GetListener() public override void Tick() { - if (Config.Render.AutomapBspThread) - { - var camera = Player.GetCamera(0); - m_automapMarker.AddPosition(camera.PositionInterpolated.Double, camera.Direction.Double, Player.AngleRadians, Player.PitchRadians, GameTicker); - } + var camera = Player.GetCamera(0); + m_automapMarker.AddPosition(camera.PositionInterpolated.Double, camera.Direction.Double, Player.AngleRadians, Player.PitchRadians, GameTicker); if (GetCrosshairTarget(out Entity? entity)) Player.SetCrosshairTarget(entity); @@ -344,8 +330,7 @@ public override void Start(WorldModel? worldModel) if (!PlayLevelMusic(musicName)) AudioSystem.Music.Stop(); - if (Config.Render.AutomapBspThread.Value) - m_automapMarker.Start(this); + m_automapMarker.Start(this); } public override bool PlayLevelMusic(string name, MusicFlags flags = MusicFlags.Loop, Entity? activator = null) @@ -545,7 +530,6 @@ protected override void PerformDispose() Config.Player.Name.OnChanged -= PlayerName_OnChanged; Config.Player.Gender.OnChanged -= PlayerGender_OnChanged; - Config.Render.AutomapBspThread.OnChanged -= AutomapBspThread_OnChanged; Config.Game.MarkSpecials.OnChanged -= MarkSpecials_OnChanged; base.PerformDispose(); diff --git a/Core/World/WorldStatic.cs b/Core/World/WorldStatic.cs index ca9db70da..791943f52 100644 --- a/Core/World/WorldStatic.cs +++ b/Core/World/WorldStatic.cs @@ -61,10 +61,6 @@ public static class WorldStatic public static float DamageApplyMultiplier = 1; public static float DamageReceiveMultiplier = 1; public static int MaxSoulsphere = 200; - public static bool Bsp; - public static int BspLineCount; - public static int BspSegCount; - public static int BspMicroseconds; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool HasCustomBlood() => BloodColor || AutoColoredBlood || FuzzBlood; From 6fb841a251869360b67afb76be0fbc42048f0cb9 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 27 Aug 2026 06:58:23 -0400 Subject: [PATCH 04/26] add time config and use seg threshold --- Client/Client.cs | 4 +++ Core/Layer/Worlds/WorldLayer.Render.Hud.cs | 2 +- .../Renderers/Legacy/World/IBspHeuristics.cs | 3 +- .../Legacy/World/LegacyWorldRenderer.cs | 29 ++++++++--------- Core/Util/Configs/Components/ConfigRender.cs | 5 ++- Core/World/Impl/SinglePlayer/AutomapMarker.cs | 32 +++++++++---------- 6 files changed, 39 insertions(+), 36 deletions(-) diff --git a/Client/Client.cs b/Client/Client.cs index 49e8c0132..8ebb34e8c 100755 --- a/Client/Client.cs +++ b/Client/Client.cs @@ -38,6 +38,7 @@ using System.IO; using System.Reflection; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using static Helion.Util.Assertion.Assert; @@ -92,6 +93,9 @@ record struct VersionTest(int Major, int Minor); private Client(CommandLineArgs commandLineArgs, PathsManager pathsManager, IConfig config, HelionConsole console, IAudioSystem audioSystem, ArchiveCollection archiveCollection) { + Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.AboveNormal; + Thread.CurrentThread.Priority = ThreadPriority.AboveNormal; + m_commandLineArgs = commandLineArgs; m_pathsManager = pathsManager; m_config = config; diff --git a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs index 1368250cc..b4612eaca 100644 --- a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs +++ b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs @@ -348,7 +348,7 @@ private void DrawBspStats(IHudRenderContext hud) m_bspString.Clear(); var x = hud.MeasureText(" ", FixedNumberFont, m_infoFontSize).Width; - m_bspString.Append(bspHeuristics.LastBspSetting ? "BSP (" : "Static ("); + m_bspString.Append(bspHeuristics.UseBsp ? "BSP (" : "Static ("); m_bspString.Append(bspHeuristics.LineCount); m_bspString.Append('/'); m_bspString.Append(bspHeuristics.SegCount); diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs index 76ff81917..dda9ac9bf 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs @@ -8,6 +8,5 @@ public interface IBspHeuristics public int LastProcessedId { get; } public int Microseconds { get; } public long LastProcessedTimeStamp { get; } - - public bool LastBspSetting { get; set; } + public bool UseBsp { get; set; } } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs index 28232f275..8e32cc41e 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs @@ -306,7 +306,7 @@ protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFra if (world.GameTicker != m_lastTicker && renderInfo.TransferHeightView == TransferHeightView.Middle) { - m_lastUseBsp = UseBspBasedOnHeuristic(world); + m_lastUseBsp = UseBspBasedOnHeuristic(); m_renderStatic = !m_lastUseBsp; } @@ -427,7 +427,7 @@ protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFra RenderTransparent(renderInfo, framebuffer); } - private bool UseBspBasedOnHeuristic(IWorld world) + private bool UseBspBasedOnHeuristic() { if (m_config.Render.Mode.Value == AdaptiveRenderMode.Bsp) return true; @@ -440,32 +440,29 @@ private bool UseBspBasedOnHeuristic(IWorld world) var now = Stopwatch.GetTimestamp(); var ageTicks = now - m_bspHeuristics.LastProcessedTimeStamp; - double ageMicroseconds = ageTicks * (1_000_000.0 / Stopwatch.Frequency); + var ageMicroseconds = ageTicks * (1_000_000.0 / Stopwatch.Frequency); const double ProcessWindowUs = 500.0; if (ageMicroseconds <= ProcessWindowUs) - return m_bspHeuristics.LastBspSetting; + return m_bspHeuristics.UseBsp; - var threshold = m_config.Render.AdaptiveBspThreshold.Value; + var threshold = m_config.Render.AdaptiveBspTimeThreshold.Value; var highRange = threshold * 1.15f; var lowRange = threshold * 0.85f; var shouldUseBsp = m_bspHeuristics.Microseconds < threshold; - if (shouldUseBsp != m_lastUseBsp) - { - if (!shouldUseBsp && m_bspHeuristics.Microseconds < highRange) - shouldUseBsp = true; - else if (shouldUseBsp && m_bspHeuristics.Microseconds > lowRange) - shouldUseBsp = false; - } + if (!shouldUseBsp && m_bspHeuristics.Microseconds < highRange) + shouldUseBsp = true; + else if (shouldUseBsp && m_bspHeuristics.Microseconds > lowRange) + shouldUseBsp = false; - // Maybe add to config. Don't let fast CPUs switch to BSP when it's likely not beneficial. Maybe should be seg count? - if (m_bspHeuristics.LineCount > 2000) + // Don't let fast CPUs switch to BSP when it's likely not beneficial. + if (m_bspHeuristics.SegCount > m_config.Render.AdaptiveBspSegThreshold.Value) shouldUseBsp = false; - m_bspHeuristics.LastBspSetting = shouldUseBsp; - return m_bspHeuristics.LastBspSetting; + m_bspHeuristics.UseBsp = shouldUseBsp; + return m_bspHeuristics.UseBsp; } private void RenderFloodFill(RenderInfo renderInfo) diff --git a/Core/Util/Configs/Components/ConfigRender.cs b/Core/Util/Configs/Components/ConfigRender.cs index e476ae172..7c63d92cc 100644 --- a/Core/Util/Configs/Components/ConfigRender.cs +++ b/Core/Util/Configs/Components/ConfigRender.cs @@ -232,5 +232,8 @@ public class ConfigRender: ConfigElement public readonly ConfigValue Mode = new(AdaptiveRenderMode.Static); [ConfigInfo("The number microseconds until the mode is switched to static when using adapative.")] - public readonly ConfigValue AdaptiveBspThreshold = new(1500); + public readonly ConfigValue AdaptiveBspTimeThreshold = new(2200); + + [ConfigInfo("The number segs until the mode is switched to static when using adapative.")] + public readonly ConfigValue AdaptiveBspSegThreshold = new(8000); } diff --git a/Core/World/Impl/SinglePlayer/AutomapMarker.cs b/Core/World/Impl/SinglePlayer/AutomapMarker.cs index f6274471c..1beab76a9 100644 --- a/Core/World/Impl/SinglePlayer/AutomapMarker.cs +++ b/Core/World/Impl/SinglePlayer/AutomapMarker.cs @@ -40,10 +40,6 @@ public class AutomapMarker(IConfig config) : IBspHeuristics private int m_subsectorCount; private int m_segCount; private int m_lineCount; - private int m_lastSubsectorCount; - private int m_lastSegCount; - private int m_lastLineCount; - private int m_lastMicroseconds; private readonly IConfig m_config = config; private readonly ConcurrentQueue m_positions = new(); @@ -51,12 +47,12 @@ public class AutomapMarker(IConfig config) : IBspHeuristics public int LastProcessedId { get; private set; } public event EventHandler? PositionProcessed; - public int SubsectorCount => m_lastSubsectorCount; - public int SegCount => m_lastSegCount; - public int LineCount => m_lastLineCount; - public int Microseconds => m_lastMicroseconds; - public bool LastBspSetting { get; set; } + public int SubsectorCount { get; private set; } + public int SegCount { get; private set; } + public int LineCount { get; private set; } + public int Microseconds { get; private set; } public long LastProcessedTimeStamp { get; private set; } + public bool UseBsp { get; set; } public void Start(IWorld world) { @@ -75,7 +71,7 @@ public void Start(IWorld world) m_thread = new Thread(() => AutomapTask(m_cancelTasks.Token)) { IsBackground = true, - Priority = ThreadPriority.AboveNormal + Priority = ThreadPriority.Normal }; m_thread.Start(); } @@ -163,22 +159,26 @@ private void AutomapTask(CancellationToken token) if (m_config.Render.Mode.Value != AdaptiveRenderMode.Adaptive) Thread.Sleep(Math.Max(ticks - (int)m_stopwatch.ElapsedMilliseconds, 0)); + else + Thread.Yield(); } } private void MaxHeuristics() { - m_lastSegCount = int.MaxValue; - m_lastLineCount = int.MaxValue; + Microseconds = int.MaxValue; + SubsectorCount = int.MaxValue; + SegCount = int.MaxValue; + LineCount = int.MaxValue; } private void SetHeuristics() { LastProcessedTimeStamp = Stopwatch.GetTimestamp(); - m_lastMicroseconds = (int)m_stopwatch.Elapsed.TotalMicroseconds; - m_lastSubsectorCount = m_subsectorCount; - m_lastSegCount = m_segCount; - m_lastLineCount = m_lineCount; + Microseconds = (int)m_stopwatch.Elapsed.TotalMicroseconds; + SubsectorCount = m_subsectorCount; + SegCount = m_segCount; + LineCount = m_lineCount; } private void SetFrustum(Rectangle viewport, PlayerPosition pos) From 0cf3fe0b748bd2e1bfcc40b3d30bc8e57ac7b9b8 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 31 Aug 2026 07:52:10 -0400 Subject: [PATCH 05/26] use window of size 3 for timing heuristic --- Core/Layer/Worlds/WorldLayer.Render.Hud.cs | 2 +- .../Renderers/Legacy/World/IBspHeuristics.cs | 10 ++- .../LegacyWorldRenderer.BspHeuristics.cs | 79 +++++++++++++++++++ .../Legacy/World/LegacyWorldRenderer.cs | 42 +--------- Core/World/Impl/SinglePlayer/AutomapMarker.cs | 6 +- 5 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs diff --git a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs index b4612eaca..a26fabba0 100644 --- a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs +++ b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs @@ -348,7 +348,7 @@ private void DrawBspStats(IHudRenderContext hud) m_bspString.Clear(); var x = hud.MeasureText(" ", FixedNumberFont, m_infoFontSize).Width; - m_bspString.Append(bspHeuristics.UseBsp ? "BSP (" : "Static ("); + m_bspString.Append(bspHeuristics.Info.UseBsp ? "BSP (" : "Static ("); m_bspString.Append(bspHeuristics.LineCount); m_bspString.Append('/'); m_bspString.Append(bspHeuristics.SegCount); diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs index dda9ac9bf..6f6d64bfc 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs @@ -1,12 +1,20 @@ namespace Helion.Render.OpenGL.Renderers.Legacy.World; +public class BspHeuristicInfo +{ + public bool UseBsp { get; set; } + public int GameTick { get; set; } + public int SmoothTime { get; set; } +} + public interface IBspHeuristics { + public bool Valid { get; } public int SubsectorCount { get; } public int SegCount { get; } public int LineCount { get; } public int LastProcessedId { get; } public int Microseconds { get; } public long LastProcessedTimeStamp { get; } - public bool UseBsp { get; set; } + public BspHeuristicInfo Info { get; } } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs new file mode 100644 index 000000000..5bad4489a --- /dev/null +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs @@ -0,0 +1,79 @@ +using Helion.Util; +using Helion.Util.Configs.Components; +using Helion.World; +using System; +using System.Diagnostics; + +namespace Helion.Render.OpenGL.Renderers.Legacy.World; + +public partial class LegacyWorldRenderer +{ + private IBspHeuristics? m_bspHeuristics; + private double m_smoothedBspTimeUs; + private readonly double[] m_bspWindowSamples = new double[3]; + private int m_windowIndex; + private bool m_windowInit; + + private double AddBspTimeSample(double time) + { + m_bspWindowSamples[m_windowIndex] = time; + m_windowIndex = (m_windowIndex + 1) % 3; + + if (!m_windowInit && m_windowIndex < 2) + return time; + + m_windowInit = true; + double a = m_bspWindowSamples[0], b = m_bspWindowSamples[1], c = m_bspWindowSamples[2]; + double med = MathHelper.Max(MathHelper.Min(a, b), MathHelper.Min(MathHelper.Max(a, b), c)); + return med; + } + + private bool UseBspBasedOnHeuristic() + { + if (m_config.Render.Mode.Value == AdaptiveRenderMode.Bsp) + { + m_bspHeuristics?.Info.UseBsp = true; + return true; + } + + if (m_config.Render.Mode.Value == AdaptiveRenderMode.Static || m_bspHeuristics?.Valid == false) + { + m_bspHeuristics?.Info.UseBsp = false; + return false; + } + + if (m_bspHeuristics == null) + return false; + + var now = Stopwatch.GetTimestamp(); + var ageTicks = now - m_bspHeuristics.LastProcessedTimeStamp; + var ageMicroseconds = ageTicks * (1_000_000.0 / Stopwatch.Frequency); + + // It needs sometime to process + const double ProcessWindowUs = 500.0; + if (ageMicroseconds <= ProcessWindowUs) + return m_bspHeuristics.Info.UseBsp; + + var threshold = m_config.Render.AdaptiveBspTimeThreshold.Value; + var highRange = threshold * 1.15f; + var lowRange = threshold * 0.85f; + + m_smoothedBspTimeUs = AddBspTimeSample(m_bspHeuristics.Microseconds); + + var shouldUseBsp = m_smoothedBspTimeUs < threshold; + + if (!shouldUseBsp && m_smoothedBspTimeUs < highRange) + shouldUseBsp = true; + else if (shouldUseBsp && m_smoothedBspTimeUs > lowRange) + shouldUseBsp = false; + + // Don't let fast CPUs switch to BSP when it's likely not beneficial. + if (m_bspHeuristics.SegCount > m_config.Render.AdaptiveBspSegThreshold.Value) + shouldUseBsp = false; + + m_bspHeuristics.Info.GameTick = WorldStatic.World.GameTicker; + m_bspHeuristics.Info.SmoothTime = (int)m_smoothedBspTimeUs; + m_bspHeuristics.Info.UseBsp = shouldUseBsp; + return m_bspHeuristics.Info.UseBsp; + } +} diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs index 8e32cc41e..f2688f1ca 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs @@ -72,7 +72,6 @@ public partial class LegacyWorldRenderer : WorldRenderer private TransferHeightView m_lastTransferHeightsView; private PlaneClipFrameBuffer? m_planeClipFrameBuffer; private PlaneClipFrameBuffer? m_wallClipFrameBuffer; - private IBspHeuristics? m_bspHeuristics; public LegacyWorldRenderer(IConfig config, ArchiveCollection archiveCollection, LegacyGLTextureManager textureManager) { @@ -133,6 +132,7 @@ public override void UpdateToNewWorld(IWorld world) m_pixelGapCorrection = m_config.Render.PixelGapCorrection.Value; m_lastTransferHeightsView = TransferHeightView.Middle; m_bspHeuristics = world.GetBspHeuristics(); + m_smoothedBspTimeUs = -1; m_stopwatch.Stop(); Log.Info($"Completed level geometry {m_stopwatch.Elapsed}"); @@ -304,7 +304,7 @@ protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFra m_renderStatic = m_config.Render.Mode.Value != AdaptiveRenderMode.Bsp && renderInfo.TransferHeightView == TransferHeightView.Middle && !m_lastUseBsp; m_postProcessingEffects = m_config.Render.PostProcessingEffects; - if (world.GameTicker != m_lastTicker && renderInfo.TransferHeightView == TransferHeightView.Middle) + if (renderInfo.TransferHeightView == TransferHeightView.Middle) { m_lastUseBsp = UseBspBasedOnHeuristic(); m_renderStatic = !m_lastUseBsp; @@ -427,44 +427,6 @@ protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFra RenderTransparent(renderInfo, framebuffer); } - private bool UseBspBasedOnHeuristic() - { - if (m_config.Render.Mode.Value == AdaptiveRenderMode.Bsp) - return true; - - if (m_config.Render.Mode.Value == AdaptiveRenderMode.Static) - return false; - - if (m_bspHeuristics == null) - return false; - - var now = Stopwatch.GetTimestamp(); - var ageTicks = now - m_bspHeuristics.LastProcessedTimeStamp; - var ageMicroseconds = ageTicks * (1_000_000.0 / Stopwatch.Frequency); - - const double ProcessWindowUs = 500.0; - if (ageMicroseconds <= ProcessWindowUs) - return m_bspHeuristics.UseBsp; - - var threshold = m_config.Render.AdaptiveBspTimeThreshold.Value; - var highRange = threshold * 1.15f; - var lowRange = threshold * 0.85f; - - var shouldUseBsp = m_bspHeuristics.Microseconds < threshold; - - if (!shouldUseBsp && m_bspHeuristics.Microseconds < highRange) - shouldUseBsp = true; - else if (shouldUseBsp && m_bspHeuristics.Microseconds > lowRange) - shouldUseBsp = false; - - // Don't let fast CPUs switch to BSP when it's likely not beneficial. - if (m_bspHeuristics.SegCount > m_config.Render.AdaptiveBspSegThreshold.Value) - shouldUseBsp = false; - - m_bspHeuristics.UseBsp = shouldUseBsp; - return m_bspHeuristics.UseBsp; - } - private void RenderFloodFill(RenderInfo renderInfo) { // Doom would draw middle textures over flood fill. diff --git a/Core/World/Impl/SinglePlayer/AutomapMarker.cs b/Core/World/Impl/SinglePlayer/AutomapMarker.cs index 1beab76a9..d472188ff 100644 --- a/Core/World/Impl/SinglePlayer/AutomapMarker.cs +++ b/Core/World/Impl/SinglePlayer/AutomapMarker.cs @@ -9,7 +9,6 @@ using Helion.Util; using Helion.Util.Configs; using Helion.Util.Configs.Components; -using Helion.Util.Container; using Helion.World.Bsp; using Helion.World.Entities; using Helion.World.Entities.Definition; @@ -47,12 +46,13 @@ public class AutomapMarker(IConfig config) : IBspHeuristics public int LastProcessedId { get; private set; } public event EventHandler? PositionProcessed; + public bool Valid { get; private set; } public int SubsectorCount { get; private set; } public int SegCount { get; private set; } public int LineCount { get; private set; } public int Microseconds { get; private set; } public long LastProcessedTimeStamp { get; private set; } - public bool UseBsp { get; set; } + public BspHeuristicInfo Info { get; } = new(); public void Start(IWorld world) { @@ -166,6 +166,7 @@ private void AutomapTask(CancellationToken token) private void MaxHeuristics() { + Valid = false; Microseconds = int.MaxValue; SubsectorCount = int.MaxValue; SegCount = int.MaxValue; @@ -174,6 +175,7 @@ private void MaxHeuristics() private void SetHeuristics() { + Valid = true; LastProcessedTimeStamp = Stopwatch.GetTimestamp(); Microseconds = (int)m_stopwatch.Elapsed.TotalMicroseconds; SubsectorCount = m_subsectorCount; From 576192700e9df3b3da5ab136f20cc40c93f3d4f6 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 31 Aug 2026 09:39:26 -0400 Subject: [PATCH 06/26] allow to run without actually marking lines --- Core/World/Impl/SinglePlayer/AutomapMarker.cs | 6 ++++-- Core/World/Impl/SinglePlayer/PlayerPosition.cs | 3 ++- Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Core/World/Impl/SinglePlayer/AutomapMarker.cs b/Core/World/Impl/SinglePlayer/AutomapMarker.cs index d472188ff..b890009a6 100644 --- a/Core/World/Impl/SinglePlayer/AutomapMarker.cs +++ b/Core/World/Impl/SinglePlayer/AutomapMarker.cs @@ -39,6 +39,7 @@ public class AutomapMarker(IConfig config) : IBspHeuristics private int m_subsectorCount; private int m_segCount; private int m_lineCount; + private bool m_markLines; private readonly IConfig m_config = config; private readonly ConcurrentQueue m_positions = new(); @@ -107,10 +108,10 @@ private void ClearData() m_viewClipper.Clear(); } - public void AddPosition(Vec3D pos, Vec3D viewDirection, double angleRadians, double pitchRadians, int id) + public void AddPosition(Vec3D pos, Vec3D viewDirection, double angleRadians, double pitchRadians, int id, bool markLines = true) { if (m_config.Render.Mode.Value != AdaptiveRenderMode.Adaptive || m_positions.IsEmpty) - m_positions.Enqueue(new PlayerPosition(pos, viewDirection, angleRadians, pitchRadians, id)); + m_positions.Enqueue(new PlayerPosition(pos, viewDirection, angleRadians, pitchRadians, id, markLines)); } private void AutomapTask(CancellationToken token) @@ -137,6 +138,7 @@ private void AutomapTask(CancellationToken token) if (token.IsCancellationRequested) return; + m_markLines = pos.MarkLines; m_subsectorCount = 0; m_segCount = 0; m_lineCount = 0; diff --git a/Core/World/Impl/SinglePlayer/PlayerPosition.cs b/Core/World/Impl/SinglePlayer/PlayerPosition.cs index 7a646b009..6fad829bd 100644 --- a/Core/World/Impl/SinglePlayer/PlayerPosition.cs +++ b/Core/World/Impl/SinglePlayer/PlayerPosition.cs @@ -2,11 +2,12 @@ namespace Helion.World.Impl.SinglePlayer; -public readonly struct PlayerPosition(Vec3D position, Vec3D viewDirection, double angleRadians, double pitchRadians, int id) +public readonly struct PlayerPosition(Vec3D position, Vec3D viewDirection, double angleRadians, double pitchRadians, int id, bool markLines) { public readonly Vec3D Position = position; public readonly Vec3D ViewDirection = viewDirection; public readonly double AngleRadians = angleRadians; public readonly double PitchRadians = pitchRadians; public readonly int Id = id; + public readonly bool MarkLines = markLines; } diff --git a/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs b/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs index 68387827b..feb4b1956 100644 --- a/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs +++ b/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs @@ -214,8 +214,9 @@ public override ListenerParams GetListener() public override void Tick() { - var camera = Player.GetCamera(0); - m_automapMarker.AddPosition(camera.PositionInterpolated.Double, camera.Direction.Double, Player.AngleRadians, Player.PitchRadians, GameTicker); + var player = m_chaseCamMode ? ChaseCamPlayer : Player; + var camera = player.GetCamera(0); + m_automapMarker.AddPosition(camera.PositionInterpolated.Double, camera.Direction.Double, player.AngleRadians, player.PitchRadians, GameTicker, !m_chaseCamMode); if (GetCrosshairTarget(out Entity? entity)) Player.SetCrosshairTarget(entity); From 23932687302391e8f72985639b300dafe467395a Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 3 Sep 2026 06:00:56 -0400 Subject: [PATCH 07/26] use above/below threshold counts to switch --- .../LegacyWorldRenderer.BspHeuristics.cs | 53 +++++++++--------- .../Legacy/World/LegacyWorldRenderer.cs | 3 + Core/Util/Configs/Components/ConfigRender.cs | 6 ++ Core/Util/TimeWindow.cs | 55 +++++++++++++++++++ 4 files changed, 92 insertions(+), 25 deletions(-) create mode 100644 Core/Util/TimeWindow.cs diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs index 5bad4489a..0f92f13ac 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs @@ -1,7 +1,6 @@ using Helion.Util; using Helion.Util.Configs.Components; using Helion.World; -using System; using System.Diagnostics; namespace Helion.Render.OpenGL.Renderers.Legacy.World; @@ -10,23 +9,11 @@ public partial class LegacyWorldRenderer { private IBspHeuristics? m_bspHeuristics; private double m_smoothedBspTimeUs; - private readonly double[] m_bspWindowSamples = new double[3]; - private int m_windowIndex; - private bool m_windowInit; + private readonly TimeWindow m_bspTimeWindow = new(32); - private double AddBspTimeSample(double time) - { - m_bspWindowSamples[m_windowIndex] = time; - m_windowIndex = (m_windowIndex + 1) % 3; - - if (!m_windowInit && m_windowIndex < 2) - return time; - - m_windowInit = true; - double a = m_bspWindowSamples[0], b = m_bspWindowSamples[1], c = m_bspWindowSamples[2]; - double med = MathHelper.Max(MathHelper.Min(a, b), MathHelper.Min(MathHelper.Max(a, b), c)); - return med; - } + private int m_aboveThresholdCount; + private int m_belowThresholdCount; + private int m_lastProcessedId; private bool UseBspBasedOnHeuristic() { @@ -51,20 +38,30 @@ private bool UseBspBasedOnHeuristic() // It needs sometime to process const double ProcessWindowUs = 500.0; - if (ageMicroseconds <= ProcessWindowUs) + if (ageMicroseconds <= ProcessWindowUs || m_lastProcessedId == m_bspHeuristics.LastProcessedId) return m_bspHeuristics.Info.UseBsp; - var threshold = m_config.Render.AdaptiveBspTimeThreshold.Value; - var highRange = threshold * 1.15f; - var lowRange = threshold * 0.85f; - + m_lastProcessedId = m_bspHeuristics.LastProcessedId; m_smoothedBspTimeUs = AddBspTimeSample(m_bspHeuristics.Microseconds); - var shouldUseBsp = m_smoothedBspTimeUs < threshold; + var threshold = m_config.Render.AdaptiveBspTimeThreshold.Value; + if (m_smoothedBspTimeUs < threshold) + { + m_belowThresholdCount++; + m_aboveThresholdCount = 0; + } + else + { + m_aboveThresholdCount++; + m_belowThresholdCount = 0; + } - if (!shouldUseBsp && m_smoothedBspTimeUs < highRange) + int thresholdCount = m_config.Render.AdaptiveBspSwitchCount.Value; + var shouldUseBsp = m_smoothedBspTimeUs < threshold; + if (!shouldUseBsp && m_belowThresholdCount >= thresholdCount) shouldUseBsp = true; - else if (shouldUseBsp && m_smoothedBspTimeUs > lowRange) + + if (shouldUseBsp && m_aboveThresholdCount >= thresholdCount) shouldUseBsp = false; // Don't let fast CPUs switch to BSP when it's likely not beneficial. @@ -76,4 +73,10 @@ private bool UseBspBasedOnHeuristic() m_bspHeuristics.Info.UseBsp = shouldUseBsp; return m_bspHeuristics.Info.UseBsp; } + + private double AddBspTimeSample(double time) + { + m_bspTimeWindow.SetWindowSize(m_config.Render.AdaptiveBspTimeWindow.Value); + return m_bspTimeWindow.AdddTimeSample(time); + } } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs index 99db08264..4e870db8f 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs @@ -133,6 +133,9 @@ public override void UpdateToNewWorld(IWorld world) m_lastTransferHeightsView = TransferHeightView.Middle; m_bspHeuristics = world.GetBspHeuristics(); m_smoothedBspTimeUs = -1; + m_bspTimeWindow.Clear(); + m_aboveThresholdCount = 0; + m_belowThresholdCount = 0; m_stopwatch.Stop(); Log.Info($"Completed level geometry {m_stopwatch.Elapsed}"); diff --git a/Core/Util/Configs/Components/ConfigRender.cs b/Core/Util/Configs/Components/ConfigRender.cs index 7c63d92cc..ccdc07aab 100644 --- a/Core/Util/Configs/Components/ConfigRender.cs +++ b/Core/Util/Configs/Components/ConfigRender.cs @@ -236,4 +236,10 @@ public class ConfigRender: ConfigElement [ConfigInfo("The number segs until the mode is switched to static when using adapative.")] public readonly ConfigValue AdaptiveBspSegThreshold = new(8000); + + [ConfigInfo("The number of window samples to use smoothing time calculations.")] + public readonly ConfigValue AdaptiveBspTimeWindow = new(10, Clamp(4, 32)); + + [ConfigInfo("The number of times to hit above/below threshold before switching modes.")] + public readonly ConfigValue AdaptiveBspSwitchCount = new(5, Clamp(1, 10)); } diff --git a/Core/Util/TimeWindow.cs b/Core/Util/TimeWindow.cs new file mode 100644 index 000000000..f3706e4b8 --- /dev/null +++ b/Core/Util/TimeWindow.cs @@ -0,0 +1,55 @@ +using System; + +namespace Helion.Util; + +public class TimeWindow +{ + private double[] m_samples = new double[32]; + private double[] m_sorted = new double[32]; + private bool m_init; + private int m_index; + private int m_windowSize; + + public TimeWindow(int windowSize) + { + SetWindowSize(windowSize); + } + + public void Clear() => Array.Clear(m_samples, 0, m_windowSize); + + public ReadOnlySpan GetTimeWindow() => m_samples.AsSpan(0, m_windowSize); + + public void SetWindowSize(int size) + { + if ((size & 1) != 0) + size++; + + size = Math.Max(size, 4); + + if (size > m_samples.Length) + { + Array.Resize(ref m_samples, size); + Array.Resize(ref m_sorted, size); + } + + m_windowSize = size; + } + + public double AdddTimeSample(double time) + { + m_samples[m_index] = time; + m_index = (m_index + 1) % m_windowSize; + + // Not enough samples + if (!m_init && m_index != 0) + return time; + + m_init = true; + + Array.Copy(m_samples, m_sorted, m_windowSize); + Array.Sort(m_sorted, 0, m_windowSize); + + int mid = m_windowSize / 2; + return 0.5 * (m_sorted[mid - 1] + m_sorted[mid]); + } +} From 90bf319080b8c1d40b3262c4ca8abd07b4da212a Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 3 Sep 2026 12:37:50 -0400 Subject: [PATCH 08/26] add counts for debugging --- Core/Layer/Worlds/WorldLayer.Render.Hud.cs | 6 +++--- Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs | 2 ++ .../Legacy/World/LegacyWorldRenderer.BspHeuristics.cs | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs index a26fabba0..c41595f5f 100644 --- a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs +++ b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs @@ -349,11 +349,11 @@ private void DrawBspStats(IHudRenderContext hud) var x = hud.MeasureText(" ", FixedNumberFont, m_infoFontSize).Width; m_bspString.Append(bspHeuristics.Info.UseBsp ? "BSP (" : "Static ("); - m_bspString.Append(bspHeuristics.LineCount); + m_bspString.Append(bspHeuristics.Info.BelowThresholdCount); m_bspString.Append('/'); - m_bspString.Append(bspHeuristics.SegCount); + m_bspString.Append(bspHeuristics.Info.AboveThresholdCount); m_bspString.Append('/'); - m_bspString.Append(bspHeuristics.Microseconds); + m_bspString.Append(bspHeuristics.Info.SmoothTime); m_bspString.Append(')'); SetRenderableString(m_bspString.AsSpan(), m_renderBspString, FixedNumberFont, m_infoFontSize, useDoomScale: false); hud.Text(m_renderBspString, (-x, m_padding / 2), Align.TopMiddle, alpha: m_hudAlpha); diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs index 6f6d64bfc..23b0781d3 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs @@ -5,6 +5,8 @@ public class BspHeuristicInfo public bool UseBsp { get; set; } public int GameTick { get; set; } public int SmoothTime { get; set; } + public int AboveThresholdCount { get; set; } + public int BelowThresholdCount { get; set; } } public interface IBspHeuristics diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs index 0f92f13ac..307b1ae78 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs @@ -70,6 +70,8 @@ private bool UseBspBasedOnHeuristic() m_bspHeuristics.Info.GameTick = WorldStatic.World.GameTicker; m_bspHeuristics.Info.SmoothTime = (int)m_smoothedBspTimeUs; + m_bspHeuristics.Info.AboveThresholdCount = m_aboveThresholdCount; + m_bspHeuristics.Info.BelowThresholdCount = m_belowThresholdCount; m_bspHeuristics.Info.UseBsp = shouldUseBsp; return m_bspHeuristics.Info.UseBsp; } From 8e543307fb5c854daf7c4c5a0ce3a42120234a23 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 4 Sep 2026 06:23:15 -0400 Subject: [PATCH 09/26] fix flip flopping --- Core/Layer/Worlds/WorldLayer.Render.Hud.cs | 2 ++ .../OpenGL/Renderers/Legacy/World/IBspHeuristics.cs | 1 + .../World/LegacyWorldRenderer.BspHeuristics.cs | 12 +++++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs index c41595f5f..81476d1c0 100644 --- a/Core/Layer/Worlds/WorldLayer.Render.Hud.cs +++ b/Core/Layer/Worlds/WorldLayer.Render.Hud.cs @@ -349,6 +349,8 @@ private void DrawBspStats(IHudRenderContext hud) var x = hud.MeasureText(" ", FixedNumberFont, m_infoFontSize).Width; m_bspString.Append(bspHeuristics.Info.UseBsp ? "BSP (" : "Static ("); + m_bspString.Append(bspHeuristics.Info.SegCount); + m_bspString.Append('/'); m_bspString.Append(bspHeuristics.Info.BelowThresholdCount); m_bspString.Append('/'); m_bspString.Append(bspHeuristics.Info.AboveThresholdCount); diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs index 23b0781d3..52abcbe3b 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs @@ -7,6 +7,7 @@ public class BspHeuristicInfo public int SmoothTime { get; set; } public int AboveThresholdCount { get; set; } public int BelowThresholdCount { get; set; } + public int SegCount { get; set; } } public interface IBspHeuristics diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs index 307b1ae78..24faf2a30 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs @@ -60,10 +60,19 @@ private bool UseBspBasedOnHeuristic() var shouldUseBsp = m_smoothedBspTimeUs < threshold; if (!shouldUseBsp && m_belowThresholdCount >= thresholdCount) shouldUseBsp = true; - if (shouldUseBsp && m_aboveThresholdCount >= thresholdCount) shouldUseBsp = false; + // Don't let the BSP heuristics flip-flop too quickly. If the smoothed time is within 10% of the threshold, don't switch. + if (shouldUseBsp != m_bspHeuristics.Info.UseBsp) + { + const double PercentRange = 0.1; + var highRange = threshold * (1 + PercentRange); + var lowRange = threshold * (1 - PercentRange); + if (m_smoothedBspTimeUs >= lowRange && m_smoothedBspTimeUs <= highRange) + shouldUseBsp = m_bspHeuristics.Info.UseBsp; + } + // Don't let fast CPUs switch to BSP when it's likely not beneficial. if (m_bspHeuristics.SegCount > m_config.Render.AdaptiveBspSegThreshold.Value) shouldUseBsp = false; @@ -72,6 +81,7 @@ private bool UseBspBasedOnHeuristic() m_bspHeuristics.Info.SmoothTime = (int)m_smoothedBspTimeUs; m_bspHeuristics.Info.AboveThresholdCount = m_aboveThresholdCount; m_bspHeuristics.Info.BelowThresholdCount = m_belowThresholdCount; + m_bspHeuristics.Info.SegCount = m_bspHeuristics.SegCount; m_bspHeuristics.Info.UseBsp = shouldUseBsp; return m_bspHeuristics.Info.UseBsp; } From 07756bd10e18c432a2017ba9b3bf84e7cfeb0fe0 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 4 Sep 2026 08:35:38 -0400 Subject: [PATCH 10/26] add suggestion --- .../LegacyWorldRenderer.BspHeuristics.cs | 35 ++++++++++++++++++- .../Legacy/World/LegacyWorldRenderer.cs | 8 +++-- Core/Render/Renderer.cs | 2 +- Core/Util/Configs/Components/ConfigRender.cs | 2 +- Core/Util/TimeWindow.cs | 2 ++ 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs index 24faf2a30..7d0144b81 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs @@ -1,5 +1,6 @@ using Helion.Util; using Helion.Util.Configs.Components; +using Helion.Util.Loggers; using Helion.World; using System.Diagnostics; @@ -10,12 +11,15 @@ public partial class LegacyWorldRenderer private IBspHeuristics? m_bspHeuristics; private double m_smoothedBspTimeUs; private readonly TimeWindow m_bspTimeWindow = new(32); + private readonly TimeWindow m_fpsWindow = new(10); private int m_aboveThresholdCount; private int m_belowThresholdCount; private int m_lastProcessedId; + private int m_adaptiveSuggestionsHitCount; + private bool m_loggedAdaptiveSuggestion; - private bool UseBspBasedOnHeuristic() + private bool UseBspBasedOnHeuristic(IWorld world) { if (m_config.Render.Mode.Value == AdaptiveRenderMode.Bsp) { @@ -25,6 +29,7 @@ private bool UseBspBasedOnHeuristic() if (m_config.Render.Mode.Value == AdaptiveRenderMode.Static || m_bspHeuristics?.Valid == false) { + CheckAdaptiveSuggest(world); m_bspHeuristics?.Info.UseBsp = false; return false; } @@ -86,6 +91,34 @@ private bool UseBspBasedOnHeuristic() return m_bspHeuristics.Info.UseBsp; } + private void CheckAdaptiveSuggest(IWorld world) + { + if (m_bspHeuristics == null || m_loggedAdaptiveSuggestion || world.GameTicker < 70 || + m_lastProcessedId == m_bspHeuristics.LastProcessedId || !m_bspHeuristics.Valid) + { + return; + } + + m_lastProcessedId = m_bspHeuristics.LastProcessedId; + + var fpsValue = m_fpsWindow.AdddTimeSample(m_fpsTracker.AverageFramesPerSecond); + if (!m_fpsWindow.IsInitialized) + return; + + if (fpsValue > 60 || (m_config.Render.MaxFPS.Value != 0 && fpsValue > m_config.Render.MaxFPS.Value)) + return; + + if (m_bspHeuristics.Microseconds >= m_config.Render.AdaptiveBspTimeThreshold.Value * 0.6) + return; + + m_adaptiveSuggestionsHitCount++; + if (m_adaptiveSuggestionsHitCount >= 3) + { + m_loggedAdaptiveSuggestion = true; + HelionLog.Info("Low FPS detected. Considering switching to adaptive rendering mode. (render.mode 2)"); + } + } + private double AddBspTimeSample(double time) { m_bspTimeWindow.SetWindowSize(m_config.Render.AdaptiveBspTimeWindow.Value); diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs index 4e870db8f..dd4c0ab93 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs @@ -16,7 +16,7 @@ using Helion.Util; using Helion.Util.Configs; using Helion.Util.Configs.Components; -using Helion.Util.Loggers; +using Helion.Util.Timing; using Helion.World; using Helion.World.Entities; using Helion.World.Geometry.Sectors; @@ -56,6 +56,7 @@ public partial class LegacyWorldRenderer : WorldRenderer private readonly Stopwatch m_stopwatch = new(); private readonly OitFrameBuffer m_oitFrameBuffer = new(); private readonly RenderInfo m_downSizedRenderInfo = new(); + private readonly FpsTracker m_fpsTracker; private readonly bool m_vanillaRender; private Vec2D m_occludeViewPos; private bool m_occlude; @@ -73,9 +74,10 @@ public partial class LegacyWorldRenderer : WorldRenderer private PlaneClipFrameBuffer? m_planeClipFrameBuffer; private PlaneClipFrameBuffer? m_wallClipFrameBuffer; - public LegacyWorldRenderer(IConfig config, ArchiveCollection archiveCollection, LegacyGLTextureManager textureManager) + public LegacyWorldRenderer(IConfig config, ArchiveCollection archiveCollection, LegacyGLTextureManager textureManager, FpsTracker fpsTracker) { m_config = config; + m_fpsTracker = fpsTracker; m_entityRenderer = new(config, textureManager, archiveCollection); m_primitiveRenderer = new(); m_worldDataManager = new(m_interpolationProgram); @@ -309,7 +311,7 @@ protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFra if (renderInfo.TransferHeightView == TransferHeightView.Middle) { - m_lastUseBsp = UseBspBasedOnHeuristic(); + m_lastUseBsp = UseBspBasedOnHeuristic(world); m_renderStatic = !m_lastUseBsp; } diff --git a/Core/Render/Renderer.cs b/Core/Render/Renderer.cs index 349a4d46b..ca8f363d5 100644 --- a/Core/Render/Renderer.cs +++ b/Core/Render/Renderer.cs @@ -93,7 +93,7 @@ public Renderer(IWindow window, IConfig config, ArchiveCollection archiveCollect SetShaderVars(); Textures = new LegacyGLTextureManager(config, archiveCollection); - m_worldRenderer = new LegacyWorldRenderer(config, archiveCollection, Textures); + m_worldRenderer = new LegacyWorldRenderer(config, archiveCollection, Textures, fpsTracker); m_hudRenderer = new LegacyHudRenderer(config, Textures, archiveCollection.DataCache); m_automapRenderer = new LegacyAutomapRenderer(archiveCollection); m_transitionRenderer = new TransitionRenderer(window); diff --git a/Core/Util/Configs/Components/ConfigRender.cs b/Core/Util/Configs/Components/ConfigRender.cs index ccdc07aab..403b1c522 100644 --- a/Core/Util/Configs/Components/ConfigRender.cs +++ b/Core/Util/Configs/Components/ConfigRender.cs @@ -241,5 +241,5 @@ public class ConfigRender: ConfigElement public readonly ConfigValue AdaptiveBspTimeWindow = new(10, Clamp(4, 32)); [ConfigInfo("The number of times to hit above/below threshold before switching modes.")] - public readonly ConfigValue AdaptiveBspSwitchCount = new(5, Clamp(1, 10)); + public readonly ConfigValue AdaptiveBspSwitchCount = new(3, Clamp(1, 10)); } diff --git a/Core/Util/TimeWindow.cs b/Core/Util/TimeWindow.cs index f3706e4b8..97c84c002 100644 --- a/Core/Util/TimeWindow.cs +++ b/Core/Util/TimeWindow.cs @@ -15,6 +15,8 @@ public TimeWindow(int windowSize) SetWindowSize(windowSize); } + public bool IsInitialized => m_init; + public void Clear() => Array.Clear(m_samples, 0, m_windowSize); public ReadOnlySpan GetTimeWindow() => m_samples.AsSpan(0, m_windowSize); From bfb7b7ac5794426ad0e45b3b1a6edd8cbd4a6d1c Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 4 Sep 2026 11:35:38 -0400 Subject: [PATCH 11/26] move adaptive to it's own section --- .../LegacyWorldRenderer.BspHeuristics.cs | 18 ++++++++--------- Core/Util/Configs/Components/ConfigRender.cs | 20 +++++++++++-------- Core/Util/{TimeWindow.cs => SampleWindow.cs} | 12 +++++------ 3 files changed, 27 insertions(+), 23 deletions(-) rename Core/Util/{TimeWindow.cs => SampleWindow.cs} (79%) diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs index 7d0144b81..708503665 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs @@ -10,8 +10,8 @@ public partial class LegacyWorldRenderer { private IBspHeuristics? m_bspHeuristics; private double m_smoothedBspTimeUs; - private readonly TimeWindow m_bspTimeWindow = new(32); - private readonly TimeWindow m_fpsWindow = new(10); + private readonly SampleWindow m_bspTimeWindow = new(32); + private readonly SampleWindow m_fpsWindow = new(10); private int m_aboveThresholdCount; private int m_belowThresholdCount; @@ -49,7 +49,7 @@ private bool UseBspBasedOnHeuristic(IWorld world) m_lastProcessedId = m_bspHeuristics.LastProcessedId; m_smoothedBspTimeUs = AddBspTimeSample(m_bspHeuristics.Microseconds); - var threshold = m_config.Render.AdaptiveBspTimeThreshold.Value; + var threshold = m_config.Render.Adaptive.TimeThreshold.Value; if (m_smoothedBspTimeUs < threshold) { m_belowThresholdCount++; @@ -61,7 +61,7 @@ private bool UseBspBasedOnHeuristic(IWorld world) m_belowThresholdCount = 0; } - int thresholdCount = m_config.Render.AdaptiveBspSwitchCount.Value; + int thresholdCount = m_config.Render.Adaptive.SwitchCount.Value; var shouldUseBsp = m_smoothedBspTimeUs < threshold; if (!shouldUseBsp && m_belowThresholdCount >= thresholdCount) shouldUseBsp = true; @@ -79,7 +79,7 @@ private bool UseBspBasedOnHeuristic(IWorld world) } // Don't let fast CPUs switch to BSP when it's likely not beneficial. - if (m_bspHeuristics.SegCount > m_config.Render.AdaptiveBspSegThreshold.Value) + if (m_bspHeuristics.SegCount > m_config.Render.Adaptive.SegThreshold.Value) shouldUseBsp = false; m_bspHeuristics.Info.GameTick = WorldStatic.World.GameTicker; @@ -101,14 +101,14 @@ private void CheckAdaptiveSuggest(IWorld world) m_lastProcessedId = m_bspHeuristics.LastProcessedId; - var fpsValue = m_fpsWindow.AdddTimeSample(m_fpsTracker.AverageFramesPerSecond); + var fpsValue = m_fpsWindow.AddSampleAndCalcMedian(m_fpsTracker.AverageFramesPerSecond); if (!m_fpsWindow.IsInitialized) return; if (fpsValue > 60 || (m_config.Render.MaxFPS.Value != 0 && fpsValue > m_config.Render.MaxFPS.Value)) return; - if (m_bspHeuristics.Microseconds >= m_config.Render.AdaptiveBspTimeThreshold.Value * 0.6) + if (m_bspHeuristics.Microseconds >= m_config.Render.Adaptive.TimeThreshold.Value * 0.6) return; m_adaptiveSuggestionsHitCount++; @@ -121,7 +121,7 @@ private void CheckAdaptiveSuggest(IWorld world) private double AddBspTimeSample(double time) { - m_bspTimeWindow.SetWindowSize(m_config.Render.AdaptiveBspTimeWindow.Value); - return m_bspTimeWindow.AdddTimeSample(time); + m_bspTimeWindow.SetWindowSize(m_config.Render.Adaptive.TimeWindow.Value); + return m_bspTimeWindow.AddSampleAndCalcMedian(time); } } diff --git a/Core/Util/Configs/Components/ConfigRender.cs b/Core/Util/Configs/Components/ConfigRender.cs index 403b1c522..5ed8098fa 100644 --- a/Core/Util/Configs/Components/ConfigRender.cs +++ b/Core/Util/Configs/Components/ConfigRender.cs @@ -83,7 +83,7 @@ public class ConfigRenderHealthBar : ConfigElement public readonly ConfigValue HealthLimit = new(0, GreaterOrEqual(0)); } -public class ConfigRender: ConfigElement +public class ConfigRender : ConfigElement { // VSync and rate limiting @@ -120,7 +120,7 @@ public class ConfigRender: ConfigElement // Viewport [ConfigInfo("Field of view.")] - [OptionMenu(OptionSectionType.Render, "Field Of View", spacer:true, sliderMin: 60.0, sliderMax: 120.0, sliderStep: .5)] + [OptionMenu(OptionSectionType.Render, "Field Of View", spacer: true, sliderMin: 60.0, sliderMax: 120.0, sliderStep: .5)] public readonly ConfigValue FieldOfView = new(90, Clamp(60.0, 400)); [ConfigInfo("Max render distance.")] @@ -166,7 +166,7 @@ public class ConfigRender: ConfigElement [ConfigInfo("Enable sprite transparency.")] [OptionMenu(OptionSectionType.Render, "Sprite Transparency")] public readonly ConfigValue SpriteTransparency = new(true); - + [ConfigInfo("Render sprites emulating software sprite clipping. May slow down rendering.", mapRestartRequired: true)] [OptionMenu(OptionSectionType.Render, "Emulate Vanilla Rendering", spacer: true)] public readonly ConfigValue VanillaRender = new(false); @@ -178,7 +178,7 @@ public class ConfigRender: ConfigElement [ConfigInfo("Emulates custom invulnerability palettes in true color mode. May not work well with all WADs. Application restart required.", restartRequired: true)] [OptionMenu(OptionSectionType.Render, "Emulate Invulnerability Colormap")] public readonly ConfigValue EmulateInvulnerabilityColorMap = new(false); - + [ConfigInfo("Uses a custom color overlay for Invulnerability instead of the vanilla inverse/white strobe.")] [OptionMenu(OptionSectionType.Render, "Alternative Invulnerability Overlay")] public readonly ConfigValue AlternativeInvulnerabilityOverlay = new(false); @@ -230,16 +230,20 @@ public class ConfigRender: ConfigElement [ConfigInfo("Changes the render mode.")] public readonly ConfigValue Mode = new(AdaptiveRenderMode.Static); + public readonly ConfigRenderAdaptive Adaptive = new(); +} +public class ConfigRenderAdaptive : ConfigElement +{ [ConfigInfo("The number microseconds until the mode is switched to static when using adapative.")] - public readonly ConfigValue AdaptiveBspTimeThreshold = new(2200); + public readonly ConfigValue TimeThreshold = new(2200, GreaterOrEqual(1)); [ConfigInfo("The number segs until the mode is switched to static when using adapative.")] - public readonly ConfigValue AdaptiveBspSegThreshold = new(8000); + public readonly ConfigValue SegThreshold = new(8000, GreaterOrEqual(1)); [ConfigInfo("The number of window samples to use smoothing time calculations.")] - public readonly ConfigValue AdaptiveBspTimeWindow = new(10, Clamp(4, 32)); + public readonly ConfigValue TimeWindow = new(10, Clamp(4, 32)); [ConfigInfo("The number of times to hit above/below threshold before switching modes.")] - public readonly ConfigValue AdaptiveBspSwitchCount = new(3, Clamp(1, 10)); + public readonly ConfigValue SwitchCount = new(3, Clamp(1, 10)); } diff --git a/Core/Util/TimeWindow.cs b/Core/Util/SampleWindow.cs similarity index 79% rename from Core/Util/TimeWindow.cs rename to Core/Util/SampleWindow.cs index 97c84c002..8b2d9fef7 100644 --- a/Core/Util/TimeWindow.cs +++ b/Core/Util/SampleWindow.cs @@ -2,7 +2,7 @@ namespace Helion.Util; -public class TimeWindow +public class SampleWindow { private double[] m_samples = new double[32]; private double[] m_sorted = new double[32]; @@ -10,7 +10,7 @@ public class TimeWindow private int m_index; private int m_windowSize; - public TimeWindow(int windowSize) + public SampleWindow(int windowSize) { SetWindowSize(windowSize); } @@ -19,7 +19,7 @@ public TimeWindow(int windowSize) public void Clear() => Array.Clear(m_samples, 0, m_windowSize); - public ReadOnlySpan GetTimeWindow() => m_samples.AsSpan(0, m_windowSize); + public ReadOnlySpan GetSampleWindow() => m_samples.AsSpan(0, m_windowSize); public void SetWindowSize(int size) { @@ -37,14 +37,14 @@ public void SetWindowSize(int size) m_windowSize = size; } - public double AdddTimeSample(double time) + public double AddSampleAndCalcMedian(double sample) { - m_samples[m_index] = time; + m_samples[m_index] = sample; m_index = (m_index + 1) % m_windowSize; // Not enough samples if (!m_init && m_index != 0) - return time; + return sample; m_init = true; From d52a8bf0ea1a85cd183bec26f2def0d485b4f15c Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 5 Sep 2026 06:06:15 -0400 Subject: [PATCH 12/26] use profilers for timing --- Client/Client.cs | 2 +- Client/Window.cs | 6 ++++-- .../World/LegacyWorldRenderer.BspHeuristics.cs | 17 ++++++++++------- .../Legacy/World/LegacyWorldRenderer.cs | 16 +++++++++++++--- Core/Render/Renderer.cs | 5 +++-- Core/Util/Profiling/ProfilerStopwatch.cs | 6 +++++- Core/Util/Profiling/Timers/RenderProfiler.cs | 13 +++++++++++++ 7 files changed, 49 insertions(+), 16 deletions(-) diff --git a/Client/Client.cs b/Client/Client.cs index c4c005efa..60bfff7f4 100755 --- a/Client/Client.cs +++ b/Client/Client.cs @@ -130,7 +130,7 @@ private Client(CommandLineArgs commandLineArgs, PathsManager pathsManager, IConf } GLFW.WindowHint(WindowHintString.WaylandAppID, "Helion"); - m_window = new Window(AppInfo.ApplicationName, config, archiveCollection, m_fpsTracker, this, GlVersion.Major, GlVersion.Minor, GlVersion.Flags, + m_window = new Window(AppInfo.ApplicationName, config, archiveCollection, m_fpsTracker, m_profiler, this, GlVersion.Major, GlVersion.Minor, GlVersion.Flags, () => CheckOpenGLSupport(!commandLineArgs.GlVersion.HasValue)); m_screenshotGenerator = new(m_window.Renderer); m_soundManager.SoundCreated += m_window.JoystickAdapter.RumbleForSoundCreated; diff --git a/Client/Window.cs b/Client/Window.cs index 39f9d75b1..be1fb9e25 100644 --- a/Client/Window.cs +++ b/Client/Window.cs @@ -10,6 +10,8 @@ using Helion.Strings; using Helion.Util.Configs; using Helion.Util.Configs.Components; +using Helion.Util.Profiling; +using Helion.Util.Profiling.Timers; using Helion.Util.Timing; using Helion.Window; using Helion.Window.Input; @@ -68,7 +70,7 @@ public Dimension ClientDimension private Vector2i? m_knownGoodWindowPos; private bool IsWindowsBorderlessFullscreen => m_isWindows && m_renderWindowState == RenderWindowState.BorderlessFullscreenWindow; - public Window(string title, IConfig config, ArchiveCollection archiveCollection, FpsTracker tracker, IInputManagement inputManagement, + public Window(string title, IConfig config, ArchiveCollection archiveCollection, FpsTracker tracker, Profiler profiler, IInputManagement inputManagement, int glMajor, int glMinor, GLContextFlags flags, Action onCreate) : base(MakeGameWindowSettings(), MakeNativeWindowSettings(config, title, glMajor, glMinor, flags)) { @@ -79,7 +81,7 @@ public Window(string title, IConfig config, ArchiveCollection archiveCollection, m_renderWindowState = config.Window.State; m_inputManagement = inputManagement; CursorState = config.Mouse.Focus ? CursorState.Grabbed : CursorState.Hidden; - Renderer = new(this, config, archiveCollection, tracker); + Renderer = new(this, config, archiveCollection, tracker, profiler.Render); KeyDown += Window_KeyDown; KeyUp += Window_KeyUp; diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs index 708503665..a46f4ad64 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs @@ -1,6 +1,5 @@ using Helion.Util; using Helion.Util.Configs.Components; -using Helion.Util.Loggers; using Helion.World; using System.Diagnostics; @@ -11,7 +10,7 @@ public partial class LegacyWorldRenderer private IBspHeuristics? m_bspHeuristics; private double m_smoothedBspTimeUs; private readonly SampleWindow m_bspTimeWindow = new(32); - private readonly SampleWindow m_fpsWindow = new(10); + private readonly SampleWindow m_worldGeometryWindow = new(10); private int m_aboveThresholdCount; private int m_belowThresholdCount; @@ -101,21 +100,25 @@ private void CheckAdaptiveSuggest(IWorld world) m_lastProcessedId = m_bspHeuristics.LastProcessedId; - var fpsValue = m_fpsWindow.AddSampleAndCalcMedian(m_fpsTracker.AverageFramesPerSecond); - if (!m_fpsWindow.IsInitialized) + // Swap buffers should take most of the time if the GPU is stressed. Include WorldGeometry generation time. + // Don't rely on the tracked FPS average because this includes things like entity AI and automap rendering that can throw this off. + var renderTimeMs = m_renderProfiler.SwapBuffers.LastFrameMilliseconds + m_renderProfiler.WorldGeometry.LastFrameMilliseconds; + var milliseconds = m_worldGeometryWindow.AddSampleAndCalcMedian(renderTimeMs); + if (!m_worldGeometryWindow.IsInitialized) return; - if (fpsValue > 60 || (m_config.Render.MaxFPS.Value != 0 && fpsValue > m_config.Render.MaxFPS.Value)) + if (milliseconds < 1000 / 60.0 || (m_config.Render.MaxFPS.Value != 0 && milliseconds < m_config.Render.MaxFPS.Value / 1000.0)) return; - if (m_bspHeuristics.Microseconds >= m_config.Render.Adaptive.TimeThreshold.Value * 0.6) + if (m_bspHeuristics.Microseconds >= m_config.Render.Adaptive.TimeThreshold.Value * 0.7) return; m_adaptiveSuggestionsHitCount++; if (m_adaptiveSuggestionsHitCount >= 3) { + var args = new DisplayMessageArgs("Low FPS detected. Considering switching to adaptive rendering mode. (render.mode 2)", null, null, ForAllPlayers: true); + world.DisplayMessage(args); m_loggedAdaptiveSuggestion = true; - HelionLog.Info("Low FPS detected. Considering switching to adaptive rendering mode. (render.mode 2)"); } } diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs index dd4c0ab93..234ce3955 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.cs @@ -16,6 +16,7 @@ using Helion.Util; using Helion.Util.Configs; using Helion.Util.Configs.Components; +using Helion.Util.Profiling.Timers; using Helion.Util.Timing; using Helion.World; using Helion.World.Entities; @@ -56,7 +57,7 @@ public partial class LegacyWorldRenderer : WorldRenderer private readonly Stopwatch m_stopwatch = new(); private readonly OitFrameBuffer m_oitFrameBuffer = new(); private readonly RenderInfo m_downSizedRenderInfo = new(); - private readonly FpsTracker m_fpsTracker; + private readonly RenderProfiler m_renderProfiler; private readonly bool m_vanillaRender; private Vec2D m_occludeViewPos; private bool m_occlude; @@ -74,10 +75,10 @@ public partial class LegacyWorldRenderer : WorldRenderer private PlaneClipFrameBuffer? m_planeClipFrameBuffer; private PlaneClipFrameBuffer? m_wallClipFrameBuffer; - public LegacyWorldRenderer(IConfig config, ArchiveCollection archiveCollection, LegacyGLTextureManager textureManager, FpsTracker fpsTracker) + public LegacyWorldRenderer(IConfig config, ArchiveCollection archiveCollection, LegacyGLTextureManager textureManager, RenderProfiler renderProfiler) { m_config = config; - m_fpsTracker = fpsTracker; + m_renderProfiler = renderProfiler; m_entityRenderer = new(config, textureManager, archiveCollection); m_primitiveRenderer = new(); m_worldDataManager = new(m_interpolationProgram); @@ -341,16 +342,19 @@ protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFra if (renderTickChange) { + m_renderProfiler.WorldTraversal.Start(); SetupRenderData(world, renderInfo); if (m_renderStatic) IterateBlockmap(world); else TraverseBsp(world, renderInfo); + m_renderProfiler.WorldTraversal.Stop(); } PopulatePrimitives(world); + m_renderProfiler.WorldGeometry.Start(); m_geometryRenderer.RenderSkies(renderInfo); RenderFloodFill(renderInfo); @@ -382,6 +386,7 @@ protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFra m_entityRenderer.RenderOpaque(renderInfo); m_primitiveRenderer.RenderAll(renderInfo); RenderTransparent(renderInfo, framebuffer); + m_renderProfiler.WorldGeometry.Stop(); return; } @@ -430,6 +435,7 @@ protected override void PerformRender(IWorld world, RenderInfo renderInfo, GLFra m_entityRenderer.RenderOpaque(renderInfo); RenderTransparent(renderInfo, framebuffer); + m_renderProfiler.WorldGeometry.Stop(); } private void RenderFloodFill(RenderInfo renderInfo) @@ -437,10 +443,12 @@ private void RenderFloodFill(RenderInfo renderInfo) // Doom would draw middle textures over flood fill. // Setting the factor using PolygonOffset will push them further away in depth so middle textures are closer and render over. // Very tiny for reversed z. Flood fill is pushed in world coordinates in the shader. + m_renderProfiler.WorldFloodFill.Start(); GL.Enable(EnableCap.PolygonOffsetFill); SetPolygonOffsetFloodFill(); m_geometryRenderer.RenderPortals(renderInfo); GL.Disable(EnableCap.PolygonOffsetFill); + m_renderProfiler.WorldFloodFill.Stop(); } private static void SetPolygonOffsetFloodFill() @@ -596,6 +604,7 @@ private void RenderTransparent(RenderInfo renderInfo, GLFramebuffer framebuffer) if (!hasEntityFuzzData && !hasEntityAlphaData && !hasDynamicAlphaGeometry && !hasStaticAlphaGeometry) return; + m_renderProfiler.WorldTransparent.Start(); SetPolygonOffsetFloodFill(); m_oitFrameBuffer.StartRender(); GL.DepthMask(false); @@ -694,6 +703,7 @@ private void RenderTransparent(RenderInfo renderInfo, GLFramebuffer framebuffer) m_entityRenderer.RenderOitFuzzRefractionPass(renderInfo, true); GL.DepthMask(true); + m_renderProfiler.WorldTransparent.Stop(); } private void RenderCompositeStyles(IStyleRenderer styleRenderer) diff --git a/Core/Render/Renderer.cs b/Core/Render/Renderer.cs index ca8f363d5..5dad134cf 100644 --- a/Core/Render/Renderer.cs +++ b/Core/Render/Renderer.cs @@ -23,6 +23,7 @@ using Helion.Util; using Helion.Util.Configs; using Helion.Util.Configs.Components; +using Helion.Util.Profiling.Timers; using Helion.Util.Timing; using Helion.Window; using Helion.World; @@ -82,7 +83,7 @@ public partial class Renderer : IDisposable public IImageDrawInfoProvider DrawInfo => Textures.ImageDrawInfoProvider; private bool UseVirtualResolution => (m_config.Window.Virtual.Enable && m_config.Window.Virtual.Dimension.Value.HasPositiveArea); - public Renderer(IWindow window, IConfig config, ArchiveCollection archiveCollection, FpsTracker fpsTracker) + public Renderer(IWindow window, IConfig config, ArchiveCollection archiveCollection, FpsTracker fpsTracker, RenderProfiler renderProfiler) { Window = window; m_config = config; @@ -93,7 +94,7 @@ public Renderer(IWindow window, IConfig config, ArchiveCollection archiveCollect SetShaderVars(); Textures = new LegacyGLTextureManager(config, archiveCollection); - m_worldRenderer = new LegacyWorldRenderer(config, archiveCollection, Textures, fpsTracker); + m_worldRenderer = new LegacyWorldRenderer(config, archiveCollection, Textures, renderProfiler); m_hudRenderer = new LegacyHudRenderer(config, Textures, archiveCollection.DataCache); m_automapRenderer = new LegacyAutomapRenderer(archiveCollection); m_transitionRenderer = new TransitionRenderer(window); diff --git a/Core/Util/Profiling/ProfilerStopwatch.cs b/Core/Util/Profiling/ProfilerStopwatch.cs index 364f023f7..6201024ab 100644 --- a/Core/Util/Profiling/ProfilerStopwatch.cs +++ b/Core/Util/Profiling/ProfilerStopwatch.cs @@ -12,6 +12,9 @@ public class ProfilerStopwatch: ProfileComponent public double FrameMilliseconds => m_stopwatch.ElapsedTicks * TicksToMs; public double TotalMilliseconds => m_totalTicks * TicksToMs; + + public double LastFrameMilliseconds { get; private set; } + public override List Profilers { get; } = []; public void Start() @@ -22,6 +25,7 @@ public void Start() public void Stop() { m_stopwatch.Stop(); + LastFrameMilliseconds = FrameMilliseconds; m_totalTicks += m_stopwatch.ElapsedTicks; } @@ -30,5 +34,5 @@ internal void Reset() m_stopwatch.Reset(); } - public override string ToString() => $"Frame = {FrameMilliseconds:0.######} ms, Total = {TotalMilliseconds:0.####} ms"; + public override string ToString() => $"Frame = {FrameMilliseconds:0.######} ms, LastFrame = {LastFrameMilliseconds:0.######} Total = {TotalMilliseconds:0.####} ms"; } diff --git a/Core/Util/Profiling/Timers/RenderProfiler.cs b/Core/Util/Profiling/Timers/RenderProfiler.cs index 710bd2f6f..07228a753 100644 --- a/Core/Util/Profiling/Timers/RenderProfiler.cs +++ b/Core/Util/Profiling/Timers/RenderProfiler.cs @@ -10,6 +10,11 @@ public class RenderProfiler: ProfileComponent public readonly ProfilerStopwatch SwapBuffers = new(); public readonly ProfilerStopwatch Total = new(); public readonly ProfilerStopwatch World = new(); + public readonly ProfilerStopwatch WorldGeometry = new(); + // BSP / Blockmap traversal + public readonly ProfilerStopwatch WorldTraversal = new(); + public readonly ProfilerStopwatch WorldTransparent = new(); + public readonly ProfilerStopwatch WorldFloodFill = new(); public readonly ProfilerStopwatch Automap = new(); public override List Profilers { get; } = []; @@ -22,6 +27,10 @@ public RenderProfiler() Profilers.Add(new(this, "Render.SwapBuffers", SwapBuffers)); Profilers.Add(new(this, "Render.Total", Total)); Profilers.Add(new(this, "Render.World", World)); + Profilers.Add(new(this, "Render.WorldGeometry", WorldGeometry)); + Profilers.Add(new(this, "Render.WorldTraversal", WorldTraversal)); + Profilers.Add(new(this, "Render.WorldTransparent", WorldTransparent)); + Profilers.Add(new(this, "Render.WorldFloodFill", WorldFloodFill)); Profilers.Add(new(this, "Render.Automap", Automap)); } @@ -33,6 +42,10 @@ internal void ResetAll() SwapBuffers.Reset(); Total.Reset(); World.Reset(); + WorldGeometry.Reset(); + WorldTraversal.Reset(); + WorldTransparent.Reset(); + WorldFloodFill.Reset(); Automap.Reset(); } } From 6f44a7abb29d2fc63cae301f7cd91ec991724ce8 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 7 Sep 2026 09:33:00 -0400 Subject: [PATCH 13/26] add counters to side/plane to check if cached data is invalidated. move checks to individual upper/middle/lower functions since parts to fix broken cache invalidation --- .../Legacy/World/Geometry/GeometryRenderer.cs | 101 +++++++++--------- Core/World/Geometry/Sectors/SectorPlane.cs | 3 + Core/World/Geometry/Sides/Side.cs | 8 ++ 3 files changed, 63 insertions(+), 49 deletions(-) diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs index 6f0f2f310..670c2849c 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs @@ -72,10 +72,6 @@ public partial class GeometryRenderer : IDisposable private IWorld m_world; private TransferHeightView m_transferHeightsView = TransferHeightView.Middle; private TransferHeightView m_prevTransferHeightsView = TransferHeightView.Middle; - private BitArray m_vertexLookupInvalidated = new(0); - private BitArray m_vertexAlphaLookupInvalidated = new(0); - private BitArray m_floorVertexLookupInvalidated = new(0); - private BitArray m_ceilingVertexLookupInvalidated = new(0); private DynamicVertex[]?[] m_vertexLookup = []; private DynamicVertex[]?[] m_vertexLowerLookup = []; private DynamicVertex[]?[] m_vertexUpperLookup = []; @@ -92,6 +88,8 @@ public partial class GeometryRenderer : IDisposable private readonly Side m_fogSide; private readonly Wall m_fogWall = new(0, WallLocation.Middle); + private int m_invalidatedCounter = 1; + private readonly Func m_renderOneSidedSliceFunc; private readonly Func m_renderTwoSidedLowerSliceFunc; private readonly Func m_renderTwoSidedUpperSliceFunc; @@ -193,10 +191,7 @@ public void UpdateTo(IWorld world, bool unitTest = false) m_vertexPlaneLookup3D.Clear(); - m_vertexLookupInvalidated = new(sideCount); - m_vertexAlphaLookupInvalidated = new(sideCount); - m_floorVertexLookupInvalidated = new(sectorCount); - m_ceilingVertexLookupInvalidated = new(sectorCount); + m_invalidatedCounter = 1; if (!world.SameAsPreviousMap) { @@ -548,19 +543,19 @@ private void RenderSectorFlats(Sector sectorForSubsectors, Sector renderSector, { if ((sector3D.RenderPlanes & SectorPlanes.Ceiling) != 0) { - success |= RenderFlat(subsectors, sector3D.ControlTop, sector3D.FakeTop, floor: true, renderFlood: false, checkViewPos: false, m_ceilingVertexLookupInvalidated, out _, out _, + success |= RenderFlat(subsectors, sector3D.ControlTop, sector3D.FakeTop, floor: true, renderFlood: false, checkViewPos: false, out _, out _, lightLevelSector: sector3D.LightTop, allowAlpha: true, alpha: sector3D.Alpha, style: sector3D.RenderDataStyle); if (sector3D.FakeTopFlipped != null) { - success |= RenderFlat(subsectors, sector3D.ControlTop, sector3D.FakeTopFlipped, floor: false, renderFlood: false, checkViewPos: false, m_ceilingVertexLookupInvalidated, out _, out _, + success |= RenderFlat(subsectors, sector3D.ControlTop, sector3D.FakeTopFlipped, floor: false, renderFlood: false, checkViewPos: false, out _, out _, lightLevelSector: sector3D.LightTop, allowAlpha: true, alpha: sector3D.Alpha, style: sector3D.RenderDataStyle); } } } else { - success = RenderFlat(subsectors, renderSector.Floor, subsectors[0].Sector.Floor, floor: true, renderFlood: false, checkViewPos: true, m_floorVertexLookupInvalidated, out _, out _); + success = RenderFlat(subsectors, renderSector.Floor, subsectors[0].Sector.Floor, floor: true, renderFlood: false, checkViewPos: true, out _, out _); } if (success) @@ -577,19 +572,19 @@ private void RenderSectorFlats(Sector sectorForSubsectors, Sector renderSector, { if ((sector3D.RenderPlanes & SectorPlanes.Floor) != 0) { - success |= RenderFlat(subsectors, sector3D.ControlBottom, sector3D.FakeBottom, floor: false, renderFlood: false, checkViewPos: false, m_ceilingVertexLookupInvalidated, out _, out _, + success |= RenderFlat(subsectors, sector3D.ControlBottom, sector3D.FakeBottom, floor: false, renderFlood: false, checkViewPos: false, out _, out _, lightLevelSector: sector3D.LightBottom, allowAlpha: true, alpha: sector3D.Alpha, style: sector3D.RenderDataStyle); if (sector3D.FakeBottomFlipped != null) { - success |= RenderFlat(subsectors, sector3D.ControlBottom, sector3D.FakeBottomFlipped, floor: true, renderFlood: false, checkViewPos: false, m_ceilingVertexLookupInvalidated, out _, out _, + success |= RenderFlat(subsectors, sector3D.ControlBottom, sector3D.FakeBottomFlipped, floor: true, renderFlood: false, checkViewPos: false, out _, out _, lightLevelSector: sector3D.LightBottom, allowAlpha: true, alpha: sector3D.Alpha, style: sector3D.RenderDataStyle); } } } else { - success = RenderFlat(subsectors, renderSector.Ceiling, subsectors[0].Sector.Ceiling, floor: false, renderFlood: false, checkViewPos: true, m_ceilingVertexLookupInvalidated, out _, out _); + success = RenderFlat(subsectors, renderSector.Ceiling, subsectors[0].Sector.Ceiling, floor: false, renderFlood: false, checkViewPos: true, out _, out _); } if (success) @@ -764,12 +759,7 @@ public void RenderAlphaSide(Side side, bool isFrontSide) var otherSide = side.PartnerSide!; m_sectorChangedLine = otherSide.Sector.CheckRenderingChanged(side.LastRenderGametickAlpha) || side.Sector.CheckRenderingChanged(side.LastRenderGametickAlpha); - var invalidated = m_vertexAlphaLookupInvalidated[side.Id]; - if (invalidated) - { - m_vertexAlphaLookupInvalidated.Set(side.Id, false); - m_sectorChangedLine = true; - } + CheckInvalidatedSideCounter(ref side.AlphaInvalidatedCount, m_invalidatedCounter); var facingSector = side.Sector.GetRenderSector(m_transferHeightsView); var otherSector = otherSide.Sector.GetRenderSector(m_transferHeightsView); @@ -823,12 +813,7 @@ public void RenderOneSided(Side side, bool isFront, out DynamicVertex[]? vertice side.LastRenderGametick = m_world.Gametick; - bool invalidated = m_vertexLookupInvalidated[side.Id]; - if (invalidated) - { - m_vertexLookupInvalidated.Set(side.Id, false); - m_sectorChangedLine = true; - } + CheckInvalidatedSideCounter(ref side.MiddleInvalidatedCount, m_invalidatedCounter); WallVertices wall = default; texture = m_glTextureManager?.GetTexture(side.Middle.TextureHandle) ?? TestTexture; @@ -951,6 +936,11 @@ public void SetRenderCeiling(SectorPlane ceiling) private void RenderTwoSided(Side facingSide, bool isFrontSide) { + if (facingSide.Line.Id == 29033) + { + int lol = 1; + } + var otherSide = facingSide.PartnerSide!; var facingSector = facingSide.Sector.GetRenderSector(m_transferHeightsView); var otherSector = otherSide.Sector.GetRenderSector(m_transferHeightsView); @@ -961,12 +951,12 @@ private void RenderTwoSided(Side facingSide, bool isFrontSide) if (!m_renderCoverOnly) facingSide.LastRenderGametick = m_world.Gametick; - bool invalidated = m_vertexLookupInvalidated[facingSide.Id]; - if (invalidated) - { - m_vertexLookupInvalidated.Set(facingSide.Id, false); - m_sectorChangedLine = true; - } + //bool invalidated = m_vertexLookupInvalidated[facingSide.Id]; + //if (invalidated) + //{ + // m_vertexLookupInvalidated.Set(facingSide.Id, false); + // m_sectorChangedLine = true; + //} var visibility = GetSideVisibility(facingSide, otherSide, facingSector, otherSector); var renderSlices3D = WorldStatic.Sector3D && facingSide.Sector.Sectors3D.Length > 0; @@ -1188,6 +1178,8 @@ public void RenderTwoSidedLower(Side facingSide, Side otherSide, Sector facingSe if (lowerWall.TextureHandle <= Constants.NullCompatibilityTextureIndex && !skyRender) return; + CheckInvalidatedSideCounter(ref facingSide.LowerInvalidatedCount, m_invalidatedCounter); + GLLegacyTexture texture = m_glTextureManager.GetTexture(lowerWall.TextureHandle); GLLegacyTexture? brightmapTexture = m_glTextureManager.GetBrightmapTexture(lowerWall.TextureHandle); @@ -1286,6 +1278,8 @@ public void RenderTwoSidedUpper(Side facingSide, Side otherSide, Sector facingSe if (!TextureManager.IsSkyTexture(facingSector.Ceiling.TextureHandle) && upperWall.TextureHandle == Constants.NoTextureIndex) return; + CheckInvalidatedSideCounter(ref facingSide.UpperInvalidatedCount, m_invalidatedCounter); + WallVertices wall = default; GLLegacyTexture texture = m_glTextureManager.GetTexture(upperWall.TextureHandle); GLLegacyTexture? brightmapTexture = m_glTextureManager.GetBrightmapTexture(upperWall.TextureHandle); @@ -1496,6 +1490,8 @@ public void RenderTwoSidedMiddle(Side facingSide, Side otherSide, Sector facingS var alpha = m_config.Render.TextureTransparency ? Math.Clamp(line.Alpha, 0, 1) : 1.0f; var data = GetCachedSide(m_vertexLookup, facingSide); + CheckInvalidatedSideCounter(ref facingSide.MiddleInvalidatedCount, m_invalidatedCounter); + if (facingSide.OffsetChanged || m_sectorChangedLine || data == null) { lightLevelSector ??= facingSector; @@ -1682,12 +1678,7 @@ public void SetRenderMode(GeometryRenderMode renderMode, TransferHeightView view m_transferHeightsView = view; if (m_prevTransferHeightsView != m_transferHeightsView) - { - m_vertexLookupInvalidated.SetAll(true); - m_vertexAlphaLookupInvalidated.SetAll(true); - m_floorVertexLookupInvalidated.SetAll(true); - m_ceilingVertexLookupInvalidated.SetAll(true); - } + m_invalidatedCounter++; var clearFloodVertices = !m_config.Developer.LockRender; if (clearFloodVertices && !newTick) @@ -1723,8 +1714,7 @@ public void RenderSectorFlats(Sector renderSector, SectorPlane renderPlane, Sect } var subsectors = m_subsectors[renderSector.Id]; - var invalidatedLookup = floor ? m_floorVertexLookupInvalidated : m_ceilingVertexLookupInvalidated; - RenderFlat(subsectors, renderPlane, geometryPlane, floor, renderFlood, checkViewPos: false, invalidatedLookup, out vertices, out skyVertices, + RenderFlat(subsectors, renderPlane, geometryPlane, floor, renderFlood, checkViewPos: false, out vertices, out skyVertices, lightLevelSector, allowAlpha, alpha, style: style); } @@ -1733,7 +1723,7 @@ public int GetFlatTextureHandle(int textureHandle, bool allowAlpha) => !allowAlpha && textureHandle == Constants.NoTextureIndex ? TextureManager.BlackTextureIndex : textureHandle; private bool RenderFlat(DynamicArray subsectors, SectorPlane renderPlane, SectorPlane geometryPlane, bool floor, bool renderFlood, bool checkViewPos, - BitArray flatInvalidatedVertexLookup, out DynamicVertex[]? vertices, out SkyGeometryVertex[]? skyVertices, + out DynamicVertex[]? vertices, out SkyGeometryVertex[]? skyVertices, Sector? lightLevelSector = null, bool allowAlpha = false, float alpha = 1, RenderDataStyle style = RenderDataStyle.Normal) { var textureHandle = GetFlatTextureHandle(renderPlane.TextureHandle, allowAlpha); @@ -1752,20 +1742,13 @@ private bool RenderFlat(DynamicArray subsectors, SectorPlane renderPl var brightmapTexture = m_glTextureManager.GetBrightmapTexture(textureHandle); var geometryType = GetGeometryType(style, GeometryType.Flat); - var flatChanged = FlatChanged(renderPlane); + var flatChanged = FlatChanged(renderPlane) | CheckInvalidatedFlatCounter(ref geometryPlane.InvalidatedCount, m_invalidatedCounter); var sector = subsectors[0].Sector; int id = geometryPlane.Sector.Id; var renderSector = sector.GetRenderSector(m_transferHeightsView); lightLevelSector ??= renderSector; var textureVector = new Vec2F(texture.Dimension.Vector.X, texture.Dimension.Vector.Y); - var invalidated = flatInvalidatedVertexLookup[id]; - if (invalidated) - { - flatInvalidatedVertexLookup.Set(id, false); - flatChanged = true; - } - int indexStart = 0; if (isSky) { @@ -2338,4 +2321,24 @@ private void ReleaseUnmanagedResources() m_skyRenderer?.Dispose(); Portals?.Dispose(); } + + private void CheckInvalidatedSideCounter(ref int sideCounter, int geometryCounter) + { + if (sideCounter != geometryCounter) + { + sideCounter = geometryCounter; + m_sectorChangedLine = true; + } + } + + private static bool CheckInvalidatedFlatCounter(ref int flatCounter, int geometryCounter) + { + if (flatCounter != geometryCounter) + { + flatCounter = geometryCounter; + return true; + } + + return false; + } } diff --git a/Core/World/Geometry/Sectors/SectorPlane.cs b/Core/World/Geometry/Sectors/SectorPlane.cs index b002c5c34..188edb7d4 100644 --- a/Core/World/Geometry/Sectors/SectorPlane.cs +++ b/Core/World/Geometry/Sectors/SectorPlane.cs @@ -33,6 +33,8 @@ public sealed class SectorPlane : SectorSoundSource public TransferHeights? TransferHeights; public override Sector SoundSector => Sector; + public int InvalidatedCount; + private readonly double m_initialZ; private readonly int m_initialTextureHandle; private readonly RenderOffsets m_initialRenderOffsets; @@ -72,6 +74,7 @@ public void Reset(short lightLevel) MidTextureHack = default; NoRender = default; SkyGeometry = default; + InvalidatedCount = default; RenderOffsets = m_initialRenderOffsets; } diff --git a/Core/World/Geometry/Sides/Side.cs b/Core/World/Geometry/Sides/Side.cs index eef07971c..c4ddb7e98 100644 --- a/Core/World/Geometry/Sides/Side.cs +++ b/Core/World/Geometry/Sides/Side.cs @@ -68,6 +68,10 @@ public sealed class Side public SideFlags Flags; public float Alpha = 1f; public RenderDataStyle RenderDataStyle; + public int UpperInvalidatedCount; + public int MiddleInvalidatedCount; + public int LowerInvalidatedCount; + public int AlphaInvalidatedCount; public MapUserProperties UserProperties; public Sector? LightSector3D; @@ -117,6 +121,10 @@ public void Reset() MidTextureFlood = default; Flags.BlockmapLinked = default; Flags.UpperSky = default; + UpperInvalidatedCount = default; + MiddleInvalidatedCount = default; + LowerInvalidatedCount = default; + AlphaInvalidatedCount = default; Upper.Reset(); Middle.Reset(); From 3e5024aa180276fb2eb1851a9fea1609da5d69b0 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 7 Sep 2026 10:05:23 -0400 Subject: [PATCH 14/26] update release notes --- RELEASENOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a5f385d2b..1b32c1be2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -28,6 +28,7 @@ - Fix status bar weapon slot condition to correctly check against switched weapon instead of the weapon that's actively being switched to. - Fix status bar uses ammo condition. - Fix BufferSubData call that could write out of bounds on GPU resulting in corrupted/missing walls. +- Fix rendering issue with lines when changing transfer heights views. ## Misc: - Use DrawArraysInstanced instead of geometry shader for sprite rendering (allows for MacOS support). From 3a698668ca36c7d333321218e02aa6b8ff2b5a99 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 7 Sep 2026 10:07:26 -0400 Subject: [PATCH 15/26] remove commented code --- .../Legacy/World/Geometry/GeometryRenderer.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs index 670c2849c..e4d767f5b 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/Geometry/GeometryRenderer.cs @@ -936,11 +936,6 @@ public void SetRenderCeiling(SectorPlane ceiling) private void RenderTwoSided(Side facingSide, bool isFrontSide) { - if (facingSide.Line.Id == 29033) - { - int lol = 1; - } - var otherSide = facingSide.PartnerSide!; var facingSector = facingSide.Sector.GetRenderSector(m_transferHeightsView); var otherSector = otherSide.Sector.GetRenderSector(m_transferHeightsView); @@ -951,13 +946,6 @@ private void RenderTwoSided(Side facingSide, bool isFrontSide) if (!m_renderCoverOnly) facingSide.LastRenderGametick = m_world.Gametick; - //bool invalidated = m_vertexLookupInvalidated[facingSide.Id]; - //if (invalidated) - //{ - // m_vertexLookupInvalidated.Set(facingSide.Id, false); - // m_sectorChangedLine = true; - //} - var visibility = GetSideVisibility(facingSide, otherSide, facingSector, otherSector); var renderSlices3D = WorldStatic.Sector3D && facingSide.Sector.Sectors3D.Length > 0; From fe4e12f2e972f91a4017e229079c033880ba0b20 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 8 Sep 2026 06:49:51 -0400 Subject: [PATCH 16/26] make hysteresis configurable --- .../Legacy/World/LegacyWorldRenderer.BspHeuristics.cs | 6 +++--- Core/Util/Configs/Components/ConfigRender.cs | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs index a46f4ad64..f3673d3c7 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs @@ -70,9 +70,9 @@ private bool UseBspBasedOnHeuristic(IWorld world) // Don't let the BSP heuristics flip-flop too quickly. If the smoothed time is within 10% of the threshold, don't switch. if (shouldUseBsp != m_bspHeuristics.Info.UseBsp) { - const double PercentRange = 0.1; - var highRange = threshold * (1 + PercentRange); - var lowRange = threshold * (1 - PercentRange); + var percentRange = m_config.Render.Adaptive.HysteresisPercent.Value; + var highRange = threshold * (1 + percentRange); + var lowRange = threshold * (1 - percentRange); if (m_smoothedBspTimeUs >= lowRange && m_smoothedBspTimeUs <= highRange) shouldUseBsp = m_bspHeuristics.Info.UseBsp; } diff --git a/Core/Util/Configs/Components/ConfigRender.cs b/Core/Util/Configs/Components/ConfigRender.cs index 5ed8098fa..4f709b151 100644 --- a/Core/Util/Configs/Components/ConfigRender.cs +++ b/Core/Util/Configs/Components/ConfigRender.cs @@ -238,6 +238,9 @@ public class ConfigRenderAdaptive : ConfigElement [ConfigInfo("The number microseconds until the mode is switched to static when using adapative.")] public readonly ConfigValue TimeThreshold = new(2200, GreaterOrEqual(1)); + [ConfigInfo("Percentage band for time threshold to switch modes.")] + public readonly ConfigValue HysteresisPercent = new(0.05, Clamp(0.01, 0.5)); + [ConfigInfo("The number segs until the mode is switched to static when using adapative.")] public readonly ConfigValue SegThreshold = new(8000, GreaterOrEqual(1)); From b1c6e1fa1d249ae0c4d96f3ca3239de494e5a539 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 11 Sep 2026 11:36:53 -0400 Subject: [PATCH 17/26] add render mode to menu --- Core/Util/Configs/Components/ConfigRender.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Core/Util/Configs/Components/ConfigRender.cs b/Core/Util/Configs/Components/ConfigRender.cs index 4f709b151..83ec023ab 100644 --- a/Core/Util/Configs/Components/ConfigRender.cs +++ b/Core/Util/Configs/Components/ConfigRender.cs @@ -85,10 +85,15 @@ public class ConfigRenderHealthBar : ConfigElement public class ConfigRender : ConfigElement { + [ConfigInfo("Changes the render mode. Adaptive can be very beneficial on itegrated GPUs.")] + [OptionMenu(OptionSectionType.Render, "Mode")] + public readonly ConfigValue Mode = new(AdaptiveRenderMode.Static); + public readonly ConfigRenderAdaptive Adaptive = new(); + // VSync and rate limiting [ConfigInfo("Vertical synchronization. Prevents tearing, but affects input processing (unless you have G-Sync).")] - [OptionMenu(OptionSectionType.Render, "VSync")] + [OptionMenu(OptionSectionType.Render, "VSync", spacer: true)] public readonly ConfigValue VSync = new(RenderVsyncMode.On); [ConfigInfo("Maximum frames per second. Zero is equivalent to no cap if vsync is off (or monitor refresh rate if vsync is on/adaptive).")] @@ -227,10 +232,6 @@ public class ConfigRender : ConfigElement // This option is a hacked test that writes everything directly to the default backbuffer. Relies on undefined behavior since certain rendering functions need the depth texture. [ConfigInfo("Disables post processing effects like spectre fuzz refraction and skips FBO. Can have rendering defects.", restartRequired: true)] public readonly ConfigValue PostProcessingEffects = new(true); - - [ConfigInfo("Changes the render mode.")] - public readonly ConfigValue Mode = new(AdaptiveRenderMode.Static); - public readonly ConfigRenderAdaptive Adaptive = new(); } public class ConfigRenderAdaptive : ConfigElement From b819e4632facdf2582f7cd51616962dfbd20c8f5 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 11 Sep 2026 11:55:41 -0400 Subject: [PATCH 18/26] remove priority testing --- Client/Client.cs | 3 --- Core/World/Impl/SinglePlayer/AutomapMarker.cs | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Client/Client.cs b/Client/Client.cs index 60bfff7f4..3c62599e4 100755 --- a/Client/Client.cs +++ b/Client/Client.cs @@ -93,9 +93,6 @@ record struct VersionTest(int Major, int Minor); private Client(CommandLineArgs commandLineArgs, PathsManager pathsManager, IConfig config, HelionConsole console, IAudioSystem audioSystem, ArchiveCollection archiveCollection) { - Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.AboveNormal; - Thread.CurrentThread.Priority = ThreadPriority.AboveNormal; - m_commandLineArgs = commandLineArgs; m_pathsManager = pathsManager; m_config = config; diff --git a/Core/World/Impl/SinglePlayer/AutomapMarker.cs b/Core/World/Impl/SinglePlayer/AutomapMarker.cs index b890009a6..272101bd9 100644 --- a/Core/World/Impl/SinglePlayer/AutomapMarker.cs +++ b/Core/World/Impl/SinglePlayer/AutomapMarker.cs @@ -71,8 +71,7 @@ public void Start(IWorld world) m_thread = new Thread(() => AutomapTask(m_cancelTasks.Token)) { - IsBackground = true, - Priority = ThreadPriority.Normal + IsBackground = true }; m_thread.Start(); } From a5b88ee52ffed1fe2e6159f110495d97daf9658e Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 11 Sep 2026 11:57:03 -0400 Subject: [PATCH 19/26] update release notes --- RELEASENOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bdafe8dc9..0ef844862 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,7 @@ - Add Radsuit intensity. - Automatic blood color and fuzz blood options. - Initial support for MacOS ARM64 builds. +- Add adaptive rendering option. Automatically swaps between BSP and static rendering when beneficial. Largely beneficial for slower integrated GPUs. ## Bug Fixes: - Do not clear player velocity when slide movement fails. Matches vanilla doom behavior where players can move out of lines with enough momentum to pass clip checks. (Fixes Hellevator MAP06 start) From 931ea213dc88d775dde0408ec331fad0fcc7a22e Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 11 Sep 2026 12:04:44 -0400 Subject: [PATCH 20/26] remove unused set --- Core/World/Impl/SinglePlayer/AutomapMarker.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Core/World/Impl/SinglePlayer/AutomapMarker.cs b/Core/World/Impl/SinglePlayer/AutomapMarker.cs index 272101bd9..dffa9fa54 100644 --- a/Core/World/Impl/SinglePlayer/AutomapMarker.cs +++ b/Core/World/Impl/SinglePlayer/AutomapMarker.cs @@ -31,7 +31,6 @@ public class AutomapMarker(IConfig config) : IBspHeuristics private readonly RenderInfo m_renderInfo = new(); private readonly OldCamera m_camera = new(default, default, 0, 0); private readonly Entity m_dummyEntity = new(); - private readonly HashSet m_visibleTextures = new(256); private Thread? m_thread; private CancellationTokenSource m_cancelTasks = new(); private IWorld m_world = null!; @@ -144,7 +143,6 @@ private void AutomapTask(CancellationToken token) m_viewClipper.Clear(); m_viewClipper.Center = pos.Position.XY; m_hitLines.SetAll(false); - m_visibleTextures.Clear(); SetFrustum(viewport, pos); MarkBspLineClips((uint)m_world.BspTree.Nodes.Length - 1, pos.Position.XY, m_world, token); From 9d35d04e3f0b2a506db058db5b828045c3a460ac Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 11 Sep 2026 12:07:26 -0400 Subject: [PATCH 21/26] clear index and init fields --- Core/Util/SampleWindow.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Core/Util/SampleWindow.cs b/Core/Util/SampleWindow.cs index 8b2d9fef7..d97faae54 100644 --- a/Core/Util/SampleWindow.cs +++ b/Core/Util/SampleWindow.cs @@ -17,7 +17,12 @@ public SampleWindow(int windowSize) public bool IsInitialized => m_init; - public void Clear() => Array.Clear(m_samples, 0, m_windowSize); + public void Clear() + { + m_index = 0; + m_init = false; + Array.Clear(m_samples, 0, m_windowSize); + } public ReadOnlySpan GetSampleWindow() => m_samples.AsSpan(0, m_windowSize); From ed9af2129c9ad887c7d1339cfbad7fdd37561bef Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 11 Sep 2026 12:07:54 -0400 Subject: [PATCH 22/26] remove using --- Core/World/Impl/SinglePlayer/AutomapMarker.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Core/World/Impl/SinglePlayer/AutomapMarker.cs b/Core/World/Impl/SinglePlayer/AutomapMarker.cs index dffa9fa54..c9f3d7578 100644 --- a/Core/World/Impl/SinglePlayer/AutomapMarker.cs +++ b/Core/World/Impl/SinglePlayer/AutomapMarker.cs @@ -17,7 +17,6 @@ using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; using System.Threading; From 9cb715ef609390e8fe46d83c1b1c79c3644c292c Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 11 Sep 2026 12:18:30 -0400 Subject: [PATCH 23/26] fix backwards divisor --- .../Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs index f3673d3c7..158d06d45 100644 --- a/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs +++ b/Core/Render/OpenGL/Renderers/Legacy/World/LegacyWorldRenderer.BspHeuristics.cs @@ -107,7 +107,7 @@ private void CheckAdaptiveSuggest(IWorld world) if (!m_worldGeometryWindow.IsInitialized) return; - if (milliseconds < 1000 / 60.0 || (m_config.Render.MaxFPS.Value != 0 && milliseconds < m_config.Render.MaxFPS.Value / 1000.0)) + if (milliseconds < 1000 / 60.0 || (m_config.Render.MaxFPS.Value != 0 && milliseconds < 1000.0 / m_config.Render.MaxFPS.Value)) return; if (m_bspHeuristics.Microseconds >= m_config.Render.Adaptive.TimeThreshold.Value * 0.7) From 3fa951063b5d0c876a55910073fc2486a9069e99 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 11 Sep 2026 12:18:38 -0400 Subject: [PATCH 24/26] add descriptions --- Core/Util/Configs/Components/ConfigRender.cs | 3 +++ RELEASENOTES.md | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Core/Util/Configs/Components/ConfigRender.cs b/Core/Util/Configs/Components/ConfigRender.cs index 83ec023ab..c46f6f130 100644 --- a/Core/Util/Configs/Components/ConfigRender.cs +++ b/Core/Util/Configs/Components/ConfigRender.cs @@ -51,8 +51,11 @@ public enum RenderContrastMode public enum AdaptiveRenderMode { + [Description("Static")] Static, + [Description("BSP")] Bsp, + [Description("Adaptive")] Adaptive } diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f2eb4624a..19bcbb6c1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,7 +7,6 @@ - Initial support for MacOS ARM64 builds. - Add weapon fire bob option. - ## Bug Fixes: - Do not clear player velocity when slide movement fails. Matches vanilla doom behavior where players can move out of lines with enough momentum to pass clip checks. (Fixes Hellevator MAP06 start) - Fix paths where checkered null texture would be used for brightmaps on sprites with null texture option. From 7e909c5d63b58e8e6b2d59a37351e4a25d668d26 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 11 Sep 2026 12:27:03 -0400 Subject: [PATCH 25/26] fix to use mark lines setting --- Core/World/Impl/SinglePlayer/AutomapMarker.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Core/World/Impl/SinglePlayer/AutomapMarker.cs b/Core/World/Impl/SinglePlayer/AutomapMarker.cs index c9f3d7578..8283568e9 100644 --- a/Core/World/Impl/SinglePlayer/AutomapMarker.cs +++ b/Core/World/Impl/SinglePlayer/AutomapMarker.cs @@ -259,8 +259,11 @@ private unsafe void MarkBspLineClips(uint nodeIndex, in Vec2D position, IWorld w if ((line.Flags & StructLineFlags.SeenForAutomap) != 0) continue; - line.Flags |= StructLineFlags.SeenForAutomap; - line.Line.DataChanges |= LineDataTypes.Automap; + if (m_markLines) + { + line.Flags |= StructLineFlags.SeenForAutomap; + line.Line.DataChanges |= LineDataTypes.Automap; + } } } From 5ed54e65fefea534f84ef5efaef0b32bd857960c Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 11 Sep 2026 12:35:33 -0400 Subject: [PATCH 26/26] update tests --- Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs | 13 +++++++++++-- Tests/Unit/GameAction/AutomapMark.cs | 2 +- Tests/Unit/GameAction/WorldAllocator.cs | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs b/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs index feb4b1956..253e21686 100644 --- a/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs +++ b/Core/World/Impl/SinglePlayer/SinglePlayerWorld.cs @@ -41,6 +41,7 @@ public class SinglePlayerWorld : WorldBase private WorldType m_worldType = WorldType.SinglePlayer; private int m_renderDistanceOverride; private bool m_firstUpdate = true; + private bool m_disableAutomapMarker; public override WorldType WorldType => m_worldType; public override Player Player { get; protected set; } @@ -158,6 +159,11 @@ public SinglePlayerWorld(GlobalData globalData, IConfig config, ArchiveCollectio } } + public void DisableAutomapMarker() + { + m_disableAutomapMarker = true; + } + private void CheckDistanceOverride() { if (CompatibilityMapDefinition != null && CompatibilityMapDefinition.MaxDistanceOverride > 0) @@ -216,7 +222,9 @@ public override void Tick() { var player = m_chaseCamMode ? ChaseCamPlayer : Player; var camera = player.GetCamera(0); - m_automapMarker.AddPosition(camera.PositionInterpolated.Double, camera.Direction.Double, player.AngleRadians, player.PitchRadians, GameTicker, !m_chaseCamMode); + + if (!m_disableAutomapMarker) + m_automapMarker.AddPosition(camera.PositionInterpolated.Double, camera.Direction.Double, player.AngleRadians, player.PitchRadians, GameTicker, !m_chaseCamMode); if (GetCrosshairTarget(out Entity? entity)) Player.SetCrosshairTarget(entity); @@ -330,7 +338,8 @@ public override void Start(WorldModel? worldModel) if (!PlayLevelMusic(musicName)) AudioSystem.Music.Stop(); - m_automapMarker.Start(this); + if (!m_disableAutomapMarker) + m_automapMarker.Start(this); } public override bool PlayLevelMusic(string name, MusicFlags flags = MusicFlags.Loop, Entity? activator = null) diff --git a/Tests/Unit/GameAction/AutomapMark.cs b/Tests/Unit/GameAction/AutomapMark.cs index 068033732..b4e39560e 100644 --- a/Tests/Unit/GameAction/AutomapMark.cs +++ b/Tests/Unit/GameAction/AutomapMark.cs @@ -108,7 +108,7 @@ void Marker_PositionProcessed(object? sender, PlayerPosition e) private AutomapMarker CreateAutomapMarker() { - var marker = new AutomapMarker(); + var marker = new AutomapMarker(World.Config); marker.Start(World); return marker; } diff --git a/Tests/Unit/GameAction/WorldAllocator.cs b/Tests/Unit/GameAction/WorldAllocator.cs index 15df35b2a..40c338edc 100644 --- a/Tests/Unit/GameAction/WorldAllocator.cs +++ b/Tests/Unit/GameAction/WorldAllocator.cs @@ -109,6 +109,7 @@ public static SinglePlayerWorld LoadMap(string resourceZip, string fileName, str skillDef, outputMap, existingPlayer, worldModel, random, unitTest: true, sameAsPreviousMap: sameAsPreviousMap) ?? throw new Exception("Failed to create world"); StaticWorld = world; world.OnTick += World_OnTick; + world.DisableAutomapMarker(); world.Start(worldModel); world.OnDestroying += World_OnDestroying; onInit(world); @@ -138,7 +139,6 @@ private static void World_OnTick(object? sender, EventArgs e) public static Config CreateConfig() { Config config = new(); - config.Render.AutomapBspThread.Set(false); return config; }