From 888a331a6dfae8568ea4389e937cf09f2760d490 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Thu, 29 Oct 2026 16:05:38 +0400 Subject: [PATCH] test(runtime): verify native generator budget consumption --- ...behavior_distributor_tick_native_oracle.cs | 47 +++++++++++++++++++ docs/appendices/knowledge-boundaries.md | 5 +- docs/appendices/script-vm.md | 28 +++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs index 1371b1f..1383c1a 100644 --- a/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs +++ b/crates/fparkan-runtime/tests/behavior_distributor_tick_native_oracle.cs @@ -1781,6 +1781,53 @@ internal static class BehaviorDistributorTickOracle { Marshal.ReadInt32(generator, 0x18) == expectedAmounts[i], "native category priority updates tag 15 before the earlier stored generator"); } + NativeControlResourceDeviceConstructor constructGenerator = (NativeControlResourceDeviceConstructor) + Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, 0x22980), typeof(NativeControlResourceDeviceConstructor)); + IntPtr nativeGenerator = fixture.Allocate(0x94); + IntPtr nativeGeneratorRecord = fixture.Allocate(0xb4); + constructGenerator(nativeGenerator); + IntPtr nativeGeneratorVtable = Marshal.ReadIntPtr(nativeGenerator); + Expect(nativeGeneratorVtable == IntPtr.Add(module, 0x3c608) && + Marshal.ReadIntPtr(nativeGeneratorVtable, 0x14) == IntPtr.Add(module, 0x229a0) && + Marshal.ReadIntPtr(nativeGeneratorVtable, 0x18) == IntPtr.Add(module, 0x22a60), + "native tag-19 generator keeps its original rate and update methods"); + Marshal.WriteIntPtr(nativeGenerator, 0x3c, owner); + Marshal.WriteIntPtr(nativeGenerator, 0x44, IntPtr.Add(owner, 4)); + Marshal.WriteInt32(nativeGeneratorRecord, 0, 19); + Marshal.WriteInt32(nativeGeneratorRecord, 0x18, -1); // Preserve constructor's enabled word 0x20. + Marshal.WriteInt32(nativeGeneratorRecord, 0x20, FloatBits(0.5f)); + Marshal.WriteIntPtr(devices, 0, nativeGenerator); + for (int capped = 0; capped < 2; capped++) { + Marshal.WriteInt32(nativeGeneratorRecord, 0x2c, FloatBits(capped == 0 ? 1.0f : 0.125f)); + Expect(applyInto(owner, 0, nativeGeneratorRecord) == IntPtr.Add(nativeGeneratorRecord, 0xb4), + "native tag-19 common record initializer"); + Expect(Marshal.ReadInt32(nativeGenerator, 0x50) == 0x20, "tag-19 constructor enable word is preserved"); + Marshal.WriteInt32(nativeGenerator, 0x4c, FloatBits(1.0f)); + Marshal.WriteInt32(providerRow, 8, FloatBits(40.0f)); + Marshal.WriteInt32(providerRow, 0x0c, FloatBits(40.0f)); + Marshal.WriteInt32(providerRow, 0x14, FloatBits(0.4f)); + Marshal.WriteInt32(owner, 0x590, FloatBits(40.0f)); + NativeControlResourceDeviceRate generatorRate = (NativeControlResourceDeviceRate) + Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, 0x229a0), typeof(NativeControlResourceDeviceRate)); + float produced = generatorRate(nativeGenerator, 0.5f); + Expect(FloatBits(produced) == FloatBits(capped == 0 ? 0.1f : 0.05f), + "real generator rate applies provider ratio and optional coefficient cap"); + update(owner, 0.5f); + uint amount = unchecked((uint)Marshal.ReadInt32(providerRow, 8)); + uint fraction = unchecked((uint)Marshal.ReadInt32(providerRow, 0x14)); + uint remaining = unchecked((uint)Marshal.ReadInt32(nativeGenerator, 0x4c)); + Console.WriteLine("native-control-real-generator-budget: ctor=0x22980 rate=0x229a0 update=0x22a60" + + " cap=" + capped + " produced=" + Float(produced) + " amount=" + Hex(amount) + + " ratio=" + Hex(fraction) + " remaining=" + Hex(remaining)); + Expect(amount == (capped == 0 ? 0x42206666u : 0x42203333u) && + fraction == (capped == 0 ? 0x3ecd4fdfu : 0x3ecd0e56u) && + remaining == unchecked((uint)FloatBits(capped == 0 ? 0.75f : 0.0f)), + "real generator and consumer update exact provider and reserve values in one budget pass"); + GC.KeepAlive(generatorRate); + } + Marshal.WriteIntPtr(devices, 0, generator); + GC.KeepAlive(constructGenerator); + // Bounded active class-10 host: no motion object, no deferred packets. // These host getters are mocks; the clock, deadline and budget code are native. IntPtr host = fixture.Allocate(4); diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index 00b261c..d7e02e5 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -348,7 +348,10 @@ visibility transition `0x11920` с исходными AniMesh callbacks. Отд бюджетный вектор исполняет `0x2d340` → `0x2dca0` → tag 15 `0x22bb0` → `0x10ba0`: подтверждены коэффициенты `0.5` и `1`, точные amount/ratio и приоритет категории перед порядком устройств. Положительный вклад генератора -в этом векторе подставлен mock-ом; его собственная генерация не проверена. Эти сценарии +в этом векторе подставлен mock-ом. Отдельный вектор с исходным генератором +tag `19` (`0x22980`, `0x229a0`, `0x22a60`) проверяет полную и ограниченную +rate, изменение provider и собственного множителя генератора; отрицательные +параметры и нулевой знаменатель не проверены. Эти сценарии используют синтетические NDP/Node38-строки; они подтверждают локальные переходы и порядок вызовов, но не полную загрузку или выполнение Control на миссии. Вызовы `0x11220` используют `updateEffects = false`, что исключает ветвь Control FX и diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md index dc42959..173721b 100644 --- a/docs/appendices/script-vm.md +++ b/docs/appendices/script-vm.md @@ -686,6 +686,34 @@ provider getter и изменение количества не подменен категории и выполнение миссии этим не проверены. Rust пока не переносит этот изменяемый бюджетный цикл. +#### Генератор tag 19 в том же бюджетном проходе + +Отдельный вектор заменяет mock-генератор исходным устройством tag `19`: +конструктор `Control.dll` RVA `0x22980`, rate `0x229a0`, update `0x22a60`, +vtable `0x3c608`. Конструктор выставляет слово включения `0x20`; descriptor +с `+0x18 = -1` сохраняет его. Оба устройства проходят через исходные +`0x2d340` и `0x2dca0`, их provider и изменение количества тоже исходные. + +При положительных параметрах проверенного вектора выработка зависит от поля +descriptor `+0x20`, текущего множителя `+0x4c`, доли provider и `dt`. Если +коэффициент selector `0` положителен, rate ограничивается значением selector +`0x200`, умноженным на долю provider. Поэтому один и тот же источник может +вернуть полную выработку или её ограниченный запас. + +| Коэффициент | Поле `+0x20` | Исходная rate | Amount после прохода | Доля provider | Множитель генератора после прохода | +|---|---|---|---|---|---| +| `1` | `0.5` | `0.1` | `0x42206666` | `0x3ecd4fdf` | `0x3f400000` (`0.75`) | +| `0.125` | `0.5` | `0.05` | `0x42203333` | `0x3ecd0e56` | `0` | + +В обоих случаях исходные amount, максимум, доля и интервал равны `40`, `100`, +`0.4` и `0.5`, а исходный множитель генератора — `1`. Его update читает +provider уже после потребителя tag `15`, повторно вычисляет rate и уменьшает +собственный множитель. Это не запись коэффициента выделения в `+0x4c`, как у +tag `15`: смысл изменения этого поля зависит от фабричного класса. +Векторы проверяют конечные положительные значения при x87 CW `0x007f`; +нулевой знаменатель, отрицательные параметры и другие классы устройств ими +не покрыты. Перенос общего бюджетного цикла в Rust ещё не завершён. + #### Активация и начальные часы Для Control класса `4` oracle вызывает исходный конструктор RVA `0x31490`