Files
FluidSimCpp/README.md

787 lines
45 KiB
Markdown

# FluidSimCpp
Solver performance measurements and reproducible SIMD/threading experiments are
documented in [the optimization audit](verification/OPTIMIZATION.md). Current
scenarios use serial pipe execution because per-substep threading costs more than
it saves at their graph sizes. Portable compiler SIMD remains enabled by default.
FluidSimCpp aims to reproduce sound from a physically accurate simulation of
compressible flow in pipes and connected volumes, sampled at 44,100 Hz. Each
audio sample advances physical time by 1/44100 second; adaptive internal
substeps resolve the fluid dynamics. Every subsystem must ultimately follow
physical laws with explicit units, assumptions and validation. Matching the
C# reference is a regression aid, not proof of physical accuracy.
This is a work in progress. Physical correctness is the acceptance goal, not
a claim about all current models. Validation must include conservation of
mass, momentum and energy, analytic acoustic modes, boundary reflection and
radiation, thermodynamics and convergence under time/space refinement.
## Build and run (Windows and Linux)
The desktop now includes a [3D machine builder](docs/MACHINE_BUILDER.md). Open
**Machine workshop** in the scenario menu to place and configure pipes, tanks,
valves, engine cylinders and piston pumps. Saved machines support **Edit** and
**Play**, and **Tab** switches between building and testing.
The picker includes 3D previews, carburetors and ECUs. **Import engine layout**
opens editable copies of the 450 single, twin, flat six and V8. Shift selects groups;
blue pins carry ECU control wires, and ideal junctions accept multiple pipes at
one point. Engine cylinders require an explicit connected fuel source.
**Ctrl+C / Ctrl+V** copies and pastes selected groups with their internal air and
ECU connections. **Assemblies** saves selected groups for reuse across machines.
Escape cancels tools or selection; use **Menu** to leave the workshop.
**Meters (M)** adds pressure, temperature, flow, AFR/lambda and shaft instruments
with live traces and CSV export. Test playback slows to 1% speed and supports
paused 10 ms steps. Carb/ECU and other safe settings can be tuned live; geometry
and compiled settings stay locked until returning to Build.
The **Mech** category adds explicit shafts, flywheels, drive motors and brakes.
Cylinders/pumps connect to named shafts; independent engines can run at different
speeds. Try **Shaft, flywheel and brake bench** from saved machines. Mechanical-only
machines need no gas pipes. Older machine saves migrate in memory, and new saves
use version 10 with stable ports, mechanical parameters, sensor attachments,
a separate typed signal graph and explicit sensor-ECU/injector bindings.
The **Sense** inspector adds pressure/MAP, differential pressure, gas temperature,
total pipe mass flow, crank speed/angle and throttle-position pickups. Sensor
channels sample independently of recorded instruments and show validity/time;
deleting a host leaves an unresolved binding. Right-click **Add sensor here**
places a tap at the clicked pipe location.
The saved **Automatic air supply / logic bench** runs pressure feedback through
hysteresis, a master switch and an AND gate to a compressor drive command. Choose
**Logic** to edit its graph or **Split** to view signals beside the 3D machine.
The node library supports search, typed click/drag wiring and live controls;
Boolean wires are red, Number green, with ring inputs and filled outputs.
ECUs now offer **Crank ignition**, **Alpha-N EFI** and **Speed-density EFI** presets alongside the
labeled legacy controller. Their grouped Logic connectors require physical crank
speed/position, plus actual TPS for Alpha-N or absolute MAP/IAT for speed-density.
Intake chambers accept explicit fuel
injector attachments with calibrated capacity and finite/ideal rail supply.
Try `machines/sensor-ignition-bench.machine.toml` and
`machines/sensor-alpha-n-bench.machine.toml` and
`machines/sensor-speed-density-bench.machine.toml`; all have working fuel transport
and combustion. See the [workshop guide](docs/MACHINE_BUILDER.md) for calibration
and the current batch-delivery model.
Launch directly with `fluidsim_desktop --builder`, edit a saved design with
`--machine machines/vacuum-pump.machine.toml`, or run it with
`--play-machine machines/vacuum-pump.machine.toml`.
Both platforms use CMake 3.28 or newer, Git, and a C++20 compiler. The presets
build with optimizations and debugging symbols (`RelWithDebInfo`). The first
desktop configure fetches SFML 3.0.2 if it is not installed and needs network
access. Visual Studio is not required.
### Windows
Install CMake and Git and ensure both are on PATH. Then run from this folder
in a regular PowerShell terminal:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\Setup-Windows.ps1
cmake --preset windows
cmake --build --preset windows
ctest --preset windows
.\out\build\windows-gcc\fluidsim_desktop.exe
```
The setup script installs the portable [WinLibs](https://winlibs.com/) x64 UCRT
GCC bundle, including GDB and Ninja, in
`%LOCALAPPDATA%/FluidSimTools/mingw64`. It verifies the download's SHA-256
checksum, removes the downloaded archive, and reuses an existing installation.
It needs no administrator access and does not change your system PATH.
Each built executable gets its GCC runtime DLLs copied beside it so it also
runs directly from a terminal or Explorer.
### Linux
Install CMake 3.28 or newer, GCC with C++20 support, Make, GDB, and the SFML
development dependencies. For Ubuntu 24.04 / Debian with a suitable CMake:
```sh
sudo apt-get update
sudo apt-get install build-essential cmake git gdb pkg-config \
libx11-dev libxrandr-dev libxcursor-dev libxi-dev libudev-dev \
libgl1-mesa-dev libfreetype6-dev libflac-dev libogg-dev libvorbis-dev \
fonts-dejavu-core
cmake --preset linux
cmake --build --preset linux
ctest --preset linux
./build-desktop/fluidsim_desktop
```
Other distributions need equivalent packages; see
[SFML's source-build dependencies](https://www.sfml-dev.org/tutorials/3.0/getting-started/build-from-source/).
Running the desktop app requires a graphical session and an audio device.
### VS Code
Install the recommended **C/C++** and **CMake Tools** extensions and open this
folder. Select the `windows` or `linux` CMake preset for your platform. Choose
**FluidSim desktop**, **FluidSim two-stroke**, or **FluidSim Saab B205** in
**Run and Debug**, then press F5 or the green Run button. The launch
configurations select the platform's executable and GDB automatically; the
build task configures and builds the desktop target before launch.
Ctrl+F5 runs without debugging; Ctrl+Shift+B builds the desktop target only.
The command-line builds above also build the CLI, benchmarks, and tests.
The UI uses `../fonts/FiraCodeNerdFont-Medium.ttf` when available, then tries
installed Windows or Linux system fonts. To bundle another font, add
`-DFLUIDSIM_FONT_FILE=/path/to/font.ttf` to the configure command. For a
headless build, add `-DFLUIDSIM_BUILD_DESKTOP=OFF`; this skips SFML and the
desktop's graphics/audio dependencies.
Controls: W/S raise/lower the pedal over 0.5 seconds, C disengages the clutch,
V applies starter torque while held,
A/D shift, Z selects neutral, and left/right adjust engine load. The on-screen
pedal is direct and springs back on release; zero pedal retains ECU idle control.
Space toggles real-time and manual playback. In manual mode, Shift+scroll or
Page Up/Down changes speed; ordinary scrolling zooms the view. Rendering uses
display VSync independently of simulation speed, including 0.01x slow motion.
The command-line runner is useful for repeatable checks:
```powershell
.\out\build\windows-gcc\fluidsim_cpp.exe --seconds 1 --throttle 0.35
.\out\build\windows-gcc\fluidsim_cpp.exe --scenario two-stroke --seconds 1 --throttle 0.3
.\out\build\windows-gcc\fluidsim_cpp.exe --scenario helmholtz --seconds 1
.\out\build\windows-gcc\fluidsim_cpp.exe --scenario pulse --seconds 1
```
On Linux, use `./build-desktop/fluidsim_cpp` with the same arguments.
To compare deterministic checkpoints from the active C# twin scenario against
the C++ port (energy variation is disabled for repeatability), build the C++
trace runner and execute the PowerShell comparison script:
```powershell
cmake --build build-msvc --target fluidsim_parity_trace
.\verification\Compare-ScenarioTrace.ps1
```
## Audio fidelity and playback
Desktop playback starts at real time: one solver output frame per 44.1 kHz
stereo device frame. A simulation worker fills a bounded 8192-frame queue in
up to 128-frame batches with a 4096-frame startup reserve (about 93 ms). Batches
shrink with playback speed so 0.01x does not advance in large bursts. The device
callback only reads samples, removes DC, converts to PCM and handles underruns;
it performs no simulation, allocation or waiting. An underrun fades out and
recovery crossfades over 64 samples rather than stepping abruptly to silence.
The callback never advances physical time to conceal missing samples.
The primary source is a linear compact-monopole approximation,
`p(r,t) = rho_ambient * (d volume_flow/dt)/(4*pi*r)`. Mouth volume flow
comes from the boundary face velocity and area. Fluid conservation retains the
actual mass flux; hot/cold density changes are excluded from the acoustic source.
Intake and exhaust sources sit at the open ends of the same world routes used
to draw the pipes, including both outlets of dual exhausts. The desktop camera
is the listener: orbit, pan and zoom change its position and ear orientation.
Left and right ears are 18 cm apart, with separate fractional-sample travel
delays at 343 m/s and inverse-distance spreading. Camera updates smooth over
20 ms while retaining propagation history and reflection filter state.
Playback calibration is 200 Pa per device full scale;
this is not calibrated speaker SPL. A second-order 6 kHz monitoring filter
reduces sharp sample-to-sample transients without saturation or synthetic bangs.
A roughly 3.5 Hz device DC blocker and final PCM clipping protection remain.
Use 1x playback for acoustic evaluation. Toggle **G** off to hear the full
distance-related loudness change; automatic gain otherwise compensates for it.
The default environment is a nearby wall. Press **R** to cycle dry, field,
wall, woodland, and tunnel. Ground/wall image paths provide early reflections;
the live eight-line feedback-delay network supplies a scene-dependent late
tail. Early reflections follow the listener geometry, with plane reflections
disabled when the listener crosses to the other side. The diffuse tail receives
a delayed centre signal rather than summing the ears, avoiding ear-delay comb
filtering in the reverb input. Woodland scatterers and the hard-surface tunnel
images are approximate.
These are listening profiles, not validated acoustics. Mechanical sounds are
not implemented.
Open pipe mouths now use a causal unflanged radiation boundary with frequency-
dependent reflection and end correction. Intake mouths draw fresh ambient gas
directly, replacing the fixed atmosphere component and its inlet orifice.
The model assumes plane waves and low mean flow; strong jet and vortex effects
remain unmodeled. The compact source does not implement validated directivity
or exhaust afterfire chemistry. Filtering cannot recover aliased
source information. See [audio verification](verification/AUDIO_RENDERING.md)
for capture commands, current renderer details and limitations.
Use the **Cells/m - / +** control in the sidebar to change the active scenario's
pipe resolution. Changes rebuild the mesh in the background and restart the
engine, retaining solver, listener, scene and driving-control inputs. Values are
remembered separately per scenario until the app closes. Set persistent defaults
in each engine TOML's `[simulation] cells_per_meter`. The desktop engine editor
has been removed; edit engine calibration in TOML.
Launching `fluidsim_desktop` opens a maximized home screen with **Machine
Workshop**, **Engine Scenarios**, and **Quit**. Workshop offers a new design or
saved machines with separate Edit/Play actions. Engine Scenarios opens the engine
collection with category filters, search, a preview and a Start button. Click a
row to select it, then start with the button or Enter; Up/Down also selects rows.
Escape returns from the scenario to the menu, stopping its simulation and
audio so another scenario can be selected. Escape returns from a browser to the
home screen, and closes the app from Home. Menu, workshop and scenario entry all
maximize the decorated Windows window, retaining its normal restore controls.
Simulation and audio start when an engine is launched. Passing
`--scenario twin`, `--scenario two-stroke`, `--scenario b205`, or `--scenario v8`
bypasses the menu; `--scene` and `--exhaust` options also apply
when choosing through the menu.
To test the two desktop engines from the repository root:
```sh
cmake --build build-desktop --target fluidsim_desktop -j 4
./build-desktop/fluidsim_desktop --scenario two-stroke --scene wall
# Or:
./build-desktop/fluidsim_desktop --scenario twin --scene field
```
Hold **W** to raise the pedal and **S** to lower it. Releasing the keyboard
returns the pedal over 0.5 seconds. The UI lever is direct and springs back. Use
**Space** to toggle real-time/manual playback and **R** to change environment.
In manual mode use **Shift+scroll** or **Page Up/Down** to adjust speed;
ordinary scrolling continues to zoom the view. The two-stroke desktop uses
the existing 125 cc configuration; its dimensions and calibrations must be
adapted before it represents a particular engine. Both scenarios use the same
motorcycle test model, dashboard and rolling peak dyno graph (kW and Nm versus
RPM). Start in neutral, hold **C** to disengage the clutch, press **D** for first
gear, apply throttle and release **C** to pull away. **D/A** shift up/down;
**Z** selects neutral. Left/right adjust an external engine load, not a wheel
brake. The graph records simulated crank power/torque, not measured wheel power.
The two-stroke now uses a YZ125 baseline: published 2022 primary, gearbox and
final reductions, 54 x 54.5 mm geometry, and an estimated racing expansion
chamber. Ports use piston-uncovered window areas. The pipe display follows
physical cell diameter and length. Compare exhausts with `--exhaust racing`
(default) or `--exhaust straight`; the desktop, CLI and WAV capture tool accept
this option. See [YZ125 verification](verification/YZ125_BASELINE.md) for the
geometry, assumptions, speed sweep and remaining unloaded-RPM limitation.
## 1980s American V8
Choose **1980s American 5.0 V8** in the desktop menu, or run:
```sh
./build-desktop/fluidsim_desktop --scenario v8 --scene field
```
This is an estimated naturally aspirated 302-class cross-plane V8: eight
101.6 x 76.2 mm cylinders, 7:1 compression, mild pushrod-style valve timing,
1-5-4-2-6-3-7-8 firing order, 750 RPM idle target and 7000 RPM limiter
(6750 RPM recovery). Crank/flywheel inertia is 1.1 kg m², versus the original
.28. Eight unequal .60--.78 m headers feed two independent 1.5-litre bank
collectors: cylinders 1--4 and 5--8. Each collector has its own 50 mm outlet
pipe (1.8 / 1.95 m), and each outlet has a separate spatial audio emitter. A
nominal 3000 kg five-speed car uses the existing clutch/gear controls.
The V8 clutch has 1100 Nm capacity to shorten launch slip with the heavy
flywheel and car; engagement remains progressive rather than forcibly locking.
Final drive is approximately 3.624:1 (6.16 / 1.7), making gearing 1.7x taller
than the previous setup: 1.7x road speed at the same RPM and selected gear.
Throttle opening uses a 65 ms response and 2.3 travels/second limit; closing
uses 160 ms and 1.4 travels/second for a slower release.
The V8 uses the shared ideal EFI fuel dose with ECU throttle/idle control,
ignition, overrun fuel cut and rev limiting. Its earlier carb setup has been
removed. Independent exhaust banks retain their uneven cross-plane pulse
spacing rather than blending both banks in a shared collector. Numerical
diffusion in the pipe solver and audio filtering still limit high-frequency rasp.
Dimensions and ignition/cam calibration are design estimates;
the output is not a validated period engine dyno curve.
The intake uses one 70 mm inlet duct, one 65 mm throttle body and a shared
4-litre plenum feeding eight 38 mm diameter, 12 cm runners (previously 30 cm).
The revised mesh has 19 pipes and 190 cells, down from 310. Runner cells are
6 cm long and exhaust cells approximately 5--6.5 cm long. This coarsening trades
some high-frequency resolution for more real-time headroom.
The earlier lift-off slowdown came from the CFL timestep falling below the
22.7 microsecond audio interval: the whole fluid network advanced almost twice
per sample, doubling boundary solves and cell updates. The revised 16-second
rev/coast benchmark stayed at one substep per sample and took .51--.57 wall
seconds per simulated second here, including coast-down (previously up to
1.28). It returned to 759 RPM with finite audio and zero pipe positivity
repairs. The CFL limit remains enforced; other operating points may still
require subdivisions. The desktop retains the 4x playback gain.
The V8 uses the same per-pipe resolution policy as other scenarios. Uniform
mesh benchmarks found that 35 cells/m was near real time under one pedal
workload and 33 cells/m was near real time at steady 4500 RPM on the tested
machine; denser uniform meshes ran slower than real time. The existing
per-pipe mesh is faster and remains in use. See [V8 performance notes](verification/V8_PIPE_PERFORMANCE.md)
for measurements and reproduction commands.
Benchmark or capture with:
```sh
./build-desktop/fluidsim_scenario_benchmark 8 transient v8
./build-desktop/fluidsim_scenario_benchmark 20 coast v8
./build-desktop/fluidsim_audio_capture --scenario v8 --seconds 6 --throttle .3 --out /tmp/v8
```
## Saab B205 / 9-5 wagon baseline
The Saab menu entry models the naturally aspirated B205 baseline.
Select **Saab B205 / 9-5 wagon** from the desktop menu, or launch directly:
```sh
./build-desktop/fluidsim_desktop --scenario b205 --scene field
./build-audit/fluidsim_cpp --scenario b205 --seconds 4 --throttle 0.5
```
The shared four-stroke scenario now supports two or four cylinders. The B205
uses 90 x 78 mm geometry, 8.8:1 compression, two intake/two exhaust valves per
cylinder and 1-3-4-2 firing order. Four intake runners and four exhaust headers
connect shared plenums and a tailpipe. The vehicle approximates a 9-5 wagon:
1675 kg with driver, five-speed manual, FM55 B05 overall ratios
13.70 / 7.12 / 4.77 / 3.62 / 2.67, and 4.05 final drive. No sixth gear is offered.
Controls and dyno match the other scenarios. Pipe/cam dimensions,
friction, idle/ignition maps, clutch, drag and loaded tire radius are estimates.
See [B205 verification](verification/B205_BASELINE.md) for sources and checks.
## Current port status
The C++ core now includes the shared `EngineCylinder` model with four-stroke and
two-stroke cycle implementations, crankshaft, crankcase, atmosphere and
configurable 0-D volume components (including thermal relaxation and moving
volume work), profiled compressible 1-D pipes and boundaries, adaptive solver
stepping, the impulse-clutch motorcycle/road-load model, and the active 600 cc
twin topology. Audio includes the flow-derivative source, spatialized
intake/exhaust radiation, outdoor feedback-delay reverb, a lock-free simulation
audio buffer, a `SoundEngine` lifecycle wrapper,
and an SFML desktop frontend. The shared 50-RPM-bin dyno history and graph are
also ported. A crankcase-scavenged 125 cc two-stroke scenario is available in
the core and desktop, with an ECU, finite-response throttle and dynamic reed.
This is still an in-progress port, not a claimed complete replacement. All four
current C# scenarios (600 cc twin, 125 cc two-stroke, Helmholtz resonator, and
pressure pulse) have headless C++ counterparts and can be selected by the CLI.
Remaining parity work includes completing the C# drawing/diagnostic controls in
the desktop UI and resolving numerical differences reported by the comparison
harness. Earlier Windows builds passed CTest, but the current audio and two-stroke
changes still need Windows runtime verification. The Linux desktop builds and
runs; installed-font fallbacks provide labels when no bundled font is present.
The one-second twin trace remains substantially outside its 1% tolerance. The
latest comparison's largest relative difference is 40.6745 for cylinder 1
pressure at checkpoint 33075 (69.8 kPa in C# versus 2.91 MPa in C++). A dense
intake trace matches closely at first, then the C# plenum pressure begins
rising at checkpoint 63 while C++ remains near ambient. This divergence needs
to be resolved before claiming numerical parity. CTest covers cylinder
geometry and valve timing, dyno-bin interpolation, configurable 0-D volume
thermodynamics, crankcase and atmosphere state, profiled-pipe geometry,
inertive and closed boundaries, gearing and clutch momentum transfer, finite
runs for every scenario, and finite audio/reverb output.
## Fuel and throttle control
Desktop playback has automatic gain disabled by default for every scenario.
Press **G** to toggle it; the dashboard shows its state. It tracks stereo RMS
and peaks after DC removal, lifting quiet idle audio gradually (1.5-second
gain-rise time constant) and reducing gain faster for loud revs (30 ms).
Extra gain is capped at 12x above the existing fixed 4x playback gain, with
linked stereo peak protection. Silence does not change the engine controls and
underruns freeze the detector. Disabling smoothly returns to fixed playback
gain. Simulation pressures and WAV capture levels are unaffected.
Both desktop engines use `Ecu` (`include/FluidSim/Ecu.hpp`). The desktop and
CLI throttle values are pedal requests; the actual throttle is advanced every
simulation sample, independently of rendering. Opening and closing use an
1 ms first-order throttle-body response capped at 1000 full travels per second.
The two-stroke carb slide uses 0.2 ms and 5000 full travels per second.
Only the keyboard pedal request ramps over 0.5 seconds; the UI slider is direct.
Physical fuel-film and mixture transport dynamics remain in the carburetor. The four-stroke uses an ideal butterfly projection; the
two-stroke uses the circular-segment aperture uncovered by a translating slide.
Shaft obstruction and edge leakage are not modeled.
The four-strokes use ideal sensors and direct access to trapped fresh-air mass
at intake-valve closure to calculate each cylinder's fuel dose. This explicitly
permitted idealization replaces injector flow/duty-cycle and fuel-film models.
Their nominal AFR is 14.7, enriched progressively toward 12.8 above 70% throttle.
A PI idle controller targets 1800 RPM with filtered RPM feedback, bounded idle
authority and anti-windup. Overrun fuel cut engages above 3500 RPM on a closed
pedal and restores fuel below 2400 RPM to catch idle. The twin's rev limiter cuts fuel
and spark at 10000 RPM, recovering below 9750 RPM.
The two-stroke uses a simulated 38.1 mm slide carburetor instead of the EFI
dose. Effective pilot, needle and main restrictions draw liquid fuel from a
finite float bowl with the orifice relation `m_dot = Cd*A*sqrt(2*rho*delta_p)`.
The jet pressure signal comes from simulated mass flow, local venturi dynamic
pressure and the measured static pressure drop. A wall film deposits and
evaporates fuel during transients. Delivered fuel is added downstream of the
carb as a conserved species and travels through the plenum, reed, crankcase,
transfer duct and cylinder. Combustion is therefore limited by the fuel and
oxygen actually trapped, rather than receiving a cylinder-side AFR dose.
The two-stroke has no rev limiter and deliberately does not use electronic
overrun fuel cut. On a high-RPM throttle lift, wet-wall and pilot fuel can pass
through unlit cycles if the trapped mixture cannot ignite. Spark remains at
the CDI timing; there is no scheduled overrun firing pattern. Unburned fuel
is transported out through the exhaust without pipe combustion. No sound
samples are mixed in.
All ECU parameters are accessible calibrations, not measured engine maps.
Holding V applies configurable constant starter torque at the crankshaft in every
scenario; release or loss of window focus disengages it. This is a torque source,
without battery, motor electrical, or starter gearing models. There is no
automatic restart, knock sensor, lambda sensor,
catalyst, cold-start temperature model or injector electrical model. Idle
control can still stall under excessive load and does not impose crankshaft
speed or add artificial sustaining torque. Existing combustion/thermal models
remain approximations; ECU changes do not establish their physical accuracy.
The two-stroke uses a fixed mechanical carb slide stop. With the pedal released,
the ECU trims ignition timing around the configured idle RPM instead of moving
the carb stop. Each carb scenario config sets
`TwoStrokeConfig::idle_slide_opening` (normalized 0..1); the catalog 125 cc
scenarios now use a calibrated 3% stop. Pedal demand above that stop opens the
slide further. The resulting idle RPM depends on the simulation and load. Its
reed is a damped lumped cantilever model with finite opening time and seating
contacts. Cylinder and crankcase moving-volume work use adiabatic energy changes.
Jet dimensions are effective flow calibrations rather than measured Mikuni
parts. Fuel uses the gas model after wall-film release; droplet size,
atomization, evaporation heat and oil are not modeled. Reed material,
combustion and controller parameters still require calibration.
C# trace parity is intentionally changed by these control and physics changes.
VS Code's default build task builds the desktop through CMake. Use
Ctrl+Shift+B to build, or choose **FluidSim desktop** in Run and Debug and
press F5 to build and run. The single-file C/C++ editor Run shortcut is disabled
because this project requires multiple sources and SFML; IntelliSense gets
include paths from CMake Tools. Open the repository root as the VS Code folder.
## Fluid physics verification and performance
See [the fluid physics audit](verification/FLUID_PHYSICS_AUDIT.md) for corrected
conservation/boundary equations, analytic tests, measured performance and
remaining model limits. The validation executable exercises 29 cases covering
walls, pressure outlets, nozzles, inertance, pipe joints, volumes, species,
acoustic modes, shocks and invalid states. The default coupled solver updates
components on every substep, uses area-weighted shared fluxes and matches
orifice discharge to duct characteristics. These changes intentionally alter
older C# trace comparisons.
Build headless with CMake, then run `ctest --test-dir build-audit
--output-on-failure`. `fluidsim_physics_benchmark` and
`fluidsim_thread_benchmark` reproduce the performance experiments.
`FLUIDSIM_ENABLE_SIMD=OFF` provides a scalar comparison; fast-math is not used.
Production fluid solving remains serial because threading was slower for the
current small pipe graph. SIMD and scalar Linux checks are covered; Windows
execution is still unverified.
The twin intake is supplied by an explicit 300 K atmospheric reservoir rather
than a pressure-only boundary with extrapolated entropy. The repeated-blip
`fluidsim_intake_regression` checks fresh-air supply and pressure/idle recovery.
Manifold vacuum at closed throttle remains part of the simulated pumping cycle.
## Pipe solver comparisons
The desktop accepts `--solver rusanov|hlle|hllc|muscl`. Press **F3** during playback to
cycle Rusanov, HLLE, HLLC and MUSCL; the dashboard shows the current solver. All pipes
switch together between simulation samples. Rusanov remains the startup default.
Changing solver affects the ongoing engine state; allow several cycles and the
audio queue to settle before judging it. Use the same scene and auto-gain setting.
Each method currently uses the same first-order spatial discretization and CFL
limit. HLLC resolves contact transport with less diffusion and uses HLLE when
its intermediate states are invalid. These are solver experiments; sharper
signals do not by themselves prove more accurate combustion or sound.
For repeatable comparisons, WAV capture accepts `--solver`, `--rpm` (an ideal
speed-holding dyno) and `--warmup` in seconds. Capture gains are identical across
solvers and do not include desktop automatic gain. For example:
```sh
./build-desktop/fluidsim_desktop --scenario v8 --scene dry --solver hllc
./build-desktop/fluidsim_desktop --scenario two-stroke --scene dry --solver hllc
./build-desktop/fluidsim_scenario_benchmark 8 loaded v8 0 hllc
./build-desktop/fluidsim_scenario_benchmark 8 loaded two-stroke-racing 0 hllc
./build-desktop/fluidsim_audio_capture --scenario v8 --solver hllc --scene dry --rpm 4500 --warmup 2 --seconds 6 --throttle .7 --out /tmp/v8-hllc
```
The benchmark's fourth argument is the existing optional uniform V8 cells/m
setting (0 keeps the scenario mesh); its fifth argument selects the pipe solver.
See [solver measurements](verification/SOLVER_COMPARISON.md) for results.
## Desktop workspace and 3D view
The desktop has a neutral black/gray workspace: a left telemetry and control
panel, an engine viewport, a dyno panel, and a cylinder inspector. Resize or
maximize the window to give the engine view more space. All desktop engine
presets use the OpenGL 3D renderer by default. Tab switches to the existing
schematic view. Left drag orbits, right/middle drag pans, the wheel zooms, and
Home fits the engine again. Camera gestures are confined to the engine viewport.
The spring-return pedal slider sends a pedal request to the ECU. W/S pedal
travel takes 0.5 seconds in either direction; releasing W returns toward zero
at the same speed. End releases the pedal immediately. Zero pedal lets the ECU
operate its idle throttle opening. Solver cycling is now F3; S lowers the pedal.
Pipes are continuous indexed tube meshes with smooth bends, physical diameters
from the simulated area profile, and per-vertex pressure colors. Route points
are keyed by distance along the simulated pipe, in metres. Moving those drawing
points changes the schematic route, not the fluid solver lengths. The fixed
pressure scale is blue at vacuum, green at 101325 Pa and red at/above 1 MPa.
Volumes and cylinder gas use the same pressure scale. Pistons and rods animate
from snapshot piston positions and crank phases. Geometry is schematic rather
than a dimensionally complete engine CAD model.
Code responsibilities are split into `DesktopUi`/`WorkspaceLayout`, `OrbitCamera`,
`PedalControl`, `PipePath`, `EngineRenderer3D`, `EngineSchematic` and `DynoRenderer`.
The renderer consumes snapshots and reuses meshes; it does not step the solver.
OpenGL is linked through CMake's `OpenGL::GL`. This first renderer uses indexed
client arrays, lighting and a depth buffer in an OpenGL compatibility context,
with SFML's GL state preservation around the viewport pass. It is not a core
profile shader renderer. Windows runtime still needs verification on Windows.
### Saab exhaust silencers
The B205 exhaust now includes a 6 L straight-through resonator and an 18 L
straight-through rear muffler, separated by 0.8 m of pipe and followed by a
radiating tailpipe. Saab uses two minimum fluid substeps per audio sample; the
acoustic CFL condition can request more. These are estimated large
passenger-car silencers, not measured Saab internals. `ExhaustConfig` and
`SilencerConfig` in `include/FluidSim/Silencer.hpp` configure length, nominal body
volume, core diameter, chamber count, baffle passage diameter, linear resistance
(1/s), and quadratic loss coefficient. Pass an `ExhaustConfig` as the third
`FourStrokeScenario` constructor argument; `enabled=false` bypasses the silencers.
Expansion and chambered silencers use resolved variable-area passages. Body
radius is derived from volume and length; tapered transitions reduce actual gas
volume below the nominal cylindrical body volume. Straight-through mode retains
a constant core through the housing and uses a linear resistance surrogate for
packing. It does not yet resolve perforation coupling or porous-material acoustics.
Loss coefficients are tuning inputs, not calibrated predictions from body geometry.
Momentum losses retain total gas energy, converting kinetic energy into heat.
The renderer draws round housings with their configured lengths and diameters.
### N-port junctions and V8 4-2-1 exhaust
`BoundarySystem::add_junction({{pipe_index, left_end}, ...})` connects any number
of pipe faces without a storage volume. A shared stationary Rusanov state is computed algebraically from all connected
faces. Integrated mass, total energy and species fluxes sum to zero at each node. Setup claims every endpoint;
updates allocate no memory. This is a dissipative one-dimensional numerical coupling, not a
geometry-resolving junction model. Momentum changes
are reacted by the fitting. Branch angle losses are not modeled. The separate
`Junction` class remains available for explicit finite-volume chambers.
Each V8 bank has a 4-2-1 header: four primaries feed two secondaries, then a
50 mm exhaust. Pairings are 1+2 / 3+4
and 5+7 / 6+8 with this simulation's bank numbering and firing order. Two T
junctions and a 780 mm x 38 mm tube form the H-pipe. Small configurable port-coupled junction volumes are used by the V8. The intake
plenum also remains a storage volume.
The TOML `primaries` pipe group configures diameter; measured
`geometry.pipes` centerlines configure length and position. Headers now use
direct swept bends with revised lengths, and the short/long variants have
distinct physical layouts. See [geometry definitions](docs/ENGINE_CONFIG.md).
The
`[junctions] h_pipe` enables the H crossover;
pass it as the fourth `FourStrokeScenario` constructor argument. The renderer
uses the same centerlines, cross-sections and active storage-volume dimensions
as the fluid system, with local pressure colors and mitered hard corners.
The previous header optimization measurements applied to the earlier finite-volume
4-1 layout and do not establish an optimum for this new 4-2-1 network.
### Example browser and small port-coupled junctions
The startup browser now scrolls with the mouse wheel, Up/Down, Page Up/Down,
Home and End. It adapts to the window size and shows a scroll indicator. Click
an example or press Enter to start it. The examples are defined in
`engines/*.toml`, loaded at startup and grouped into categories with selectable variants. These are configuration comparisons of
the existing engine models, not measurements of additional production engines.
The V8 currently uses port-coupled finite-volume fittings, configured by
`JunctionConfig::junction_volume` (default 50 cc per fitting). They use the
existing conservative component/port exchange and have no volume boxes in the
renderer. The earlier zero-storage Rusanov junction remains available for
comparison through `BoundarySystem::add_junction`, but is not used by the V8.
The 4-2-1 piping and the H crossover geometry remain the same.
### TOML engine definitions
Every engine is defined in its own file under `engines/`. Copy an existing
`.toml` file, change its `id`, name and calibration, then restart to load it.
No rebuild is required when editing definitions in the active directory.
Unknown keys, invalid dimensions, malformed timing and duplicate IDs produce
a file-specific startup error. See [the engine configuration guide](docs/ENGINE_CONFIG.md)
for units, directory discovery, sections and pipe topology ordering.
### Engine families and inspired builds
Choose **Engine Scenarios** on the home screen. Categories are listed beside the
engine collection; wheel over them to reach more families. Select a variant,
then press Enter or **Start engine**. Ctrl+F focuses search. Backspace, Escape or
**Back** returns to the main menu. Lists support wheel and keyboard scrolling.
`EngineConfig` supplies cylinder count (up to 12), inline/V/boxer layout,
bank angle, bore/stroke/rod, compression, firing phases, rev limit and fueling.
The catalog includes four-stroke singles, parallel and V twins, inline 3/4/5/6,
V6/V8/V10/V12 and flat 4/6 examples. Inspired builds include F20C, Audi five,
RB26, Busso V6, EJ20, 911 flat-six, 916 V-twin and 250 V12 concepts. These use
estimated calibrations and simplified common-plenum/exhaust networks: they are
not factory replicas, measured torque curves, or turbocharged configurations.
The 8.0 L big-block variant transports carburetor fuel through its intake into
the cylinders. Its fixed idle stop is configured with `[ecu] idle_base_opening` in its TOML file.
It reuses the lumped carb model with scaled effective jets; multiple carb barrels,
floats and cylinder-specific fuel distribution are not resolved. Larger engine
geometry requires its own mixture and idle calibration.
Two-stroke families include 50, 125 and 500 cc singles. The 125 variants compare
standard, straight, long, short and wide chambers. The 50/500 concepts scale
bore/stroke, rod, crankcase clearance, port widths, flywheel inertia and chamber
geometry; they retain estimated carb and combustion calibrations. A different
chamber is a geometry experiment, not a guaranteed RPM or power improvement.
Launch a specific variant directly with `--example big-carb`, `--example flat6`,
`--example fifty` or another catalog ID. Carb jet scaling is configurable per
engine: four-strokes use effective jet diameters in `[fuel]`; two-strokes
use jet multipliers in `[two_stroke]`.
### Pulse-preserving custom exhausts
The custom inline three, F20 four, inline five and inline six examples use
unequal-length primaries, 50 cc junction collectors and secondary pipes rather
than a single large mixing volume. Their pipe damping matches the undamped Saab
path. Inline fours/fives/sixes use physical firing orders so collector grouping
preserves pulse timing; combustion remains even-fire.
The custom big-block V8 uses the American V8's cross-plane 1-5-4-2-6-3-7-8 firing
order and 4-2-1 headers with an H crossover, retaining its own displacement and
carburetor. V10/V12 examples retain separate banks through junction collectors,
secondary pipes and two radiating outlets, including both outlets in the spatial
audio renderer. The existing Saab, American V8, single and twin presets retain
their exhaust paths.
Capture any catalog entry at controlled RPM for comparison:
```powershell
./out/build/windows-gcc/fluidsim_audio_capture.exe --example v12 --rpm 3000 --warmup 0.6 --seconds 2 --throttle 0.55 --scene dry --out out/audio/v12
```
See [custom exhaust verification](verification/CUSTOM_EXHAUST_CHARACTER.md) for
fixed-RPM comparison results.
### Carburetor calibration
All seven two-stroke catalog variants and both carbureted four-strokes have
separate pilot, main-jet and needle-taper calibrations. Progressive pilot metering
fills the 5% throttle transition after lowering the 125 idle stops. The smaller four-stroke
single (`single-450`) now uses a 40 mm carburetor, transported fuel and milder
355/565-degree intake and 145/370-degree exhaust timing. The 650 cc single keeps
EFI. The big-block idle opening is 7%; the straight-pipe 125 uses 3%. The racing
125 and its chamber variants use the calibrated 3% mechanical slide stop.
Needle annulus area is interpolated continuously across slide positions; liquid
fuel still comes from the simulated jet pressure drop and float bowl. There is
no AFR feedback or cylinder-side fuel correction. The displayed carb AFR is the
uncapped ratio of filtered air flow to delivered fuel flow.
`fluidsim_carb_calibration --check` tests every carbureted catalog scenario at
idle, 5%, 10%, 15%, 25%, 50%, 75% and full pedal on an imposed-speed dyno, then
releases the dyno for an idle run. It fails on AFR outside the calibration
bounds, stalling, nonfinite RPM/AFR, pipe positivity repairs, a changed racing
125 stop, or loss of transported carb fuel in the 450 single. Run it through
`ctest --preset windows -R carb_calibration --output-on-failure`.
The tool also prints CSV diagnostics: run without arguments for the four main
operating points, or use `--sweep single-450` for a denser sweep of one scenario.
See [carb calibration results](verification/CARB_CALIBRATION.md) for operating
conditions, AFR values and the distinction between carb and recirculated charge.
### Source-separated audio diagnostics
The capture tool writes `-intake.wav`, `-exhaust.wav`, `-exhaust-2.wav`, and
`-valve-contact.wav` alongside the existing rendered, dry, and unfiltered mixes.
Source stems contain direct sound and early reflections, before the shared diffuse
reverb; `-tail.wav` contains that shared tail. With all sources enabled, the four
stems plus the tail reconstruct the rendered mix before WAV quantization/clipping.
Absent sources produce silent files. Peak, RMS, maximum sample delta, and clipping
counts are reported for every stem, in device full-scale units, without normalization.
Use `--solo intake`, `--solo exhaust`, `--solo exhaust-2`, or
`--solo valve-contact` to select a source in the output mix (default: `--solo all`).
All diagnostic stems remain active during soloing. These controls affect audio only;
they do not change the fluid simulation. Source controls are simulation-thread APIs.
```powershell
.\out\build\windows-gcc\fluidsim_audio_capture.exe --example single-450 --rpm 3000 --throttle .3 --warmup 1 --seconds 2 --scene dry --out out/audio/single-450-baseline
```
For intake-only listening, repeat with `--solo intake` and a different output prefix.
The shared tail can retain decay when controls change during playback.
Captures use 32-bit IEEE float WAV to preserve very quiet sources without clipping.
For audible, individually peak-normalized PCM16 listening copies, run:
```powershell
py tools/audio_preview.py out/audio/single-450-baseline
```
Previews appear in `out/audio/previews/`, with gain reported for each file.
Silent sources are skipped. Preview loudness does not represent relative source
strength; use the original captures and metrics for calibration.
### Acoustic pulse diagnostics
The `fluidsim_acoustic_benchmark` target compares straight connections, idealized
three-way junctions, and finite-volume fittings across meshes, timesteps, and
all three flux solvers. It exports pressure/velocity traces and acoustic energy
ratios, with conservation and numerical-repair diagnostics. See
[the benchmark guide and initial findings](docs/ACOUSTIC_BENCHMARK.md).
### Listening comparison: limited second-order pipes
Desktop auto gain now starts **off**. Press **G** to enable it if desired.
For a direct comparison on the current engine, keep throttle and master volume
fixed and press **F3** to cycle `rusanov ? hlle ? hllc ? muscl`.
The dashboard shows the selected solver. Rusanov remains the startup default.
To start directly in the new mode:
```powershell
.\out\build\windows-gcc\fluidsim_desktop.exe --example single-450 --solver muscl --scene dry
```
`muscl` uses minmod-limited primitive MUSCL-Hancock reconstruction with HLLC
fluxes on uniform pipe sections. Matching direct connections share reconstruction
stencils. Component boundaries and area/profile transitions retain first-order
coupling; nonphysical predicted edges fall back to cell averages, and HLLC keeps
its existing HLLE fallback. Reconstruction buffers are allocated at construction.
This is a listening candidate, not a calibrated real-engine sound model.
At 3000 RPM / 0.3 pedal, four-second dry captures are in
`out/audio/pipe-ab-rusanov-rendered.wav` and `out/audio/pipe-ab-muscl-rendered.wav`.
They preserve the actual level difference. New wave resolution can also change
cylinder filling, combustion, and torque. Restart in each solver for clean
comparisons; live switching retains the engine's current state.
For a comparison at equal RMS loudness, use
`out/audio/pipe-ab-rusanov-level-matched.wav` and
`out/audio/pipe-ab-muscl-level-matched.wav`. Both are PCM16 listening copies;
original float captures keep their physical output scale. Equal RMS does not
imply identical perceived loudness or identical engine state.
In one local loaded V8 check, MUSCL took 1.23 wall seconds per simulated second,
versus 0.78 for Rusanov. Large engines may underrun in real-time playback;
use the single-450 for the first listening comparison.
### Direction-aware characteristic junctions
A nonlinear zero-storage exhaust junction solver is now available separately
from the pipe solver. Start with Rusanov pipes to isolate the junction change:
```powershell
.\out\build\windows-gcc\fluidsim_desktop.exe --example v8-h --solver rusanov --junctions characteristic --junction-loss 0.5 --scene dry
```
Use `--junctions volume` to compare against the original volume fittings.
`--junctions legacy` selects the old dissipative zero-storage Rusanov node.
Default engine behavior still uses volume fittings. Direction losses use local
route tangents, with explicit TOML branch direction overrides supported. The
loss coefficient is an uncalibrated engineering parameter; zero disables angle
losses. Auto gain still defaults off.
The single-450 also supports these switches, but its two-port collector cannot
exercise multi-branch turning. Selecting a zero-storage node there removes its
collector-volume resonance. See [junction details](docs/JUNCTION_SOLVER.md).