Bicameral Engine
GPU-resident engine for a magic-research open-world game
Loading...
Searching...
No Matches
lab_panel.cpp
Go to the documentation of this file.
1// lab_panel.cpp — エディタの「実験室」のパネル(lab_panel.h)。
2#include "editor/lab_panel.h"
3
4#include <algorithm>
5#include <format>
6#include <fstream>
7#include <iterator>
8
9#include <imgui.h>
10
11#include "core/log.h"
13
14namespace bicameral::editor {
15
16 namespace {
17
18 constexpr int MAX_KELVIN = static_cast<int>(lab::LAB_MAX_TEMPERATURE_MILLIKELVIN / 1000);
19 constexpr float SLICE_CELL_PIXELS = 22.0f;
20 constexpr float HOT_KELVIN = 2000.0f; // 断面の色がいちばん明るくなる温度
21
22 // --auto-lab の実験(tests/gpu_lab_box_test と同じ形を短く)
23 constexpr uint32_t AUTO_IGNITE_TICK = 3;
24 constexpr uint32_t AUTO_TICKS = 60;
25 constexpr uint32_t AUTO_IGNITE_MILLIKELVIN = 1500000;
26
27 int32_t TemperatureOf(const sim::BakedReactionTable& table, const reaction::RxCell& cell) {
28 return reaction::RxComputeThermal(table.View(), cell).temperature;
29 }
30
31 // 温度を色に(黒 → 赤 → 黄。表示だけなので浮動小数点を使う)
32 ImVec4 TemperatureColor(int32_t milliKelvin) {
33 const float t = std::clamp((static_cast<float>(milliKelvin) / 1000.0f - 300.0f) / (HOT_KELVIN - 300.0f),
34 0.0f, 1.0f);
35 return {0.15f + (0.85f * std::min(1.0f, t * 2.0f)), std::max(0.0f, (t * 2.0f) - 1.0f) * 0.9f, 0.1f, 1.0f};
36 }
37
38 sim::LabCellPosition PositionOf(const std::array<int, 3>& cell) {
39 return {.x = static_cast<uint32_t>(cell[0]),
40 .y = static_cast<uint32_t>(cell[1]),
41 .z = static_cast<uint32_t>(cell[2])};
42 }
43
44 } // namespace
45
46 LabPanel::LabPanel(ID3D12Device* device) {
47 if (device != nullptr)
48 static_cast<void>(device->QueryInterface(IID_PPV_ARGS(&m_device)));
49 }
50
53 if (!table) {
54 m_error = table.error();
55 return false;
56 }
57
58 if (!m_device) {
59 m_error = "ID3D12Device5 が無い";
60 return false;
61 }
62
63 auto session = sim::LabSession::Create(m_device.Get(), D3D12_COMMAND_LIST_TYPE_COMPUTE, *table);
64 if (!session) {
65 m_error = session.error();
66 return false;
67 }
68
69 m_session.emplace(std::move(*session));
71 m_material = std::min<int>(m_material, static_cast<int>(m_materials.size()) - 1);
72 Log(Channel::Sim, Level::Info, "実験室の箱を作った");
73
74 return true;
75 }
76
77 void LabPanel::Report(const std::expected<void, std::string>& result) {
78 if (!result) {
79 m_error = result.error();
80 m_running = false;
81 Log(Channel::Sim, Level::Error, "実験室: {}", m_error);
82 }
83
84 if (m_session && m_session->Mismatch())
85 m_running = false;
86 }
87
88 // --- 窓 ---
89
91 if (m_autoPending) {
92 m_autoPending = false;
93 RunAuto();
94 }
95
96 ImGui::SetNextWindowPos({12.0f, 420.0f}, ImGuiCond_FirstUseEver);
97 ImGui::Begin("実験室", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
98 if (!m_session) {
99 ImGui::TextUnformatted(
100 "小さな箱(8³ セル・0.5 m 角・300 K の空気)に物を置いて反応を試し、\n"
101 "GPU と CPU リファレンスを刻みごとに比べる(試験の表: 燃焼・熱分解)");
102 if (ImGui::Button("箱を作る"))
103 static_cast<void>(CreateSession());
104
105 if (!m_error.empty())
106 ImGui::TextColored({1.0f, 0.4f, 0.4f, 1.0f}, "%s", m_error.c_str());
107
108 ImGui::End();
109 return;
110 }
111
112 if (m_running)
113 Report(m_session->Step(1));
114
115 BuildStatus();
117 BuildSlice();
118 BuildCell();
120 ImGui::End();
121 }
122
124 ImGui::Text("次の刻み %llu ・ 置いて待っているコマンド %zu",
125 static_cast<unsigned long long>(m_session->NextTick()), m_session->PendingCommands());
126 if (const auto& mismatch = m_session->Mismatch(); mismatch) {
127 ImGui::TextColored({1.0f, 0.3f, 0.3f, 1.0f}, "GPU と CPU が食い違った: %s", mismatch->what.c_str());
128 if (mismatch->cell != lab::LAB_NO_CELL) {
129 ImGui::Text(" エネルギー CPU %lld mJ / GPU %lld mJ、物質の数 CPU %u / GPU %u",
130 static_cast<long long>(mismatch->cpu.energy), static_cast<long long>(mismatch->gpu.energy),
131 mismatch->cpu.speciesCount, mismatch->gpu.speciesCount);
132 }
133 } else if (m_session->NextTick() > 0) {
134 ImGui::TextColored({0.4f, 1.0f, 0.4f, 1.0f}, "刻み 0〜%llu で GPU と CPU がビット一致",
135 static_cast<unsigned long long>(m_session->NextTick() - 1));
136 }
137
138 if (const auto divergence = m_session->ReplayDivergence(); divergence)
139 ImGui::TextColored({1.0f, 0.6f, 0.2f, 1.0f}, "再生が記録と刻み %llu で違う",
140 static_cast<unsigned long long>(*divergence));
141
142 if (!m_error.empty())
143 ImGui::TextColored({1.0f, 0.4f, 0.4f, 1.0f}, "%s", m_error.c_str());
144 }
145
147 ImGui::SeparatorText("置く(次の刻みのコマンド)");
148 ImGui::SliderInt3("セル (x, y, z)", m_cell.data(), 0, static_cast<int>(sim::LAB_BOX_EDGE) - 1);
149
150 const char* preview = m_materials.empty() ? "" : m_materials[static_cast<size_t>(m_material)].name.data();
151 if (ImGui::BeginCombo("材料", preview)) {
152 for (size_t i = 0; i < m_materials.size(); ++i) {
153 if (ImGui::Selectable(m_materials[i].name.data(), static_cast<int>(i) == m_material))
154 m_material = static_cast<int>(i);
155 }
156
157 ImGui::EndCombo();
158 }
159
160 ImGui::SliderInt("温度 (K)", &m_temperatureKelvin, 0, MAX_KELVIN);
161 const auto milliKelvin = static_cast<uint32_t>(m_temperatureKelvin) * 1000u;
162 if (ImGui::Button("置く") && !m_materials.empty())
163 static_cast<void>(m_session->Place(PositionOf(m_cell),
164 m_materials[static_cast<size_t>(m_material)].contents, milliKelvin));
165
166 ImGui::SameLine();
167 if (ImGui::Button("温度だけ"))
168 static_cast<void>(m_session->SetTemperature(PositionOf(m_cell), milliKelvin));
169
170 // --- 刻む ---
171 ImGui::SeparatorText("刻む");
172 ImGui::BeginDisabled(m_session->Mismatch().has_value());
173 for (const uint32_t ticks : {1u, 10u, 60u}) {
174 if (ImGui::Button(std::format("{} 刻み", ticks).c_str()))
175 Report(m_session->Step(ticks));
176
177 ImGui::SameLine();
178 }
179
180 ImGui::Checkbox("流し続ける", &m_running);
181 ImGui::EndDisabled();
182 ImGui::SameLine();
183 if (ImGui::Button("最初に戻す")) {
184 Report(m_session->Reset());
185 m_error.clear();
186 m_message.clear();
187 }
188 }
189
190 // 選んだ z の断面の温度(CPU)。押すとセルを選ぶ
192 ImGui::SeparatorText(std::format("断面 z = {}(色 = CPU の温度)", m_cell[2]).c_str());
193 const sim::MultiresNest& cpu = m_session->Cpu();
194 for (int y = static_cast<int>(sim::LAB_BOX_EDGE) - 1; y >= 0; --y) {
195 for (int x = 0; x < static_cast<int>(sim::LAB_BOX_EDGE); ++x) {
196 const uint32_t index = multires::MrCellIndex(static_cast<uint32_t>(x), static_cast<uint32_t>(y),
197 static_cast<uint32_t>(m_cell[2]));
198 const int32_t temperature = TemperatureOf(m_session->Table(), sim::LabBoxCell(cpu, index));
199 const std::string id = std::format("##lab{}_{}", x, y);
200 const bool selected = x == m_cell[0] && y == m_cell[1];
201 if (selected)
202 ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f);
203
204 if (ImGui::ColorButton(id.c_str(), TemperatureColor(temperature), ImGuiColorEditFlags_NoTooltip,
205 {SLICE_CELL_PIXELS, SLICE_CELL_PIXELS})) {
206 m_cell[0] = x;
207 m_cell[1] = y;
208 }
209
210 if (selected)
211 ImGui::PopStyleVar();
212
213 if (ImGui::IsItemHovered())
214 ImGui::SetTooltip("(%d, %d, %d) %.1f K", x, y, m_cell[2],
215 static_cast<double>(temperature) / 1000.0);
216
217 if (x + 1 < static_cast<int>(sim::LAB_BOX_EDGE))
218 ImGui::SameLine(0.0f, 2.0f);
219 }
220 }
221 }
222
223 // 選んだセルの成分と温度を CPU と GPU で並べる
224 void LabPanel::BuildCell() const {
225 const uint32_t index = PositionOf(m_cell).Index();
226 const reaction::RxCell cpu = sim::LabBoxCell(m_session->Cpu(), index);
227 const reaction::RxCell gpu = sim::LabBoxCell(m_session->Gpu(), index);
228 const sim::BakedReactionTable& table = m_session->Table();
229
230 ImGui::SeparatorText(std::format("セル ({}, {}, {})", m_cell[0], m_cell[1], m_cell[2]).c_str());
231 ImGui::Text("温度 CPU %.1f K / GPU %.1f K エネルギー CPU %lld mJ / GPU %lld mJ",
232 static_cast<double>(TemperatureOf(table, cpu)) / 1000.0,
233 static_cast<double>(TemperatureOf(table, gpu)) / 1000.0, static_cast<long long>(cpu.energy),
234 static_cast<long long>(gpu.energy));
235 if (!ImGui::BeginTable("lab_cell", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit))
236 return;
237
238 ImGui::TableSetupColumn("物質");
239 ImGui::TableSetupColumn("CPU (µmol)");
240 ImGui::TableSetupColumn("GPU (µmol)");
241 ImGui::TableHeadersRow();
242 for (uint32_t species = 1; species < table.speciesNames.size(); ++species) {
243 const auto amountOf = [&](const reaction::RxCell& cell) {
244 for (uint32_t i = 0; i < cell.speciesCount; ++i) {
245 if (cell.species[i] == species)
246 return cell.amounts[i];
247 }
248
249 return uint64_t{0};
250 };
251
252 const uint64_t a = amountOf(cpu);
253 const uint64_t b = amountOf(gpu);
254 if (a == 0 && b == 0)
255 continue;
256
257 ImGui::TableNextRow();
258 ImGui::TableNextColumn();
259 ImGui::TextUnformatted(table.speciesNames[species].c_str());
260 ImGui::TableNextColumn();
261 ImGui::Text("%llu", static_cast<unsigned long long>(a));
262 ImGui::TableNextColumn();
263 if (a == b)
264 ImGui::Text("%llu", static_cast<unsigned long long>(b));
265 else
266 ImGui::TextColored({1.0f, 0.3f, 0.3f, 1.0f}, "%llu", static_cast<unsigned long long>(b));
267 }
268
269 ImGui::EndTable();
270 }
271
272 // 記録(コマンドの列 + ハッシュの列)の保存と、読み込んでの再生
274 ImGui::SeparatorText("記録");
275 std::array<char, 260> path{};
276 std::copy_n(m_recordingPath.begin(), std::min(m_recordingPath.size(), path.size() - 1), path.begin());
277 if (ImGui::InputText("ファイル", path.data(), path.size()))
278 m_recordingPath = path.data();
279
280 if (ImGui::Button("記録を保存")) {
281 const std::vector<std::byte> bytes = sim::SerializeLabRecording(m_session->Recording());
282 std::ofstream file(m_recordingPath, std::ios::binary);
283 file.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
284 m_message = file ? std::format("{} 刻み・コマンド {} 個を保存した", m_session->NextTick(),
285 m_session->Recording().commands.size())
286 : "保存できない";
287 }
288
289 ImGui::SameLine();
290 if (ImGui::Button("読んで再生")) {
291 std::ifstream file(m_recordingPath, std::ios::binary);
292 const std::vector<char> chars{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
293 const auto recording = sim::ParseLabRecording(std::as_bytes(std::span(chars)));
294 if (!recording) {
295 m_message = recording.error();
296 } else {
297 Report(m_session->Replay(*recording));
298 m_message = std::format("{} 刻みを再生した", recording->tickCount);
299 }
300 }
301
302 if (!m_message.empty())
303 ImGui::TextUnformatted(m_message.c_str());
304 }
305
306 // --- 人がいない確認(--auto-lab)---
307
309 m_autoFailed = true;
310 if (!CreateSession()) {
311 Log(Channel::Sim, Level::Error, "--auto-lab: 箱を作れない: {}", m_error);
312 return;
313 }
314
315 const auto wood = std::ranges::find_if(m_materials, [](const sim::LabMaterial& m) { return m.name == "木"; });
316 if (wood == m_materials.end())
317 return;
318
319 for (uint32_t i = 0; i < 8; ++i)
320 static_cast<void>(m_session->Place({.x = 3 + (i & 1u), .y = 3 + ((i >> 1) & 1u), .z = 3 + (i >> 2)},
321 wood->contents, 300000));
322
323 auto result = m_session->Step(AUTO_IGNITE_TICK);
324 static_cast<void>(m_session->SetTemperature({.x = 3, .y = 3, .z = 3}, AUTO_IGNITE_MILLIKELVIN));
325 if (result)
326 result = m_session->Step(AUTO_TICKS - AUTO_IGNITE_TICK);
327
328 const sim::LabRecording recorded = m_session->Recording();
329 const auto parsed = sim::ParseLabRecording(sim::SerializeLabRecording(recorded));
330 if (result && parsed)
331 result = m_session->Replay(*parsed);
332
333 if (!result || !parsed || m_session->Mismatch() || m_session->ReplayDivergence() ||
334 m_session->Recording().hashes != recorded.hashes) {
335 Log(Channel::Sim, Level::Error, "--auto-lab: 失敗({})",
336 !result ? result.error() : (m_session->Mismatch() ? m_session->Mismatch()->what : "再生が記録と違う"));
337 return;
338 }
339
340 m_autoFailed = false;
341 m_cell = {3, 3, 3};
342 Log(Channel::Sim, Level::Info, "--auto-lab: {} 刻みで GPU と CPU が一致し、記録から再生しても同じ", AUTO_TICKS);
343 }
344
345} // namespace bicameral::editor
ComPtr< ID3D12Device5 > m_device
Definition lab_panel.h:47
std::array< int, 3 > m_cell
Definition lab_panel.h:54
std::optional< sim::LabSession > m_session
Definition lab_panel.h:48
void Report(const std::expected< void, std::string > &result)
Definition lab_panel.cpp:77
std::vector< sim::LabMaterial > m_materials
Definition lab_panel.h:49
LabPanel(ID3D12Device *device)
Definition lab_panel.cpp:46
static std::expected< LabSession, std::string > Create(ID3D12Device5 *device, D3D12_COMMAND_LIST_TYPE queueType, const BakedReactionTable &table)
int32_t TemperatureOf(const sim::BakedReactionTable &table, const reaction::RxCell &cell)
Definition lab_panel.cpp:27
sim::LabCellPosition PositionOf(const std::array< int, 3 > &cell)
Definition lab_panel.cpp:38
std::expected< LabRecording, std::string > ParseLabRecording(std::span< const std::byte > bytes)
Definition lab_box.cpp:271
RxCell LabBoxCell(const MultiresNest &nest, uint32_t index)
Definition lab_box.cpp:179
std::vector< LabMaterial > MakeLabMaterials(const BakedReactionTable &table)
Definition lab_box.cpp:122
constexpr uint32_t LAB_BOX_EDGE
Definition lab_box.h:30
std::expected< BakedReactionTable, std::string > BakeReactionTable(const ReactionTableDefinition &definition)
ReactionTableDefinition MakeCombustionTestTable()
std::vector< std::byte > SerializeLabRecording(const LabRecording &recording)
Definition lab_box.cpp:254
void Log(Channel channel, Level level, FormatWithLocation< std::type_identity_t< Args >... > pattern, Args &&... args)
Definition log.h:140
std::vector< std::string > speciesNames
ReactionTableView View() const
std::string_view name
Definition lab_box.h:62
std::vector< uint64_t > hashes
Definition lab_box.h:107