222 lines
7.4 KiB
C#
222 lines
7.4 KiB
C#
using FluidSim.Audio;
|
||
using FluidSim.Core;
|
||
using FluidSim.Tests;
|
||
using SFML.Graphics;
|
||
using SFML.System;
|
||
using SFML.Window;
|
||
using System;
|
||
using System.Diagnostics;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace FluidSim;
|
||
|
||
public class Program
|
||
{
|
||
private const int SampleRate = 44100;
|
||
private const double DrawFrequency = 60.0;
|
||
|
||
// Playback speed
|
||
private static double _desiredSpeed = 0.01;
|
||
private static double _currentDisplaySpeed = _desiredSpeed;
|
||
private const double MinSpeed = 0.0001;
|
||
private const double MaxSpeed = 1.0;
|
||
private const double ScrollFactor = 1.1;
|
||
private static double _lastNormalSpeed = 0.1;
|
||
private static bool _isRealTime = false;
|
||
|
||
private static volatile bool _timeWarpActive;
|
||
|
||
// Thread load tracking
|
||
private static ThreadLoadTracker _loadTracker = new ThreadLoadTracker();
|
||
|
||
// Audio & simulation
|
||
private static SimulationRingBuffer _simRingBuffer = null!;
|
||
private static SoundEngine _soundEngine = null!;
|
||
private static TestScenario _scenario = null!; // cast to access ThrottleArea
|
||
private static Font? _overlayFont;
|
||
private static Text? _overlayText;
|
||
|
||
// Throttle control
|
||
private static double _throttleTarget = 1.0; // 0‑1, set by arrow keys
|
||
private static double _throttleCurrent = 0.0; // actual current fraction (lerped)
|
||
private const double ThrottleLerpRate = 10.0; // times per second (speed of movement)
|
||
private static bool _wKeyHeld = false;
|
||
private static double _lastThrottleUpdateTime;
|
||
|
||
private const int TargetMaxFill = (int)(SampleRate * 0.2);
|
||
|
||
public static void Main()
|
||
{
|
||
var window = CreateWindow();
|
||
LoadFont();
|
||
_scenario = (TestScenario)InitializeScenario();
|
||
_lastThrottleUpdateTime = 0.0;
|
||
|
||
_simRingBuffer = new SimulationRingBuffer(131072);
|
||
_soundEngine = new SoundEngine(_simRingBuffer) { Volume = 100 };
|
||
_soundEngine.Start();
|
||
|
||
var cts = new CancellationTokenSource();
|
||
Task.Run(() => SimulationLoop(cts.Token), cts.Token);
|
||
|
||
var stopwatch = Stopwatch.StartNew();
|
||
double lastDrawTime = 0.0;
|
||
|
||
while (window.IsOpen)
|
||
{
|
||
window.DispatchEvents();
|
||
|
||
double now = stopwatch.Elapsed.TotalSeconds;
|
||
|
||
// ---- Playback speed smoothing ----
|
||
double targetSpeed = _timeWarpActive ? 1.0 : _desiredSpeed;
|
||
_currentDisplaySpeed += (targetSpeed - _currentDisplaySpeed) *
|
||
(1.0 - Math.Exp(-8.0 * (now - lastDrawTime)));
|
||
_soundEngine.Speed = _currentDisplaySpeed;
|
||
|
||
// ---- Throttle update ----
|
||
double dtThrottle = now - _lastThrottleUpdateTime;
|
||
_lastThrottleUpdateTime = now;
|
||
|
||
double throttleDesiredFraction = _wKeyHeld ? _throttleTarget : 0.0;
|
||
|
||
// Snap to zero instantly when target is zero (key released)
|
||
if (throttleDesiredFraction == 0.0)
|
||
{
|
||
_throttleCurrent = 0.0;
|
||
}
|
||
else
|
||
{
|
||
double smoothing = 1.0 - Math.Exp(-ThrottleLerpRate * dtThrottle);
|
||
_throttleCurrent += (throttleDesiredFraction - _throttleCurrent) * smoothing;
|
||
}
|
||
|
||
_scenario.Throttle = _throttleCurrent;
|
||
|
||
// ---- Drawing ----
|
||
if (now - lastDrawTime >= 1.0 / DrawFrequency)
|
||
{
|
||
if (_overlayText != null)
|
||
{
|
||
string toggleHint = _isRealTime ? "[Space] slow mo" : "[Space] real time";
|
||
_overlayText.DisplayedString =
|
||
$"{toggleHint} Speed: {_currentDisplaySpeed:F3}x RT: {(_currentDisplaySpeed * 100.0):F1}% Sim load: {_loadTracker.LoadPercent:F0}%\n" +
|
||
$"Throttle: {_throttleCurrent * 100:F0}% Target: {_throttleTarget * 100:F0}% [W] {(_wKeyHeld ? "BLIP" : "---")}";
|
||
}
|
||
|
||
window.Clear(Color.Black);
|
||
_scenario.Draw(window);
|
||
if (_overlayText != null) window.Draw(_overlayText);
|
||
window.Display();
|
||
lastDrawTime = now;
|
||
}
|
||
}
|
||
|
||
cts.Cancel();
|
||
_soundEngine.Dispose();
|
||
window.Dispose();
|
||
}
|
||
|
||
private static void SimulationLoop(CancellationToken token)
|
||
{
|
||
while (!token.IsCancellationRequested)
|
||
{
|
||
long cycleStart = Stopwatch.GetTimestamp();
|
||
|
||
long workStart = Stopwatch.GetTimestamp();
|
||
float sample = _scenario.Process();
|
||
_simRingBuffer.Write(sample);
|
||
long workEnd = Stopwatch.GetTimestamp();
|
||
|
||
while (_simRingBuffer.AvailableSamples > TargetMaxFill &&
|
||
!token.IsCancellationRequested)
|
||
{
|
||
Thread.Sleep(1);
|
||
}
|
||
|
||
long cycleEnd = Stopwatch.GetTimestamp();
|
||
|
||
double busyMs = (workEnd - workStart) / (double)Stopwatch.Frequency * 1000.0;
|
||
double totalMs = (cycleEnd - cycleStart) / (double)Stopwatch.Frequency * 1000.0;
|
||
_loadTracker.Record(busyMs, totalMs);
|
||
}
|
||
}
|
||
|
||
// ---------- Window & input ----------
|
||
private static RenderWindow CreateWindow()
|
||
{
|
||
var mode = new VideoMode(new Vector2u(1280, 720));
|
||
var window = new RenderWindow(mode, "FluidSim");
|
||
window.SetVerticalSyncEnabled(false);
|
||
window.SetFramerateLimit(60);
|
||
window.Closed += (_, _) => window.Close();
|
||
window.MouseWheelScrolled += OnMouseWheel;
|
||
window.KeyPressed += OnKeyPressed;
|
||
window.KeyReleased += OnKeyReleased;
|
||
return window;
|
||
}
|
||
|
||
private static void LoadFont()
|
||
{
|
||
try { _overlayFont = new Font("fonts/FiraCodeNerdFont-Medium.ttf"); }
|
||
catch { _overlayFont = null; }
|
||
|
||
if (_overlayFont != null)
|
||
_overlayText = new Text(_overlayFont)
|
||
{
|
||
FillColor = Color.White,
|
||
Position = new Vector2f(10, 10)
|
||
};
|
||
}
|
||
|
||
private static Scenario InitializeScenario()
|
||
{
|
||
var sc = new TestScenario();
|
||
sc.Initialize(SampleRate);
|
||
return sc;
|
||
}
|
||
|
||
private static void OnMouseWheel(object? sender, MouseWheelScrollEventArgs e)
|
||
{
|
||
if (_timeWarpActive) return;
|
||
if (e.Delta > 0) _desiredSpeed *= ScrollFactor;
|
||
else if (e.Delta < 0) _desiredSpeed /= ScrollFactor;
|
||
_desiredSpeed = Math.Clamp(_desiredSpeed, MinSpeed, MaxSpeed);
|
||
_lastNormalSpeed = _desiredSpeed;
|
||
_isRealTime = Math.Abs(_desiredSpeed - 1.0) < 1e-6;
|
||
}
|
||
|
||
private static void OnKeyPressed(object? sender, KeyEventArgs e)
|
||
{
|
||
switch (e.Code)
|
||
{
|
||
case Keyboard.Key.Space:
|
||
_timeWarpActive = !_timeWarpActive;
|
||
if (!_timeWarpActive)
|
||
{
|
||
_desiredSpeed = _lastNormalSpeed;
|
||
_isRealTime = false;
|
||
}
|
||
break;
|
||
|
||
case Keyboard.Key.W:
|
||
_wKeyHeld = true;
|
||
break;
|
||
|
||
case Keyboard.Key.Up:
|
||
_throttleTarget = Math.Min(1.0, _throttleTarget + 0.05);
|
||
break;
|
||
|
||
case Keyboard.Key.Down:
|
||
_throttleTarget = Math.Max(0.0, _throttleTarget - 0.05);
|
||
break;
|
||
}
|
||
}
|
||
|
||
private static void OnKeyReleased(object? sender, KeyEventArgs e)
|
||
{
|
||
if (e.Code == Keyboard.Key.W)
|
||
_wKeyHeld = false;
|
||
}
|
||
} |