Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
23da1ea
working
nstlaurent Aug 24, 2026
7a900a2
start config cleanup
nstlaurent Aug 25, 2026
3c105e0
update processing for adaptive and remove automap bsp config
nstlaurent Aug 26, 2026
6fb841a
add time config and use seg threshold
nstlaurent Aug 27, 2026
983193f
Merge branch 'dev' into adaptive-rendering
nstlaurent Aug 30, 2026
0cf3fe0
use window of size 3 for timing heuristic
nstlaurent Aug 31, 2026
5761927
allow to run without actually marking lines
nstlaurent Aug 31, 2026
ab0ce76
Merge branch 'dev' into adaptive-rendering
nstlaurent Sep 1, 2026
91cc172
Merge branch 'dev' into adaptive-rendering
nstlaurent Sep 2, 2026
2393268
use above/below threshold counts to switch
nstlaurent Sep 3, 2026
90bf319
add counts for debugging
nstlaurent Sep 3, 2026
8e54330
fix flip flopping
nstlaurent Sep 4, 2026
ce68021
Merge branch 'dev' into adaptive-rendering
nstlaurent Sep 4, 2026
07756bd
add suggestion
nstlaurent Sep 4, 2026
3174e18
Merge branch 'dev' into adaptive-rendering
nstlaurent Sep 4, 2026
bfb7b7a
move adaptive to it's own section
nstlaurent Sep 4, 2026
d52a8bf
use profilers for timing
nstlaurent Sep 5, 2026
6f44a7a
add counters to side/plane to check if cached data is invalidated. mo…
nstlaurent Sep 7, 2026
3e5024a
update release notes
nstlaurent Sep 7, 2026
3a69866
remove commented code
nstlaurent Sep 7, 2026
cbf4c86
Merge branch 'invalidated-side-fix' into adaptive-rendering
nstlaurent Sep 7, 2026
1f410a4
Merge branch 'dev' into adaptive-rendering
nstlaurent Sep 7, 2026
fe4e12f
make hysteresis configurable
nstlaurent Sep 8, 2026
0f98a99
Merge branch 'dev' into adaptive-rendering
nstlaurent Sep 8, 2026
b1c6e1f
add render mode to menu
nstlaurent Sep 11, 2026
b819e46
remove priority testing
nstlaurent Sep 11, 2026
a5b88ee
update release notes
nstlaurent Sep 11, 2026
ef8bbfd
Merge branch 'dev' into adaptive-rendering
nstlaurent Sep 11, 2026
931ea21
remove unused set
nstlaurent Sep 11, 2026
9d35d04
clear index and init fields
nstlaurent Sep 11, 2026
ed9af21
remove using
nstlaurent Sep 11, 2026
9cb715e
fix backwards divisor
nstlaurent Sep 11, 2026
3fa9510
add descriptions
nstlaurent Sep 11, 2026
7e909c5
fix to use mark lines setting
nstlaurent Sep 11, 2026
5ed54e6
update tests
nstlaurent Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Client/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -126,7 +127,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;
Expand Down
6 changes: 4 additions & 2 deletions Client/Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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))
{
Expand All @@ -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;
Expand Down
30 changes: 29 additions & 1 deletion Core/Layer/Worlds/WorldLayer.Render.Hud.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -125,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);
Expand Down Expand Up @@ -287,7 +291,6 @@ 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);
for (int i = 0; i < m_renderStats.Length; i++)
{
Expand Down Expand Up @@ -333,6 +336,31 @@ private void DrawStatInfo(IHudRenderContext hud, bool automapVisible, Vec2I star
topRightY = labelPos.Y;
}

private void DrawBspStats(IHudRenderContext hud)
{
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(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);
m_bspString.Append('/');
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);
}

private static SpanString AppendStatString(SpanString str, int current, int max)
{
str.Append(current);
Expand Down
1 change: 1 addition & 0 deletions Core/Layer/Worlds/WorldLayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -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())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1668,7 +1668,7 @@ public void SetRenderMode(GeometryRenderMode renderMode, TransferHeightView view
if (m_prevTransferHeightsView != m_transferHeightsView)
m_invalidatedCounter++;

var clearFloodVertices = !m_config.Developer.LockRender;
var clearFloodVertices = !m_config.Developer.Render.Lock.Value;
if (clearFloodVertices && !newTick)
clearFloodVertices = false;

Expand Down
23 changes: 23 additions & 0 deletions Core/Render/OpenGL/Renderers/Legacy/World/IBspHeuristics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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 int AboveThresholdCount { get; set; }
public int BelowThresholdCount { get; set; }
public int SegCount { 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 BspHeuristicInfo Info { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using Helion.Util;
using Helion.Util.Configs.Components;
using Helion.World;
using System.Diagnostics;

namespace Helion.Render.OpenGL.Renderers.Legacy.World;

public partial class LegacyWorldRenderer
{
private IBspHeuristics? m_bspHeuristics;
private double m_smoothedBspTimeUs;
private readonly SampleWindow m_bspTimeWindow = new(32);
private readonly SampleWindow m_worldGeometryWindow = 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(IWorld world)
{
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)
{
CheckAdaptiveSuggest(world);
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 || m_lastProcessedId == m_bspHeuristics.LastProcessedId)
return m_bspHeuristics.Info.UseBsp;

m_lastProcessedId = m_bspHeuristics.LastProcessedId;
m_smoothedBspTimeUs = AddBspTimeSample(m_bspHeuristics.Microseconds);

var threshold = m_config.Render.Adaptive.TimeThreshold.Value;
if (m_smoothedBspTimeUs < threshold)
{
m_belowThresholdCount++;
m_aboveThresholdCount = 0;
}
else
{
m_aboveThresholdCount++;
m_belowThresholdCount = 0;
}

int thresholdCount = m_config.Render.Adaptive.SwitchCount.Value;
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)
{
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;
}

// Don't let fast CPUs switch to BSP when it's likely not beneficial.
if (m_bspHeuristics.SegCount > m_config.Render.Adaptive.SegThreshold.Value)
shouldUseBsp = false;

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.SegCount = m_bspHeuristics.SegCount;
m_bspHeuristics.Info.UseBsp = shouldUseBsp;
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;

// 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 (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)
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;
}
}

private double AddBspTimeSample(double time)
{
m_bspTimeWindow.SetWindowSize(m_config.Render.Adaptive.TimeWindow.Value);
return m_bspTimeWindow.AddSampleAndCalcMedian(time);
}
}
Loading
Loading