Add turbocharged twin scenario and cross-platform test harness
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user