Add dynamic render quality settings and scaling

Introduce a RenderQuality enum (Low/Medium/High) and allow users to switch quality at runtime via keyboard shortcuts. The fractal renderer now adapts its internal resolution, particle counts, and computational workload based on selected quality and real-time performance, aiming to maintain a target FPS. HUD and window title display current quality and FPS. Renderer buffers and limits are dynamically resized on window or quality changes for improved performance and flexibility.
This commit is contained in:
Michal Vrba
2026-02-13 20:28:57 +01:00
parent 6fd9e52bf0
commit 2926f739ee
3 changed files with 165 additions and 17 deletions
+126 -15
View File
@@ -11,12 +11,12 @@ public sealed class FractalRenderer : IDisposable
private readonly GraphicsDevice _graphicsDevice;
private readonly SpriteBatch _spriteBatch;
private readonly int _internalWidth;
private readonly int _internalHeight;
private readonly Texture2D _fractalTexture;
private readonly Color[] _pixelBuffer;
private readonly Color[] _postBuffer;
private readonly Color[] _previousFrame;
private int _internalWidth;
private int _internalHeight;
private Texture2D _fractalTexture;
private Color[] _pixelBuffer;
private Color[] _postBuffer;
private Color[] _previousFrame;
private readonly List<NoteParticle> _notes = [];
private readonly List<FireworkRocket> _fireworkRockets = [];
private readonly List<FireworkSpark> _fireworkSparks = [];
@@ -24,6 +24,10 @@ public sealed class FractalRenderer : IDisposable
private float _noteSpawnAccumulator;
private float _fireworkSpawnAccumulator;
private float _runtimePerformanceScale = 1f;
private RenderQuality _quality = RenderQuality.Medium;
private int _lastViewportWidth;
private int _lastViewportHeight;
private bool _disposed;
@@ -31,17 +35,109 @@ public sealed class FractalRenderer : IDisposable
{
_graphicsDevice = graphicsDevice ?? throw new ArgumentNullException(nameof(graphicsDevice));
_spriteBatch = new SpriteBatch(_graphicsDevice);
_internalWidth = internalWidth;
_internalHeight = internalHeight;
_internalWidth = Math.Max(32, internalWidth);
_internalHeight = Math.Max(18, internalHeight);
_fractalTexture = new Texture2D(_graphicsDevice, _internalWidth, _internalHeight, false, SurfaceFormat.Color);
_pixelBuffer = new Color[_internalWidth * _internalHeight];
_postBuffer = new Color[_internalWidth * _internalHeight];
_previousFrame = new Color[_internalWidth * _internalHeight];
_lastViewportWidth = _graphicsDevice.Viewport.Width;
_lastViewportHeight = _graphicsDevice.Viewport.Height;
}
public RenderQuality Quality => _quality;
public void SetQuality(RenderQuality quality)
{
if (_quality == quality)
{
return;
}
_quality = quality;
ResizeForQuality();
}
public void UpdatePerformanceHint(int currentFps, int targetFps)
{
if (targetFps <= 0)
{
_runtimePerformanceScale = 1f;
return;
}
float ratio = currentFps / (float)targetFps;
_runtimePerformanceScale = Math.Clamp(ratio, 0.55f, 1.15f);
}
private void EnsureRenderSurface()
{
Viewport viewport = _graphicsDevice.Viewport;
if (viewport.Width == _lastViewportWidth && viewport.Height == _lastViewportHeight)
{
return;
}
_lastViewportWidth = viewport.Width;
_lastViewportHeight = viewport.Height;
ResizeForQuality();
}
private void ResizeForQuality()
{
Viewport viewport = _graphicsDevice.Viewport;
float scale = _quality switch
{
RenderQuality.Low => 0.35f,
RenderQuality.Medium => 0.50f,
RenderQuality.High => 0.75f,
_ => 0.50f
};
int width = Math.Max(64, (int)(viewport.Width * scale));
int height = Math.Max(36, (int)(viewport.Height * scale));
ResizeBuffers(width, height);
}
private void ResizeBuffers(int width, int height)
{
if (_internalWidth == width && _internalHeight == height)
{
return;
}
_fractalTexture.Dispose();
_internalWidth = width;
_internalHeight = height;
_fractalTexture = new Texture2D(_graphicsDevice, _internalWidth, _internalHeight, false, SurfaceFormat.Color);
_pixelBuffer = new Color[_internalWidth * _internalHeight];
_postBuffer = new Color[_internalWidth * _internalHeight];
_previousFrame = new Color[_internalWidth * _internalHeight];
_notes.Clear();
_fireworkRockets.Clear();
_fireworkSparks.Clear();
_noteSpawnAccumulator = 0f;
_fireworkSpawnAccumulator = 0f;
}
private int ScaleCount(int baseCount, int minCount = 1)
{
float qualityScale = _quality switch
{
RenderQuality.Low => 0.70f,
RenderQuality.Medium => 1.0f,
RenderQuality.High => 1.35f,
_ => 1.0f
};
int scaled = (int)MathF.Round(baseCount * qualityScale * _runtimePerformanceScale);
return Math.Max(minCount, scaled);
}
public void Draw(AudioFeatures features, float timeSeconds, int presetIndex)
{
ThrowIfDisposed();
EnsureRenderSurface();
switch (presetIndex)
{
@@ -239,7 +335,7 @@ public sealed class FractalRenderer : IDisposable
float glow = 0f;
float minDist = 10f;
int steps = (int)detail;
int steps = ScaleCount((int)detail, 14);
for (int step = 0; step < steps; step++)
{
float px = rayX * march;
@@ -606,7 +702,7 @@ public sealed class FractalRenderer : IDisposable
}
}
float spawnRate = 0.08f + intensity * 1.8f + high * 0.7f;
float spawnRate = (0.08f + intensity * 1.8f + high * 0.7f) * _runtimePerformanceScale;
_noteSpawnAccumulator += spawnRate;
while (_noteSpawnAccumulator >= 1f)
{
@@ -654,7 +750,7 @@ public sealed class FractalRenderer : IDisposable
}
}
float spawnRate = 0.02f + intensity * 0.45f + high * 0.15f;
float spawnRate = (0.02f + intensity * 0.45f + high * 0.15f) * _runtimePerformanceScale;
_fireworkSpawnAccumulator += spawnRate;
while (_fireworkSpawnAccumulator >= 1f)
{
@@ -786,7 +882,7 @@ public sealed class FractalRenderer : IDisposable
}
float beat = Clamp01(features.Beat);
int particles = 2200;
int particles = ScaleCount(2200, 600);
float arms = 4f + Clamp01(features.Mid * 0.002f) * 3f;
for (int i = 0; i < particles; i++)
{
@@ -889,7 +985,7 @@ public sealed class FractalRenderer : IDisposable
}
}
int drops = (int)density;
int drops = ScaleCount((int)density, 20);
for (int i = 0; i < drops; i++)
{
float seed = i * 13.37f;
@@ -1015,7 +1111,7 @@ public sealed class FractalRenderer : IDisposable
}
}
int totalBars = Math.Max(120, _internalWidth / 2);
int totalBars = ScaleCount(Math.Max(120, _internalWidth / 2), 60);
float bandWidth = _internalWidth / 3f;
float maxHeight = _internalHeight * (0.22f + 0.62f * (0.35f + level * 0.65f));
@@ -1064,6 +1160,11 @@ public sealed class FractalRenderer : IDisposable
private void SpawnFireworkRocket(float intensity)
{
if (_fireworkRockets.Count >= ScaleCount(42, 12))
{
return;
}
float x = 0.1f + (float)_rng.NextDouble() * 0.8f;
float y = 1.02f;
float vy = -(0.0065f + (float)_rng.NextDouble() * (0.0025f + intensity * 0.0025f));
@@ -1092,11 +1193,16 @@ public sealed class FractalRenderer : IDisposable
private void ExplodeFirework(FireworkRocket rocket, float intensity)
{
int count = 22 + (int)(intensity * 38f);
int count = ScaleCount(22 + (int)(intensity * 38f), 10);
float speedBase = 0.0018f + intensity * 0.0034f;
for (int i = 0; i < count; i++)
{
if (_fireworkSparks.Count >= ScaleCount(4500, 800))
{
break;
}
float a = (float)(i * (Math.PI * 2.0 / count));
float speed = speedBase + (float)_rng.NextDouble() * speedBase;
float vx = MathF.Cos(a) * speed;
@@ -1117,6 +1223,11 @@ public sealed class FractalRenderer : IDisposable
private void SpawnNote(float intensity)
{
if (_notes.Count >= ScaleCount(320, 80))
{
return;
}
float x = 0.08f + (float)_rng.NextDouble() * 0.84f;
float y = 0.94f + (float)_rng.NextDouble() * 0.08f;
float vy = -(0.0025f + (float)_rng.NextDouble() * (0.0035f + intensity * 0.004f));
@@ -0,0 +1,8 @@
namespace FractalVisualizer.Rendering;
public enum RenderQuality
{
Low = 0,
Medium = 1,
High = 2
}