perf: harden runtime for weaker hardware

This commit is contained in:
Rijad Zuzo
2026-08-12 10:02:45 +02:00
parent cd29da95e0
commit 34428d836a
47 changed files with 2164 additions and 243 deletions
+20 -4
View File
@@ -21,6 +21,7 @@ SimulationClock
-> EventKnowledgeSystem records, ranks, transfers, and retains bounded knowledge
-> RelationshipSystem applies evidence-gated social consequences
-> VillageOpportunitySystem projects one known unresolved need
-> ConflictSystem advances indexed combatants and authoritative combat outcomes
-> WorldViewManager presents travel, NPC state, and world-state cues
-> ActiveWorldAdapter supplies loaded-world positions/capacity
-> LoadedResourceSpatialIndex bounds finite-anchor discovery
@@ -48,9 +49,13 @@ would otherwise obscure that lifecycle:
- `simulation/conflict/ConflictSystem.gd` owns combatants (health, weapons,
factions, hostility), village/tribe faction records, wolf hostility, the
tribe war motivation decision (desire + victory confidence), raid spawning,
deterministic battle resolution, and war consequences. It emits combat and
war narrative facts and spawn/death signals that presentation binds to
`HostileCombatant` visuals;
deterministic battle resolution, and real pantry-backed war consequences.
Player commands are revalidated against authoritative life, faction, range,
and cooldown state. Player attack readiness advances in unscaled presentation
time; NPC battle cooldowns remain deterministic simulation ticks. Transient
NPC and sorted-combatant indexes bound stable-ID callbacks without entering
saves. It emits combat and war narrative facts
and spawn/death signals that presentation binds to `HostileCombatant` visuals;
- `simulation/events/SimulationEventLog.gd` owns ordered event identity,
history queries, and rate calculations;
- `simulation/knowledge/EventKnowledgeSystem.gd` owns per-NPC references to
@@ -82,7 +87,8 @@ would otherwise obscure that lifecycle:
`world/creatures/creature_visual.gd` is the shared root for creature
presentation: any `CreatureVisual` follows a simulation-owned position through
the navigation mesh, reports position changes, and plays a shared death
collapse. `HostileCombatant` extends it and builds its body from an
collapse. Failed paths keep their target and use bounded exponential retry
instead of querying navigation every physics frame. `HostileCombatant` extends it and builds its body from an
`EnemyDefinition`; `NpcVisual` and `AnimalNode` already follow the same
follow-the-authoritative-position contract, so future creatures (bears, boars,
archers) add a definition and a visual hook instead of a new movement system.
@@ -98,6 +104,16 @@ discovery uses their stable IDs and registration lifecycle, never a parent
path. Decorative siblings without a `ResourceNode` remain outside the index
and simulation state.
Player resource interaction uses the same radius-bounded loaded-anchor grid.
HUD proximity probes, the time dial, player status, and day/night environment
are presentation-only and update at human-readable rates rather than every
rendered frame. `JajceWorld` exposes reversible High, Balanced, and Low
presentation profiles with instance-local mutable render resources and owned
viewport scaling. The default Balanced tier reduces 3D scale, grass, shadow
coverage, and volumetric work, while Low can stop grass/effects without changing
simulation state or UI resolution. Forward+-only volumetric fog stays disabled
under Mobile and Compatibility feature selection, even when High is requested.
Outside the runtime lifecycle, `simulation/benchmark/` owns reusable,
schema-valid workload fixtures. CLI tools and headless scenarios consume those
fixtures; production simulation does not depend on benchmark code.
+21
View File
@@ -419,6 +419,18 @@ grass, conifers, richer deciduous silhouettes, and oversized butterflies add
life without changing simulation authority. The matched review lives in
[`baselines/JAJCE_CENTER_02.md`](baselines/JAJCE_CENTER_02.md).
The world now also has a structural weak-PC contract. High restores that
authored look; Balanced is the runtime default at 85% 3D scale with ordinary
fog, shorter two-split shadows, roughly half the grass, fewer interactors, and
no volumetric fog; Low uses 70% 3D scale, one short shadow region, disables
glow/volumetric fog/color adjustment, and stops grass rendering and processing.
Use `-- --quality=high|balanced|low` for reproducible runs. Headless regression
proves the profiles are reversible, presentation-only, resource-isolated, and
warning-clean under Compatibility feature selection. This does not exercise a
real OpenGL GPU because headless Godot uses its dummy renderer. A representative
weak-PC High/Low capture with CPU/GPU p50/p95, memory, and draw statistics is
still required before claiming the visual frame-time exit condition.
### Exit condition
The lookdev scene can produce a compelling 1020 second clip with no gameplay.
@@ -854,6 +866,15 @@ Completed:
49. `Jajce Seasons 16`: a deterministic four-day seasonal cycle halves gather
yields and pauses berry regrowth on the cold day, so the garden keeps
producing real scarcity the player can help address.
50. Runtime health and weak-PC pass: the default Balanced presentation tier
removes volumetric fog, halves grass density, lowers 3D scale, and shortens
shadows; Low stops grass and costly effects while High remains reversible.
UI/environment probes are rate-limited, failed creature paths back off,
player resource prompts use the loaded-anchor grid, and simulation/combat
ID callbacks use disposable indexes. Combat authority now enforces range,
faction, unscaled player cooldown, recovery, melee approach, death ordering,
and real pantry plunder. Benchmark workload v2 validates one combatant per synthetic
NPC and records 90.63 ticks/s at 600 records with stable checksums.
Next:
+18
View File
@@ -474,6 +474,13 @@ keeps identical final checksums after one reusable per-tick population view and
improves that case from 65.84 to 85.81 ticks per second. Revisit the target when
world presentation or LOD enters the measured workload.
[Baseline 03](benchmarks/SIMULATION_SCALING_BASELINE_03.md) versions the
workload after combat was added and corrects its fixture to require one matching
combatant per synthetic NPC. Indexed callbacks and cached combatant iteration
keep the now-honest 600-record workload at 90.63 ticks per second with identical
checksums across three samples. It remains data-only; rendered scale still
requires the active-visual workload described by this milestone.
[Loaded Resource Discovery 01](benchmarks/LOADED_RESOURCE_DISCOVERY_01.md)
proves the first real spatial consumer. Exact loaded-anchor resolution remains
near four inspected candidates from 18 through 1,800 resources, preserves every
@@ -1015,6 +1022,17 @@ Headless tests cover combat determinism, wolf attacks, enemy definitions,
player downing/recovery, defend duty, war motivation, raid-to-resolution
chains, player kills, and save/restore.
The subsequent runtime health pass tightened those contracts: recovery revives
both player records, hostile strikes require melee reach and deterministic
approach, NPC death listeners see completed authoritative death state, player
attacks revalidate faction/exact weapon reach and use an unscaled real-time
cooldown, raids withdraw actual pantry food, and dash has one `CharacterBody3D`
movement owner with separate duration and re-entry cooldown. Presentation hot paths are
bounded through the existing spatial/population indexes, 510 Hz UI/environment
updates, cached combatant order, and navigation retry backoff. High, Balanced,
and Low profiles now prove a reversible weak-PC structural budget; real
weak-hardware frame-time/GPU/memory measurement remains intentionally open.
Recently completed:
- `Jajce Villager Field Note 12`: a separate player-facing note selects the
+10
View File
@@ -189,6 +189,11 @@ study, and patrol target typed activity sites. The migration is documented in
- **Engine:** Godot 4.7 project configuration
- **Renderer feature:** Forward Plus
- **Presentation budget:** reversible High/Balanced/Low profiles; Balanced is
the default and Low can disable costly effects/grass without changing UI or
simulation state (`-- --quality=high|balanced|low`). Mutable render resources
and root-viewport scale have explicit world-instance ownership, and unsupported
volumetric fog is never enabled outside Forward+.
- **Language:** GDScript (`gdformat`, `gdlint`, headless Godot 4.7 scenarios,
and GUT run through the local quality gate)
- **Main scene:** `res://main.tscn`
@@ -254,6 +259,11 @@ plugin content, not game architecture.
the navigation mesh through a shared `CreatureVisual` root, and enemies are
data-driven `EnemyDefinition`s so new types add with a definition plus a
visual hook.
- Runtime safety now revalidates player attack faction, distance, and cooldown;
wolves move into real claw reach before damage; recovery restores combat
targetability; and raids remove authoritative pantry food. The player owns
the only `move_and_slide()` call, so dashes do not double-step. Player combat
cooldowns use unscaled real time while deterministic NPC combat remains ticked.
- `Escape` releases captured mouse input.
### Village simulation
+3
View File
@@ -32,6 +32,9 @@ Reviewed captures:
- [Simulation scaling baseline 02](SIMULATION_SCALING_BASELINE_02.md) records
checksum-identical results after the shared per-tick population view: the
600-NPC case is 23.3% faster, while small fixtures expose its fixed cost.
- [Simulation scaling baseline 03](SIMULATION_SCALING_BASELINE_03.md) versions
the workload after conflict was added, requires one authoritative combatant
per synthetic NPC, and records 90.63 ticks/s at 600 full-fidelity records.
- [Loaded Resource Discovery 01](LOADED_RESOURCE_DISCOVERY_01.md) compares the
former all-node scan with the exact resource-anchor grid at 18, 180, and
1,800 loaded sources.
@@ -0,0 +1,53 @@
# Simulation scaling baseline 03
This is the first reviewed capture of benchmark workload schema 2,
`full_fidelity_combatant_headless_arrival_v2`. Unlike baselines 01 and 02, the
fixture now gives every synthetic NPC exactly one authoritative combatant
record and rejects missing, duplicate, orphaned, or mismatched coverage. It
also exercises the current state-schema 14 conflict tick.
The machine-readable samples are in
[`simulation_scaling_baseline_03.json`](simulation_scaling_baseline_03.json).
They were captured on the same `Apple_M1_Max_64_GB` host label with Godot 4.7,
seed 8088, 10 warmup ticks, 200 measured ticks, and three samples per case.
## Results
| Case | NPCs | Seeded history | Median us/tick | Ticks/s | Simulated realtime | Arrival share | End JSON |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| Population 6 | 6 | 0 | 114.42 | 8,740.11 | 10,488.1x | 3.4% | 0.06 MiB |
| Population 60 | 60 | 0 | 904.73 | 1,105.30 | 1,326.4x | 3.0% | 0.58 MiB |
| Population 600 | 600 | 0 | 11,033.58 | 90.63 | 108.8x | 2.8% | 5.83 MiB |
| History 600 | 60 | 600 | 992.78 | 1,007.28 | 1,208.7x | 2.8% | 0.72 MiB |
| History 6,000 | 60 | 6,000 | 1,667.58 | 599.67 | 719.6x | 1.7% | 2.06 MiB |
Every sample in every case produced the same final checksum. The 600-person
fixture sustains about 90.6 ticks per second—well above the 50-tick local
reference—while updating all 600 combatants. Indexed manager callbacks reduce
the deterministic immediate-arrival phase to 2.8% of that case; cached
combatant ID ordering separately bounds work inside the simulation phase.
## Versioning decision
Do not compare baseline-03 checksums or timings directly with baselines 01/02.
Those reports predate combat and accidentally retained only the manager's six
initial combatants after replacing the synthetic population. The workload ID
and schema are bumped so this semantic change is explicit. Baselines 01/02
remain useful historical evidence for the population-view optimization, but
baseline 03 is the valid comparison point for future full-fidelity simulation
changes.
This still excludes world scenes, rendering, navigation, and `NpcVisual`.
High/low gameplay-camera frame-time and GPU/memory captures on representative
weak hardware remain a separate presentation ledger task; the data-only result
does not claim rendered 600-NPC support.
## Capture command
```bash
/Applications/Godot.app/Contents/MacOS/Godot \
--headless --path "$PWD" \
--script res://tools/benchmark_simulation_scaling.gd -- \
--host-label=Apple_M1_Max_64_GB \
--output=res://docs/benchmarks/simulation_scaling_baseline_03.json
```
@@ -0,0 +1,324 @@
{
"benchmark_seed": 8088,
"captured_utc": "2026-08-12T07:08:36Z",
"cases": [
{
"arrival_share_percent": 3.36057335139623,
"arrival_usec_median": 769,
"arrival_usec_samples": [
740,
769,
855
],
"arrivals_processed": 209,
"case_id": "population_006",
"elapsed_usec_max": 23503,
"elapsed_usec_median": 22883,
"elapsed_usec_min": 22033,
"elapsed_usec_samples": [
22033,
22883,
23503
],
"end_event_count": 221,
"end_known_reference_count": 0,
"end_state_bytes": 60818,
"end_tick": 210,
"events_recorded": 209,
"final_checksum": "1020a2e4b05d77cf61b01490404e910c1e212a07a331ea6d16483a48e283cf4d",
"history_seed_events": 0,
"measured_ticks": 200,
"npc_combatant_count": 6,
"npc_combatant_coverage_valid": true,
"npc_updates": 1200,
"population": 6,
"realtime_factor_median": 10488.1352969453,
"sample_count": 3,
"schema_version": 2,
"seed": 8088,
"setup_usec_median": 1257,
"setup_usec_samples": [
1005,
1257,
4207
],
"simulation_state_schema_version": 14,
"simulation_usec_median": 22091,
"simulation_usec_samples": [
21276,
22091,
22630
],
"start_event_count": 12,
"start_known_reference_count": 0,
"start_state_bytes": 9863,
"start_tick": 10,
"state_growth_bytes": 50955,
"tick_interval": 1.2,
"ticks_per_second_median": 8740.11274745444,
"usec_per_tick_median": 114.415,
"warmup_arrivals": 12,
"warmup_ticks": 10,
"workload_id": "full_fidelity_combatant_headless_arrival_v2"
},
{
"arrival_share_percent": 2.98210515844506,
"arrival_usec_median": 5396,
"arrival_usec_samples": [
5376,
5396,
5430
],
"arrivals_processed": 2090,
"case_id": "population_060",
"elapsed_usec_max": 181114,
"elapsed_usec_median": 180946,
"elapsed_usec_min": 178439,
"elapsed_usec_samples": [
178439,
180946,
181114
],
"end_event_count": 2210,
"end_known_reference_count": 0,
"end_state_bytes": 604532,
"end_tick": 210,
"events_recorded": 2090,
"final_checksum": "d23a14828b34e93460d96079492eda19001e8b91c589b01d7d454feaed561493",
"history_seed_events": 0,
"measured_ticks": 200,
"npc_combatant_count": 60,
"npc_combatant_coverage_valid": true,
"npc_updates": 12000,
"population": 60,
"realtime_factor_median": 1326.36256120611,
"sample_count": 3,
"schema_version": 2,
"seed": 8088,
"setup_usec_median": 6193,
"setup_usec_samples": [
6161,
6193,
6303
],
"simulation_state_schema_version": 14,
"simulation_usec_median": 175483,
"simulation_usec_samples": [
173040,
175483,
175703
],
"start_event_count": 120,
"start_known_reference_count": 0,
"start_state_bytes": 88474,
"start_tick": 10,
"state_growth_bytes": 516058,
"tick_interval": 1.2,
"ticks_per_second_median": 1105.30213433842,
"usec_per_tick_median": 904.73,
"warmup_arrivals": 120,
"warmup_ticks": 10,
"workload_id": "full_fidelity_combatant_headless_arrival_v2"
},
{
"arrival_share_percent": 2.79292976211246,
"arrival_usec_median": 61632,
"arrival_usec_samples": [
60845,
61632,
61770
],
"arrivals_processed": 20950,
"case_id": "population_600",
"elapsed_usec_max": 2232943,
"elapsed_usec_median": 2206715,
"elapsed_usec_min": 2205128,
"elapsed_usec_samples": [
2205128,
2206715,
2232943
],
"end_event_count": 22150,
"end_known_reference_count": 0,
"end_state_bytes": 6116918,
"end_tick": 210,
"events_recorded": 20950,
"final_checksum": "fa32206742bb2f7ca894747fbee8a9b5720ed33ce3e13b938363a9613aaaa9f9",
"history_seed_events": 0,
"measured_ticks": 200,
"npc_combatant_count": 600,
"npc_combatant_coverage_valid": true,
"npc_updates": 120000,
"population": 600,
"realtime_factor_median": 108.758947122759,
"sample_count": 3,
"schema_version": 2,
"seed": 8088,
"setup_usec_median": 55643,
"setup_usec_samples": [
55267,
55643,
57129
],
"simulation_state_schema_version": 14,
"simulation_usec_median": 2145800,
"simulation_usec_samples": [
2143434,
2145800,
2171102
],
"start_event_count": 1200,
"start_known_reference_count": 0,
"start_state_bytes": 881819,
"start_tick": 10,
"state_growth_bytes": 5235099,
"tick_interval": 1.2,
"ticks_per_second_median": 90.6324559356328,
"usec_per_tick_median": 11033.575,
"warmup_arrivals": 1200,
"warmup_ticks": 10,
"workload_id": "full_fidelity_combatant_headless_arrival_v2"
},
{
"arrival_share_percent": 2.77656065070132,
"arrival_usec_median": 5513,
"arrival_usec_samples": [
5476,
5513,
5750
],
"arrivals_processed": 2090,
"case_id": "history_000600",
"elapsed_usec_max": 201427,
"elapsed_usec_median": 198555,
"elapsed_usec_min": 193650,
"elapsed_usec_samples": [
193650,
198555,
201427
],
"end_event_count": 2810,
"end_known_reference_count": 0,
"end_state_bytes": 760212,
"end_tick": 810,
"events_recorded": 2090,
"final_checksum": "d727bf8a8f841d634796258755cb32c2a73b5d512a33494f8372cc74dfb8677b",
"history_seed_events": 600,
"measured_ticks": 200,
"npc_combatant_count": 60,
"npc_combatant_coverage_valid": true,
"npc_updates": 12000,
"population": 60,
"realtime_factor_median": 1208.7330966231,
"sample_count": 3,
"schema_version": 2,
"seed": 8088,
"setup_usec_median": 25277,
"setup_usec_samples": [
24952,
25277,
26430
],
"simulation_state_schema_version": 14,
"simulation_usec_median": 193021,
"simulation_usec_samples": [
188158,
193021,
195651
],
"start_event_count": 720,
"start_known_reference_count": 180,
"start_state_bytes": 270294,
"start_tick": 610,
"state_growth_bytes": 489918,
"tick_interval": 1.2,
"ticks_per_second_median": 1007.27758051925,
"usec_per_tick_median": 992.775,
"warmup_arrivals": 120,
"warmup_ticks": 10,
"workload_id": "full_fidelity_combatant_headless_arrival_v2"
},
{
"arrival_share_percent": 1.65569268041113,
"arrival_usec_median": 5522,
"arrival_usec_samples": [
5430,
5522,
5548
],
"arrivals_processed": 2090,
"case_id": "history_006000",
"elapsed_usec_max": 336114,
"elapsed_usec_median": 333516,
"elapsed_usec_min": 328622,
"elapsed_usec_samples": [
328622,
333516,
336114
],
"end_event_count": 8210,
"end_known_reference_count": 0,
"end_state_bytes": 2158824,
"end_tick": 6210,
"events_recorded": 2090,
"final_checksum": "60fe6c481e67e4937a835c76f9a7feb80b007db50c06598bddf8cb643e6b1909",
"history_seed_events": 6000,
"measured_ticks": 200,
"npc_combatant_count": 60,
"npc_combatant_coverage_valid": true,
"npc_updates": 12000,
"population": 60,
"realtime_factor_median": 719.605656100457,
"sample_count": 3,
"schema_version": 2,
"seed": 8088,
"setup_usec_median": 156396,
"setup_usec_samples": [
154919,
156396,
157845
],
"simulation_state_schema_version": 14,
"simulation_usec_median": 328069,
"simulation_usec_samples": [
323051,
328069,
330564
],
"start_event_count": 6120,
"start_known_reference_count": 180,
"start_state_bytes": 1666896,
"start_tick": 6010,
"state_growth_bytes": 491928,
"tick_interval": 1.2,
"ticks_per_second_median": 599.671380083714,
"usec_per_tick_median": 1667.58,
"warmup_arrivals": 120,
"warmup_ticks": 10,
"workload_id": "full_fidelity_combatant_headless_arrival_v2"
}
],
"engine_version": "4.7-stable (official)",
"exclusions": [
"manager_and_fixture_setup",
"state_serialization",
"world_scene",
"rendering",
"navigation",
"npc_visual"
],
"host_label": "Apple_M1_Max_64_GB",
"measured_ticks": 200,
"platform": "macOS",
"processor_count": 10,
"sample_count": 3,
"schema_version": 2,
"simulation_state_schema_version": 14,
"timed_phases": [
"simulation_tick",
"headless_arrival_completion"
],
"warmup_ticks": 10,
"workload": "All NPCs receive full per-tick needs/task updates, ordinary action decisions, and one matching authoritative combatant record; travel resolves through the deterministic immediate-arrival headless convention.",
"workload_id": "full_fidelity_combatant_headless_arrival_v2"
}
+13
View File
@@ -190,3 +190,16 @@ portable watchdog on stock macOS as well as Linux/Windows, and nonzero
`gdformat`/`gdlint` exits fail the gate even when their output is not a familiar
diagnostic string. Import receives a 60-second timeout on both shell and
PowerShell; the focused checks retain their 30-second timeout.
Scenario logs are also classified after successful process exits. Unexpected
`ERROR:`, `SCRIPT ERROR:`, and `WARNING:` headers fail the gate and name their
originating scenario. The only narrow platform/vendor exceptions are the
isolated-profile macOS certificate-store condition, Terrain3D 1.0.2's Godot 4.7
interpolation deprecation, and the dummy renderer's exact one-`DummyShader` RID
exit report. Different warning text, leak counts, or RID types remain failures;
the full unfiltered output stays in `logs/quality/latest/scenarios.log`.
The scenario stage also runs the presentation-quality contract once with
`--rendering-method gl_compatibility`. This catches unsupported feature toggles
and profile-selection regressions; because the gate is headless, it is not a
substitute for a rendered OpenGL frame-time capture on representative hardware.