feat(runtime): distribute Control provider amounts
Docs Deploy / Build and Deploy MkDocs (push) Successful in 39s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 39s
This commit is contained in:
5 files changed
+1022
-38
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -1464,6 +1464,10 @@ internal static class BehaviorDistributorTickOracle {
|
||||
return ReadFloat(table.Rows, index * 0x2c + 0x08);
|
||||
}
|
||||
|
||||
private static float ReadNativeProviderAccumulatedDelta(NativeProviderTable table, int index) {
|
||||
return ReadFloat(table.Rows, index * 0x2c + 0x10);
|
||||
}
|
||||
|
||||
private static byte ReadNativeProviderFlags(NativeProviderTable table, int index) {
|
||||
return Marshal.ReadByte(table.Rows, index * 0x2c + 0x28);
|
||||
}
|
||||
@@ -1489,6 +1493,10 @@ internal static class BehaviorDistributorTickOracle {
|
||||
"Control proportional distribution publishes each row update through the existing native node callback");
|
||||
Expect(String.Join(",", controlNodeUpdateIndices.ConvertAll(index => index.ToString()).ToArray()) == "0,1,2",
|
||||
"Control proportional distribution visits rows in ascending order");
|
||||
Expect(ReadNativeProviderAccumulatedDelta(table, 0) == 4.0f &&
|
||||
ReadNativeProviderAccumulatedDelta(table, 1) == 8.0f &&
|
||||
ReadNativeProviderAccumulatedDelta(table, 2) == 10.0f,
|
||||
"Control distribution accumulates each actual amount change in provider row +0x10");
|
||||
}
|
||||
|
||||
using (Fixture fixture = NewFixture()) {
|
||||
@@ -1503,6 +1511,58 @@ internal static class BehaviorDistributorTickOracle {
|
||||
"Control native sequential withdrawal consumes rows in reverse order");
|
||||
Expect(String.Join(",", controlNodeUpdateIndices.ConvertAll(index => index.ToString()).ToArray()) == "2,1",
|
||||
"Control sequential withdrawal calls the node updater from the last row backwards");
|
||||
Expect(ReadNativeProviderAccumulatedDelta(table, 0) == 0.0f &&
|
||||
ReadNativeProviderAccumulatedDelta(table, 1) == -5.0f &&
|
||||
ReadNativeProviderAccumulatedDelta(table, 2) == -30.0f,
|
||||
"Control reverse withdrawal accumulates the applied changes in provider row +0x10");
|
||||
}
|
||||
|
||||
using (Fixture fixture = NewFixture()) {
|
||||
NativeProviderTable table = CreateNativeProviderTable(fixture, module,
|
||||
new float[] { 33554432.0f, 3.0f }, new float[] { 16777216.0f, 2.0f },
|
||||
new byte[] { 0, 0 });
|
||||
for (int i = 0; i < 2; i++) {
|
||||
IntPtr row = IntPtr.Add(table.Rows, i * 0x2c);
|
||||
Marshal.WriteInt32(row, 0x1c, 1); // Avoid the zero-count synthetic stage boundary.
|
||||
Marshal.WriteByte(row, 0x28, 0x02);
|
||||
}
|
||||
distribute(table.Owner, 33554432.0f, 1, 0);
|
||||
Expect(ReadNativeProviderAmount(table, 0) == 33554432.0f &&
|
||||
ReadNativeProviderAmount(table, 1) == 3.0f,
|
||||
"Control sequential mode keeps the original request after summed free capacity rounds down");
|
||||
Expect(ReadNativeProviderAccumulatedDelta(table, 0) == 16777216.0f &&
|
||||
ReadNativeProviderAccumulatedDelta(table, 1) == 1.0f,
|
||||
"Control sequential mode accumulates each row's applied amount at the capacity-rounding boundary");
|
||||
}
|
||||
|
||||
using (Fixture fixture = NewFixture()) {
|
||||
NativeProviderTable table = CreateNativeProviderTable(fixture, module,
|
||||
new float[] { 33554432.0f, 3.0f }, new float[] { 33554432.0f, 3.0f },
|
||||
new byte[] { 0, 0 });
|
||||
for (int i = 0; i < 2; i++) {
|
||||
IntPtr row = IntPtr.Add(table.Rows, i * 0x2c);
|
||||
Marshal.WriteInt32(row, 0x1c, 1);
|
||||
Marshal.WriteByte(row, 0x28, 0x02);
|
||||
}
|
||||
NativeControlProviderAmountUpdate update = (NativeControlProviderAmountUpdate)
|
||||
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlProviderAmountUpdateRva),
|
||||
typeof(NativeControlProviderAmountUpdate));
|
||||
update(table.Owner, 1, -1.0f, 0);
|
||||
update(table.Owner, 1, -1.0f, 0);
|
||||
update(table.Owner, 1, -1.0f, 0);
|
||||
update(table.Owner, 1, 3.0f, 0);
|
||||
int roundedAmount = FloatBits(ReadFloat(table.Owner, 0x590));
|
||||
Expect(ReadNativeProviderAmount(table, 1) == 3.0f &&
|
||||
ReadFloat(table.Owner, 0x58c) == 33554436.0f &&
|
||||
roundedAmount == FloatBits(33554440.0f),
|
||||
"Control ordered aggregate updates can round the current amount above its initial capacity");
|
||||
distribute(table.Owner, 1.0f, 0, 1);
|
||||
Expect(ReadNativeProviderAmount(table, 0) == 33554432.0f &&
|
||||
ReadNativeProviderAmount(table, 1) == 3.0f &&
|
||||
ReadFloat(table.Owner, 0x58c) == 33554436.0f &&
|
||||
ReadFloat(table.Owner, 0x590) == 33554440.0f,
|
||||
"Control proportional revive path preserves signed negative capacity after native aggregate rounding");
|
||||
GC.KeepAlive(update);
|
||||
}
|
||||
|
||||
using (Fixture fixture = NewFixture()) {
|
||||
@@ -1524,6 +1584,9 @@ internal static class BehaviorDistributorTickOracle {
|
||||
(ReadNativeProviderFlags(table, 0) & 0x10) == 0 &&
|
||||
(ReadNativeProviderFlags(table, 1) & 0x10) == 0,
|
||||
"Control native revive flag admits exhausted rows and clears their exhausted flags when refilled");
|
||||
Expect(ReadNativeProviderAccumulatedDelta(table, 0) == 1.0f &&
|
||||
ReadNativeProviderAccumulatedDelta(table, 1) == 5.0f,
|
||||
"Control refill adds to the existing provider row +0x10 accumulator");
|
||||
}
|
||||
|
||||
using (Fixture fixture = NewFixture()) {
|
||||
@@ -1539,6 +1602,9 @@ internal static class BehaviorDistributorTickOracle {
|
||||
secondBits == 0x3ffffffe && totalBits == unchecked((uint)FloatBits(16777216.0f)),
|
||||
"Control proportional distribution preserves the native x87 add-then-subtract rounding at a 2^24 cancellation boundary; actual=[" +
|
||||
Hex(firstBits) + "," + Hex(secondBits) + "] total=" + Hex(totalBits));
|
||||
Expect(FloatBits(ReadNativeProviderAccumulatedDelta(table, 0)) == FloatBits(-2.0f) &&
|
||||
FloatBits(ReadNativeProviderAccumulatedDelta(table, 1)) == unchecked((int)0xb4800000),
|
||||
"Control distribution accumulates the rounded applied change, not the requested delta");
|
||||
}
|
||||
|
||||
Console.WriteLine("native-control-provider-distribution: controlHash=" + ExpectedControlSha256 +
|
||||
|
||||
@@ -338,17 +338,37 @@ MSH-часть проверки подтверждает начальный сн
|
||||
остаётся статическим выводом по RVA `AniMesh.dll+0xa460`, а последующие
|
||||
изменения в полном Control/animation tick-е не проверялись.
|
||||
|
||||
Для provider-строки Rust сохраняет исходные и текущие amount/type-1 поля
|
||||
отдельно, а `processed_amount_bits` соответствует native `+0x0c`. Инициализатор
|
||||
`Control.dll+0xf940` кладёт результат selector `3` в текущее и обработанное
|
||||
количество; при подтверждённых единичных множителях это raw `max_volume` NDP.
|
||||
Прямой API `apply_loaded_mission_control_node_delta` меняет amount, долю, флаг
|
||||
истощения, aggregate и dirty, применяя публикацию `0x200` к CPU-проекции
|
||||
соответствующего узла модели. Исходный `AniMesh.dll+0x5750` подтверждён в
|
||||
связке с `Control+0x10f30`: меняется только поле узла `+0x124`, а стадия и
|
||||
соседние узлы сохраняются. Эта проекция ещё не подключена к GPU/window path
|
||||
и сама delta-операция не выполняет пересчёт стадий, видимости, FX или время
|
||||
Control. Обработанное количество при ней остаётся прежним. Отдельный
|
||||
Для provider-строки Rust хранит amount `+0x08`, накопитель `+0x10`, обработанное
|
||||
количество `+0x0c`, долю `+0x14`, стадию `+0x18` и flags `+0x28` раздельно.
|
||||
Инициализатор `Control.dll+0xf940` кладёт результат selector `3` в текущее и
|
||||
обработанное количество; при подтверждённых единичных множителях это raw
|
||||
`max_volume` NDP. Таблица также хранит capacity aggregate `+0x58c`, текущий
|
||||
aggregate `+0x590`, dirty `+0x598` и начальный gate `+0x618`.
|
||||
|
||||
Прямой API `apply_loaded_mission_control_node_delta` сохраняет native четвёртый
|
||||
аргумент `0`: меняет amount, долю, флаг истощения, aggregate и dirty, оставляя
|
||||
`+0x10` нетронутым. Публикация `0x200` отражается в CPU-проекции соответствующего
|
||||
узла модели по правилу исходного обработчика; стадия и соседние узлы не меняются.
|
||||
Обработанное количество при этой прямой операции остаётся прежним.
|
||||
API `distribute_loaded_mission_control_provider_amount` переносит
|
||||
`Control+0x10ba0`: proportional mode делит добавку по свободной ёмкости, а
|
||||
вычитание распределяет по текущим количествам; sequential заполняет строки
|
||||
вперёд, снимает с конца и использует исходный запрос даже если общая ёмкость
|
||||
округлилась вниз. Row updater вызывается с четвёртым аргументом `1`, поэтому
|
||||
`+0x10` получает фактическую разницу amount. После распределения обязателен
|
||||
`0x11130(root, true)`.
|
||||
|
||||
Оба API работают только с CPU-проекцией и не подключены к GPU/window path или
|
||||
игровому Control clock. Gate `+0x618` изначально выключен; событие `7` задаёт его
|
||||
по правилу `gate = (arg == 2)` и вызывает `0x2dc20`, только если gate изменился.
|
||||
События Control в Rust пока не исполняются.
|
||||
Capacity aggregate выведен при подтверждённых единичных множителях Unit DAT;
|
||||
изменение масштабов `0x9ee0`/`0x9f90` не реализовано. Из-за округления `+0x590`
|
||||
может стать больше сохранённого `+0x58c`; распределение сохраняет этот signed
|
||||
capacity по native формуле, не вводя отдельного запрета. Если итоговый stage walk
|
||||
нуждается в неизвестном состоянии, depletion lifecycle, FX или host callback,
|
||||
распределение сообщает ошибку и не публикует ни одной частичной записи.
|
||||
Отдельный
|
||||
`update_loaded_mission_control_node_stage` реализует наследование parent ratio
|
||||
и stage, ceiling-арифметику, CPU visible stage и копию processed amount.
|
||||
Отдельная `propagate_loaded_mission_control_node_stage` переносит `0x11130`:
|
||||
|
||||
@@ -624,12 +624,25 @@ oracle вызывает оригинальные `FUN_10010ba0` (RVA `0x10ba0`),
|
||||
`0xdc40`. Пропорциональный режим проверен на максимумах `[30, 60, 50]`, текущих
|
||||
значениях `[10, 20, 0]` и добавке `22`, результат `[14, 28, 10]`; отдельный
|
||||
вектор подтверждает последовательное заполнение вперёд и вычитание с конца.
|
||||
Проверены ограничение строк, снятие признака истощения при пополнении и порядок
|
||||
обратных вызовов. При x87 control word `0x007f` для `[2^24, 2]` с запросом `-1`
|
||||
получены значения `[16777214, 1.99999976]` и итог `16777216`: округление
|
||||
вычисленного изменения даёт `-2`. Oracle задаёт внешнюю публикацию результата
|
||||
обратным вызовом; provider строки не имеют родительских узлов и не проходят
|
||||
отдельный жизненный цикл истощения корневого объекта.
|
||||
Проверены ограничение строк, пропуск истощённых записей, их пополнение с флагом
|
||||
revive и порядок обратных вызовов. Все распределительные вызовы используют
|
||||
четвёртый аргумент `1` в `0x10f30`: фактическое изменение строки прибавляется к
|
||||
её полю `+0x10`. Для исходных значений `[2^24, 2]` и запроса `-1` получены
|
||||
`[16777214, 1.99999976]`, общий итог `16777216` и накопленные разницы
|
||||
`[-2, -2.3841858e-7]`. Проверка также подтверждает, что последовательный режим
|
||||
несёт исходный запрос между строками: сумма свободных мест `[16777216, 1]`
|
||||
округляется до `16777216`, однако запрос `33554432` заполняет обе строки.
|
||||
Oracle задаёт внешнюю публикацию результата обратным вызовом; provider-строки
|
||||
не имеют родительских узлов и не проходят отдельный жизненный цикл истощения
|
||||
корневого объекта.
|
||||
|
||||
Агрегаты тоже округляются независимо. Начальная ёмкость `+0x58c = 33554436`
|
||||
для максимумов `[33554432, 3]` не меняется при обновлениях одной строки
|
||||
`-1, -1, -1, +3`, но текущий итог `+0x590` округляется до `33554440`.
|
||||
Следующая добавка `1` с revive использует signed capacity `-4`, выдаёт ratio `1`
|
||||
и сохраняет уже полные строки. Native oracle и Rust повторяют эту достижимую
|
||||
последовательность; отрицательный результат разности сам по себе не является
|
||||
ошибкой состояния.
|
||||
|
||||
Для `FUN_10012a40` (RVA `0x12a40`) нативный вектор задаёт класс `4`, интервал
|
||||
`2`, коэффициент `+0x1a4` равный `1` и текущее значение `10.000001`; результат
|
||||
@@ -654,14 +667,16 @@ oracle вызывает оригинальные `FUN_10010ba0` (RVA `0x10ba0`),
|
||||
хосте класса `10`, как описано ниже. Источник входного времени и порядок его
|
||||
доставки до обработчика установлены статически;
|
||||
Rust ещё не вызывает этот бюджетный цикл; отдельно перенесён прямой путь
|
||||
изменения одной строки `0x10f30`, описанный ниже.
|
||||
изменения одной строки `0x10f30` и явный шаг распределения `0x10ba0`, описанные
|
||||
ниже. Ни один из них пока не подключён к игровому Control clock.
|
||||
|
||||
#### Прямое изменение одной строки в Rust
|
||||
|
||||
`apply_loaded_mission_control_node_delta(engine, handle, key, delta)` моделирует
|
||||
`0x10f30` с нулевым четвёртым аргументом: накопление анимационного изменения
|
||||
`+0x10` и detached-ветвь сюда не входят. Максимум читается из исходной NDP
|
||||
строки при подтверждённых единичных множителях Unit DAT. Сначала складываются
|
||||
`+0x10` не выполняется, а текущее значение поля сохраняется; detached-ветвь
|
||||
объекта тоже не моделируется. Максимум читается из исходной NDP строки при
|
||||
подтверждённых единичных множителях Unit DAT. Сначала складываются
|
||||
`f32` amount и delta, результат ограничивается `0..maximum`; при изменении
|
||||
native `+0x590` получает `max(0, total + (new - old))`, `+0x14` — долю
|
||||
`new / maximum`, а `+0x598` становится dirty. Флаг `+0x28 & 0x10` отмечает
|
||||
@@ -711,6 +726,38 @@ Native oracle отдельно подтверждает сохранение `+0
|
||||
native записывает биты `+0.0`, но сравнение количеств считает их равными:
|
||||
доля, флаги, итог и callback остаются прежними. Этот случай сохранён и в Rust.
|
||||
|
||||
#### Распределение provider-таблицы в Rust
|
||||
|
||||
`distribute_loaded_mission_control_provider_amount` переносит общий шаг
|
||||
`Control.dll+0x10ba0`. Режим proportional распределяет добавку по свободной
|
||||
ёмкости, а снятие — по текущему количеству строк; sequential заполняет их с
|
||||
первой и снимает ресурс с последней. Для sequential native сохраняет исходную
|
||||
величину запроса, даже когда сумма свободных мест округляется вниз.
|
||||
`revive_exhausted` включает строки с флагом истощения `+0x28 & 0x10` при добавке.
|
||||
Rust хранит максимум общей таблицы `+0x58c` отдельно от текущей суммы `+0x590`;
|
||||
для загруженных Unit DAT эти значения рассчитаны при подтверждённых единичных
|
||||
масштабах. Их разность может стать отрицательной из-за независимого округления
|
||||
изменений текущего агрегата; распределитель сохраняет такой signed capacity по
|
||||
формуле оригинала. Обновления масштабов `0x9ee0` и `0x9f90` в Rust не реализованы.
|
||||
|
||||
Native row updater вызывается здесь с четвёртым аргументом `1`, поэтому
|
||||
изменение `(new amount - old amount)` накапливается в provider `+0x10`.
|
||||
`apply_loaded_mission_control_node_delta` сохраняет прежний четвёртый аргумент
|
||||
`0` и оставляет этот накопитель нетронутым. После всех строк `0x10ba0`
|
||||
обязательно запускает `0x11130(root, update_effects=true)`. Rust переносит этот
|
||||
проход на копии таблицы: если ему нужны неизвестное состояние, depletion
|
||||
lifecycle, FX или host callback, весь шаг завершается ошибкой без частичных
|
||||
amount, accumulator, aggregate или stage изменений.
|
||||
|
||||
Поле `+0x618` изначально выключено; обработчик события `7` задаёт gate по
|
||||
правилу `gate = (arg == 2)` и вызывает пересчёт `0x2dc20`, только если gate
|
||||
изменился. Проекция сохраняет начальное состояние и уважает установленную
|
||||
suspension-метку. События Control пока не исполняются,
|
||||
поэтому это API остаётся явным CPU-шагом и не подключено к игровому clock.
|
||||
Тесты Rust повторяют пропорциональный вектор, накопитель `+0x10`, округление
|
||||
`2^24`, sequential capacity boundary, достижимый negative capacity и откат при
|
||||
unsupported FX/lifecycle.
|
||||
|
||||
#### Бюджет и обновление устройства tag 15
|
||||
|
||||
Для той же сборки `Control.dll` oracle исполняет оригинальный диспетчер
|
||||
|
||||
@@ -995,7 +995,13 @@ clock Control.
|
||||
добавку либо пропорционально свободному месту, либо по очереди: при максимумах
|
||||
`[30, 60, 50]`, текущих значениях `[10, 20, 0]` и добавке `22` первый способ
|
||||
даёт `[14, 28, 10]`. При вычитании последовательный способ начинает с последней
|
||||
строки. Так одна таблица описывает и текущее значение, и то, как оно меняется.
|
||||
строки. Пропорциональное вычитание делит запрос по текущим количествам.
|
||||
У каждой изменившейся строки есть отдельный накопитель изменений количества: он
|
||||
получает фактически применённую разницу, а не исходный запрос.
|
||||
Большие суммы округляются в двоичном формате. Поэтому после нескольких
|
||||
маленьких изменений общий запас иногда оказывается чуть больше первоначальной
|
||||
ёмкости; оригинал продолжает считать с этой отрицательной разностью, а не
|
||||
исправляет её отдельным правилом.
|
||||
|
||||
У строк есть и связи с родительскими узлами. Нативный `Control.dll` сначала
|
||||
обновляет запрошенную строку через `0x11220`, затем `0x11130` ищет её детей
|
||||
@@ -1040,8 +1046,10 @@ clock Control.
|
||||
Для точного повтора важен порядок округления. При исходных значениях `[2^24, 2]`
|
||||
и запросе `-1` оригинал сначала складывает общий итог с изменением, затем
|
||||
вычитает прежний итог. Из-за округления фактическое изменение получается `-2`.
|
||||
Нативная проверка подтвердила это поведение; общий распределитель
|
||||
`0x10ba0` в Rust ещё не перенесён, хотя прямая добавка к одной строке уже доступна.
|
||||
Rust переносит этот порядок, сохраняет native накопитель каждой строки и
|
||||
запускает обязательный проход стадий с effects. Если ему нужны ещё не
|
||||
реализованные FX или lifecycle-вызовы, таблица остаётся без изменений. Эти
|
||||
операции обновляют только CPU-снимок и пока не подключены к игровым часам Control.
|
||||
|
||||
Отдельный путь объекта класса `4` умножает интервал на контактный коэффициент и
|
||||
передаёт изменение строке. В проверенном примере коэффициент `1`, интервал `2`,
|
||||
@@ -1058,7 +1066,8 @@ Control заново разыгрывает срок обновления, да
|
||||
Принудительный шаг тоже расходует RNG; неактивный объект синхронизирует часы
|
||||
без такого расхода. Это локальные native проверки, а не работающий Rust
|
||||
Control: запросы Rust `0xe00` и `0x1002` читают текущее состояние строк,
|
||||
которое пока меняет только прямой API, а `0x1005` — загруженную ставку профиля.
|
||||
которое меняют явные API одной строки и распределения, а `0x1005` — загруженную
|
||||
ставку профиля.
|
||||
Точные нативные векторы и открытые границы описаны в [приложении](../appendices/script-vm.md#изменение-provider-состояния-control).
|
||||
|
||||
На x87 control word `0x007f` каждое умножение округляет мантиссу до 24 бит,
|
||||
|
||||
Reference in new issue
Block a user