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
+31 -2
View File
@@ -30,6 +30,8 @@ public class Game1 : Game
private int _fps; private int _fps;
private int _frameCounter; private int _frameCounter;
private TimeSpan _fpsElapsed; private TimeSpan _fpsElapsed;
private const int TargetFps = 120;
private RenderQuality _renderQuality = RenderQuality.Medium;
private readonly (int Width, int Height)[] _windowedResolutions = private readonly (int Width, int Height)[] _windowedResolutions =
[ [
(1280, 720), (1280, 720),
@@ -41,11 +43,14 @@ public class Game1 : Game
public Game1() public Game1()
{ {
_graphics = new GraphicsDeviceManager(this); _graphics = new GraphicsDeviceManager(this);
_graphics.SynchronizeWithVerticalRetrace = false;
_graphics.PreferredBackBufferWidth = 1280; _graphics.PreferredBackBufferWidth = 1280;
_graphics.PreferredBackBufferHeight = 720; _graphics.PreferredBackBufferHeight = 720;
Content.RootDirectory = "Content"; Content.RootDirectory = "Content";
IsMouseVisible = true; IsMouseVisible = true;
Window.AllowUserResizing = true; Window.AllowUserResizing = true;
IsFixedTimeStep = true;
TargetElapsedTime = TimeSpan.FromSeconds(1.0 / 120.0);
_audioCapture = new AudioCapture(); _audioCapture = new AudioCapture();
_audioAnalyzer = new AudioAnalyzer(_audioCapture, fftSize: 2048, spectrumBins: 64); _audioAnalyzer = new AudioAnalyzer(_audioCapture, fftSize: 2048, spectrumBins: 64);
@@ -65,6 +70,7 @@ public class Game1 : Game
{ {
_spriteBatch = new SpriteBatch(GraphicsDevice); _spriteBatch = new SpriteBatch(GraphicsDevice);
_fractalRenderer = new FractalRenderer(GraphicsDevice); _fractalRenderer = new FractalRenderer(GraphicsDevice);
_fractalRenderer.SetQuality(_renderQuality);
_pixel = new Texture2D(GraphicsDevice, 1, 1, false, SurfaceFormat.Color); _pixel = new Texture2D(GraphicsDevice, 1, 1, false, SurfaceFormat.Color);
_pixel.SetData([Color.White]); _pixel.SetData([Color.White]);
_hudFont = new HudBitmapFont(); _hudFont = new HudBitmapFont();
@@ -107,6 +113,21 @@ public class Game1 : Game
CycleWindowResolution(); CycleWindowResolution();
} }
if (IsPressed(keyboard, Keys.D1))
{
SetRenderQuality(RenderQuality.Low);
}
if (IsPressed(keyboard, Keys.D2))
{
SetRenderQuality(RenderQuality.Medium);
}
if (IsPressed(keyboard, Keys.D3))
{
SetRenderQuality(RenderQuality.High);
}
if (IsPressed(keyboard, Keys.Space)) if (IsPressed(keyboard, Keys.Space))
{ {
_isPaused = !_isPaused; _isPaused = !_isPaused;
@@ -145,7 +166,7 @@ public class Game1 : Game
_fpsElapsed -= TimeSpan.FromSeconds(1); _fpsElapsed -= TimeSpan.FromSeconds(1);
} }
Window.Title = $"Fractal Visualizer | {GetPresetName(_fractalPresetIndex)} | Mode: {_currentInputMode} | L:{_latestFeatures.Level:F3} B:{_latestFeatures.Bass:F2}"; Window.Title = $"Fractal Visualizer | {GetPresetName(_fractalPresetIndex)} | {_renderQuality} | Mode: {_currentInputMode} | FPS:{_fps}";
_previousKeyboardState = keyboard; _previousKeyboardState = keyboard;
base.Update(gameTime); base.Update(gameTime);
} }
@@ -156,6 +177,7 @@ public class Game1 : Game
if (_fractalRenderer is not null) if (_fractalRenderer is not null)
{ {
_fractalRenderer.UpdatePerformanceHint(_fps, TargetFps);
float renderTime = _isPaused ? _latestFeatures.TimeSeconds : (float)gameTime.TotalGameTime.TotalSeconds; float renderTime = _isPaused ? _latestFeatures.TimeSeconds : (float)gameTime.TotalGameTime.TotalSeconds;
_fractalRenderer.Draw(_latestFeatures, renderTime, _fractalPresetIndex); _fractalRenderer.Draw(_latestFeatures, renderTime, _fractalPresetIndex);
} }
@@ -173,7 +195,8 @@ public class Game1 : Game
_hudFont.DrawText(_spriteBatch, _pixel, "BEAT " + _latestFeatures.Beat.ToString("0.000"), 12, 130, 2, Color.White); _hudFont.DrawText(_spriteBatch, _pixel, "BEAT " + _latestFeatures.Beat.ToString("0.000"), 12, 130, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "PAUSED " + (_isPaused ? "YES" : "NO"), 12, 150, 2, Color.White); _hudFont.DrawText(_spriteBatch, _pixel, "PAUSED " + (_isPaused ? "YES" : "NO"), 12, 150, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "FPS " + _fps, 12, 170, 2, Color.White); _hudFont.DrawText(_spriteBatch, _pixel, "FPS " + _fps, 12, 170, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "F OR F11 FULLSCREEN R RESIZE", 12, 190, 2, Color.White); _hudFont.DrawText(_spriteBatch, _pixel, "QUALITY " + _renderQuality + " 1 2 3", 12, 190, 2, Color.White);
_hudFont.DrawText(_spriteBatch, _pixel, "F OR F11 FULLSCREEN R RESIZE", 12, 210, 2, Color.White);
_spriteBatch.End(); _spriteBatch.End();
} }
@@ -232,6 +255,12 @@ public class Game1 : Game
_graphics.ToggleFullScreen(); _graphics.ToggleFullScreen();
} }
private void SetRenderQuality(RenderQuality quality)
{
_renderQuality = quality;
_fractalRenderer?.SetQuality(quality);
}
private void CycleWindowResolution() private void CycleWindowResolution()
{ {
if (_graphics.IsFullScreen) if (_graphics.IsFullScreen)
+126 -15
View File
@@ -11,12 +11,12 @@ public sealed class FractalRenderer : IDisposable
private readonly GraphicsDevice _graphicsDevice; private readonly GraphicsDevice _graphicsDevice;
private readonly SpriteBatch _spriteBatch; private readonly SpriteBatch _spriteBatch;
private readonly int _internalWidth; private int _internalWidth;
private readonly int _internalHeight; private int _internalHeight;
private readonly Texture2D _fractalTexture; private Texture2D _fractalTexture;
private readonly Color[] _pixelBuffer; private Color[] _pixelBuffer;
private readonly Color[] _postBuffer; private Color[] _postBuffer;
private readonly Color[] _previousFrame; private Color[] _previousFrame;
private readonly List<NoteParticle> _notes = []; private readonly List<NoteParticle> _notes = [];
private readonly List<FireworkRocket> _fireworkRockets = []; private readonly List<FireworkRocket> _fireworkRockets = [];
private readonly List<FireworkSpark> _fireworkSparks = []; private readonly List<FireworkSpark> _fireworkSparks = [];
@@ -24,6 +24,10 @@ public sealed class FractalRenderer : IDisposable
private float _noteSpawnAccumulator; private float _noteSpawnAccumulator;
private float _fireworkSpawnAccumulator; private float _fireworkSpawnAccumulator;
private float _runtimePerformanceScale = 1f;
private RenderQuality _quality = RenderQuality.Medium;
private int _lastViewportWidth;
private int _lastViewportHeight;
private bool _disposed; private bool _disposed;
@@ -31,17 +35,109 @@ public sealed class FractalRenderer : IDisposable
{ {
_graphicsDevice = graphicsDevice ?? throw new ArgumentNullException(nameof(graphicsDevice)); _graphicsDevice = graphicsDevice ?? throw new ArgumentNullException(nameof(graphicsDevice));
_spriteBatch = new SpriteBatch(_graphicsDevice); _spriteBatch = new SpriteBatch(_graphicsDevice);
_internalWidth = internalWidth; _internalWidth = Math.Max(32, internalWidth);
_internalHeight = internalHeight; _internalHeight = Math.Max(18, internalHeight);
_fractalTexture = new Texture2D(_graphicsDevice, _internalWidth, _internalHeight, false, SurfaceFormat.Color); _fractalTexture = new Texture2D(_graphicsDevice, _internalWidth, _internalHeight, false, SurfaceFormat.Color);
_pixelBuffer = new Color[_internalWidth * _internalHeight]; _pixelBuffer = new Color[_internalWidth * _internalHeight];
_postBuffer = new Color[_internalWidth * _internalHeight]; _postBuffer = new Color[_internalWidth * _internalHeight];
_previousFrame = 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) public void Draw(AudioFeatures features, float timeSeconds, int presetIndex)
{ {
ThrowIfDisposed(); ThrowIfDisposed();
EnsureRenderSurface();
switch (presetIndex) switch (presetIndex)
{ {
@@ -239,7 +335,7 @@ public sealed class FractalRenderer : IDisposable
float glow = 0f; float glow = 0f;
float minDist = 10f; float minDist = 10f;
int steps = (int)detail; int steps = ScaleCount((int)detail, 14);
for (int step = 0; step < steps; step++) for (int step = 0; step < steps; step++)
{ {
float px = rayX * march; 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; _noteSpawnAccumulator += spawnRate;
while (_noteSpawnAccumulator >= 1f) 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; _fireworkSpawnAccumulator += spawnRate;
while (_fireworkSpawnAccumulator >= 1f) while (_fireworkSpawnAccumulator >= 1f)
{ {
@@ -786,7 +882,7 @@ public sealed class FractalRenderer : IDisposable
} }
float beat = Clamp01(features.Beat); float beat = Clamp01(features.Beat);
int particles = 2200; int particles = ScaleCount(2200, 600);
float arms = 4f + Clamp01(features.Mid * 0.002f) * 3f; float arms = 4f + Clamp01(features.Mid * 0.002f) * 3f;
for (int i = 0; i < particles; i++) 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++) for (int i = 0; i < drops; i++)
{ {
float seed = i * 13.37f; 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 bandWidth = _internalWidth / 3f;
float maxHeight = _internalHeight * (0.22f + 0.62f * (0.35f + level * 0.65f)); 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) private void SpawnFireworkRocket(float intensity)
{ {
if (_fireworkRockets.Count >= ScaleCount(42, 12))
{
return;
}
float x = 0.1f + (float)_rng.NextDouble() * 0.8f; float x = 0.1f + (float)_rng.NextDouble() * 0.8f;
float y = 1.02f; float y = 1.02f;
float vy = -(0.0065f + (float)_rng.NextDouble() * (0.0025f + intensity * 0.0025f)); 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) 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; float speedBase = 0.0018f + intensity * 0.0034f;
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
if (_fireworkSparks.Count >= ScaleCount(4500, 800))
{
break;
}
float a = (float)(i * (Math.PI * 2.0 / count)); float a = (float)(i * (Math.PI * 2.0 / count));
float speed = speedBase + (float)_rng.NextDouble() * speedBase; float speed = speedBase + (float)_rng.NextDouble() * speedBase;
float vx = MathF.Cos(a) * speed; float vx = MathF.Cos(a) * speed;
@@ -1117,6 +1223,11 @@ public sealed class FractalRenderer : IDisposable
private void SpawnNote(float intensity) private void SpawnNote(float intensity)
{ {
if (_notes.Count >= ScaleCount(320, 80))
{
return;
}
float x = 0.08f + (float)_rng.NextDouble() * 0.84f; float x = 0.08f + (float)_rng.NextDouble() * 0.84f;
float y = 0.94f + (float)_rng.NextDouble() * 0.08f; float y = 0.94f + (float)_rng.NextDouble() * 0.08f;
float vy = -(0.0025f + (float)_rng.NextDouble() * (0.0035f + intensity * 0.004f)); 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
}