Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 31 additions & 19 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,27 @@ without the owner's explicit agreement.
chaotic and neither the behaviour nor the sound is a real two-stroke's. Free
rein to add what a 2T needs (carburettor, reed and power valves) or to rework
the ports deeply. Half the chaos was the scavenging model (fixed 2026-10-21)
and a carburettor exists (2026-10-22); open are reed and power valves and the
3,500/7,000/7,500 rpm holding band, the engine's own and not the dyno's.
3. **Performance**: test duration, build time, real-time factor.
and a carburettor exists (2026-10-22); the 3,500/7,000/7,500 rpm holding
band was the 240 Hz frame's alias of the firing ripple, and a power valve
exists (2026-10-23). The reed stays a perfect check valve until a measured
gap asks for its petals (VISION.md's decision rule).
3. **Performance**: test duration, build time, real-time factor. JUCE compiles
once since 2026-10-23 (Linux CI build 18 -> 4.5 min, Windows 25 -> 32: one
serial MSBuild project). Open, and measurable only on the reference machine:
the V10-V16 designs below real time (pool spin vs park, duct numerics).
4. **Knock.** Most engines, even the biggest, survive a boost plus an advance
that would destroy a real one. Fix the knock PHYSICS, not the damage model.
The designer's ignition is a template because there is no knock limit to
calibrate MBT against, so this item unblocks that one.
5. **Editing on the engine**: compression ratio by clicking the engine, every
turbo visible when two or more are fitted, carburettor parameters by
clicking (if one is added), and two exhaust paths joinable into one (a
boxer six cannot be made 6-into-1 today). The designer window built on
2026-10-19 has never been checked on screen: do that here.
calibrate MBT against, so this item unblocks that one. The end gas knocks
since 2026-10-22, a stepped control holds the limit, and Douaud-Eyzat's
prefactor (x2) puts the catalogue's maps at their limit (2026-10-23). Open:
the I5 holds 8-12 deg of retard at its maker's boost; Woschni's combustion
term (right for the TDI's BSFC, it stalls lugged engines) was backed out.
5. **Editing on the engine** -- done 2026-10-23, each checked on screen
(journal): compression ratio on the liner/piston, every turbo drawn, the
carburettor drawn and jetted by clicking, two exhaust paths joined into one
(*Joined* on the ports or a junction), and the designer window. Open: the
designer lists valve and injector rows a two-stroke does not use.

**Paused: milestone 1, the sound of the CP2** (MT-07, Arrow exhaust):
`docs/recording-protocol-cp2.md` holds the method, and the two unverified leads
Expand Down Expand Up @@ -125,9 +134,10 @@ masks everything); `EngineLabPhysicsPerfHarness` needs `--idle` beside
`--trace 1` (`--trace <low rpm>` alone is full-load lugging, not an idle).

**Flags that separate a real effect from an artefact**:
`EngineLabDynoSweepHarness` `--crank-degrees` and `--coupling-us` resize the
mechanical substep and the exhaust flush clock, and a gas-dynamic feature stays
where it is when either changes; `EngineLabGeometrySensitivityHarness` says
`EngineLabDynoSweepHarness` `--crank-degrees`, `--coupling-us` and
`--frame-hz` resize the mechanical substep, the exhaust flush clock and the
240 Hz outer frame, and a gas-dynamic feature stays where it is when any one
changes; `EngineLabGeometrySensitivityHarness` says
whether an exhaust change moved the timbre (`shape`) or only the level
(`level`).

Expand Down Expand Up @@ -194,7 +204,8 @@ Each measurement is in `docs/journal.md` or the docs archive (tag
- **Performance**: fork-join per cylinder with woken threads (the resident pool
runs the pass since 2026-10-20); AVX/AVX2 (dependent-division latency); a
telemetry cache for listening; state-variable valve-flow reconstruction
against clicks.
against clicks; whole-project LTO (bit-identical, within noise); `/MP2` on
the JUCE project in CI (C1060 beside the app's LTCG link).
- **Two-stroke**: a 50 mm mesh; coupling the exhaust every substep beyond
BDC-to-transfer-close, a characteristic valve, finer transfer coupling (none
of them real time); un-gating exhaust oxidation before scavenging is right
Expand All @@ -208,8 +219,8 @@ Each measurement is in `docs/journal.md` or the docs archive (tag
crankcase); a transit-vs-window scavenging race (monotonic in rpm by
construction); a scavenging bias from ONE pressure sample at port opening (a
hunting limit cycle over 4,500-6,250 rpm: integrate the front); "the trapped
residual is too low" (a unit error). The 3,500/7,000/7,500 rpm holding band
is none of pipe length, settle, step, crank step, flush clock or carburettor.
residual is too low" (a unit error). A holding band locked to crank speed is
first a frame-rate alias: `DynoSweepHarness --frame-hz` moves it.
- **Intake**: four inertance formulations (unstable or out of phase); the charge
bias as the MUSCL limiter, the finite plenum, wall heat or valve linearisation
(it was the valve nozzle boundary, Courant-dependent); front-loaded or
Expand All @@ -225,10 +236,11 @@ Each measurement is in `docs/journal.md` or the docs archive (tag
fail, 5 more trade the lean first cycle for rich or misfires); seeding the
high-load correction cell from the idle cell (saves a start at 4 s but stalls
10 engines of 14 after 25 s of idle) and an asynchronous pulse on tip-in.
- **Knock, one input at a time** (journal 2026-10-13): the end-gas integral
reads the MEAN chamber temperature with a delay 2-5 x slow; isentropic end gas
never knocks; plus Douaud-Eyzat it over-knocks (1,000-1,200 K); integral
control pulls 12 deg for nothing.
- **Knock** (journal 2026-10-13, -22): end gas at the MEAN temperature, or at
constant cv (1,000-1,200 K); proportional or integral control; Kalghatgi's
octane index (boost gains 1-3 octane); an ECU boost cut on knock (I5 363
N.m); the I5 at 2.1 bar. A weaker Douaud-Eyzat pressure exponent has no
source (pressure matters more in the NTC band).
- **The other simulator's ingredients, one at a time** (journal 2026-10-13/14):
output stage, pressure-release outlet, valve-area noise, mild room IR -- none
kept (blind listening or measurement). Its fine mesh is not shown to matter.
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,18 @@ jobs:
run: cmake --preset windows-vs2022

# Two parallel MSBuild nodes: the hosted runner runs out of compiler heap
# (C1060) with the default one-node-per-core when JUCE is compiling.
# (C1060) with the default one-node-per-core when JUCE is compiling. Two
# can still run out (run 98, on run 96's green tree): the build then
# resumes on one node, as .claude/CLAUDE.md says to, rebuilding only
# what failed.
- name: Build
shell: pwsh
run: |
cmake --build --preset windows-release -- /m:2 /nologo /verbosity:minimal *>&1 | Tee-Object -FilePath build.log
if ($LASTEXITCODE -ne 0 -and (Select-String -Path build.log -Pattern 'C1060' -Quiet)) {
Write-Output "::warning::C1060 (compiler out of heap) on two nodes; resuming on one."
cmake --build --preset windows-release -- /m:1 /nologo /verbosity:minimal *>&1 | Tee-Object -FilePath build.log
}
if ($LASTEXITCODE -ne 0) {
Select-String -Path build.log -Pattern 'error [A-Z]+[0-9]+|fatal error' |
Select-Object -First 30 |
Expand Down
61 changes: 61 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ file formats, always with an in-memory migration).

### Fixed

- **The designer's real-time check failed every carburetted two-stroke**
("did not hold 5000 rpm"): it cranked the engine at full throttle, where a
carburettor cannot start it. It now cranks with the throttle shut until the
engine runs.
- The designer names the engine from what it is (*Two-stroke Inline Twin*)
until a name is typed, instead of keeping *Inline Seven*, and its table's
columns are headed *Quantity* and *Value*.
- A carburetted engine no longer draws, or offers to edit, injectors it does
not have.

- A designed V or W engine shares one crank pin between the cylinders of a
row, as real ones do; each had its own pin. A 90 deg V-twin now fires
270-450 and a 45 deg one 315-405. Where a shared pin cannot fire six
Expand All @@ -74,9 +84,60 @@ file formats, always with an in-memory migration).
125 cc cylinders a 16.6 mm bore on a single and 33.2 mm on a four. Singles gain
18-29 % of peak torque; triples, fours and V4s lose the 6-9 % their oversized
bores gave them; twins are unchanged.
- **Engines knock.** A boost and an advance that would destroy a real engine
now knock: the end gas the flame has not reached is followed as its own
zone, compressed from the seal, against an autoignition delay fitted on
reference fuels in a knocking engine (Douaud-Eyzat), and the knock's
intensity is the pressure that gas adds. The delay's prefactor is
calibrated so that the catalogue's own spark maps sit at their knock limit,
as a production map does: 10 deg of advance over a map knocks, and a 2JZ
run that way at full load with knock control off knocks heavily. The knock
control now works as a production one: it pulls the spark back in steps of
about a degree while the engine knocks and gives it back at 1 deg/s, so an
engine settles at its knock limit instead of sitting in light knock. The
Audi I5 gets the intercooler its real engine has.
- **The compression ratio is edited by clicking the engine**: a cylinder
liner or piston offers it beside the bore and stroke, and the running
engine takes it (at once or over the ramp).
- **A carburettor is drawn and tuned in the 3-D view**: on a carburetted
engine the throttle body becomes a carburettor (slide tower, float bowl),
and clicking it offers its main and pilot jets, needle clip and air screw,
taken by the running engine.
- **Two exhaust paths can be joined into one**: the exhaust ports or a
junction offer *Joined*, which gives the other path's cylinders a copy of
this path's primary into its collector (a boxer six's two banks become a
6-into-1) and restarts the engine.
- **Every turbocharger is drawn.** With two or more units the 3-D view drew
only the first; now each unit an exhaust path drives sits on that path's
collector (a flat six's two banks get one each), and clicking one opens its
own inspector, titled with its number and its path.
- **Two-strokes can have a power valve** (`two_stroke.power_valve_*`): it
lowers the exhaust port at low speed and lifts as the pipe comes in. The
designer fits one to sport two-strokes; a designed 500 cc twin gains 20-39 %
of torque between 3,250 and 5,750 rpm and keeps its top end.
- A two-stroke saved as JSON keeps its scavenging loop length (it came back
at the default).
- A two-stroke twin no longer fails to hold 3,500, 7,000 and 7,500 rpm on the
dyno: the brake answered the crankshaft speed sampled at the end of each
240 Hz frame, where a firing rate near 240 Hz (or 120) aliases into a slow
wander. The dyno now holds the speed averaged over the frame.
- A designed carburetted two-stroke idles: its pilot jet was sized for about
eight times the air a two-stroke idles on, so twins and triples flooded and
stalled. The pilot is now sized from the air the idle path passes and
follows it as the idle air opens, and each carburettor's idle bypass goes
with the cylinder it feeds. Designed 1-4 cylinder two-strokes idle at their
1,300 rpm; an RD350-sized design keeps a 35 pilot.
- A two-stroke's idle no longer hunts: the idle governor runs at half the
gain on two-strokes, whose idle air passes the crankcase first. The RD350
held 1,175-1,398 rpm with a 5 s period; now 1,284-1,325.
- Boost no longer burns the charge faster: the flame's turbulence follows the
piston, not the intake pressure, so a boosted cylinder burns like a full
unboosted one.

### Changed

- The build compiles JUCE once instead of once per executable: a clean
Linux build takes 6 minutes instead of 15 on four cores.
- Engines with eight cylinders or more simulate their cylinders in parallel,
with exactly the same result: 22-33 % more real-time headroom (a designed
6 L V12 at 7,500 rpm 0.71 -> 0.93, the LS3 1.17 -> 1.43, the Merlin
Expand Down
20 changes: 20 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,26 @@ if(NOT MSVC)
enginelab_vendor_headers_are_system(${enginelab_vendor_targets})
endif()

# JUCE's modules arrive as interface sources, so every target that linked one
# compiled JUCE again: about 130 of the Linux tree's 390 compile units were
# JUCE, six to fifteen per executable. They are compiled once here, linked
# PRIVATE so their sources stay in this library, and their include directories
# and definitions are re-exported (JUCE's own pattern for a shared code
# library). Our targets link this, never a `juce::` module directly.
add_library(EngineLabJuce STATIC)
target_link_libraries(EngineLabJuce
PRIVATE
juce::juce_audio_basics juce::juce_audio_devices juce::juce_audio_formats
juce::juce_audio_utils juce::juce_dsp juce::juce_gui_extra juce::juce_opengl
juce::juce_cryptography
juce::juce_recommended_config_flags)
target_compile_definitions(EngineLabJuce
PUBLIC JUCE_WEB_BROWSER=0 JUCE_USE_CURL=0
INTERFACE $<TARGET_PROPERTY:EngineLabJuce,COMPILE_DEFINITIONS>)
target_include_directories(EngineLabJuce SYSTEM
INTERFACE $<TARGET_PROPERTY:EngineLabJuce,INCLUDE_DIRECTORIES>)
set_target_properties(EngineLabJuce PROPERTIES POSITION_INDEPENDENT_CODE TRUE)

add_subdirectory(src/foundation)
add_subdirectory(src/calibration)
add_subdirectory(src/events)
Expand Down
25 changes: 0 additions & 25 deletions cmake/CompilerWarnings.cmake
Original file line number Diff line number Diff line change
@@ -1,27 +1,3 @@
# JUCE's module units are compiled INTO the targets that link them (they arrive
# as interface sources), so they would inherit the warning set below. They are
# not our code and they do not build clean under -Wconversion, which drowned
# EngineLab's own 125 diagnostics in ~400 of JUCE's on the first Linux build.
# The categories JUCE trips are turned off for those units only; our own
# sources keep the full set. JUCE's module units are one level deep inside
# `modules/` by construction, so a single glob finds them.
function(enginelab_quiet_vendor_units)
if(MSVC OR NOT juce_SOURCE_DIR)
return()
endif()
get_property(units GLOBAL PROPERTY ENGINELAB_VENDOR_UNITS)
if(NOT units)
file(GLOB units "${juce_SOURCE_DIR}/modules/*/*.cpp")
set_property(GLOBAL PROPERTY ENGINELAB_VENDOR_UNITS "${units}")
endif()
if(units)
# A source property resolves in the directory that defines the target,
# which is this function's caller: a function is not a new scope.
set_source_files_properties(${units} PROPERTIES COMPILE_OPTIONS
"-Wno-conversion;-Wno-float-conversion;-Wno-missing-field-initializers;-Wno-shadow;-Wno-comment")
endif()
endfunction()

# A vendored *header* is compiled as part of whatever includes it, so no source
# property can reach it. One line of JUCE's `juce_UMPUtils.h` broke fourteen of
# our translation units on the first Linux build with warnings as errors, and
Expand Down Expand Up @@ -66,6 +42,5 @@ function(enginelab_set_warnings target)
if(ENGINELAB_ENABLE_WARNINGS_AS_ERRORS)
target_compile_options(${target} PRIVATE -Werror)
endif()
enginelab_quiet_vendor_units()
endif()
endfunction()
Loading
Loading