test(runtime): verify native Control budget updates
Docs Deploy / Build and Deploy MkDocs (push) Successful in 36s

This commit is contained in:
Valentin Popov committed 2026-10-29 15:36:40 +04:00
1 parent e6408fe912
commit 35b9d86637
3 files changed
+178 -1

No files matched your search

@@ -54,6 +54,13 @@ internal static class BehaviorDistributorTickOracle {
private const int ControlResourceTag9ConstructorRva = 0x25460;
private const int ControlResourceTag15ConstructorRva = 0x22ae0;
private const int ControlResourceTag25ConstructorRva = 0x20800;
private const int ControlResourceTag15RateRva = 0x22b20;
private const int ControlResourceTag15UpdateRva = 0x22bb0;
private const int ControlResourceBudgetUpdateRva = 0x2d340;
private const int ControlResourceBudgetCategoryRva = 0x2c3a0;
private const int ControlResourceIntoRva = 0x2d890;
private const int ControlResourceAvailabilityRva = 0x10b10;
private const int ControlResourceTag15GateRva = 0x21820;
private const int ControlResourceTag9VtableRva = 0x3c938;
private const int ControlResourceTag15VtableRva = 0x3c648;
private const int ControlResourceTag25VtableRva = 0x3c448;
@@ -110,6 +117,10 @@ internal static class BehaviorDistributorTickOracle {
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeAniMeshNodeMetadataQuery(IntPtr owner, uint nodeIndex, uint selector, IntPtr result);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate uint NativeAniMeshNodeStageCount(IntPtr owner, uint nodeIndex);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlResourceDeviceConstructor(IntPtr self);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlResourceBudgetUpdate(IntPtr self, float deltaSeconds);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceDeviceRate(IntPtr self, float deltaSeconds);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate int NativeControlResourceDeviceGate(IntPtr self);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate float NativeControlResourceAvailability(IntPtr self, byte flags);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr NativeControlResourceComponentOverride(IntPtr self, int deviceIndex, IntPtr typedRecord);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateQuery(IntPtr self, uint selector, IntPtr result);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeControlAggregateSet(IntPtr self, uint selector, IntPtr value);
@@ -690,6 +701,7 @@ internal static class BehaviorDistributorTickOracle {
RunNativeControlContactRateUpdateCase(controlModule);
RunNativeControlTimedUpdateBaselineCases(controlModule);
RunNativeControlAggregateResourceCases(controlModule);
RunNativeControlResourceBudgetTag15Case(controlModule);
RunNativeBehaviorProfileMapCases(module);
RunNativeBehaviorVarSyntheticCase(module);
RunNativeBehaviorRealProfileCases(module);
@@ -1639,6 +1651,136 @@ internal static class BehaviorDistributorTickOracle {
return device;
}
private static void RunNativeControlResourceBudgetTag15Case(IntPtr module) {
NativeControlOwnerConstructor constructOwner = (NativeControlOwnerConstructor)Marshal.GetDelegateForFunctionPointer(
IntPtr.Add(module, ControlOwnerConstructorRva), typeof(NativeControlOwnerConstructor));
NativeControlProviderRowConstructor constructProviderRow = (NativeControlProviderRowConstructor)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlProviderRowConstructorRva),
typeof(NativeControlProviderRowConstructor));
NativeControlResourceDeviceConstructor constructTag15 = (NativeControlResourceDeviceConstructor)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceTag15ConstructorRva),
typeof(NativeControlResourceDeviceConstructor));
NativeControlResourceComponentOverride applyInto = (NativeControlResourceComponentOverride)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceIntoRva),
typeof(NativeControlResourceComponentOverride));
NativeControlResourceDeviceRate rate = (NativeControlResourceDeviceRate)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceTag15RateRva),
typeof(NativeControlResourceDeviceRate));
NativeControlResourceDeviceGate gate = (NativeControlResourceDeviceGate)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceTag15GateRva),
typeof(NativeControlResourceDeviceGate));
NativeControlResourceDeviceGet getter = (NativeControlResourceDeviceGet)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceDeviceGetterRva),
typeof(NativeControlResourceDeviceGet));
NativeControlResourceAvailability availability = (NativeControlResourceAvailability)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceAvailabilityRva),
typeof(NativeControlResourceAvailability));
NativeControlResourceBudgetUpdate update = (NativeControlResourceBudgetUpdate)
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(module, ControlResourceBudgetUpdateRva),
typeof(NativeControlResourceBudgetUpdate));
using (Fixture fixture = NewFixture()) {
IntPtr owner = fixture.Allocate(0x664);
Expect(constructOwner(owner, IntPtr.Zero, 4) == owner, "native budget owner constructor");
IntPtr providerRow = fixture.Allocate(0x2c);
IntPtr ndpDescriptor = fixture.Allocate(0x4c);
Marshal.WriteInt32(ndpDescriptor, 4, FloatBits(100.0f));
constructProviderRow(providerRow, ndpDescriptor);
Marshal.WriteInt32(providerRow, 4, -1); // No parent or stage lifecycle in this budget vector.
Marshal.WriteInt32(providerRow, 0x24, -1);
Marshal.WriteIntPtr(owner, 0x55c, providerRow);
Marshal.WriteInt32(owner, 0x560, 1);
Marshal.WriteInt32(owner, 0x65c, FloatBits(1.0f)); // Explicit maximum multiplier read by native selector 3.
Marshal.WriteInt32(owner, 0x58c, FloatBits(100.0f));
IntPtr ani = fixture.Allocate(4);
IntPtr aniVtable = fixture.Allocate(0x38);
Marshal.WriteIntPtr(ani, aniVtable);
Marshal.WriteIntPtr(aniVtable, 0x34, Marshal.GetFunctionPointerForDelegate(ControlNodeUpdateCallback));
Marshal.WriteIntPtr(owner, 0x20, ani);
IntPtr device = fixture.Allocate(0x98);
IntPtr typedRecord = fixture.Allocate(0xb4);
Expect(constructTag15(device) == device, "native tag-15 constructor");
IntPtr tag15Vtable = Marshal.ReadIntPtr(device);
Expect(tag15Vtable == IntPtr.Add(module, ControlResourceTag15VtableRva) &&
Marshal.ReadIntPtr(tag15Vtable, 0x14) == IntPtr.Add(module, ControlResourceTag15RateRva) &&
Marshal.ReadIntPtr(tag15Vtable, 0x18) == IntPtr.Add(module, ControlResourceTag15UpdateRva),
"budget fixture keeps original tag-15 rate and update methods");
Marshal.WriteIntPtr(device, 0x3c, owner);
Marshal.WriteIntPtr(device, 0x44, IntPtr.Add(owner, 4));
Marshal.WriteInt32(typedRecord, 0, 15);
Marshal.WriteInt32(typedRecord, 0x18, 1);
Marshal.WriteInt32(typedRecord, 0x2c, FloatBits(1.0f));
Marshal.WriteInt32(typedRecord, 0x30, FloatBits(1.0f));
// Mock only the generator: rate is read from +8; update records dt, scale,
// provider amount at callback time, and call count. All tag-15 code stays native.
IntPtr generator = fixture.Allocate(0x4c);
IntPtr generatorRecord = fixture.Allocate(4);
IntPtr generatorVtable = fixture.Allocate(0x1c);
Marshal.WriteInt32(generatorRecord, 14); // Native category 1, processed after category 0.
Marshal.WriteIntPtr(generator, generatorVtable);
Marshal.WriteIntPtr(generator, 0x48, generatorRecord);
byte[] rateCode = { 0xd9, 0x41, 0x08, 0xc2, 0x04, 0x00 }; // fld [ecx+8]; ret 4
IntPtr mockRate = IntPtr.Add(fastcallThunks, 0x280);
Marshal.Copy(rateCode, 0, mockRate, rateCode.Length);
byte[] updateCode = { 0x8b,0x44,0x24,0x04, 0x89,0x41,0x10,
0x8b,0x44,0x24,0x08, 0x89,0x41,0x14, 0xa1,0,0,0,0,
0x89,0x41,0x18, 0xff,0x41,0x1c, 0xc2,0x08,0x00 };
WriteImm32(updateCode, 15, IntPtr.Add(providerRow, 8).ToInt32());
IntPtr mockUpdate = IntPtr.Add(fastcallThunks, 0x2c0);
Marshal.Copy(updateCode, 0, mockUpdate, updateCode.Length);
Marshal.WriteIntPtr(generatorVtable, 0x14, mockRate);
Marshal.WriteIntPtr(generatorVtable, 0x18, mockUpdate);
IntPtr devices = fixture.Allocate(8);
Marshal.WriteIntPtr(devices, 0, generator); // Reverse storage order makes priority observable.
Marshal.WriteIntPtr(devices, 4, device);
Marshal.WriteIntPtr(owner, 0x5bc, devices);
Marshal.WriteInt32(owner, 0x5c0, 2);
Expect(applyInto(owner, 1, typedRecord) == IntPtr.Add(typedRecord, 0xb4) &&
Marshal.ReadIntPtr(device, 0x48) == typedRecord, "native tag-15 descriptor initializer");
Expect(Marshal.ReadIntPtr(Marshal.ReadIntPtr(owner, 0x0c), 0x24) ==
IntPtr.Add(module, ControlResourceBudgetCategoryRva), "native descriptor category dispatch");
float[] budgets = { 0.1f, 0.5f };
int[] expectedAmounts = { unchecked((int)0x42206666), unchecked((int)0x4220cccd) };
int[] expectedRatios = { unchecked((int)0x3ecd4fdf), unchecked((int)0x3ecdd2f2) };
for (int i = 0; i < budgets.Length; i++) {
Marshal.WriteInt32(generator, 8, FloatBits(budgets[i]));
Marshal.WriteInt32(generator, 0x1c, 0);
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));
const float dt = 0.5f;
Expect(gate(device) == 0 && FloatBits(getter(device, 0x100)) == FloatBits(0.4f) &&
FloatBits(availability(owner, 0)) == FloatBits(60.0f), "consistent native provider inputs");
Expect(FloatBits(rate(device, dt)) == FloatBits(-0.2f), "native tag-15 demand at non-unity dt");
controlNodeUpdateCount = 0;
update(owner, dt);
Console.WriteLine("native-control-resource-budget-tag15: controlHash=" + ExpectedControlSha256 +
" budgetRva=0x2d340 updateRva=0x22bb0 dt=" + Float(dt) + " budget=" + Float(budgets[i]) +
" scale=" + Float(ReadFloat(device, 0x4c)) + " amount=" + Hex(unchecked((uint)Marshal.ReadInt32(providerRow, 8))) +
" ratio=" + Hex(unchecked((uint)Marshal.ReadInt32(providerRow, 0x14))) +
" total=" + Hex(unchecked((uint)Marshal.ReadInt32(owner, 0x590))) +
" processed=" + Hex(unchecked((uint)Marshal.ReadInt32(providerRow, 0x0c))));
Expect(Marshal.ReadInt32(providerRow, 8) == expectedAmounts[i] &&
Marshal.ReadInt32(providerRow, 0x14) == expectedRatios[i] &&
Marshal.ReadInt32(owner, 0x590) == expectedAmounts[i] &&
Marshal.ReadInt32(providerRow, 0x0c) == expectedAmounts[i], "native budget exact provider result");
Expect(controlNodeUpdateCount == 1 && controlNodeUpdateIndex == 0 &&
controlNodeUpdateSelector == 0x200 && controlNodeUpdateValueBits == unchecked((uint)expectedRatios[i]),
"native budget publishes updated provider ratio exactly once");
Expect(FloatBits(ReadFloat(device, 0x4c)) == FloatBits(i == 0 ? 0.5f : 1.0f), "native budget scale");
Expect(Marshal.ReadInt32(generator, 0x1c) == 1 &&
Marshal.ReadInt32(generator, 0x10) == FloatBits(dt) &&
Marshal.ReadInt32(generator, 0x14) == FloatBits((budgets[i] - (i == 0 ? 0.1f : 0.2f) - budgets[i]) / -budgets[i]) &&
Marshal.ReadInt32(generator, 0x18) == expectedAmounts[i],
"native category priority updates tag 15 before the earlier stored generator");
}
}
}
private static void RunNativeArealMapComponentBuilderCase(IntPtr module) {
using (ArealMapFixture fixture = new ArealMapFixture()) {
fixture.OwnerVtable = fixture.Allocate(0x40);