@series-inc/rundot-kinetix 0.0.0-bootstrap.0

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.
Files changed (58) hide show
  1. package/README.md +77 -0
  2. package/authoring.d.ts +30 -0
  3. package/index.d.ts +221 -0
  4. package/native/component_runtime.cpp +341 -0
  5. package/native/component_runtime.hpp +112 -0
  6. package/native/deterministic_math.cpp +594 -0
  7. package/native/deterministic_math.hpp +21 -0
  8. package/native/kinetix-f64-native-profile.cpp +406 -0
  9. package/native/kinetix-f64-native-profile.hpp +5 -0
  10. package/native/kinetix-fixed1000-native-profile.cpp +777 -0
  11. package/native/kinetix-fixed1000-native-profile.hpp +58 -0
  12. package/native/kinetix-fixed1000-physics.cpp +887 -0
  13. package/native/kinetix-fixed1000-physics.hpp +28 -0
  14. package/native/kinetix-installed-f64-renderer.cpp +344 -0
  15. package/native/kinetix-installed-f64-renderer.hpp +35 -0
  16. package/native/kinetix-installed-f64-runtime.cpp +1085 -0
  17. package/native/kinetix-installed-f64-runtime.hpp +141 -0
  18. package/native/kinetix-installed-fixed1000-data.hpp +77 -0
  19. package/native/kinetix-native-main.cpp +37 -0
  20. package/native/kinetix-native-runtime.cpp +20 -0
  21. package/native/kinetix-native-runtime.hpp +25 -0
  22. package/native/kinetix-render-projection.cpp +20 -0
  23. package/native/kinetix-render-projection.hpp +14 -0
  24. package/package.json +65 -0
  25. package/runtime.d.ts +76 -0
  26. package/scripts/build-native.mjs +67 -0
  27. package/scripts/emit-product-digests.mjs +33 -0
  28. package/scripts/preflight.mjs +76 -0
  29. package/src/index.mjs +57 -0
  30. package/src/kinetix-authoring.mjs +69 -0
  31. package/src/kinetix-baker.mjs +587 -0
  32. package/src/kinetix-deterministic-math.mjs +1044 -0
  33. package/src/kinetix-fixed1000-runtime-adapter.mjs +33 -0
  34. package/src/kinetix-fixed1000-runtime.mjs +954 -0
  35. package/src/kinetix-installed-f64-reference.mjs +157 -0
  36. package/src/kinetix-installed-f64-render-frames.mjs +53 -0
  37. package/src/kinetix-installed-f64-renderer.mjs +240 -0
  38. package/src/kinetix-installed-f64-runtime-adapter.mjs +68 -0
  39. package/src/kinetix-installed-f64-runtime.mjs +607 -0
  40. package/src/kinetix-installed-mechanics.mjs +377 -0
  41. package/src/kinetix-installed-systems.mjs +181 -0
  42. package/src/kinetix-native-product.mjs +72 -0
  43. package/src/kinetix-product-contract.mjs +121 -0
  44. package/src/kinetix-project-compiler.mjs +1017 -0
  45. package/src/kinetix-render-projection.mjs +28 -0
  46. package/src/kinetix-runtime-adapter-utils.mjs +168 -0
  47. package/src/kinetix-runtime-contract.mjs +54 -0
  48. package/src/kinetix-session-config.mjs +170 -0
  49. package/src/kinetix-source-snapshot.mjs +24 -0
  50. package/src/kinetix-survival-runtime-adapter.mjs +305 -0
  51. package/src/kinetix-survival-runtime.generated.mjs +1 -0
  52. package/src/kinetix-world-kernel.mjs +580 -0
  53. package/src/kinetix-world-runtime.mjs +171 -0
  54. package/src/runtime.mjs +14 -0
  55. package/src/shared/f64-bits.mjs +14 -0
  56. package/src/shared/kinetix-envelope-v1.mjs +589 -0
  57. package/src/shared/render-records-v1.mjs +168 -0
  58. package/src/shared/sha256.mjs +73 -0
@@ -0,0 +1,28 @@
1
+ // C++20 transliteration of the syncplay deterministic 3D physics slice used by
2
+ // by the installed fixed-1000 profile: dynamic spheres plus static boxes, one TGS-Soft
3
+ // step (dt=1, gravity 0, velocityIterations 3, substeps 6), no materials, no
4
+ // orientation, no joints, no warm cache, no CCD.
5
+ //
6
+ // Source of truth: venus packages/syncplay/src/physics3d{,-solver,-shared}.ts.
7
+ // Every arithmetic expression is ported verbatim (same IEEE-754 operation
8
+ // order); compile with -ffp-contract=off and doubles throughout.
9
+ #pragma once
10
+
11
+ #include <string>
12
+ #include <vector>
13
+
14
+ struct PhysBody {
15
+ std::string id;
16
+ bool dynamic;
17
+ double x, y, z, vx, vy, vz;
18
+ bool isSphere;
19
+ double radius;
20
+ double halfX, halfY, halfZ;
21
+ int layer;
22
+ unsigned mask;
23
+ };
24
+
25
+ // Sorts `bodies` by id lexicographically (canonical body order), runs one
26
+ // deterministic rigid step, and writes back quantized positions + velocities
27
+ // to dynamic bodies (statics untouched, apart from the sort).
28
+ void stepPhysicsSlice(std::vector<PhysBody>& bodies);
@@ -0,0 +1,344 @@
1
+ // C++ transliteration of parity-renderer-f64.mjs. Mirrors the JS renderer
2
+ // exactly: same draw order, same loop bounds (Math.floor/ceil on doubles),
3
+ // same pixel-center math (x + 0.5), same comparison operators, same colors,
4
+ // same integer (a + b) >> 1 mix. Frames must be byte-identical to the Node
5
+ // reference dump.
6
+ //
7
+ // The runtime's entity model lives in an anonymous namespace inside
8
+ // the installed runtime source; renaming its sole external-linkage symbol lets
9
+ // this TU include the runtime source verbatim (bit-identical sim, no
10
+ // duplicate symbol) and read Runtime/Entity/Component directly.
11
+ #define runReplayF64 runReplayF64RendererCopyUnused
12
+ #define RUNDOT_F64RT_OMIT_SIM_STEPPER
13
+ #include "kinetix-installed-f64-runtime.cpp"
14
+ #undef RUNDOT_F64RT_OMIT_SIM_STEPPER
15
+ #undef runReplayF64
16
+
17
+ #include <algorithm>
18
+ #include <cstdio>
19
+ #include <fstream>
20
+
21
+ #include "kinetix-installed-f64-renderer.hpp"
22
+
23
+ namespace rundot::f64rt {
24
+ namespace {
25
+
26
+ constexpr int FRAME_WIDTH = kFrameWidth;
27
+ constexpr int FRAME_HEIGHT = kFrameHeight;
28
+
29
+ struct Rgb {
30
+ std::uint8_t r = 0;
31
+ std::uint8_t g = 0;
32
+ std::uint8_t b = 0;
33
+ };
34
+
35
+ constexpr Rgb kBackground{10, 10, 18};
36
+ constexpr Rgb kGrid{20, 20, 40};
37
+ constexpr Rgb kBorder{42, 42, 85};
38
+ constexpr Rgb kShip{102, 255, 238};
39
+ constexpr Rgb kShipShield{220, 240, 255};
40
+ constexpr Rgb kBulletCore{255, 243, 199};
41
+ constexpr Rgb kBulletGlow{138, 122, 64};
42
+ constexpr Rgb kWanderer{176, 64, 255};
43
+ constexpr Rgb kShard{207, 122, 255};
44
+ constexpr Rgb kGrunt{170, 255, 51};
45
+ constexpr Rgb kTelegraph{90, 90, 140};
46
+ constexpr Rgb kScoreBar{255, 216, 77};
47
+ constexpr Rgb kDeathPip{255, 80, 80};
48
+
49
+ void putPixel(std::uint8_t* frame, int x, int y, const Rgb& color) {
50
+ if (x < 0 || y < 0 || x >= FRAME_WIDTH || y >= FRAME_HEIGHT) return;
51
+ const std::size_t offset = (static_cast<std::size_t>(y) * FRAME_WIDTH + static_cast<std::size_t>(x)) * 3;
52
+ frame[offset] = color.r;
53
+ frame[offset + 1] = color.g;
54
+ frame[offset + 2] = color.b;
55
+ }
56
+
57
+ void mixPixel(std::uint8_t* frame, int x, int y, const Rgb& color) {
58
+ if (x < 0 || y < 0 || x >= FRAME_WIDTH || y >= FRAME_HEIGHT) return;
59
+ const std::size_t offset = (static_cast<std::size_t>(y) * FRAME_WIDTH + static_cast<std::size_t>(x)) * 3;
60
+ frame[offset] = static_cast<std::uint8_t>((frame[offset] + color.r) >> 1);
61
+ frame[offset + 1] = static_cast<std::uint8_t>((frame[offset + 1] + color.g) >> 1);
62
+ frame[offset + 2] = static_cast<std::uint8_t>((frame[offset + 2] + color.b) >> 1);
63
+ }
64
+
65
+ void fillCircle(std::uint8_t* frame, double cx, double cy, double radius, const Rgb& color) {
66
+ const int x0 = static_cast<int>(std::max(0.0, std::floor(cx - radius)));
67
+ const int x1 = static_cast<int>(std::min(static_cast<double>(FRAME_WIDTH - 1), std::ceil(cx + radius)));
68
+ const int y0 = static_cast<int>(std::max(0.0, std::floor(cy - radius)));
69
+ const int y1 = static_cast<int>(std::min(static_cast<double>(FRAME_HEIGHT - 1), std::ceil(cy + radius)));
70
+ const double r2 = radius * radius;
71
+ for (int y = y0; y <= y1; y += 1) {
72
+ for (int x = x0; x <= x1; x += 1) {
73
+ const double dx = x + 0.5 - cx;
74
+ const double dy = y + 0.5 - cy;
75
+ if (dx * dx + dy * dy <= r2) putPixel(frame, x, y, color);
76
+ }
77
+ }
78
+ }
79
+
80
+ void ring(std::uint8_t* frame, double cx, double cy, double innerRadius, double outerRadius, const Rgb& color) {
81
+ const int x0 = static_cast<int>(std::max(0.0, std::floor(cx - outerRadius)));
82
+ const int x1 = static_cast<int>(std::min(static_cast<double>(FRAME_WIDTH - 1), std::ceil(cx + outerRadius)));
83
+ const int y0 = static_cast<int>(std::max(0.0, std::floor(cy - outerRadius)));
84
+ const int y1 = static_cast<int>(std::min(static_cast<double>(FRAME_HEIGHT - 1), std::ceil(cy + outerRadius)));
85
+ const double inner2 = innerRadius * innerRadius;
86
+ const double outer2 = outerRadius * outerRadius;
87
+ for (int y = y0; y <= y1; y += 1) {
88
+ for (int x = x0; x <= x1; x += 1) {
89
+ const double dx = x + 0.5 - cx;
90
+ const double dy = y + 0.5 - cy;
91
+ const double d2 = dx * dx + dy * dy;
92
+ if (d2 >= inner2 && d2 <= outer2) putPixel(frame, x, y, color);
93
+ }
94
+ }
95
+ }
96
+
97
+ void fillDiamond(std::uint8_t* frame, double cx, double cy, double size, const Rgb& color) {
98
+ const int x0 = static_cast<int>(std::max(0.0, std::floor(cx - size)));
99
+ const int x1 = static_cast<int>(std::min(static_cast<double>(FRAME_WIDTH - 1), std::ceil(cx + size)));
100
+ const int y0 = static_cast<int>(std::max(0.0, std::floor(cy - size)));
101
+ const int y1 = static_cast<int>(std::min(static_cast<double>(FRAME_HEIGHT - 1), std::ceil(cy + size)));
102
+ for (int y = y0; y <= y1; y += 1) {
103
+ for (int x = x0; x <= x1; x += 1) {
104
+ const double dx = x + 0.5 - cx;
105
+ const double dy = y + 0.5 - cy;
106
+ const double adx = dx < 0 ? -dx : dx;
107
+ const double ady = dy < 0 ? -dy : dy;
108
+ if (adx + ady <= size) putPixel(frame, x, y, color);
109
+ }
110
+ }
111
+ }
112
+
113
+ double edge(double ax, double ay, double bx, double by, double px, double py) {
114
+ return (bx - ax) * (py - ay) - (by - ay) * (px - ax);
115
+ }
116
+
117
+ void fillTriangle(std::uint8_t* frame, double ax, double ay, double bx, double by, double cx, double cy, const Rgb& color) {
118
+ const int minX = static_cast<int>(std::max(0.0, std::floor(std::min(std::min(ax, bx), cx))));
119
+ const int maxX = static_cast<int>(std::min(static_cast<double>(FRAME_WIDTH - 1), std::ceil(std::max(std::max(ax, bx), cx))));
120
+ const int minY = static_cast<int>(std::max(0.0, std::floor(std::min(std::min(ay, by), cy))));
121
+ const int maxY = static_cast<int>(std::min(static_cast<double>(FRAME_HEIGHT - 1), std::ceil(std::max(std::max(ay, by), cy))));
122
+ const double area = edge(ax, ay, bx, by, cx, cy);
123
+ if (area == 0) return;
124
+ for (int y = minY; y <= maxY; y += 1) {
125
+ for (int x = minX; x <= maxX; x += 1) {
126
+ const double px = x + 0.5;
127
+ const double py = y + 0.5;
128
+ const double w0 = edge(ax, ay, bx, by, px, py);
129
+ const double w1 = edge(bx, by, cx, cy, px, py);
130
+ const double w2 = edge(cx, cy, ax, ay, px, py);
131
+ if (area > 0 ? w0 >= 0 && w1 >= 0 && w2 >= 0 : w0 <= 0 && w1 <= 0 && w2 <= 0) {
132
+ putPixel(frame, x, y, color);
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ const Component* componentOf(const Entity& entity, Kind kind) {
139
+ const int index = entity.byKind[static_cast<std::size_t>(kind)];
140
+ return index < 0 ? nullptr : &entity.components[static_cast<std::size_t>(index)];
141
+ }
142
+
143
+ // orderedEntities(): statics in pack order (alive only) then alive pooled
144
+ // sorted by ascending spawnId — a filtered copy, sim state untouched.
145
+ std::vector<const Entity*> orderedEntities(const Runtime& runtime) {
146
+ std::vector<const Entity*> ordered;
147
+ ordered.reserve(runtime.statics.size() + runtime.pooled.size());
148
+ for (const Entity& entity : runtime.statics) {
149
+ if (entity.alive) ordered.push_back(&entity);
150
+ }
151
+ std::vector<const Entity*> pooledAlive;
152
+ pooledAlive.reserve(runtime.pooled.size());
153
+ for (const Entity& entity : runtime.pooled) {
154
+ if (entity.alive) pooledAlive.push_back(&entity);
155
+ }
156
+ std::sort(pooledAlive.begin(), pooledAlive.end(),
157
+ [](const Entity* a, const Entity* b) { return a->spawnId < b->spawnId; });
158
+ ordered.insert(ordered.end(), pooledAlive.begin(), pooledAlive.end());
159
+ return ordered;
160
+ }
161
+
162
+ void renderF64Frame(const Runtime& runtime, std::uint8_t* frame) {
163
+ // background
164
+ const std::size_t frameBytes = static_cast<std::size_t>(FRAME_WIDTH) * FRAME_HEIGHT * 3;
165
+ for (std::size_t offset = 0; offset < frameBytes; offset += 3) {
166
+ frame[offset] = kBackground.r;
167
+ frame[offset + 1] = kBackground.g;
168
+ frame[offset + 2] = kBackground.b;
169
+ }
170
+ // grid every 80 px
171
+ for (int x = 0; x < FRAME_WIDTH; x += 80) {
172
+ for (int y = 0; y < FRAME_HEIGHT; y += 1) putPixel(frame, x, y, kGrid);
173
+ }
174
+ for (int y = 0; y < FRAME_HEIGHT; y += 80) {
175
+ for (int x = 0; x < FRAME_WIDTH; x += 1) putPixel(frame, x, y, kGrid);
176
+ }
177
+ // arena border (2 px)
178
+ for (int x = 0; x < FRAME_WIDTH; x += 1) {
179
+ putPixel(frame, x, 0, kBorder);
180
+ putPixel(frame, x, 1, kBorder);
181
+ putPixel(frame, x, FRAME_HEIGHT - 2, kBorder);
182
+ putPixel(frame, x, FRAME_HEIGHT - 1, kBorder);
183
+ }
184
+ for (int y = 0; y < FRAME_HEIGHT; y += 1) {
185
+ putPixel(frame, 0, y, kBorder);
186
+ putPixel(frame, 1, y, kBorder);
187
+ putPixel(frame, FRAME_WIDTH - 2, y, kBorder);
188
+ putPixel(frame, FRAME_WIDTH - 1, y, kBorder);
189
+ }
190
+
191
+ const std::vector<const Entity*> entities = orderedEntities(runtime);
192
+
193
+ // telegraph rings (behind bodies)
194
+ for (const Entity* entity : entities) {
195
+ const Component* telegraph = componentOf(*entity, Kind::Telegraph);
196
+ if (telegraph == nullptr || telegraph->remaining <= 0) continue;
197
+ const Component* transform = componentOf(*entity, Kind::Transform2D);
198
+ const Component* circle = componentOf(*entity, Kind::CollisionCircle);
199
+ const double base = circle != nullptr ? circle->radius : 8;
200
+ const double radius = base * (1.5 + telegraph->remaining);
201
+ ring(frame, transform->position.x, transform->position.y, radius - 1.5, radius, kTelegraph);
202
+ }
203
+
204
+ // enemies + bullets + ship
205
+ for (const Entity* entity : entities) {
206
+ const Component* transform = componentOf(*entity, Kind::Transform2D);
207
+ if (transform == nullptr) continue;
208
+ const Component* telegraph = componentOf(*entity, Kind::Telegraph);
209
+ const bool telegraphing = telegraph != nullptr && telegraph->remaining > 0;
210
+
211
+ if (componentOf(*entity, Kind::WanderMover) != nullptr) {
212
+ if (!telegraphing) {
213
+ fillCircle(frame, transform->position.x, transform->position.y, 12, kWanderer);
214
+ }
215
+ continue;
216
+ }
217
+ if (componentOf(*entity, Kind::SeekMover) != nullptr) {
218
+ if (!telegraphing) {
219
+ fillDiamond(frame, transform->position.x, transform->position.y, 11, kGrunt);
220
+ }
221
+ continue;
222
+ }
223
+ if (entity->ref == "shard") {
224
+ fillCircle(frame, transform->position.x, transform->position.y, 7.5, kShard);
225
+ continue;
226
+ }
227
+ if (entity->ref == "bullet") {
228
+ ring(frame, transform->position.x, transform->position.y, 4, 7, kBulletGlow);
229
+ fillCircle(frame, transform->position.x, transform->position.y, 4, kBulletCore);
230
+ continue;
231
+ }
232
+ if (componentOf(*entity, Kind::AxisDriveMover) != nullptr) {
233
+ const double heading = transform->heading;
234
+ const double cos = rundot::detmath::det_cos(heading);
235
+ const double sin = rundot::detmath::det_sin(heading);
236
+ const double size = 14;
237
+ const double noseX = transform->position.x + cos * size;
238
+ const double noseY = transform->position.y + sin * size;
239
+ const double leftX = transform->position.x + (-cos * 0.7 - sin * 0.6) * size;
240
+ const double leftY = transform->position.y + (-sin * 0.7 + cos * 0.6) * size;
241
+ const double rightX = transform->position.x + (-cos * 0.7 + sin * 0.6) * size;
242
+ const double rightY = transform->position.y + (-sin * 0.7 - cos * 0.6) * size;
243
+ fillTriangle(frame, noseX, noseY, leftX, leftY, rightX, rightY, kShip);
244
+ const Component* shield = componentOf(*entity, Kind::SpawnShield);
245
+ if (shield != nullptr && shield->remaining > 0) {
246
+ ring(frame, transform->position.x, transform->position.y, 16, 18, kShipShield);
247
+ }
248
+ }
249
+ }
250
+
251
+ // HUD: score bar + death pips (mix 50% over background for a HUD feel).
252
+ // JS uses staticEntities.find() here — NOT filtered by alive.
253
+ const Entity* game = nullptr;
254
+ for (const Entity& entity : runtime.statics) {
255
+ if (componentOf(entity, Kind::ResourceCounter) != nullptr) {
256
+ game = &entity;
257
+ break;
258
+ }
259
+ }
260
+ if (game != nullptr) {
261
+ const Component* counter = componentOf(*game, Kind::ResourceCounter);
262
+ const double score = static_cast<double>(counter->score);
263
+ const int barWidth = static_cast<int>(std::floor(std::min(score, 2000.0) * 0.15));
264
+ for (int y = 12; y < 22; y += 1) {
265
+ for (int x = 16; x < 16 + barWidth; x += 1) mixPixel(frame, x, y, kScoreBar);
266
+ }
267
+ const std::int64_t deaths = counter->deaths;
268
+ const std::int64_t pipCount = deaths < 10 ? deaths : 10; // Math.min(deaths, 10)
269
+ for (std::int64_t pip = 0; pip < pipCount; pip += 1) {
270
+ const int pipBase = 16 + static_cast<int>(pip) * 12;
271
+ for (int y = 26; y < 34; y += 1) {
272
+ for (int x = pipBase; x < pipBase + 8; x += 1) putPixel(frame, x, y, kDeathPip);
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ void writeFrameFile(const std::string& framesDir, std::int64_t frameIndex, const std::vector<std::uint8_t>& frame) {
279
+ char name[32];
280
+ std::snprintf(name, sizeof(name), "frame-%03lld.rgb", static_cast<long long>(frameIndex));
281
+ const std::string path = framesDir + "/" + name;
282
+ std::ofstream out(path, std::ios::binary);
283
+ if (!out) fail("cannot write frame " + path);
284
+ out.write(reinterpret_cast<const char*>(frame.data()), static_cast<std::streamsize>(frame.size()));
285
+ if (!out) fail("failed writing frame " + path);
286
+ }
287
+
288
+ } // namespace
289
+
290
+ F64FrameDumpResult runReplayF64WithFrames(const PackDef& pack,
291
+ const ReplayDef& replay,
292
+ bool hashEveryTick,
293
+ const std::string& framesDir) {
294
+ Runtime runtime = createRuntime(pack);
295
+ std::map<std::int64_t, const std::vector<SlotInputDef>*> inputsByTick;
296
+ for (const InputFrameDef& frame : replay.inputFrames) {
297
+ inputsByTick[frame.tick] = &frame.slots;
298
+ }
299
+ std::vector<SlotInputDef> currentInputs(static_cast<std::size_t>(pack.inputSlots));
300
+
301
+ F64FrameDumpResult out;
302
+ F64RunResult& result = out.run;
303
+ std::string streamAccum;
304
+ std::vector<std::uint8_t> frameBuffer(static_cast<std::size_t>(kFrameWidth) * kFrameHeight * 3);
305
+ std::int64_t frameIndex = 0;
306
+ for (std::int64_t tick = 0; tick < replay.tickCount; tick += 1) {
307
+ const auto frame = inputsByTick.find(tick);
308
+ if (frame != inputsByTick.end()) currentInputs = *frame->second;
309
+ tickRuntime(runtime, currentInputs);
310
+ if (tick % 2 == 1) {
311
+ renderF64Frame(runtime, frameBuffer.data());
312
+ writeFrameFile(framesDir, frameIndex, frameBuffer);
313
+ out.frameHashes.push_back(rundot::sha256Hex(frameBuffer.data(), frameBuffer.size()));
314
+ frameIndex += 1;
315
+ }
316
+ if (hashEveryTick || tick == replay.tickCount - 1) {
317
+ const std::string tickHash = canonicalTickHash(runtime);
318
+ if (hashEveryTick) {
319
+ result.tickHashes.push_back(tickHash);
320
+ } else {
321
+ result.tickHashes.assign(1, tickHash);
322
+ }
323
+ streamAccum += tickHash;
324
+ }
325
+ }
326
+ result.finalDigest = rundot::sha256Hex(reinterpret_cast<const std::uint8_t*>(streamAccum.data()), streamAccum.size());
327
+ result.completedTicks = replay.tickCount;
328
+ std::int64_t alivePooled = 0;
329
+ for (const Entity& entity : runtime.pooled) {
330
+ if (entity.alive) alivePooled += 1;
331
+ }
332
+ result.liveEntitiesAtEnd = static_cast<std::int64_t>(runtime.statics.size()) + alivePooled;
333
+ for (Entity& entity : runtime.statics) {
334
+ Component* counter = findComponent(entity, Kind::ResourceCounter);
335
+ if (counter != nullptr) {
336
+ result.finalScore = counter->score;
337
+ result.shipDeaths = counter->deaths;
338
+ break;
339
+ }
340
+ }
341
+ return out;
342
+ }
343
+
344
+ } // namespace rundot::f64rt
@@ -0,0 +1,35 @@
1
+ // Deterministic parity renderer for the installed f64 runtime: C++
2
+ // transliteration of parity-renderer-f64.mjs. Pure function of sim state ->
3
+ // 1280x720 RGB24 frame; the only transcendentals are detmath det_cos/det_sin
4
+ // for the ship heading triangle. The renderer never mutates runtime state.
5
+ //
6
+ // The runtime's entity model is TU-private (anonymous namespace inside
7
+ // the installed runtime source), so the public entrypoint here is a replay
8
+ // runner that mirrors runReplayF64 exactly and additionally renders after
9
+ // every tick with tick % 2 == 1 (0-based), matching render-frames-node.mjs.
10
+ #pragma once
11
+
12
+ #include <cstdint>
13
+ #include <string>
14
+ #include <vector>
15
+
16
+ #include "kinetix-installed-f64-runtime.hpp"
17
+
18
+ namespace rundot::f64rt {
19
+
20
+ inline constexpr int kFrameWidth = 1280;
21
+ inline constexpr int kFrameHeight = 720;
22
+
23
+ struct F64FrameDumpResult {
24
+ F64RunResult run;
25
+ std::vector<std::string> frameHashes; // sha256 hex of each raw RGB24 frame
26
+ };
27
+
28
+ // Runs the replay bit-identically to runReplayF64 (same tick-hash outputs)
29
+ // while writing framesDir/frame-%03d.rgb after each odd tick.
30
+ F64FrameDumpResult runReplayF64WithFrames(const PackDef& pack,
31
+ const ReplayDef& replay,
32
+ bool hashEveryTick,
33
+ const std::string& framesDir);
34
+
35
+ } // namespace rundot::f64rt