@series-inc/rundot-syncplay 5.26.0-beta.2 → 5.26.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/core/native/deterministic_math.hpp +4 -3
- package/core/native/fixed_math.cpp +741 -0
- package/core/native/fixed_math.hpp +111 -0
- package/core/native/kinetix-native-main.cpp +227 -88
- package/core/native/mobile/box3d_probe_scenario.cpp +101 -0
- package/core/native/mobile/box3d_probe_scenario.hpp +25 -0
- package/core/native/mobile/syncplay_native_probe.cpp +114 -0
- package/core/native/mobile/syncplay_native_probe_abi.h +46 -0
- package/core/physics.d.ts +48 -0
- package/core/scripts/build-mobile-native.mjs +101 -0
- package/core/scripts/build-native.mjs +15 -1
- package/core/scripts/build-wasm-fixed-math.mjs +58 -0
- package/core/scripts/run-box3d-benchmark.mjs +22 -124
- package/core/src/physics/box3d-probe.mjs +171 -0
- package/core/src/physics/index.mjs +156 -19
- package/core/src/physics/physics-wasm-module.generated.mjs +3 -3
- package/dist/browser.d.ts +2 -1
- package/dist/browser.js +1 -0
- package/dist/creator.d.ts +1 -0
- package/dist/creator.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/movement3d.d.ts +31 -0
- package/dist/movement3d.js +265 -30
- package/dist/physics3d-heightfield.d.ts +10 -0
- package/dist/physics3d-heightfield.js +33 -0
- package/dist/physics3d-hull.d.ts +14 -0
- package/dist/physics3d-hull.js +45 -0
- package/dist/physics3d.js +28 -1
- package/package.json +8 -4
- package/dist/prediction-culling.d.ts +0 -34
- package/dist/prediction-culling.js +0 -200
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// C++ twin of src/math.ts (DeterministicMath). A WebView peer runs the TS; a
|
|
2
|
+
// native peer runs this; both must agree bit-for-bit or the match desyncs.
|
|
3
|
+
// Values are `double`, not int64: the reference computes every intermediate as
|
|
4
|
+
// an IEEE double, and past 2^53 the two disagree (see mul).
|
|
5
|
+
// Parity is gated by core/tests/fixed-parity.test.mjs.
|
|
6
|
+
#ifndef RUNDOT_FIXED_MATH_HPP
|
|
7
|
+
#define RUNDOT_FIXED_MATH_HPP
|
|
8
|
+
|
|
9
|
+
namespace rundot::fixedmath {
|
|
10
|
+
|
|
11
|
+
struct Config {
|
|
12
|
+
double fixedScale;
|
|
13
|
+
double minFixed;
|
|
14
|
+
double maxFixed;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
struct Vec2 {
|
|
18
|
+
double x;
|
|
19
|
+
double y;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
struct Vec3 {
|
|
23
|
+
double x;
|
|
24
|
+
double y;
|
|
25
|
+
double z;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
struct Quat {
|
|
29
|
+
double x;
|
|
30
|
+
double y;
|
|
31
|
+
double z;
|
|
32
|
+
double w;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
Config defaultConfig();
|
|
36
|
+
|
|
37
|
+
// `ok` is set false where the TypeScript reference throws. Callers must check it
|
|
38
|
+
// before using the return value; a false `ok` never carries a meaningful number.
|
|
39
|
+
double toFixed(const Config& c, double value, bool& ok);
|
|
40
|
+
double fromFixed(const Config& c, double value, bool& ok);
|
|
41
|
+
double add(const Config& c, double a, double b, bool& ok);
|
|
42
|
+
double sub(const Config& c, double a, double b, bool& ok);
|
|
43
|
+
double mul(const Config& c, double a, double b, bool& ok);
|
|
44
|
+
double div(const Config& c, double a, double b, bool& ok);
|
|
45
|
+
double saturatingAdd(const Config& c, double a, double b, bool& ok);
|
|
46
|
+
double saturatingSub(const Config& c, double a, double b, bool& ok);
|
|
47
|
+
double quantize(const Config& c, double value, double engine, bool& ok);
|
|
48
|
+
double sqrtFixed(const Config& c, double value, bool& ok);
|
|
49
|
+
double integerSqrt(const Config& c, double value, bool& ok);
|
|
50
|
+
double absFixed(const Config& c, double value, bool& ok);
|
|
51
|
+
double sign(const Config& c, double value, bool& ok);
|
|
52
|
+
double minFixed(const Config& c, double a, double b, bool& ok);
|
|
53
|
+
double maxFixed(const Config& c, double a, double b, bool& ok);
|
|
54
|
+
double lerp(const Config& c, double a, double b, double t, bool& ok);
|
|
55
|
+
double mod(const Config& c, double a, double b, bool& ok);
|
|
56
|
+
double inverseLerp(const Config& c, double a, double b, double v, bool& ok);
|
|
57
|
+
double remap(const Config& c, double inMin, double inMax, double outMin, double outMax, double v, bool& ok);
|
|
58
|
+
double smoothstep(const Config& c, double edge0, double edge1, double x, bool& ok);
|
|
59
|
+
double log2Fixed(const Config& c, double value, bool& ok);
|
|
60
|
+
double expFixed(const Config& c, double value, bool& ok);
|
|
61
|
+
double logFixed(const Config& c, double value, bool& ok);
|
|
62
|
+
double sinTurns(const Config& c, double turns, bool& ok);
|
|
63
|
+
double cosTurns(const Config& c, double turns, bool& ok);
|
|
64
|
+
double tanTurns(const Config& c, double turns, bool& ok);
|
|
65
|
+
double atan2Turns(const Config& c, double y, double x, bool& ok);
|
|
66
|
+
double atan2Signed(const Config& c, double y, double x, bool& ok);
|
|
67
|
+
double wrapTurns(const Config& c, double t, bool& ok);
|
|
68
|
+
double angleDeltaTurns(const Config& c, double a, double b, bool& ok);
|
|
69
|
+
double turnsToRadians(const Config& c, double t, bool& ok);
|
|
70
|
+
double radiansToTurns(const Config& c, double r, bool& ok);
|
|
71
|
+
double hypot(const Config& c, double x, double y, bool& ok);
|
|
72
|
+
|
|
73
|
+
Vec2 vec2(const Config& c, double x, double y, bool& ok);
|
|
74
|
+
Vec2 vec2Add(const Config& c, Vec2 a, Vec2 b, bool& ok);
|
|
75
|
+
Vec2 vec2Sub(const Config& c, Vec2 a, Vec2 b, bool& ok);
|
|
76
|
+
Vec2 vec2Scale(const Config& c, Vec2 v, double s, bool& ok);
|
|
77
|
+
double vec2Dot(const Config& c, Vec2 a, Vec2 b, bool& ok);
|
|
78
|
+
double vec2Cross(const Config& c, Vec2 a, Vec2 b, bool& ok);
|
|
79
|
+
double vec2LengthSq(const Config& c, Vec2 v, bool& ok);
|
|
80
|
+
double vec2Length(const Config& c, Vec2 v, bool& ok);
|
|
81
|
+
double vec2DistanceSq(const Config& c, Vec2 a, Vec2 b, bool& ok);
|
|
82
|
+
double vec2Distance(const Config& c, Vec2 a, Vec2 b, bool& ok);
|
|
83
|
+
Vec2 vec2Normalize(const Config& c, Vec2 v, bool& ok);
|
|
84
|
+
Vec2 vec2Perp(const Config& c, Vec2 v, bool& ok);
|
|
85
|
+
Vec2 vec2Negate(const Config& c, Vec2 v, bool& ok);
|
|
86
|
+
Vec2 vec2Rotate(const Config& c, Vec2 v, double turns, bool& ok);
|
|
87
|
+
Vec2 vec2FromAngleTurns(const Config& c, double turns, double length, bool& ok);
|
|
88
|
+
double vec2AngleTurns(const Config& c, Vec2 v, bool& ok);
|
|
89
|
+
Vec2 vec2Lerp(const Config& c, Vec2 a, Vec2 b, double t, bool& ok);
|
|
90
|
+
|
|
91
|
+
Vec3 vec3(const Config& c, double x, double y, double z, bool& ok);
|
|
92
|
+
Vec3 vec3Add(const Config& c, Vec3 a, Vec3 b, bool& ok);
|
|
93
|
+
Vec3 vec3Sub(const Config& c, Vec3 a, Vec3 b, bool& ok);
|
|
94
|
+
Vec3 vec3Scale(const Config& c, Vec3 v, double s, bool& ok);
|
|
95
|
+
double vec3Dot(const Config& c, Vec3 a, Vec3 b, bool& ok);
|
|
96
|
+
Vec3 vec3Cross(const Config& c, Vec3 a, Vec3 b, bool& ok);
|
|
97
|
+
double vec3LengthSq(const Config& c, Vec3 v, bool& ok);
|
|
98
|
+
double vec3Length(const Config& c, Vec3 v, bool& ok);
|
|
99
|
+
double vec3Distance(const Config& c, Vec3 a, Vec3 b, bool& ok);
|
|
100
|
+
Vec3 vec3Normalize(const Config& c, Vec3 v, bool& ok);
|
|
101
|
+
Vec3 vec3Lerp(const Config& c, Vec3 a, Vec3 b, double t, bool& ok);
|
|
102
|
+
Vec3 vec3Negate(const Config& c, Vec3 v, bool& ok);
|
|
103
|
+
|
|
104
|
+
Quat quatFromYawTurns(const Config& c, double turns, bool& ok);
|
|
105
|
+
Quat quatLerp(const Config& c, Quat a, Quat b, double t, bool& ok);
|
|
106
|
+
|
|
107
|
+
double clamp(const Config& c, double value, double lo, double hi, bool& ok);
|
|
108
|
+
|
|
109
|
+
} // namespace rundot::fixedmath
|
|
110
|
+
|
|
111
|
+
#endif // RUNDOT_FIXED_MATH_HPP
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
#include <algorithm>
|
|
3
3
|
#include <bit>
|
|
4
4
|
#include <chrono>
|
|
5
|
+
#include <cmath>
|
|
5
6
|
#include <cstdint>
|
|
6
7
|
#include <fstream>
|
|
7
8
|
#include <iomanip>
|
|
@@ -12,6 +13,8 @@
|
|
|
12
13
|
#include <vector>
|
|
13
14
|
|
|
14
15
|
#include "deterministic_math.hpp"
|
|
16
|
+
#include "fixed_math.hpp"
|
|
17
|
+
#include "mobile/box3d_probe_scenario.hpp"
|
|
15
18
|
#include "physics/physics.hpp"
|
|
16
19
|
#include "kinetix-render-projection.hpp"
|
|
17
20
|
#include "kinetix-native-runtime.hpp"
|
|
@@ -542,76 +545,13 @@ int runPhysicsQuerySession() {
|
|
|
542
545
|
return 0;
|
|
543
546
|
}
|
|
544
547
|
|
|
545
|
-
std::uint64_t benchmarkHash(const std::vector<double>& bodyData, std::uint32_t bodyCount) {
|
|
546
|
-
std::uint64_t hash = 1469598103934665603ULL;
|
|
547
|
-
constexpr std::uint64_t prime = 1099511628211ULL;
|
|
548
|
-
const auto hashFloat = [&](double value) {
|
|
549
|
-
const float narrowed = static_cast<float>(value);
|
|
550
|
-
hash ^= std::bit_cast<std::uint32_t>(narrowed);
|
|
551
|
-
hash *= prime;
|
|
552
|
-
};
|
|
553
|
-
// Match the pinned Box3D harness: dynamic bodies only, using the common
|
|
554
|
-
// public state available from both engines and float32 serialization.
|
|
555
|
-
for (std::uint32_t bodyIndex = 1; bodyIndex < bodyCount; bodyIndex += 1) {
|
|
556
|
-
const std::size_t offset = static_cast<std::size_t>(bodyIndex) * rundot::kinetix::physics::kBodyStride;
|
|
557
|
-
for (const std::uint32_t field : {0, 1, 2, 3, 4, 5, 19, 20, 21, 22, 23, 24, 25}) {
|
|
558
|
-
hashFloat(bodyData[offset + field]);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
return hash;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
548
|
int runPhysicsBenchmark(std::uint32_t bodyCount, std::uint32_t frameCount, bool useDynamicTree) {
|
|
565
|
-
constexpr std::uint32_t stride = rundot::kinetix::physics::kBodyStride;
|
|
566
549
|
if (bodyCount < 2 || bodyCount > 100000 || frameCount == 0) {
|
|
567
550
|
std::cerr << "KINETIX_PHYSICS_BENCHMARK_ARGUMENTS_INVALID\n";
|
|
568
551
|
return 2;
|
|
569
552
|
}
|
|
570
|
-
std::vector<double> bodyData
|
|
571
|
-
|
|
572
|
-
bodyData[offset + 9] = motion;
|
|
573
|
-
bodyData[offset + 10] = 0.0;
|
|
574
|
-
bodyData[offset + 11] = 1.0;
|
|
575
|
-
bodyData[offset + 12] = 1.0;
|
|
576
|
-
bodyData[offset + 13] = 1.0;
|
|
577
|
-
bodyData[offset + 14] = 1.0;
|
|
578
|
-
bodyData[offset + 18] = 1.0;
|
|
579
|
-
bodyData[offset + 25] = 1.0;
|
|
580
|
-
bodyData[offset + 26] = 1.0;
|
|
581
|
-
bodyData[offset + 27] = 0.6;
|
|
582
|
-
bodyData[offset + 31] = 1.0;
|
|
583
|
-
bodyData[offset + 32] = 1.0;
|
|
584
|
-
bodyData[offset + 33] = 1.0;
|
|
585
|
-
};
|
|
586
|
-
setCommon(0, 0);
|
|
587
|
-
bodyData[0] = 0.0;
|
|
588
|
-
bodyData[1] = -32.5;
|
|
589
|
-
bodyData[10] = 1.0;
|
|
590
|
-
bodyData[6] = 32.5;
|
|
591
|
-
for (std::uint32_t index = 1; index < bodyCount; index += 1) {
|
|
592
|
-
const std::size_t offset = static_cast<std::size_t>(index) * stride;
|
|
593
|
-
setCommon(offset, 1);
|
|
594
|
-
bodyData[offset + 10] = 1.0;
|
|
595
|
-
const std::uint32_t column = (index - 1) % 16;
|
|
596
|
-
const std::uint32_t row = (index - 1) / 16;
|
|
597
|
-
bodyData[offset + 0] = static_cast<double>(column) * 1.05 - 7.875;
|
|
598
|
-
bodyData[offset + 1] = 0.5 + static_cast<double>(row) * 1.05;
|
|
599
|
-
bodyData[offset + 2] = 0.0;
|
|
600
|
-
bodyData[offset + 3] = (column % 2 == 0) ? 0.1 : -0.1;
|
|
601
|
-
bodyData[offset + 6] = 0.5;
|
|
602
|
-
// Match the Box3D harness, which builds these spheres from density=1.0 and
|
|
603
|
-
// lets the engine derive mass/inertia. For a solid sphere of radius 0.5:
|
|
604
|
-
// mass = (4/3)*pi*r^3 = pi/6
|
|
605
|
-
// inertia = (2/5)*mass*r^2 = 0.1*mass (isotropic)
|
|
606
|
-
// Without this, Kinetix would run mass=1/inertia=1 spheres and the same-scene
|
|
607
|
-
// native-vs-Box3D proof-gate ratio would compare different simulations.
|
|
608
|
-
constexpr double kSphereMass = 3.14159265358979311599796346854 / 6.0;
|
|
609
|
-
bodyData[offset + 18] = kSphereMass;
|
|
610
|
-
bodyData[offset + 31] = 0.1 * kSphereMass;
|
|
611
|
-
bodyData[offset + 32] = 0.1 * kSphereMass;
|
|
612
|
-
bodyData[offset + 33] = 0.1 * kSphereMass;
|
|
613
|
-
}
|
|
614
|
-
rundot::kinetix::physics::setGeometryData(nullptr, 0);
|
|
553
|
+
std::vector<double> bodyData = rundot::kinetix::mobile_probe::makeBox3dBodies(bodyCount);
|
|
554
|
+
rundot::kinetix::mobile_probe::clearPhysicsGlobalState();
|
|
615
555
|
rundot::kinetix::physics::setDynamicTreeEnabled(useDynamicTree);
|
|
616
556
|
const auto start = std::chrono::steady_clock::now();
|
|
617
557
|
for (std::uint32_t frame = 0; frame < frameCount; frame += 1) {
|
|
@@ -633,7 +573,221 @@ int runPhysicsBenchmark(std::uint32_t bodyCount, std::uint32_t frameCount, bool
|
|
|
633
573
|
// directly, so an unpadded hash with a leading-zero nibble would look
|
|
634
574
|
// mismatched even when native and wasm-core state are bit-identical.
|
|
635
575
|
<< " checksum=" << std::hex << std::setw(16) << std::setfill('0')
|
|
636
|
-
<<
|
|
576
|
+
<< rundot::kinetix::mobile_probe::hashBox3dBodies(bodyData.data(), bodyCount)
|
|
577
|
+
<< std::dec << std::setfill(' ') << '\n';
|
|
578
|
+
return 0;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Wire encoding for both parity gates; mirrors core/tests/helpers/f64-bits.mjs.
|
|
582
|
+
// Results canonicalise NaN (payload is architecture-defined); args do not, since
|
|
583
|
+
// they must round-trip back to the same double.
|
|
584
|
+
std::string vectorBits(double value) {
|
|
585
|
+
if (std::isnan(value)) return "nan";
|
|
586
|
+
std::ostringstream out;
|
|
587
|
+
out << std::hex << std::setfill('0') << std::setw(16) << std::bit_cast<std::uint64_t>(value);
|
|
588
|
+
return out.str();
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
double vectorValue(const std::string& hex) {
|
|
592
|
+
return std::bit_cast<double>(static_cast<std::uint64_t>(std::stoull(hex, nullptr, 16)));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// stdin corpus, one line out per line in: "u <hex>" -> sin cos atan exp log;
|
|
596
|
+
// "b <hexY> <hexX>" -> atan2 hypot2. Results are 16-digit hex bit patterns.
|
|
597
|
+
// NaN prints as `nan` because its payload is architecture-defined (x86-64 SSE
|
|
598
|
+
// and ARM64 disagree) and IEEE-754 does not pin it; the JS side matches.
|
|
599
|
+
// Bench kernel for the cross-lane harness. Prints through vectorBits for the
|
|
600
|
+
// same reason the parity emitters do: a decimal digest would drag ECMAScript
|
|
601
|
+
// number formatting into a cross-lane comparison.
|
|
602
|
+
int runBenchFixedKernel(int iterations) {
|
|
603
|
+
namespace fx = rundot::fixedmath;
|
|
604
|
+
const fx::Config cfg = fx::defaultConfig();
|
|
605
|
+
bool ok = true;
|
|
606
|
+
fx::Vec3 p{0, 0, 0};
|
|
607
|
+
fx::Vec3 v{1000, 2000, -500};
|
|
608
|
+
double acc = 0;
|
|
609
|
+
// Timed inside the process so the ~10ms spawn stays out of the measurement.
|
|
610
|
+
const auto started = std::chrono::steady_clock::now();
|
|
611
|
+
for (int i = 0; i < iterations; i += 1) {
|
|
612
|
+
p = fx::vec3Add(cfg, p, fx::vec3Scale(cfg, v, 990, ok), ok);
|
|
613
|
+
acc += fx::vec3Length(cfg, p, ok);
|
|
614
|
+
v = fx::vec3Scale(cfg, v, 999, ok);
|
|
615
|
+
if (p.x > 2000000000.0) p = fx::Vec3{0, 0, 0};
|
|
616
|
+
}
|
|
617
|
+
const auto elapsed = std::chrono::steady_clock::now() - started;
|
|
618
|
+
std::cout << vectorBits(acc) << ' '
|
|
619
|
+
<< std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count() << '\n';
|
|
620
|
+
return ok ? 0 : 1;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
int runMathVectors() {
|
|
624
|
+
std::string line;
|
|
625
|
+
while (std::getline(std::cin, line)) {
|
|
626
|
+
if (line.empty()) continue;
|
|
627
|
+
std::istringstream fields(line);
|
|
628
|
+
std::string kind;
|
|
629
|
+
fields >> kind;
|
|
630
|
+
if (kind == "u") {
|
|
631
|
+
std::string bits;
|
|
632
|
+
fields >> bits;
|
|
633
|
+
const double value = vectorValue(bits);
|
|
634
|
+
std::cout << vectorBits(rundot::detmath::det_sin(value)) << ' '
|
|
635
|
+
<< vectorBits(rundot::detmath::det_cos(value)) << ' '
|
|
636
|
+
<< vectorBits(rundot::detmath::det_atan(value)) << ' '
|
|
637
|
+
<< vectorBits(rundot::detmath::det_exp(value)) << ' '
|
|
638
|
+
<< vectorBits(rundot::detmath::det_log(value)) << '\n';
|
|
639
|
+
} else if (kind == "b") {
|
|
640
|
+
std::string yBits;
|
|
641
|
+
std::string xBits;
|
|
642
|
+
fields >> yBits >> xBits;
|
|
643
|
+
const double y = vectorValue(yBits);
|
|
644
|
+
const double x = vectorValue(xBits);
|
|
645
|
+
std::cout << vectorBits(rundot::detmath::det_atan2(y, x)) << ' '
|
|
646
|
+
<< vectorBits(rundot::detmath::det_hypot2(y, x)) << '\n';
|
|
647
|
+
} else {
|
|
648
|
+
std::cerr << "KINETIX_MATH_VECTORS_BAD_KIND " << kind << '\n';
|
|
649
|
+
return 1;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return 0;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// stdin corpus for the fixed-point twin, one line out per line in:
|
|
656
|
+
// "<op> <arg>..." where vector args are flattened. Args and results are all
|
|
657
|
+
// 16-digit hex f64 bit patterns — decimal would drag ECMAScript number
|
|
658
|
+
// formatting into the gate and truncate clamp's non-integer results. Output is
|
|
659
|
+
// one pattern, a comma-separated list for vectors/quaternions, `nan`, or ERR
|
|
660
|
+
// where the TypeScript reference throws.
|
|
661
|
+
int runFixedVectors() {
|
|
662
|
+
namespace fx = rundot::fixedmath;
|
|
663
|
+
const fx::Config cfg = fx::defaultConfig();
|
|
664
|
+
// Widened bounds so an over-2^53 mul result is not saturated, which is the
|
|
665
|
+
// only way the double-rounding semantic is observable. See fixed_math.cpp.
|
|
666
|
+
const fx::Config wide{1000.0, -4611686018427387904.0, 4611686018427387904.0};
|
|
667
|
+
|
|
668
|
+
std::string line;
|
|
669
|
+
while (std::getline(std::cin, line)) {
|
|
670
|
+
if (line.empty()) continue;
|
|
671
|
+
std::istringstream fields(line);
|
|
672
|
+
std::string op;
|
|
673
|
+
fields >> op;
|
|
674
|
+
|
|
675
|
+
std::vector<double> a;
|
|
676
|
+
std::string token;
|
|
677
|
+
while (fields >> token) {
|
|
678
|
+
a.push_back(vectorValue(token));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Guards the a[0]..a[8] indexing below. A short line is a harness bug, not
|
|
682
|
+
// a parity result: fail loudly rather than read past the end.
|
|
683
|
+
const auto arity = [&](std::size_t n) {
|
|
684
|
+
if (a.size() >= n) return true;
|
|
685
|
+
std::cerr << "KINETIX_FIXED_VECTORS_BAD_ARITY op=" << op
|
|
686
|
+
<< " want=" << n << " got=" << a.size() << '\n';
|
|
687
|
+
return false;
|
|
688
|
+
};
|
|
689
|
+
std::size_t want = 0;
|
|
690
|
+
if (op == "remap") want = 5;
|
|
691
|
+
else if (op == "quatLerp") want = 9;
|
|
692
|
+
else if (op == "vec3Lerp") want = 7;
|
|
693
|
+
else if (op == "vec3Add" || op == "vec3Sub" || op == "vec3Dot" || op == "vec3Cross" ||
|
|
694
|
+
op == "vec3Distance") want = 6;
|
|
695
|
+
else if (op == "vec2Lerp") want = 5;
|
|
696
|
+
else if (op == "vec3Scale" || op == "vec2Add" || op == "vec2Sub" || op == "vec2Dot" ||
|
|
697
|
+
op == "vec2Cross" || op == "vec2DistanceSq" || op == "vec2Distance") want = 4;
|
|
698
|
+
else if (op == "lerp" || op == "inverseLerp" || op == "smoothstep" || op == "clamp" ||
|
|
699
|
+
op == "vec3" || op == "vec2Rotate" || op == "vec2Scale" || op == "vec3LengthSq" ||
|
|
700
|
+
op == "vec3Length" || op == "vec3Normalize" || op == "vec3Negate") want = 3;
|
|
701
|
+
else if (op == "fromFixed" || op == "toFixed" || op == "sqrt" || op == "integerSqrt" ||
|
|
702
|
+
op == "abs" || op == "sign" || op == "log2Fixed" || op == "exp" || op == "log" ||
|
|
703
|
+
op == "sinTurns" || op == "cosTurns" || op == "tanTurns" || op == "wrapTurns" ||
|
|
704
|
+
op == "turnsToRadians" || op == "radiansToTurns" || op == "quatFromYawTurns") want = 1;
|
|
705
|
+
else want = 2;
|
|
706
|
+
if (!arity(want)) return 1;
|
|
707
|
+
|
|
708
|
+
bool ok = true;
|
|
709
|
+
std::string out;
|
|
710
|
+
const auto scalar = [&](double v) { out = vectorBits(v); };
|
|
711
|
+
const auto v2 = [&](fx::Vec2 v) { out = vectorBits(v.x) + "," + vectorBits(v.y); };
|
|
712
|
+
const auto v3 = [&](fx::Vec3 v) { out = vectorBits(v.x) + "," + vectorBits(v.y) + "," + vectorBits(v.z); };
|
|
713
|
+
|
|
714
|
+
if (op == "toFixed") scalar(fx::toFixed(cfg, a[0], ok));
|
|
715
|
+
else if (op == "fromFixed") scalar(fx::fromFixed(cfg, a[0], ok));
|
|
716
|
+
else if (op == "add") scalar(fx::add(cfg, a[0], a[1], ok));
|
|
717
|
+
else if (op == "sub") scalar(fx::sub(cfg, a[0], a[1], ok));
|
|
718
|
+
else if (op == "mul") scalar(fx::mul(cfg, a[0], a[1], ok));
|
|
719
|
+
else if (op == "mulWide") scalar(fx::mul(wide, a[0], a[1], ok));
|
|
720
|
+
else if (op == "div") scalar(fx::div(cfg, a[0], a[1], ok));
|
|
721
|
+
else if (op == "saturatingAdd") scalar(fx::saturatingAdd(cfg, a[0], a[1], ok));
|
|
722
|
+
else if (op == "saturatingSub") scalar(fx::saturatingSub(cfg, a[0], a[1], ok));
|
|
723
|
+
else if (op == "quantize") scalar(fx::quantize(cfg, a[0], a[1], ok));
|
|
724
|
+
else if (op == "sqrt") scalar(fx::sqrtFixed(cfg, a[0], ok));
|
|
725
|
+
else if (op == "integerSqrt") scalar(fx::integerSqrt(cfg, a[0], ok));
|
|
726
|
+
else if (op == "abs") scalar(fx::absFixed(cfg, a[0], ok));
|
|
727
|
+
else if (op == "sign") scalar(fx::sign(cfg, a[0], ok));
|
|
728
|
+
else if (op == "min") scalar(fx::minFixed(cfg, a[0], a[1], ok));
|
|
729
|
+
else if (op == "max") scalar(fx::maxFixed(cfg, a[0], a[1], ok));
|
|
730
|
+
else if (op == "lerp") scalar(fx::lerp(cfg, a[0], a[1], a[2], ok));
|
|
731
|
+
else if (op == "mod") scalar(fx::mod(cfg, a[0], a[1], ok));
|
|
732
|
+
else if (op == "inverseLerp") scalar(fx::inverseLerp(cfg, a[0], a[1], a[2], ok));
|
|
733
|
+
else if (op == "remap") scalar(fx::remap(cfg, a[0], a[1], a[2], a[3], a[4], ok));
|
|
734
|
+
else if (op == "smoothstep") scalar(fx::smoothstep(cfg, a[0], a[1], a[2], ok));
|
|
735
|
+
else if (op == "clamp") scalar(fx::clamp(cfg, a[0], a[1], a[2], ok));
|
|
736
|
+
else if (op == "log2Fixed") scalar(fx::log2Fixed(cfg, a[0], ok));
|
|
737
|
+
else if (op == "exp") scalar(fx::expFixed(cfg, a[0], ok));
|
|
738
|
+
else if (op == "log") scalar(fx::logFixed(cfg, a[0], ok));
|
|
739
|
+
else if (op == "sinTurns") scalar(fx::sinTurns(cfg, a[0], ok));
|
|
740
|
+
else if (op == "cosTurns") scalar(fx::cosTurns(cfg, a[0], ok));
|
|
741
|
+
else if (op == "tanTurns") scalar(fx::tanTurns(cfg, a[0], ok));
|
|
742
|
+
else if (op == "atan2") scalar(fx::atan2Turns(cfg, a[0], a[1], ok));
|
|
743
|
+
else if (op == "atan2Signed") scalar(fx::atan2Signed(cfg, a[0], a[1], ok));
|
|
744
|
+
else if (op == "wrapTurns") scalar(fx::wrapTurns(cfg, a[0], ok));
|
|
745
|
+
else if (op == "angleDeltaTurns") scalar(fx::angleDeltaTurns(cfg, a[0], a[1], ok));
|
|
746
|
+
else if (op == "turnsToRadians") scalar(fx::turnsToRadians(cfg, a[0], ok));
|
|
747
|
+
else if (op == "radiansToTurns") scalar(fx::radiansToTurns(cfg, a[0], ok));
|
|
748
|
+
else if (op == "hypot") scalar(fx::hypot(cfg, a[0], a[1], ok));
|
|
749
|
+
else if (op == "vec2") v2(fx::vec2(cfg, a[0], a[1], ok));
|
|
750
|
+
else if (op == "vec2Add") v2(fx::vec2Add(cfg, {a[0], a[1]}, {a[2], a[3]}, ok));
|
|
751
|
+
else if (op == "vec2Sub") v2(fx::vec2Sub(cfg, {a[0], a[1]}, {a[2], a[3]}, ok));
|
|
752
|
+
else if (op == "vec2Scale") v2(fx::vec2Scale(cfg, {a[0], a[1]}, a[2], ok));
|
|
753
|
+
else if (op == "vec2Dot") scalar(fx::vec2Dot(cfg, {a[0], a[1]}, {a[2], a[3]}, ok));
|
|
754
|
+
else if (op == "vec2Cross") scalar(fx::vec2Cross(cfg, {a[0], a[1]}, {a[2], a[3]}, ok));
|
|
755
|
+
else if (op == "vec2LengthSq") scalar(fx::vec2LengthSq(cfg, {a[0], a[1]}, ok));
|
|
756
|
+
else if (op == "vec2Length") scalar(fx::vec2Length(cfg, {a[0], a[1]}, ok));
|
|
757
|
+
else if (op == "vec2DistanceSq") scalar(fx::vec2DistanceSq(cfg, {a[0], a[1]}, {a[2], a[3]}, ok));
|
|
758
|
+
else if (op == "vec2Distance") scalar(fx::vec2Distance(cfg, {a[0], a[1]}, {a[2], a[3]}, ok));
|
|
759
|
+
else if (op == "vec2Normalize") v2(fx::vec2Normalize(cfg, {a[0], a[1]}, ok));
|
|
760
|
+
else if (op == "vec2Perp") v2(fx::vec2Perp(cfg, {a[0], a[1]}, ok));
|
|
761
|
+
else if (op == "vec2Negate") v2(fx::vec2Negate(cfg, {a[0], a[1]}, ok));
|
|
762
|
+
else if (op == "vec2Rotate") v2(fx::vec2Rotate(cfg, {a[0], a[1]}, a[2], ok));
|
|
763
|
+
else if (op == "vec2FromAngleTurns") v2(fx::vec2FromAngleTurns(cfg, a[0], a[1], ok));
|
|
764
|
+
else if (op == "vec2AngleTurns") scalar(fx::vec2AngleTurns(cfg, {a[0], a[1]}, ok));
|
|
765
|
+
else if (op == "vec2Lerp") v2(fx::vec2Lerp(cfg, {a[0], a[1]}, {a[2], a[3]}, a[4], ok));
|
|
766
|
+
else if (op == "vec3") v3(fx::vec3(cfg, a[0], a[1], a[2], ok));
|
|
767
|
+
else if (op == "vec3Add") v3(fx::vec3Add(cfg, {a[0], a[1], a[2]}, {a[3], a[4], a[5]}, ok));
|
|
768
|
+
else if (op == "vec3Sub") v3(fx::vec3Sub(cfg, {a[0], a[1], a[2]}, {a[3], a[4], a[5]}, ok));
|
|
769
|
+
else if (op == "vec3Scale") v3(fx::vec3Scale(cfg, {a[0], a[1], a[2]}, a[3], ok));
|
|
770
|
+
else if (op == "vec3Dot") scalar(fx::vec3Dot(cfg, {a[0], a[1], a[2]}, {a[3], a[4], a[5]}, ok));
|
|
771
|
+
else if (op == "vec3Cross") v3(fx::vec3Cross(cfg, {a[0], a[1], a[2]}, {a[3], a[4], a[5]}, ok));
|
|
772
|
+
else if (op == "vec3LengthSq") scalar(fx::vec3LengthSq(cfg, {a[0], a[1], a[2]}, ok));
|
|
773
|
+
else if (op == "vec3Length") scalar(fx::vec3Length(cfg, {a[0], a[1], a[2]}, ok));
|
|
774
|
+
else if (op == "vec3Distance") scalar(fx::vec3Distance(cfg, {a[0], a[1], a[2]}, {a[3], a[4], a[5]}, ok));
|
|
775
|
+
else if (op == "vec3Normalize") v3(fx::vec3Normalize(cfg, {a[0], a[1], a[2]}, ok));
|
|
776
|
+
else if (op == "vec3Lerp") v3(fx::vec3Lerp(cfg, {a[0], a[1], a[2]}, {a[3], a[4], a[5]}, a[6], ok));
|
|
777
|
+
else if (op == "vec3Negate") v3(fx::vec3Negate(cfg, {a[0], a[1], a[2]}, ok));
|
|
778
|
+
else if (op == "quatFromYawTurns") {
|
|
779
|
+
const fx::Quat q = fx::quatFromYawTurns(cfg, a[0], ok);
|
|
780
|
+
out = vectorBits(q.x) + "," + vectorBits(q.y) + "," + vectorBits(q.z) + "," + vectorBits(q.w);
|
|
781
|
+
} else if (op == "quatLerp") {
|
|
782
|
+
const fx::Quat q = fx::quatLerp(cfg, {a[0], a[1], a[2], a[3]}, {a[4], a[5], a[6], a[7]}, a[8], ok);
|
|
783
|
+
out = vectorBits(q.x) + "," + vectorBits(q.y) + "," + vectorBits(q.z) + "," + vectorBits(q.w);
|
|
784
|
+
} else {
|
|
785
|
+
std::cerr << "KINETIX_FIXED_VECTORS_UNKNOWN_OP " << op << '\n';
|
|
786
|
+
return 1;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
std::cout << (ok ? out : std::string("ERR")) << '\n';
|
|
790
|
+
}
|
|
637
791
|
return 0;
|
|
638
792
|
}
|
|
639
793
|
|
|
@@ -641,28 +795,13 @@ int runPhysicsBenchmark(std::uint32_t bodyCount, std::uint32_t frameCount, bool
|
|
|
641
795
|
|
|
642
796
|
int main(int argc, char** argv) {
|
|
643
797
|
if (argc == 2 && std::string(argv[1]) == "--math-vectors") {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
0x400921fb54442d18ULL, 0x4010000000000000ULL,
|
|
652
|
-
0x4014000000000000ULL, 0x4024000000000000ULL,
|
|
653
|
-
};
|
|
654
|
-
for (const std::uint64_t input_bits : inputs) {
|
|
655
|
-
const double value = std::bit_cast<double>(input_bits);
|
|
656
|
-
const auto bits = [](double result) { return std::bit_cast<std::uint64_t>(result); };
|
|
657
|
-
std::cout << std::hex << std::setfill('0')
|
|
658
|
-
<< std::setw(16) << input_bits << ' '
|
|
659
|
-
<< std::setw(16) << bits(rundot::detmath::det_sin(value)) << ' '
|
|
660
|
-
<< std::setw(16) << bits(rundot::detmath::det_cos(value)) << ' '
|
|
661
|
-
<< std::setw(16) << bits(rundot::detmath::det_atan2(value, value + 0.25)) << ' '
|
|
662
|
-
<< std::setw(16) << bits(rundot::detmath::det_exp(value)) << ' '
|
|
663
|
-
<< std::setw(16) << bits(rundot::detmath::det_log(value)) << '\n';
|
|
664
|
-
}
|
|
665
|
-
return 0;
|
|
798
|
+
return runMathVectors();
|
|
799
|
+
}
|
|
800
|
+
if (argc == 2 && std::string(argv[1]) == "--fixed-vectors") {
|
|
801
|
+
return runFixedVectors();
|
|
802
|
+
}
|
|
803
|
+
if (argc == 3 && std::string(argv[1]) == "--bench-fixed-kernel") {
|
|
804
|
+
return runBenchFixedKernel(std::stoi(argv[2]));
|
|
666
805
|
}
|
|
667
806
|
if (argc == 3 && std::string(argv[1]) == "--physics-vectors") {
|
|
668
807
|
return runPhysicsVectors(argv[2]);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#include "box3d_probe_scenario.hpp"
|
|
2
|
+
|
|
3
|
+
#include "../physics/physics.hpp"
|
|
4
|
+
|
|
5
|
+
#include <bit>
|
|
6
|
+
#include <limits>
|
|
7
|
+
#include <stdexcept>
|
|
8
|
+
|
|
9
|
+
namespace rundot::kinetix::mobile_probe {
|
|
10
|
+
|
|
11
|
+
namespace {
|
|
12
|
+
|
|
13
|
+
static_assert(sizeof(float) == sizeof(std::uint32_t));
|
|
14
|
+
static_assert(std::numeric_limits<float>::is_iec559);
|
|
15
|
+
|
|
16
|
+
constexpr double kSphereMass = 3.14159265358979311599796346854 / 6.0;
|
|
17
|
+
|
|
18
|
+
void setCommonFields(std::vector<double>& bodyData, std::size_t offset, std::uint32_t motion) {
|
|
19
|
+
bodyData[offset + 9] = static_cast<double>(motion);
|
|
20
|
+
bodyData[offset + 10] = 0.0;
|
|
21
|
+
bodyData[offset + 11] = 1.0;
|
|
22
|
+
bodyData[offset + 12] = 1.0;
|
|
23
|
+
bodyData[offset + 13] = 1.0;
|
|
24
|
+
bodyData[offset + 14] = 1.0;
|
|
25
|
+
bodyData[offset + 18] = 1.0;
|
|
26
|
+
bodyData[offset + 25] = 1.0;
|
|
27
|
+
bodyData[offset + 26] = 1.0;
|
|
28
|
+
bodyData[offset + 27] = 0.6;
|
|
29
|
+
bodyData[offset + 31] = 1.0;
|
|
30
|
+
bodyData[offset + 32] = 1.0;
|
|
31
|
+
bodyData[offset + 33] = 1.0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
void hashFloat32(std::uint64_t& hash, double value) {
|
|
35
|
+
constexpr std::uint64_t prime = 1099511628211ULL;
|
|
36
|
+
const float narrowed = static_cast<float>(value);
|
|
37
|
+
hash ^= static_cast<std::uint64_t>(std::bit_cast<std::uint32_t>(narrowed));
|
|
38
|
+
hash *= prime;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
} // namespace
|
|
42
|
+
|
|
43
|
+
std::vector<double> makeBox3dBodies(std::uint32_t bodyCount) {
|
|
44
|
+
if (bodyCount < 2 || bodyCount > 100000) {
|
|
45
|
+
throw std::invalid_argument("SYNCPLAY_BOX3D_BODY_COUNT_INVALID");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
std::vector<double> bodyData(static_cast<std::size_t>(bodyCount) * kBodyStride, 0.0);
|
|
49
|
+
setCommonFields(bodyData, 0, 0);
|
|
50
|
+
bodyData[0] = 0.0;
|
|
51
|
+
bodyData[1] = -32.5;
|
|
52
|
+
bodyData[10] = 1.0;
|
|
53
|
+
bodyData[6] = 32.5;
|
|
54
|
+
|
|
55
|
+
for (std::uint32_t index = 1; index < bodyCount; index += 1) {
|
|
56
|
+
const std::size_t offset = static_cast<std::size_t>(index) * kBodyStride;
|
|
57
|
+
setCommonFields(bodyData, offset, 1);
|
|
58
|
+
bodyData[offset + 10] = 1.0;
|
|
59
|
+
const std::uint32_t column = (index - 1) % 16;
|
|
60
|
+
const std::uint32_t row = (index - 1) / 16;
|
|
61
|
+
bodyData[offset + 0] = static_cast<double>(column) * 1.05 - 7.875;
|
|
62
|
+
bodyData[offset + 1] = 0.5 + static_cast<double>(row) * 1.05;
|
|
63
|
+
bodyData[offset + 2] = 0.0;
|
|
64
|
+
bodyData[offset + 3] = (column % 2 == 0) ? 0.1 : -0.1;
|
|
65
|
+
bodyData[offset + 6] = 0.5;
|
|
66
|
+
bodyData[offset + 18] = kSphereMass;
|
|
67
|
+
bodyData[offset + 31] = 0.1 * kSphereMass;
|
|
68
|
+
bodyData[offset + 32] = 0.1 * kSphereMass;
|
|
69
|
+
bodyData[offset + 33] = 0.1 * kSphereMass;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return bodyData;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
std::uint64_t hashBox3dBodies(const double* bodyData, std::uint32_t bodyCount) {
|
|
76
|
+
if (bodyData == nullptr || bodyCount < 2) return 0;
|
|
77
|
+
constexpr std::uint64_t offsetBasis = 1469598103934665603ULL;
|
|
78
|
+
std::uint64_t hash = offsetBasis;
|
|
79
|
+
for (std::uint32_t bodyIndex = 1; bodyIndex < bodyCount; bodyIndex += 1) {
|
|
80
|
+
const std::size_t offset = static_cast<std::size_t>(bodyIndex) * kBodyStride;
|
|
81
|
+
// x/y/z, linear velocity, angular velocity, and quaternion orientation.
|
|
82
|
+
for (const std::uint32_t field : {0U, 1U, 2U, 3U, 4U, 5U, 19U, 20U, 21U,
|
|
83
|
+
22U, 23U, 24U, 25U}) {
|
|
84
|
+
hashFloat32(hash, bodyData[offset + field]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return hash;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
void clearPhysicsGlobalState() {
|
|
91
|
+
using rundot::kinetix::physics::setContactCacheData;
|
|
92
|
+
using rundot::kinetix::physics::setDynamicTreeEnabled;
|
|
93
|
+
using rundot::kinetix::physics::setExternalForceData;
|
|
94
|
+
using rundot::kinetix::physics::setGeometryData;
|
|
95
|
+
setGeometryData(nullptr, 0);
|
|
96
|
+
setContactCacheData(nullptr, 0);
|
|
97
|
+
setExternalForceData(nullptr, 0);
|
|
98
|
+
setDynamicTreeEnabled(true);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
} // namespace rundot::kinetix::mobile_probe
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
#include <vector>
|
|
5
|
+
|
|
6
|
+
namespace rundot::kinetix::mobile_probe {
|
|
7
|
+
|
|
8
|
+
constexpr std::uint32_t kScenarioBox3dV1 = 1;
|
|
9
|
+
constexpr std::uint32_t kBodyCount = 128;
|
|
10
|
+
constexpr std::uint32_t kSmokeFrames = 120;
|
|
11
|
+
constexpr std::uint32_t kPerfFrames = 1200;
|
|
12
|
+
constexpr std::uint32_t kBodyStride = 44;
|
|
13
|
+
|
|
14
|
+
// The probe intentionally uses the same SoA record as the existing Kinetix
|
|
15
|
+
// physics ABI. The returned vector owns all state and is safe to hand to the
|
|
16
|
+
// physics stepper for the duration of one serialized probe call.
|
|
17
|
+
std::vector<double> makeBox3dBodies(std::uint32_t bodyCount = kBodyCount);
|
|
18
|
+
|
|
19
|
+
// Hash dynamic bodies in canonical body order. Every value is narrowed to an
|
|
20
|
+
// IEEE-754 binary32 before its little-endian bit pattern is mixed into FNV-1a.
|
|
21
|
+
std::uint64_t hashBox3dBodies(const double* bodyData, std::uint32_t bodyCount);
|
|
22
|
+
|
|
23
|
+
void clearPhysicsGlobalState();
|
|
24
|
+
|
|
25
|
+
} // namespace rundot::kinetix::mobile_probe
|