Add American V8 preset, carb fuel transport, and adaptive desktop audio

This commit is contained in:
2026-10-23 12:43:57 +02:00
parent 7a5c77c1c8
commit 85d4fb828a
36 changed files with 805 additions and 144 deletions

View File

@@ -37,6 +37,7 @@ public:
p.owner_energy = &energy_;
p.owner_air_mass = &air_mass_;
p.owner_exhaust_mass = &exhaust_mass_;
p.owner_fuel_mass = &fuel_mass_;
sync_port(p);
return p;
}
@@ -44,19 +45,30 @@ public:
float density() const { return mass() / std::max(volume_, 1e-12f); }
float temperature() const { return pressure() / std::max(density() * gas_constant, 1e-9f); }
float volume() const { return volume_; }
float mass() const { return air_mass_ + exhaust_mass_; }
float mass() const { return air_mass_ + exhaust_mass_ + fuel_mass_; }
float internal_energy() const { return energy_; }
float air_fraction() const { return air_mass_ / std::max(mass(), 1e-12f); }
float fuel_fraction() const { return fuel_mass_ / std::max(mass(), 1e-12f); }
float fuel_mass() const { return fuel_mass_; }
void add_fuel_mass(float value, float temperature = 300.0f) {
if (!std::isfinite(value) || value < 0.0f || !std::isfinite(temperature) || temperature <= 0.0f)
throw std::invalid_argument("invalid fuel addition");
fuel_mass_ += value;
energy_ += value * gas_constant / (gamma - 1.0f) * temperature;
sync_ports();
}
void set_pressure(float value, float temp = -1.0f) {
if (!std::isfinite(value) || value <= 0 || !std::isfinite(temp))
throw std::invalid_argument("invalid volume pressure/temperature");
const float target_t = temp > 0.0f ? temp : temperature();
const float total = value * volume_ / (gas_constant * target_t);
const float fraction = air_fraction();
const float fuel = fuel_fraction();
air_mass_ = total * fraction;
exhaust_mass_ = total * (1.0f - fraction);
fuel_mass_ = total * fuel;
exhaust_mass_ = total * std::max(1.0f - fraction - fuel, 0.0f);
energy_ = value * volume_ / (gamma - 1.0f);
air_roundoff_ = exhaust_roundoff_ = energy_roundoff_ = 0;
air_roundoff_ = exhaust_roundoff_ = fuel_roundoff_ = energy_roundoff_ = 0;
sync_ports();
}
void update(float dt) {
@@ -64,16 +76,20 @@ public:
throw std::invalid_argument("invalid volume timestep");
if (dt == 0)
return;
double d_air = 0, d_exhaust = 0, d_energy = 0;
double d_air = 0, d_exhaust = 0, d_fuel = 0, d_energy = 0;
const float fraction = air_fraction();
const float fuel = fuel_fraction();
for (const Port &p : ports_) {
const float flow_fraction = p.mass_flow >= 0.0f ? p.air_fraction : fraction;
const float flow_fuel = p.mass_flow >= 0.0f ? p.fuel_fraction : fuel;
d_air += double(p.mass_flow) * flow_fraction;
d_exhaust += double(p.mass_flow) * (1.0f - flow_fraction);
d_fuel += double(p.mass_flow) * flow_fuel;
d_exhaust += double(p.mass_flow) * std::max(1.0f - flow_fraction - flow_fuel, 0.0f);
d_energy += double(p.mass_flow) * p.enthalpy;
}
accumulate(air_mass_, air_roundoff_, d_air * dt);
accumulate(exhaust_mass_, exhaust_roundoff_, d_exhaust * dt);
accumulate(fuel_mass_, fuel_roundoff_, d_fuel * dt);
accumulate(energy_, energy_roundoff_, d_energy * dt);
const float new_volume = volume_ + volume_change_rate * dt;
if (!(new_volume > 0) || !std::isfinite(new_volume))
@@ -93,10 +109,11 @@ public:
if (!(mass() > 0) || !(energy_ > 0) || !std::isfinite(mass()) || !std::isfinite(energy_))
throw std::runtime_error(
"nonphysical volume state; reduce timestep or check external port fluxes");
if (air_mass_ < -1e-12f || exhaust_mass_ < -1e-12f)
if (air_mass_ < -1e-12f || exhaust_mass_ < -1e-12f || fuel_mass_ < -1e-12f)
throw std::runtime_error("negative volume species mass");
air_mass_ = std::max(air_mass_, 0.0f);
exhaust_mass_ = std::max(exhaust_mass_, 0.0f);
fuel_mass_ = std::max(fuel_mass_, 0.0f);
sync_ports();
}
void update_state(float dt) override { update(dt); }
@@ -110,9 +127,9 @@ public:
const std::deque<Port> &ports() const { return ports_; }
private:
float volume_, air_mass_{}, exhaust_mass_{}, energy_;
float volume_, air_mass_{}, exhaust_mass_{}, fuel_mass_{}, energy_;
// Preserve sub-ulp transfers without changing the float port/budget ABI.
double air_roundoff_{}, exhaust_roundoff_{}, energy_roundoff_{};
double air_roundoff_{}, exhaust_roundoff_{}, fuel_roundoff_{}, energy_roundoff_{};
static void accumulate(float& value, double& roundoff, double increment) {
const double precise = double(value)+roundoff+increment;
value = float(precise);
@@ -127,6 +144,7 @@ private:
p.temperature = temperature();
p.enthalpy = gamma / (gamma - 1.0f) * p.pressure / std::max(p.density, 1e-12f);
p.air_fraction = air_fraction();
p.fuel_fraction = fuel_fraction();
}
void sync_ports() {
for (Port &p : ports_)
@@ -138,16 +156,18 @@ struct GasCell {
float rho{1.225f}, momentum{};
float energy{1.225f * (101325.0f / (pipe_gm1 * 1.225f))};
float air{1.0f};
float fuel{};
};
struct GasState {
float rho{1.225f}, velocity{}, pressure{101325.0f}, air{1.0f};
float fuel{};
};
class Pipe1D {
public:
// Integrated face fluxes: kg/s, N, W, kg/s of fresh air, positive along +x.
// Integrated face fluxes: kg/s, N, W, kg/s of fresh air and fuel, positive along +x.
struct Flux {
float m{}, p{}, e{}, y{};
float m{}, p{}, e{}, y{}, fuel{};
};
float area{}, dx{};
float damping_multiplier{0.0f}, energy_relaxation_rate{0.0f}, ambient_pressure{101325.0f};
@@ -221,7 +241,9 @@ public:
}
static GasState primitive(const GasCell &c) {
const float u = c.momentum / std::max(c.rho, 1e-12f);
return {c.rho, u, pipe_gm1 * (c.energy - .5f * c.momentum * u), std::clamp(c.air, 0.0f, 1.0f)};
const float air = std::clamp(c.air, 0.0f, 1.0f);
return {c.rho, u, pipe_gm1 * (c.energy - .5f * c.momentum * u), air,
std::clamp(c.fuel, 0.0f, 1.0f - air)};
}
static float wave(const GasState &s) {
return std::abs(s.velocity) + std::sqrt(gamma * s.pressure / s.rho);
@@ -253,7 +275,9 @@ public:
float mom = cells[i].momentum - factor * (fr.p - fl.p - states_[i].pressure * (ar - al));
float en = cells[i].energy - factor * (fr.e - fl.e);
const float air = cells[i].rho * cells[i].air - factor * (fr.y - fl.y);
if (!std::isfinite(rho) || !std::isfinite(mom) || !std::isfinite(en) || !std::isfinite(air))
const float fuel = cells[i].rho * cells[i].fuel - factor * (fr.fuel - fl.fuel);
if (!std::isfinite(rho) || !std::isfinite(mom) || !std::isfinite(en) ||
!std::isfinite(air) || !std::isfinite(fuel))
throw std::runtime_error("nonfinite finite-volume update");
if (rho < 1e-12f) {
rho = 1e-12f;
@@ -273,14 +297,18 @@ public:
// than 1.18e-38 of the gas mass. Avoid repeatedly transporting
// subnormal concentrations; total gas mass and energy are unchanged.
const float fraction = std::clamp(air / rho, 0.0f, 1.0f);
const float fuel_fraction = std::clamp(fuel / rho, 0.0f, 1.0f - fraction);
next_cells_[i] = {rho, mom, en,
fraction < std::numeric_limits<float>::min() ? 0.0f : fraction};
fraction < std::numeric_limits<float>::min() ? 0.0f : fraction,
fuel_fraction < std::numeric_limits<float>::min() ? 0.0f : fuel_fraction};
}
cells.swap(next_cells_);
prepared_ = false;
left_override_ = right_override_ = false;
}
static Flux scaled(Flux f, float a) { return {f.m * a, f.p * a, f.e * a, f.y * a}; }
static Flux scaled(Flux f, float a) {
return {f.m * a, f.p * a, f.e * a, f.y * a, f.fuel * a};
}
private:
std::vector<GasCell> next_cells_;
@@ -301,7 +329,8 @@ private:
return {.5f * (ma + mb - c * (b.rho - a.rho)),
.5f * (ma * a.velocity + a.pressure + mb * b.velocity + b.pressure - c * (mb - ma)),
.5f * ((ea + a.pressure) * a.velocity + (eb + b.pressure) * b.velocity - c * (eb - ea)),
.5f * (ma * a.air + mb * b.air - c * (b.rho * b.air - a.rho * a.air))};
.5f * (ma * a.air + mb * b.air - c * (b.rho * b.air - a.rho * a.air)),
.5f * (ma * a.fuel + mb * b.fuel - c * (b.rho * b.fuel - a.rho * a.fuel))};
}
};
@@ -329,7 +358,8 @@ public:
[&](const Budget &b) { return b.air == port.owner_air_mass; });
if (it == budgets_.end()) {
o.budget = budgets_.size();
budgets_.push_back({port.owner_air_mass, port.owner_exhaust_mass, port.owner_energy});
budgets_.push_back({port.owner_air_mass, port.owner_exhaust_mass,
port.owner_fuel_mass, port.owner_energy});
} else
o.budget = static_cast<std::size_t>(it - budgets_.begin());
}
@@ -385,7 +415,7 @@ public:
if (!std::isfinite(dt) || dt <= 0)
throw std::invalid_argument("boundary timestep must be positive");
for (auto &b : budgets_) {
b.mass = .25f * (*b.air + (b.exhaust ? *b.exhaust : 0));
b.mass = .25f * (*b.air + (b.exhaust ? *b.exhaust : 0) + (b.fuel ? *b.fuel : 0));
b.heat = b.energy ? .25f * (*b.energy) : std::numeric_limits<float>::max();
}
for (auto &o : orifices_)
@@ -398,7 +428,7 @@ public:
private:
struct Budget {
float *air{}, *exhaust{}, *energy{};
float *air{}, *exhaust{}, *fuel{}, *energy{};
float mass{}, heat{};
};
struct Orifice {
@@ -587,15 +617,18 @@ private:
o.length > 0 ? .5f * (v.pressure + s.pressure) : static_cast<float>(duct_pressure);
const float direction = o.left ? 1.f : -1.f;
const float species = from ? v.air_fraction : s.air;
const float fuel = from ? v.fuel_fraction : s.fuel;
const float signed_mass = direction * mdot;
p.set_boundary_flux(o.left, {signed_mass,
mdot * velocity + face_pressure * area + s.pressure * (face_area - area),
signed_mass * h, signed_mass * species});
ghost(p, o.left, {density, direction * velocity, face_pressure, species});
signed_mass * h, signed_mass * species, signed_mass * fuel});
ghost(p, o.left, {density, direction * velocity, face_pressure, species, fuel});
v.mass_flow = -mdot;
v.enthalpy = h;
if (!from)
v.air_fraction = s.air;
if (!from)
v.fuel_fraction = s.fuel;
}
void resolve_open(Open &o) {
auto &p = pipes_[o.pipe];
@@ -611,7 +644,9 @@ private:
const float u =
o.left ? s.velocity - 2 * c / gm1 + 2 * cb / gm1 : s.velocity + 2 * c / gm1 - 2 * cb / gm1;
const bool out = o.left ? s.velocity <= -c : s.velocity >= c;
GasState g = out ? s : GasState{rho, u, o.pressure, (o.left ? u >= 0 : u <= 0) ? 1.f : s.air};
const bool ambient_inflow = o.left ? u >= 0 : u <= 0;
GasState g = out ? s : GasState{rho, u, o.pressure, ambient_inflow ? 1.f : s.air,
ambient_inflow ? 0.f : s.fuel};
ghost(p, o.left, g);
const float a = std::min(o.area, p.cell_area(i));
auto f = Pipe1D::scaled(o.left ? Pipe1D::riemann(g, s) : Pipe1D::riemann(s, g), a);
@@ -631,9 +666,11 @@ private:
const float area = std::min(a.cell_area(ia), b.cell_area(ib));
auto f = Pipe1D::scaled(Pipe1D::riemann(sa, sb), area);
a.set_boundary_flux(o.left_a,
{da * f.m, f.p + sa.pressure * (a.cell_area(ia) - area), da * f.e, da * f.y});
{da * f.m, f.p + sa.pressure * (a.cell_area(ia) - area), da * f.e,
da * f.y, da * f.fuel});
b.set_boundary_flux(o.left_b,
{db * f.m, f.p + sb.pressure * (b.cell_area(ib) - area), db * f.e, db * f.y});
{db * f.m, f.p + sb.pressure * (b.cell_area(ib) - area), db * f.e,
db * f.y, db * f.fuel});
}
};