Bicameral Engine
GPU-resident engine for a magic-research open-world game
Loading...
Searching...
No Matches
editor_overlay.cpp
Go to the documentation of this file.
1// editor_overlay.cpp — エディタの殻の Dear ImGui のパネル(T-0023)。考え方は editor_overlay.h。
2//
3// ImGui のヘッダは pch.h に入れない(pch.h は ImGui をリンクしないライブラリとテストでも使うので)。ImGui を使うのはこのファイルだけ。
4// ImGui の DX12 backend は <d3d12.h> を include するが、pch.h の <directx/d3d12.h>(Agility SDK と同じ版。ADR-0004)が先に入っていて、
5// 同じ include guard で飛ばされる。
7
8#include <algorithm>
9#include <format>
10
11#include <imgui.h>
12#include <imgui_impl_dx12.h>
13#include <imgui_impl_win32.h>
14
15#include "core/aliases.h"
16#include "core/log.h"
17#include "core/unicode.h"
18#include "gpu/resources.h"
19#include "platform/window.h"
20
21// imgui_impl_win32.h は windows.h に依存しないよう、この宣言を #if 0 で囲んでいる(使う側が書く約束)
22// NOLINTNEXTLINE(readability-identifier-naming) ImGui の名前
23extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
24
26 namespace {
27
28 // フォントの画像(1.92 からは大きさごとに増える)と、将来の画像の表示に足りる数
29 constexpr uint32_t SRV_DESCRIPTOR_COUNT = 64;
30 constexpr float FONT_SIZE_PIXELS = 18.0f;
31 constexpr float PANEL_MARGIN_PIXELS = 12.0f;
32
33 // 日本語の字がある Windows の標準のフォント(上から順に探す)
34 constexpr std::array<std::wstring_view, 3> JAPANESE_FONT_FILES = {L"meiryo.ttc", L"YuGothM.ttc",
35 L"msgothic.ttc"};
36
37 void Line(std::string_view text) {
38 ImGui::TextUnformatted(text.data(), text.data() + text.size());
39 }
40
41 std::string FormatSpeed(double speed) {
42 return speed >= 1.0 ? std::format("×{:g}", speed) : std::format("×1/{:g}", 1.0 / speed);
43 }
44
45 // 2 つの依頼を 1 つに(キーとボタンが同じフレームに来たとき)
47 return {.togglePause = a.togglePause != b.togglePause,
48 .stepTicks = a.stepTicks + b.stepTicks,
49 .speedSteps = a.speedSteps + b.speedSteps,
50 .resetSpeed = a.resetSpeed || b.resetSpeed,
51 .rewindTick = std::min(a.rewindTick, b.rewindTick)};
52 }
53
54 } // namespace
55
56 // --- 作る ---
57
58 EditorOverlay::EditorOverlay(Window& window, uint32_t frameCount) : m_window(window), m_frames(frameCount) {}
59
60 std::expected<std::unique_ptr<EditorOverlay>, std::string> EditorOverlay::Create(
61 Window& window, ID3D12Device* device, gpu::Queue& direct, DXGI_FORMAT format, uint32_t frameCount) {
62 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) コンストラクタが private なので make_unique を使えない
63 std::unique_ptr<EditorOverlay> overlay(new EditorOverlay(window, frameCount));
64 overlay->m_lab = std::make_unique<LabPanel>(device);
65 if (!overlay->CreateGpuObjects(device, frameCount))
66 return std::unexpected("エディタの描画のリソースを作れない");
67
68 IMGUI_CHECKVERSION();
69 ImGui::CreateContext();
70 overlay->m_contextCreated = true;
71
72 ImGuiIO& io = ImGui::GetIO();
73 io.IniFilename = nullptr; // 窓の配置を exe の横に書かない(テストの作業フォルダを汚さない)
74 ImGui::StyleColorsDark();
75 overlay->LoadJapaneseFont();
76
77 if (!overlay->InitializeBackends(device, direct, format))
78 return std::unexpected("ImGui の Win32 / DX12 の backend を作れない");
79
80 window.SetMessageHook([](HWND handle, UINT message, WPARAM wParam, LPARAM lParam) {
81 return ImGui_ImplWin32_WndProcHandler(handle, message, wParam, lParam) != 0;
82 });
83
84 Log(Channel::Render, Level::Info, "エディタの殻(Dear ImGui {})を作った", IMGUI_VERSION);
85
86 return overlay;
87 }
88
89 // 呼ぶ側が direct キューを待ってから壊す(frame/frame_loop の Finish)
91 m_window.SetMessageHook({});
93 ImGui_ImplDX12_Shutdown();
94
96 ImGui_ImplWin32_Shutdown();
97
99 ImGui::DestroyContext();
100 }
101
102 bool EditorOverlay::CreateGpuObjects(ID3D12Device* device, uint32_t frameCount) {
103 const D3D12_DESCRIPTOR_HEAP_DESC heapDesc{.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV,
104 .NumDescriptors = SRV_DESCRIPTOR_COUNT,
105 .Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE};
106 if (FAILED(device->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(&m_srvHeap))))
107 return false;
108
109 m_srvIncrement = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
110 for (uint32_t slot = SRV_DESCRIPTOR_COUNT; slot > 0; --slot)
111 m_freeSrvSlots.push_back(slot - 1);
112
113 for (FrameSlot& frame : m_frames) {
114 if (FAILED(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&frame.allocator))))
115 return false;
116 }
117
118 if (frameCount == 0 ||
119 FAILED(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_frames.front().allocator.Get(),
120 nullptr, IID_PPV_ARGS(&m_list)))) {
121 return false;
122 }
123
124 return SUCCEEDED(m_list->Close());
125 }
126
127 bool EditorOverlay::InitializeBackends(ID3D12Device* device, gpu::Queue& direct, DXGI_FORMAT format) {
128 m_win32Initialized = ImGui_ImplWin32_Init(m_window.Handle());
130 return false;
131
132 ImGui_ImplDX12_InitInfo info;
133 info.Device = device;
134 info.CommandQueue = direct.Native(); // フォントの画像の転送に使う
135 info.NumFramesInFlight = static_cast<int>(m_frames.size());
136 info.RTVFormat = format;
137 info.DSVFormat = DXGI_FORMAT_UNKNOWN;
138 info.UserData = this;
139 info.SrvDescriptorHeap = m_srvHeap.Get();
140 info.SrvDescriptorAllocFn = &EditorOverlay::AllocateDescriptor;
141 info.SrvDescriptorFreeFn = &EditorOverlay::FreeDescriptor;
142 m_dx12Initialized = ImGui_ImplDX12_Init(&info);
143
144 return m_dx12Initialized;
145 }
146
147 // 標準のフォントには日本語の字が無い。1.92 のフォントは使う字だけを後から作るので、字の範囲は要らない
149 std::array<wchar_t, MAX_PATH> windows{};
150 const UINT length = GetWindowsDirectoryW(windows.data(), static_cast<UINT>(windows.size()));
151 const fs::path fonts = fs::path(std::wstring_view(windows.data(), length)) / L"Fonts";
152
153 for (const std::wstring_view file : JAPANESE_FONT_FILES) {
154 const fs::path path = fonts / file;
155 std::error_code error;
156 if (!fs::exists(path, error))
157 continue;
158
159 const std::string utf8 = ToUtf8(path.wstring());
160 if (ImGui::GetIO().Fonts->AddFontFromFileTTF(utf8.c_str(), FONT_SIZE_PIXELS) != nullptr)
161 return;
162 }
163
164 Log(Channel::Render, Level::Warning, "日本語のフォントが見つからない(パネルの日本語は ? になる)");
165 }
166
167 // --- SRV の記述子 ---
168
169 void EditorOverlay::AllocateDescriptor(::ImGui_ImplDX12_InitInfo* info, D3D12_CPU_DESCRIPTOR_HANDLE* cpuHandle,
170 D3D12_GPU_DESCRIPTOR_HANDLE* gpuHandle) {
171 auto* overlay = static_cast<EditorOverlay*>(info->UserData);
172 IM_ASSERT(!overlay->m_freeSrvSlots.empty()); // 足りなければ SRV_DESCRIPTOR_COUNT を増やす
173
174 const uint32_t slot = overlay->m_freeSrvSlots.back();
175 overlay->m_freeSrvSlots.pop_back();
176
177 *cpuHandle = overlay->m_srvHeap->GetCPUDescriptorHandleForHeapStart();
178 cpuHandle->ptr += size_t{slot} * overlay->m_srvIncrement;
179 *gpuHandle = overlay->m_srvHeap->GetGPUDescriptorHandleForHeapStart();
180 gpuHandle->ptr += uint64_t{slot} * overlay->m_srvIncrement;
181 }
182
183 void EditorOverlay::FreeDescriptor(::ImGui_ImplDX12_InitInfo* info, D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle,
184 D3D12_GPU_DESCRIPTOR_HANDLE /*gpuHandle*/) {
185 auto* overlay = static_cast<EditorOverlay*>(info->UserData);
186 const size_t start = overlay->m_srvHeap->GetCPUDescriptorHandleForHeapStart().ptr;
187 overlay->m_freeSrvSlots.push_back(static_cast<uint32_t>((cpuHandle.ptr - start) / overlay->m_srvIncrement));
188 }
189
190 // --- 入力 ---
191
193 return ImGui::GetIO().WantCaptureMouse;
194 }
195
197 return ImGui::GetIO().WantCaptureKeyboard;
198 }
199
200 // Space = 止める / 動かす、N = 1 刻み(押しっぱなしで続けて進む)。文字を打っている間は効かない
202 if (WantsKeyboard())
203 return {};
204
205 TimeRequest request;
206 request.togglePause = ImGui::IsKeyPressed(ImGuiKey_Space, false);
207 if (time.Paused() && ImGui::IsKeyPressed(ImGuiKey_N, true))
208 request.stepTicks = 1;
209
210 return request;
211 }
212
213 // --- パネル ---
214
216 ImGui_ImplDX12_NewFrame();
217 ImGui_ImplWin32_NewFrame();
218 ImGui::NewFrame();
219
220 const TimeRequest keys = TakeShortcuts(time);
221 const TimeRequest buttons = BuildTimePanel(status, time);
222 BuildStatusPanel(status);
223 BuildGraphPanel(status.graph);
224 m_lab->Build();
225
226 ImGui::Render();
227
228 return Merge(keys, buttons);
229 }
230
232 TimeRequest request;
233 ImGui::SetNextWindowPos({PANEL_MARGIN_PIXELS, PANEL_MARGIN_PIXELS}, ImGuiCond_FirstUseEver);
234 ImGui::Begin("時間", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
235
236 // --- 止める・1 刻み ---
237 if (ImGui::Button(time.Paused() ? "動かす (Space)" : "止める (Space)"))
238 request.togglePause = true;
239
240 ImGui::SameLine();
241 ImGui::BeginDisabled(!time.Paused());
242 if (ImGui::Button("1 刻み (N)"))
243 request.stepTicks += 1;
244
245 ImGui::SameLine();
246 if (ImGui::Button("60 刻み"))
247 request.stepTicks += 60;
248
249 ImGui::EndDisabled();
250
251 // --- 速さ ---
252 if (ImGui::ArrowButton("##slower", ImGuiDir_Left))
253 request.speedSteps -= 1;
254
255 ImGui::SameLine();
256 Line(std::format("速さ {}", FormatSpeed(time.Speed())));
257 ImGui::SameLine();
258 if (ImGui::ArrowButton("##faster", ImGuiDir_Right))
259 request.speedSteps += 1;
260
261 ImGui::SameLine();
262 if (ImGui::Button("等速"))
263 request.resetSpeed = true;
264
265 // --- 今の刻み ---
266 ImGui::Separator();
267 if (!time.Paused())
268 Line(std::format("動いている: 刻み {}", status.tick));
269 else if (status.unit == 0)
270 Line(std::format("止まっている: 刻み {} の前", status.tick));
271 else
272 Line(std::format("止めている: 刻み {} の残りを進めている", status.tick));
273
274 Line(std::format("1 刻みずつ進めた: {} 刻み", time.SteppedTicks()));
275 request.rewindTick = BuildRewind(status);
276 ImGui::End();
277
278 return request;
279 }
280
281 // 巻き戻し(保存点 + 再生。T-0143・ADR-0036): 押した保存点の刻み(押さなければ UINT64_MAX)
282 uint64_t EditorOverlay::BuildRewind(const EditorStatus& status) const {
283 ImGui::SeparatorText("巻き戻す(保存点 + 再生)");
284 if (!status.rewindUnavailable.empty()) {
285 ImGui::TextDisabled("%s", status.rewindUnavailable.c_str());
286 return UINT64_MAX;
287 }
288
289 if (status.savePointTicks.empty()) {
290 ImGui::TextDisabled("保存点はまだ無い(%llu 刻みごとに写す)",
291 static_cast<unsigned long long>(status.saveIntervalTicks));
292 return UINT64_MAX;
293 }
294
295 uint64_t rewindTick = UINT64_MAX;
296 // 1 つ前 = 今の刻みより前の最新の保存点
297 const auto previous = std::find_if(status.savePointTicks.rbegin(), status.savePointTicks.rend(),
298 [&](uint64_t tick) { return tick < status.tick; });
299 ImGui::BeginDisabled(previous == status.savePointTicks.rend());
300 if (ImGui::Button("1 つ前の保存点へ") && previous != status.savePointTicks.rend())
301 rewindTick = *previous;
302
303 ImGui::EndDisabled();
304 for (const uint64_t tick : status.savePointTicks) {
305 ImGui::SameLine();
306 if (ImGui::SmallButton(std::format("{}##save", tick).c_str()))
307 rewindTick = tick;
308 }
309
310 return rewindTick;
311 }
312
313 // 反応表とホットリロード(T-0139): 世界の表の版・差し替えた回数・最後の読み直しの結果(誤りは赤で、ファイル:行:列 から)
315 ImGui::SeparatorText("反応表");
316 Line(std::format("版 {:016x} 差し替え {} 回 読めなかった {} 回", table.version, table.swaps, table.failures));
317 if (!table.watching)
318 ImGui::TextDisabled("ファイルを見ていない(再生中か、--editor でない)");
319 else if (table.waiting)
320 Line("読めた表が刻みの境界を待っている(止めているなら 1 刻み進める)");
321 else
322 ImGui::TextDisabled("パッケージのファイルを見ている(保存すると読み直す)");
323
324 if (table.message.empty())
325 return;
326
327 ImGui::PushTextWrapPos(ImGui::GetFontSize() * 40.0f);
328 if (table.lastFailed)
329 ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), "今の表のまま: %s", table.message.c_str());
330 else
331 ImGui::TextWrapped("%s", table.message.c_str());
332
333 ImGui::PopTextWrapPos();
334 }
335
337 ImGui::SetNextWindowPos({PANEL_MARGIN_PIXELS, 240.0f}, ImGuiCond_FirstUseEver);
338 ImGui::Begin("状態", nullptr, ImGuiWindowFlags_AlwaysAutoResize);
339
340 // --- 世界(読み戻しは数フレーム遅れる)---
341 Line(std::format("刻み {}(単位 {} / {}) 未処理 {:.1f} 刻み 捨てた刻み {}", status.tick, status.unit,
342 status.unitsPerTick, status.pendingTicks, status.droppedTicks));
343 Line(std::format("S({}) = {:016x}", status.hashedTick, status.worldHash));
344 Line(std::format("エネルギー {} mJ 計算したブロック {}", static_cast<int64_t>(status.energyMillijoules),
345 status.scheduledBlocks));
346
347 // --- 重さ(直近 1 秒)---
348 ImGui::Separator();
349 Line(std::format("{:.1f} fps 世界 {:.1f} 刻み/秒", status.framesPerSecond, status.ticksPerSecond));
350 Line(std::format("CPU {:.2f} ms/フレーム", status.cpuMilliseconds));
351 Line(std::format("シミュ GPU {:.2f} ms/投入(予算 {:.2f})", status.simGpuMilliseconds,
352 status.budgetMilliseconds));
353 Line(std::format("描画 GPU {:.2f} ms/フレーム", status.renderGpuMilliseconds));
354
355 // --- 入力と表示 ---
356 ImGui::Separator();
357 Line(std::format("表示: {}", status.view));
358 if (status.replaying)
359 Line("再生中(窓の操作は世界に入らない)");
360
361 if (status.recording)
362 Line("記録中");
363
364 Line(std::format("GPU に渡す前のコマンド: {}", status.waitingCommands));
366
367 if (ImGui::CollapsingHeader("操作")) {
368 Line("左クリック: つつく(断面) Shift + 左: 押す");
369 Line("右ドラッグ: 回る 中ドラッグ: 平行移動 ホイール: 寄る");
370 Line("1/2/3: 表示 X/Y/Z・Q/E: 断面 L: 対数 B: 活性 O: 透視 R: 戻す");
371 Line("T: トレース P: 覗き窓 Space: 止める N: 1 刻み");
372 }
373
374 ImGui::End();
375 }
376
377 // --- 描く ---
378
379 bool EditorOverlay::Submit(gpu::Queue& direct, ID3D12Resource* backBuffer,
380 D3D12_CPU_DESCRIPTOR_HANDLE renderTargetView) {
382 if (frame.fence != 0 && !direct.IsComplete(frame.fence))
383 direct.WaitCpu(frame.fence); // スワップチェインの待ちがあるので、ふつうは起きない
384
385 if (FAILED(frame.allocator->Reset()) || FAILED(m_list->Reset(frame.allocator.Get(), nullptr)))
386 return false;
387
388 const D3D12_RESOURCE_BARRIER toRenderTarget = gpu::Transition(backBuffer, D3D12_RESOURCE_STATE_PRESENT,
389 D3D12_RESOURCE_STATE_RENDER_TARGET);
390 m_list->ResourceBarrier(1, &toRenderTarget);
391 m_list->OMSetRenderTargets(1, &renderTargetView, FALSE, nullptr);
392
393 ID3D12DescriptorHeap* heaps[] = {m_srvHeap.Get()};
394 m_list->SetDescriptorHeaps(1, heaps);
395 ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), m_list.Get());
396
397 const D3D12_RESOURCE_BARRIER toPresent = gpu::Transition(backBuffer, D3D12_RESOURCE_STATE_RENDER_TARGET,
398 D3D12_RESOURCE_STATE_PRESENT);
399 m_list->ResourceBarrier(1, &toPresent);
400 if (FAILED(m_list->Close()))
401 return false;
402
403 frame.fence = direct.Submit(m_list.Get());
405
406 return true;
407 }
408
409} // namespace bicameral::editor
void SetMessageHook(MessageHook hook)
Definition window.h:50
static void AllocateDescriptor(::ImGui_ImplDX12_InitInfo *info, D3D12_CPU_DESCRIPTOR_HANDLE *cpuHandle, D3D12_GPU_DESCRIPTOR_HANDLE *gpuHandle)
bool CreateGpuObjects(ID3D12Device *device, uint32_t frameCount)
EditorOverlay(const EditorOverlay &)=delete
std::unique_ptr< LabPanel > m_lab
void BuildStatusPanel(const EditorStatus &status) const
TimeRequest TakeShortcuts(const TimeControl &time) const
ComPtr< ID3D12DescriptorHeap > m_srvHeap
bool Submit(gpu::Queue &direct, ID3D12Resource *backBuffer, D3D12_CPU_DESCRIPTOR_HANDLE renderTargetView)
uint64_t BuildRewind(const EditorStatus &status) const
void BuildReactionTable(const ReactionTableStatus &table) const
TimeRequest BuildTimePanel(const EditorStatus &status, const TimeControl &time) const
static std::expected< std::unique_ptr< EditorOverlay >, std::string > Create(Window &window, ID3D12Device *device, gpu::Queue &direct, DXGI_FORMAT format, uint32_t frameCount)
static void FreeDescriptor(::ImGui_ImplDX12_InitInfo *info, D3D12_CPU_DESCRIPTOR_HANDLE cpuHandle, D3D12_GPU_DESCRIPTOR_HANDLE gpuHandle)
ComPtr< ID3D12GraphicsCommandList > m_list
std::vector< uint32_t > m_freeSrvSlots
bool InitializeBackends(ID3D12Device *device, gpu::Queue &direct, DXGI_FORMAT format)
TimeRequest Build(const EditorStatus &status, const TimeControl &time)
std::vector< FrameSlot > m_frames
bool WaitCpu(uint64_t value) const
Definition queue.cpp:49
bool IsComplete(uint64_t value) const
Definition queue.h:43
ID3D12CommandQueue * Native() const
Definition queue.h:28
uint64_t Submit(std::span< ID3D12CommandList *const > lists)
Definition queue.cpp:28
IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
TimeRequest Merge(const TimeRequest &a, const TimeRequest &b)
constexpr std::array< std::wstring_view, 3 > JAPANESE_FONT_FILES
void BuildGraphPanel(const GraphPanelStatus &status)
D3D12_RESOURCE_BARRIER Transition(ID3D12Resource *resource, D3D12_RESOURCE_STATES before, D3D12_RESOURCE_STATES after)
Definition resources.cpp:65
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
std::vector< uint64_t > savePointTicks
ReactionTableStatus reactionTable