Разделить частоту обновления часов и расчёта симуляции
Счётчик времени UI обновляется до 10 Гц по запрошенному времени, а тяжёлые циклы по-прежнему запускаются с адаптивным интервалом и обрабатывают весь накопленный промежуток. Обновить тесты и описание модели.
This commit is contained in:
@@ -151,6 +151,8 @@ HP деградирует в RP. У эонов возможен откат к п
|
|||||||
|
|
||||||
Порядок полного шага: производство за интервал → непрерывные потоки существующих цивилизаций → поколения звёзд и биологической жизни и бласты → итоговые ограничения среды → тренды. Возникшие из C0.1 цивилизации C0.2 получают равномерный профиль, смешанный с существующим населением, и начинают непрерывную эволюцию со следующего шага. Разные размеры полного шага могут отличаться из-за этого порядка, ограничений среды и нелинейных эффектов; чистая непрерывная цепочка без ограничений согласована при разделении интервала.
|
Порядок полного шага: производство за интервал → непрерывные потоки существующих цивилизаций → поколения звёзд и биологической жизни и бласты → итоговые ограничения среды → тренды. Возникшие из C0.1 цивилизации C0.2 получают равномерный профиль, смешанный с существующим населением, и начинают непрерывную эволюцию со следующего шага. Разные размеры полного шага могут отличаться из-за этого порядка, ограничений среды и нелинейных эффектов; чистая непрерывная цепочка без ограничений согласована при разделении интервала.
|
||||||
|
|
||||||
|
Счётчик времени UI показывает запрошенные часы и обновляется с частотой до 10 Гц независимо от тяжёлой транзакции. Состояние мира и его индикаторы публикуются по завершении транзакции; при высокой нагрузке отображаемые часы поэтому могут немного опережать рассчитанные объекты. Тяжёлые циклы адаптивно запускаются с целевым интервалом 0,2–0,5 реальной секунды, каждый использует весь накопленный игровой интервал.
|
||||||
|
|
||||||
В `civilization_remainders` сохраняются квоты и дробные остатки потоков; в `civilization_stats` — войны, колонии, побочный ущерб и поклонение. Возрастные партии сохраняются для звёзд, HP, эонов, C0.0 и C0.1.
|
В `civilization_remainders` сохраняются квоты и дробные остатки потоков; в `civilization_stats` — войны, колонии, побочный ущерб и поклонение. Возрастные партии сохраняются для звёзд, HP, эонов, C0.0 и C0.1.
|
||||||
|
|
||||||
### Структуры и периоды
|
### Структуры и периоды
|
||||||
|
|||||||
+9
-4
@@ -98,6 +98,7 @@ var g_active_items: Dictionary = {}
|
|||||||
var pending_page: int = -1
|
var pending_page: int = -1
|
||||||
var active_actions_dirty: bool = true
|
var active_actions_dirty: bool = true
|
||||||
var active_actions_elapsed: float = 0.0
|
var active_actions_elapsed: float = 0.0
|
||||||
|
var clock_display_elapsed: float = 0.0
|
||||||
var interface_sounds_enabled: bool = false
|
var interface_sounds_enabled: bool = false
|
||||||
|
|
||||||
var owniverse:Owniverse = Owniverse.new()
|
var owniverse:Owniverse = Owniverse.new()
|
||||||
@@ -389,7 +390,7 @@ func create_new_universe() -> void:
|
|||||||
func _refresh_universe_ui() -> void:
|
func _refresh_universe_ui() -> void:
|
||||||
owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
|
owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
|
||||||
_change_speed(0)
|
_change_speed(0)
|
||||||
set_datetime(owniverse.state.time.year)
|
set_datetime(owniverse.display_clock().year)
|
||||||
set_owniverse_item_value()
|
set_owniverse_item_value()
|
||||||
_unlock_items()
|
_unlock_items()
|
||||||
if world_indicators != null: world_indicators.update(owniverse)
|
if world_indicators != null: world_indicators.update(owniverse)
|
||||||
@@ -397,6 +398,10 @@ func _refresh_universe_ui() -> void:
|
|||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
|
|
||||||
owniverse.evolute_frame(delta, get_tree())
|
owniverse.evolute_frame(delta, get_tree())
|
||||||
|
clock_display_elapsed += delta
|
||||||
|
if clock_display_elapsed >= 0.1:
|
||||||
|
clock_display_elapsed = fmod(clock_display_elapsed, 0.1)
|
||||||
|
set_datetime(owniverse.display_clock().year)
|
||||||
indicator_elapsed += delta
|
indicator_elapsed += delta
|
||||||
if indicator_elapsed >= 0.1 and indicator_revision != owniverse.presentation.revision:
|
if indicator_elapsed >= 0.1 and indicator_revision != owniverse.presentation.revision:
|
||||||
indicator_elapsed = 0.0
|
indicator_elapsed = 0.0
|
||||||
@@ -404,9 +409,9 @@ func _process(delta: float) -> void:
|
|||||||
indicator_revision = owniverse.presentation.revision
|
indicator_revision = owniverse.presentation.revision
|
||||||
_update_descriptions(delta)
|
_update_descriptions(delta)
|
||||||
if not owniverse.presentation.pending_amounts.is_empty(): set_owniverse_item_value()
|
if not owniverse.presentation.pending_amounts.is_empty(): set_owniverse_item_value()
|
||||||
if displayed_year == null or E.ne(displayed_year, owniverse.state.time.year):
|
var committed_year: Variant = owniverse.state.time.year
|
||||||
set_datetime(owniverse.state.time.year)
|
if displayed_year == null or E.ne(displayed_year, committed_year):
|
||||||
displayed_year = owniverse.state.time.year
|
displayed_year = committed_year
|
||||||
|
|
||||||
#owniverse.cycle(delta)
|
#owniverse.cycle(delta)
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -253,7 +253,18 @@ godot --headless --path Owniverse.GI --script res://tests/test_flow_commit.gd
|
|||||||
Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает
|
Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает
|
||||||
следующему кадру внутри создания партий и обработки стадий. Расчёт остаётся
|
следующему кадру внутри создания партий и обработки стадий. Расчёт остаётся
|
||||||
на главном потоке, состояние публикуется только после полного завершения.
|
на главном потоке, состояние публикуется только после полного завершения.
|
||||||
Расчёт запускается после накопления 0,2 реальной секунды и обрабатывает весь ожидающий интервал. Время, поступившее во время расчёта, обрабатывается следующим циклом. При перегрузке шаг растёт; пять завершений в секунду остаются целевой частотой, а не гарантией.
|
Запрошенные игровые часы и счётчик UI обновляются независимо от тяжёлого расчёта;
|
||||||
|
Stage показывает запрошенное время раз в 0,1 секунды (10 Гц), а опубликованное
|
||||||
|
состояние мира остаётся на последнем завершённом цикле. Расчёт запускается после
|
||||||
|
накопления выбранного интервала и обрабатывает весь ожидающий интервал. Время,
|
||||||
|
поступившее во время расчёта, обрабатывается следующим циклом. Контроллер нагрузки
|
||||||
|
меняет целевой шаг между 0,2, 0,25, 0,333 и 0,5 секунды (5/4/3/2 цикла в секунду):
|
||||||
|
вниз при EMA расчёта выше 90% текущего шага, вверх после 30 циклов ниже 65%.
|
||||||
|
Временное разрешение укрупнения партий масштабируется с шагом, исходная точность
|
||||||
|
сохраняется на 5 Гц. Контроллер не пропускает накопленное игровое время, но 2/с —
|
||||||
|
нижняя целевая частота, не гарантия при перегрузке.
|
||||||
|
|
||||||
|
`benchmark_dynamic_cadence.gd` сравнивает пропускную способность полного цикла на двух диагностических сохранениях. `benchmark_dynamic_cadence_ui.gd` сравнивает 10 секунд работы полной сцены на сохранении со всеми объектами; результаты сохранены в `artifacts/dynamic_cadence_results.json` и `artifacts/dynamic_cadence_ui_results.json`.
|
||||||
|
|
||||||
Производство во время транзакции выполняется после её завершения. Запрошенное
|
Производство во время транзакции выполняется после её завершения. Запрошенное
|
||||||
время, активный интервал, скорость и очередь производства входят в сохранение;
|
время, активный интервал, скорость и очередь производства входят в сохранение;
|
||||||
|
|||||||
@@ -496,3 +496,23 @@ godot --headless --path . --script res://tests/benchmark_civilization_flows.gd -
|
|||||||
Проверки: адаптивное качество — 18, хронология поколений — 28, границы партий — 117, оптимизации симуляции — 1889, планировщик кадров — 39; ошибок нет. Все 12 конечных диагностических состояний прошли валидацию. Хеши входных файлов: `44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67` и `1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a`.
|
Проверки: адаптивное качество — 18, хронология поколений — 28, границы партий — 117, оптимизации симуляции — 1889, планировщик кадров — 39; ошибок нет. Все 12 конечных диагностических состояний прошли валидацию. Хеши входных файлов: `44bee3b3753fa51615f766d8f800a10cc2b70c8e7fb4ea12b6e18da7f07a7d67` и `1e95ef398923f26b912a305c84f9b3eb502d24cb14e8ae687a4becccf047324a`.
|
||||||
|
|
||||||
Детали уровней, минимумы/максимумы и воспроизводимые результаты: `adaptive_quality_results.json`. Штатный benchmark принимает параметр `speed=1..6`.
|
Детали уровней, минимумы/максимумы и воспроизводимые результаты: `adaptive_quality_results.json`. Штатный benchmark принимает параметр `speed=1..6`.
|
||||||
|
|
||||||
|
## Адаптивная частота циклов 5 → 4 → 3 — 2026-10-17
|
||||||
|
|
||||||
|
Частота запуска полного цикла теперь снижается при устойчивой нагрузке: пороги шага — 0,2, 0,25 и 0,333 секунды. EMA времени расчёта переключает частоту вниз, когда цикл занимает больше 90% текущего интервала; возврат на одну ступень происходит после 30 циклов быстрее 65% интервала. Уровень частоты временный и после загрузки начинается с 5 Гц. Игровые часы продолжают накапливать весь прошедший интервал. Временное объединение партий масштабируется с выбранным интервалом, при этом при 5 Гц прежняя точность остаётся прежней.
|
||||||
|
|
||||||
|
Проверка сцены на диагностическом сохранении `all_objects_speed6.adventure.json`, скорость 6, Godot 4.7.2, Mac16,8. Сравнивались фиксированные 5 Гц и адаптивные 5/4/3 Гц, два прогона каждого режима в чередующемся порядке, по 10 секунд реального времени. Последняя транзакция каждого прогона завершалась до снятия результата.
|
||||||
|
|
||||||
|
| Метрика | Фиксированные 5 Гц | Адаптивно, установилось 3 Гц | Изменение |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| Завершённые циклы/с | 4,64 | 2,96 | −36% |
|
||||||
|
| Средний FPS сцены | 22,60 | 64,35 | ×2,85 |
|
||||||
|
| p95 времени кадра | 115,6 мс | 66,5 мс | −42% |
|
||||||
|
| Средний максимум кадра | 227 мс | 205 мс | −10% |
|
||||||
|
| Опубликованные игровые годы/с | 99 017 | 98 049 | −1,0% |
|
||||||
|
|
||||||
|
На этих замерах реальное игровое время почти не замедлилось: симуляция завершала около трёх более крупных циклов вместо примерно 4,6 меньших. Средний FPS заметно вырос, но отдельные кадры всё ещё превышали 200 мс; это не гарантия отсутствия коротких задержек. UI-замер выполнен в headless-сцене на компьютере, поэтому его FPS не переносится напрямую на телефон.
|
||||||
|
|
||||||
|
Отдельно прогнан синхронный benchmark полного CPU-цикла на тех же двух тяжёлых сохранениях (скорость 6, 30 циклов × 2 повтора). При фиксированном шаге 0,2 секунды и адаптивном шаге итоговая обработка игрового времени за CPU-секунду выросла с 1,099 до 1,656 на сохранении со всеми объектами (+50,7%) и с 1,036 до 1,697 на сохранении со множеством объектов и партий (+63,9%). В каждом адаптивном прогоне частоты установились на 3 Гц; интервалы игровых часов сверены с запрошенными, все конечные состояния прошли проверку сохранения. Синхронный показатель показывает пропускную способность расчёта, а UI-прогон — фактический темп транзакций сцены.
|
||||||
|
|
||||||
|
Повторяемые результаты: `dynamic_cadence_results.json` (CPU-прогоны) и `dynamic_cadence_ui_results.json` (сцена и UI). Запуски выполняются скриптами `benchmark_dynamic_cadence.gd` и `benchmark_dynamic_cadence_ui.gd`; исходные диагностические сохранения не изменялись.
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
{
|
||||||
|
"cycles_per_run": 30,
|
||||||
|
"godot": {
|
||||||
|
"build": "official",
|
||||||
|
"hash": "ed1daf0bf001b61586d9930840f2f1394092c079",
|
||||||
|
"hex": 263938,
|
||||||
|
"major": 4,
|
||||||
|
"minor": 7,
|
||||||
|
"patch": 2,
|
||||||
|
"status": "stable",
|
||||||
|
"string": "4.7.2-stable (official)",
|
||||||
|
"timestamp": 0
|
||||||
|
},
|
||||||
|
"input_saves_unchanged": true,
|
||||||
|
"machine": "Mac16,8",
|
||||||
|
"repeats": 2,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"applied_game_years": "600000",
|
||||||
|
"cadence_steps": {
|
||||||
|
"0.2": 30
|
||||||
|
},
|
||||||
|
"cpu_cycles_per_second": 5.5818824005294605,
|
||||||
|
"cycles": 30,
|
||||||
|
"final_adaptive_quality": 3,
|
||||||
|
"final_cadence_hz": 5.0,
|
||||||
|
"max_ms": 351.156,
|
||||||
|
"mean_ms_per_cycle": 179.15103333333334,
|
||||||
|
"mode": "fixed_5",
|
||||||
|
"p95_ms": 188.985,
|
||||||
|
"repeat": 0,
|
||||||
|
"requested_simulation_seconds": 6.000000000000003,
|
||||||
|
"save": "all_objects_speed6.adventure.json",
|
||||||
|
"simulated_seconds_per_cpu_second": 1.1163764801058926
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"applied_game_years": "978333",
|
||||||
|
"cadence_steps": {
|
||||||
|
"0.2": 1,
|
||||||
|
"0.25": 1,
|
||||||
|
"0.33333333333333": 28
|
||||||
|
},
|
||||||
|
"cpu_cycles_per_second": 5.126867760649103,
|
||||||
|
"cycles": 30,
|
||||||
|
"final_adaptive_quality": 3,
|
||||||
|
"final_cadence_hz": 3.0,
|
||||||
|
"max_ms": 349.806,
|
||||||
|
"mean_ms_per_cycle": 195.05086666666668,
|
||||||
|
"mode": "adaptive_5_4_3",
|
||||||
|
"p95_ms": 214.441,
|
||||||
|
"repeat": 0,
|
||||||
|
"requested_simulation_seconds": 9.783333333333333,
|
||||||
|
"save": "all_objects_speed6.adventure.json",
|
||||||
|
"simulated_seconds_per_cpu_second": 1.671928541945013
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"applied_game_years": "978333",
|
||||||
|
"cadence_steps": {
|
||||||
|
"0.2": 1,
|
||||||
|
"0.25": 1,
|
||||||
|
"0.33333333333333": 28
|
||||||
|
},
|
||||||
|
"cpu_cycles_per_second": 5.029407785556781,
|
||||||
|
"cycles": 30,
|
||||||
|
"final_adaptive_quality": 3,
|
||||||
|
"final_cadence_hz": 3.0,
|
||||||
|
"max_ms": 340.747,
|
||||||
|
"mean_ms_per_cycle": 198.83056666666667,
|
||||||
|
"mode": "adaptive_5_4_3",
|
||||||
|
"p95_ms": 233.775,
|
||||||
|
"repeat": 1,
|
||||||
|
"requested_simulation_seconds": 9.783333333333333,
|
||||||
|
"save": "all_objects_speed6.adventure.json",
|
||||||
|
"simulated_seconds_per_cpu_second": 1.6401457611787948
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"applied_game_years": "600000",
|
||||||
|
"cadence_steps": {
|
||||||
|
"0.2": 30
|
||||||
|
},
|
||||||
|
"cpu_cycles_per_second": 5.403485464263869,
|
||||||
|
"cycles": 30,
|
||||||
|
"final_adaptive_quality": 3,
|
||||||
|
"final_cadence_hz": 5.0,
|
||||||
|
"max_ms": 351.698,
|
||||||
|
"mean_ms_per_cycle": 185.06573333333333,
|
||||||
|
"mode": "fixed_5",
|
||||||
|
"p95_ms": 195.396,
|
||||||
|
"repeat": 1,
|
||||||
|
"requested_simulation_seconds": 6.000000000000003,
|
||||||
|
"save": "all_objects_speed6.adventure.json",
|
||||||
|
"simulated_seconds_per_cpu_second": 1.0806970928527742
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"applied_game_years": "600000",
|
||||||
|
"cadence_steps": {
|
||||||
|
"0.2": 30
|
||||||
|
},
|
||||||
|
"cpu_cycles_per_second": 5.218331511555821,
|
||||||
|
"cycles": 30,
|
||||||
|
"final_adaptive_quality": 3,
|
||||||
|
"final_cadence_hz": 5.0,
|
||||||
|
"max_ms": 373.477,
|
||||||
|
"mean_ms_per_cycle": 191.63213333333334,
|
||||||
|
"mode": "fixed_5",
|
||||||
|
"p95_ms": 250.5,
|
||||||
|
"repeat": 0,
|
||||||
|
"requested_simulation_seconds": 6.000000000000003,
|
||||||
|
"save": "many_objects_fragmented_stars_cycle.adventure.json",
|
||||||
|
"simulated_seconds_per_cpu_second": 1.0436663023111648
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"applied_game_years": "978333",
|
||||||
|
"cadence_steps": {
|
||||||
|
"0.2": 1,
|
||||||
|
"0.25": 1,
|
||||||
|
"0.33333333333333": 28
|
||||||
|
},
|
||||||
|
"cpu_cycles_per_second": 5.21296979935453,
|
||||||
|
"cycles": 30,
|
||||||
|
"final_adaptive_quality": 3,
|
||||||
|
"final_cadence_hz": 3.0,
|
||||||
|
"max_ms": 373.168,
|
||||||
|
"mean_ms_per_cycle": 191.82923333333332,
|
||||||
|
"mode": "adaptive_5_4_3",
|
||||||
|
"p95_ms": 258.047,
|
||||||
|
"repeat": 0,
|
||||||
|
"requested_simulation_seconds": 9.783333333333333,
|
||||||
|
"save": "many_objects_fragmented_stars_cycle.adventure.json",
|
||||||
|
"simulated_seconds_per_cpu_second": 1.7000073734561718
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"applied_game_years": "978333",
|
||||||
|
"cadence_steps": {
|
||||||
|
"0.2": 1,
|
||||||
|
"0.25": 1,
|
||||||
|
"0.33333333333333": 28
|
||||||
|
},
|
||||||
|
"cpu_cycles_per_second": 5.195109254879333,
|
||||||
|
"cycles": 30,
|
||||||
|
"final_adaptive_quality": 3,
|
||||||
|
"final_cadence_hz": 3.0,
|
||||||
|
"max_ms": 372.878,
|
||||||
|
"mean_ms_per_cycle": 192.48873333333333,
|
||||||
|
"mode": "adaptive_5_4_3",
|
||||||
|
"p95_ms": 254.848,
|
||||||
|
"repeat": 1,
|
||||||
|
"requested_simulation_seconds": 9.783333333333333,
|
||||||
|
"save": "many_objects_fragmented_stars_cycle.adventure.json",
|
||||||
|
"simulated_seconds_per_cpu_second": 1.694182851452316
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"applied_game_years": "600000",
|
||||||
|
"cadence_steps": {
|
||||||
|
"0.2": 30
|
||||||
|
},
|
||||||
|
"cpu_cycles_per_second": 5.137478056546851,
|
||||||
|
"cycles": 30,
|
||||||
|
"final_adaptive_quality": 3,
|
||||||
|
"final_cadence_hz": 5.0,
|
||||||
|
"max_ms": 375.784,
|
||||||
|
"mean_ms_per_cycle": 194.64803333333333,
|
||||||
|
"mode": "fixed_5",
|
||||||
|
"p95_ms": 255.446,
|
||||||
|
"repeat": 1,
|
||||||
|
"requested_simulation_seconds": 6.000000000000003,
|
||||||
|
"save": "many_objects_fragmented_stars_cycle.adventure.json",
|
||||||
|
"simulated_seconds_per_cpu_second": 1.0274956113093707
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"speed": 6
|
||||||
|
}
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
{
|
||||||
|
"godot": {
|
||||||
|
"build": "official",
|
||||||
|
"hash": "ed1daf0bf001b61586d9930840f2f1394092c079",
|
||||||
|
"hex": 263938,
|
||||||
|
"major": 4,
|
||||||
|
"minor": 7,
|
||||||
|
"patch": 2,
|
||||||
|
"status": "stable",
|
||||||
|
"string": "4.7.2-stable (official)",
|
||||||
|
"timestamp": 0
|
||||||
|
},
|
||||||
|
"machine": "Mac16,8",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"commits": 48,
|
||||||
|
"completed_cycles_per_second": 4.743091908967023,
|
||||||
|
"cpu_profile_usec_calls": {
|
||||||
|
"assign_homes": [
|
||||||
|
51095,
|
||||||
|
170
|
||||||
|
],
|
||||||
|
"civilization_flow_capacity": [
|
||||||
|
111005,
|
||||||
|
48
|
||||||
|
],
|
||||||
|
"civilization_flow_colonies": [
|
||||||
|
131724,
|
||||||
|
48
|
||||||
|
],
|
||||||
|
"civilization_flow_finish": [
|
||||||
|
325933,
|
||||||
|
48
|
||||||
|
],
|
||||||
|
"civilization_flow_ledger": [
|
||||||
|
928271,
|
||||||
|
48
|
||||||
|
],
|
||||||
|
"civilization_flow_plan": [
|
||||||
|
825706,
|
||||||
|
48
|
||||||
|
],
|
||||||
|
"civilization_operator_build": [
|
||||||
|
148485,
|
||||||
|
48
|
||||||
|
],
|
||||||
|
"civilization_operator_solve": [
|
||||||
|
104924,
|
||||||
|
2064
|
||||||
|
],
|
||||||
|
"enforce_capacity": [
|
||||||
|
626179,
|
||||||
|
170
|
||||||
|
],
|
||||||
|
"generation_products": [
|
||||||
|
235626,
|
||||||
|
74
|
||||||
|
],
|
||||||
|
"generation_withdrawal": [
|
||||||
|
455348,
|
||||||
|
97
|
||||||
|
],
|
||||||
|
"generations": [
|
||||||
|
3654469,
|
||||||
|
48
|
||||||
|
],
|
||||||
|
"initialize": [
|
||||||
|
215278,
|
||||||
|
48
|
||||||
|
],
|
||||||
|
"newborn_batches": [
|
||||||
|
237070,
|
||||||
|
3088
|
||||||
|
],
|
||||||
|
"physical_amounts_build": [
|
||||||
|
630353,
|
||||||
|
437
|
||||||
|
],
|
||||||
|
"production": [
|
||||||
|
1115470,
|
||||||
|
48
|
||||||
|
],
|
||||||
|
"trends": [
|
||||||
|
865132,
|
||||||
|
48
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"elapsed_seconds": 10.119981,
|
||||||
|
"final_adaptive_quality": 3,
|
||||||
|
"final_cadence_hz": 5.0,
|
||||||
|
"frames": 246,
|
||||||
|
"mean_render_fps": 24.584772192104204,
|
||||||
|
"mode": "fixed_5",
|
||||||
|
"published_to_requested": 0.9943600000000001,
|
||||||
|
"published_years": "1006667",
|
||||||
|
"render_max_ms": 237.132,
|
||||||
|
"render_p95_ms": 111.449,
|
||||||
|
"requested_years": "1012372"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"commits": 21,
|
||||||
|
"completed_cycles_per_second": 2.085573876935847,
|
||||||
|
"cpu_profile_usec_calls": {
|
||||||
|
"assign_homes": [
|
||||||
|
30276,
|
||||||
|
75
|
||||||
|
],
|
||||||
|
"civilization_flow_capacity": [
|
||||||
|
51694,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"civilization_flow_colonies": [
|
||||||
|
52773,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"civilization_flow_finish": [
|
||||||
|
142952,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"civilization_flow_ledger": [
|
||||||
|
309711,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"civilization_flow_plan": [
|
||||||
|
435139,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"civilization_operator_build": [
|
||||||
|
74385,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"civilization_operator_solve": [
|
||||||
|
87284,
|
||||||
|
903
|
||||||
|
],
|
||||||
|
"enforce_capacity": [
|
||||||
|
414865,
|
||||||
|
75
|
||||||
|
],
|
||||||
|
"generation_products": [
|
||||||
|
105449,
|
||||||
|
33
|
||||||
|
],
|
||||||
|
"generation_withdrawal": [
|
||||||
|
152279,
|
||||||
|
43
|
||||||
|
],
|
||||||
|
"generations": [
|
||||||
|
1503309,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"initialize": [
|
||||||
|
255626,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"newborn_batches": [
|
||||||
|
114834,
|
||||||
|
1633
|
||||||
|
],
|
||||||
|
"physical_amounts_build": [
|
||||||
|
274341,
|
||||||
|
193
|
||||||
|
],
|
||||||
|
"production": [
|
||||||
|
685165,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"trends": [
|
||||||
|
377582,
|
||||||
|
21
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"elapsed_seconds": 10.069171,
|
||||||
|
"final_adaptive_quality": 3,
|
||||||
|
"final_cadence_hz": 2.0,
|
||||||
|
"frames": 879,
|
||||||
|
"mean_render_fps": 87.80117977216634,
|
||||||
|
"mode": "adaptive_5_4_3_2",
|
||||||
|
"published_to_requested": 0.9889800000000001,
|
||||||
|
"published_years": "989844",
|
||||||
|
"render_max_ms": 203.26,
|
||||||
|
"render_p95_ms": 42.712,
|
||||||
|
"requested_years": "1000874"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"commits": 21,
|
||||||
|
"completed_cycles_per_second": 2.0775335521668676,
|
||||||
|
"cpu_profile_usec_calls": {
|
||||||
|
"assign_homes": [
|
||||||
|
31004,
|
||||||
|
76
|
||||||
|
],
|
||||||
|
"civilization_flow_capacity": [
|
||||||
|
51172,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"civilization_flow_colonies": [
|
||||||
|
52782,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"civilization_flow_finish": [
|
||||||
|
142851,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"civilization_flow_ledger": [
|
||||||
|
309607,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"civilization_flow_plan": [
|
||||||
|
431321,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"civilization_operator_build": [
|
||||||
|
74854,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"civilization_operator_solve": [
|
||||||
|
86348,
|
||||||
|
903
|
||||||
|
],
|
||||||
|
"enforce_capacity": [
|
||||||
|
423638,
|
||||||
|
76
|
||||||
|
],
|
||||||
|
"generation_products": [
|
||||||
|
105537,
|
||||||
|
34
|
||||||
|
],
|
||||||
|
"generation_withdrawal": [
|
||||||
|
151611,
|
||||||
|
43
|
||||||
|
],
|
||||||
|
"generations": [
|
||||||
|
1516916,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"initialize": [
|
||||||
|
261806,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"newborn_batches": [
|
||||||
|
112492,
|
||||||
|
1621
|
||||||
|
],
|
||||||
|
"physical_amounts_build": [
|
||||||
|
278871,
|
||||||
|
195
|
||||||
|
],
|
||||||
|
"production": [
|
||||||
|
696352,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"trends": [
|
||||||
|
377969,
|
||||||
|
21
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"elapsed_seconds": 10.10814,
|
||||||
|
"final_adaptive_quality": 3,
|
||||||
|
"final_cadence_hz": 2.0,
|
||||||
|
"frames": 880,
|
||||||
|
"mean_render_fps": 88.00051040296032,
|
||||||
|
"mode": "adaptive_5_4_3_2",
|
||||||
|
"published_to_requested": 0.9933700000000001,
|
||||||
|
"published_years": "994002",
|
||||||
|
"render_max_ms": 201.104,
|
||||||
|
"render_p95_ms": 42.207,
|
||||||
|
"requested_years": "1000640"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"commits": 47,
|
||||||
|
"completed_cycles_per_second": 4.646829581875308,
|
||||||
|
"cpu_profile_usec_calls": {
|
||||||
|
"assign_homes": [
|
||||||
|
50139,
|
||||||
|
165
|
||||||
|
],
|
||||||
|
"civilization_flow_capacity": [
|
||||||
|
108674,
|
||||||
|
47
|
||||||
|
],
|
||||||
|
"civilization_flow_colonies": [
|
||||||
|
129977,
|
||||||
|
47
|
||||||
|
],
|
||||||
|
"civilization_flow_finish": [
|
||||||
|
323458,
|
||||||
|
47
|
||||||
|
],
|
||||||
|
"civilization_flow_ledger": [
|
||||||
|
911542,
|
||||||
|
47
|
||||||
|
],
|
||||||
|
"civilization_flow_plan": [
|
||||||
|
807459,
|
||||||
|
47
|
||||||
|
],
|
||||||
|
"civilization_operator_build": [
|
||||||
|
147856,
|
||||||
|
47
|
||||||
|
],
|
||||||
|
"civilization_operator_solve": [
|
||||||
|
96154,
|
||||||
|
2021
|
||||||
|
],
|
||||||
|
"enforce_capacity": [
|
||||||
|
619397,
|
||||||
|
165
|
||||||
|
],
|
||||||
|
"generation_products": [
|
||||||
|
234744,
|
||||||
|
71
|
||||||
|
],
|
||||||
|
"generation_withdrawal": [
|
||||||
|
451797,
|
||||||
|
95
|
||||||
|
],
|
||||||
|
"generations": [
|
||||||
|
3665554,
|
||||||
|
47
|
||||||
|
],
|
||||||
|
"initialize": [
|
||||||
|
217827,
|
||||||
|
47
|
||||||
|
],
|
||||||
|
"newborn_batches": [
|
||||||
|
265766,
|
||||||
|
3178
|
||||||
|
],
|
||||||
|
"physical_amounts_build": [
|
||||||
|
623211,
|
||||||
|
428
|
||||||
|
],
|
||||||
|
"production": [
|
||||||
|
1142731,
|
||||||
|
47
|
||||||
|
],
|
||||||
|
"trends": [
|
||||||
|
856364,
|
||||||
|
47
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"elapsed_seconds": 10.114423,
|
||||||
|
"final_adaptive_quality": 3,
|
||||||
|
"final_cadence_hz": 5.0,
|
||||||
|
"frames": 239,
|
||||||
|
"mean_render_fps": 23.87480014844331,
|
||||||
|
"mode": "fixed_5",
|
||||||
|
"published_to_requested": 0.9904200000000001,
|
||||||
|
"published_years": "992039",
|
||||||
|
"render_max_ms": 201.438,
|
||||||
|
"render_p95_ms": 114.211,
|
||||||
|
"requested_years": "1001635"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"save": "all_objects_speed6.adventure.json",
|
||||||
|
"speed": 6,
|
||||||
|
"wall_seconds_per_run": 10.0
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Paired, read-only throughput benchmark for adaptive 5/4/3/2 Hz cadence.
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const SAVE_PATHS: Array[String] = [
|
||||||
|
"res://tests/artifacts/all_objects_speed6.adventure.json",
|
||||||
|
"res://tests/artifacts/many_objects_fragmented_stars_cycle.adventure.json"
|
||||||
|
]
|
||||||
|
const RESULT_PATH := "res://tests/artifacts/dynamic_cadence_results.json"
|
||||||
|
var cycles: int = 40
|
||||||
|
var repeats: int = 2
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func restore_json_integers(value):
|
||||||
|
if value is float and value == floorf(value) and absf(value) <= 9007199254740991:
|
||||||
|
return int(value)
|
||||||
|
if value is Dictionary:
|
||||||
|
for key in value: value[key] = restore_json_integers(value[key])
|
||||||
|
elif value is Array:
|
||||||
|
for i in value.size(): value[i] = restore_json_integers(value[i])
|
||||||
|
return value
|
||||||
|
|
||||||
|
func create_world(initial: Dictionary, use_adaptive_cadence: bool):
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
world.state = initial.duplicate(true)
|
||||||
|
world.speed = 6
|
||||||
|
world.adaptive_cadence = use_adaptive_cadence
|
||||||
|
world.frame_step_seconds = 0.2
|
||||||
|
world.cadence_level = 0
|
||||||
|
return world
|
||||||
|
|
||||||
|
func dispose(world) -> void:
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func measure(initial: Dictionary, adaptive: bool, save_name: String, repeat_index: int) -> Dictionary:
|
||||||
|
var world = create_world(initial, adaptive)
|
||||||
|
var elapsed_usec: int = 0
|
||||||
|
var requested_seconds: float = 0.0
|
||||||
|
var step_counts: Dictionary = {}
|
||||||
|
var samples: Array[float] = []
|
||||||
|
var initial_clock: Variant = world.state.time.datetime
|
||||||
|
for i in cycles:
|
||||||
|
var step: float = world.frame_step_seconds
|
||||||
|
step_counts[str(step)] = int(step_counts.get(str(step), 0)) + 1
|
||||||
|
requested_seconds += step
|
||||||
|
var started: int = Time.get_ticks_usec()
|
||||||
|
world.evolute(step)
|
||||||
|
var usec: int = Time.get_ticks_usec() - started
|
||||||
|
elapsed_usec += usec
|
||||||
|
samples.append(float(usec) / 1000.0)
|
||||||
|
var applied_years: Variant = E.subtract(world.state.time.datetime, initial_clock)
|
||||||
|
var expected_years: float = requested_seconds * 100000.0
|
||||||
|
assert(absf(E.approx(applied_years) - expected_years) <= maxf(1.0, expected_years * 1e-10), "Clock advance differs from accumulated cadence interval")
|
||||||
|
var validation: String = root.get_node("SaveManager").validate_data({
|
||||||
|
"format_version": 6,
|
||||||
|
"app_version": "0.7.0",
|
||||||
|
"ui": {"current_page": 0},
|
||||||
|
"state": world.state
|
||||||
|
})
|
||||||
|
assert(validation.is_empty(), validation)
|
||||||
|
samples.sort()
|
||||||
|
var result: Dictionary = {
|
||||||
|
"save": save_name,
|
||||||
|
"mode": "adaptive_5_4_3_2" if adaptive else "fixed_5",
|
||||||
|
"repeat": repeat_index,
|
||||||
|
"cycles": cycles,
|
||||||
|
"mean_ms_per_cycle": float(elapsed_usec) / (cycles * 1000.0),
|
||||||
|
"p95_ms": samples[int(ceil(samples.size() * 0.95)) - 1],
|
||||||
|
"max_ms": samples.back(),
|
||||||
|
"cpu_cycles_per_second": float(cycles) * 1000000.0 / elapsed_usec,
|
||||||
|
"simulated_seconds_per_cpu_second": requested_seconds * 1000000.0 / elapsed_usec,
|
||||||
|
"requested_simulation_seconds": requested_seconds,
|
||||||
|
"applied_game_years": E.fixed(applied_years),
|
||||||
|
"cadence_steps": step_counts,
|
||||||
|
"final_cadence_hz": 1.0 / world.frame_step_seconds,
|
||||||
|
"final_adaptive_quality": world.adaptive_quality
|
||||||
|
}
|
||||||
|
print("CADENCE_RESULT ", JSON.stringify(result))
|
||||||
|
dispose(world)
|
||||||
|
return result
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
for arg in OS.get_cmdline_user_args():
|
||||||
|
if arg.begins_with("cycles="): cycles = maxi(1, arg.trim_prefix("cycles=").to_int())
|
||||||
|
if arg.begins_with("repeats="): repeats = maxi(1, arg.trim_prefix("repeats=").to_int())
|
||||||
|
var inputs: Dictionary = {}
|
||||||
|
for path in SAVE_PATHS:
|
||||||
|
var parsed = JSON.parse_string(FileAccess.get_file_as_string(path))
|
||||||
|
assert(parsed is Dictionary and parsed.get("format_version") == 6, "Invalid benchmark save: " + path)
|
||||||
|
var payload: Dictionary = E.decode(restore_json_integers(parsed))
|
||||||
|
var validation: String = root.get_node("SaveManager").validate_data(payload)
|
||||||
|
assert(validation.is_empty(), validation)
|
||||||
|
inputs[path.get_file()] = payload.state
|
||||||
|
var results: Array[Dictionary] = []
|
||||||
|
for save_name in inputs:
|
||||||
|
var initial: Dictionary = inputs[save_name]
|
||||||
|
# Alternate order to limit machine warm-up and thermal bias.
|
||||||
|
for repeat_index in repeats:
|
||||||
|
if repeat_index % 2 == 0:
|
||||||
|
results.append(measure(initial, false, save_name, repeat_index))
|
||||||
|
results.append(measure(initial, true, save_name, repeat_index))
|
||||||
|
else:
|
||||||
|
results.append(measure(initial, true, save_name, repeat_index))
|
||||||
|
results.append(measure(initial, false, save_name, repeat_index))
|
||||||
|
var output := FileAccess.open(RESULT_PATH, FileAccess.WRITE)
|
||||||
|
assert(output != null)
|
||||||
|
output.store_string(JSON.stringify({
|
||||||
|
"machine": OS.get_model_name(),
|
||||||
|
"godot": Engine.get_version_info(),
|
||||||
|
"speed": 6,
|
||||||
|
"cycles_per_run": cycles,
|
||||||
|
"repeats": repeats,
|
||||||
|
"input_saves_unchanged": true,
|
||||||
|
"results": results
|
||||||
|
}, "\t", true, true))
|
||||||
|
output.close()
|
||||||
|
print("Wrote paired cadence results: ", RESULT_PATH)
|
||||||
|
quit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://3fogff2lfmy5
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Measures real Stage scheduling on the full-load diagnostic save.
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const RUN_SECONDS: float = 10.0
|
||||||
|
const RESULT_PATH := "res://tests/artifacts/dynamic_cadence_ui_results.json"
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func restore(value):
|
||||||
|
if value is float and value == floorf(value) and absf(value) <= 9007199254740991: return int(value)
|
||||||
|
if value is Dictionary:
|
||||||
|
for key in value: value[key] = restore(value[key])
|
||||||
|
elif value is Array:
|
||||||
|
for i in value.size(): value[i] = restore(value[i])
|
||||||
|
return value
|
||||||
|
|
||||||
|
func prepare(stage, state: Dictionary, adaptive: bool) -> void:
|
||||||
|
var world = stage.owniverse
|
||||||
|
world.state = state.duplicate(true)
|
||||||
|
world.speed = 6
|
||||||
|
world.adaptive_cadence = adaptive
|
||||||
|
world.adaptive_cycle_ema_ms = 0.0
|
||||||
|
world.adaptive_quality = 0
|
||||||
|
world.adaptive_recovery_cycles = 0
|
||||||
|
world.cadence_level = 0
|
||||||
|
world.cadence_recovery_cycles = 0
|
||||||
|
world.frame_step_seconds = 0.2
|
||||||
|
world.frame_elapsed_seconds = 0.0
|
||||||
|
world.frame_clock = world.state.time.duplicate()
|
||||||
|
world.frame_active_clock = {}
|
||||||
|
world._resume_clock = {}
|
||||||
|
world._queued_productions.clear()
|
||||||
|
world.production_queue_busy = false
|
||||||
|
world.frame_busy = false
|
||||||
|
world.frame_commits = 0
|
||||||
|
world.profile.clear()
|
||||||
|
stage.frame_samples.clear()
|
||||||
|
stage.process_samples.clear()
|
||||||
|
stage.previous_frame = 0
|
||||||
|
stage.measuring = false
|
||||||
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
|
stage.set_owniverse_item_value()
|
||||||
|
|
||||||
|
func run_measurement(stage, initial: Dictionary, adaptive: bool) -> Dictionary:
|
||||||
|
prepare(stage, initial, adaptive)
|
||||||
|
var world = stage.owniverse
|
||||||
|
var start_usec: int = Time.get_ticks_usec()
|
||||||
|
var requested_start: Variant = world.frame_clock.datetime
|
||||||
|
var published_start: Variant = world.state.time.datetime
|
||||||
|
stage.measuring = true
|
||||||
|
while float(Time.get_ticks_usec() - start_usec) / 1000000.0 < RUN_SECONDS:
|
||||||
|
await process_frame
|
||||||
|
stage.measuring = false
|
||||||
|
var measured_frames: int = stage.frame_samples.size()
|
||||||
|
# Let the final atomic transaction publish before starting the paired run.
|
||||||
|
while world.frame_busy or world.production_queue_busy:
|
||||||
|
await process_frame
|
||||||
|
var elapsed_usec: int = Time.get_ticks_usec() - start_usec
|
||||||
|
var requested: Variant = E.subtract(world.frame_clock.datetime, requested_start)
|
||||||
|
var published: Variant = E.subtract(world.state.time.datetime, published_start)
|
||||||
|
var samples: Array = stage.frame_samples.duplicate()
|
||||||
|
var total_frame_ms: float = 0.0
|
||||||
|
for value in samples: total_frame_ms += value
|
||||||
|
samples.sort()
|
||||||
|
var result: Dictionary = {
|
||||||
|
"mode": "adaptive_5_4_3_2" if adaptive else "fixed_5",
|
||||||
|
"frames": measured_frames,
|
||||||
|
"elapsed_seconds": float(elapsed_usec) / 1000000.0,
|
||||||
|
"commits": world.frame_commits,
|
||||||
|
"completed_cycles_per_second": float(world.frame_commits) * 1000000.0 / elapsed_usec,
|
||||||
|
"requested_years": E.fixed(requested),
|
||||||
|
"published_years": E.fixed(published),
|
||||||
|
"published_to_requested": snappedf(E.approx(E.divide(published, requested)), 0.00001),
|
||||||
|
"mean_render_fps": float(samples.size()) * 1000.0 / total_frame_ms if total_frame_ms > 0.0 else 0.0,
|
||||||
|
"render_p95_ms": samples[int(ceil(samples.size() * 0.95)) - 1] if not samples.is_empty() else 0.0,
|
||||||
|
"render_max_ms": samples.back() if not samples.is_empty() else 0.0,
|
||||||
|
"final_cadence_hz": 1.0 / world.frame_step_seconds,
|
||||||
|
"final_adaptive_quality": world.adaptive_quality,
|
||||||
|
"cpu_profile_usec_calls": world.profile.duplicate(true)
|
||||||
|
}
|
||||||
|
print("UI_CADENCE_RESULT ", JSON.stringify(result))
|
||||||
|
return result
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var raw = JSON.parse_string(FileAccess.get_file_as_string("res://tests/artifacts/all_objects_speed6.adventure.json"))
|
||||||
|
assert(raw is Dictionary and raw.get("format_version") == 6)
|
||||||
|
var payload: Dictionary = E.decode(restore(raw))
|
||||||
|
var validation: String = root.get_node("SaveManager").validate_data(payload)
|
||||||
|
assert(validation.is_empty(), validation)
|
||||||
|
var scene = load("res://world/world.tscn").instantiate()
|
||||||
|
var stage = scene.get_node("Stage")
|
||||||
|
stage.owniverse.free()
|
||||||
|
stage.set_script(load("res://tests/fixtures/benchmark_ui_stage.gd"))
|
||||||
|
root.add_child(scene)
|
||||||
|
await process_frame
|
||||||
|
stage.owniverse.profile_enabled = true
|
||||||
|
var results: Array[Dictionary] = []
|
||||||
|
for repeat_index in 2:
|
||||||
|
if repeat_index % 2 == 0:
|
||||||
|
results.append(await run_measurement(stage, payload.state, false))
|
||||||
|
results.append(await run_measurement(stage, payload.state, true))
|
||||||
|
else:
|
||||||
|
results.append(await run_measurement(stage, payload.state, true))
|
||||||
|
results.append(await run_measurement(stage, payload.state, false))
|
||||||
|
var report: Dictionary = {"machine": OS.get_model_name(), "godot": Engine.get_version_info(), "speed": 6, "wall_seconds_per_run": RUN_SECONDS, "save": "all_objects_speed6.adventure.json", "results": results}
|
||||||
|
var output := FileAccess.open(RESULT_PATH, FileAccess.WRITE)
|
||||||
|
assert(output != null)
|
||||||
|
output.store_string(JSON.stringify(report, "\t", true, true))
|
||||||
|
output.close()
|
||||||
|
print("UI_CADENCE_RESULTS ", JSON.stringify(report))
|
||||||
|
print("Wrote paired UI cadence results: ", RESULT_PATH)
|
||||||
|
for entity in stage.owniverse.catalog.entities.values(): entity.free()
|
||||||
|
stage.owniverse.free()
|
||||||
|
scene.free()
|
||||||
|
quit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://olp81bbycovp
|
||||||
@@ -14,6 +14,7 @@ func check(condition: bool, message: String) -> void:
|
|||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.adaptive_cadence = false
|
||||||
world.update_adaptive_quality(250000)
|
world.update_adaptive_quality(250000)
|
||||||
check(world.adaptive_quality == 2, "A slow cycle selects the medium approximation")
|
check(world.adaptive_quality == 2, "A slow cycle selects the medium approximation")
|
||||||
world.update_adaptive_quality(250000)
|
world.update_adaptive_quality(250000)
|
||||||
@@ -26,6 +27,12 @@ func run() -> void:
|
|||||||
for i in 30: world.update_adaptive_quality(100000)
|
for i in 30: world.update_adaptive_quality(100000)
|
||||||
check(world.adaptive_quality == 0, "The exact mode returns after sustained headroom")
|
check(world.adaptive_quality == 0, "The exact mode returns after sustained headroom")
|
||||||
|
|
||||||
|
var cadence_world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
for i in 3: cadence_world.update_adaptive_quality(350000)
|
||||||
|
check(cadence_world.cadence_level == 3, "Sustained 350 ms cycles select the 2 Hz cadence")
|
||||||
|
check(is_equal_approx(cadence_world.frame_step_seconds, 0.5), "The lowest cadence uses a 0.5 second frame step")
|
||||||
|
cadence_world.free()
|
||||||
|
|
||||||
var rows: Array = []
|
var rows: Array = []
|
||||||
for node in 12:
|
for node in 12:
|
||||||
var edges: Dictionary = {}
|
var edges: Dictionary = {}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ func run():
|
|||||||
sliced.evolute_frame(0.2, self)
|
sliced.evolute_frame(0.2, self)
|
||||||
check(sliced.frame_busy, "Heavy transaction yields before publishing")
|
check(sliced.frame_busy, "Heavy transaction yields before publishing")
|
||||||
check(E.eq(sliced.state.time.year, 0), "Published clock stays at completed transaction")
|
check(E.eq(sliced.state.time.year, 0), "Published clock stays at completed transaction")
|
||||||
|
check(E.eq(sliced.display_clock().year, Clock.advance({"datetime": 0, "year": 0}, 0.2, sliced.speed).year), "Display clock advances independently of the heavy transaction")
|
||||||
check(sliced.request_production("H", 1, true), "Production requested during a frame is queued")
|
check(sliced.request_production("H", 1, true), "Production requested during a frame is queued")
|
||||||
check(sliced.frame_checkpoint().productions.size() == 1, "Queued production remains part of the save checkpoint")
|
check(sliced.frame_checkpoint().productions.size() == 1, "Queued production remains part of the save checkpoint")
|
||||||
check(E.eq(sliced.state.amount.H, initial_h), "Player action waits for transaction commit")
|
check(E.eq(sliced.state.amount.H, initial_h), "Player action waits for transaction commit")
|
||||||
@@ -117,15 +118,17 @@ func run():
|
|||||||
check(E.eq(behind.state.time.datetime, 100000) and E.eq(behind.state.time, behind.frame_clock), "Delayed start consumes all elapsed time in one cycle")
|
check(E.eq(behind.state.time.datetime, 100000) and E.eq(behind.state.time, behind.frame_clock), "Delayed start consumes all elapsed time in one cycle")
|
||||||
behind.evolute_frame(0.0, self)
|
behind.evolute_frame(0.0, self)
|
||||||
check(behind.frame_commits == 1 and not behind.frame_busy, "Consumed interval cannot be processed twice")
|
check(behind.frame_commits == 1 and not behind.frame_busy, "Consumed interval cannot be processed twice")
|
||||||
behind.evolute_frame(0.23, self)
|
var next_interval: float = behind.frame_step_seconds + 0.03
|
||||||
|
behind.evolute_frame(next_interval, self)
|
||||||
var remained_busy: bool = behind.frame_busy
|
var remained_busy: bool = behind.frame_busy
|
||||||
if remained_busy: behind.evolute_frame(0.53, self)
|
if remained_busy: behind.evolute_frame(0.53, self)
|
||||||
var pending_clock: Dictionary = behind.frame_clock.duplicate()
|
var pending_clock: Dictionary = behind.frame_clock.duplicate()
|
||||||
if remained_busy:
|
if remained_busy:
|
||||||
check(E.eq(behind.frame_active_clock.datetime, 123000), "Busy transaction reserves its original target")
|
var expected_active: Dictionary = Clock.advance({"datetime": 100000, "year": 100000}, next_interval, behind.speed)
|
||||||
|
check(E.eq(behind.frame_active_clock, expected_active), "Busy transaction reserves its original target")
|
||||||
while behind.frame_busy: await process_frame
|
while behind.frame_busy: await process_frame
|
||||||
if remained_busy:
|
if remained_busy:
|
||||||
check(E.eq(behind.state.time.datetime, 123000), "Busy transaction keeps its original target")
|
check(E.eq(behind.state.time, behind.frame_active_clock), "Busy transaction keeps its original target")
|
||||||
behind.evolute_frame(0.0, self)
|
behind.evolute_frame(0.0, self)
|
||||||
while behind.frame_busy: await process_frame
|
while behind.frame_busy: await process_frame
|
||||||
check(E.eq(behind.state.time, pending_clock), "Next cycle consumes all time accumulated during computation")
|
check(E.eq(behind.state.time, pending_clock), "Next cycle consumes all time accumulated during computation")
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ func coarsen_cycle_stars(target: Dictionary, id: String) -> void:
|
|||||||
func temporal_resolution(target: Dictionary) -> Variant:
|
func temporal_resolution(target: Dictionary) -> Variant:
|
||||||
var speed: int = int(target.get("speed", 1))
|
var speed: int = int(target.get("speed", 1))
|
||||||
if speed < 4: return 0
|
if speed < 4: return 0
|
||||||
var width: Variant = E.divide(E.power(10, speed - 1), 10)
|
# Keep the existing 0.1-second resolution at the default 5 Hz cadence,
|
||||||
|
# and scale temporal grouping with the adaptive cycle interval.
|
||||||
|
var width: Variant = E.multiply(E.power(10, speed - 1), world.frame_step_seconds / 2.0)
|
||||||
return E.multiply(width, E.power(2, world.adaptive_quality))
|
return E.multiply(width, E.power(2, world.adaptive_quality))
|
||||||
|
|
||||||
func window_duration(target: Dictionary, lifespan: Variant) -> Variant:
|
func window_duration(target: Dictionary, lifespan: Variant) -> Variant:
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ var adaptive_quality: int = 0
|
|||||||
var adaptive_cycle_ema_ms: float = 0.0
|
var adaptive_cycle_ema_ms: float = 0.0
|
||||||
var adaptive_recovery_cycles: int = 0
|
var adaptive_recovery_cycles: int = 0
|
||||||
var adaptive_generation_phase: int = 0
|
var adaptive_generation_phase: int = 0
|
||||||
|
## Runtime-only cadence: 5, 4, 3, or 2 full simulation cycles per second.
|
||||||
|
var adaptive_cadence: bool = true
|
||||||
|
var cadence_level: int = 0
|
||||||
|
var cadence_recovery_cycles: int = 0
|
||||||
|
const CADENCE_STEPS: Array[float] = [0.2, 0.25, 1.0 / 3.0, 0.5]
|
||||||
|
var frame_step_seconds: float = 0.2
|
||||||
|
|
||||||
func update_adaptive_quality(cycle_usec: int) -> void:
|
func update_adaptive_quality(cycle_usec: int) -> void:
|
||||||
var cycle_ms: float = float(cycle_usec) / 1000.0
|
var cycle_ms: float = float(cycle_usec) / 1000.0
|
||||||
@@ -46,6 +52,20 @@ func update_adaptive_quality(cycle_usec: int) -> void:
|
|||||||
adaptive_recovery_cycles = 0
|
adaptive_recovery_cycles = 0
|
||||||
else:
|
else:
|
||||||
adaptive_recovery_cycles = 0
|
adaptive_recovery_cycles = 0
|
||||||
|
if adaptive_cadence:
|
||||||
|
var step: float = CADENCE_STEPS[cadence_level]
|
||||||
|
if adaptive_cycle_ema_ms > step * 900.0 and cadence_level < CADENCE_STEPS.size() - 1:
|
||||||
|
cadence_level += 1
|
||||||
|
frame_step_seconds = CADENCE_STEPS[cadence_level]
|
||||||
|
cadence_recovery_cycles = 0
|
||||||
|
elif adaptive_cycle_ema_ms < step * 650.0 and cadence_level > 0:
|
||||||
|
cadence_recovery_cycles += 1
|
||||||
|
if cadence_recovery_cycles >= 30:
|
||||||
|
cadence_level -= 1
|
||||||
|
frame_step_seconds = CADENCE_STEPS[cadence_level]
|
||||||
|
cadence_recovery_cycles = 0
|
||||||
|
else:
|
||||||
|
cadence_recovery_cycles = 0
|
||||||
if adaptive_quality >= 3:
|
if adaptive_quality >= 3:
|
||||||
adaptive_generation_phase = 1 - adaptive_generation_phase
|
adaptive_generation_phase = 1 - adaptive_generation_phase
|
||||||
|
|
||||||
@@ -103,9 +123,12 @@ var _production_queue_cancelled: bool = false
|
|||||||
var frame_active_clock: Dictionary = {}
|
var frame_active_clock: Dictionary = {}
|
||||||
var _resume_clock: Dictionary = {}
|
var _resume_clock: Dictionary = {}
|
||||||
var frame_elapsed_seconds: float = 0.0
|
var frame_elapsed_seconds: float = 0.0
|
||||||
const frame_step_seconds: float = 0.2
|
|
||||||
const production_slice_usec: int = 4000
|
const production_slice_usec: int = 4000
|
||||||
|
|
||||||
|
## The HUD follows requested time; world state remains at the last committed cycle.
|
||||||
|
func display_clock() -> Dictionary:
|
||||||
|
return frame_clock if not frame_clock.is_empty() else state.time
|
||||||
|
|
||||||
## Keep the requested clock advancing while a transaction spans render frames.
|
## Keep the requested clock advancing while a transaction spans render frames.
|
||||||
func evolute_frame(delta: float, tree: SceneTree) -> void:
|
func evolute_frame(delta: float, tree: SceneTree) -> void:
|
||||||
if not is_finite(delta) or delta < 0.0 or not is_finite(delta * pow(10.0, speed - 1)): return
|
if not is_finite(delta) or delta < 0.0 or not is_finite(delta * pow(10.0, speed - 1)): return
|
||||||
|
|||||||
Reference in New Issue
Block a user