perf(simulation): Optimize Web Worker simulation logic for reduced computation overhead and improved thread handling in "Age of Dwarves" game

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-31 22:47:31 -07:00
parent e3aae475a4
commit 5e15021933

View file

@ -8,6 +8,8 @@ import type {
WorkerResponse,
FramePayload,
EasterEggSeed,
EventCatalog,
CrossTriggers,
} from '@magic-civ/engine-ts'
import {
SCENARIOS,
@ -18,13 +20,13 @@ import {
STARTING_ATMOSPHERES,
deriveSubstrates,
classifyBiome,
} from '@magic-civ/engine-ts'
import {
WasmClimatePhysics,
WasmEcologyPhysics,
WasmGrid,
WasmMapGenerator,
} from '@magic-civ/physics-rs'
EventEvaluator,
DEFAULT_DT,
} from '@magic-civ/engine-ts'
// ---------------------------------------------------------------------------
// Worker-internal scenario state
@ -40,10 +42,12 @@ interface ScenarioState {
wasmGrid: WasmGrid
physics: WasmClimatePhysics
ecology: WasmEcologyPhysics | null
eventEvaluator: EventEvaluator | null
cursorScenarioTurn: number
totalScenarioTurns: number
worldSeed: number
isVolcanicWinter: boolean
dt: number
}
const CHUNK_SIZE = 5
@ -72,9 +76,10 @@ function wasmPhysicsStep(
wasmGrid: WasmGrid,
turn: number,
seed: number,
dt: number,
): void {
physics.processStep(wasmGrid, turn, seed)
if (ecology) ecology.processStep(wasmGrid)
physics.processStep(wasmGrid, turn, seed, dt)
if (ecology) ecology.processStep(wasmGrid, dt)
physics.stepAtmosphericChemistry(wasmGrid)
}
@ -144,14 +149,29 @@ function stepOneTurn(state: ScenarioState, scenarioTurn: number, specJson: strin
state.wasmGrid.free()
state.wasmGrid = jsGridToWasm(state.grid)
}
wasmPhysicsStep(state.physics, state.ecology, state.wasmGrid, absTurn, state.worldSeed)
wasmPhysicsStep(state.physics, state.ecology, state.wasmGrid, absTurn, state.worldSeed, state.dt)
// Compute stats in Rust — avoids full 960-tile JSON deserialization every turn.
const prevStats = state.stats.length > 0 ? state.stats[state.stats.length - 1] : undefined
const stats: TurnStats = JSON.parse(
state.wasmGrid.computeStatsJson(prevStats?.avg_temp ?? 0.5, prevStats?.avg_moisture ?? 0.5)
)
state.cursorScenarioTurn = scenarioTurn + 1
return { stats, events: [] }
// Fire stochastic events — only deserialize JS grid when events would fire or
// there are active multi-turn effects already running.
let events: EcologicalEvent[] = []
const evaluator = state.eventEvaluator
if (evaluator !== null && (evaluator.checkWillFire(absTurn, state.dt) || evaluator.hasActiveEffects())) {
state.grid = wasmGridToJs(state.wasmGrid)
events = evaluator.evaluateTurn(state.grid, absTurn, state.dt)
if (events.length > 0) {
// Sync any event-driven grid mutations back to WASM
state.wasmGrid.free()
state.wasmGrid = jsGridToWasm(state.grid)
}
}
return { stats, events }
}
function addCheckpoint(state: ScenarioState, turn: number): void {
@ -187,7 +207,7 @@ function seekToTurn(state: ScenarioState, targetSt: number): void {
state.wasmGrid.free()
state.wasmGrid = jsGridToWasm(state.grid)
}
wasmPhysicsStep(state.physics, state.ecology, state.wasmGrid, absTurn, state.worldSeed)
wasmPhysicsStep(state.physics, state.ecology, state.wasmGrid, absTurn, state.worldSeed, state.dt)
}
state.grid = wasmGridToJs(state.wasmGrid)
state.cursorScenarioTurn = targetSt
@ -219,7 +239,7 @@ function seekToTurn(state: ScenarioState, targetSt: number): void {
state.wasmGrid.free()
state.wasmGrid = jsGridToWasm(state.grid)
}
wasmPhysicsStep(state.physics, state.ecology, state.wasmGrid, absTurn, state.worldSeed)
wasmPhysicsStep(state.physics, state.ecology, state.wasmGrid, absTurn, state.worldSeed, state.dt)
}
state.grid = wasmGridToJs(state.wasmGrid)
state.cursorScenarioTurn = targetSt
@ -263,7 +283,7 @@ async function prebufferFrames(state: ScenarioState, scenarioId: string, frameCo
tmpWasmGrid.free()
tmpWasmGrid = jsGridToWasm(tmpGrid)
}
wasmPhysicsStep(tmpPhysics, tmpEcology, tmpWasmGrid, absTurn, state.worldSeed)
wasmPhysicsStep(tmpPhysics, tmpEcology, tmpWasmGrid, absTurn, state.worldSeed, state.dt)
}
// Use writeFrameBuffers for the texture data (hot path)
@ -370,6 +390,7 @@ async function handleRun(scenarioId: string, turns: number, prebufferFrameCount:
const worldAge = config.worldAge
const totalAbsTurns = worldAge + turns
const isVolcanicWinter = config.id === 'volcanic_winter'
const dt = config.dt ?? DEFAULT_DT
// Planet-specific params override earth defaults when available
const planet = config.planet ?? 'earth'
@ -436,7 +457,7 @@ async function handleRun(scenarioId: string, turns: number, prebufferFrameCount:
if (geoEcology) geoEcology.free()
return
}
wasmPhysicsStep(geoPhysics, geoEcology, geoWasmGrid, turn, seed)
wasmPhysicsStep(geoPhysics, geoEcology, geoWasmGrid, turn, seed, dt)
if (turn % CHUNK_SIZE === 0) {
post({ type: 'progress', scenarioId, turn, total: totalAbsTurns, phase: 'geology' })
@ -468,15 +489,29 @@ async function handleRun(scenarioId: string, turns: number, prebufferFrameCount:
const physics = new WasmClimatePhysics(effectiveParamsJson, terrainJson, baseSpecJson)
const ecology = config.abioticWorld ? null : new WasmEcologyPhysics()
const wasmGrid = jsGridToWasm(grid)
// Build event evaluator from ecological_events catalog loaded at init
const ecologicalEvents = baseSpec.ecological_events as Record<string, unknown> | undefined
let eventEvaluator: EventEvaluator | null = null
if (ecologicalEvents) {
const { cross_triggers: crossTriggersRaw, ...eventCategories } = ecologicalEvents
eventEvaluator = new EventEvaluator(
eventCategories as EventCatalog,
(crossTriggersRaw ?? {}) as CrossTriggers,
seed,
)
}
const state: ScenarioState = {
config, grid, wasmGrid, physics,
ecology,
ecology, eventEvaluator,
stats: [], events: [], timings: [],
checkpoints: new Map(),
cursorScenarioTurn: 0,
totalScenarioTurns: turns,
worldSeed: seed,
isVolcanicWinter,
dt,
}
addCheckpoint(state, 0)
@ -592,7 +627,7 @@ function handleFrame(scenarioId: string, turn: number, lookahead: number): void
tmpWasmGrid.free()
tmpWasmGrid = jsGridToWasm(tmpGrid)
}
wasmPhysicsStep(tmpPhysics, tmpEcology, tmpWasmGrid, absTurn, state.worldSeed)
wasmPhysicsStep(tmpPhysics, tmpEcology, tmpWasmGrid, absTurn, state.worldSeed, state.dt)
}
// Use writeFrameBuffers for texture data