Skip to content
Open
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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2025-01-24 - High-Frequency Allocation Anti-Pattern in Lua
**Learning:** In Lua-based simulation environments, defining functions (closures) or table literals inside high-frequency functions (like `updateTorque` at 2000Hz) causes significant garbage collection pressure. This creates CPU overhead and can lead to micro-stutters during simulation.
**Action:** Always hoist static data structures (lookup maps) and helper functions to the module scope if they do not depend on local loop state.
218 changes: 87 additions & 131 deletions combustionEngine.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,50 @@ local torqueToPower = 0.0001404345295653085
local psToWatt = 735.499
local hydrolockThreshold = 1.9

-- Static enrichment map to avoid re-allocation in the hot path
local enrichmentMap = {
[-30] = 3.0,
[-20] = 2.6,
[-10] = 2.2,
[0] = 1.8,
[10] = 1.5,
[20] = 1.3,
[30] = 1.15,
[40] = 1.05,
[50] = 1.02,
[60] = 1.0,
[70] = 1.0
}

-- Optimized cold start enrichment calculation moved to module scope
local function getColdEnrichment(tempC)
-- Find the two closest temperature points
local lowerTemp = -20
local upperTemp = 80
local lowerEnrich = 3.0
local upperEnrich = 0.85

-- Find the two closest temperature points in the map
for temp, enrich in pairs(enrichmentMap) do
if temp <= tempC and temp > lowerTemp then
lowerTemp = temp
lowerEnrich = enrich
end
if temp >= tempC and temp < upperTemp then
upperTemp = temp
upperEnrich = enrich
end
end

-- Linear interpolation between the two closest points
if lowerTemp == upperTemp then
return lowerEnrich
end

local t = (tempC - lowerTemp) / (upperTemp - lowerTemp)
return lowerEnrich + (upperEnrich - lowerEnrich) * t
end
Comment on lines +39 to +81
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix the cold-enrichment interpolation bounds.

Operator, the sentinels are... confused. lowerTemp = -20 starts with lowerEnrich = 3.0, so temperatures between -20 and -10 interpolate from the wrong enrichment. Values above 70 also use an implicit 80 → 0.85 point that is not in the map.

🐛 Proposed fix
--- Static enrichment map to avoid re-allocation in the hot path
-local enrichmentMap = {
-  [-30] = 3.0,
-  [-20] = 2.6,
-  [-10] = 2.2,
-  [0] = 1.8,
-  [10] = 1.5,
-  [20] = 1.3,
-  [30] = 1.15,
-  [40] = 1.05,
-  [50] = 1.02,
-  [60] = 1.0,
-  [70] = 1.0
+-- Static enrichment points to avoid re-allocation in the hot path
+local enrichmentPoints = {
+  {-30, 3.0},
+  {-20, 2.6},
+  {-10, 2.2},
+  {0, 1.8},
+  {10, 1.5},
+  {20, 1.3},
+  {30, 1.15},
+  {40, 1.05},
+  {50, 1.02},
+  {60, 1.0},
+  {70, 1.0}
 }
 
 -- Optimized cold start enrichment calculation moved to module scope
 local function getColdEnrichment(tempC)
-  -- Find the two closest temperature points
-  local lowerTemp = -20
-  local upperTemp = 80
-  local lowerEnrich = 3.0
-  local upperEnrich = 0.85
-
-  -- Find the two closest temperature points in the map
-  for temp, enrich in pairs(enrichmentMap) do
-    if temp <= tempC and temp > lowerTemp then
-      lowerTemp = temp
-      lowerEnrich = enrich
-    end
-    if temp >= tempC and temp < upperTemp then
-      upperTemp = temp
-      upperEnrich = enrich
-    end
+  if tempC <= enrichmentPoints[1][1] then
+    return enrichmentPoints[1][2]
   end
 
-  -- Linear interpolation between the two closest points
-  if lowerTemp == upperTemp then
-    return lowerEnrich
+  for i = 2, `#enrichmentPoints` do
+    local upperPoint = enrichmentPoints[i]
+    if tempC <= upperPoint[1] then
+      local lowerPoint = enrichmentPoints[i - 1]
+      local t = (tempC - lowerPoint[1]) / (upperPoint[1] - lowerPoint[1])
+      return lowerPoint[2] + (upperPoint[2] - lowerPoint[2]) * t
+    end
   end
 
-  local t = (tempC - lowerTemp) / (upperTemp - lowerTemp)
-  return lowerEnrich + (upperEnrich - lowerEnrich) * t
+  return enrichmentPoints[`#enrichmentPoints`][2]
 end

If the 80°C → 0.85 behavior is intentional, make it an explicit point instead of a hidden default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@combustionEngine.lua` around lines 39 - 81, The interpolation sentinels in
getColdEnrichment are wrong: lowerTemp/upperTemp and their enrich defaults don't
reflect actual enrichmentMap entries (causing -20..-10 to interpolate against
wrong values and using a hidden 80→0.85 point). Fix by either (A) making the
80→0.85 point explicit in enrichmentMap, or (B) change getColdEnrichment to
initialize lowerTemp/upperTemp and lowerEnrich/upperEnrich from the actual
first/last keys of enrichmentMap (or use -math.huge/+math.huge sentinels) and
then pick the nearest lower/upper entries during the pairs() scan so the enrich
values always come from enrichmentMap entries (use enrichmentMap[temp] to set
lowerEnrich/upperEnrich when updating lowerTemp/upperTemp).


local function getTorqueData(device)
local curves = {}
local curveCounter = 1
Expand Down Expand Up @@ -299,6 +343,43 @@ local function enable(device)
damageTracker.setDamage("engine", "engineDisabled", false)
end

local function initBattery(device, jbeamData)
-- Set battery parameters based on system voltage (12V or 24V)
local is24V = device.batterySystemVoltage == 24

-- Set voltage thresholds based on system voltage
device.batteryNominalVoltage = is24V and 27.6 or 13.8 -- 27.6V for 24V, 13.8V for 12V when fully charged
device.batteryMinVoltage = is24V and 18.0 or 9.0 -- 18V for 24V, 9V for 12V systems
device.batteryCutoffVoltage = is24V and 16.0 or 8.0 -- Absolute minimum voltage before complete cutoff
device.batteryWarningVoltage = is24V and 22.0 or 11.0 -- Voltage when warning indicators activate
device.batteryLowVoltage = is24V and 20.0 or 10.0 -- Voltage when systems start to fail

-- Set charge and drain rates based on system voltage
device.batteryChargeRate = is24V and 1.0 or 0.5 -- Higher charge rate for 24V systems
device.batteryDrainRate = is24V and 30.0 or 15.0 -- Base drain rate when cranking (A)

-- Get battery capacity from vehicle battery if available
if electrics.values.batteryCapacity then
device.batteryCapacity = electrics.values.batteryCapacity
else
-- Fallback to JBeam value or default (100Ah)
device.batteryCapacity = jbeamData.batteryCapacity or 100.0
end

-- Initialize battery charge from vehicle state if available
if electrics.values.batteryCharge then
device.batteryCharge = electrics.values.batteryCharge
else
-- Start with full charge by default
device.batteryCharge = 1.0
end

-- Log battery initialization
log('I', 'combustionEngine.initBattery',
string.format('Battery initialized: %.1fV system, %.1fAh capacity',
device.batterySystemVoltage, device.batteryCapacity))
end

local function lockUp(device)
device.outputTorqueState = 0
device.outputAVState = 0
Expand Down Expand Up @@ -563,52 +644,7 @@ local function updateGFX(device, dt)
local currentRPM = device.outputAV1 * avToRPM

-- Update battery state
local dt = 1/60 -- Fixed timestep for battery updates

-- Local function to initialize battery parameters
local function initBattery(device, jbeamData)
-- Set battery parameters based on system voltage (12V or 24V)
local is24V = device.batterySystemVoltage == 24

-- Set voltage thresholds based on system voltage
device.batteryNominalVoltage = is24V and 27.6 or 13.8 -- 27.6V for 24V, 13.8V for 12V when fully charged
device.batteryMinVoltage = is24V and 18.0 or 9.0 -- 18V for 24V, 9V for 12V systems
device.batteryCutoffVoltage = is24V and 16.0 or 8.0 -- Absolute minimum voltage before complete cutoff
device.batteryWarningVoltage = is24V and 22.0 or 11.0 -- Voltage when warning indicators activate
device.batteryLowVoltage = is24V and 20.0 or 10.0 -- Voltage when systems start to fail

-- Set charge and drain rates based on system voltage
device.batteryChargeRate = is24V and 1.0 or 0.5 -- Higher charge rate for 24V systems
device.batteryDrainRate = is24V and 30.0 or 15.0 -- Base drain rate when cranking (A)

-- Get battery capacity from vehicle battery if available
if electrics.values.batteryCapacity then
device.batteryCapacity = electrics.values.batteryCapacity
else
-- Fallback to JBeam value or default (100Ah)
device.batteryCapacity = jbeamData.batteryCapacity or 100.0
end

-- Initialize battery charge from vehicle state if available
if electrics.values.batteryCharge then
device.batteryCharge = electrics.values.batteryCharge
else
-- Start with full charge by default
device.batteryCharge = 1.0
end

-- Log battery initialization
log('I', 'combustionEngine.initBattery',
string.format('Battery initialized: %.1fV system, %.1fAh capacity',
device.batterySystemVoltage, device.batteryCapacity))
end

-- Ensure battery parameters are initialized
if not device.batteryNominalVoltage then
-- Initialize battery if not already done
local jbeamData = device.jbeamData or {}
initBattery(device, jbeamData)
end
local batteryDt = 1/60 -- Fixed timestep for battery updates

-- Update battery state based on engine and starter status
local starterActive = device.starterEngagedCoef > 0
Expand All @@ -621,13 +657,13 @@ local function updateGFX(device, dt)
if starterActive and not engineRunning then
-- Drain battery when starting (higher drain for 24V systems)
local drainRate = (device.batteryDrainRate or 15.0) * (device.batteryDrainScale or 1.0)
device.batteryCharge = math.max(0, device.batteryCharge - (drainRate * dt) / ((device.batteryCapacity or 100.0) * 3600))
device.batteryCharge = math.max(0, device.batteryCharge - (drainRate * batteryDt) / ((device.batteryCapacity or 100.0) * 3600))
device.batteryLoad = drainRate -- Track current load in Amps
elseif engineRunning then
-- Recharge battery when engine is running above idle
-- Charge rate is higher for 24V systems and scales with RPM
local chargeRate = (device.batteryChargeRate or 0.5) * (device.outputAV1 / math.max(1, device.idleAV))
device.batteryCharge = math.min(1.0, device.batteryCharge + (chargeRate * dt) / 3600)
device.batteryCharge = math.min(1.0, device.batteryCharge + (chargeRate * batteryDt) / 3600)
device.batteryLoad = -chargeRate -- Negative load indicates charging
else
device.batteryLoad = 0 -- No load when engine is off and starter not engaged
Expand Down Expand Up @@ -1241,52 +1277,7 @@ local function updateTorque(device, dt)

-- Temperature effect on starter torque (reduces torque in cold conditions)
local tempEffectOnStarter = 1.0 - math.max(0, math.min(0.7, (0 - engineTempC) / 30))

-- Cold start enrichment using temperature-based lookup table (reduced values)
local function getColdEnrichment(tempC)
-- Temperature in Celsius to enrichment factor mapping
-- [tempC] = enrichmentMultiplier
local enrichmentMap = {
[-30] = 3.0, -- Reduced from 4.0
[-20] = 2.6, -- Reduced from 3.5
[-10] = 2.2, -- Reduced from 3.0
[0] = 1.8, -- Reduced from 2.5
[10] = 1.5, -- Reduced from 2.0
[20] = 1.3, -- Reduced from 1.5
[30] = 1.15, -- Reduced from 1.25
[40] = 1.05, -- Reduced from 1.1
[50] = 1.02, -- Reduced from 1.05
[60] = 1.0,
[70] = 1.0
}

-- Find the two closest temperature points
local lowerTemp = -20
local upperTemp = 80
local lowerEnrich = 3.0
local upperEnrich = 0.85

-- Find the two closest temperature points in the map
for temp, _ in pairs(enrichmentMap) do
if temp <= tempC and temp > lowerTemp then
lowerTemp = temp
lowerEnrich = enrichmentMap[temp]
end
if temp >= tempC and temp < upperTemp then
upperTemp = temp
upperEnrich = enrichmentMap[temp]
end
end

-- Linear interpolation between the two closest points
if lowerTemp == upperTemp then
return lowerEnrich
end

local t = (tempC - lowerTemp) / (upperTemp - lowerTemp)
return lowerEnrich + (upperEnrich - lowerEnrich) * t
end


-- Calculate cold start enrichment based on engine temperature
local coldStartEnrichment = getColdEnrichment(engineTempC)

Expand Down Expand Up @@ -2873,43 +2864,6 @@ local function reset(device, jbeamData)
selectUpdates(device)
end

local function initBattery(device, jbeamData)
-- Set battery parameters based on system voltage (12V or 24V)
local is24V = device.batterySystemVoltage == 24

-- Set voltage thresholds based on system voltage
device.batteryNominalVoltage = is24V and 27.6 or 13.8 -- 27.6V for 24V, 13.8V for 12V when fully charged
device.batteryMinVoltage = is24V and 18.0 or 9.0 -- 18V for 24V, 9V for 12V systems
device.batteryCutoffVoltage = is24V and 16.0 or 8.0 -- Absolute minimum voltage before complete cutoff
device.batteryWarningVoltage = is24V and 22.0 or 11.0 -- Voltage when warning indicators activate
device.batteryLowVoltage = is24V and 20.0 or 10.0 -- Voltage when systems start to fail

-- Set charge and drain rates based on system voltage
device.batteryChargeRate = is24V and 1.0 or 0.5 -- Higher charge rate for 24V systems
device.batteryDrainRate = is24V and 30.0 or 15.0 -- Base drain rate when cranking (A)

-- Get battery capacity from vehicle battery if available
if electrics.values.batteryCapacity then
device.batteryCapacity = electrics.values.batteryCapacity
else
-- Fallback to JBeam value or default (100Ah)
device.batteryCapacity = jbeamData.batteryCapacity or 100.0
end

-- Initialize battery charge from vehicle state if available
if electrics.values.batteryCharge then
device.batteryCharge = electrics.values.batteryCharge
else
-- Start with full charge by default
device.batteryCharge = 1.0
end

-- Log battery initialization
log('I', 'combustionEngine.initBattery',
string.format('Battery initialized: %.1fV system, %.1fAh capacity',
device.batterySystemVoltage, device.batteryCapacity))
end

local function initSounds(device, jbeamData)
local exhaustEndNodes = device.thermals.exhaustEndNodes or {}

Expand Down Expand Up @@ -3360,6 +3314,8 @@ local function new(jbeamData)
device.ignitionCoef = spawnWithIgnitionOn and 1 or 0
device.invStarterMaxAV = 1 / device.starterMaxAV

initBattery(device, jbeamData)

device.initialFriction = device.friction
device.engineBrakeTorque = jbeamData.engineBrakeTorque or device.friction * 2

Expand Down