Files
FluidSimCpp/README.md
max bf8621ad39 Load engines from TOML and improve scenario audio and carb tuning
Migrate 32 engine definitions into validated startup-loaded TOML files and separate configuration and snapshots from scenario assembly. Add camera-based stereo propagation from shared pipe geometry, preserve exhaust character, tune carburetor mixtures and idle, use ignition cuts for carb engines, and support returning to the scenario menu.

Validation: full Windows build and all 14 CTest checks passed.
2026-10-24 03:24:35 +02:00

622 lines
35 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)
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.
Page Up/Down change playback speed; Space toggles real time.
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
512-frame batches with a 4096-frame startup reserve (about 93 ms). 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.
Launching `fluidsim_desktop` without `--scenario` opens a small scenario menu.
Choose the twin, YZ125, Saab B205, or American V8 card, or select with Up/Down
and press Enter.
Escape returns from a running scenario to the menu, stopping its simulation and
audio so another scenario can be selected. Escape closes the menu at the category
screen. Simulation and audio start after selection. 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 restore real-time playback and **R** to change environment. 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 enabled 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`. Press **F3** during playback to
cycle Rusanov, HLLE and HLLC; 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 now has a 4-2-1 header: four equal 750 mm x 38 mm primaries feed two
equal 400 mm x 45 mm 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 primary length/diameter, while
`[junctions] h_pipe` enables the H crossover;
pass it as the fourth `FourStrokeScenario` constructor argument. The renderer
routes the primary pairs into Y merges, then the final Y collectors and H-pipe,
using local pipe diameter and pressure colors without collector boxes.
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
The browser opens at engine categories; click a category to see variants, then
click a variant or press Enter to start. Backspace, Left, Escape or the
`< Categories` header returns to categories. Both levels 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.