feat(runtime): project load-time Control profile values
Docs Deploy / Build and Deploy MkDocs (push) Successful in 41s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 41s
This commit is contained in:
5 files changed
+1435
-17
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -81,6 +81,13 @@ internal static class BehaviorDistributorTickOracle {
|
||||
private const int ControlResourceNoOpStringPairRva = 0x0bcd0;
|
||||
private const int ControlResourceTag19VtableRva = 0x3c608;
|
||||
private const int ControlResourceTag19StringPairNoOpRva = 0x0bcd0;
|
||||
private const int ControlProfileFac0Rva = 0x0fac0;
|
||||
private const int ControlProfileOwnerConstructorRva = 0x31490;
|
||||
private const int ControlProfileDeviceVtableRva = 0x3d1b4;
|
||||
private const int ControlProfileDeviceCountRva = 0x08830;
|
||||
private const int ControlProfileDeviceQueryRva = 0x2bb40;
|
||||
private const int AniMeshComponentFirstRva = 0x05780;
|
||||
private const int AniMeshComponentCountRva = 0x057c0;
|
||||
private const int BehaviorVarTableVtableRva = 0x591b0;
|
||||
private const int BehaviorVarArrayLoadRva = 0x49fa0;
|
||||
private const int BehaviorVarFieldBindRva = 0x49090;
|
||||
@@ -129,6 +136,7 @@ internal static class BehaviorDistributorTickOracle {
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeFastcallComponentBuilder(IntPtr owner, IntPtr node, uint parentId);
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeArealMapEvent(IntPtr owner, uint message, uint subcode, IntPtr descriptor);
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate int NativeAniMeshNodeMetadataQuery(IntPtr owner, uint nodeIndex, uint selector, IntPtr result);
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate uint NativeAniMeshRangeLookup(IntPtr owner, uint componentId);
|
||||
[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);
|
||||
@@ -151,6 +159,7 @@ internal static class BehaviorDistributorTickOracle {
|
||||
[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 IntPtr NativeControlOwnerConstructor(IntPtr self, IntPtr host, uint classCode);
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeControlProfileFac0(IntPtr owner);
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderInitialState(IntPtr self);
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate void NativeControlNodeUpdateCallback(IntPtr self, uint nodeIndex, uint selector, uint valueBits);
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void NativeControlProviderStageUpdate(IntPtr self, int nodeIndex, uint updateEffects);
|
||||
@@ -691,7 +700,7 @@ internal static class BehaviorDistributorTickOracle {
|
||||
Marshal.WriteIntPtr(import, Marshal.GetFunctionPointerForDelegate(AreaMapGetterCallback));
|
||||
Marshal.WriteIntPtr(queueImport, Marshal.GetFunctionPointerForDelegate(QueueGetterCallback));
|
||||
RunSmokeCases(module, controlModule, arealMapModule, aniMeshModule);
|
||||
Console.WriteLine("original native Behavior/ArealMap/Control oracle passed (Behavior Add 0x1aac0, normalize 0x1ac90, scheduler 0x1adb0, tick 0x19e80; ArealMap UnitDAT projection 0x14b90; Control INTO override 0x2d890, provider distribution 0x10ba0, class-4 update 0x12a40 and bounded class-10 active clock 0xbcf0; synthetic host callbacks, GetSystemArealMap/GetQueue IAT slots, and bounded vector insertion are substituted; Control provider selector vtable is original; no full Control update or Rust parity is claimed).");
|
||||
Console.WriteLine("original native Behavior/ArealMap/Control oracle passed (Behavior Add 0x1aac0, normalize 0x1ac90, scheduler 0x1adb0, tick 0x19e80; ArealMap UnitDAT projection 0x14b90; Control INTO override 0x2d890, FAC0 load profile 0xfac0, provider distribution 0x10ba0, class-4 update 0x12a40 and bounded class-10 active clock 0xbcf0; FAC0 uses synthetic backing with original AniMesh and Control query methods; other host callbacks, GetSystemArealMap/GetQueue IAT slots, and bounded vector insertion are substituted; no full UnitDAT loading or gameplay parity is claimed).");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception error) {
|
||||
@@ -734,6 +743,7 @@ internal static class BehaviorDistributorTickOracle {
|
||||
RunNativeArealMapComponentBuilderCase(arealMapModule);
|
||||
RunNativeAniMeshScaleLoadSnapshot(aniMeshModule);
|
||||
RunNativeAniMeshNodeMetadataCases(aniMeshModule);
|
||||
RunNativeControlFac0LoadCases(controlModule, aniMeshModule);
|
||||
RunNativeControlProviderInitialStateCase(controlModule, aniMeshModule);
|
||||
RunNativeControlIntoOverrideCase(controlModule);
|
||||
RunNativeControlResourceDeviceCase(controlModule);
|
||||
@@ -2765,6 +2775,268 @@ internal static class BehaviorDistributorTickOracle {
|
||||
GC.KeepAlive(stageCount);
|
||||
}
|
||||
|
||||
private static void RunNativeControlFac0LoadCases(IntPtr controlModule, IntPtr aniMeshModule) {
|
||||
NativeControlOwnerConstructor constructOwner = (NativeControlOwnerConstructor)
|
||||
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(controlModule, ControlProfileOwnerConstructorRva),
|
||||
typeof(NativeControlOwnerConstructor));
|
||||
NativeControlProfileFac0 fac0 = (NativeControlProfileFac0)Marshal.GetDelegateForFunctionPointer(
|
||||
WriteStdCallToFastcallOneArgThunk(0x440, IntPtr.Add(controlModule, ControlProfileFac0Rva)),
|
||||
typeof(NativeControlProfileFac0));
|
||||
NativeAniMeshNodeMetadataQuery queryNode = (NativeAniMeshNodeMetadataQuery)
|
||||
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(aniMeshModule, AniMeshNodeMetadataQueryRva),
|
||||
typeof(NativeAniMeshNodeMetadataQuery));
|
||||
NativeAniMeshRangeLookup findFirst = (NativeAniMeshRangeLookup)
|
||||
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(aniMeshModule, AniMeshComponentFirstRva),
|
||||
typeof(NativeAniMeshRangeLookup));
|
||||
NativeAniMeshRangeLookup countMatches = (NativeAniMeshRangeLookup)
|
||||
Marshal.GetDelegateForFunctionPointer(IntPtr.Add(aniMeshModule, AniMeshComponentCountRva),
|
||||
typeof(NativeAniMeshRangeLookup));
|
||||
|
||||
RunFac0CombinedCase(controlModule, aniMeshModule, constructOwner, fac0, queryNode, findFirst, countMatches);
|
||||
uint noCache = RunFac0CacheCase(controlModule, aniMeshModule, constructOwner, fac0, false, 0.0f);
|
||||
uint zeroCache = RunFac0CacheCase(controlModule, aniMeshModule, constructOwner, fac0, true, 0.0f);
|
||||
uint nonzeroCache = RunFac0CacheCase(controlModule, aniMeshModule, constructOwner, fac0, true, 2.0f);
|
||||
Expect(noCache == zeroCache && noCache != nonzeroCache,
|
||||
"FAC0 distinguishes a missing cache from its value while a present-zero cache contributes zero");
|
||||
RunFac0NegativeClamp(controlModule, aniMeshModule, constructOwner, fac0, findFirst, countMatches);
|
||||
RunFac0NullAndSignedZeroCases(controlModule, aniMeshModule, constructOwner, fac0);
|
||||
RunFac0OrderedCase(controlModule, aniMeshModule, constructOwner, fac0);
|
||||
Expect(CurrentX87ControlWord() == 0x007f, "FAC0 vectors use x87 control word 0x007f");
|
||||
Console.WriteLine("native-control-fac0-load: controlHash=" + ExpectedControlSha256 +
|
||||
" aniMeshHash=" + ExpectedAniMeshSha256 + " rva=0x0fac0 cw=0x" +
|
||||
CurrentX87ControlWord().ToString("x4") + " original owner/node/device methods");
|
||||
GC.KeepAlive(fac0);
|
||||
GC.KeepAlive(constructOwner);
|
||||
GC.KeepAlive(queryNode);
|
||||
GC.KeepAlive(findFirst);
|
||||
GC.KeepAlive(countMatches);
|
||||
}
|
||||
|
||||
private static IntPtr CreateFac0AniMesh(Fixture fixture, IntPtr aniMeshModule, uint[] componentIds,
|
||||
float scale158, float scale15c, float scale160, float slot30, float slot34) {
|
||||
IntPtr ani = fixture.Allocate(0x1b0);
|
||||
IntPtr vtable = fixture.Allocate(0x50);
|
||||
IntPtr nodes = fixture.Allocate(Math.Max(1, componentIds.Length) * 0x130);
|
||||
IntPtr type1NodeData = fixture.Allocate(0x26);
|
||||
IntPtr slotData = fixture.Allocate(0x44);
|
||||
IntPtr modelContext = fixture.Allocate(3 * IntPtr.Size);
|
||||
Marshal.WriteInt16(type1NodeData, 0x08, 0);
|
||||
Marshal.WriteIntPtr(modelContext, 0, type1NodeData);
|
||||
Marshal.WriteIntPtr(modelContext, 2 * IntPtr.Size, slotData);
|
||||
Marshal.WriteInt32(slotData, 0x30, FloatBits(slot30));
|
||||
Marshal.WriteInt32(slotData, 0x34, FloatBits(slot34));
|
||||
Marshal.WriteIntPtr(ani, 0, vtable);
|
||||
Marshal.WriteIntPtr(ani, 0x1a8, nodes);
|
||||
Marshal.WriteInt32(ani, 0x1ac, componentIds.Length);
|
||||
Marshal.WriteInt32(ani, 0x158, FloatBits(scale158));
|
||||
Marshal.WriteInt32(ani, 0x15c, FloatBits(scale15c));
|
||||
Marshal.WriteInt32(ani, 0x160, FloatBits(scale160));
|
||||
for (int i = 0; i < componentIds.Length; i++) {
|
||||
IntPtr node = IntPtr.Add(nodes, i * 0x130);
|
||||
Marshal.WriteInt32(node, 0, unchecked((int)componentIds[i]));
|
||||
Marshal.WriteInt32(node, 0x08, 0);
|
||||
Marshal.WriteInt32(node, 0x1c, 0);
|
||||
Marshal.WriteIntPtr(node, 0x12c, modelContext);
|
||||
}
|
||||
Marshal.WriteIntPtr(vtable, 0x0c, IntPtr.Add(aniMeshModule, AniMeshNodeMetadataQueryRva));
|
||||
Marshal.WriteIntPtr(vtable, 0x38, IntPtr.Add(aniMeshModule, AniMeshComponentFirstRva));
|
||||
Marshal.WriteIntPtr(vtable, 0x3c, IntPtr.Add(aniMeshModule, AniMeshComponentCountRva));
|
||||
return ani;
|
||||
}
|
||||
|
||||
private static IntPtr CreateFac0Owner(Fixture fixture, IntPtr controlModule,
|
||||
NativeControlOwnerConstructor constructOwner, IntPtr ani, uint originalWord26,
|
||||
float[] descriptorMobility, float[] mutableAmounts, bool hasCache, float cacheC,
|
||||
uint[] deviceKeys, float[] deviceValues) {
|
||||
Expect(descriptorMobility.Length == mutableAmounts.Length, "FAC0 provider fixture lengths");
|
||||
Expect(deviceKeys.Length == deviceValues.Length, "FAC0 device fixture lengths");
|
||||
IntPtr owner = fixture.Allocate(0x700);
|
||||
Expect(constructOwner(owner, IntPtr.Zero, 4) == owner, "original Control owner constructor 0x31490");
|
||||
IntPtr deviceInterface = IntPtr.Add(owner, 0x0c);
|
||||
IntPtr deviceVtable = Marshal.ReadIntPtr(deviceInterface);
|
||||
Expect(deviceVtable == IntPtr.Add(controlModule, ControlProfileDeviceVtableRva) &&
|
||||
Marshal.ReadIntPtr(deviceVtable, 0x0c) == IntPtr.Add(controlModule, ControlProfileDeviceCountRva) &&
|
||||
Marshal.ReadIntPtr(deviceVtable, 0x18) == IntPtr.Add(controlModule, ControlProfileDeviceQueryRva),
|
||||
"constructor installs original Control device count/query methods");
|
||||
Marshal.WriteIntPtr(owner, 0x20, ani);
|
||||
Marshal.WriteIntPtr(owner, 0x38, deviceInterface);
|
||||
IntPtr profile = fixture.Allocate(27 * 4);
|
||||
Marshal.WriteIntPtr(owner, 0x46c, profile);
|
||||
Marshal.WriteInt32(profile, 26 * 4, unchecked((int)originalWord26));
|
||||
Marshal.WriteInt32(owner, 0x4d8, FloatBits(-999.0f));
|
||||
if (descriptorMobility.Length != 0) {
|
||||
IntPtr rows = fixture.Allocate(descriptorMobility.Length * 0x2c);
|
||||
for (int i = 0; i < descriptorMobility.Length; i++) {
|
||||
IntPtr descriptor = fixture.Allocate(0x20);
|
||||
IntPtr row = IntPtr.Add(rows, i * 0x2c);
|
||||
Marshal.WriteInt32(descriptor, 0x08, FloatBits(descriptorMobility[i]));
|
||||
Marshal.WriteIntPtr(row, 0x00, descriptor);
|
||||
Marshal.WriteInt32(row, 0x08, FloatBits(mutableAmounts[i]));
|
||||
Marshal.WriteInt32(row, 0x20, unchecked((int)0xdeadbeef));
|
||||
}
|
||||
Marshal.WriteIntPtr(owner, 0x55c, rows);
|
||||
Marshal.WriteInt32(owner, 0x560, descriptorMobility.Length);
|
||||
}
|
||||
if (hasCache) {
|
||||
IntPtr cache = fixture.Allocate(0x10);
|
||||
Marshal.WriteIntPtr(owner, 0x5b4, cache);
|
||||
Marshal.WriteInt32(cache, 0x0c, FloatBits(cacheC));
|
||||
}
|
||||
if (deviceKeys.Length != 0) {
|
||||
IntPtr devices = fixture.Allocate(deviceKeys.Length * IntPtr.Size);
|
||||
for (int i = 0; i < deviceKeys.Length; i++) {
|
||||
IntPtr device = fixture.Allocate(0x50);
|
||||
IntPtr descriptor = fixture.Allocate(0x20);
|
||||
Marshal.WriteInt32(device, 0x04, unchecked((int)deviceKeys[i]));
|
||||
Marshal.WriteIntPtr(device, 0x48, descriptor);
|
||||
Marshal.WriteInt32(descriptor, 0x1c, FloatBits(deviceValues[i]));
|
||||
Marshal.WriteIntPtr(devices, i * IntPtr.Size, device);
|
||||
}
|
||||
Marshal.WriteIntPtr(owner, 0x5bc, devices);
|
||||
Marshal.WriteInt32(owner, 0x5c0, deviceKeys.Length);
|
||||
}
|
||||
return owner;
|
||||
}
|
||||
|
||||
private static uint ReadFac0Word(IntPtr address, int offset) {
|
||||
return unchecked((uint)Marshal.ReadInt32(address, offset));
|
||||
}
|
||||
|
||||
private static void ExpectFac0Word(IntPtr address, int offset, float expected, string name) {
|
||||
uint expectedBits = FloatBitsUnsigned(expected);
|
||||
Expect(ReadFac0Word(address, offset) == expectedBits,
|
||||
name + " expected " + Hex(expectedBits) + " got " + Hex(ReadFac0Word(address, offset)));
|
||||
}
|
||||
|
||||
private static void RunFac0CombinedCase(IntPtr controlModule, IntPtr aniMeshModule,
|
||||
NativeControlOwnerConstructor constructOwner, NativeControlProfileFac0 fac0,
|
||||
NativeAniMeshNodeMetadataQuery queryNode, NativeAniMeshRangeLookup findFirst,
|
||||
NativeAniMeshRangeLookup countMatches) {
|
||||
using (Fixture fixture = NewFixture()) {
|
||||
IntPtr ani = CreateFac0AniMesh(fixture, aniMeshModule, new uint[] { 1, 0 }, 2.0f, 3.0f, 5.0f, 7.0f, 11.0f);
|
||||
IntPtr owner = CreateFac0Owner(fixture, controlModule, constructOwner, ani,
|
||||
FloatBitsUnsigned(2000.0f), new float[] { 2.0f, 3.0f }, new float[] { 123.0f, 456.0f },
|
||||
true, 2.0f, new uint[] { 1, 0 }, new float[] { 5.25f, 0.5f });
|
||||
Expect(findFirst(ani, 0) == 1 && countMatches(ani, 0) == 1,
|
||||
"AniMesh component-zero range is first=1,count=1");
|
||||
IntPtr outCell = fixture.Allocate(IntPtr.Size);
|
||||
Expect(NativeBoolean(queryNode(ani, 0, 0x0f, outCell)) == 1 &&
|
||||
ReadFloat(Marshal.ReadIntPtr(outCell), 0) == 42.0f, "original AniMesh selector F result");
|
||||
Expect(NativeBoolean(queryNode(ani, 1, 0x10, outCell)) == 1 &&
|
||||
ReadFloat(Marshal.ReadIntPtr(outCell), 0) == 330.0f, "original AniMesh selector 10 result");
|
||||
fac0(owner);
|
||||
Expect(CurrentX87ControlWord() == 0x007f, "FAC0 combined vector retains CW007f");
|
||||
ExpectFac0Word(owner, 0x530, 84.0f, "aggregate +0x530");
|
||||
ExpectFac0Word(owner, 0x534, 660.0f, "aggregate +0x534");
|
||||
ExpectFac0Word(owner, 0x538, 1823.75f, "aggregate +0x538");
|
||||
ExpectFac0Word(owner, 0x4d8, 1166.25f, "range-gated profile word 26");
|
||||
IntPtr rows = Marshal.ReadIntPtr(owner, 0x55c);
|
||||
ExpectFac0Word(rows, 0x20, 744.5f, "provider row 0 after device key 0");
|
||||
ExpectFac0Word(IntPtr.Add(rows, 0x2c), 0x20, 1079.25f, "provider row 1 after device key 1");
|
||||
Console.WriteLine("native-control-fac0-combined: scales=[2,3,5] slots=[7,11] selector-F/10=[42,330]" +
|
||||
" range=[1,1] descriptor+8=[2,3] mutable-row+8=[123,456] device-word7=[5.25,0.5]" +
|
||||
" totals=[530:" + Hex(ReadFac0Word(owner, 0x530)) + ",534:" + Hex(ReadFac0Word(owner, 0x534)) +
|
||||
",538:" + Hex(ReadFac0Word(owner, 0x538)) + ",4d8:" + Hex(ReadFac0Word(owner, 0x4d8)) +
|
||||
"] rows20=[" + Hex(ReadFac0Word(rows, 0x20)) + "," +
|
||||
Hex(ReadFac0Word(IntPtr.Add(rows, 0x2c), 0x20)) + "]");
|
||||
}
|
||||
}
|
||||
|
||||
private static uint RunFac0CacheCase(IntPtr controlModule, IntPtr aniMeshModule,
|
||||
NativeControlOwnerConstructor constructOwner, NativeControlProfileFac0 fac0,
|
||||
bool hasCache, float cacheC) {
|
||||
using (Fixture fixture = NewFixture()) {
|
||||
IntPtr ani = CreateFac0AniMesh(fixture, aniMeshModule, new uint[] { 0 }, 2.0f, 3.0f, 5.0f, 7.0f, 11.0f);
|
||||
IntPtr owner = CreateFac0Owner(fixture, controlModule, constructOwner, ani,
|
||||
FloatBitsUnsigned(1000.0f), new float[] { 1.0f }, new float[] { 777.0f }, hasCache,
|
||||
cacheC, new uint[0], new float[0]);
|
||||
fac0(owner);
|
||||
uint row20 = ReadFac0Word(Marshal.ReadIntPtr(owner, 0x55c), 0x20);
|
||||
uint expectedRow20 = FloatBitsUnsigned(330.0f + cacheC * 42.0f);
|
||||
uint expectedFinal = FloatBitsUnsigned(hasCache && cacheC != 0.0f ? 916.0f : 1000.0f);
|
||||
Expect(row20 == expectedRow20, "FAC0 row20 reflects cache presence/value");
|
||||
ExpectFac0Word(owner, 0x530, 42.0f, "cache selector F aggregate");
|
||||
ExpectFac0Word(owner, 0x534, 330.0f, "cache selector 10 aggregate");
|
||||
Expect(ReadFac0Word(owner, 0x4d8) == expectedFinal, "FAC0 cache final profile word");
|
||||
Console.WriteLine("native-control-fac0-cache: ptr=" + (hasCache ? "present" : "null") +
|
||||
" C=" + Hex(FloatBitsUnsigned(cacheC)) + " row20=" + Hex(row20) +
|
||||
" total538=" + Hex(ReadFac0Word(owner, 0x538)) + " final4d8=" + Hex(ReadFac0Word(owner, 0x4d8)));
|
||||
return ReadFac0Word(owner, 0x4d8);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunFac0NegativeClamp(IntPtr controlModule, IntPtr aniMeshModule,
|
||||
NativeControlOwnerConstructor constructOwner, NativeControlProfileFac0 fac0,
|
||||
NativeAniMeshRangeLookup findFirst, NativeAniMeshRangeLookup countMatches) {
|
||||
using (Fixture fixture = NewFixture()) {
|
||||
IntPtr ani = CreateFac0AniMesh(fixture, aniMeshModule, new uint[] { 1 }, 2.0f, 3.0f, 5.0f, 7.0f, 11.0f);
|
||||
IntPtr owner = CreateFac0Owner(fixture, controlModule, constructOwner, ani, 0,
|
||||
new float[] { 1.0f }, new float[] { 44.0f }, false, 0.0f, new uint[0], new float[0]);
|
||||
Expect(findFirst(ani, 0) == 0xffffffff && countMatches(ani, 0) == 0,
|
||||
"AniMesh reports no component-zero range");
|
||||
fac0(owner);
|
||||
ExpectFac0Word(owner, 0x538, 330.0f, "unranged provider aggregate");
|
||||
Expect(ReadFac0Word(owner, 0x4d8) == 0, "negative result clamps to positive zero");
|
||||
Console.WriteLine("native-control-fac0-negative-clamp: total538=" +
|
||||
Hex(ReadFac0Word(owner, 0x538)) + " final4d8=" + Hex(ReadFac0Word(owner, 0x4d8)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunFac0NullAndSignedZeroCases(IntPtr controlModule, IntPtr aniMeshModule,
|
||||
NativeControlOwnerConstructor constructOwner, NativeControlProfileFac0 fac0) {
|
||||
using (Fixture fixture = NewFixture()) {
|
||||
IntPtr owner = CreateFac0Owner(fixture, controlModule, constructOwner, IntPtr.Zero,
|
||||
0xc0a00000, new float[0], new float[0], false, 0.0f, new uint[0], new float[0]);
|
||||
fac0(owner);
|
||||
Expect(ReadFac0Word(owner, 0x4d8) == 0xc0a00000 && ReadFac0Word(owner, 0x530) == 0 &&
|
||||
ReadFac0Word(owner, 0x534) == 0 && ReadFac0Word(owner, 0x538) == 0,
|
||||
"null AniMesh returns after copying word 26 and before clamp");
|
||||
Console.WriteLine("native-control-fac0-null-model: original26=0xc0a00000 final4d8=" +
|
||||
Hex(ReadFac0Word(owner, 0x4d8)));
|
||||
}
|
||||
using (Fixture fixture = NewFixture()) {
|
||||
IntPtr ani = CreateFac0AniMesh(fixture, aniMeshModule, new uint[0], 2.0f, 3.0f, 5.0f, 7.0f, 11.0f);
|
||||
IntPtr owner = CreateFac0Owner(fixture, controlModule, constructOwner, ani, 0x80000000,
|
||||
new float[0], new float[0], false, 0.0f, new uint[0], new float[0]);
|
||||
fac0(owner);
|
||||
Expect(ReadFac0Word(owner, 0x4d8) == 0x80000000, "FAC0 preserves negative zero");
|
||||
Console.WriteLine("native-control-fac0-signed-zero: total538=" +
|
||||
Hex(ReadFac0Word(owner, 0x538)) + " final4d8=" + Hex(ReadFac0Word(owner, 0x4d8)));
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunFac0OrderedCase(IntPtr controlModule, IntPtr aniMeshModule,
|
||||
NativeControlOwnerConstructor constructOwner, NativeControlProfileFac0 fac0) {
|
||||
using (Fixture fixture = NewFixture()) {
|
||||
IntPtr ani = CreateFac0AniMesh(fixture, aniMeshModule, new uint[] { 1, 1, 1 },
|
||||
1.0f, 1.0f, 1.0f, 0.0f, 1.0f);
|
||||
IntPtr owner = CreateFac0Owner(fixture, controlModule, constructOwner, ani,
|
||||
FloatBitsUnsigned(10.0f), new float[] { 16777216.0f, 1.0f, -16777216.0f },
|
||||
new float[] { 13.0f, 17.0f, 19.0f }, false, 0.0f, new uint[0], new float[0]);
|
||||
fac0(owner);
|
||||
IntPtr rows = Marshal.ReadIntPtr(owner, 0x55c);
|
||||
Expect(ReadFac0Word(owner, 0x538) == 0 &&
|
||||
ReadFac0Word(owner, 0x534) == FloatBitsUnsigned(3.0f) &&
|
||||
ReadFac0Word(owner, 0x4d8) == FloatBitsUnsigned(10.0f),
|
||||
"FAC0 keeps forward single-precision aggregate rounding");
|
||||
Expect(ReadFac0Word(rows, 0x20) == 0x4b800000 &&
|
||||
ReadFac0Word(IntPtr.Add(rows, 0x2c), 0x20) == 0x3f800000 &&
|
||||
ReadFac0Word(IntPtr.Add(rows, 0x58), 0x20) == 0xcb800000,
|
||||
"FAC0 preserves provider-row products in source order");
|
||||
Console.WriteLine("native-control-fac0-ordered: row-products=[0x4b800000,0x3f800000,0xcb800000]" +
|
||||
" total538=0x00000000 regrouped=0x3f800000");
|
||||
}
|
||||
}
|
||||
|
||||
private static IntPtr WriteStdCallToFastcallOneArgThunk(int offset, IntPtr fastcallTarget) {
|
||||
// Adapt stdcall(owner) to the native fastcall owner register in ECX.
|
||||
byte[] code = { 0x8b, 0x4c, 0x24, 0x04, 0xb8, 0, 0, 0, 0, 0xff, 0xd0, 0xc2, 0x04, 0x00 };
|
||||
WriteImm32(code, 5, fastcallTarget.ToInt32());
|
||||
IntPtr thunk = IntPtr.Add(fastcallThunks, offset);
|
||||
Marshal.Copy(code, 0, thunk, code.Length);
|
||||
return thunk;
|
||||
}
|
||||
|
||||
private static void RunNativeControlProviderInitialStateCase(IntPtr controlModule, IntPtr aniMeshModule) {
|
||||
const int rowCount = 4;
|
||||
NativeControlOwnerConstructor constructOwner = (NativeControlOwnerConstructor)Marshal.GetDelegateForFunctionPointer(
|
||||
|
||||
@@ -540,14 +540,52 @@ resource-device значения во время игры.
|
||||
|
||||
### Профиль владельца Control из CTLD
|
||||
|
||||
Смысл отдельных слов, последующие изменения mutable-копии и чтения в Control
|
||||
`fca0/fac0` остаются открытыми. Загрузочный тест проверяет CPU-проекцию, но не
|
||||
исполнение native полного загрузчика или игрового tick. Точные writer, RVA и
|
||||
указатели описаны в [техническом разделе о загрузке CTLD](script-vm.md#профиль-владельца-control-при-загрузке-ctld).
|
||||
Общий проход оригинального `Control.dll!FUN_1000fac0` (RVA `0x0fac0`, SHA-256
|
||||
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`) проверен
|
||||
прямым x86-вызовом под control word `0x007f` на synthetic backing. Оригинальный
|
||||
`AniMesh.dll` query `0x51f0` и методы диапазона component ID `0`
|
||||
`0x5780/0x57c0` участвуют в векторе; Rust воспроизводит его raw output bits и
|
||||
отдельные cache, order, clamp и signed-zero случаи.
|
||||
|
||||
Закрывающий эксперимент: трассировать `LoadControlSystem`,
|
||||
`LoadPhysicalModel`, `CreateCollManager` и создание collision objects; связать
|
||||
каждый изменяемый field с созданным shape, contact или реакцией на движение.
|
||||
Для обычной начальной загрузки scale-input тоже закрыт. AniMesh constructor
|
||||
оставляет `+0x158/+0x15c/+0x160` равными `1.0`; перед Control event `4` World3D
|
||||
проходит Agent event `0`, AniMesh RVA `0x1a80` вызывает базовый return `0x17df0`
|
||||
и передаёт event `4/null` модели, FX, CollManager и CollObject. Их проверенные
|
||||
ветви `0x75b0/0x6fc0`, Effect RVA `0x3d30`, Control RVA `0x1ce00` и `0x1fe00`
|
||||
не вызывают AniMesh setter `0x14770` и не меняют outer scale. Поэтому FAC0 в
|
||||
стандартном `LoadAgent` получает `[1, 1, 1]`. Другой маршрут
|
||||
`AddNewObjectToGame`/mirror к setter и последующие placement/tick остаются
|
||||
отдельными границами. В synthetic x86-векторе по-прежнему явно заданы `[2,3,5]`;
|
||||
он не является источником scale для полного load.
|
||||
|
||||
Смысл `Slot68 +0x30/+0x34`, tag-27 cache DWORD, owner accumulators `+0x530`,
|
||||
`+0x534`, `+0x538` и результата mutable word `26` не имеет доказанных единиц
|
||||
измерения. Неизвестные слова остаются raw; их нельзя подписывать как массу или
|
||||
площадь. Synthetic x86 vector напрямую записывает scales `[2, 3, 5]` в
|
||||
`AniMesh +0x158/+0x15c/+0x160`; это не значения обычной миссии. Проверка
|
||||
стандартных `ArealMap`/`World3D` и AniMesh callback-ветвей до FAC0 подтвердила,
|
||||
что setter не вызывается, поэтому полный начальный loader использует defaults
|
||||
конструктора `[1, 1, 1]`. Конкретный отдельный путь World3D `AddNewObjectToGame`
|
||||
и mirror-варианты передают свой transform-вектор setter-у; это отдельная
|
||||
граница, как и более поздние placement/tick.
|
||||
|
||||
Полный Rust loader теперь вызывает FAC0 при построении
|
||||
`build_control_device_projection` и публикует результат в
|
||||
`MissionControlOwnerProfile::load_projection`. Игнорируемый corpus-тест
|
||||
`full_load_builds_effective_control_device_table_for_mine_unit_dat` на лицензированном
|
||||
`min_l_n1.dat` проверяет 11 устройств, все 25 соответствий provider ordinal
|
||||
модели и упорядоченные значения `row +0x20`. Его агрегаты —
|
||||
`0x470833fc/0x4897e712/0x4d9452cb` для `+0x530/+0x534/+0x538`; исходный и
|
||||
начальный mutable word `26` равны `0x7f7fffff`. Это подтверждает Rust full-load
|
||||
результат на оригинальных ресурсах, но не совпадение с полным нативным load
|
||||
оригинала. Поведение позже при placement/tick и физический смысл raw-полей
|
||||
остаётся неизвестным. Точные RVA, owner layout и векторы приведены в
|
||||
[описании изолированного прохода FAC0](script-vm.md#изолированный-проход-fac0).
|
||||
|
||||
Остаётся сравнить этот полный mine load с original loader при тех же входных
|
||||
данных и продолжить трассировку `LoadControlSystem`, `LoadPhysicalModel`,
|
||||
`CreateCollManager` и collision objects, если потребуется связать profile words
|
||||
с shapes, contacts или реакцией на движение.
|
||||
|
||||
## DirectPlay wire
|
||||
|
||||
|
||||
@@ -977,6 +977,143 @@ Ignored Rust-тест также проходит полный неотфиль
|
||||
профилю. Это подтверждает декодирование и CPU-проекцию, но не исполнение native
|
||||
полного загрузчика.
|
||||
|
||||
#### Изолированный проход FAC0
|
||||
|
||||
В оригинальном `Control.dll` SHA-256
|
||||
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`
|
||||
функция `FUN_1000fac0` (RVA `0x0fac0`) рассчитывает начальное состояние
|
||||
mutable-копии CTLD профиля. x86 oracle вызывает её с указателем owner в `ECX`
|
||||
через короткий stdcall-to-fastcall переходник. Вектор создаёт owner исходным
|
||||
конструктором RVA `0x31490`, но памяти для профиля, provider rows, device list и
|
||||
AniMesh модели выделяет искусственно. Код FAC0 и выбранные методы DLL при этом
|
||||
остаются оригинальными.
|
||||
|
||||
В используемой раскладке owner функция читает и записывает следующие поля:
|
||||
|
||||
| Смещение owner | Что находится в тестовом ABI |
|
||||
| --- | --- |
|
||||
| `+0x20` | указатель на AniMesh; в отдельном векторе здесь null |
|
||||
| `+0x46c` | указатель на исходный блок из 27 слов профиля |
|
||||
| `+0x470` | изменяемая копия из 27 слов; её word `26` находится по `+0x4d8` |
|
||||
| `+0x530`, `+0x534`, `+0x538` | три накопителя профиля |
|
||||
| `+0x55c`, `+0x560` | указатель на строки provider и их count; шаг строки — `0x2c` |
|
||||
| `+0x5b4` | необязательный указатель cache; используемый DWORD расположен по `+0x0c` |
|
||||
| `+0x5bc`, `+0x5c0` | массив указателей на устройства и его count |
|
||||
|
||||
Это описание ABI-вектора, а не отдельная публичная Rust layout-структура.
|
||||
|
||||
Для чтения MSH функции используется оригинальный `AniMesh.dll` SHA-256
|
||||
`65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`:
|
||||
`FUN_100051f0` (RVA `0x51f0`) запрашивает значение узла по selector `0x0f` или
|
||||
`0x10`, а `FUN_10005780` / `FUN_100057c0` (RVA `0x5780` / `0x57c0`) дают
|
||||
начало и длину диапазона глобальных узлов component ID `0`. В model data функция
|
||||
`0x51f0` читает Node38 mapping и использует два выбранных DWORD Slot68 `+0x30`
|
||||
и `+0x34` при вычислении selectors. Это opaque `opaque[0]` и `opaque[1]`, а
|
||||
не доказанные физические коэффициенты или значения, которые query возвращает без
|
||||
преобразования.
|
||||
|
||||
Slot выбирается первым словом `Node38.slot_index[state * 5 + lod]`, то есть для
|
||||
начального профиля — state `0`, LOD `0` и byte offset `+0x08` в Node38.
|
||||
`0xffff` обозначает отсутствие slot и даёт нулевые selectors без умножения на
|
||||
scales. В Rust проекции другой непригодный индекс или нечитаемая запись —
|
||||
неизвестное сопоставление, а не известный нулевой slot. Для валидной ссылки Rust
|
||||
использует `selected_slot` и сохраняет эту границу через `Option`.
|
||||
|
||||
Если три входных `f32` из AniMesh `+0x158`, `+0x15c`, `+0x160` обозначить
|
||||
`s158`, `s15c`, `s160`, а два raw Slot68 слова — `v30` и `v34`, порядок native
|
||||
умножений задаёт selectors:
|
||||
|
||||
```text
|
||||
selector F = (s15c * s158) * v30
|
||||
selector 10 = ((s160 * s15c) * s158) * v34
|
||||
```
|
||||
|
||||
Затем для каждой provider row по её global-node ordinal вычисляется
|
||||
`base = row.mobility_per_volume * selector 10`. `+0x534` накапливает selector
|
||||
`10`, `+0x530` — selector `F`, а `+0x538` — `row +0x20` после начального
|
||||
расчёта и device contributions. Если ordinal попадает в диапазон component ID
|
||||
`0`, `base` добавляется к **исходному** profile word `26`; накопление начинается
|
||||
из original CTLD copy, а не из прежнего mutable word `+0x4d8`.
|
||||
|
||||
Ненулевой cache pointer `owner +0x5b4` указывает на raw cache DWORD `+0x0c`.
|
||||
Он добавляет в каждый `row +0x20` значение `cache27 * selector F`. В
|
||||
проекции Rust последняя tag `27` запись начального append задаёт raw cache из
|
||||
`fixed_words[11]`; последующий tag `27` в `INTO` обновляет существующее значение,
|
||||
а другой tag его не меняет. Если `INTO` встречается до создания cache, результат
|
||||
профиля помечается недоступным и остаётся таким после более позднего append.
|
||||
Cache отсутствует и cache со значением ноль численно дают одинаковую добавку;
|
||||
это не превращает пропущенную ссылку в нулевые данные.
|
||||
|
||||
После начального прохода устройства читаются в обратном порядке. Каждое
|
||||
устройство берёт индекс provider row из ключа `+0x04`, а DWORD descriptor
|
||||
`+0x1c` прибавляет и к выбранному `row +0x20`, и к `+0x538`. Накопители
|
||||
складываются в наблюдаемом порядке одиночной точности; перестановка или
|
||||
переассоциация слагаемых меняет биты.
|
||||
|
||||
После вычитания `+0x538` из подготовленного word `26` отрицательный результат
|
||||
записывается как положительный ноль. Ненегативный результат, включая `-0.0`,
|
||||
сохраняет свои исходные биты. Если AniMesh pointer отсутствует, оригинальный
|
||||
word `26` просто копируется в mutable copy, и функция возвращает до этого
|
||||
вычитания. Ни слова профиля, ни cache, ни Slot68 DWORD пока не получили названий
|
||||
«масса» или «площадь».
|
||||
|
||||
Объединённый вектор под x87 control word `0x007f` задаёт scales `[2, 3, 5]`,
|
||||
Slot68 raw words `[7, 11]`, коэффициенты двух provider rows `[2, 3]`, диапазон
|
||||
component `0` `[first=1, count=1]`, cache `27 = 2`, device keys `[1, 0]` с
|
||||
descriptor values `[5.25, 0.5]`, original word `26 = 2000` и прежнюю mutable
|
||||
копию word `26 = -999`. Исходные функции выдают:
|
||||
|
||||
| Поле | Десятичное значение | Raw bits |
|
||||
| --- | ---: | --- |
|
||||
| `owner +0x530` | `84` | `0x42a80000` |
|
||||
| `owner +0x534` | `660` | `0x44250000` |
|
||||
| `owner +0x538` | `1823.75` | `0x44e3f800` |
|
||||
| `owner +0x4d8` (mutable word `26`) | `1166.25` | `0x4491c800` |
|
||||
| provider row `+0x20`, row `0` | `744.5` | `0x443a2000` |
|
||||
| provider row `+0x20`, row `1` | `1079.25` | `0x4486e800` |
|
||||
|
||||
Другие векторы показывают различие отсутствующего и нулевого cache, положительный
|
||||
ноль после negative clamp, сохранение signed zero и прямое сложение в source
|
||||
order: `2^24 + 1 - 2^24` даёт `0`, тогда как группировка `2^24 - 2^24 + 1`
|
||||
дала бы `1`. Rust helper повторяет эти тестовые биты в поддерживаемом домене
|
||||
normal-or-zero; он отклоняет непредставимые значения и промежуточные результаты,
|
||||
а не заявляет parity для каждого x87 режима или всех допустимых битовых payload.
|
||||
|
||||
Этот x86-вектор подтверждает арифметику оригинальной функции на synthetic
|
||||
backing. Значения `[2, 3, 5]` относятся только к нему; источник scales в
|
||||
обычной загрузке — другой, отдельно проверенный путь, привязанный к GOG
|
||||
сборкам: `ArealMap.dll` SHA-256
|
||||
`a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea`,
|
||||
`World3D.dll` `17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e`,
|
||||
`AniMesh.dll` `65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`,
|
||||
`Effect.dll` `386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789`
|
||||
и `Control.dll` `bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`.
|
||||
Сначала `AniMesh::FUN_10002ff0` (RVA `0x2ff0`) вызывает `LoadAniMesh`, затем —
|
||||
`LoadControlSystem` на RVA `0x32e7`. После возврата из этой фабрики обычный
|
||||
`ArealMap` путь `0x14fd0/0x15290` вызывает `World3D.CreateObject`
|
||||
(`0x7cb0/0x7a50`), который до возврата посылает Agent event `0` через primary
|
||||
vtable slot `+0x38`. Agent RVA `0x1a80` сначала вызывает базовый обработчик
|
||||
`0x17df0`, затем рассылает компонентам event `4` с null payload. Конструктор
|
||||
AniMesh задаёт `+0x158/+0x15c/+0x160` равными `1.0`.
|
||||
Обработчик модели `0x75b0/0x6fc0` привязывает интерфейсы и матрицу, Effect RVA
|
||||
`0x3d30` и Control RVA `0x1ce00` сохраняют host/service указатели, а Control
|
||||
`0x1fe00` меняет только локальный collision-флаг. Эти ветви не вызывают AniMesh
|
||||
scale setter `0x14770` и не пишут поля масштаба. После них Control проходит
|
||||
`0x31b00 -> 0x314f0 -> 0x7830 -> 0xf940`, то есть в FAC0 входит с constructor
|
||||
scales `[1, 1, 1]`. Это подтверждено только для обычной начальной загрузки и
|
||||
rebind; отдельные AddNewObject/mirror scale setters и более поздние placement/
|
||||
tick пути в этот вывод не входят.
|
||||
|
||||
Теперь полный Rust loader вызывает FAC0 в `build_control_device_projection`
|
||||
после сборки provider rows и устройств. Игнорируемый тест
|
||||
`full_load_builds_effective_control_device_table_for_mine_unit_dat` прогнан на
|
||||
лицензированном `min_l_n1.dat`: он проверяет 11 устройств, сопоставление всех
|
||||
25 provider rows и весь упорядоченный вектор `row +0x20`. Агрегаты mine равны
|
||||
`+0x530 = 0x470833fc`, `+0x534 = 0x4897e712`, `+0x538 = 0x4d9452cb`; исходное и
|
||||
начальное mutable word `26` оба равны `0x7f7fffff`. Это измеренный результат
|
||||
Rust loader-а на оригинальных ресурсах, но не сравнение полного load результата
|
||||
с оригинальным loader-ом. Имена и физические единицы полей остаются неизвестны.
|
||||
|
||||
#### Входной класс Control при загрузке Unit DAT
|
||||
|
||||
Этот класс задаётся до разбора CTLD и не выводится из device tag или MSH-
|
||||
|
||||
@@ -717,6 +717,34 @@ Rust full-loader теста на оригинальных ресурсах. Ве
|
||||
`None`, без дочернего fallback или синтетических нулей. Точные native writes и
|
||||
открытые вопросы о смысле полей приведены в [техническом разборе загрузки CTLD](../appendices/script-vm.md#профиль-владельца-control-при-загрузке-ctld).
|
||||
|
||||
Изолированный x86-вектор подтвердил общий проход `Control.dll!FUN_1000fca0`
|
||||
(RVA `0x0fac0`): расчёт начинает с исходного profile word 26, а затем пишет
|
||||
производное значение в изменяемую копию и заполняет три поля-накопителя
|
||||
владельца, а также `+0x20` каждой строки provider. Для этого выбираются два
|
||||
сырых слова `Slot68` через `Node38` state 0 / LOD 0, учитываются cache tag 27 и
|
||||
устройства таблицы.
|
||||
Смысл слов `Slot68` и итоговых чисел не установлен; мы не называем их массой
|
||||
или площадью.
|
||||
|
||||
Rust повторяет эту формулу изолированным helper-ом, и его тест совпадает с
|
||||
оригинальным кодом на синтетических векторах при x87 control word `0x007f`.
|
||||
В обычной цепочке загрузки AniMesh начинает с масштабов `[1, 1, 1]`, а вызванные
|
||||
до FAC0 обработчики привязки модели, FX и collision-компонентов эти поля не
|
||||
меняют. Поэтому загрузчик использует подтверждённые значения конструктора, а
|
||||
не scale из TMA. Теперь `build_control_device_projection` выполняет FAC0 после
|
||||
сборки provider rows и списка устройств и публикует результат вместе с копией
|
||||
профиля владельца.
|
||||
|
||||
Игнорируемый corpus-тест полного загрузчика на оригинальном
|
||||
`UNITS/BUILDS/MINE/min_l_n1.dat` подтверждает сопоставление всех 25 provider
|
||||
rows с начальными узлами и проекцию 11 устройств. Для профиля mine получены
|
||||
агрегаты `+0x530 = 0x470833fc`, `+0x534 = 0x4897e712` и
|
||||
`+0x538 = 0x4d9452cb`; тест сверяет также все 25 значений строк. Это результат
|
||||
Rust loader-а на лицензированных данных, а не сравнение с полным native load
|
||||
оригинала. Смысл слов `Slot68` и итоговых чисел по-прежнему неизвестен. Точные
|
||||
входы, вектор и границы приведены в
|
||||
[техническом разборе загрузки CTLD](../appendices/script-vm.md#изолированный-проход-fac0).
|
||||
|
||||
Для tag `19` фабрика создаёт объект с его исходными классом и vtable. Базовый
|
||||
конструктор `FUN_100218b0` задаёт multiplier `+0x4c = 1.0`. Override через
|
||||
`FUN_1002d890` вызывает у выбранного объекта vtable slot `+0x1c`, то есть
|
||||
|
||||
Reference in new issue
Block a user