Refactor simulation timing and frame processing logic for improved accuracy and performance
This commit is contained in:
+3
-3
@@ -46,9 +46,9 @@ float. Весь порядок E27 сохраняет целые единицы.
|
|||||||
|
|
||||||
### Общие окна рождения и эволюции
|
### Общие окна рождения и эволюции
|
||||||
|
|
||||||
На всех скоростях полный цикл симуляции обрабатывает 0,2 реальной секунды: пять циклов на секунду запрошенного времени. До накопления полного интервала расчёт не запускается. Игровая длительность цикла равна `0.2 × 10^(speed - 1)` лет. При отставании размер шага не увеличивается, долг сохраняется. Незавершённая транзакция из сохранения возобновляется с прежней границей. Кооперативный бюджет кадра — 40 мс; частота отрисовки не определяет частоту полных циклов.
|
На всех скоростях целевая частота запуска — пять полных циклов в секунду. После накопления не менее 0,2 реальной секунды запускается расчёт за весь ожидающий игровой интервал. Время, поступающее во время расчёта, остаётся для следующего цикла. При задержках шаг увеличивается; время не отбрасывается. Смена скорости не сокращает реальный интервал между запусками. Незавершённая транзакция из сохранения возобновляется с прежней границей. Кооперативный бюджет кадра — 40 мс; фактическая частота завершения зависит от стоимости расчёта.
|
||||||
|
|
||||||
На скоростях 4–6 укрупнение звёздных партий зависит от размера цикла и прежнего окна рождения. Размер цикла — `0.2 × 10^(speed - 1)` игровых лет: 200, 2000 и 20000 лет. Если прежнее окно `max(lifespan / 40, временное разрешение)` шире цикла, оно сохраняется: долгоживущие звёзды не дробятся на более мелкую сетку. Если оно не шире цикла, новые звёзды группы `Star` (включая красные гиганты) объединяются по общей границе завершения жизни и переходят целиком.
|
На скоростях 4–6 укрупнение звёздных партий зависит от размера цикла и прежнего окна рождения. Номинальная ширина сетки партий — `0.2 × 10^(speed - 1)` игровых лет: 200, 2000 и 20000 лет. Если прежнее окно `max(lifespan / 40, временное разрешение)` шире цикла, оно сохраняется: долгоживущие звёзды не дробятся на более мелкую сетку. Если оно не шире цикла, новые звёзды группы `Star` (включая красные гиганты) объединяются по общей границе завершения жизни и переходят целиком.
|
||||||
|
|
||||||
При обработке существующих партий объединяются только две или более партии одного типа, весь оставшийся интервал превращения которых попадает в один и тот же цикл. Широкие партии и партии, пересекающие границу циклов, остаются без изменений. Количества и накопленные остатки складываются точно; объединённая партия переходит целиком на границе цикла. Повторное укрупнение не меняет результат.
|
При обработке существующих партий объединяются только две или более партии одного типа, весь оставшийся интервал превращения которых попадает в один и тот же цикл. Широкие партии и партии, пересекающие границу циклов, остаются без изменений. Количества и накопленные остатки складываются точно; объединённая партия переходит целиком на границе цикла. Повторное укрупнение не меняет результат.
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ GI не хранит накопленные счётчики первичной
|
|||||||
|
|
||||||
Прогресс и деградация округляются вниз с сохранением остатков. Если оба накопленных исхода конкурируют за последнюю планету, распределяются только существующие объекты, а неиспользованный остаток переносится на следующую попытку. Деградация распределяется по весам `fall`; знаковые ошибки распределения также сохраняются.
|
Прогресс и деградация округляются вниз с сохранением остатков. Если оба накопленных исхода конкурируют за последнюю планету, распределяются только существующие объекты, а неиспользованный остаток переносится на следующую попытку. Деградация распределяется по весам `fall`; знаковые ошибки распределения также сохраняются.
|
||||||
|
|
||||||
Для всех шести стадий базовые доли за попытку — `progress=0.00025`, `degradation=0.0025`. Интервалы HP/EON1/EON2/EON3/EON4/EON5: 5 млн, 10 млн, 10 млн, 1 млн, 1 млн, 500 тыс. лет. Используются постепенные окна GI шириной `span / 40`. Случайные множители Unity не применяются. Агрессия, порядок и просвещение изменяют вероятности по формулам из раздела индикаторов ниже.
|
Базовые доли за попытку: для HP `progress=0.00025`, для EON1–EON5 `progress=0.025`; у всех `degradation=0.0025`. Текущие интервалы HP/EON1/EON2/EON3/EON4/EON5: 5 млн, 1 млн, 1 млн, 100 тыс., 100 тыс., 500 тыс. лет. Используются постепенные окна GI шириной `span / 40`. Случайные множители Unity не применяются. Агрессия, порядок и просвещение изменяют вероятности по формулам из раздела индикаторов ниже.
|
||||||
|
|
||||||
HP деградирует в RP. У эонов возможен откат к предыдущим стадиям по таблице. RP не развивается сам. C0.0 и C0.1 также развиваются: период 2000 лет, прогресс 0.0175, деградация 0.025. Переходы C0.0 → C0.1 → C0.2, откат C0.0 → EON5 и C0.1 → C0.0. Развитие C0.2 и выше подключено через этические правила. Конфликт civ_bonus устранён повторным импортом: бонусы структур совпадают с Unity. Для существующих этических цивилизаций пользователь выбрал равномерное распределение между шестью этиками с детерминированным целым остатком.
|
HP деградирует в RP. У эонов возможен откат к предыдущим стадиям по таблице. RP не развивается сам. C0.0 и C0.1 также развиваются: период 2000 лет, прогресс 0.0175, деградация 0.025. Переходы C0.0 → C0.1 → C0.2, откат C0.0 → EON5 и C0.1 → C0.0. Развитие C0.2 и выше подключено через этические правила. Конфликт civ_bonus устранён повторным импортом: бонусы структур совпадают с Unity. Для существующих этических цивилизаций пользователь выбрал равномерное распределение между шестью этиками с детерминированным целым остатком.
|
||||||
|
|
||||||
|
|||||||
+2
-9
@@ -243,9 +243,7 @@ godot --path Owniverse.GI --script res://tests/benchmark_civilizations.gd -- ui
|
|||||||
Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает
|
Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает
|
||||||
следующему кадру внутри создания партий и обработки стадий. Расчёт остаётся
|
следующему кадру внутри создания партий и обработки стадий. Расчёт остаётся
|
||||||
на главном потоке, состояние публикуется только после полного завершения.
|
на главном потоке, состояние публикуется только после полного завершения.
|
||||||
Одна транзакция обрабатывает 0,2 реальной секунды на любой скорости. Запрошенное время хранится отдельно, поэтому отставание не превращается
|
Расчёт запускается после накопления 0,2 реальной секунды и обрабатывает весь ожидающий интервал. Время, поступившее во время расчёта, обрабатывается следующим циклом. При перегрузке шаг растёт; пять завершений в секунду остаются целевой частотой, а не гарантией.
|
||||||
в один всё более тяжёлый шаг. При предельной нагрузке время симуляции может
|
|
||||||
отставать от запрошенного; это не ограничение FPS интерфейса и не удаление событий.
|
|
||||||
|
|
||||||
Производство во время транзакции выполняется после её завершения. Запрошенное
|
Производство во время транзакции выполняется после её завершения. Запрошенное
|
||||||
время, активный интервал, скорость и очередь производства входят в сохранение;
|
время, активный интервал, скорость и очередь производства входят в сохранение;
|
||||||
@@ -292,12 +290,7 @@ Stage вызывает `evolute_frame`: транзакция получает б
|
|||||||
1.057. Замер включает сокращение накопленного отставания сохранения;
|
1.057. Замер включает сокращение накопленного отставания сохранения;
|
||||||
это результат конкретного сценария на Mac, а не гарантия для Android.
|
это результат конкретного сценария на Mac, а не гарантия для Android.
|
||||||
|
|
||||||
Полный шаг фиксирован: 0,2 реальной секунды на всех скоростях, пять циклов
|
Целевая частота запуска — пять циклов в секунду. Размер шага равен всему накопленному интервалу. Checkpoint сохраняет границу активного расчёта и ожидающее реальное время. Тест проверяет все шесть скоростей, смену скорости, задержку запуска и накопление времени во время расчёта.
|
||||||
на секунду запрошенного времени. Неполный интервал накапливается. При отставании
|
|
||||||
шаг сохраняется, долг не отбрасывается. Возобновляемая транзакция сохраняет
|
|
||||||
границу из checkpoint. Бюджет кадра остаётся 40 мс. `test_frame_evolution`
|
|
||||||
проверяет расписание всех шести скоростей, сохранение долга и пять циклов
|
|
||||||
на одну секунду.
|
|
||||||
|
|
||||||
Дипломатический drift использует шесть взвешенных вкладов вместо матрицы
|
Дипломатический drift использует шесть взвешенных вкладов вместо матрицы
|
||||||
пар этик: вес = сумма вкладов + 195 × свой вклад − противоположный вклад.
|
пар этик: вес = сумма вкладов + 195 × свой вклад − противоположный вклад.
|
||||||
|
|||||||
@@ -50,7 +50,9 @@ func run():
|
|||||||
direct.speed = 3
|
direct.speed = 3
|
||||||
check(E.eq(direct.state, sliced.state), "Queued action and speed change survive publication")
|
check(E.eq(direct.state, sliced.state), "Queued action and speed change survive publication")
|
||||||
sliced.evolute_frame(0.001, self)
|
sliced.evolute_frame(0.001, self)
|
||||||
var second_clock: Dictionary = Clock.advance(requested, 0.001, 3)
|
check(not sliced.frame_busy, "Speed change does not bypass real-time cadence")
|
||||||
|
sliced.evolute_frame(0.189, self)
|
||||||
|
var second_clock: Dictionary = Clock.advance(Clock.advance(requested, 0.001, 3), 0.189, 3)
|
||||||
var active: Dictionary = sliced.frame_active_clock.duplicate()
|
var active: Dictionary = sliced.frame_active_clock.duplicate()
|
||||||
while sliced.frame_busy: await process_frame
|
while sliced.frame_busy: await process_frame
|
||||||
direct.evolution.evolute_to_datetime(active.datetime, active)
|
direct.evolution.evolute_to_datetime(active.datetime, active)
|
||||||
@@ -111,11 +113,17 @@ func run():
|
|||||||
var behind = fixture()
|
var behind = fixture()
|
||||||
behind.evolute_frame(1.0, self)
|
behind.evolute_frame(1.0, self)
|
||||||
while behind.frame_busy: await process_frame
|
while behind.frame_busy: await process_frame
|
||||||
check(E.eq(behind.state.time.datetime, 20000) and E.eq(behind.frame_clock.datetime, 100000), "Debt keeps the fixed step and preserves all requested time")
|
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")
|
||||||
for cycle in 4:
|
behind.evolute_frame(0.0, self)
|
||||||
|
check(behind.frame_commits == 1 and not behind.frame_busy, "Consumed interval cannot be processed twice")
|
||||||
|
behind.evolute_frame(0.23, self)
|
||||||
|
behind.evolute_frame(0.53, self)
|
||||||
|
var pending_clock: Dictionary = behind.frame_clock.duplicate()
|
||||||
|
while behind.frame_busy: await process_frame
|
||||||
|
check(E.eq(behind.state.time.datetime, 123000), "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(behind.frame_commits == 5 and E.eq(behind.state.time, behind.frame_clock), "One second is processed in exactly five full cycles")
|
check(E.eq(behind.state.time, pending_clock), "Next cycle consumes all time accumulated during computation")
|
||||||
dispose(behind)
|
dispose(behind)
|
||||||
print("Frame evolution: %d checks, %d failures" % [checks, failures])
|
print("Frame evolution: %d checks, %d failures" % [checks, failures])
|
||||||
quit(1 if failures else 0)
|
quit(1 if failures else 0)
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ var _requested_speed: int = 0
|
|||||||
var _queued_productions: Array = []
|
var _queued_productions: Array = []
|
||||||
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
|
||||||
const frame_step_seconds: float = 0.2
|
const frame_step_seconds: float = 0.2
|
||||||
|
|
||||||
## Keep the requested clock advancing while a transaction spans render frames.
|
## Keep the requested clock advancing while a transaction spans render frames.
|
||||||
@@ -73,12 +74,14 @@ 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
|
||||||
if frame_clock.is_empty(): frame_clock = state.time.duplicate()
|
if frame_clock.is_empty(): frame_clock = state.time.duplicate()
|
||||||
frame_clock = SimulationTime.advance(frame_clock, delta, speed)
|
frame_clock = SimulationTime.advance(frame_clock, delta, speed)
|
||||||
|
frame_elapsed_seconds += delta
|
||||||
if frame_busy: return
|
if frame_busy: return
|
||||||
var next_clock: Dictionary = _resume_clock
|
var next_clock: Dictionary = _resume_clock
|
||||||
if next_clock.is_empty():
|
if next_clock.is_empty():
|
||||||
next_clock = SimulationTime.advance(state.time, frame_step_seconds, speed)
|
# Cadence limits starts; each transaction consumes the whole pending interval.
|
||||||
# Start only after a full 0.2 seconds of simulation time is requested.
|
if frame_elapsed_seconds + 1e-12 < frame_step_seconds: return
|
||||||
if E.gt(next_clock.datetime, frame_clock.datetime): return
|
next_clock = frame_clock.duplicate()
|
||||||
|
frame_elapsed_seconds = 0.0
|
||||||
frame_busy = true
|
frame_busy = true
|
||||||
frame_budget = preload("res://world/owniverse/frame_budget.gd").new(tree)
|
frame_budget = preload("res://world/owniverse/frame_budget.gd").new(tree)
|
||||||
frame_active_clock = next_clock
|
frame_active_clock = next_clock
|
||||||
@@ -95,11 +98,14 @@ func request_production(id: String, amount: Variant, resource: bool = false) ->
|
|||||||
|
|
||||||
## Persist work requests, never the partially calculated transaction.
|
## Persist work requests, never the partially calculated transaction.
|
||||||
func frame_checkpoint() -> Dictionary:
|
func frame_checkpoint() -> Dictionary:
|
||||||
return {"requested_clock": frame_clock, "active_clock": frame_active_clock if frame_busy else {}, "speed": speed, "productions": _queued_productions}
|
return {"requested_clock": frame_clock, "active_clock": frame_active_clock if frame_busy else {}, "speed": speed, "productions": _queued_productions, "elapsed_seconds": frame_elapsed_seconds}
|
||||||
|
|
||||||
func restore_frame_checkpoint(checkpoint: Dictionary) -> void:
|
func restore_frame_checkpoint(checkpoint: Dictionary) -> void:
|
||||||
frame_clock = checkpoint.get("requested_clock", {})
|
frame_clock = checkpoint.get("requested_clock", {})
|
||||||
_resume_clock = checkpoint.get("active_clock", {})
|
_resume_clock = checkpoint.get("active_clock", {})
|
||||||
|
var reserved_clock: Dictionary = _resume_clock if not _resume_clock.is_empty() else state.time
|
||||||
|
var pending_years: Variant = E.maximum(0, E.subtract(frame_clock.get("datetime", reserved_clock.datetime), reserved_clock.datetime))
|
||||||
|
frame_elapsed_seconds = float(checkpoint.get("elapsed_seconds", E.approx(pending_years) / pow(10.0, int(checkpoint.get("speed", state.speed)) - 1)))
|
||||||
_queued_productions = checkpoint.get("productions", [])
|
_queued_productions = checkpoint.get("productions", [])
|
||||||
if _resume_clock.is_empty():
|
if _resume_clock.is_empty():
|
||||||
speed = checkpoint.get("speed", state.speed)
|
speed = checkpoint.get("speed", state.speed)
|
||||||
|
|||||||
Reference in New Issue
Block a user