Bicameral Engine
GPU-resident engine for a magic-research open-world game
Loading...
Searching...
No Matches
work_graph_stats.cpp
Go to the documentation of this file.
1// work_graph_stats.cpp — Work Graphs のノードごとのカウンタの読み戻し・要約・上限の検出(T-0008、16 §1)。
3
4#include <algorithm>
5#include <format>
6#include <span>
7
8#include "core/log.h"
9
10namespace bicameral::gpu {
11 namespace {
12
13 constexpr uint32_t KIND_COUNT = static_cast<uint32_t>(GraphFindingKind::Count);
14 constexpr uint32_t FINDING_SLOTS_PER_KIND = std::max(WG_STATS_MAX_NODES, WG_STATS_MAX_GAUGES);
15
16 GraphNodeCounters ReadNode(std::span<const uint32_t> words, uint32_t node) {
17 const std::span<const uint32_t> entry = words.subspan(size_t{node} * WG_NODE_WORDS, WG_NODE_WORDS);
18 return {.launches = entry[WG_NODE_LAUNCHES],
19 .inputRecords = entry[WG_NODE_INPUT_RECORDS],
20 .outputRecords = entry[WG_NODE_OUTPUT_RECORDS],
21 .refusedOutputs = entry[WG_NODE_REFUSED_OUTPUTS],
22 .peakRequestedOutputs = entry[WG_NODE_PEAK_REQUESTED],
23 .deepestRecursion = entry[WG_NODE_DEEPEST_RECURSION],
24 .refusedRecursions = entry[WG_NODE_REFUSED_RECURSIONS]};
25 }
26
27 // --- 上限の判定(ノード 1 つ・計器 1 つ)---
28
29 void EvaluateNode(const GraphNodeLimits& limits, const GraphNodeCounters& counters, uint32_t index,
30 std::vector<GraphFinding>& findings) {
31 if (counters.refusedOutputs > 0) {
32 findings.push_back({.kind = GraphFindingKind::OutputsRefused,
33 .index = index,
34 .text = std::format("ノード {}: 出力の上限 {} を超える要求を {} 件止めた"
35 "(1 回の起動の要求の最大 {})",
36 limits.name, limits.maxOutputRecords, counters.refusedOutputs,
37 counters.peakRequestedOutputs)});
38 } else if (limits.warnOutputRecords > 0 && counters.peakRequestedOutputs >= limits.warnOutputRecords) {
39 findings.push_back(
41 .index = index,
42 .text = std::format("ノード {}: 1 回の起動の出力の要求 {} が上限 {} に近い", limits.name,
43 counters.peakRequestedOutputs, limits.maxOutputRecords)});
44 }
45
46 if (counters.refusedRecursions > 0) {
47 findings.push_back({.kind = GraphFindingKind::RecursionRefused,
48 .index = index,
49 .text = std::format("ノード {}: 再帰の深さの上限 {} で {} 回止めた", limits.name,
50 limits.maxRecursionDepth, counters.refusedRecursions)});
51 } else if (limits.maxRecursionDepth > 0 && counters.deepestRecursion > 0 &&
52 uint64_t{counters.deepestRecursion} * 4 >= uint64_t{limits.maxRecursionDepth} * 3) {
53 findings.push_back({.kind = GraphFindingKind::RecursionNearLimit,
54 .index = index,
55 .text = std::format("ノード {}: 再帰の深さ {} が上限 {} に近い", limits.name,
56 counters.deepestRecursion, limits.maxRecursionDepth)});
57 }
58 }
59
60 void EvaluateGauge(const GraphGaugeLimits& limits, uint32_t peak, uint32_t index,
61 std::vector<GraphFinding>& findings) {
62 if (limits.capacity == 0)
63 return;
64
65 if (peak > limits.capacity) {
66 findings.push_back({.kind = GraphFindingKind::GaugeOverCapacity,
67 .index = index,
68 .text = std::format("{}: 容量 {} を超えて {} まで使おうとした(溢れた分は落ちた)",
69 limits.name, limits.capacity, peak)});
70 } else if (uint64_t{peak} * 100 >= uint64_t{limits.capacity} * limits.warnPercent) {
71 findings.push_back({.kind = GraphFindingKind::GaugeNearCapacity,
72 .index = index,
73 .text = std::format("{}: 使った量の最大 {} が容量 {} に近い", limits.name, peak,
74 limits.capacity)});
75 }
76 }
77
78 } // namespace
79
80 // --- 要約 ---
81
83 // 読めなかったフレーム
84 if (frame.nodes.empty() && frame.gaugePeaks.empty())
85 return;
86
87 if (total.nodes.size() != frame.nodes.size() || total.gaugePeaks.size() != frame.gaugePeaks.size()) {
88 total = frame;
89 return;
90 }
91
92 for (size_t index = 0; index < frame.nodes.size(); ++index) {
93 GraphNodeCounters& sum = total.nodes[index];
94 const GraphNodeCounters& add = frame.nodes[index];
95 sum.launches += add.launches;
96 sum.inputRecords += add.inputRecords;
100 sum.deepestRecursion = std::max(sum.deepestRecursion, add.deepestRecursion);
102 }
103
104 for (size_t index = 0; index < frame.gaugePeaks.size(); ++index)
105 total.gaugePeaks[index] = std::max(total.gaugePeaks[index], frame.gaugePeaks[index]);
106 }
107
108 std::string FormatGraphStats(const GraphStatsLayout& layout, const GraphStatsSnapshot& snapshot) {
109 std::string text = layout.name + ":";
110 const char* separator = " ";
111 for (size_t index = 0; index < layout.nodes.size() && index < snapshot.nodes.size(); ++index) {
112 const GraphNodeCounters& node = snapshot.nodes[index];
113 text += std::format("{}{} 起動 {}・入力 {}・出力 {}", separator, layout.nodes[index].name, node.launches,
114 node.inputRecords, node.outputRecords);
115 if (node.refusedOutputs > 0)
116 text += std::format("・止めた出力 {}", node.refusedOutputs);
117
118 if (layout.nodes[index].maxRecursionDepth > 0)
119 text += std::format("・深さ {}/{}", node.deepestRecursion, layout.nodes[index].maxRecursionDepth);
120
121 if (node.refusedRecursions > 0)
122 text += std::format("・止めた再帰 {}", node.refusedRecursions);
123
124 separator = " | ";
125 }
126
127 for (size_t index = 0; index < layout.gauges.size() && index < snapshot.gaugePeaks.size(); ++index) {
128 text += std::format("{}{} 最大 {}/{}", separator, layout.gauges[index].name, snapshot.gaugePeaks[index],
129 layout.gauges[index].capacity);
130 separator = " | ";
131 }
132
133 return text;
134 }
135
136 std::vector<GraphFinding> EvaluateGraphStats(const GraphStatsLayout& layout, const GraphStatsSnapshot& snapshot) {
137 std::vector<GraphFinding> findings;
138 for (size_t index = 0; index < layout.nodes.size() && index < snapshot.nodes.size(); ++index)
139 EvaluateNode(layout.nodes[index], snapshot.nodes[index], static_cast<uint32_t>(index), findings);
140
141 for (size_t index = 0; index < layout.gauges.size() && index < snapshot.gaugePeaks.size(); ++index)
142 EvaluateGauge(layout.gauges[index], snapshot.gaugePeaks[index], static_cast<uint32_t>(index), findings);
143
144 return findings;
145 }
146
147 // --- GPU のバッファ ---
148
150 : m_ring(std::move(ring)),
151 m_layout(std::move(layout)),
152 m_lastWarnedReport(size_t{KIND_COUNT} * FINDING_SLOTS_PER_KIND, 0) {}
153
154 std::expected<WorkGraphStats, std::string> WorkGraphStats::Create(ID3D12Device* device, GraphStatsLayout layout,
155 uint32_t slotCount) {
156 if (layout.nodes.size() > WG_STATS_MAX_NODES || layout.gauges.size() > WG_STATS_MAX_GAUGES) {
157 return std::unexpected(std::format("{}: ノード {} 個・計器 {} 個は多すぎる(最大 {}・{})", layout.name,
158 layout.nodes.size(), layout.gauges.size(), WG_STATS_MAX_NODES,
159 WG_STATS_MAX_GAUGES));
160 }
161
162 // 見出し = 全体(読み戻すたびに全部を 0 に戻す)
163 auto ring = ReadbackRing::Create(device, WG_STATS_BYTES, WG_STATS_BYTES, slotCount, L"WorkGraphStats");
164 if (!ring)
165 return std::unexpected("Work Graphs のカウンタを作れない: " + ring.error());
166
167 return WorkGraphStats(std::move(*ring), std::move(layout));
168 }
169
170 void WorkGraphStats::RecordBegin(ID3D12GraphicsCommandList* list) const {
171 m_ring.RecordBegin(list);
172 }
173
174 void WorkGraphStats::RecordReadbackAndReset(ID3D12GraphicsCommandList* list, uint32_t slot) const {
175 m_ring.RecordReadbackAndReset(list, slot);
176 }
177
178 std::expected<GraphStatsSnapshot, std::string> WorkGraphStats::Read(uint32_t slot) const {
179 std::vector<uint32_t> words(WG_STATS_WORDS);
180 if (!m_ring.Read(slot, std::as_writable_bytes(std::span(words))))
181 return std::unexpected(std::format("{}: カウンタを読み戻せない(slot {})", m_layout.name, slot));
182
183 GraphStatsSnapshot snapshot;
184 snapshot.nodes.reserve(m_layout.nodes.size());
185 for (uint32_t node = 0; node < m_layout.nodes.size(); ++node)
186 snapshot.nodes.push_back(ReadNode(words, node));
187
188 const std::span<const uint32_t> gauges = std::span(words).subspan(size_t{WG_STATS_MAX_NODES} * WG_NODE_WORDS,
189 WG_STATS_MAX_GAUGES);
190 snapshot.gaugePeaks.assign(gauges.begin(), gauges.begin() + static_cast<ptrdiff_t>(m_layout.gauges.size()));
191
192 return snapshot;
193 }
194
195 std::vector<GraphFinding> WorkGraphStats::Report(const GraphStatsSnapshot& snapshot) {
197 if (GetLogger().IsEnabled(Level::Trace)) // 捨てるなら要約を作らない
199 std::vector<GraphFinding> findings = EvaluateGraphStats(m_layout, snapshot);
200
201 for (const GraphFinding& finding : findings) {
202 uint64_t& last = m_lastWarnedReport[size_t{static_cast<uint32_t>(finding.kind)} * FINDING_SLOTS_PER_KIND +
203 finding.index];
204 if (last != 0 && m_reportCount - last < WARNING_REPEAT_REPORTS)
205 continue;
206
207 last = m_reportCount;
208 Log(Channel::WorkGraph, Level::Warning, "{}", finding.text);
209 }
210
211 return findings;
212 }
213
214} // namespace bicameral::gpu
static std::expected< ReadbackRing, std::string > Create(ID3D12Device *device, uint64_t bytes, uint64_t headerBytes, uint32_t slotCount, std::wstring_view name)
void RecordReadbackAndReset(ID3D12GraphicsCommandList *list, uint32_t slot=0) const
std::expected< GraphStatsSnapshot, std::string > Read(uint32_t slot=0) const
WorkGraphStats(ReadbackRing &&ring, GraphStatsLayout &&layout)
std::vector< GraphFinding > Report(const GraphStatsSnapshot &snapshot)
static std::expected< WorkGraphStats, std::string > Create(ID3D12Device *device, GraphStatsLayout layout, uint32_t slotCount=1)
static constexpr uint64_t WARNING_REPEAT_REPORTS
std::vector< uint64_t > m_lastWarnedReport
void RecordBegin(ID3D12GraphicsCommandList *list) const
void EvaluateGauge(const GraphGaugeLimits &limits, uint32_t peak, uint32_t index, std::vector< GraphFinding > &findings)
GraphNodeCounters ReadNode(std::span< const uint32_t > words, uint32_t node)
void EvaluateNode(const GraphNodeLimits &limits, const GraphNodeCounters &counters, uint32_t index, std::vector< GraphFinding > &findings)
std::string FormatGraphStats(const GraphStatsLayout &layout, const GraphStatsSnapshot &snapshot)
std::vector< GraphFinding > EvaluateGraphStats(const GraphStatsLayout &layout, const GraphStatsSnapshot &snapshot)
void AccumulateGraphStats(GraphStatsSnapshot &total, const GraphStatsSnapshot &frame)
Logger & GetLogger()
Definition log.h:120
void Log(Channel channel, Level level, FormatWithLocation< std::type_identity_t< Args >... > pattern, Args &&... args)
Definition log.h:140
std::vector< GraphNodeLimits > nodes
std::vector< GraphGaugeLimits > gauges
std::vector< uint32_t > gaugePeaks
std::vector< GraphNodeCounters > nodes