test(runtime): verify native Control activation clocks
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s
This commit is contained in:
3 files changed
+122
-5
No files matched your search
@@ -127,6 +127,8 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderAmountUpdate(IntPtr self, int nodeIndex, float delta, uint animationFlag);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderAmountUpdate(IntPtr self, int nodeIndex, float delta, uint animationFlag);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderAmountDistribute(IntPtr self, float delta, uint mode, uint flags);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderAmountDistribute(IntPtr self, float delta, uint mode, uint flags);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlContactRateUpdate(IntPtr self, float deltaSeconds);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlContactRateUpdate(IntPtr self, float deltaSeconds);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeControlEvent(IntPtr self, uint eventCode, uint payload);
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeControlScaleQuery(IntPtr self, IntPtr result);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlTimedUpdate(IntPtr self, float inputTime);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlTimedUpdate(IntPtr self, float inputTime);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderRowConstructor(IntPtr self, IntPtr sourceNdpRow);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderRowConstructor(IntPtr self, IntPtr sourceNdpRow);
|
||||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlOwnerConstructor(IntPtr self, IntPtr host, uint classCode);
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlOwnerConstructor(IntPtr self, IntPtr host, uint classCode);
|
||||||
@@ -702,6 +704,7 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
RunNativeControlTimedUpdateBaselineCases(controlModule);
|
RunNativeControlTimedUpdateBaselineCases(controlModule);
|
||||||
RunNativeControlAggregateResourceCases(controlModule);
|
RunNativeControlAggregateResourceCases(controlModule);
|
||||||
RunNativeControlResourceBudgetTag15Case(controlModule);
|
RunNativeControlResourceBudgetTag15Case(controlModule);
|
||||||
|
RunNativeControlActivationCase(controlModule);
|
||||||
RunNativeBehaviorProfileMapCases(module);
|
RunNativeBehaviorProfileMapCases(module);
|
||||||
RunNativeBehaviorVarSyntheticCase(module);
|
RunNativeBehaviorVarSyntheticCase(module);
|
||||||
RunNativeBehaviorRealProfileCases(module);
|
RunNativeBehaviorRealProfileCases(module);
|
||||||
@@ -1816,6 +1819,7 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
IntPtr randomState = IntPtr.Add(module, 0x43228);
|
IntPtr randomState = IntPtr.Add(module, 0x43228);
|
||||||
int savedPeriod = Marshal.ReadInt32(period);
|
int savedPeriod = Marshal.ReadInt32(period);
|
||||||
int savedRandom = Marshal.ReadInt32(randomState);
|
int savedRandom = Marshal.ReadInt32(randomState);
|
||||||
|
Expect(savedPeriod == FloatBits(250.0f), "native Control settings constructor default resource period");
|
||||||
try {
|
try {
|
||||||
Marshal.WriteInt32(period, FloatBits(500.0f));
|
Marshal.WriteInt32(period, FloatBits(500.0f));
|
||||||
Marshal.WriteInt32(randomState, 0); // Fixed point: random offset is -period/8.
|
Marshal.WriteInt32(randomState, 0); // Fixed point: random offset is -period/8.
|
||||||
@@ -1849,6 +1853,81 @@ internal static class BehaviorDistributorTickOracle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void RunNativeControlActivationCase(IntPtr module) {
|
||||||
|
NativeControlOwnerConstructor construct = (NativeControlOwnerConstructor)Marshal.GetDelegateForFunctionPointer(
|
||||||
|
IntPtr.Add(module, 0x31490), typeof(NativeControlOwnerConstructor));
|
||||||
|
using (Fixture fixture = NewFixture()) {
|
||||||
|
IntPtr owner = fixture.Allocate(0x670);
|
||||||
|
Expect(construct(owner, IntPtr.Zero, 4) == owner, "native derived Control constructor");
|
||||||
|
IntPtr host = fixture.Allocate(4);
|
||||||
|
IntPtr hostVtable = fixture.Allocate(0x24);
|
||||||
|
IntPtr ani = fixture.Allocate(4);
|
||||||
|
IntPtr aniVtable = fixture.Allocate(0x84);
|
||||||
|
IntPtr scale = fixture.Allocate(4);
|
||||||
|
IntPtr scaleVtable = fixture.Allocate(0x3c);
|
||||||
|
IntPtr matrix = fixture.Allocate(64);
|
||||||
|
for (int i = 0; i < 16; i++) Marshal.WriteInt32(matrix, i * 4, FloatBits(i % 5 == 0 ? 1.0f : 0.0f));
|
||||||
|
Marshal.WriteIntPtr(host, hostVtable);
|
||||||
|
Marshal.WriteIntPtr(ani, aniVtable);
|
||||||
|
Marshal.WriteIntPtr(scale, scaleVtable);
|
||||||
|
List<string> trace = new List<string>();
|
||||||
|
bool wrongArguments = false;
|
||||||
|
InterfaceGetProperty transform = delegate(IntPtr self, uint selector, IntPtr result) {
|
||||||
|
wrongArguments |= self != host || selector != 2;
|
||||||
|
trace.Add("transform"); Marshal.WriteIntPtr(result, matrix); return 1;
|
||||||
|
};
|
||||||
|
NativeControlScaleQuery queryScale = delegate(IntPtr self, IntPtr result) {
|
||||||
|
wrongArguments |= self != scale; trace.Add("scale");
|
||||||
|
for (int i = 0; i < 3; i++) Marshal.WriteInt32(result, i * 4, FloatBits(1.0f));
|
||||||
|
};
|
||||||
|
// Explicit zero-geometry host mocks: capture parameters and supply outputs.
|
||||||
|
InterfaceGetProperty geometryState = delegate(IntPtr self, uint first, IntPtr second) {
|
||||||
|
wrongArguments |= self != ani; trace.Add("geometry-state");
|
||||||
|
Marshal.WriteInt32(new IntPtr(unchecked((int)first)), 0);
|
||||||
|
Marshal.WriteInt32(second, 0); return 1;
|
||||||
|
};
|
||||||
|
InterfaceGetProperty geometrySet = delegate(IntPtr self, uint node, IntPtr value) {
|
||||||
|
wrongArguments |= self != ani || node != 0;
|
||||||
|
trace.Add("geometry-set:" + Hex(unchecked((uint)value.ToInt32()))); return 1;
|
||||||
|
};
|
||||||
|
InterfaceGetProperty geometryPoint = delegate(IntPtr self, uint node, IntPtr result) {
|
||||||
|
wrongArguments |= self != ani || node != 0; trace.Add("geometry-point");
|
||||||
|
for (int i = 0; i < 3; i++) Marshal.WriteInt32(result, i * 4, 0); return 1;
|
||||||
|
};
|
||||||
|
Marshal.WriteIntPtr(hostVtable, 0x20, Marshal.GetFunctionPointerForDelegate(transform));
|
||||||
|
Marshal.WriteIntPtr(scaleVtable, 0x38, Marshal.GetFunctionPointerForDelegate(queryScale));
|
||||||
|
Marshal.WriteIntPtr(aniVtable, 0x68, Marshal.GetFunctionPointerForDelegate(geometryState));
|
||||||
|
Marshal.WriteIntPtr(aniVtable, 0x64, Marshal.GetFunctionPointerForDelegate(geometrySet));
|
||||||
|
Marshal.WriteIntPtr(aniVtable, 0x80, Marshal.GetFunctionPointerForDelegate(geometryPoint));
|
||||||
|
Marshal.WriteIntPtr(owner, 0x1c, host);
|
||||||
|
Marshal.WriteIntPtr(owner, 0x20, ani);
|
||||||
|
Marshal.WriteIntPtr(owner, 0x24, scale);
|
||||||
|
IntPtr eventVtable = Marshal.ReadIntPtr(owner, 4);
|
||||||
|
Expect(Marshal.ReadIntPtr(eventVtable, 8) == IntPtr.Add(module, 0x31ae0), "native activation event interface");
|
||||||
|
NativeControlEvent activate = (NativeControlEvent)Marshal.GetDelegateForFunctionPointer(
|
||||||
|
WriteStdCallToFastcallThunk(0x380, Marshal.ReadIntPtr(eventVtable, 8)), typeof(NativeControlEvent));
|
||||||
|
foreach (uint clock in new uint[] { 1000, 33554433 }) {
|
||||||
|
trace.Clear();
|
||||||
|
activate(IntPtr.Add(owner, 4), 4, clock);
|
||||||
|
float absolute = (float)clock;
|
||||||
|
float baseline = absolute - 100.0f;
|
||||||
|
Expect(Marshal.ReadByte(owner, 0xe0) == 1 &&
|
||||||
|
ReadFloat(owner, 0xe4) == baseline && ReadFloat(owner, 0xe8) == baseline &&
|
||||||
|
ReadFloat(owner, 0xec) == baseline && ReadFloat(owner, 0xd8) == absolute && ReadFloat(owner, 0xdc) == absolute,
|
||||||
|
"native event 4 activates the object and initializes absolute clock baselines");
|
||||||
|
Expect(!wrongArguments && String.Join(",", trace.ToArray()) ==
|
||||||
|
"transform,scale,geometry-state,geometry-set:0x00000000,geometry-point,geometry-state,geometry-set:0x3f800000",
|
||||||
|
"native activation zero-geometry host callback order and arguments");
|
||||||
|
Expect(Marshal.ReadInt32(owner, 0x370) == 1, "only first activation queues the initial command");
|
||||||
|
Console.WriteLine("native-control-activation: eventRva=0x31ae0 handlerRva=0x314f0 class=4 clock=" + clock +
|
||||||
|
" absolute=" + Float(absolute) + " e4/e8/ec=" + Float(baseline) + " e0=1 pending=1 callbacks=" + String.Join(",", trace.ToArray()));
|
||||||
|
}
|
||||||
|
GC.KeepAlive(transform); GC.KeepAlive(queryScale); GC.KeepAlive(geometryState);
|
||||||
|
GC.KeepAlive(geometrySet); GC.KeepAlive(geometryPoint); GC.KeepAlive(activate);
|
||||||
|
}
|
||||||
|
GC.KeepAlive(construct);
|
||||||
|
}
|
||||||
|
|
||||||
private static void RunNativeArealMapComponentBuilderCase(IntPtr module) {
|
private static void RunNativeArealMapComponentBuilderCase(IntPtr module) {
|
||||||
using (ArealMapFixture fixture = new ArealMapFixture()) {
|
using (ArealMapFixture fixture = new ArealMapFixture()) {
|
||||||
fixture.OwnerVtable = fixture.Allocate(0x40);
|
fixture.OwnerVtable = fixture.Allocate(0x40);
|
||||||
|
|||||||
@@ -360,7 +360,11 @@ Control выставлен flag `0x102`, слово флагов ANI `+0x14` и
|
|||||||
часть активного `0xbcf0` исполнена на синтетическом хосте класса `10`:
|
часть активного `0xbcf0` исполнена на синтетическом хосте класса `10`:
|
||||||
при периоде `500` и нулевом RNG проверены пропуск до deadline и на равенстве
|
при периоде `500` и нулевом RNG проверены пропуск до deadline и на равенстве
|
||||||
`437.5`, затем ресурсный `dt = 0.5` при времени `500`. Движение и очереди
|
`437.5`, затем ресурсный `dt = 0.5` при времени `500`. Движение и очереди
|
||||||
исключены условиями вектора; активация `+0xe0` задана явно. Источник времени
|
исключены условиями вектора; активация `+0xe0` задана явно. Отдельный вектор
|
||||||
|
класса `4` исполняет событие `4` через `0x31ae0` → `0x314f0`, проверяя
|
||||||
|
активность, начальные часы `time - 100`, округление DWORD в `f32` и одну
|
||||||
|
начальную команду. Геометрические callbacks подставлены явно, таблицы
|
||||||
|
устройств и shapes пусты; полный загрузочный путь не проверен. Источник времени
|
||||||
и доставка прослежены статически:
|
и доставка прослежены статически:
|
||||||
World3D RVA `0x6bf0` вычисляет часы из `timeGetTime()` с игровым смещением и
|
World3D RVA `0x6bf0` вычисляет часы из `timeGetTime()` с игровым смещением и
|
||||||
поправкой на паузу, затем выполняет первый обход `6/1`, шаг мировой физики и
|
поправкой на паузу, затем выполняет первый обход `6/1`, шаг мировой физики и
|
||||||
@@ -368,7 +372,8 @@ World3D RVA `0x6bf0` вычисляет часы из `timeGetTime()` с игр
|
|||||||
обработчик Control RVA `0x7830` численно преобразует unsigned DWORD в `f32`
|
обработчик Control RVA `0x7830` численно преобразует unsigned DWORD в `f32`
|
||||||
перед primary slot `+0x58`. Фабрика выбирает RVA `0xcbb0` для класса `9` и
|
перед primary slot `+0x58`. Фабрика выбирает RVA `0xcbb0` для класса `9` и
|
||||||
RVA `0xbcf0` для остальных. Это подтверждает статическую цепочку вызовов, но не
|
RVA `0xbcf0` для остальных. Это подтверждает статическую цепочку вызовов, но не
|
||||||
её исполнение на миссии: загрузочная активация, остальные ветви `0xbcf0` и
|
её исполнение на миссии: связывание интерфейсов, активация устройств и shapes,
|
||||||
|
остальные ветви `0xbcf0` и
|
||||||
изменение provider-снимков Rust остаются открытыми. Источник контактного коэффициента прослежен статически,
|
изменение provider-снимков Rust остаются открытыми. Источник контактного коэффициента прослежен статически,
|
||||||
но не исполнен. Подробные векторы и статические границы приведены в
|
но не исполнен. Подробные векторы и статические границы приведены в
|
||||||
[техническом подразделе о provider-состоянии Control](script-vm.md#изменение-provider-состояния-control).
|
[техническом подразделе о provider-состоянии Control](script-vm.md#изменение-provider-состояния-control).
|
||||||
|
|||||||
@@ -573,7 +573,9 @@ Tick-векторы проверяют нулевой шаг, обе ветви
|
|||||||
равенство спросу, разные веса и достаточную/недостаточную мощность. Тот же
|
равенство спросу, разные веса и достаточную/недостаточную мощность. Тот же
|
||||||
harness проверяет в оригинальном Control пропорциональное и последовательное
|
harness проверяет в оригинальном Control пропорциональное и последовательное
|
||||||
распределение provider amount, коэффициент класса `4` с интервалом `2`, а также
|
распределение provider amount, коэффициент класса `4` с интервалом `2`, а также
|
||||||
неактивную временную ветвь с входами `0.5`, `1` и `2`. Вызовы Control используют
|
неактивную временную ветвь с входами `0.5`, `1` и `2`. Отдельные векторы
|
||||||
|
проверяют активацию класса `4` и бюджетную часть активной ветви класса `10`.
|
||||||
|
Вызовы Control используют
|
||||||
синтетические строки и хостовые callbacks; ArealMap тоже получает синтетические
|
синтетические строки и хостовые callbacks; ArealMap тоже получает синтетические
|
||||||
объекты. Harness сверяет SHA-256 всех трёх DLL; ArealMap загружается из
|
объекты. Harness сверяет SHA-256 всех трёх DLL; ArealMap загружается из
|
||||||
изолированной временной рабочей папки, которую процесс затем удаляет. Эти тесты
|
изолированной временной рабочей папки, которую процесс затем удаляет. Эти тесты
|
||||||
@@ -676,6 +678,36 @@ provider getter и изменение количества не подменен
|
|||||||
категории и выполнение миссии этим не проверены.
|
категории и выполнение миссии этим не проверены.
|
||||||
Rust пока не переносит этот изменяемый бюджетный цикл.
|
Rust пока не переносит этот изменяемый бюджетный цикл.
|
||||||
|
|
||||||
|
#### Активация и начальные часы
|
||||||
|
|
||||||
|
Для Control класса `4` oracle вызывает исходный конструктор RVA `0x31490`
|
||||||
|
и обработчик интерфейса RVA `0x31ae0` → `0x314f0` с событием `4` и ненулевым
|
||||||
|
DWORD времени. Он выставляет `+0xe0` по условию `class != 0`, копирует матрицу
|
||||||
|
хоста в три transform-поля и численно преобразует беззнаковое время в `f32`.
|
||||||
|
Поля `+0xe4`, `+0xe8` и `+0xec` получают `time - 100`, а командные часы
|
||||||
|
`+0xd8` и `+0xdc` — `time`. Это разные базовые значения:
|
||||||
|
|
||||||
|
| Вход DWORD | Абсолютное `f32` | `+0xe4`, `+0xe8`, `+0xec` |
|
||||||
|
|---|---|---|
|
||||||
|
| `1000` | `1000` | `900` |
|
||||||
|
| `33554433` | `33554432` | `33554332` |
|
||||||
|
|
||||||
|
Первый вызов при старом `+0xe4 == 0` добавляет начальную команду в очередь;
|
||||||
|
повторный вызов в этом векторе сохраняет одну запись. Хост явно подставляет
|
||||||
|
identity transform, единичный масштаб и нулевую геометрию. Oracle проверяет
|
||||||
|
аргументы и порядок этих callbacks, а функции Control и изменения его часов
|
||||||
|
остаются исходными. Таблицы устройств, shapes и дорожек пусты: их активация,
|
||||||
|
полный object loader и ветвь события `4` с нулевым payload здесь не исполнены.
|
||||||
|
Нулевой payload статически выбирает отдельную фазу связывания интерфейсов,
|
||||||
|
поэтому его нельзя считать обычным ненулевым временем активации.
|
||||||
|
|
||||||
|
Период ресурсного обновления — поле `+8` глобального объекта настроек
|
||||||
|
RVA `0x42478`. Инициализатор RVA `0x6350` и reset-метод RVA `0x6420` задают
|
||||||
|
ему `250` миллисекунд (`0x437a0000`); oracle проверяет значение после загрузки
|
||||||
|
DLL. Экспорт `InitializeSettings` (RVA `0x32260`) регистрирует объект с ключом
|
||||||
|
`0x15` в `World3D::CreateGameSettings`. Это default, а не доказательство
|
||||||
|
неизменности настройки во всех игровых режимах.
|
||||||
|
|
||||||
#### Активный ресурсный clock
|
#### Активный ресурсный clock
|
||||||
|
|
||||||
Тот же native budget-вектор проходит через `Control.dll` RVA `0xbcf0` при
|
Тот же native budget-вектор проходит через `Control.dll` RVA `0xbcf0` при
|
||||||
@@ -703,8 +735,9 @@ Rust пока не переносит этот изменяемый бюджет
|
|||||||
до `0x42206666`, долю до `0x3ecd4fdf`; mock-генератор получает `dt = 0.5`
|
до `0x42206666`, долю до `0x3ecd4fdf`; mock-генератор получает `dt = 0.5`
|
||||||
ровно один раз. После вызова dirty равен `1`, а агрегат `+0x588` ещё не
|
ровно один раз. После вызова dirty равен `1`, а агрегат `+0x588` ещё не
|
||||||
пересчитан: бюджетное изменение происходит после предшествующего `0x12a40`.
|
пересчитан: бюджетное изменение происходит после предшествующего `0x12a40`.
|
||||||
Активация через загрузочные события, ненулевые состояния RNG, принудительный
|
Событие активации проверено отдельно выше. Ненулевые состояния RNG,
|
||||||
цикл по `+0xd4`, движение и непустые очереди остаются отдельными границами.
|
принудительный цикл по `+0xd4`, движение и непустые очереди остаются отдельными
|
||||||
|
границами.
|
||||||
|
|
||||||
#### Доставка игровых часов в Control
|
#### Доставка игровых часов в Control
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user