Files
FluidSim/Program.cs
2026-06-09 20:20:56 +02:00

232 lines
7.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.001;
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 Scenario _scenario = null!;
private static Font? _overlayFont;
private static Text? _overlayText;
// Throttle control
private static float _throttleTarget = 1.0f;
private static float _throttleCurrent = 0.0f;
private const float ThrottleLerpRate = 10.0f;
private static bool _wKeyHeld = false;
private static float _lastThrottleUpdateTime;
// Load
private static float _loadTarget = 0.0f; // 01
private static float _loadCurrent = 0.0f;
private const int TargetMaxFill = (int)(SampleRate * 0.2);
public static void Main()
{
var window = CreateWindow();
LoadFont();
_scenario = new SingleCylScenario();
_scenario.Font = _overlayFont;
_scenario.Initialize(SampleRate);
_lastThrottleUpdateTime = 0.0f;
_simRingBuffer = new SimulationRingBuffer(8192);
_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 & Load update (shared dt) ----
float dtThrottle = (float)now - _lastThrottleUpdateTime;
_lastThrottleUpdateTime = (float)now;
float throttleDesiredFraction = _wKeyHeld ? _throttleTarget : 0.0f;
if (throttleDesiredFraction == 0.0f)
{
_throttleCurrent = 0.0f;
}
else
{
float smoothing = 1.0f - MathF.Exp(-ThrottleLerpRate * dtThrottle);
_throttleCurrent += (throttleDesiredFraction - _throttleCurrent) * smoothing;
}
float loadSmoothing = 1.0f - MathF.Exp(-ThrottleLerpRate * dtThrottle);
_loadCurrent += (_loadTarget - _loadCurrent) * loadSmoothing;
_scenario.Load = _loadCurrent;
_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" +
$"Load: {_loadCurrent*100:F0}% [←][→] 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 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 = MathF.Min(1.0f, _throttleTarget + 0.05f);
break;
case Keyboard.Key.Down:
_throttleTarget = MathF.Max(0.0f, _throttleTarget - 0.05f);
break;
case Keyboard.Key.Left:
_loadTarget = MathF.Max(0.0f, _loadTarget - 0.05f);
break;
case Keyboard.Key.Right:
_loadTarget = MathF.Min(1.0f, _loadTarget + 0.05f);
break;
}
}
private static void OnKeyReleased(object? sender, KeyEventArgs e)
{
if (e.Code == Keyboard.Key.W)
_wKeyHeld = false;
}
}