Bicameral Engine
GPU-resident engine for a magic-research open-world game
Loading...
Searching...
No Matches
frame_loop.cpp
Go to the documentation of this file.
1// frame_loop.cpp — 窓・2 つのキュー・予算ぶんのシミュ・待たない読み戻しでフレームを回す(T-0004・T-0012)。考え方は frame_loop.h。
2//
3// 1 フレームの流れ(CPU):
4// 窓のメッセージ → スワップチェインの待ち(歩調)→ 経過時間を SimScheduler へ
5// → 終わったシミュのリストの読み戻し(フェンスが進んだ分だけ。待たない。単位の GPU 時間・ハッシュ・イベント)
6// → 窓の入力をカメラ・表示とクリックに分け(render/debug_view_controller。--editor なら ImGui が使っている入力は渡さない)、クリック(再生なら再生ファイル)をコマンドに → 予算ぶんの単位と、GPU のキューへ足すコマンドを記録して compute キューへ(ADR-0011)
7// (T が押されていたら、そのフレームから連鎖のトレースの範囲を変える。frame/trace_capture.h。T-0088)
8// → 描画を direct キューへ(終わっている最新の抽出を見せる。シミュを待たない)
9// → --editor ならパネル(editor/editor_overlay)を作って重ね、押された時間の操作を TimeControl へ(次のフレームから効く)→ Present
10// 時間の操作(止める・1 刻み・速さ。editor/time_control)は「現実の時間から始めてよい刻みの数」だけを変え、世界には入らない(ADR-0035)。
11//
12// 抽出の 3 組の約束(06 §4): 抽出は 1 フレームに 1 回まで、そのフレームに投げた単位の後ろで刻みの境界の状態を写す。
13// 抽出 n は組 n % 3 に書き、CPU から見て終わっている抽出が n − 2 以上のときだけ投げる(でなければそのフレームは写さない)。
14// 描画は終わっている最新の抽出 d(≥ n − 2、< n)を読むので、書いている組と重ならない。
15// さらに抽出 n の前に、組 n % 3 を最後に読んだ描画のフェンスを compute キューに GPU の上で待たせる(m_lastRenderReading)。
16// 描画がシミュを待つことは無い(D-201)。
17#include "frame/frame_loop.h"
18
19#include <chrono>
20#include <cmath>
21#include <deque>
22#include <expected>
23#include <thread>
24
25#include "core/aliases.h"
26#include "core/log.h"
27#include "core/unicode.h"
29#include "editor/time_control.h"
30#include "frame/sim_scheduler.h"
32#include "frame/trace_capture.h"
33#include "gpu/com_ptr.h"
34#include "gpu/queue.h"
35#include "gpu/resources.h"
36#include "gpu/swap_chain.h"
37#include "platform/window.h"
39#include "render/probe_view.h"
40#include "render/screenshot.h"
41#include "save/replay_session.h"
43#include "sim/physics_world.h"
44#include "sim/probe_peek.h"
45#include "sim/probe_sim.h"
46#include "sim/probe_trace.h"
47#include "sim/reaction_table.h"
48
50 namespace {
51
52 using Clock = chr::steady_clock;
53
56 constexpr uint32_t INITIAL_CLIENT_WIDTH = 1280;
57 constexpr uint32_t INITIAL_CLIENT_HEIGHT = 720;
58 constexpr uint32_t FRAME_WAIT_TIMEOUT_MS = 100;
59 constexpr uint32_t CONSTANTS_BYTES = 256;
60 constexpr uint32_t TIMESTAMPS_PER_FRAME = 2;
61 constexpr uint32_t AUTO_CLICK_INTERVAL_FRAMES = 20;
62 constexpr auto STATS_INTERVAL = chr::seconds(1);
63 constexpr auto MINIMIZED_SLEEP = chr::milliseconds(16);
64
65 // --- 窓の T で始める連鎖のトレース(T-0088)---
66 constexpr uint64_t TRACE_KEY_TICKS = 60; // 次の刻みから何刻み
67 constexpr uint32_t TRACE_KEY_RADIUS_CELLS = 8; // 最後につついたセルの周り ±何セル
68 constexpr uint64_t AUTO_TRACE_FRAME = 30; // --auto-trace が T を押すフレーム
69 constexpr uint64_t AUTO_PUSH_FRAME = 60; // --auto-push が押すフレーム(T-0098)
70
71 // --auto-time(T-0023): 止める → 止まったかを確かめる → 1 刻みを 3 回 → 3 刻みだけ進んだかを確かめる → 速さを変えて動かす
72 constexpr uint64_t AUTO_TIME_PAUSE_FRAME = 20;
73 constexpr uint64_t AUTO_TIME_HOLD_FRAME = 36; // 止まった刻みを控える(始めていた刻みが終わるのを待ってから)
74 constexpr uint64_t AUTO_TIME_HOLD_CHECK_FRAME = 44;
75 constexpr std::array<uint64_t, 3> AUTO_TIME_STEP_FRAMES = {45, 50, 55};
76 constexpr uint64_t AUTO_TIME_STEP_CHECK_FRAME = 64;
77 constexpr uint64_t AUTO_TIME_FASTER_FRAME = 65;
78 constexpr uint64_t AUTO_TIME_RESUME_FRAME = 70;
79 constexpr uint64_t AUTO_TIME_SLOWER_FRAME = 100;
80 constexpr uint64_t AUTO_TIME_NORMAL_FRAME = 130;
81
84 return {.togglePause = true};
85
86 if (rng::contains(AUTO_TIME_STEP_FRAMES, frame))
87 return {.stepTicks = 1};
88
90 return {.speedSteps = 1};
91
93 return {.speedSteps = -2};
94
96 return {.resetSpeed = true};
97
98 return {};
99 }
100
101 // 押す力積(T-0098): 5000 N·s(積み木の 500 kg の箱に 10 m/s)。押された箱はその上の段の摩擦に抗って抜ける
102 constexpr uint32_t PUSH_IMPULSE_MILLINEWTON_SECONDS = 5'000'000;
103
104 double Milliseconds(Clock::duration duration) {
105 return chr::duration<double, std::milli>(duration).count();
106 }
107
108 double TimestampMilliseconds(uint64_t ticks, uint64_t frequency) {
109 if (frequency == 0)
110 return 0.0;
111 return static_cast<double>(ticks) * 1000.0 / static_cast<double>(frequency);
112 }
113
114 double TimestampMilliseconds(uint64_t begin, uint64_t end, uint64_t frequency) {
115 return end < begin ? 0.0 : TimestampMilliseconds(end - begin, frequency);
116 }
117
118 // 描画のフレームごとに持つもの(バックバッファの番号 = スワップチェインの CurrentIndex)
119 struct FrameSlot {
120 ComPtr<ID3D12CommandAllocator> allocator;
121 ComPtr<ID3D12GraphicsCommandList10> list; // 大きさが変わったときだけ記録し直す
122 ComPtr<ID3D12Resource> constants; // アップロード(render::ProbeViewConstants)
124 uint64_t renderFence = 0; // このリストを最後に投げた direct のフェンスの値(0 = まだ)
125 };
126
127 // 投げたシミュのリスト(ProbeSim のフレームの枠ごと)。フェンスが fence に届いたら読む
129 uint64_t fence = 0; // compute のフェンスの値
130 uint64_t extraction = 0; // このリストが書いた抽出の番号(0 = 写していない)
131 uint64_t extractionTick = 0; // その抽出が写した状態の刻み(S(この刻み))
132 bool read = true;
133 };
134
135 // 描画が見せる抽出(CPU から見て終わっている最新)
137 uint64_t number = 0; // 0 = まだ無い(組 0 は 0 のまま)
138 uint64_t fence = 0; // それを書いたシミュのリストの compute のフェンスの値
139 uint64_t tick = 0; // 写した状態の刻み(S(この刻み))
140 };
141
144 uint64_t tick = UINT64_MAX; // 載せた刻み(まだなら UINT64_MAX)
145 Clock::time_point time;
146 };
147
148 // 計測(1 秒ごとにログへ。最後に全体の要約)
149 struct Stats {
150 // --- CPU のフレーム ---
151 uint64_t frames = 0;
152 double cpuMilliseconds = 0.0;
153 double cpuMaxMilliseconds = 0.0;
154 double presentMilliseconds = 0.0; // cpuMilliseconds のうち Present の中にいた時間
155 uint64_t cpuWaits = 0; // 描画の枠を使い回す前に CPU が GPU を待った回数(0 のはず)
156
157 // --- シミュ ---
158 uint64_t ticks = 0; // 終わった刻み(読み戻したハッシュの数)
159 uint64_t simSubmissions = 0;
160 uint64_t units = 0;
161 uint64_t events = 0;
162 uint64_t skippedSubmissions = 0; // シミュの枠が空いていないので投げなかったフレーム
163 uint64_t skippedExtractions = 0; // 抽出の 3 組の約束で写さなかったフレーム
164
165 // --- GPU の時間 ---
166 double simGpuMilliseconds = 0.0; // シミュのリスト全体(抽出と読み戻しを含む)
170
171 // 伝導のグラフのノードのカウンタ(足したもの・最大。T-0008)
173
174 // 刻みの中の単位ごとの GPU 時間の合計(番号 = 刻みの中の単位。エディタの性能のパネル。T-0143)
175 std::vector<double> unitGpuMilliseconds;
176 };
177
178 // 作るもの一式(FrameLoop はこれを受け取ってから動く。作れなかったら FrameLoop を作らない)
180 // --- 窓と GPU ---
181 std::unique_ptr<Window> window;
186
187 // --- シミュと表示 ---
189 sim::ProbePeek peek; // 覗き窓(T-0096)
191
192 // --replay のときだけ 1 つ(std::optional は tidy の警告が多いので使わない)
193 std::vector<save::ReplayPlayer> replay;
194
195 // 起動時に読んだ反応表(ホットリロードの元。T-0139)
196 std::shared_ptr<const script::LoadedReactionTable> reactionTable;
197 };
198
199 class FrameLoop {
200 public:
202 : m_options(options),
203 m_window(std::move(parts.window)),
204 m_device(std::move(parts.device)),
205 m_direct(std::move(parts.direct)),
206 m_compute(std::move(parts.compute)),
207 m_swapChain(std::move(parts.swapChain)),
208 m_sim(std::move(parts.simulation)),
209 m_peek(std::move(parts.peek)),
210 m_view(std::move(parts.view)),
211 m_viewController(sim::PROBE_GRID_SIZE, options.view, options.camera),
212 m_replay(std::move(parts.replay)),
213 m_tableReload({.packageRoot = options.packageRoot}, std::move(parts.reactionTable),
214 options.editor && options.replayPath.empty()),
215 m_scheduler(m_sim.UnitsPerTick(), {.targetFps = static_cast<double>(options.targetFps),
216 .maxUnitsPerFrame = sim::ProbeSim::MAX_UNITS_PER_FRAME}),
217 m_computeFrequency(m_compute.TimestampFrequency()),
218 m_directFrequency(m_direct.TimestampFrequency()) {
219 // --screenshot-tick: その刻みの始めで世界を止める(写すのは S(その刻み)。T-0025)
220 if (options.screenshotTick)
221 m_scheduler.SetStopTick(*options.screenshotTick);
222
223 // --trace: 起動時の範囲(ProbeSim を作った時に GPU へ渡した)を集める
224 if (!options.tracePath.empty())
225 m_traceCapture.emplace_back(options.tracePath, options.trace);
226
227 // --peek: 起動時から覗く
228 if (options.peek)
229 m_viewController.Peek(options.peekCell, options.peekDepth);
230
231 // --check-physics: 窓と同じ場面の CPU の物理
232 if (options.physics && options.checkPhysics) {
233 m_physicsCheck = std::make_unique<sim::PhysicsWorld>(sim::MakeProbeStackScene(),
234 physics::PxDefaultParameters());
235 }
236 }
237
238 [[nodiscard]] bool CreateFrameSlots();
239 [[nodiscard]] bool CreateEditor();
240 [[nodiscard]] bool CreateSavePoints();
241 [[nodiscard]] int Run();
242
243 private:
244 // --- 描画 ---
245 bool RecordRenderLists();
246 bool HandleResize();
247 void SubmitRender();
248 [[nodiscard]] bool WantsScreenshot() const;
249 [[nodiscard]] bool SubmitScreenshot();
250 [[nodiscard]] bool WriteScreenshot();
251 [[nodiscard]] bool NeedsStopExtraction() const;
252
253 // --- シミュの読み戻し ---
254 void CollectSimSubmissions();
255 void ReportSimReadback(const sim::ProbeFrameReadback& readback);
256 void ReportEvents(const sim::ProbeFrameReadback& readback);
257 void CheckPhysics(const sim::ProbeTickHash& tickHash);
258 void CollectFrameSlot(uint32_t slotIndex);
259
260 // --- コマンド(クリックと再生)---
261 void QueueClicks();
262 void ApplyPeekChange();
263 void QueuePushes();
264 [[nodiscard]] std::vector<sim::ProbeCommand> TakeCommands(SimCursor start);
265 [[nodiscard]] std::vector<sim::ProbeCommand> TakeClickCommands(uint64_t applyTick, uint32_t limit);
266 void TakeTableSwaps(SimCursor start, uint32_t limit, std::vector<sim::ProbeCommand>& commands);
267 [[nodiscard]] bool FinishReplay();
268 bool SubmitSim();
269
270 // --- 連鎖のトレース(--trace・T。frame/trace_capture.h)---
271 void RequestTrace();
272 void StartRequestedTrace(SimCursor start);
273 void CollectTrace(const sim::ProbeFrameReadback& readback);
274 [[nodiscard]] bool FinishTrace();
275
276 // --- フレームの流れ ---
277 bool RunFrame(Clock::time_point frameStart);
278 int Finish(bool failed, Clock::time_point start);
279 [[nodiscard]] bool IsDeviceLost() const;
280
281 // --- エディタと時間の操作(T-0023)---
282 void BuildEditor();
283 [[nodiscard]] bool SubmitEditor();
284 void ApplyTimeRequest(const editor::TimeRequest& request);
285 void CheckAutoTime();
286 void FilterEditorInput(std::vector<InputEvent>& events) const;
287 [[nodiscard]] editor::EditorStatus MakeEditorStatus() const;
288 [[nodiscard]] editor::GraphPanelStatus MakeGraphPanelStatus() const;
289
290 // --- 巻き戻し(保存点 + 再生。T-0143・ADR-0036)---
291 [[nodiscard]] std::string RewindUnavailableReason() const;
292 [[nodiscard]] std::vector<uint64_t> SavePointTicks() const;
293 void RequestRewind(uint64_t tick);
294 [[nodiscard]] uint32_t PrepareRewind();
295 [[nodiscard]] uint32_t SaveSlotFor(SimCursor start, uint32_t unitCount, uint32_t restoreFrom) const;
296 [[nodiscard]] std::vector<sim::ProbeCommand> TakeLiveCommands(uint64_t applyTick, uint64_t endApplyTick,
297 uint32_t limit);
298 void CheckAutoRewind();
299
300 // --- 計測 ---
301 void LogStats(const Stats& stats, Clock::duration elapsed, std::string_view label) const;
302 void AddStats(const Stats& frame);
303
304 // --- 窓と GPU ---
306 std::unique_ptr<Window> m_window;
311
312 // --- シミュと表示 ---
317
318 // --- 記録・再生・画面の保存 ---
319 std::vector<save::ReplayPlayer> m_replay; // 再生中なら 1 つ
320 save::ReplayRecorder m_recorder; // --record のときだけ使う
322 m_tableReload; // 反応表のホットリロード(--editor でファイルを見る。再生は印の表を当てる。T-0139)
323 std::vector<sim::ProbeTableSwap> m_frameTableSwaps; // 次に記録するフレームで当てる差し替え
324 bool m_tableSwapFailed = false; // 再生の印の表を持っていない(止まる)
325 std::vector<render::ScreenshotCapture> m_screenshot; // --screenshot で写したら 1 つ
326
327 // --- 連鎖のトレース ---
328 std::vector<TraceCapture> m_traceCapture; // 集めているトレース(0 か 1 つ。GPU の範囲は 1 つだけなので)
329 bool m_traceRequested = false; // T が押された(次に投げるフレームから始める)
330 bool m_traceWriteFailed = false; // 途中で書けなかった(終わるときに失敗にする)
331 render::CellCoordinate m_lastPokedCell{.x = sim::PROBE_GRID_SIZE / 2,
332 .y = sim::PROBE_GRID_SIZE / 2,
333 .z = sim::PROBE_GRID_SIZE / 2}; // T の中心(まだなら格子の真ん中)
334
335 // --- 描画の枠 ---
336 std::array<FrameSlot, FRAME_SLOT_COUNT> m_frames;
337 ComPtr<ID3D12QueryHeap> m_renderTimestamps;
338 ComPtr<ID3D12Resource> m_renderTimestampReadback;
339 uint64_t m_frameNumber = 0; // 描いたフレームの数
340
341 // --- シミュの投入と抽出 ---
343 std::array<SimSubmission, SIM_SLOT_COUNT> m_simSubmissions;
345 uint64_t m_lastExtraction = 0; // 最後に投げた抽出の番号
346 uint64_t m_lastExtractionTick = UINT64_MAX; // 最後に投げた抽出が写す状態の刻み(まだなら UINT64_MAX)
348
349 // 抽出の組ごとに、最後にそれを読んだ描画のフェンスの値
350 std::array<uint64_t, sim::PROBE_EXTRACTION_COUNT> m_lastRenderReading{};
351 sim::ProbeTickHash m_latestHash; // 最後に読み戻した刻みの状態のハッシュ
352
353 // --- CPU の物理との突き合わせ(--check-physics。T-0098)---
354 std::unique_ptr<sim::PhysicsWorld> m_physicsCheck;
355 std::vector<sim::ProbeCommand> m_physicsCheckPushes; // 投げた押すコマンド(刻みの順)
357
358 // --- コマンド ---
359 std::vector<sim::ProbeCommand> m_pendingCommands;
360 std::vector<PendingClick> m_clicks;
361 uint32_t m_nextSequence = 0;
362
363 // --- エディタと時間の操作(T-0023)---
365 std::unique_ptr<editor::EditorOverlay> m_editor; // --editor のときだけ(窓より先に壊す)
366 SimCursor m_autoTimeHold; // --auto-time: 止めた後に控えたカーソル
367 bool m_autoTimeFailed = false;
368
369 // --- 巻き戻し(T-0143)---
370 uint32_t m_savePointCount = 0;
371 uint64_t m_rewindRequest = UINT64_MAX; // 次に投げるフレームで戻す保存点の刻み
372 uint32_t m_rewindCount = 0;
373 bool m_extractAfterRewind = false; // 止めている間に戻したら、戻した状態を抽出だけのリストで見せる
374 // 今の流れで GPU のキューへ足したコマンド(刻みの順。再生ファイルを流していない時)と、巻き戻した後に足し直すコマンド
375 std::vector<sim::ProbeCommand> m_commandHistory;
376 std::deque<sim::ProbeCommand> m_rewindFeed;
377
378 // --- 計測 ---
379 uint64_t m_computeFrequency = 0;
380 uint64_t m_directFrequency = 0;
381 Stats m_interval; // この 1 秒
382 Stats m_lastInterval; // 前の 1 秒(エディタの表示)
384 Stats m_total; // 全体
385 };
386
387 // --- 作る ---
388
389 std::expected<std::unique_ptr<Window>, std::string> CreateWindowForLoop() {
390 auto window = Window::Create(L"Bicameral Engine", INITIAL_CLIENT_WIDTH, INITIAL_CLIENT_HEIGHT);
391 if (!window)
392 return std::unexpected(window.error());
393
394 (*window)->PumpMessages();
395 (void)(*window)->TakeResized(); // 最初の WM_SIZE。スワップチェインはこの大きさで作る
396
397 return window;
398 }
399
400 // デバイスの検証の設定。物理のシェーダーは GPU-based validation の計装が最初の実行で数分かかる(debug。gpu_physics_test と同じ)ので、
401 // 物理を入れるときは切る(debug layer は残す。T-0098)
404 if (options.physics && deviceOptions.gpuBasedValidation) {
405 deviceOptions.gpuBasedValidation = false;
406 Log(Channel::Gpu, Level::Info, "物理を入れるので GPU-based validation を切る(--no-physics なら有効)");
407 }
408
409 return deviceOptions;
410 }
411
412 // 仮の世界(物理の場面は積み木。T-0098。ProbeSim は作るときに読むだけ)
413 std::expected<sim::ProbeSim, std::string> CreateSimulation(ID3D12Device5* device,
414 const FrameLoopOptions& options,
415 const sim::BakedReactionTable& reactionTable) {
416 const sim::PhysicsScene physicsScene = sim::MakeProbeStackScene();
417
418 return sim::ProbeSim::Create(device, D3D12_COMMAND_LIST_TYPE_COMPUTE, reactionTable,
419 {.busyIterations = options.simLoad,
420 .busyPieces = options.simSplit,
421 .trace = options.trace,
422 .traceCapacity = TRACE_CAPACITY_PER_FRAME,
423 .physicsScene = options.physics ? &physicsScene : nullptr,
424 .physicsOptions = {.broadphaseGraph = !options.physicsComputeBroadphase}});
425 }
426
427 // 世界の反応表: データのフォルダのパッケージ(既定は exe の横の data/packages)からベイクする(T-0157・ADR-0033)。
428 // 読めない・形の誤り・検査で落ちたら、窓を開く前に起動を止める(半端な表で世界を動かさない)。
429 // Mod が読めなかったとき・文献の反応熱と食い違うときは、警告をログに出して続ける(ADR-0031 の 3・02 §2 の 2)
430 std::expected<std::shared_ptr<const script::LoadedReactionTable>, std::string> LoadWorldReactionTable(
431 const FrameLoopOptions& options) {
432 const auto start = chr::steady_clock::now();
433 auto loaded = script::LoadReactionTable({.packageRoot = options.packageRoot});
434 if (!loaded)
435 return std::unexpected(loaded.error());
436
437 const auto elapsedMs = chr::duration_cast<chr::milliseconds>(chr::steady_clock::now() - start).count();
438
439 std::string packages;
440 for (const std::string& name : loaded->loadOrder)
441 packages += (packages.empty() ? "" : "・") + name;
442
443 Log(Channel::Sim, Level::Info, "反応表: パッケージ {}(物質 {}・規則 {}・版 {:016x}{}・読んでベイク {} ms)",
444 packages, loaded->table.species.size() - 1, loaded->table.rules.size(), loaded->tableVersion,
445 loaded->modifiedWorld ? "・改造された世界" : "", elapsedMs);
446
447 for (const script::PackageRejection& rejection : loaded->rejected)
448 Log(Channel::Sim, Level::Warning, "パッケージ {} を読まなかった: {}", rejection.package,
449 rejection.reason);
450
451 for (const std::string& warning : loaded->table.warnings)
452 Log(Channel::Sim, Level::Warning, "反応表: {}", warning);
453
454 return std::make_shared<const script::LoadedReactionTable>(std::move(*loaded));
455 }
456
457 std::expected<FrameLoopParts, std::string> CreateParts(const FrameLoopOptions& options) {
458 const auto reactionTable = LoadWorldReactionTable(options);
459 if (!reactionTable)
460 return std::unexpected(reactionTable.error());
461
462 auto window = CreateWindowForLoop();
463 if (!window)
464 return std::unexpected(window.error());
465
466 gpu::DeviceOptions deviceOptions = LoopDeviceOptions(options);
467 deviceOptions.presentMonitor = MonitorFromWindow((*window)->Handle(), MONITOR_DEFAULTTONEAREST);
468 auto device = gpu::Device::Create(options.adapter, deviceOptions);
469 if (!device)
470 return std::unexpected(device.error());
471
472 ID3D12Device5* native = device->Get();
473
474 // 描画のキューは優先度を上げる(効果は無かったが害も無い。R-LOOP-2。docs/perf.md)
475 auto direct = gpu::Queue::Create(
476 native, D3D12_COMMAND_LIST_TYPE_DIRECT, L"Render",
477 options.renderHighPriority ? D3D12_COMMAND_QUEUE_PRIORITY_HIGH : D3D12_COMMAND_QUEUE_PRIORITY_NORMAL);
478
479 auto compute = gpu::Queue::Create(native, D3D12_COMMAND_LIST_TYPE_COMPUTE, L"Sim");
480 if (!direct || !compute)
481 return std::unexpected("キューを作れない");
482
483 auto swapChain = gpu::SwapChain::Create(native, device->Factory(), direct->Native(), (*window)->Handle(),
484 options.maxFrameLatency);
485 if (!swapChain)
486 return std::unexpected(swapChain.error());
487
488 auto simulation = CreateSimulation(native, options, (*reactionTable)->table);
489 if (!simulation)
490 return std::unexpected(simulation.error());
491
492 auto peek = sim::ProbePeek::Create(native, (*reactionTable)->table);
493 if (!peek)
494 return std::unexpected(peek.error());
495
497 if (!view)
498 return std::unexpected(view.error());
499
500 std::vector<save::ReplayPlayer> replay;
501 if (!options.replayPath.empty()) {
502 auto player = save::ReplayPlayer::Load(options.replayPath);
503 if (!player)
504 return std::unexpected(player.error());
505
506 replay.push_back(std::move(*player));
507 }
508
509 return FrameLoopParts{.window = std::move(*window),
510 .device = std::move(*device),
511 .direct = std::move(*direct),
512 .compute = std::move(*compute),
513 .swapChain = std::move(*swapChain),
514 .simulation = std::move(*simulation),
515 .peek = std::move(*peek),
516 .view = std::move(*view),
517 .replay = std::move(replay),
518 .reactionTable = *reactionTable};
519 }
520
522 ID3D12Device5* device = m_device.Get();
523 for (FrameSlot& slot : m_frames) {
524 if (FAILED(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT,
525 IID_PPV_ARGS(&slot.allocator))) ||
526 FAILED(device->CreateCommandList1(0, D3D12_COMMAND_LIST_TYPE_DIRECT, D3D12_COMMAND_LIST_FLAG_NONE,
527 IID_PPV_ARGS(&slot.list)))) {
528 return false;
529 }
530
532 if (!slot.constants)
533 return false;
534
535 void* mapped = nullptr;
536 const D3D12_RANGE noRead{};
537 if (FAILED(slot.constants->Map(0, &noRead, &mapped)))
538 return false;
539
540 slot.mappedConstants = static_cast<render::ProbeViewConstants*>(mapped);
541 }
542
543 const D3D12_QUERY_HEAP_DESC queryDesc{.Type = D3D12_QUERY_HEAP_TYPE_TIMESTAMP,
545 if (FAILED(device->CreateQueryHeap(&queryDesc, IID_PPV_ARGS(&m_renderTimestamps))))
546 return false;
547
550
551 return m_renderTimestampReadback != nullptr && RecordRenderLists();
552 }
553
554 // バックバッファごとに描画のリストを記録する(作るときと、大きさが変わったときだけ)
556 for (uint32_t index = 0; index < FRAME_SLOT_COUNT; ++index) {
557 FrameSlot& slot = m_frames[index];
558 if (FAILED(slot.allocator->Reset()) || FAILED(slot.list->Reset(slot.allocator.Get(), nullptr)))
559 return false;
560
561 ID3D12GraphicsCommandList10* list = slot.list.Get();
562 const uint32_t firstQuery = index * TIMESTAMPS_PER_FRAME;
563 list->EndQuery(m_renderTimestamps.Get(), D3D12_QUERY_TYPE_TIMESTAMP, firstQuery);
564
565 m_view.Record(list,
566 {.backBuffer = m_swapChain.BackBuffer(index),
567 .renderTargetView = m_swapChain.RenderTargetView(index),
568 .width = m_swapChain.Width(),
569 .height = m_swapChain.Height()},
570 {.extraction0 = m_sim.Extraction(0)->GetGPUVirtualAddress(),
571 .extraction1 = m_sim.Extraction(1)->GetGPUVirtualAddress(),
572 .extraction2 = m_sim.Extraction(2)->GetGPUVirtualAddress(),
573 .constants = slot.constants->GetGPUVirtualAddress()});
574
575 list->EndQuery(m_renderTimestamps.Get(), D3D12_QUERY_TYPE_TIMESTAMP, firstQuery + 1);
576 list->ResolveQueryData(m_renderTimestamps.Get(), D3D12_QUERY_TYPE_TIMESTAMP, firstQuery,
577 TIMESTAMPS_PER_FRAME, m_renderTimestampReadback.Get(), uint64_t{firstQuery} * 8);
578 if (FAILED(list->Close()))
579 return false;
580 }
581
582 return true;
583 }
584
586 if (m_window->IsMinimized())
587 return true;
588
589 // バックバッファを使うリストを GPU が全部終えてから(ここだけ CPU が待つ。Present の分も待つ)
590 if (!m_direct.Flush() || !m_compute.Flush())
591 return false;
592
594 for (uint32_t index = 0; index < FRAME_SLOT_COUNT; ++index)
595 CollectFrameSlot(index);
596
597 if (!m_swapChain.Resize(m_device.Get(), m_window->ClientWidth(), m_window->ClientHeight()))
598 return false;
599
600 Log(Channel::Render, Level::Info, "窓の大きさ: {}×{}", m_swapChain.Width(), m_swapChain.Height());
601
602 return RecordRenderLists();
603 }
604
605 // --- 読み戻し(待たない)---
606
607 // 投げた順に読む(枠は投げた順に回るので、次に使う枠がいちばん古い)。枠の番号の順に読むと、2 つが同時に終わっていたとき
608 // 新しい方の刻みのハッシュが先に届き、再生の突き合わせが古い刻みを「戻ってこなかった」と数える(T-0096 で見つけた)
610 for (uint32_t index = 0; index < SIM_SLOT_COUNT; ++index) {
611 const auto slot = static_cast<uint32_t>((m_simSubmissionCount + index) % SIM_SLOT_COUNT);
612 SimSubmission& submission = m_simSubmissions[slot];
613 if (submission.read || !m_compute.IsComplete(submission.fence))
614 continue;
615
616 submission.read = true;
617 ReportSimReadback(m_sim.ReadFrame(slot));
618 if (submission.extraction > m_completedExtraction.number) {
620 .number = submission.extraction, .fence = submission.fence, .tick = submission.extractionTick};
621 }
622 }
623 }
624
625 // 単位ごとの GPU 時間を SimScheduler へ。終えた刻みのハッシュとイベント
627 const uint32_t unitsPerTick = m_sim.UnitsPerTick();
628 m_interval.unitGpuMilliseconds.resize(unitsPerTick);
629 for (size_t index = 0; index < readback.unitGpuTicks.size(); ++index) {
630 const auto unit = static_cast<uint32_t>((readback.firstUnit + index) % unitsPerTick);
631 const double milliseconds = TimestampMilliseconds(readback.unitGpuTicks[index], m_computeFrequency);
632 m_scheduler.ReportUnitTime(unit, milliseconds);
633 m_interval.unitGpuMilliseconds[unit] += milliseconds;
634 }
635
636 gpu::AccumulateGraphStats(m_interval.conductGraph, readback.graphStats);
637 m_interval.simGpuMilliseconds += TimestampMilliseconds(readback.gpuBeginTimestamp, readback.gpuEndTimestamp,
639 ++m_interval.simSubmissionsMeasured;
640 m_interval.ticks += readback.hashes.size();
641 if (!readback.hashes.empty())
642 m_latestHash = readback.hashes.back();
643
644 for (const sim::ProbeTickHash& tickHash : readback.hashes) {
645 // 再生ファイルにはセルと物を合わせた要約を入れる(T-0098)
646 if (!m_options.recordPath.empty())
647 m_recorder.AddHash(tickHash.tick, tickHash.WorldHash());
648
649 if (!m_replay.empty())
650 m_replay.front().CheckHash(tickHash.tick, tickHash.WorldHash());
651
652 if (m_physicsCheck)
653 CheckPhysics(tickHash);
654 }
655
656 ReportEvents(readback);
657 CollectTrace(readback);
658 }
659
660 // 押すコマンド(probe_sim.hlsli の PROBE_COMMAND_TYPE_PUSH の payload)を CPU の物理へ
662 const auto word64 = [&](size_t index) {
663 return static_cast<int64_t>(uint64_t{command.payload[index]} |
664 (uint64_t{command.payload[index + 1]} << 32));
665 };
666 const auto direction = [&](size_t index) {
667 return static_cast<int64_t>(static_cast<int32_t>(command.payload[index]));
668 };
669
670 (void)world.Push(physics::PxMakeVec3(word64(0), word64(2), word64(4)),
671 physics::PxMakeVec3(direction(6), direction(7), direction(8)), command.payload[9]);
672 }
673
674 // CPU の物理を S(tickHash.tick) まで進めて、物のハッシュを比べる(押すコマンドは同じ刻みの Step の前に)
676 while (m_physicsCheck->Tick() < tickHash.tick) {
677 const uint64_t tick = m_physicsCheck->Tick();
678 for (const sim::ProbeCommand& command : m_physicsCheckPushes) {
679 if (command.targetTick == tick)
681 }
682
683 m_physicsCheck->Step();
684 }
685
686 if (m_physicsCheck->StateHash() == tickHash.bodyHash)
687 return;
688
689 if (m_physicsCheckMismatches++ == 0) {
690 Log(Channel::Sim, Level::Error, "物理の突き合わせ: S({}) の物が CPU と違う(GPU {:016x} CPU {:016x})",
691 tickHash.tick, tickHash.bodyHash, m_physicsCheck->StateHash());
692 }
693 }
694
695 // つつきが適用されたことをログへ(クリックから CPU に戻るまでの時間つき)。イベントは (刻み, 種類, 場所) の順に届く
697 const auto now = Clock::now();
698 for (const sim::ProbeEvent& event : readback.events) {
699 ++m_interval.events;
700 if (event.type == sim::PROBE_EVENT_COMMAND_LATE) {
701 Log(Channel::Sim, Level::Warning, "種類 {} のコマンドが刻み {} の適用に遅れて届いた(捨てた)",
702 event.place, event.tick);
703 continue;
704 }
705
706 if (event.type == sim::PROBE_EVENT_BODY_PUSHED) {
707 if (event.place == sim::PROBE_PUSH_NOTHING)
708 Log(Channel::Sim, Level::Info, "押す: 刻み {} で光線の先に動く物が無かった", event.tick);
709 else
710 Log(Channel::Sim, Level::Info, "押す: 刻み {} で物 {} を押した", event.tick, event.place);
711
712 continue;
713 }
714
715 const render::CellCoordinate cell{.x = event.PokeX(), .y = event.PokeY(), .z = event.PokeZ()};
716 const auto click = rng::find_if(m_clicks, [&](const PendingClick& pending) {
717 return pending.tick == event.tick && pending.cell == cell;
718 });
719
720 const double latency = click != m_clicks.end() ? Milliseconds(now - click->time) : 0.0;
721
723 "つつき ({}, {}, {}) を刻み {} で適用(クリックから CPU に戻るまで {:.1f} ms)", cell.x, cell.y,
724 cell.z, event.tick, latency);
725
726 if (click != m_clicks.end())
727 m_clicks.erase(click);
728 }
729
730 if (readback.droppedEventCount > 0) {
731 Log(Channel::Sim, Level::Warning, "イベントのリングが溢れて {} 件を落とした",
732 readback.droppedEventCount);
733 }
734 }
735
736 // 描画の枠を使い回す前に: 前にその枠で投げたリストの終わりを確かめ、タイムスタンプを読む
737 void FrameLoop::CollectFrameSlot(uint32_t slotIndex) {
738 FrameSlot& slot = m_frames[slotIndex];
739 if (slot.renderFence == 0)
740 return;
741
742 if (!m_direct.IsComplete(slot.renderFence)) {
743 ++m_interval.cpuWaits; // スワップチェインの待ちがあるので、ふつうは起きない
744 m_direct.WaitCpu(slot.renderFence);
745 }
746
747 std::array<uint64_t, size_t{FRAME_SLOT_COUNT} * TIMESTAMPS_PER_FRAME> timestamps{};
748 if (gpu::ReadBuffer(m_renderTimestampReadback.Get(), std::as_writable_bytes(std::span(timestamps)))) {
749 const size_t first = size_t{slotIndex} * TIMESTAMPS_PER_FRAME;
750 const double milliseconds = TimestampMilliseconds(timestamps[first], timestamps[first + 1],
752 m_scheduler.ReportRenderTime(milliseconds);
753 m_interval.renderGpuMilliseconds += milliseconds;
754 ++m_interval.renderFramesMeasured;
755 }
756
757 slot.renderFence = 0;
758 }
759
760 // --- 入力 → コマンド ---
761
762 // 窓の入力: カメラと表示は再生中も動かせる。つつき(左クリックが断面に当たったセル)と自動のクリックはコマンドに
764 std::vector<InputEvent> events = m_window->TakeInputEvents();
765 FilterEditorInput(events);
766 if (m_options.autoTrace && m_frameNumber == AUTO_TRACE_FRAME)
767 events.push_back({.kind = InputKind::KeyDown, .key = 'T'}); // 人がいない確認で T の流れを通す
768
769 std::vector<render::CellCoordinate> cells = m_viewController.HandleInput(events, m_swapChain.Width(),
770 m_swapChain.Height());
771
772 // トレースと覗き窓は世界に入らない(View)ので、再生中も使える
773 if (m_viewController.TakeTraceRequest())
774 RequestTrace();
775
777
778 // 再生中は窓の操作を世界に入れない(世界は再生ファイルのコマンドだけで進む)
779 if (!m_replay.empty())
780 return;
781
782 const bool autoClick = m_options.autoClick && m_frameNumber % AUTO_CLICK_INTERVAL_FRAMES == 0;
783 const bool autoIgnite = m_options.autoIgnite && m_frameNumber == 0; // 下の最初の 1 回と同じ場所
784 if (autoClick || autoIgnite) {
785 // z = PROBE_VIEW_Z の面の決まった場所を順に押す(人がいない確認用。表示やカメラに依らない)。
786 // 最初の 1 回は木箱の壁 (28, 32)(初めの世界。sim/probe_sim.cpp)に火をつける(T-0089)
787 const auto step = static_cast<uint32_t>(m_frameNumber / AUTO_CLICK_INTERVAL_FRAMES);
788 const uint32_t u = step == 0 ? 28 : (step * 37 % 16 + 1) * sim::PROBE_GRID_SIZE / 18;
789 const uint32_t v = step == 0 ? 32 : (step * 11 % 16 + 1) * sim::PROBE_GRID_SIZE / 18;
790 cells.push_back(render::CellOnSlice(2, sim::PROBE_VIEW_Z, u, v));
791 }
792
793 QueuePushes();
794
795 const auto now = Clock::now();
796 for (const render::CellCoordinate& cell : cells) {
797 m_pendingCommands.push_back(sim::MakePokeCommand(0, m_nextSequence++, cell.x, cell.y, cell.z));
798 m_clicks.push_back({.cell = cell, .time = now});
799 m_lastPokedCell = cell;
800 }
801 }
802
803 // Shift + 左クリックの光線(と --auto-push)を押すコマンドに(T-0098)。光線は格子の座標なので物理の座標へ直す
804 // (probe_sim.hlsli の対応: y を裏返し、1 セル = 0.5 m)。どの物に当たるかは GPU が決める(CPU は物の場所を知らない。D-107)
806 std::vector<render::CameraRay> rays = m_viewController.TakePushRays();
807 if (m_options.autoPush && m_frameNumber == AUTO_PUSH_FRAME) {
808 // 積み木の 5 段目(物理の座標 x = 10 m・y = 5.5 m・z = 16.25 m。格子の (20, 53, 32.5))を −z の側から
809 rays.push_back({.origin = {20.0f, 53.0f, 0.0f}, .direction = {0.0f, 0.0f, 1.0f}});
810 }
811
812 for (const render::CameraRay& ray : rays) {
813 const float length = std::sqrt((ray.direction.x * ray.direction.x) +
814 (ray.direction.y * ray.direction.y) +
815 (ray.direction.z * ray.direction.z));
816 if (length <= 0.0f)
817 continue;
818
819 constexpr auto UNITS_PER_CELL = static_cast<double>(1u << sim::PROBE_PHYSICS_CELL_SHIFT);
820 constexpr auto DIRECTION_ONE = static_cast<double>(1u << 30);
821 const std::array<int64_t, 3> origin = {
822 std::llround(ray.origin.x * UNITS_PER_CELL),
823 std::llround((static_cast<double>(sim::PROBE_GRID_SIZE) - ray.origin.y) * UNITS_PER_CELL),
824 std::llround(ray.origin.z * UNITS_PER_CELL)};
825 const std::array<int32_t, 3> direction = {
826 static_cast<int32_t>(std::lround(ray.direction.x / length * DIRECTION_ONE)),
827 static_cast<int32_t>(std::lround(-ray.direction.y / length * DIRECTION_ONE)),
828 static_cast<int32_t>(std::lround(ray.direction.z / length * DIRECTION_ONE))};
829
830 m_pendingCommands.push_back(
832 }
833 }
834
835 // 覗く場所が変わったら覗き窓へ(次の抽出から効く。T-0096)
838 if (!m_viewController.TakePeekChange())
839 return;
840
841 const render::PeekView& peek = m_viewController.Peeking();
842 if (peek.peeking)
843 m_peek.Look({.x = peek.cell.x, .y = peek.cell.y, .z = peek.cell.z});
844 else
845 m_peek.Stop();
846 }
847
848 // このフレームに GPU のキューへ足すコマンド(クリックか再生ファイルから)。記録するならここで控える。
849 // 適用する刻みは「まだ記録していない最初の適用の単位の刻み」以上(06 §3)。コマンドはそこまで GPU のキューで待つので、
850 // このフレームに適用の単位が無くてもよい。キューの空きを超える分は次のフレームへ
851 std::vector<sim::ProbeCommand> FrameLoop::TakeCommands(SimCursor start) {
852 const uint64_t applyTick = sim::ProbeSim::NextApplyTick(start.tick, start.unit);
853 const uint32_t limit = std::min(m_sim.FreeCommandSlots(), sim::PROBE_MAX_COMMANDS);
854
855 // このフレームの単位が記録する最後の適用の次の刻み(巻き戻した後のコマンドは、ここより前の分だけ足す)
856 const uint64_t endApplyTick = sim::ProbeSim::NextApplyTick(m_scheduler.Cursor().tick,
857 m_scheduler.Cursor().unit);
858 std::vector<sim::ProbeCommand> commands = m_replay.empty()
859 ? TakeLiveCommands(applyTick, endApplyTick, limit)
860 : m_replay.front().TakeCommands(applyTick, limit);
861
862 TakeTableSwaps(start, limit, commands);
863 if (!m_options.recordPath.empty())
864 m_recorder.AddCommands(commands);
865
866 if (m_physicsCheck) {
867 rng::copy_if(commands, std::back_inserter(m_physicsCheckPushes), [](const sim::ProbeCommand& command) {
868 return command.type == sim::PROBE_COMMAND_TYPE_PUSH;
869 });
870 }
871
872 return commands;
873 }
874
875 // 反応表の差し替え(T-0139・ADR-0047): 読み直した表を、このフレームの最初の適用の単位の刻みへ(印のコマンドを足すので
876 // 記録にも残る)。再生は印の版の表を探す。当てる差し替えは m_frameTableSwaps(SubmitSim が RecordFrame に渡す)
877 void FrameLoop::TakeTableSwaps(SimCursor start, uint32_t limit, std::vector<sim::ProbeCommand>& commands) {
878 const uint32_t unitsPerTick = m_sim.UnitsPerTick();
879 const uint64_t firstUnit = (start.tick * unitsPerTick) + start.unit;
880 const uint64_t endUnit = (m_scheduler.Cursor().tick * unitsPerTick) + m_scheduler.Cursor().unit;
881 auto swaps = m_tableReload.TakeSwaps(firstUnit, static_cast<uint32_t>(endUnit - firstUnit), unitsPerTick,
882 !m_replay.empty(), commands.size() < limit, m_nextSequence, commands);
883 if (!swaps) {
884 Log(Channel::Sim, Level::Error, "{}", swaps.error());
885 m_tableSwapFailed = true;
886 m_frameTableSwaps.clear();
887 return;
888 }
889
890 m_frameTableSwaps = std::move(*swaps);
891 }
892
893 std::vector<sim::ProbeCommand> FrameLoop::TakeClickCommands(uint64_t applyTick, uint32_t limit) {
894 const size_t count = std::min<size_t>(m_pendingCommands.size(), limit);
895 std::vector<sim::ProbeCommand> commands(m_pendingCommands.begin(),
896 m_pendingCommands.begin() + static_cast<std::ptrdiff_t>(count));
898 m_pendingCommands.begin() + static_cast<std::ptrdiff_t>(count));
899
900 for (sim::ProbeCommand& command : commands) {
901 command.targetTick = applyTick;
902 if (command.type != sim::PROBE_COMMAND_TYPE_POKE)
903 continue; // クリックから戻るまでの時間を測るのはつつきだけ
904
905 const render::CellCoordinate cell{
906 .x = command.payload[0], .y = command.payload[1], .z = command.payload[2]};
907 const auto click = rng::find_if(m_clicks, [&](const PendingClick& pending) {
908 return pending.tick == UINT64_MAX && pending.cell == cell;
909 });
910
911 if (click != m_clicks.end())
912 click->tick = applyTick;
913 }
914
915 return commands;
916 }
917
918 // --- 投げる ---
919
920 // 予算ぶんの単位を記録して compute キューへ(ADR-0011)。記録に失敗したら false(直せない誤り)
922 const auto slot = static_cast<uint32_t>(m_simSubmissionCount % SIM_SLOT_COUNT);
923 SimSubmission& submission = m_simSubmissions[slot];
924 if (!submission.read) { // その枠のリストがまだ GPU にある(読めていない)
925 ++m_interval.skippedSubmissions;
926
927 return true;
928 }
929
930 // 巻き戻し(T-0143): GPU を待って読み戻しを全部読んでから、カーソルを保存点の刻みへ戻す。このフレームの先頭で世界を戻す
931 const uint32_t restoreFrom = m_rewindRequest != UINT64_MAX ? PrepareRewind() : sim::NO_SAVE_POINT;
932 const bool restoring = restoreFrom != sim::NO_SAVE_POINT;
933
934 const SimCursor start = m_scheduler.Cursor();
935 const uint32_t unitCount = m_scheduler.TakeUnits();
936 // 止まる刻みに着いたのに、その状態をまだ抽出していない(最後のリストが抽出を飛ばした)か、
937 // 止めたまま戻した状態をまだ見せていないなら、抽出だけのリストを投げる
938 const bool extractOnly = unitCount == 0 && (NeedsStopExtraction() || m_extractAfterRewind);
939 if (unitCount == 0 && !extractOnly && !restoring)
940 return true;
941
942 const uint64_t extraction = m_lastExtraction + 1;
943 const bool extract = m_completedExtraction.number + 2 >= extraction; // 抽出の 3 組の約束(ファイルの先頭)
944 if (extractOnly && !extract && !restoring)
945 return true; // 描画が抽出を読み終えるのを待つ(次のフレームでもう一度)
946
947 if (!extract)
948 ++m_interval.skippedExtractions;
949
950 const std::vector<sim::ProbeCommand> commands = TakeCommands(start);
952 return false;
953
954 StartRequestedTrace(start);
955 // 抽出は S(終わった後のカーソルの刻み)。刻みの途中で終わったら、その刻みの始めの状態(ProbeSim::RecordFrame)
956 const uint64_t extractionTick = m_scheduler.Cursor().tick;
957
958 const auto extractionTarget = static_cast<uint32_t>(extraction % sim::PROBE_EXTRACTION_COUNT);
959
960 ID3D12CommandList* list = m_sim.RecordFrame(
961 slot, {.firstTick = start.tick,
962 .firstUnit = start.unit,
963 .unitCount = unitCount,
964 .extract = extract,
965 .extractionTarget = extractionTarget,
966 .commands = commands,
967 .afterExtract = [this](auto* simList,
968 const auto& context) { m_peek.RecordAfterExtract(simList, context); },
969 .saveTo = SaveSlotFor(start, unitCount, restoreFrom),
970 .restoreFrom = restoreFrom,
971 .tableSwaps = m_frameTableSwaps});
972
973 if (list == nullptr)
974 return false;
975
976 // 表を差し替えたら、保存点(このフレームの先頭で写したものも)は前の表の世界なので捨てる(戻ると表と食い違う。ADR-0047)
977 if (!m_frameTableSwaps.empty()) {
978 m_sim.DiscardSavePointsAfter(0);
979 m_frameTableSwaps.clear();
980 }
981
982 if (extract) {
983 // 抽出の組を描画が読み終えるまで、GPU の上で待ってから走る
984 m_compute.GpuWait(m_direct, m_lastRenderReading[extractionTarget]);
985 m_lastExtraction = extraction;
986 m_lastExtractionTick = extractionTick;
987 m_extractAfterRewind = false; // 戻した後の流れの状態を見せた
988 }
989
990 submission = {.fence = m_compute.Submit(list),
991 .extraction = extract ? extraction : 0,
992 .extractionTick = extractionTick,
993 .read = false};
995 ++m_interval.simSubmissions;
996 m_interval.units += unitCount;
997
998 return true;
999 }
1000
1002 const uint32_t index = m_swapChain.CurrentIndex();
1003 CollectFrameSlot(index);
1004 FrameSlot& slot = m_frames[index];
1005
1006 // 見せる抽出: CPU から見て終わっている最新(シミュを待たない。抽出の 3 組の約束。ファイルの先頭)
1007 const auto extraction = static_cast<uint32_t>(m_completedExtraction.number % sim::PROBE_EXTRACTION_COUNT);
1008 *slot.mappedConstants = m_viewController.Constants(extraction, m_swapChain.Width(), m_swapChain.Height());
1010 slot.renderFence = m_direct.Submit(slot.list.Get());
1011 m_lastRenderReading[extraction] = slot.renderFence;
1012 }
1013
1014 // 止まる刻み(--screenshot-tick)に着いたが、その状態 S(刻み) をまだ抽出していない
1016 return m_options.screenshotTick && m_scheduler.ReachedStopTick() &&
1017 m_lastExtractionTick != m_scheduler.Cursor().tick;
1018 }
1019
1020 // このフレームを写すか。--screenshot-tick なら、止まる刻みの状態 S(刻み) をこのフレームの描画が見せている時(T-0025)。
1021 // そうでなければ最後のフレーム(--frames。シミュの進み方は実行ごとに違うので、写る刻みは決まらない)
1023 if (m_options.screenshotPath.empty() || !m_screenshot.empty())
1024 return false;
1025
1026 if (!m_options.screenshotTick)
1027 return m_frameNumber + 1 == m_options.frameLimit;
1028
1029 return m_scheduler.ReachedStopTick() && m_completedExtraction.number > 0 &&
1030 m_completedExtraction.tick == *m_options.screenshotTick;
1031 }
1032
1033 // --screenshot: 写すフレームの描画の後・Present の前に、バックバッファを読み戻しへ写す
1035 if (!WantsScreenshot())
1036 return true;
1037
1038 ID3D12Resource* backBuffer = m_swapChain.BackBuffer(m_swapChain.CurrentIndex());
1039 auto capture = render::ScreenshotCapture::Create(m_device.Get(), backBuffer);
1040 if (!capture) {
1041 Log(Channel::Render, Level::Error, "画像を写せない: {}", capture.error());
1042 return false;
1043 }
1044
1045 m_screenshot.push_back(std::move(*capture));
1046 ID3D12CommandList* list = m_screenshot.front().Record(backBuffer);
1047 if (list == nullptr)
1048 return false;
1049
1050 m_direct.Submit(list);
1051
1052 return true;
1053 }
1054
1055 // 写した画像をファイルへ(GPU を待った後)。写していなければ何もしない
1057 if (m_screenshot.empty())
1058 return true;
1059
1060 const auto written = m_screenshot.front().WriteBmp(m_options.screenshotPath);
1061 if (!written) {
1062 Log(Channel::Render, Level::Error, "画像を書けない: {}", written.error());
1063 return false;
1064 }
1065
1066 Log(Channel::Render, Level::Info, "画像: {}{}", ToUtf8(m_options.screenshotPath.wstring()),
1067 m_options.screenshotTick ? std::format("(S({}))", *m_options.screenshotTick) : "");
1068
1069 return true;
1070 }
1071
1073 return m_direct.IsDeviceLost() || m_compute.IsDeviceLost() ||
1074 FAILED(m_device.Get()->GetDeviceRemovedReason());
1075 }
1076
1077 // --- エディタと時間の操作(T-0023)---
1078
1080 if (!m_options.editor)
1081 return true;
1082
1085 if (!overlay) {
1086 Log(Channel::Render, Level::Error, "エディタの殻を作れない: {}", overlay.error());
1087 return false;
1088 }
1089
1090 m_editor = std::move(*overlay);
1091 if (m_options.autoLab)
1092 m_editor->Lab().StartAuto();
1093
1094 return true;
1095 }
1096
1097 // ImGui のパネルの上の入力はカメラとつつきに渡さない。離す・動かすは渡す(パネルの外で始めたドラッグを終わらせるため)
1098 void FrameLoop::FilterEditorInput(std::vector<InputEvent>& events) const {
1099 if (!m_editor)
1100 return;
1101
1102 const bool pointer = m_editor->WantsPointer();
1103 const bool keyboard = m_editor->WantsKeyboard();
1104 std::erase_if(events, [&](const InputEvent& event) {
1105 if (event.kind == InputKind::KeyDown)
1106 return keyboard;
1107
1108 return pointer && (event.kind == InputKind::ButtonDown || event.kind == InputKind::Wheel);
1109 });
1110 }
1111
1112 // パネルを作り、押された時間の操作を受ける(--auto-time の操作もここで入れる)
1114 editor::TimeRequest request;
1115 if (m_editor)
1116 request = m_editor->Build(MakeEditorStatus(), m_timeControl);
1117
1118 if (m_options.autoTime) {
1119 CheckAutoTime();
1121 if (automatic.Any())
1122 request = automatic;
1123 }
1124
1125 if (request.rewindTick != UINT64_MAX)
1126 RequestRewind(request.rewindTick);
1127
1129 ApplyTimeRequest(request);
1130 }
1131
1133 if (!m_editor)
1134 return true;
1135
1136 const uint32_t index = m_swapChain.CurrentIndex();
1137
1138 return m_editor->Submit(m_direct, m_swapChain.BackBuffer(index), m_swapChain.RenderTargetView(index));
1139 }
1140
1142 if (!request.Any())
1143 return;
1144
1145 m_timeControl.Apply(request, m_scheduler);
1146 const SimCursor cursor = m_scheduler.Cursor();
1147 Log(Channel::Sim, Level::Info, "時間の操作: {} 速さ ×{:g} 刻み {}(単位 {}) 1 刻みずつ {}",
1148 m_timeControl.Paused() ? "止めている" : "動いている", m_timeControl.Speed(), cursor.tick, cursor.unit,
1149 m_timeControl.SteppedTicks());
1150 }
1151
1152 // --auto-time: 止めている間は刻みが進まず、1 刻みを 3 回押したらちょうど 3 刻み進んだか
1154 const SimCursor cursor = m_scheduler.Cursor();
1156 m_autoTimeHold = cursor;
1157
1158 const bool holdBroken = m_frameNumber == AUTO_TIME_HOLD_CHECK_FRAME &&
1159 (cursor != m_autoTimeHold || cursor.unit != 0);
1160 const SimCursor stepped{.tick = m_autoTimeHold.tick + AUTO_TIME_STEP_FRAMES.size(), .unit = 0};
1161 const bool stepBroken = m_frameNumber == AUTO_TIME_STEP_CHECK_FRAME && cursor != stepped;
1162 if (!holdBroken && !stepBroken)
1163 return;
1164
1165 m_autoTimeFailed = true;
1166 Log(Channel::Sim, Level::Error, "--auto-time: フレーム {} で刻み {}(単位 {})。期待は刻み {}(単位 0)",
1167 m_frameNumber, cursor.tick, cursor.unit, holdBroken ? m_autoTimeHold.tick : stepped.tick);
1168 }
1169
1171 const SimCursor cursor = m_scheduler.Cursor();
1172 const double seconds = m_lastIntervalSeconds;
1173 const auto perSecond = [&](uint64_t count) {
1174 return seconds > 0.0 ? static_cast<double>(count) / seconds : 0.0;
1175 };
1176
1177 const auto average = [](double sum, uint64_t count) {
1178 return count > 0 ? sum / static_cast<double>(count) : 0.0;
1179 };
1180
1181 const Stats& last = m_lastInterval;
1182
1183 return {.tick = cursor.tick,
1184 .unit = cursor.unit,
1185 .unitsPerTick = m_scheduler.UnitsPerTick(),
1186 .pendingTicks = m_scheduler.PendingTicks(),
1187 .droppedTicks = m_scheduler.DroppedTicks(),
1188 .hashedTick = m_latestHash.tick,
1189 .worldHash = m_latestHash.WorldHash(),
1190 .energyMillijoules = m_latestHash.energy,
1191 .scheduledBlocks = m_latestHash.scheduledBlocks,
1192 .framesPerSecond = perSecond(last.frames),
1193 .ticksPerSecond = perSecond(last.ticks),
1194 .cpuMilliseconds = average(last.cpuMilliseconds, last.frames),
1195 .simGpuMilliseconds = average(last.simGpuMilliseconds, last.simSubmissionsMeasured),
1196 .renderGpuMilliseconds = average(last.renderGpuMilliseconds, last.renderFramesMeasured),
1197 .budgetMilliseconds = m_scheduler.BudgetMilliseconds(),
1198 .view = m_viewController.Describe(),
1199 .replaying = !m_replay.empty(),
1200 .recording = !m_options.recordPath.empty(),
1201 .waitingCommands = m_pendingCommands.size(),
1202 .savePointTicks = SavePointTicks(),
1203 .saveIntervalTicks = m_options.saveIntervalTicks,
1204 .rewindUnavailable = RewindUnavailableReason(),
1205 .graph = MakeGraphPanelStatus(),
1206 .reactionTable = {.version = m_tableReload.AppliedVersion(),
1207 .swaps = m_tableReload.AppliedCount(),
1208 .failures = m_tableReload.FailedCount(),
1209 .watching = m_tableReload.Watching(),
1210 .waiting = m_tableReload.Waiting(),
1211 .lastFailed = m_tableReload.LastFailed(),
1212 .message = m_tableReload.LastMessage()}};
1213 }
1214
1215 // 性能のパネル(T-0143): 直近 1 秒の単位ごとの GPU 時間を 1 刻みあたりに、伝導のグラフのカウンタはそのまま
1217 const Stats& last = m_lastInterval;
1219 .layout = &m_sim.ConductStatsLayout(), .stats = last.conductGraph, .ticks = last.ticks};
1220
1221 const uint32_t unitsPerTick = m_sim.UnitsPerTick();
1222 for (uint32_t unit = 0; unit < last.unitGpuMilliseconds.size(); ++unit) {
1223 const double perTick = last.ticks > 0 ? last.unitGpuMilliseconds[unit] / static_cast<double>(last.ticks)
1224 : 0.0;
1225 const bool physics = m_sim.Physics() != nullptr && unit == sim::PROBE_UNIT_PHYSICS;
1226 const char* name = unit == sim::PROBE_UNIT_APPLY ? "コマンドの適用"
1227 : unit == sim::PROBE_UNIT_CONDUCT ? "伝導と反応"
1228 : physics ? "物理"
1229 : unit == unitsPerTick - 1 ? "ハッシュとイベント"
1230 : "重さの試験";
1231 status.units.push_back({.name = name,
1232 .workGraph = unit == sim::PROBE_UNIT_CONDUCT || physics,
1233 .millisecondsPerTick = perTick});
1234 }
1235
1236 return status;
1237 }
1238
1239 // --- 巻き戻し(保存点 + 再生。T-0143・ADR-0036)---
1240 // 保存点: saveIntervalTicks の倍数の刻みの境界で、そのフレームの先頭に S(刻み) を VRAM の保存点へ写す(GPU → GPU)。
1241 // SimScheduler がその境界でフレームを切るので、どの境界も必ずどれかのフレームの先頭になる。
1242 // 戻す: GPU を待って読み戻しを全部読み、カーソルを保存点の刻みへ。次のリストの先頭で世界を保存点の状態に戻し、
1243 // その刻み以降のコマンド(再生ファイル、なければ今の流れで足したコマンド)を足し直す。同じコマンドなら同じハッシュ列になる。
1244 // 戻した後のつつきは、足し直すコマンドに混ぜて刻みの順に足す(先の流れは変わりうるので、戻した刻みより先の保存点は捨てる)。
1245
1247 const uint32_t requested = m_options.savePoints == AUTO_SAVE_POINTS
1248 ? (m_options.editor ? DEFAULT_EDITOR_SAVE_POINTS : 0u)
1249 : std::min(m_options.savePoints, MAX_SAVE_POINTS);
1250 if (requested == 0)
1251 return true;
1252
1253 if (!m_sim.CreateSavePoints(m_device.Get(), requested)) {
1254 Log(Channel::Sim, Level::Error, "保存点を作れない({} 個)", requested);
1255 return false;
1256 }
1257
1258 m_savePointCount = requested;
1259 m_scheduler.SetBreakInterval(m_options.saveIntervalTicks);
1260 Log(Channel::Sim, Level::Info, "巻き戻し: 保存点 {} 個 × {:.1f} MiB、{} 刻みごと", requested,
1261 static_cast<double>(m_sim.SavePointBytes()) / (1024.0 * 1024.0), m_options.saveIntervalTicks);
1262
1263 return true;
1264 }
1265
1266 // 巻き戻せない理由(空なら巻き戻せる)。記録と CPU の物理の突き合わせとトレースは、刻みが一度ずつ進む前提なので
1268 if (m_savePointCount == 0)
1269 return "保存点なし(--save-points 0)";
1270
1271 if (!m_options.recordPath.empty())
1272 return "記録中は巻き戻さない";
1273
1274 if (m_physicsCheck)
1275 return "--check-physics の間は巻き戻さない";
1276
1277 if (!m_traceCapture.empty())
1278 return "トレースを集めている間は巻き戻さない";
1279
1280 return {};
1281 }
1282
1283 std::vector<uint64_t> FrameLoop::SavePointTicks() const {
1284 std::vector<uint64_t> ticks;
1285 for (uint32_t index = 0; index < m_savePointCount; ++index) {
1286 const uint64_t tick = m_sim.SavePointTick(index);
1287 if (tick != UINT64_MAX)
1288 ticks.push_back(tick);
1289 }
1290
1291 rng::sort(ticks);
1292
1293 return ticks;
1294 }
1295
1296 void FrameLoop::RequestRewind(uint64_t tick) {
1297 const std::string reason = RewindUnavailableReason();
1298 if (!reason.empty() || !rng::contains(SavePointTicks(), tick)) {
1299 Log(Channel::Sim, Level::Warning, "刻み {} へ巻き戻せない: {}", tick,
1300 reason.empty() ? "その保存点が無い" : reason);
1301 return;
1302 }
1303
1304 m_rewindRequest = tick;
1305 }
1306
1307 // GPU を待ち、読み戻しを全部読んでから、カーソルとコマンドを保存点の刻みへ戻す。戻す保存点の番号を返す
1309 const uint64_t tick = std::exchange(m_rewindRequest, UINT64_MAX);
1310 if (!m_compute.WaitIdle())
1311 return sim::NO_SAVE_POINT;
1312
1314 uint32_t index = sim::NO_SAVE_POINT;
1315 for (uint32_t candidate = 0; candidate < m_savePointCount; ++candidate) {
1316 if (m_sim.SavePointTick(candidate) == tick)
1317 index = candidate;
1318 }
1319
1320 if (index == sim::NO_SAVE_POINT)
1321 return index;
1322
1323 const SimCursor from = m_scheduler.Cursor();
1324 m_scheduler.Rewind(tick);
1325 m_sim.DiscardSavePointsAfter(tick);
1326 if (!m_replay.empty())
1327 m_replay.front().Rewind(tick);
1328
1329 // 今の流れで足したコマンドのうち、戻した刻み以降のものを足し直す(まだ足していなかった分の後ろに)
1330 const auto kept = rng::find_if(
1331 m_commandHistory, [&](const sim::ProbeCommand& command) { return command.targetTick >= tick; });
1332 std::deque<sim::ProbeCommand> feed(kept, m_commandHistory.end());
1333 feed.insert(feed.end(), m_rewindFeed.begin(), m_rewindFeed.end());
1334 m_rewindFeed = std::move(feed);
1335 m_commandHistory.erase(kept, m_commandHistory.end());
1336
1337 m_extractAfterRewind = true;
1338 ++m_rewindCount;
1339 Log(Channel::Sim, Level::Info, "巻き戻し: 刻み {}(単位 {})→ 保存点の刻み {}。足し直すコマンド: {}",
1340 from.tick, from.unit, tick,
1341 m_replay.empty() ? std::format("{} 個", m_rewindFeed.size()) : std::string("再生ファイルから"));
1342
1343 return index;
1344 }
1345
1346 // 保存点の刻みの境界から単位を始めるフレームなら、写す保存点の番号(刻み / 間隔 を数で回す)
1347 uint32_t FrameLoop::SaveSlotFor(SimCursor start, uint32_t unitCount, uint32_t restoreFrom) const {
1348 const uint64_t interval = m_options.saveIntervalTicks;
1349 const bool due = m_savePointCount > 0 && restoreFrom == sim::NO_SAVE_POINT && unitCount > 0 &&
1350 start.unit == 0 && start.tick > 0 && start.tick % interval == 0 &&
1351 m_simSubmissionCount > 0; // 最初のリストは世界を初期化する(写すのはその後)
1352 if (!due)
1353 return sim::NO_SAVE_POINT;
1354
1355 return static_cast<uint32_t>((start.tick / interval) % m_savePointCount);
1356 }
1357
1358 // 窓のクリックと、巻き戻した後に足し直すコマンド(endApplyTick より前の分)を、刻み・番号の順に混ぜる
1359 std::vector<sim::ProbeCommand> FrameLoop::TakeLiveCommands(uint64_t applyTick, uint64_t endApplyTick,
1360 uint32_t limit) {
1361 std::vector<sim::ProbeCommand> commands = TakeClickCommands(applyTick, limit);
1362 // クリックを足すなら、同じ刻みの足し直しも一緒に(後のフレームで、番号の小さいものを同じ刻みに足せなくなるので)
1363 const uint64_t feedEnd = commands.empty() ? endApplyTick : std::max(endApplyTick, applyTick + 1);
1364 while (!m_rewindFeed.empty() && m_rewindFeed.front().targetTick < feedEnd && commands.size() < limit) {
1365 const sim::ProbeCommand command = m_rewindFeed.front();
1366 m_rewindFeed.pop_front();
1367 if (command.targetTick < applyTick) {
1368 Log(Channel::Sim, Level::Error, "巻き戻し: 刻み {} のコマンドが足し直しに間に合わなかった(捨てた)",
1369 command.targetTick);
1370 continue;
1371 }
1372
1373 commands.push_back(command);
1374 }
1375
1376 rng::sort(commands, [](const sim::ProbeCommand& a, const sim::ProbeCommand& b) {
1377 return a.targetTick != b.targetTick ? a.targetTick < b.targetTick : a.sequence < b.sequence;
1378 });
1379 m_commandHistory.insert(m_commandHistory.end(), commands.begin(), commands.end());
1380
1381 return commands;
1382 }
1383
1384 // --auto-rewind: 保存点が 2 つできたら、古い方へ 1 回だけ戻す
1386 if (!m_options.autoRewind || m_rewindCount > 0 || m_rewindRequest != UINT64_MAX)
1387 return;
1388
1389 const std::vector<uint64_t> ticks = SavePointTicks();
1390 if (ticks.size() >= 2)
1391 RequestRewind(ticks.front());
1392 }
1393
1394 // --- 計測 ---
1395
1397 m_total.frames += frame.frames;
1398 m_total.cpuMilliseconds += frame.cpuMilliseconds;
1399 m_total.cpuMaxMilliseconds = std::max(m_total.cpuMaxMilliseconds, frame.cpuMaxMilliseconds);
1400 m_total.presentMilliseconds += frame.presentMilliseconds;
1401 m_total.ticks += frame.ticks;
1402 m_total.simSubmissions += frame.simSubmissions;
1403 m_total.units += frame.units;
1404 m_total.simGpuMilliseconds += frame.simGpuMilliseconds;
1405 m_total.simSubmissionsMeasured += frame.simSubmissionsMeasured;
1406 m_total.renderGpuMilliseconds += frame.renderGpuMilliseconds;
1407 m_total.renderFramesMeasured += frame.renderFramesMeasured;
1408 m_total.cpuWaits += frame.cpuWaits;
1409 m_total.skippedSubmissions += frame.skippedSubmissions;
1410 m_total.skippedExtractions += frame.skippedExtractions;
1411 m_total.events += frame.events;
1412 gpu::AccumulateGraphStats(m_total.conductGraph, frame.conductGraph);
1413 }
1414
1415 void FrameLoop::LogStats(const Stats& stats, Clock::duration elapsed, std::string_view label) const {
1416 const double seconds = chr::duration<double>(elapsed).count();
1417 if (seconds <= 0.0 || stats.frames == 0)
1418 return;
1419
1420 const auto average = [](double sum, uint64_t count) {
1421 return count > 0 ? sum / static_cast<double>(count) : 0.0;
1422 };
1423
1424 const auto perSecond = [&](uint64_t count) {
1425 return static_cast<double>(count) / seconds;
1426 };
1427
1429 "{}: {:.1f} fps CPU {:.3f} ms/フレーム(うち Present {:.3f}、最大 {:.3f}) 世界 {:.1f} 刻み/秒 "
1430 "シミュ {:.1f} 単位/投入・GPU {:.3f} ms/投入(予算 {:.2f}) 描画 GPU {:.3f} ms/フレーム "
1431 "捨てた刻み {} 見送り {}(抽出 {}) CPU の待ち {} イベント {} 状態 S({}) = {:016x}"
1432 "(エネルギー {} mJ・計算したブロック {})",
1433 label, perSecond(stats.frames), average(stats.cpuMilliseconds, stats.frames),
1434 average(stats.presentMilliseconds, stats.frames), stats.cpuMaxMilliseconds, perSecond(stats.ticks),
1435 average(static_cast<double>(stats.units), stats.simSubmissions),
1436 average(stats.simGpuMilliseconds, stats.simSubmissionsMeasured), m_scheduler.BudgetMilliseconds(),
1437 average(stats.renderGpuMilliseconds, stats.renderFramesMeasured), m_scheduler.DroppedTicks(),
1438 stats.skippedSubmissions, stats.skippedExtractions, stats.cpuWaits, stats.events, m_latestHash.tick,
1439 m_latestHash.hash, static_cast<int64_t>(m_latestHash.energy), m_latestHash.scheduledBlocks);
1440
1441 if (!stats.conductGraph.nodes.empty()) {
1442 Log(Channel::WorkGraph, Level::Info, "{}: {}", label,
1443 gpu::FormatGraphStats(m_sim.ConductStatsLayout(), stats.conductGraph));
1444 }
1445 }
1446
1447 // --- ループ ---
1448
1449 // 1 フレーム分(歩調の待ちの後)。デバイスの喪失や Present の失敗なら false
1450 bool FrameLoop::RunFrame(Clock::time_point frameStart) {
1452 QueueClicks();
1454 if (!SubmitSim())
1455 return false;
1456
1457 SubmitRender();
1458 BuildEditor();
1459 if (!SubmitEditor() || !SubmitScreenshot())
1460 return false;
1461
1462 const auto presentStart = Clock::now();
1463 const bool presented = m_swapChain.Present(m_options.vsync);
1464 m_interval.presentMilliseconds += Milliseconds(Clock::now() - presentStart);
1465 if (!presented || IsDeviceLost())
1466 return false;
1467
1468 const double cpuMilliseconds = Milliseconds(Clock::now() - frameStart);
1469 m_interval.cpuMilliseconds += cpuMilliseconds;
1470 m_interval.cpuMaxMilliseconds = std::max(m_interval.cpuMaxMilliseconds, cpuMilliseconds);
1471 ++m_interval.frames;
1472 ++m_frameNumber;
1473
1474 return true;
1475 }
1476
1479 "フレームのループを始める(vsync {} 先行 {} 目標 {} fps 重さ {}(分けて {} 個。1 刻み {} 単位) "
1480 "描画の優先度 {} 自動クリック {})",
1481 m_options.vsync ? "あり" : "なし", m_options.maxFrameLatency, m_options.targetFps, m_options.simLoad,
1482 m_options.simSplit, m_sim.UnitsPerTick(), m_options.renderHighPriority ? "HIGH" : "NORMAL",
1483 m_options.autoClick ? "あり" : "なし");
1484
1485 const render::OrbitCameraState& camera = m_viewController.Camera().State();
1486 Log(Channel::Render, Level::Info, "表示: {} カメラ 向き {:.0f}° 上下 {:.0f}° 距離 {:.0f}",
1487 m_viewController.Describe(), camera.yawDegrees, camera.pitchDegrees, camera.distance);
1488 if (!m_options.screenshotPath.empty() && m_options.frameLimit == 0 && !m_options.screenshotTick)
1489 Log(Channel::Render, Level::Warning, "--screenshot は --frames か --screenshot-tick と一緒に使う");
1490
1491 if (!m_replay.empty()) {
1492 Log(Channel::Sim, Level::Info, "再生: {}(コマンド {} 個、ハッシュ {} 個、刻み {} まで)",
1493 ToUtf8(m_options.replayPath.wstring()), m_replay.front().CommandCount(),
1494 m_replay.front().HashCount(), m_replay.front().LastTick());
1495 }
1496
1497 const auto start = Clock::now();
1498 auto lastFrame = start;
1499 auto intervalStart = start;
1500
1501 while (m_window->PumpMessages()) {
1502 if (m_options.frameLimit > 0 && m_frameNumber >= m_options.frameLimit)
1503 break;
1504
1505 // --screenshot-tick: 止まる刻みの画面を写した
1506 if (m_options.screenshotTick && !m_screenshot.empty())
1507 break;
1508
1509 // 最後のハッシュまで確かめた(--screenshot-tick なら写すまで続ける)
1510 if (!m_replay.empty() && m_replay.front().Finished() && !m_options.screenshotTick)
1511 break;
1512
1513 if (m_window->TakeResized() && !HandleResize())
1514 return Finish(true, start);
1515
1516 if (m_window->IsMinimized()) {
1517 std::this_thread::sleep_for(MINIMIZED_SLEEP);
1518 lastFrame = Clock::now(); // 最小化の間は世界を進めない
1519 continue;
1520 }
1521
1522 m_swapChain.WaitForFrame(FRAME_WAIT_TIMEOUT_MS); // フレームの歩調(CPU が GPU より先に行き過ぎない)
1523 const auto frameStart = Clock::now();
1524 m_timeControl.AdvanceRealTime(chr::duration<double>(frameStart - lastFrame).count(), m_scheduler);
1525 lastFrame = frameStart;
1526 if (!RunFrame(frameStart))
1527 return Finish(true, start);
1528
1529 if (frameStart - intervalStart < STATS_INTERVAL)
1530 continue;
1531
1532 LogStats(m_interval, frameStart - intervalStart, "1 秒");
1535 m_lastIntervalSeconds = chr::duration<double>(frameStart - intervalStart).count();
1536 m_interval = {};
1537 intervalStart = frameStart;
1538 }
1539
1540 return Finish(false, start);
1541 }
1542
1543 // 終わり: GPU を待って残りを読み、全体の要約を出す
1544 int FrameLoop::Finish(bool failed, Clock::time_point start) {
1545 if (failed || IsDeviceLost()) {
1546 if (IsDeviceLost())
1548 else {
1549 m_direct.Flush(); // 解放する前に GPU を終わらせる
1550 m_compute.Flush();
1551 }
1552
1553 Log(Channel::Core, Level::Error, "フレームのループが失敗で終わった");
1554
1555 return 1;
1556 }
1557
1558 m_direct.Flush(); // Present の分も待つ(バックバッファを解放する前に)
1559 m_compute.Flush();
1560 if (!WriteScreenshot())
1561 return 1;
1562
1563 if (m_options.screenshotTick && !m_options.screenshotPath.empty() && m_screenshot.empty()) {
1565 "--screenshot-tick: 刻み {} の画面を写せなかった(--frames {} のうちに届かなかった)",
1566 *m_options.screenshotTick, m_options.frameLimit);
1567
1568 return 1;
1569 }
1570
1573 LogStats(m_total, Clock::now() - start, "全体");
1574 const bool replayPassed = FinishReplay();
1575 if (!FinishTrace())
1576 return 1;
1577
1578 if (m_device.ValidationErrorCount() > 0) {
1579 Log(Channel::Gpu, Level::Error, "debug layer のエラーが {} 件", m_device.ValidationErrorCount());
1580 return 1;
1581 }
1582
1583 if (m_autoTimeFailed)
1584 return 1;
1585
1586 if (m_options.autoLab && (!m_editor || m_editor->Lab().AutoFailed())) {
1587 Log(Channel::Sim, Level::Error, "--auto-lab: 実験室の確かめが通らなかった(--editor が要る)");
1588 return 1;
1589 }
1590
1591 if (m_options.autoRewind && m_rewindCount == 0) {
1592 Log(Channel::Sim, Level::Error, "--auto-rewind: 巻き戻せなかった(保存点が 2 つできなかった)");
1593 return 1;
1594 }
1595
1596 return replayPassed ? 0 : 1;
1597 }
1598
1599 // --- 連鎖のトレース ---
1600
1601 // T が押された: 次に投げるフレームから始める。集めている途中なら無視する(GPU の範囲は 1 つだけ)
1603 if (!m_traceCapture.empty()) {
1604 Log(Channel::Sim, Level::Warning, "トレースを集めている途中なので T を無視した({})",
1605 ToUtf8(m_traceCapture.front().Path().wstring()));
1606 return;
1607 }
1608
1609 m_traceRequested = true;
1610 }
1611
1612 // 最後につついたセルの周り ±TRACE_KEY_RADIUS_CELLS を、このフレームの最初の丸ごとの刻みから TRACE_KEY_TICKS 刻み。
1613 // 刻みの途中から始めると、その刻みの前半(記録済みの単位)が欠けるので、まだ適用を記録していない最初の刻みから
1615 if (!m_traceRequested)
1616 return;
1617
1618 m_traceRequested = false;
1619 const uint64_t tickBegin = sim::ProbeSim::NextApplyTick(start.tick, start.unit);
1621 const auto below = [](uint32_t cell) {
1622 return cell - std::min(cell, TRACE_KEY_RADIUS_CELLS);
1623 };
1624 const std::array<uint32_t, 3> cellMin = {below(center.x), below(center.y), below(center.z)};
1625 const std::array<uint32_t, 3> cellMax = {
1626 center.x + TRACE_KEY_RADIUS_CELLS + 1, center.y + TRACE_KEY_RADIUS_CELLS + 1,
1627 center.z + TRACE_KEY_RADIUS_CELLS + 1}; // 格子の外は切り詰められる
1628
1630 tickBegin, tickBegin + TRACE_KEY_TICKS, cellMin, cellMax, TRACE_CAPACITY_PER_FRAME);
1631 const fs::path path = m_options.traceDirectory /
1632 std::format(L"trace-t{}-cell{}_{}_{}.txt", tickBegin, center.x, center.y, center.z);
1633
1634 m_sim.SetTraceFilter(filter);
1635 m_traceCapture.emplace_back(path, filter);
1636 Log(Channel::Sim, Level::Info, "トレースを始める: セル ({}, {}, {}) ±{}・刻み [{}, {}) → {}", center.x,
1637 center.y, center.z, TRACE_KEY_RADIUS_CELLS, tickBegin, tickBegin + TRACE_KEY_TICKS,
1638 ToUtf8(path.wstring()));
1639 }
1640
1641 // 読み戻した記録を足す。範囲の刻みを全部読んだら書いて、GPU の範囲を無効に戻す(使っていない間の費用は 0。docs/perf.md)
1643 if (m_traceCapture.empty())
1644 return;
1645
1646 TraceCapture& capture = m_traceCapture.front();
1647 capture.Add(readback.trace, readback.droppedTraceCount);
1648 if (!capture.IsComplete(m_latestHash.tick))
1649 return;
1650
1651 const auto written = capture.Write();
1652 if (!written) {
1653 Log(Channel::Sim, Level::Error, "{}", written.error());
1654 m_traceWriteFailed = true;
1655 }
1656
1657 m_traceCapture.clear();
1658 m_sim.SetTraceFilter({});
1659 }
1660
1661 // 終わるときに、集めている途中のトレース(--trace の終わりの無い範囲・終わっていない T)を書く
1663 if (!m_traceCapture.empty()) {
1664 const auto written = m_traceCapture.front().Write();
1665 m_traceCapture.clear();
1666 if (!written) {
1667 Log(Channel::Sim, Level::Error, "{}", written.error());
1668 return false;
1669 }
1670 }
1671
1672 return !m_traceWriteFailed;
1673 }
1674
1675 // 記録を書き、再生の結果を出す。記録を書けない・再生が合わない(終わっていない)なら false
1677 bool passed = true;
1678 if (!m_options.recordPath.empty()) {
1679 const save::ReplayFile replay = m_recorder.Build();
1680 const auto written = save::WriteReplayFile(m_options.recordPath, replay);
1681 if (written) {
1682 Log(Channel::Sim, Level::Info, "記録: {}(コマンド {} 個、ハッシュ {} 個)",
1683 ToUtf8(m_options.recordPath.wstring()), replay.commands.size(), replay.tickHashes.size());
1684 } else {
1685 Log(Channel::Sim, Level::Error, "記録を書けない: {}", written.error());
1686 passed = false;
1687 }
1688 }
1689
1690 if (m_replay.empty())
1691 return passed;
1692
1693 const save::ReplayPlayer& player = m_replay.front();
1694
1696 "再生: {}(ハッシュ 一致 {} / 不一致 {} / 全部 {}、間に合わなかったコマンド {}{})",
1697 player.Passed() ? "OK" : "FAILED", player.Matches(), player.Mismatches(), player.HashCount(),
1698 player.LateCommands(), player.Finished() ? "" : "、最後まで進む前に終わった");
1699
1700 return passed && player.Passed();
1701 }
1702
1703 } // namespace
1704
1705 int RunFrameLoop(const FrameLoopOptions& options) {
1706 auto parts = CreateParts(options);
1707 if (!parts) {
1708 Log(Channel::Core, Level::Error, "フレームのループを始められない: {}", parts.error());
1709 return 1;
1710 }
1711
1712 FrameLoop loop(options, std::move(*parts));
1713 if (!loop.CreateFrameSlots()) {
1714 Log(Channel::Gpu, Level::Error, "描画のフレームの枠を作れない");
1715 return 1;
1716 }
1717
1718 if (!loop.CreateEditor() || !loop.CreateSavePoints())
1719 return 1;
1720
1721 return loop.Run();
1722 }
1723
1724} // namespace bicameral::frame
static std::expected< std::unique_ptr< Window >, std::string > Create(std::wstring_view title, uint32_t clientWidth, uint32_t clientHeight)
Definition window.cpp:32
static std::expected< std::unique_ptr< EditorOverlay >, std::string > Create(Window &window, ID3D12Device *device, gpu::Queue &direct, DXGI_FORMAT format, uint32_t frameCount)
void Add(std::span< const gpu::GraphTraceRecord > records, uint32_t droppedCount)
std::expected< void, std::string > Write()
bool IsComplete(uint64_t latestStateTick) const
FrameLoop(const FrameLoopOptions &options, FrameLoopParts &&parts)
void LogStats(const Stats &stats, Clock::duration elapsed, std::string_view label) const
std::vector< render::ScreenshotCapture > m_screenshot
std::vector< sim::ProbeCommand > TakeClickCommands(uint64_t applyTick, uint32_t limit)
uint32_t SaveSlotFor(SimCursor start, uint32_t unitCount, uint32_t restoreFrom) const
void CollectTrace(const sim::ProbeFrameReadback &readback)
std::array< uint64_t, sim::PROBE_EXTRACTION_COUNT > m_lastRenderReading
std::vector< sim::ProbeCommand > TakeCommands(SimCursor start)
std::vector< sim::ProbeCommand > TakeLiveCommands(uint64_t applyTick, uint64_t endApplyTick, uint32_t limit)
void ReportEvents(const sim::ProbeFrameReadback &readback)
void TakeTableSwaps(SimCursor start, uint32_t limit, std::vector< sim::ProbeCommand > &commands)
void ApplyTimeRequest(const editor::TimeRequest &request)
void ReportSimReadback(const sim::ProbeFrameReadback &readback)
void CheckPhysics(const sim::ProbeTickHash &tickHash)
std::array< SimSubmission, SIM_SLOT_COUNT > m_simSubmissions
void FilterEditorInput(std::vector< InputEvent > &events) const
static std::expected< Device, std::string > Create(AdapterKind kind, const DeviceOptions &options=DefaultDeviceOptions())
Definition device.cpp:313
static std::expected< Queue, std::string > Create(ID3D12Device *device, D3D12_COMMAND_LIST_TYPE type, std::wstring_view name, D3D12_COMMAND_QUEUE_PRIORITY priority=D3D12_COMMAND_QUEUE_PRIORITY_NORMAL)
Definition queue.cpp:10
static constexpr DXGI_FORMAT FORMAT
Definition swap_chain.h:22
static constexpr uint32_t BUFFER_COUNT
Definition swap_chain.h:21
static std::expected< SwapChain, std::string > Create(ID3D12Device *device, IDXGIFactory6 *factory, ID3D12CommandQueue *directQueue, HWND window, uint32_t maxFrameLatency)
static std::expected< ProbeView, std::string > Create(ID3D12Device *device, DXGI_FORMAT renderTargetFormat)
static std::expected< ScreenshotCapture, std::string > Create(ID3D12Device *device, ID3D12Resource *backBuffer)
static std::expected< ReplayPlayer, std::string > Load(const fs::path &path)
uint32_t Push(const physics::PxVec3 &origin, const physics::PxVec3 &direction, uint32_t impulseMillinewtonSeconds)
static std::expected< ProbePeek, std::string > Create(ID3D12Device5 *device, const BakedReactionTable &table)
static constexpr uint32_t MAX_UNITS_PER_FRAME
Definition probe_sim.h:204
static std::expected< ProbeSim, std::string > Create(ID3D12Device5 *device, D3D12_COMMAND_LIST_TYPE listType, const BakedReactionTable &table, const ProbeSimOptions &options={})
static uint64_t NextApplyTick(uint64_t tick, uint32_t unit)
Definition probe_sim.h:222
static constexpr uint32_t FRAME_SLOT_COUNT
Definition probe_sim.h:203
std::expected< FrameLoopParts, std::string > CreateParts(const FrameLoopOptions &options)
constexpr std::array< uint64_t, 3 > AUTO_TIME_STEP_FRAMES
std::expected< std::shared_ptr< const script::LoadedReactionTable >, std::string > LoadWorldReactionTable(const FrameLoopOptions &options)
editor::TimeRequest AutoTimeRequest(uint64_t frame)
std::expected< sim::ProbeSim, std::string > CreateSimulation(ID3D12Device5 *device, const FrameLoopOptions &options, const sim::BakedReactionTable &reactionTable)
std::expected< std::unique_ptr< Window >, std::string > CreateWindowForLoop()
double TimestampMilliseconds(uint64_t ticks, uint64_t frequency)
void PushFromCommand(sim::PhysicsWorld &world, const sim::ProbeCommand &command)
gpu::DeviceOptions LoopDeviceOptions(const FrameLoopOptions &options)
constexpr uint32_t DEFAULT_EDITOR_SAVE_POINTS
Definition frame_loop.h:32
constexpr uint32_t MAX_SAVE_POINTS
Definition frame_loop.h:33
constexpr uint32_t TRACE_CAPACITY_PER_FRAME
Definition frame_loop.h:28
constexpr uint32_t AUTO_SAVE_POINTS
Definition frame_loop.h:31
int RunFrameLoop(const FrameLoopOptions &options)
DeviceRemovedReport LogDeviceRemoved(ID3D12Device *device)
Definition device.cpp:379
std::string FormatGraphStats(const GraphStatsLayout &layout, const GraphStatsSnapshot &snapshot)
bool ReadBuffer(ID3D12Resource *readback, std::span< std::byte > destination)
Definition resources.cpp:52
void AccumulateGraphStats(GraphStatsSnapshot &total, const GraphStatsSnapshot &frame)
ComPtr< ID3D12Resource > CreateBuffer(ID3D12Device *device, uint64_t sizeBytes, BufferKind kind)
Definition resources.cpp:18
DeviceOptions DefaultDeviceOptions()
Definition device.cpp:303
CellCoordinate CellOnSlice(uint32_t axis, uint32_t plane, uint32_t u, uint32_t v)
constexpr uint32_t PEEK_MAX_DEPTH
std::expected< void, std::string > WriteReplayFile(const fs::path &path, const ReplayFile &replay)
std::expected< LoadedReactionTable, std::string > LoadReactionTable(const ReactionTableSource &source)
constexpr uint32_t NO_SAVE_POINT
Definition probe_sim.h:133
PhysicsScene MakeProbeStackScene()
constexpr uint32_t PEEK_LEVEL_COUNT
Definition probe_peek.h:34
Command ProbeCommand
Definition probe_sim.h:53
ProbeCommand MakePokeCommand(uint64_t targetTick, uint32_t sequence, uint32_t x, uint32_t y, uint32_t z)
ProbeCommand MakePushCommand(uint64_t targetTick, uint32_t sequence, const std::array< int64_t, 3 > &origin, const std::array< int32_t, 3 > &direction, uint32_t impulseMillinewtonSeconds)
gpu::GraphTraceFilter ProbeTraceFilterForCells(uint64_t tickBegin, uint64_t tickEnd, std::array< uint32_t, 3 > cellMin, std::array< uint32_t, 3 > cellMax, uint32_t capacity)
void Log(Channel channel, Level level, FormatWithLocation< std::type_identity_t< Args >... > pattern, Args &&... args)
Definition log.h:140
std::string ToUtf8(std::wstring_view text)
Definition unicode.cpp:10
InputKind kind
Definition input.h:23
std::vector< UnitTime > units
Definition graph_panel.h:28
gpu::GraphTraceFilter trace
Definition frame_loop.h:63
std::shared_ptr< const script::LoadedReactionTable > reactionTable
std::vector< GraphNodeCounters > nodes
std::vector< sim::Command > commands
Definition replay_file.h:53
std::vector< ReplayTickHash > tickHashes
Definition replay_file.h:54
std::array< uint32_t, COMMAND_PAYLOAD_WORDS > payload
Definition command.h:24
std::vector< ProbeTickHash > hashes
Definition probe_sim.h:104
gpu::GraphStatsSnapshot graphStats
Definition probe_sim.h:116
std::vector< gpu::GraphTraceRecord > trace
Definition probe_sim.h:120
std::vector< uint64_t > unitGpuTicks
Definition probe_sim.h:107
std::vector< ProbeEvent > events
Definition probe_sim.h:102
uint64_t WorldHash() const
Definition probe_sim.h:97