feat: scale village queries and enrich Jajce center

This commit is contained in:
Rijad Zuzo
2026-07-16 23:51:08 +02:00
parent 4be72639d8
commit 2c88ed6ea8
48 changed files with 2059 additions and 280 deletions
+9 -1
View File
@@ -5,6 +5,7 @@
```text
SimulationManager tick
-> ActionExecutionSystem advances needs and working progress
-> SimulationPopulationView refreshes transient living/starving indexes
-> VillageOpportunitySystem re-derives the current capable helper
-> ActionSelectionSystem chooses an action when the NPC is idle
-> SimulationManager stores the selected action
@@ -25,6 +26,8 @@ SimulationManager tick
- queries `RelationshipSystem` after personal survival/schedule overrides, so a
trusted starving acquaintance can redirect ordinary work toward food
gathering while the pantry is low;
- consumes the manager's current `SimulationPopulationView` for that social
query instead of rebuilding an all-NPC lookup for each idle decision;
- accepts the current ephemeral `OpportunityHelperResult` and, only for its
matching NPC, selects the reported ordinary gather/deposit action after
urgent self-care and sleep/meal schedule branches;
@@ -71,7 +74,10 @@ SimulationManager tick
SimulationManager remains the orchestrator and event boundary. It owns
simulation records, invokes the focused systems, stores selected actions and
targets, and translates presentation callbacks into simulation transitions.
It also publishes the latest `ActionSelectionResult` for presentation; the UI
It also owns one disposable `SimulationPopulationView`, rebuilt at tick start
and refreshed after each NPC advances so interleaved decision semantics remain
unchanged.
The manager publishes the latest `ActionSelectionResult` for presentation; the UI
does not recompute decisions. Each idle selection re-derives the capable helper
instead of consulting persisted assignment state. At completion it atomically
pays any definition-backed stored-resource cost before applying the action
@@ -128,6 +134,8 @@ simulation ticks.
- stores the exact event ID as the trust cause and ignores replayed or older
events;
- selects trusted starving subjects deterministically for action selection;
- uses a supplied per-tick population view when available, with a compatibility
fallback for isolated callers;
- does not choose actions, perform transactions, or format presentation text.
These collaborators are `RefCounted` rule services, not additional scene-tree
+12
View File
@@ -10,6 +10,7 @@ results.
```text
SimulationClock
-> SimulationManager orchestrates one deterministic tick
-> SimulationPopulationView indexes all/living/starving NPCs by stable ID
-> ActionExecutionSystem advances needs and work
-> VillageOpportunitySystem re-derives the current capable helper
-> ActionSelectionSystem chooses an action
@@ -42,6 +43,10 @@ would otherwise obscure that lifecycle:
first without weakening its direct-source or one-hop checks;
- `simulation/relationships/RelationshipSystem.gd` owns directed relationship
queries, event-driven trust changes, and deterministic social tie-breaking;
- `simulation/population/SimulationPopulationView.gd` owns a transient derived
all/living/starving stable-ID index. The manager rebuilds it once at the tick
boundary and refreshes it after each interleaved NPC update; relationship and
action queries may read it, but it never enters save records;
- `simulation/opportunities/VillageOpportunitySystem.gd` observes immutable
event references plus current NPC/storage state, then owns the bounded shared
open/resolved/invalidated lifecycle for the proven pantry-food and blocked-
@@ -76,6 +81,7 @@ hard to read.
| `simulation/events/` | Immutable event history and derived event queries |
| `simulation/knowledge/` | Per-NPC knowledge of objective event IDs |
| `simulation/relationships/` | Directed social consequences and relationship queries |
| `simulation/population/` | Transient per-tick population query indexes |
| `simulation/opportunities/` | Knowledge-gated unresolved-condition projections |
| `simulation/state/` | Versioned, serializable mutable records |
| `simulation/definitions/` | Stable IDs and immutable gameplay definitions |
@@ -118,6 +124,12 @@ improving ownership.
- Benchmark fixtures use ordinary simulation records and must round-trip
through the current state schema. They may control workload setup, but must
not add benchmark-only fields or branches to production saves and ticks.
- Derived population indexes are disposable acceleration structures. NPC
records remain authoritative, and rebuilding the view after initialization,
restore, or a tick must produce the same decisions and checksum.
- Camera-local grass, ambient butterflies, water animation, smoke, and foliage
motion are presentation only. Grass consumes bounded real actor transforms;
it does not write NPC positions or become persistent state.
- Opportunity records reference stable NPC, storage, resource, trigger-event,
and resolution-event IDs. Their generator may observe authoritative state
and history, but it does not mutate the economy or command NPC behavior. The
+23 -5
View File
@@ -412,6 +412,13 @@ coverage validates the texture contract, presentation nodes, terrain shape,
stable resources, and navigation. Clip capture and a visual performance review
remain presentation checkpoints rather than missing implementation.
`Jajce Center 02` adds the first reviewed composition pass beyond that baseline:
the summit citadel and residential roof amphitheater read as one center, the
waterfall feeds a Terrain3D-sampled turquoise ribbon, and bounded camera-local
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).
### Exit condition
The lookdev scene can produce a compelling 1020 second clip with no gameplay.
@@ -755,14 +762,25 @@ Completed:
NPC and event counts. The first reviewed capture sets a local 600-NPC target,
records identical checksums across samples, and exposes repeated scarce-food
population scans plus objective-event growth as the next measured work.
33. Shared population query view: stable-ID all/living/starving indexes are
built once per deterministic tick and refreshed through interleaved NPC
updates. Baseline 02 preserves every checksum and improves the 600-NPC case
by 23.3% without simulation LOD.
34. `Jajce Center 02`: the reviewed matched capture strengthens the citadel,
cascading homes, bridge, waterfall, terrain-following river, conifer/tree
clusters, and ambient butterflies. Roughly 10,400 camera-local grass
particles remain short and patchy and bend from real actor transforms.
Next:
1. Reuse one stable per-tick population view in the existing
trusted-starving-subject query, preserve exact action/checksum behavior, and
rerun simulation scaling baseline 01. Let the resulting profile determine
whether the following slice needs another bounded query optimization, a
spatial index, or the first active/abstract LOD contract.
1. Measure and implement a loaded-resource spatial query through
`ActiveWorldAdapter` for far-apart Terrain3D-authored finite sources. Keep
simulation-owned resource records, stable IDs, current score/reachability,
reservations, player parity, and unload/rebind behavior unchanged.
2. Sculpt a real Terrain3D river channel and waterfall shelf before adding a
broad foliage asset pass. Use Terrain3D particles for nearby grass and
intentional instancing for tree/fern/rock masses; add livestock only with a
real identity and interaction contract.
Do not start with GIS data, a full city, a large asset pack, or more NPC
mechanics. The next proof is a beautiful stage for the systems that already
+30 -8
View File
@@ -469,8 +469,10 @@ important history.
[Simulation scaling baseline 01](benchmarks/SIMULATION_SCALING_BASELINE_01.md)
sets the first local target at 600 data-only full-fidelity NPCs while preserving
deterministic state. Revisit that target when world presentation or LOD enters
the measured workload.
deterministic state. [Baseline 02](benchmarks/SIMULATION_SCALING_BASELINE_02.md)
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.
## Milestone 9 — Player interaction and social agency
@@ -862,15 +864,35 @@ workload exclusions, checksums, and local 50-ticks-per-second reference target
live in
[Simulation scaling baseline 01](benchmarks/SIMULATION_SCALING_BASELINE_01.md).
The immediate next slice should build one bounded per-tick population view for
the existing trusted-starving-subject query. That scarce-food path currently
rebuilds an all-NPC lookup for each applicable idle decision, making it the
first code-level candidate consistent with the measured superlinear growth.
Preserve exact selection/checksum behavior and rerun the same ledger before
choosing a spatial index or active/abstract LOD design.
The first measured optimization is also complete. `SimulationPopulationView`
builds stable-ID all/living/starving indexes once per tick, refreshes them after
each interleaved NPC update, and serves relationship/action queries without
entering saved state. Every baseline-02 checksum exactly matches baseline 01.
The 600-NPC case falls from 15,187.74 to 11,654.10 microseconds per tick, a
23.3% improvement, while small fixtures expose the expected fixed dictionary
cost. See
[Simulation scaling baseline 02](benchmarks/SIMULATION_SCALING_BASELINE_02.md).
The immediate next systems slice should now prove spatial discovery for the
world vision: index loaded finite `ResourceNode` anchors through
`ActiveWorldAdapter` so Terrain3D-authored sources can be placed much farther
apart without an all-node scan for every resolution. Preserve stable IDs,
current distance/risk/comfort/priority scoring, reachability, reservations,
player parity, unload/rebind authority, and exact deterministic outcomes. First
measure 18, 180, and 1,800 loaded candidates; do not choose a generic spatial
framework or active/abstract LOD mode before that real consumer proves the
contract.
Recently completed:
- Shared population query view: one transient per-tick all/living/starving
index replaces repeated scarce-food relationship scans, preserves exact
checksums, and improves the reviewed 600-NPC workload by 23.3%.
- `Jajce Center 02`: matched before/after captures now document a larger summit
citadel, cascading timber-plaster homes, terrain-following turquoise river,
rock-framed waterfall, richer tree silhouettes, restrained conifer clusters,
oversized butterflies, and short camera-local grass that bends around real
player/NPC transforms without becoming simulation state.
- Deterministic simulation scaling baseline: schema-valid full-fidelity
fixtures, a repeatable CLI runner, and a fast headless regression now record
population throughput, phase timing, serialized-state growth, history
+14 -5
View File
@@ -2,7 +2,7 @@
> Agent-facing context for understanding the project quickly.
>
> Snapshot basis: repository state on July 14, 2026. Treat the code as the
> Snapshot basis: repository state on July 16, 2026. Treat the code as the
> source of truth when this document and the implementation differ.
See the [documentation map](README.md) for the authority and scope of each plan.
@@ -193,12 +193,15 @@ study, and patrol target typed activity sites. The migration is documented in
and GUT run through the local quality gate)
- **Main scene:** `res://main.tscn`
- **Terrain:** Terrain3D 1.0.2 is installed and enabled
- **Jajce runtime:** reusable 512 m Terrain3D seed, greybox landmarks, stable
ResourceNode placement, lookdev camera, and Terrain3D-derived runtime
navigation with collision guardrails
- **Jajce runtime:** reusable 512 m Terrain3D seed, stable ResourceNode
placement, larger citadel/village-center composition, terrain-following river
ribbon, reactive camera-local grass, and Terrain3D-derived runtime navigation
with collision guardrails
- **Lookdev baseline:** `Jajce Lookdev 01` is captured at
`docs/baselines/jajce_lookdev_01.png` with
`tools/capture_jajce_lookdev.gd`
- **Matched center baseline:** `Jajce Center 02` preserves a fixed before/after
overview plus a close grass-readability capture under `docs/baselines/`
- **Runtime baseline:** `Simulation Garden 01` is captured as paired debug and
cinematic `main.tscn` images plus an empty/restocked pantry close pair under
`docs/baselines/` with `tools/capture_simulation_garden.gd`
@@ -336,13 +339,16 @@ distance with resource `safety_risk`, `comfort_distance`, and
- a centered 512 m Terrain3D landscape split across four regions;
- deterministic shaped terrain data and six game-owned surface layers;
- fortress, houses, mill, bridge, shader river/waterfall, mist, and smoke;
- a larger summit citadel, eight cascading houses, mill, full-span bridge,
rock-framed waterfall, mist, smoke, and a Terrain3D-sampled river ribbon;
- authored dirt path strips that make the current village task loop readable;
- compact silhouette props for the ridge landmark, guard, study, and rest
sites, plus a physical pantry whose empty/low/stocked arrangement follows
authoritative food storage;
- warm sky, fog, shadows, coherent wind-reactive foliage, and breeze-aligned
chimney smoke;
- short patchy camera-local grass that receives at most eight real player/NPC
transforms, authored conifer clusters, and oversized ambient butterflies;
- one sparse terrain-aware calligraphic gust field with two or three soft
tapered strokes per burst;
- eighteen ResourceNodes preserving stable food/wood discovery IDs;
@@ -418,6 +424,8 @@ views, modifiers, and priorities.
Focused `RefCounted` collaborators keep rule ownership visible:
- action systems own selection, execution progress, and target resolution;
- `SimulationPopulationView` owns the disposable all/living/starving stable-ID
query indexes rebuilt at deterministic tick boundaries;
- `VillageEconomy` owns storage/inventory transactions and synchronized
village resource views;
- `SimulationEventLog` owns deterministic event history and queries;
@@ -545,6 +553,7 @@ NpcVisual navigates through the active world
│ ├── events/ Ordered event history and queries
│ ├── knowledge/ Per-NPC facts, provenance, transfer, and retention
│ ├── opportunities/ Knowledge-gated unresolved-condition projection
│ ├── population/ Transient per-tick NPC query indexes
│ ├── persistence/ Validated local save-slot storage
│ ├── relationships/ Directed social consequences and queries
│ └── state/ Versioned simulation-state records
+95
View File
@@ -0,0 +1,95 @@
# Jajce Center 02
Captured: 2026-07-16
## Files
- [before overview](jajce_center_02_before.png)
- [after overview](jajce_center_02_after.png)
- [close gameplay grass check](jajce_grass_gameplay_02.png)
- `tools/capture_jajce_lookdev.gd`
The two overview images use the same 1280×720 camera, fixed daylight value,
warmup, and Metal renderer. The gameplay image is a separate readability check,
not part of the matched comparison.
## Reference reading
The reduction uses Jajce's hierarchy rather than attempting a literal city
reconstruction:
- the [UNESCO tentative-list description](https://whc.unesco.org/en/tentativelists/2098/)
describes the citadel at the summit of a rocky pyramid, ramparts descending
its slopes, and residential roofs forming an amphitheater beneath it;
- the [official Jajce fortress overview](https://www.agencija-jajce.ba/en/the-fortress/)
supports treating the fortress as the primary long-distance landmark;
- official Studio Ghibli still collections for
[My Neighbor Totoro](https://www.ghibli.jp/works/totoro/) and
[Princess Mononoke](https://www.ghibli.jp/works/mononoke/) were used only as
broad references for layered green masses, clear silhouettes, restrained
natural motion, and small warm human details inside large landscapes;
- [Tiny Glade](https://store.steampowered.com/app/2198150/Tiny_Glade/) supports
the low-detail diorama reading of whimsical castles, cottages, ruins, and
incidental life;
- the [Terrain3D instancer guidance](https://terrain3d.readthedocs.io/en/latest/docs/instancer.html)
informed the split between camera-local particle grass and intentional tree
placement.
No building, frame, or asset is copied from those references. The project keeps
its own compact low-poly language and real simulation constraints.
## Implemented reduction
- The ridge block becomes a larger warm-stone citadel with a rocky plinth,
ramparts, descending walls, towers, gate, crenellations, banner, and narrow
window slits.
- Eight timber-accented plaster houses now cascade below the citadel instead of
leaving three isolated boxes in an empty field.
- The waterfall is wider, softly animated, misted, and framed by irregular
mossy rock columns. Its plunge pool feeds a turquoise ribbon that samples the
live Terrain3D height field instead of disappearing beneath it.
- The bridge gains a full river span, stone abutments, center pier, rails, and
posts. House windows and chimney smoke add small warm points.
- Existing rounded deciduous trees are richer and wind-reactive; authored
conifer clusters strengthen the citadel, falls, and valley edges.
- Three intentionally oversized butterflies add quiet ambient life.
- A bounded camera-local Terrain3D grass field uses roughly 10,400 particles.
Its project-owned process material keeps blades short and patchy, while a
shader receives at most eight real player/NPC transforms and bends grass
away from their feet.
- Haze and saturation are reduced so green terrain, warm roofs, stone, and
turquoise water separate more clearly.
## Authority boundary
Grass, butterflies, smoke, water motion, and foliage sway are transient
presentation. They do not enter saves or checksums. Player and `NpcVisual`
nodes merely expose real transforms to the grass shader.
Finite food and wood still come only from stable-ID `ResourceNode` anchors
bound to simulation-owned records. The conifers and grass are decorative; the
next resource-discovery phase can pair selected Terrain3D-authored visuals with
intentional resource anchors without turning every blade or background tree
into authoritative state. Livestock is deliberately deferred until one animal
has a real identity/state/interaction contract; ambient butterflies do not
pretend to be simulated food or work.
## Remaining visual work
This is a stronger prototype composition, not final environment art. The next
art pass should sculpt a true river channel and waterfall shelf into Terrain3D,
replace procedural fortress/house geometry gradually, add bank stones and
flower/fern clusters through bounded instancing, and validate a representative
frame-time budget. The matched comparison should remain the checkpoint before
those asset-heavy changes.
## Capture command
```bash
HOME="$PWD/logs/quality/godot_profile" \
APPDATA="$PWD/logs/quality/godot_profile" \
LOCALAPPDATA="$PWD/logs/quality/godot_profile" \
/Applications/Godot.app/Contents/MacOS/Godot \
--path "$PWD" --script res://tools/capture_jajce_lookdev.gd -- \
--output=res://docs/baselines/jajce_center_02_after.png
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 729 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dcg13u5wqr23m"
path="res://.godot/imported/jajce_center_02_after.png-be0a331c22c145d626fa09f682c7e379.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_center_02_after.png"
dest_files=["res://.godot/imported/jajce_center_02_after.png-be0a331c22c145d626fa09f682c7e379.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dipu1epmps356"
path="res://.godot/imported/jajce_center_02_before.png-a6dad6237a960278853b124b5c1180c2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_center_02_before.png"
dest_files=["res://.godot/imported/jajce_center_02_before.png-a6dad6237a960278853b124b5c1180c2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 553 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://du1awnpktb7q0"
path="res://.godot/imported/jajce_grass_gameplay_02.png-4b072b6e4f1c146d62492370903db793.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/baselines/jajce_grass_gameplay_02.png"
dest_files=["res://.godot/imported/jajce_grass_gameplay_02.png-4b072b6e4f1c146d62492370903db793.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+3
View File
@@ -21,3 +21,6 @@ Reviewed captures:
- [Simulation scaling baseline 01](SIMULATION_SCALING_BASELINE_01.md) records
the first full-fidelity population/history measurements and the bounded next
optimization.
- [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.
@@ -0,0 +1,70 @@
# Simulation scaling baseline 02
This is the reviewed comparison after introducing one reusable per-tick
`SimulationPopulationView`. The view indexes all, living, and starving NPCs by
stable ID, is refreshed as each NPC advances, and is shared by relationship and
action-selection queries. It is transient derived state and is not serialized.
The machine-readable samples are in
[`simulation_scaling_baseline_02.json`](simulation_scaling_baseline_02.json).
The workload, seed, host, exclusions, warmup, and three-sample median contract
match [baseline 01](SIMULATION_SCALING_BASELINE_01.md).
## Results
| Case | NPCs | Seeded history | Median us/tick | Ticks/s | Simulated realtime | Arrival share | End JSON |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| Population 6 | 6 | 0 | 63.85 | 15,662.93 | 18,795.5x | 4.5% | 0.06 MiB |
| Population 60 | 60 | 0 | 662.62 | 1,509.17 | 1,811.0x | 5.7% | 0.56 MiB |
| Population 600 | 600 | 0 | 11,654.10 | 85.81 | 103.0x | 18.0% | 5.67 MiB |
| History 600 | 60 | 600 | 722.60 | 1,383.90 | 1,660.7x | 5.2% | 0.71 MiB |
| History 6,000 | 60 | 6,000 | 1,430.65 | 698.98 | 838.8x | 2.6% | 2.04 MiB |
All samples within each case were deterministic. More importantly, every final
checksum exactly matches the corresponding baseline-01 checksum, so the
optimization preserves selection, interleaved NPC updates, event growth, and
serialized continuation state.
## Comparison
| Case | Baseline 01 us/tick | Baseline 02 us/tick | Change |
| --- | ---: | ---: | ---: |
| Population 6 | 55.74 | 63.85 | 14.6% slower |
| Population 60 | 621.87 | 662.62 | 6.6% slower |
| Population 600 | 15,187.74 | 11,654.10 | **23.3% faster** |
| History 600 | 677.12 | 722.60 | 6.7% slower |
| History 6,000 | 1,421.19 | 1,430.65 | 0.7% slower |
The shared dictionaries add a small fixed per-tick cost, visible in tiny
fixtures and normal local timing noise. At the measured population knee they
remove repeated all-NPC lookup construction and improve the 600-NPC case from
65.84 to 85.81 ticks per second. The local 50-ticks-per-second reference target
remains comfortably met without introducing simulation LOD.
## Decision and next measurement
Keep the population view. It has a real relationship/action consumer, improves
the measured pressure point, and preserves exact state. Do not add batching or
active/abstract NPC modes merely to improve this data-only number.
The next bounded scale slice should support the world vision directly: a
loaded-resource spatial query owned by `ActiveWorldAdapter`. It should let
villagers discover finite, stable-ID `ResourceNode` anchors placed much farther
apart across Terrain3D while preserving the current score, reachability,
reservation, and save/rebind contracts. A benchmark should compare 18, 180,
and 1,800 loaded candidates before choosing a grid, tree, or other index.
Terrain3D-instanced visual trees and grass are not automatically authoritative
resources. Intentional harvest anchors must continue to bind simulation-owned
`ResourceStateRecord` data by stable ID; large decorative populations can stay
presentation-only until a gameplay rule needs them.
## 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_02.json
```
@@ -0,0 +1,308 @@
{
"benchmark_seed": 8088,
"captured_utc": "2026-07-16T21:38:42Z",
"cases": [
{
"arrival_share_percent": 4.47176756206437,
"arrival_usec_median": 571,
"arrival_usec_samples": [
563,
571,
579
],
"arrivals_processed": 209,
"case_id": "population_006",
"elapsed_usec_max": 12771,
"elapsed_usec_median": 12769,
"elapsed_usec_min": 12507,
"elapsed_usec_samples": [
12507,
12769,
12771
],
"end_event_count": 221,
"end_known_reference_count": 0,
"end_state_bytes": 58576,
"end_tick": 210,
"events_recorded": 209,
"final_checksum": "100dd5dc1e0c22ea23f35dd876d6f56fd5c33c58b1586b6fa9aec47ee1536511",
"history_seed_events": 0,
"measured_ticks": 200,
"npc_updates": 1200,
"population": 6,
"realtime_factor_median": 18795.5204009711,
"sample_count": 3,
"schema_version": 1,
"seed": 8088,
"setup_usec_median": 627,
"setup_usec_samples": [
546,
627,
6728
],
"simulation_usec_median": 12173,
"simulation_usec_samples": [
11916,
12173,
12186
],
"start_event_count": 12,
"start_known_reference_count": 0,
"start_state_bytes": 7632,
"start_tick": 10,
"state_growth_bytes": 50944,
"tick_interval": 1.2,
"ticks_per_second_median": 15662.9336674759,
"usec_per_tick_median": 63.845,
"warmup_arrivals": 12,
"warmup_ticks": 10,
"workload_id": "full_fidelity_headless_arrival"
},
{
"arrival_share_percent": 5.66845000490481,
"arrival_usec_median": 7512,
"arrival_usec_samples": [
7458,
7512,
7571
],
"arrivals_processed": 2090,
"case_id": "population_060",
"elapsed_usec_max": 133541,
"elapsed_usec_median": 132523,
"elapsed_usec_min": 130182,
"elapsed_usec_samples": [
130182,
132523,
133541
],
"end_event_count": 2210,
"end_known_reference_count": 0,
"end_state_bytes": 587324,
"end_tick": 210,
"events_recorded": 2090,
"final_checksum": "58c7f96fc5df7f5399fd82d1a9c212b3c53067e807059e88ffa484ba0eb41e16",
"history_seed_events": 0,
"measured_ticks": 200,
"npc_updates": 12000,
"population": 60,
"realtime_factor_median": 1811.00639134339,
"sample_count": 3,
"schema_version": 1,
"seed": 8088,
"setup_usec_median": 3788,
"setup_usec_samples": [
3481,
3788,
3917
],
"simulation_usec_median": 125039,
"simulation_usec_samples": [
122647,
125039,
125926
],
"start_event_count": 120,
"start_known_reference_count": 0,
"start_state_bytes": 71278,
"start_tick": 10,
"state_growth_bytes": 516046,
"tick_interval": 1.2,
"ticks_per_second_median": 1509.17199278616,
"usec_per_tick_median": 662.615,
"warmup_arrivals": 120,
"warmup_ticks": 10,
"workload_id": "full_fidelity_headless_arrival"
},
{
"arrival_share_percent": 17.9571567087978,
"arrival_usec_median": 418549,
"arrival_usec_samples": [
417929,
418549,
425849
],
"arrivals_processed": 20950,
"case_id": "population_600",
"elapsed_usec_max": 2346707,
"elapsed_usec_median": 2330820,
"elapsed_usec_min": 2326981,
"elapsed_usec_samples": [
2326981,
2330820,
2346707
],
"end_event_count": 22150,
"end_known_reference_count": 0,
"end_state_bytes": 5948447,
"end_tick": 210,
"events_recorded": 20950,
"final_checksum": "fa4efac63fde63f92eb28141bebd1922f665e85ffe7b7ac2b9702dae6e4c24b7",
"history_seed_events": 0,
"measured_ticks": 200,
"npc_updates": 120000,
"population": 600,
"realtime_factor_median": 102.968054161196,
"sample_count": 3,
"schema_version": 1,
"seed": 8088,
"setup_usec_median": 34966,
"setup_usec_samples": [
34505,
34966,
34980
],
"simulation_usec_median": 1912210,
"simulation_usec_samples": [
1908995,
1912210,
1920790
],
"start_event_count": 1200,
"start_known_reference_count": 0,
"start_state_bytes": 713361,
"start_tick": 10,
"state_growth_bytes": 5235086,
"tick_interval": 1.2,
"ticks_per_second_median": 85.8067118009971,
"usec_per_tick_median": 11654.1,
"warmup_arrivals": 1200,
"warmup_ticks": 10,
"workload_id": "full_fidelity_headless_arrival"
},
{
"arrival_share_percent": 5.24844484116275,
"arrival_usec_median": 7585,
"arrival_usec_samples": [
7540,
7585,
7604
],
"arrivals_processed": 2090,
"case_id": "history_000600",
"elapsed_usec_max": 145539,
"elapsed_usec_median": 144519,
"elapsed_usec_min": 144335,
"elapsed_usec_samples": [
144335,
144519,
145539
],
"end_event_count": 2810,
"end_known_reference_count": 0,
"end_state_bytes": 743004,
"end_tick": 810,
"events_recorded": 2090,
"final_checksum": "7ae86044f7683359d6bfbc8753e2656f3af90f23d040039154c6869dca48cf65",
"history_seed_events": 600,
"measured_ticks": 200,
"npc_updates": 12000,
"population": 60,
"realtime_factor_median": 1660.68129450107,
"sample_count": 3,
"schema_version": 1,
"seed": 8088,
"setup_usec_median": 22723,
"setup_usec_samples": [
21887,
22723,
23413
],
"simulation_usec_median": 136897,
"simulation_usec_samples": [
136707,
136897,
137966
],
"start_event_count": 720,
"start_known_reference_count": 180,
"start_state_bytes": 253098,
"start_tick": 610,
"state_growth_bytes": 489906,
"tick_interval": 1.2,
"ticks_per_second_median": 1383.90107875089,
"usec_per_tick_median": 722.595,
"warmup_arrivals": 120,
"warmup_ticks": 10,
"workload_id": "full_fidelity_headless_arrival"
},
{
"arrival_share_percent": 2.64320413797924,
"arrival_usec_median": 7563,
"arrival_usec_samples": [
7552,
7563,
7628
],
"arrivals_processed": 2090,
"case_id": "history_006000",
"elapsed_usec_max": 291609,
"elapsed_usec_median": 286130,
"elapsed_usec_min": 285483,
"elapsed_usec_samples": [
285483,
286130,
291609
],
"end_event_count": 8210,
"end_known_reference_count": 0,
"end_state_bytes": 2141615,
"end_tick": 6210,
"events_recorded": 2090,
"final_checksum": "b4df2bac21ed5ce6dcd92736e210db5f3114d959dcb15ae7796493362911b6d7",
"history_seed_events": 6000,
"measured_ticks": 200,
"npc_updates": 12000,
"population": 60,
"realtime_factor_median": 838.779575717331,
"sample_count": 3,
"schema_version": 1,
"seed": 8088,
"setup_usec_median": 153306,
"setup_usec_samples": [
153290,
153306,
154064
],
"simulation_usec_median": 278467,
"simulation_usec_samples": [
277892,
278467,
284022
],
"start_event_count": 6120,
"start_known_reference_count": 180,
"start_state_bytes": 1649700,
"start_tick": 6010,
"state_growth_bytes": 491915,
"tick_interval": 1.2,
"ticks_per_second_median": 698.982979764443,
"usec_per_tick_median": 1430.65,
"warmup_arrivals": 120,
"warmup_ticks": 10,
"workload_id": "full_fidelity_headless_arrival"
}
],
"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": 1,
"timed_phases": [
"simulation_tick",
"headless_arrival_completion"
],
"warmup_ticks": 10,
"workload": "All NPCs receive full per-tick needs/task updates and ordinary action decisions; travel resolves through the deterministic immediate-arrival headless convention.",
"workload_id": "full_fidelity_headless_arrival"
}