Add turbocharged twin scenario and cross-platform test harness

This commit is contained in:
2026-10-23 02:27:03 +02:00
parent a859740a95
commit 589d22aa5e
63 changed files with 4903 additions and 771 deletions

View File

@@ -1,4 +1,4 @@
#include "FluidSim/Scenario.hpp"
#include "FluidSim/DesktopScenario.hpp"
#include "FluidSim/SoundEngine.hpp"
#include "FluidSim/DynoCurve.hpp"
@@ -6,6 +6,8 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdlib>
#include <iostream>
#include <cmath>
#include <filesystem>
#include <iomanip>
@@ -19,8 +21,8 @@ using namespace fluidsim::port;
constexpr unsigned int window_width = 1280;
constexpr unsigned int window_height = 720;
constexpr std::size_t ring_capacity = 8192;
constexpr std::size_t target_fill = 1024;
constexpr std::size_t preroll_samples = 1024;
constexpr std::size_t target_fill = 4096;
constexpr std::size_t preroll_samples = 4096;
constexpr float throttle_lerp_rate = 10.0f;
sf::Color pressure_color(float pressure) {
@@ -54,6 +56,72 @@ void draw_label(sf::RenderWindow& window, const sf::Font* font, const std::strin
window.draw(label);
}
std::optional<ScenarioKind> choose_scenario(sf::RenderWindow& window, const sf::Font* font) {
const std::array<sf::FloatRect, 4> cards{{{{32, 110}, {656, 84}}, {{32, 206}, {656, 84}}, {{32, 302}, {656, 84}}, {{32, 398}, {656, 84}}}};
const sf::FloatRect start_button{{32, 506}, {656, 52}};
const std::array<const char*, 4> titles{"600 cc parallel twin", "YZ125 two-stroke", "Saab B205 / 9-5 wagon", "600 cc twin + VZ21-size turbo"};
const std::array<const char*, 4> descriptions{
"Four-stroke twin, motorcycle test and dyno",
"125 cc single, expansion chamber, motorcycle test and dyno",
"2.0 litre inline-four, no turbo yet, five-speed wagon and dyno",
"Coupled turbine/compressor, shaft inertia and pressure diagnostics"};
int selected = 0;
while (window.isOpen()) {
while (const auto event = window.pollEvent()) {
if (event->is<sf::Event::Closed>()) return std::nullopt;
if (const auto* key = event->getIf<sf::Event::KeyPressed>()) {
switch (key->code) {
case sf::Keyboard::Key::Escape: return std::nullopt;
case sf::Keyboard::Key::Up: selected = (selected + 3) % 4; break;
case sf::Keyboard::Key::Down:
case sf::Keyboard::Key::Tab: selected = (selected + 1) % 4; break;
case sf::Keyboard::Key::Num1: selected = 0; break;
case sf::Keyboard::Key::Num2: selected = 1; break;
case sf::Keyboard::Key::Num3: selected = 2; break;
case sf::Keyboard::Key::Num4: selected = 3; break;
case sf::Keyboard::Key::Enter: return static_cast<ScenarioKind>(selected);
default: break;
}
}
if (const auto* move = event->getIf<sf::Event::MouseMoved>()) {
const auto point = window.mapPixelToCoords(move->position);
for (int i = 0; i < 4; ++i)
if (cards[i].contains(point)) selected = i;
}
if (const auto* click = event->getIf<sf::Event::MouseButtonReleased>();
click && click->button == sf::Mouse::Button::Left) {
const auto point = window.mapPixelToCoords(click->position);
for (int i = 0; i < 4; ++i)
if (cards[i].contains(point)) return static_cast<ScenarioKind>(i);
if (start_button.contains(point)) return static_cast<ScenarioKind>(selected);
}
}
window.clear({9, 12, 17});
draw_label(window, font, "FluidSim", {32, 24}, sf::Color::White, 28);
draw_label(window, font, "Choose a scenario to start", {32, 66}, {180, 190, 205}, 16);
for (int i = 0; i < 4; ++i) {
sf::RectangleShape card(cards[i].size);
card.setPosition(cards[i].position);
card.setFillColor(i == selected ? sf::Color(24, 46, 60) : sf::Color(18, 24, 32));
card.setOutlineColor(i == selected ? sf::Color(90, 200, 210) : sf::Color(55, 65, 78));
card.setOutlineThickness(2);
window.draw(card);
draw_label(window, font, titles[i], cards[i].position + sf::Vector2f{20, 16},
sf::Color::White, 20);
draw_label(window, font, descriptions[i], cards[i].position + sf::Vector2f{20, 54},
{180, 190, 205}, 13);
}
sf::RectangleShape start(start_button.size);
start.setPosition(start_button.position);
start.setFillColor({35, 105, 115});
window.draw(start);
draw_label(window, font, "Start selected scenario [Enter]", {52, 519}, sf::Color::White, 17);
draw_label(window, font, "Click a scenario or use Up/Down. Esc: quit", {32, 574}, {150, 165, 180}, 12);
window.display();
}
return std::nullopt;
}
void draw_volume(sf::RenderWindow& window, float pressure, sf::FloatRect bounds) {
sf::RectangleShape body({bounds.size.x, bounds.size.y});
body.setPosition(bounds.position);
@@ -67,20 +135,27 @@ void draw_pipe(sf::RenderWindow& window, const ScenarioSnapshot& snapshot, std::
float center_y, float x0, float x1) {
const auto& cells = snapshot.pipe_states[pipe_index];
if (cells.empty()) return;
const float cell_width = (x1 - x0) / static_cast<float>(cells.size());
const auto& areas = snapshot.pipe_areas[pipe_index];
const auto& lengths = snapshot.pipe_lengths[pipe_index];
float total_length = 0;
for (float length : lengths) total_length += length;
float x = x0;
for (std::size_t i = 0; i < cells.size(); ++i) {
const auto& cell = cells[i];
const float thickness = std::clamp(6.0f + cell.pressure / 101325.0f * 0.7f, 6.0f, 20.0f);
const float cell_width = total_length > 0 ? (x1 - x0) * lengths[i] / total_length
: (x1 - x0) / cells.size();
const float thickness = areas.empty() ? 8 : 600 * std::sqrt(4 * areas[i] / pi);
sf::RectangleShape segment({cell_width + 1.0f, thickness});
segment.setPosition({x0 + static_cast<float>(i) * cell_width, center_y - thickness * 0.5f});
segment.setPosition({x, center_y - thickness * .5f});
segment.setFillColor(temperature_color(cell.pressure / std::max(cell.rho * 287.0f, 1e-9f)));
window.draw(segment);
x += cell_width;
}
}
void draw_cylinder(sf::RenderWindow& window, const sf::Font* font, float pressure,
float piston_fraction, float top, const char* name, bool intake_open, bool exhaust_open) {
constexpr float x = 470.0f, width = 74.0f, height = 142.0f;
float piston_fraction, float top, const char* name, bool intake_open, bool exhaust_open, float x = 470.0f) {
constexpr float width = 74.0f, height = 142.0f;
const float fraction = std::clamp(piston_fraction, 0.0f, 1.0f);
const float gas_height = height * fraction;
sf::RectangleShape wall({width, height});
@@ -219,18 +294,95 @@ void draw_dyno_curve(sf::RenderWindow& window, const sf::Font* font, const DynoC
}
}
void draw_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& state,
float throttle_target, float brake, double playback_speed, std::size_t buffer_fill,
std::size_t buffer_capacity, bool time_warp, DynoCurve& dyno_curve) {
void draw_dashboard(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& state,
float throttle_target, float brake, double playback_speed, std::size_t buffer_fill,
std::size_t buffer_capacity, bool time_warp, std::uint64_t underrun_blocks) {
std::ostringstream status;
status << (time_warp ? "[Space] normal speed" : "[Space] real time")
<< " Sim " << std::fixed << std::setprecision(3) << playback_speed << "x"
<< " Audio buffer " << buffer_fill << "/" << buffer_capacity
<< " Underruns " << underrun_blocks
<< (state.cylinder_count == 4 ? "\nWagon " : "\nBike ") << std::setprecision(1) << state.speed_kmh << " km/h Gear "
<< (state.gear == 0 ? "N" : std::to_string(state.gear))
<< " Throttle " << state.throttle * 100.0f << "% / target " << throttle_target * 100.0f << "% [W, up/down]"
<< " Clutch " << state.clutch * 100.0f << "% [C] Engine load " << brake * 100.0f << "% [left/right]"
<< " Shift [A/D], Neutral [Z]"
<< "\nScene [R]: " << scene_name(state.audio_scene) << " " << (state.fuel_cut ? "FUEL CUT" : (state.idle_control ? "IDLE CONTROL" : "RUNNING"));
if (!state.two_stroke)
status << " Mechanical [M]: " << (state.mechanical_sound_enabled ? "ON" : "OFF");
if (font) {
sf::RectangleShape panel({1260.0f, 92.0f});
panel.setPosition({10.0f, 10.0f});
panel.setFillColor({12, 18, 26, 236});
panel.setOutlineColor({80, 110, 130});
panel.setOutlineThickness(1.0f);
window.draw(panel);
draw_label(window, font, status.str(), {22.0f, 20.0f}, sf::Color::White, 14);
draw_label(window, font,
"RPM " + std::to_string(static_cast<int>(state.rpm)) +
" Power " + std::to_string(state.average_power / 1000.0f) + " kW" +
" Vehicle kinetic energy " + std::to_string(static_cast<int>(state.kinetic_energy)) + " J",
{24.0f, 122.0f}, {220, 230, 240}, 17);
draw_label(window, font, state.two_stroke ? "REV LIMIT DISABLED" :
((state.rev_limit1 || state.rev_limit2) ? std::string("REV CUT") : "REV LIMIT " + std::to_string(int(state.rev_limit_rpm)) + " rpm"),
{24.0f, 151.0f}, {120, 220, 120}, 14);
}
if (state.turbo_enabled) {
const auto& t = state.turbo;
std::ostringstream text;
text << std::fixed << std::setprecision(0) << "VZ21-size: " << t.rpm << " rpm "
<< std::setprecision(2) << "CPR " << t.compressor_pressure_ratio
<< " Charge " << t.charge_pressure/1000 << " kPa / " << std::setprecision(0) << t.charge_temperature << " K"
<< "\nTurbine inlet " << t.turbine_inlet_pressure/1000 << " kPa Outlet " << t.turbine_outlet_pressure/1000 << " kPa"
<< " Power " << t.turbine_power << " / " << t.compressor_power << " W";
draw_label(window, font, text.str(), {625, 125}, {130, 215, 220}, 12);
}
}
void draw_two_stroke_scene(sf::RenderWindow& window,const sf::Font* font,const ScenarioSnapshot& s) {
window.clear({9,12,17});
draw_label(window,font,"YZ125 baseline | piston-controlled ports",
{650,185},sf::Color::White,14);
draw_cylinder(window,font,s.cylinder_pressure1,s.cylinder_piston_fraction1,220,"CYLINDER",
s.valve1_intake_area>0,s.valve1_exhaust_area>0);
draw_volume(window,s.crankcase_pressure,{{455,410},{110,95}});
draw_label(window,font,"CRANKCASE",{455,385},sf::Color::White,14);
draw_pipe(window,s,0,460,30,170);
draw_volume(window,s.intake_pressure,{{180,425},{95,70}});
draw_label(window,font,"INTAKE",{180,400},sf::Color::White,14);
draw_pipe(window,s,1,460,285,445);
draw_label(window,font,s.reed_open?"REED OPEN":"REED CLOSED",{290,485},s.reed_open?sf::Color::Green:sf::Color::Red,12);
draw_pipe(window,s,2,350,315,450);
draw_graph_line(window,{510,505},{510,530},{180,190,200});
draw_graph_line(window,{510,530},{315,530},{180,190,200});
draw_graph_line(window,{315,530},{315,350},{180,190,200});
draw_graph_line(window,{450,350},{450,245},{180,190,200});
draw_graph_line(window,{450,245},{467,245},{180,190,200});
draw_label(window,font,"TRANSFER",{320,325},sf::Color::White,12);
draw_graph_line(window,{551,235},{590,235},{180,190,200});
draw_pipe(window,s,3,235,590,1230);
draw_label(window,font,s.racing_exhaust ? "HEADER / DIFFUSERS / BELLY / BAFFLE / STINGER" : "STRAIGHT EXHAUST",{670,205},sf::Color::White,13);
std::ostringstream pressures;
pressures<<std::fixed<<std::setprecision(1)<<"Cylinder "<<s.cylinder_pressure1/1000<<" kPa Crankcase "
<<s.crankcase_pressure/1000<<" kPa Intake "<<s.intake_pressure/1000<<" kPa";
draw_label(window,font,pressures.str(),{22,580},sf::Color::White,17);
draw_label(window,font,"54 x 54.5 mm | 8.2:1 trapped compression | estimated pipe/ports/CDI | fixed-open power valve",
{22,625},{180,190,200},11);
}
void draw_twin_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& state) {
constexpr float intake_y = 310.0f;
constexpr float upper_y = 152.0f;
constexpr float lower_y = 400.0f;
window.clear({9, 12, 17});
const float power_kw = state.average_power / 1000.0f;
dyno_curve.update(state.rpm, power_kw, state.average_torque);
// Intake: ambient pipe → throttle → plenum → two cylinder runners.
draw_pipe(window, state, 0, intake_y, 36.0f, 165.0f);
draw_pipe(window, state, 0, intake_y, state.turbo_enabled ? 135.f : 36.f, 165.0f);
if (state.turbo_enabled) {
draw_pipe(window, state, 6, intake_y, 36, 90);
draw_volume(window, float(state.turbo.charge_pressure), {{95, intake_y-18}, {35, 36}});
draw_label(window, font, "COMP", {92, intake_y+30}, {130,215,220}, 10);
}
sf::RectangleShape throttle({8.0f, 28.0f});
throttle.setPosition({169.0f, intake_y - 14.0f});
throttle.setFillColor(sf::Color::Yellow);
@@ -247,7 +399,12 @@ void draw_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSn
draw_pipe(window, state, 3, upper_y + 71.0f, 556.0f, 820.0f);
draw_pipe(window, state, 4, lower_y + 71.0f, 556.0f, 820.0f);
draw_volume(window, state.exhaust_pressure, {{838.0f, 305.0f}, {70.0f, 82.0f}});
draw_pipe(window, state, 5, 346.0f, 915.0f, 1236.0f);
draw_pipe(window, state, 5, 346.0f, 915.0f, state.turbo_enabled ? 1040.f : 1236.f);
if (state.turbo_enabled) {
draw_volume(window, float(state.turbo.turbine_inlet_pressure), {{1045, 323}, {45, 46}});
draw_label(window, font, "TURBINE", {1040, 380}, {130,215,220}, 10);
draw_pipe(window, state, 7, 346, 1100, 1236);
}
draw_label(window, font, "AMBIENT", {37.0f, intake_y + 22.0f}, {180, 190, 200}, 12);
draw_label(window, font, "THROTTLE", {151.0f, intake_y + 24.0f}, {240, 210, 90}, 11);
@@ -255,42 +412,49 @@ void draw_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSn
draw_label(window, font, "SHARED EXHAUST", {818.0f, 285.0f}, sf::Color::White, 11);
draw_label(window, font, "TAILPIPE", {1084.0f, 362.0f}, {180, 190, 200}, 11);
std::ostringstream status;
status << (time_warp ? "[Space] normal speed" : "[Space] real time")
<< " Sim " << std::fixed << std::setprecision(3) << playback_speed << "x"
<< " Audio buffer " << buffer_fill << "/" << buffer_capacity
<< "\nBike " << std::setprecision(1) << state.speed_kmh << " km/h Gear "
<< (state.gear == 0 ? "N" : std::to_string(state.gear))
<< " Throttle " << state.throttle * 100.0f << "% / target " << throttle_target * 100.0f << "% [W, up/down]"
<< " Clutch " << state.clutch * 100.0f << "% [C] Brake " << brake * 100.0f << "% [left/right]"
<< " Shift [A/D], Neutral [Z] Mechanical [M]: "
<< (state.mechanical_sound_enabled ? "ON" : "OFF");
if (font) {
sf::RectangleShape panel({1260.0f, 92.0f});
panel.setPosition({10.0f, 10.0f});
panel.setFillColor({12, 18, 26, 236});
panel.setOutlineColor({80, 110, 130});
panel.setOutlineThickness(1.0f);
window.draw(panel);
draw_label(window, font, status.str(), {22.0f, 20.0f}, sf::Color::White, 14);
draw_label(window, font,
"RPM " + std::to_string(static_cast<int>(state.rpm)) +
" Power " + std::to_string(state.average_power / 1000.0f) + " kW" +
" Bike kinetic energy " + std::to_string(static_cast<int>(state.kinetic_energy)) + " J",
{24.0f, 122.0f}, {220, 230, 240}, 17);
const sf::Color rev = state.rev_limit1 || state.rev_limit2 ? sf::Color::Red : sf::Color(120, 220, 120);
draw_label(window, font, (state.rev_limit1 || state.rev_limit2) ? "REV CUT" : "REV LIMIT 10000 rpm",
{24.0f, 151.0f}, rev, 14);
draw_label(window, font,
"Cylinder pressure " + std::to_string(state.cylinder_pressure1 / 1e5f) + " / " +
std::to_string(state.cylinder_pressure2 / 1e5f) + " bar Intake " +
std::to_string(state.intake_pressure / 1000.0f) + " kPa Exhaust " +
std::to_string(state.exhaust_pressure / 1000.0f) + " kPa",
{24.0f, 176.0f}, {205, 215, 225}, 13);
}
}
draw_dyno_curve(window, font, dyno_curve, 860.0f, 455.0f, 400.0f, 250.0f,
state.rpm, power_kw);
void draw_b205_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& s) {
window.clear({9, 12, 17});
draw_label(window, font, "SAAB B205 | 1985 cc inline-four | firing 1-3-4-2 | no turbo yet",
{260, 181}, sf::Color::White, 16);
draw_pipe(window, s, 0, 215, 25, 110);
draw_volume(window, s.intake_pressure, {{120, 195}, {65, 65}});
draw_label(window, font, "PLENUM", {120, 270}, sf::Color::White, 12);
draw_graph_line(window, {190, 211}, {820, 211}, {80, 145, 200});
draw_graph_line(window, {220, 420}, {850, 420}, {200, 120, 80});
for (int i = 0; i < 4; ++i) {
const float x = 230 + i*150;
const std::string title = "CYL " + std::to_string(i+1);
draw_cylinder(window, font, s.cylinder_pressures[i], s.piston_fractions[i], 255, title.c_str(),
s.intake_areas[i] > 1e-7f, s.exhaust_areas[i] > 1e-7f, x);
draw_pipe(window, s, 1+i, 235, x-15, x+75);
draw_pipe(window, s, 5+i, 410, x-15, x+75);
draw_graph_line(window, {x-15, 211}, {x-15, 235}, {80,145,200});
draw_graph_line(window, {x+75, 410}, {x+75, 420}, {200,120,80});
draw_label(window, font, std::to_string(int(s.cylinder_pressures[i]/1000))+" kPa",
{x, 445}, {180,190,205}, 12);
}
draw_volume(window, s.exhaust_pressure, {{850, 393}, {65, 48}});
draw_pipe(window, s, 9, 420, 920, 1240);
draw_label(window, font, "COLLECTOR / OPEN EXHAUST", {930, 390}, sf::Color::White, 12);
draw_label(window, font, "9-5 wagon baseline: 1675 kg with driver, five-speed manual, 4.05 final drive",
{25, 550}, sf::Color::White, 14);
draw_label(window, font, "Engine dimensions/firing from Saab WIS. Manifolds, cams, losses and ECU are estimates.",
{25, 580}, {180,190,205}, 12);
}
void draw_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSnapshot& state,
float throttle_target, float brake, double playback_speed, std::size_t buffer_fill,
std::size_t buffer_capacity, bool time_warp, DynoCurve& dyno_curve, std::uint64_t underrun_blocks) {
const float power_kw = state.average_power / 1000.0f;
dyno_curve.update(state.rpm, power_kw, state.average_torque);
if (state.two_stroke) draw_two_stroke_scene(window, font, state);
else if (state.cylinder_count == 4) draw_b205_scene(window, font, state);
else draw_twin_scene(window, font, state);
draw_dashboard(window, font, state, throttle_target, brake, playback_speed, buffer_fill,
buffer_capacity, time_warp, underrun_blocks);
draw_dyno_curve(window, font, dyno_curve, 860, 455, 400, 250, state.rpm, power_kw);
window.display();
}
@@ -298,22 +462,72 @@ void draw_scene(sf::RenderWindow& window, const sf::Font* font, const ScenarioSn
int main(int argc, char** argv) {
using namespace fluidsim::port;
sf::RenderWindow window(sf::VideoMode({window_width, window_height}), "FluidSim C++ - parallel twin");
ScenarioKind selected_kind=ScenarioKind::Twin;
bool scenario_from_args=false;
bool turbo_from_args=false;
TwoStrokeConfig two_stroke_config;
OutdoorScene selected_scene=OutdoorScene::Wall;
for(int i=1;i<argc;++i){
const std::string option=argv[i];
if((option=="--scenario"||option=="--scene"||option=="--exhaust"||option=="--turbo")&&i+1<argc){
const std::string value=argv[++i];
if(option=="--scenario"){
if(value!="twin"&&value!="two-stroke"&&value!="b205"){std::cerr<<"Choose twin, two-stroke or b205\n";return 2;}
selected_kind=value=="b205" ? ScenarioKind::SaabB205 :
(value=="two-stroke" ? ScenarioKind::TwoStroke : ScenarioKind::Twin);
scenario_from_args=true;
}else if(option=="--turbo"){
if(value!="none"&&value!="vz21"){std::cerr<<"Choose none or vz21 turbo\n";return 2;}
turbo_from_args=value=="vz21";
}else if(option=="--exhaust"){
if(value=="racing") two_stroke_config.exhaust=TwoStrokeExhaust::Racing;
else if(value=="straight") two_stroke_config.exhaust=TwoStrokeExhaust::Straight;
else { std::cerr<<"Choose racing or straight exhaust\n";return 2; }
}else{
if(value=="dry")selected_scene=OutdoorScene::Dry;
else if(value=="field")selected_scene=OutdoorScene::Field;
else if(value=="wall")selected_scene=OutdoorScene::Wall;
else if(value=="woodland")selected_scene=OutdoorScene::Woodland;
else {std::cerr<<"Choose dry, field, wall or woodland\n";return 2;}
}
}else{std::cerr<<"Usage: fluidsim_desktop [--scenario twin|two-stroke|b205] [--scene dry|field|wall|woodland] [--exhaust racing|straight] [--turbo none|vz21]\n";return 2;}
}
const sf::Vector2u initial_size = scenario_from_args ? sf::Vector2u{window_width, window_height}
: sf::Vector2u{720, 606};
sf::RenderWindow window(sf::VideoMode(initial_size), "FluidSim");
window.setVerticalSyncEnabled(false);
window.setFramerateLimit(60);
window.setKeyRepeatEnabled(false);
sf::Font font;
std::filesystem::path font_path{"fonts/FiraCodeNerdFont-Medium.ttf"};
if (!std::filesystem::exists(font_path) && argc > 0) {
const auto executable_font = std::filesystem::path(argv[0]).parent_path() /
"fonts/FiraCodeNerdFont-Medium.ttf";
if (std::filesystem::exists(executable_font)) font_path = executable_font;
}
const bool has_font = font.openFromFile(font_path.string());
const sf::Font* font_ptr = has_font ? &font : nullptr;
std::vector<std::filesystem::path> font_paths{"fonts/FiraCodeNerdFont-Medium.ttf"};
if(argc>0)font_paths.push_back(std::filesystem::path(argv[0]).parent_path()/"fonts/FiraCodeNerdFont-Medium.ttf");
font_paths.emplace_back("/usr/share/fonts/Adwaita/AdwaitaMono-Regular.ttf");
font_paths.emplace_back("/usr/share/fonts/TTF/DejaVuSansMono.ttf");
font_paths.emplace_back("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf");
if(const char* windows_directory=std::getenv("WINDIR"))font_paths.push_back(std::filesystem::path(windows_directory)/"Fonts/consola.ttf");
bool has_font=false;
for(const auto& path:font_paths)if(std::filesystem::exists(path)&&font.openFromFile(path)){has_font=true;break;}
if(!has_font)std::cerr<<"No UI font available; configure FLUIDSIM_FONT_FILE.\n";
const sf::Font* font_ptr=has_font?&font:nullptr;
FourStrokeTwinScenario scenario(44100);
if (!scenario_from_args) {
const auto choice = choose_scenario(window, font_ptr);
if (!choice) return 0;
selected_kind = *choice;
window.setSize({window_width, window_height});
window.setView(sf::View(sf::FloatRect({0, 0}, {float(window_width), float(window_height)})));
}
if(turbo_from_args) {
if(selected_kind!=ScenarioKind::Twin && selected_kind!=ScenarioKind::TurboTwin) {
std::cerr<<"The VZ21 turbo test uses the twin scenario\n";return 2;
}
selected_kind=ScenarioKind::TurboTwin;
}
window.setTitle(selected_kind == ScenarioKind::TurboTwin ? "FluidSim - twin + VZ21 turbo" : selected_kind == ScenarioKind::SaabB205 ? "FluidSim - Saab B205 / 9-5 wagon" :
(selected_kind == ScenarioKind::TwoStroke ? "FluidSim - YZ125 two-stroke" : "FluidSim - parallel twin"));
DesktopScenario scenario(selected_kind, two_stroke_config);
scenario.set_audio_scene(selected_scene);
SimulationRingBuffer ring(ring_capacity);
for (std::size_t i = 0; i < preroll_samples; ++i) {
const AudioFrame audio = scenario.process();
@@ -325,17 +539,19 @@ int main(int argc, char** argv) {
audio.set_volume(100.0f);
audio.start();
std::jthread simulation([&](std::stop_token stop) {
while (ring.wait_for_space(target_fill, stop)) {
const AudioFrame frame = scenario.process();
if (!ring.write(frame.left, frame.right, stop)) break;
constexpr std::size_t batch_size = 512;
while (ring.wait_for_space(target_fill - batch_size, stop)) {
for (std::size_t i = 0; i < batch_size; ++i) {
const AudioFrame frame = scenario.process();
if (!ring.write(frame.left, frame.right, stop)) return;
}
}
});
double desired_speed = 0.001;
double desired_speed = 1.0;
double current_speed = desired_speed;
double last_normal_speed = 0.1;
double last_normal_speed = 1.0;
bool time_warp = false;
bool real_time = false;
float throttle_target = 1.0f, throttle_current = 0.0f;
float brake_target = 0.0f, brake_current = 0.0f;
float clutch_current = 0.0f;
@@ -350,7 +566,7 @@ int main(int argc, char** argv) {
switch (key->code) {
case sf::Keyboard::Key::Space:
time_warp = !time_warp;
if (!time_warp) { desired_speed = last_normal_speed; real_time = false; }
if (!time_warp) { desired_speed = last_normal_speed; }
break;
case sf::Keyboard::Key::W: throttle_held = true; break;
case sf::Keyboard::Key::C: clutch_held = true; break;
@@ -361,6 +577,10 @@ int main(int argc, char** argv) {
case sf::Keyboard::Key::D: scenario.shift_up(); break;
case sf::Keyboard::Key::A: scenario.shift_down(); break;
case sf::Keyboard::Key::Z: scenario.set_neutral(); break;
case sf::Keyboard::Key::R:
selected_scene=static_cast<OutdoorScene>((static_cast<int>(selected_scene)+1)%4);
scenario.set_audio_scene(selected_scene);
break;
case sf::Keyboard::Key::M: scenario.toggle_mechanical_sound(); break;
default: break;
}
@@ -373,7 +593,6 @@ int main(int argc, char** argv) {
desired_speed *= wheel->delta > 0.0f ? 1.1 : (wheel->delta < 0.0f ? 1.0 / 1.1 : 1.0);
desired_speed = std::clamp(desired_speed, 0.0001, 1.0);
last_normal_speed = desired_speed;
real_time = std::abs(desired_speed - 1.0) < 1e-6;
}
}
@@ -385,15 +604,14 @@ int main(int argc, char** argv) {
audio.set_speed(current_speed);
const float wanted_throttle = throttle_held ? throttle_target : 0.0f;
if (wanted_throttle == 0.0f) throttle_current = 0.0f;
else throttle_current += (wanted_throttle - throttle_current) * (1.0f - std::exp(-throttle_lerp_rate * frame_dt));
throttle_current = wanted_throttle; // ECU actuator evolves at the simulation sample rate.
brake_current += (brake_target - brake_current) * (1.0f - std::exp(-throttle_lerp_rate * frame_dt));
const float wanted_clutch = clutch_held ? 1.0f : 0.0f;
clutch_current += (wanted_clutch - clutch_current) * (1.0f - std::exp(-throttle_lerp_rate * frame_dt));
scenario.set_controls(throttle_current, brake_current, clutch_current);
draw_scene(window, font_ptr, scenario.snapshot(), throttle_target, brake_current,
current_speed, ring.available_samples(), ring_capacity, time_warp, dyno_curve);
current_speed, ring.available_samples(), ring_capacity, time_warp, dyno_curve, audio.underrun_blocks());
}
simulation.request_stop();