diff --git a/FractalVisualizer/Game1.cs b/FractalVisualizer/Game1.cs index df62bf1..61a11ae 100644 --- a/FractalVisualizer/Game1.cs +++ b/FractalVisualizer/Game1.cs @@ -30,6 +30,8 @@ public class Game1 : Game private int _fps; private int _frameCounter; private TimeSpan _fpsElapsed; + private const int TargetFps = 120; + private RenderQuality _renderQuality = RenderQuality.Medium; private readonly (int Width, int Height)[] _windowedResolutions = [ (1280, 720), @@ -41,11 +43,14 @@ public class Game1 : Game public Game1() { _graphics = new GraphicsDeviceManager(this); + _graphics.SynchronizeWithVerticalRetrace = false; _graphics.PreferredBackBufferWidth = 1280; _graphics.PreferredBackBufferHeight = 720; Content.RootDirectory = "Content"; IsMouseVisible = true; Window.AllowUserResizing = true; + IsFixedTimeStep = true; + TargetElapsedTime = TimeSpan.FromSeconds(1.0 / 120.0); _audioCapture = new AudioCapture(); _audioAnalyzer = new AudioAnalyzer(_audioCapture, fftSize: 2048, spectrumBins: 64); @@ -65,6 +70,7 @@ public class Game1 : Game { _spriteBatch = new SpriteBatch(GraphicsDevice); _fractalRenderer = new FractalRenderer(GraphicsDevice); + _fractalRenderer.SetQuality(_renderQuality); _pixel = new Texture2D(GraphicsDevice, 1, 1, false, SurfaceFormat.Color); _pixel.SetData([Color.White]); _hudFont = new HudBitmapFont(); @@ -107,6 +113,21 @@ public class Game1 : Game 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)) { _isPaused = !_isPaused; @@ -145,7 +166,7 @@ public class Game1 : Game _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; base.Update(gameTime); } @@ -156,6 +177,7 @@ public class Game1 : Game if (_fractalRenderer is not null) { + _fractalRenderer.UpdatePerformanceHint(_fps, TargetFps); float renderTime = _isPaused ? _latestFeatures.TimeSeconds : (float)gameTime.TotalGameTime.TotalSeconds; _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, "PAUSED " + (_isPaused ? "YES" : "NO"), 12, 150, 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(); } @@ -232,6 +255,12 @@ public class Game1 : Game _graphics.ToggleFullScreen(); } + private void SetRenderQuality(RenderQuality quality) + { + _renderQuality = quality; + _fractalRenderer?.SetQuality(quality); + } + private void CycleWindowResolution() { if (_graphics.IsFullScreen) diff --git a/FractalVisualizer/Rendering/FractalRenderer.cs b/FractalVisualizer/Rendering/FractalRenderer.cs index f65a959..ba06c9b 100644 --- a/FractalVisualizer/Rendering/FractalRenderer.cs +++ b/FractalVisualizer/Rendering/FractalRenderer.cs @@ -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 _notes = []; private readonly List _fireworkRockets = []; private readonly List _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)); diff --git a/FractalVisualizer/Rendering/RenderQuality.cs b/FractalVisualizer/Rendering/RenderQuality.cs new file mode 100644 index 0000000..0540417 --- /dev/null +++ b/FractalVisualizer/Rendering/RenderQuality.cs @@ -0,0 +1,8 @@ +namespace FractalVisualizer.Rendering; + +public enum RenderQuality +{ + Low = 0, + Medium = 1, + High = 2 +}