Files
FluidSimCpp/README.md

45 KiB

FluidSimCpp

Solver performance measurements and reproducible SIMD/threading experiments are documented in the optimization audit. 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. 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 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 -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 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:

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. 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:

.\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:

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 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:

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 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:

./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 for measurements and reproduction commands.

Benchmark or capture with:

./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:

./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 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 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:

./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 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. 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 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:

./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 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 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.

.\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:

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.

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:

.\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:

.\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.