Bicameral Engine
GPU-resident engine for a magic-research open-world game
Loading...
Searching...
No Matches
multires_conduction.cpp
Go to the documentation of this file.
1// multires_conduction.cpp — 多重解像度の木の上の熱の伝導の CPU リファレンス(07 §1・17 §5「熱の伝導」。T-0019)。
2// 式と面の隣の探し方は shaders/common/multires_conduction.hlsli(GPU も同じ関数を呼ぶ。T-0107)。
3// 1 刻みの順(multires_nest.cpp の StepBlocks から呼ぶ):
4// 1. MarkConductionWants: 刻むブロックのセルの面の流れを調べ、流れのある一様なブロックに MR_PAGE_WANTED を付け、
5// 端数が要る粗いブロックに印を付ける(どちらも「端数の枠がある」とした場合の流れで決める)
6// 2. (呼ぶ側)頁を枠の順に配る。足りなかったブロックはこの刻みは凍らせる(刻まず、面の流れも 0)
7// 3. ComputeConduction: 端数の枠を枠の順に配り、刻むブロックのセルの面の流れを変化の表に足す
8// 4. (呼ぶ側)変化を足してから反応
9// 流れはどれも刻みの初めのセルから計算する(Jacobi 型。順に依存しない)。頁に広げても論理のセルは変わらないので、
10// セルの熱は刻みの中で 1 回だけ作って使い回す(CellThermals)。
11// 同じレベルの面は両側が同じ式で逆向きの値を出す(どちらも刻むことは活性が保証する。17 §5「活性」)。違うレベルの面は細かい側だけが
12// 計算して、粗いセルの変化(整数部 + 端数)を表に足す。浮動小数点は使わない(engine/src/sim は検査の対象)。
13// 細かいレベルの刻み(T-0108。StepConduction): レベル k の伝導を 1 刻みに 4^σ(k) 回の小刻みに分け、上の 1〜3 を小刻みごとに繰り返す。
14// 小刻みでは、その回に小刻みが始まるレベルのブロックだけが流れを計算し、変化は表に溜めて、そのレベルの小刻みの終わりに足す
15// (粗い側の値は粗い小刻みの初めのまま。細かい側が送った流れの合計を粗い側が 1 回で受ける = Berger–Colella の refluxing の形)。
16// 活性(StepActive)では、小刻みで変わったブロックの面の隣を起こし、以後の小刻みから刻む(流れのある面は両側とも刻む、を小刻みでも保つ)。
17// 細かいレベルの熱の陰解法(T-0119。options.implicitConduction): 基準より細かい本物のブロックの面はここでは計算せず、最後に
18// AddImplicitConduction(multires_implicit_conduction.cpp)が陰解法の 1 刻みで解いて変化の表に足す(印と頁・端数の枠はここと同じ)。
19#include <algorithm>
20#include <array>
21
22#include "common/multires_conduction.hlsli"
23#include "sim/multires_nest.h"
25
26using namespace bicameral::multires;
27using namespace bicameral::reaction;
28
29namespace bicameral::sim {
30
31 namespace {
32
35
36 // レベル level の小刻みの数の指数(4^σ 回。T-0108)
37 uint32_t SubcycleShift(const MultiresStepOptions& options, int32_t level) {
38 return MrSubcycleShift(level, options.subcycleBaseLevel, options.maxSubcycleGap);
39 }
40
41 // セル 1 つの面の流れ
42 struct CellFaces {
43 int64_t sameLevelOutflow = 0; // 同じレベルの面の流れの和(出ていくのが正。自分のレベルの単位)
44
45 // --- 粗い側への面(細かい単位の流れ。細かい → 粗いが正)---
46 uint32_t crossCount = 0;
47 std::array<MrFaceNeighbor, MR_FACES> crossNeighbors{};
48 std::array<int64_t, MR_FACES> crossFlows{};
49 };
50
51 bool IsFrozen(std::span<const uint8_t> frozen, uint32_t slot) {
52 return !frozen.empty() && frozen[slot] != 0;
53 }
54
55 // 枠 slot のセル index の 6 面。細かい側の面(相手が計算する)・外(断熱)・凍らせたブロックとの面は流れない
56 CellFaces CollectCellFaces(const MultiresNest& nest, CellThermals& thermals, uint32_t slot, uint32_t index,
57 std::span<const uint8_t> frozen, std::span<const uint8_t> implicitBlocks,
58 const MultiresStepOptions& options) {
59 const CpuTree tree{.nest = &nest};
60 const MrBlock& block = nest.blocks[slot];
61 const bool joinsImplicit = slot < nest.capacity.worldBlocks && block.kind == MR_BLOCK_REAL;
62 const MrThermal self = thermals.At(slot, index);
63 const uint32_t shift = SubcycleShift(options, block.level);
64
65 CellFaces faces;
66 for (uint32_t face = 0; face < MR_FACES; ++face) {
67 const MrFaceNeighbor neighbor = MrFindFaceNeighbor(tree, slot, block, index, face,
68 nest.capacity.rootLevel);
69 if (neighbor.kind != MR_NEIGHBOR_SAME && neighbor.kind != MR_NEIGHBOR_COARSER)
70 continue;
71
72 if (IsFrozen(frozen, neighbor.slot))
73 continue;
74
75 // 陰解法で解くブロックとの同じレベルの面は、こちらのセルが境のセルとして陰解法の系で受ける(T-0178。上限で選ばれなかった側)
76 if (neighbor.kind == MR_NEIGHBOR_SAME && joinsImplicit &&
77 nest_detail::IsImplicitBlock(implicitBlocks, neighbor.slot))
78 continue;
79
80 const MrThermal other = thermals.At(neighbor.slot, neighbor.index);
81 if (neighbor.kind == MR_NEIGHBOR_SAME) {
82 faces.sameLevelOutflow += MrSubstepSameLevelFlow(self, other, block.level, shift);
83 continue;
84 }
85
86 const uint32_t coarseShift = SubcycleShift(options, nest.blocks[neighbor.slot].level);
87 const int64_t flow = MrSubstepCrossLevelFlow(self, other, block.level, neighbor.gap, shift,
88 coarseShift);
89 if (flow == 0)
90 continue;
91
92 faces.crossNeighbors[faces.crossCount] = neighbor;
93 faces.crossFlows[faces.crossCount] = flow;
94 faces.crossCount += 1;
95 }
96
97 return faces;
98 }
99
100 bool IsSteppedSlot(std::span<const uint8_t> stepped, uint32_t slot) {
101 return stepped[slot] != 0;
102 }
103
104 // ブロックの面に接するセルか。一様なブロックの中のセルどうしは同じ値なので、流れがありうるのは面に接するセルだけ
105 bool OnBlockSurface(uint32_t index) {
106 constexpr uint32_t LAST = MR_BLOCK_EDGE - 1;
107 const uint32_t x = MrCellX(index);
108 const uint32_t y = MrCellY(index);
109 const uint32_t z = MrCellZ(index);
110
111 return x == 0 || x == LAST || y == 0 || y == LAST || z == 0 || z == LAST;
112 }
113
114 // 粗い側へ送る面を印にする(送るなら相手も変わるので一様なら頁に広げる。端数が要るなら相手に印)。送る面があれば true
115 bool MarkCrossSends(MultiresNest& nest, const CellFaces& faces, std::span<uint8_t> wantsFraction) {
116 bool sends = false;
117 for (uint32_t i = 0; i < faces.crossCount; ++i) {
118 const MrCrossTransfer transfer = MrSplitCrossFlow(faces.crossFlows[i], faces.crossNeighbors[i].gap,
119 true);
120 if (transfer.fineDelta == 0)
121 continue;
122
123 sends = true;
124 const uint32_t coarseSlot = faces.crossNeighbors[i].slot;
125 if (nest.blocks[coarseSlot].page == MR_NO_PAGE)
126 nest.blocks[coarseSlot].page = MR_PAGE_WANTED;
127
128 if (transfer.coarseDelta.fraction != 0)
129 wantsFraction[coarseSlot] = 1;
130 }
131
132 return sends;
133 }
134
135 // 刻むブロック 1 つの面の流れを調べ、印を付ける。一様で流れがあれば自分も頁に広げる
136 void MarkBlockWants(MultiresNest& nest, CellThermals& thermals, uint32_t slot, std::span<uint8_t> wantsFraction,
137 const MultiresStepOptions& options) {
138 const bool uniform = MrIsUniform(nest.blocks[slot]);
139 bool flows = false;
140 for (uint32_t index = 0; index < MR_BLOCK_CELLS; ++index) {
141 if (!MrIsSteppedCell(nest.blocks[slot], index) || (uniform && !OnBlockSurface(index)))
142 continue;
143
144 const CellFaces faces = CollectCellFaces(nest, thermals, slot, index, {}, {}, options);
145 const bool sends = MarkCrossSends(nest, faces, wantsFraction);
146 flows = flows || sends || faces.sameLevelOutflow != 0;
147 }
148
149 if (flows && nest.blocks[slot].page == MR_NO_PAGE)
150 nest.blocks[slot].page = MR_PAGE_WANTED;
151 }
152
153 // 端数が要るブロックに端数の枠を枠の順に配る(空きのスタックの上から。足りなければ数え、整数の単位の倍数だけ送る)
154 void AllocateConductionFractions(MultiresNest& nest, std::span<const uint8_t> frozen,
155 std::span<const uint8_t> wantsFraction) {
156 for (uint32_t slot = 0; slot < nest.capacity.worldBlocks; ++slot) {
157 MrBlock& block = nest.blocks[slot];
158 if (wantsFraction[slot] == 0 || frozen[slot] != 0 || block.fraction != MR_NO_FRACTION)
159 continue;
160
161 uint32_t& freeCount = nest.counters[MR_COUNTER_FREE_FRACTIONS];
162 if (freeCount == 0) {
163 nest.counters[MR_COUNTER_FRACTION_SHORTAGE] += 1;
164 continue;
165 }
166
167 block.fraction = nest.freeFractions[--freeCount];
168 for (uint32_t index = 0; index < MR_BLOCK_CELLS; ++index)
169 nest_detail::SetFraction(nest, block.fraction, index, MrMakeEmptyFraction());
170 }
171 }
172
173 // セル 1 つの面の流れを変化の表に足す(自分と、粗い側へ送った先)
174 void AddCellFlows(const MultiresNest& nest, CellThermals& thermals, uint32_t slot, uint32_t index,
175 std::span<const uint8_t> frozen, std::span<const uint8_t> implicitBlocks,
176 const MultiresStepOptions& options, std::span<MrEnergyDelta> deltas) {
177 const CellFaces faces = CollectCellFaces(nest, thermals, slot, index, frozen, implicitBlocks, options);
178 MrEnergyDelta own = MrMakeEnergyDelta();
179 own.whole = -faces.sameLevelOutflow;
180 for (uint32_t i = 0; i < faces.crossCount; ++i) {
181 const MrFaceNeighbor& coarse = faces.crossNeighbors[i];
182 const MrBlock& coarseBlock = nest.blocks[coarse.slot];
183 const MrCrossTransfer transfer = MrSplitCrossFlow(faces.crossFlows[i], coarse.gap,
184 coarseBlock.fraction != MR_NO_FRACTION);
185 if (transfer.fineDelta == 0)
186 continue;
187
188 FX_ASSERT(!MrIsUniform(coarseBlock));
189 own.whole += transfer.fineDelta;
190 MrEnergyDelta& target = deltas[nest_detail::PageCellAddress(nest, coarseBlock.page, coarse.index)];
191 target = MrAddEnergyDelta(target, transfer.coarseDelta);
192 }
193
194 MrEnergyDelta& entry = deltas[nest_detail::PageCellAddress(nest, nest.blocks[slot].page, index)];
195 entry = MrAddEnergyDelta(entry, own);
196 }
197
198 // 小刻み substep に小刻みが始まるブロックだけを刻む印
199 void MarkSubstepBlocks(const MultiresNest& nest, std::span<const uint8_t> stepped, uint32_t substep,
200 const MultiresStepOptions& options, std::span<uint8_t> active) {
201 for (uint32_t slot = 0; slot < nest.blocks.size(); ++slot) {
202 const uint32_t shift = SubcycleShift(options, nest.blocks[slot].level);
203 const bool begins = MrSubstepBegins(substep, shift, options.maxSubcycleGap);
204 active[slot] = stepped[slot] != 0 && begins ? 1 : 0;
205 }
206 }
207
208 // 小刻み substep に小刻みが終わるレベルの頁のブロックに、溜めた変化を足して表を 0 に戻す。変わったブロックの枠を返す(枠の順)
209 std::vector<uint32_t> ApplyEndingDeltas(MultiresNest& nest, CellThermals& thermals, uint32_t substep,
210 const MultiresStepOptions& options, std::span<MrEnergyDelta> deltas) {
211 std::vector<uint32_t> changed;
212 for (uint32_t slot = 0; slot < nest.blocks.size(); ++slot) {
213 const MrBlock block = nest.blocks[slot];
214 const uint32_t shift = SubcycleShift(options, block.level);
215 if (MrIsUniform(block) || !MrSubstepEnds(substep, shift, options.maxSubcycleGap))
216 continue;
217
218 bool blockChanged = false;
219 for (uint32_t index = 0; index < MR_BLOCK_CELLS; ++index) {
220 MrEnergyDelta& delta = deltas[nest_detail::PageCellAddress(nest, block.page, index)];
221 if (!MrIsSteppedCell(block, index) || MrEnergyDeltaIsZero(delta))
222 continue;
223
224 nest_detail::ApplyEnergyDelta(nest, slot, index, nest_detail::CellAt(nest, slot, index), delta);
225 delta = MrMakeEnergyDelta();
226 blockChanged = true;
227 }
228
229 if (!blockChanged)
230 continue;
231
232 thermals.Invalidate(slot);
233 changed.push_back(slot);
234 }
235
236 return changed;
237 }
238
239 // 小刻みで変わったブロックの面の隣を起こし、以後の小刻みから刻む(活性。wakeMark = この刻みの印)
240 void WakeChangedBlocks(MultiresNest& nest, std::span<const uint32_t> changed, uint32_t wakeMark,
241 std::span<uint8_t> stepped) {
242 for (const uint32_t slot : changed) {
243 if (slot < nest.capacity.worldBlocks && nest.blocks[slot].kind == MR_BLOCK_REAL)
244 nest_detail::WakeAround(nest, slot, wakeMark);
245 }
246
247 for (uint32_t slot = 0; slot < nest.capacity.worldBlocks; ++slot) {
248 if (nest.blocks[slot].activeTick == wakeMark)
249 stepped[slot] = 1;
250 }
251 }
252
253 } // namespace
254
255 namespace nest_detail {
256
258 : m_nest(&nest),
259 m_view(view),
260 m_values(nest.blocks.size() * (MR_BLOCK_CELLS + 1)),
261 m_known(nest.blocks.size() * (MR_BLOCK_CELLS + 1), 0) {}
262
263 MrThermal CellThermals::At(uint32_t slot, uint32_t index) {
264 const bool uniform = MrIsUniform(m_nest->blocks[slot]);
265 const size_t address = (size_t{slot} * (MR_BLOCK_CELLS + 1)) + (uniform ? MR_BLOCK_CELLS : index);
266 if (m_known[address] == 0) {
267 m_values[address] = MrCellThermal(m_view, LoadNestCell(*m_nest, slot, index));
268 m_known[address] = 1;
269 }
270
271 return m_values[address];
272 }
273
274 void CellThermals::Invalidate(uint32_t slot) {
275 const auto first = static_cast<std::ptrdiff_t>(size_t{slot} * (MR_BLOCK_CELLS + 1));
276 std::fill(m_known.begin() + first, m_known.begin() + first + MR_BLOCK_CELLS + 1, uint8_t{0});
277 }
278
279 std::vector<uint8_t> MarkConductionWants(MultiresNest& nest, CellThermals& thermals,
280 std::span<const uint8_t> stepped, const MultiresStepOptions& options) {
281 std::vector<uint8_t> wantsFraction(nest.blocks.size(), 0);
282 for (uint32_t slot = 0; slot < nest.blocks.size(); ++slot) {
283 if (IsSteppedSlot(stepped, slot))
284 MarkBlockWants(nest, thermals, slot, wantsFraction, options);
285 }
286
287 return wantsFraction;
288 }
289
290 void ComputeConduction(MultiresNest& nest, CellThermals& thermals, std::span<const uint8_t> stepped,
291 std::span<const uint8_t> frozen, std::span<const uint8_t> wantsFraction,
292 std::span<const uint8_t> implicitBlocks, const MultiresStepOptions& options,
293 std::span<MrEnergyDelta> deltas) {
294 AllocateConductionFractions(nest, frozen, wantsFraction);
295
296 // --- 刻む頁のブロック(凍らせたものを除く)のセルの面の流れ ---
297 for (uint32_t slot = 0; slot < nest.blocks.size(); ++slot) {
298 const MrBlock& block = nest.blocks[slot];
299 if (!IsSteppedSlot(stepped, slot) || IsFrozen(frozen, slot) || MrIsUniform(block) ||
300 IsImplicitBlock(implicitBlocks, slot))
301 continue;
302
303 for (uint32_t index = 0; index < MR_BLOCK_CELLS; ++index) {
304 if (MrIsSteppedCell(block, index))
305 AddCellFlows(nest, thermals, slot, index, frozen, implicitBlocks, options, deltas);
306 }
307 }
308 }
309
310 std::vector<MrEnergyDelta> StepConduction(MultiresNest& nest, const ReactionTableView& view,
311 std::span<uint8_t> stepped, const MultiresStepOptions& options,
312 uint32_t wakeMark, std::span<BlockStepResult> results) {
313 FX_ASSERT(options.maxSubcycleGap <= MULTIRES_MAX_SUBCYCLE_GAP);
314 FX_ASSERT(!options.implicitConduction || options.maxSubcycleGap == 0);
315 const uint32_t substeps = 1u << (2 * options.maxSubcycleGap);
316 CellThermals thermals(nest, view);
317 std::vector<MrEnergyDelta> deltas(nest.cells.size(), MrMakeEnergyDelta());
318 std::vector<uint8_t> active(nest.blocks.size(), 0);
319 for (uint32_t substep = 0;; ++substep) {
320 // --- 印 → 頁 → 端数の枠と流れ(T-0019 の 1 刻みと同じ順)---
321 MarkSubstepBlocks(nest, stepped, substep, options, active);
322 const std::vector<uint8_t> wantsFraction = MarkConductionWants(nest, thermals, active, options);
323 const std::vector<uint8_t> frozen = ExpandForStep(nest, results);
324 const std::vector<uint8_t> implicitBlocks = MarkImplicitBlocks(nest, thermals, frozen, options);
325 ComputeConduction(nest, thermals, active, frozen, wantsFraction, implicitBlocks, options, deltas);
326 if (substep + 1 == substeps) {
327 if (options.implicitConduction)
328 AddImplicitConduction(nest, thermals, frozen, implicitBlocks, options, deltas);
329
330 return deltas;
331 }
332
333 // --- 小刻みが終わるレベルに変化を足す。変わったブロックの隣を起こす(活性)---
334 const std::vector<uint32_t> changed = ApplyEndingDeltas(nest, thermals, substep, options, deltas);
335 for (const uint32_t slot : changed)
336 results[slot].changed = true;
337
338 if (wakeMark != 0)
339 WakeChangedBlocks(nest, changed, wakeMark, stepped);
340 }
341 }
342
343 } // namespace nest_detail
344
345} // namespace bicameral::sim
CellThermals(const MultiresNest &nest, const ReactionTableView &view)
std::vector< multires::MrThermal > m_values
multires::MrThermal At(uint32_t slot, uint32_t index)
bool IsSteppedSlot(std::span< const uint8_t > stepped, uint32_t slot)
bool MarkCrossSends(MultiresNest &nest, const CellFaces &faces, std::span< uint8_t > wantsFraction)
void MarkBlockWants(MultiresNest &nest, CellThermals &thermals, uint32_t slot, std::span< uint8_t > wantsFraction, const MultiresStepOptions &options)
bool IsFrozen(std::span< const uint8_t > frozen, uint32_t slot)
void AddCellFlows(const MultiresNest &nest, CellThermals &thermals, uint32_t slot, uint32_t index, std::span< const uint8_t > frozen, std::span< const uint8_t > implicitBlocks, const MultiresStepOptions &options, std::span< MrEnergyDelta > deltas)
CellFaces CollectCellFaces(const MultiresNest &nest, CellThermals &thermals, uint32_t slot, uint32_t index, std::span< const uint8_t > frozen, std::span< const uint8_t > implicitBlocks, const MultiresStepOptions &options)
void MarkSubstepBlocks(const MultiresNest &nest, std::span< const uint8_t > stepped, uint32_t substep, const MultiresStepOptions &options, std::span< uint8_t > active)
uint32_t SubcycleShift(const MultiresStepOptions &options, int32_t level)
std::vector< uint32_t > ApplyEndingDeltas(MultiresNest &nest, CellThermals &thermals, uint32_t substep, const MultiresStepOptions &options, std::span< MrEnergyDelta > deltas)
void WakeChangedBlocks(MultiresNest &nest, std::span< const uint32_t > changed, uint32_t wakeMark, std::span< uint8_t > stepped)
void AllocateConductionFractions(MultiresNest &nest, std::span< const uint8_t > frozen, std::span< const uint8_t > wantsFraction)
size_t PageCellAddress(const MultiresNest &nest, uint32_t page, uint32_t index)
void ComputeConduction(MultiresNest &nest, CellThermals &thermals, std::span< const uint8_t > stepped, std::span< const uint8_t > frozen, std::span< const uint8_t > wantsFraction, std::span< const uint8_t > implicitBlocks, const MultiresStepOptions &options, std::span< MrEnergyDelta > deltas)
void WakeAround(MultiresNest &nest, uint32_t slot, uint32_t mark)
std::vector< uint8_t > ExpandForStep(MultiresNest &nest, std::span< BlockStepResult > results)
std::vector< uint8_t > MarkImplicitBlocks(MultiresNest &nest, CellThermals &thermals, std::span< const uint8_t > frozen, const MultiresStepOptions &options)
std::vector< uint8_t > MarkConductionWants(MultiresNest &nest, CellThermals &thermals, std::span< const uint8_t > stepped, const MultiresStepOptions &options)
void ApplyEnergyDelta(MultiresNest &nest, uint32_t slot, uint32_t index, RxCell &cell, const MrEnergyDelta &delta)
void SetFraction(MultiresNest &nest, uint32_t fractionSlot, uint32_t index, const multires::MrFraction &fraction)
std::vector< MrEnergyDelta > StepConduction(MultiresNest &nest, const ReactionTableView &view, std::span< uint8_t > stepped, const MultiresStepOptions &options, uint32_t wakeMark, std::span< BlockStepResult > results)
reaction::RxCell & CellAt(MultiresNest &nest, uint32_t slot, uint32_t index)
bool IsImplicitBlock(std::span< const uint8_t > implicitBlocks, uint32_t slot)
void AddImplicitConduction(MultiresNest &nest, CellThermals &thermals, std::span< const uint8_t > frozen, std::span< const uint8_t > implicitBlocks, const MultiresStepOptions &options, std::span< MrEnergyDelta > deltas)
constexpr uint32_t MULTIRES_MAX_SUBCYCLE_GAP
RxCell LoadNestCell(const MultiresNest &nest, uint32_t slot, uint32_t index)
std::vector< reaction::RxCell > cells
std::vector< uint32_t > index
std::vector< multires::MrBlock > blocks
std::vector< uint32_t > freeFractions
std::array< uint32_t, multires::MR_COUNTER_COUNT > counters