GCC Code Coverage Report


ernie/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
20 / 20
100.0%
Branches:
20 / 36
55.6%
Functions:
1 / 1, 3 excl
100.0%

render/transform_graph.h
Line Branch Exec Source
1 // SPDX-License-Identifier: BSD-3-Clause
2 /// @file transform_graph.h
3 /// @brief World transforms composed on the device: each prim a node at its id's slot, one small
4 /// kernel per level of the hierarchy composing its nodes from their parents', and transform ops
5 /// written in place by CUDA, Warp or kernels evaluated on the device without the host.
6 #pragma once
7
8 #include "ernie/device/column.h"
9 #include "ernie/device/kernel.h"
10 #include "ernie/device/shader_compiler.h"
11 #include "ernie/device/staging.h"
12 #include "ernie/graph/graph.h"
13 #include "ernie/render/prim_rows.h"
14 #include "ernie/scene/scene.h"
15 #include "ernie/scene/xform.h"
16 #include "ernie/shaders/scene/transform.slang"
17
18 #include <array>
19 #include <cstdint>
20 #include <limits>
21 #include <memory>
22 #include <optional>
23 #include <span>
24 #include <string>
25 #include <string_view>
26 #include <vector>
27
28 namespace ernie {
29
30 /// @brief The scene's transform hierarchy on the device. A prim's node is its id's slot, so the
31 /// stable id finds its transform with no table: `Worlds` holds a 3x4 row-major matrix a slot
32 /// (VkTransformMatrixKHR's layout) relative to the render origin. Each node keeps its parent's
33 /// slot, none at a root or under `!resetXformStack!`.
34 ///
35 /// Worlds are exact to about 48 bits from the Sun to a wheel nut, with no 64-bit floats on the
36 /// device: roots subtract the origin on the host in double, so values near the views stay small,
37 /// and every level composes in double-float (each value a high float plus a low one), so large
38 /// offsets that cancel on the way down lose nothing. Kernels read a world as float rows through
39 /// `WorldModules`' `LoadWorld`, rounding only there.
40 ///
41 /// Local transforms are the host's, uploaded when their ops are authored, unless a prim's op order
42 /// uses an attribute bound to a device column (`Bind`). Then the prim's ops become a short program
43 /// the device evaluates each update: the bound ops read their rows in place, as CUDA, Warp or
44 /// kernels wrote them, and runs of the host's other ops are folded into one matrix each.
45 ///
46 /// Updates follow the scene's changes and cost what changed: added and removed prims are found by
47 /// replaying the buckets' row logs, and a prim defined between a prim and its descendants takes
48 /// them over by walking only that subtree.
49 class TransformGraph {
50 public:
51 static constexpr std::uint32_t kNone = shaders::kNone;
52 static constexpr std::uint32_t kMaxOps = shaders::kMaxOps;
53 /// A transform's top three rows in double-float, as the device keeps them.
54 using SplitRows = shaders::SplitRows;
55 /// One op of a program, as the device reads it.
56 using DeviceOp = shaders::Op;
57 using Program = std::array<DeviceOp, kMaxOps>;
58 /// @return An op's kind: each `XformOpKind` after the folded constant's.
59 1606 static constexpr std::uint32_t OpKind(XformOpKind kind) {
60
1/2
✓ Branch 0 taken 1606 times.
✗ Branch 1 not taken.
1606 return 1 + static_cast<std::uint32_t>(kind);
61 }
62
63 TransformGraph(DeviceContext& context, Arenas& arenas, const Scene& scene,
64 const ShaderCompiler& compiler);
65 ~TransformGraph();
66 TransformGraph(const TransformGraph&) = delete;
67 TransformGraph& operator=(const TransformGraph&) = delete;
68
69 /// @brief Evaluates the bucket's `attribute` op on the device from `column`, a row a prim as
70 /// `SceneColumns` keeps it: translate and Euler rotations read three floats, scale one or
71 /// three, rotateX, rotateY and rotateZ one angle in degrees, orient four (x, y, z, w) and
72 /// transform sixteen (column-major).
73 /// @throws std::invalid_argument when `attribute` is no transform op, the column does not hold
74 /// the bucket's rows of floats in the op's shape, or the attribute is bound already.
75 void Bind(BucketId bucket, std::string_view attribute, const DeviceColumn& column);
76 /// @brief Follows the scene's changes since the last update and composes every world
77 /// transform, after `waits`: the tickets of the bound columns' writers and their sync.
78 /// @return The ticket passes wait on before reading `Worlds`.
79 /// @throws std::invalid_argument when a prim's program needs more than `kMaxOps` ops.
80 Ticket Update(std::span<const Ticket> waits = {});
81 /// @brief `Update` as passes of a graph run on the main queue, in a "Transforms" scope: the
82 /// uploads, the programs and one pass a level. Bound columns are imported and read after the
83 /// passes that write them; later passes import `Worlds` to read it. The graph executes before
84 /// the next update.
85 /// @throws std::invalid_argument as `Update`.
86 void AddUpdate(graph::Graph& graph);
87
88 /// @brief Moves the render origin the worlds are relative to. Only roots change: a rebase
89 /// costs their uploads and the next update's composition, never a bottom level's rebuild.
90 void SetOrigin(const glm::dvec3& origin);
91 20 const glm::dvec3& Origin() const {
92
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
20 return origin_;
93 }
94
95 /// @return Each slot's world transform relative to the origin, `SplitRows` a slot.
96 35 const DeviceColumn& Worlds() const {
97
9/18
<unknown function 91>:
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 7 taken 1 time.
✗ Branch 8 not taken.
✓ Branch 10 taken 1 time.
✗ Branch 11 not taken.
✓ Branch 13 taken 1 time.
✗ Branch 14 not taken.
✓ Branch 17 taken 1 time.
✗ Branch 18 not taken.
✓ Branch 20 taken 1 time.
✗ Branch 21 not taken.
<unknown function 96>:
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
35 return *worlds_;
98 }
99 /// @return The Slang modules kernels import to read `Worlds`: `LoadWorld(float4* worlds, uint
100 /// slot)` gives a slot's world as float rows, relative to the origin.
101 static std::span<const ShaderModule> WorldModules();
102 /// @return A slot's world transform relative to the origin, from `Worlds`' floats read back.
103 static glm::dmat4 ReadWorld(std::span<const float> worlds, std::uint32_t slot);
104 /// @return The prim's node, its slot, or none when it is not in the graph yet.
105 std::optional<std::uint32_t> Node(PrimId prim) const;
106 /// @return The number of levels, each one dispatch.
107 3 std::size_t Levels() const {
108
3/6
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 7 taken 1 time.
✗ Branch 8 not taken.
3 return levels_.size();
109 }
110
111 private:
112 struct Binding {
113 BucketId bucket = 0;
114 AttributeId attribute = 0;
115 const DeviceColumn* column = nullptr;
116 /// The column's address its programs point into; a column that moves rewrites them.
117 VkDeviceAddress address = 0;
118 };
119 5350 struct NodeState {
120 PrimId prim;
121 bool live = false;
122 std::uint32_t parent = kNone;
123 std::uint32_t level = 0;
124 /// Where the node is in its level's list, its parent's children and the programmed list.
125 std::uint32_t position = kNone;
126 std::uint32_t siblingPosition = kNone;
127 std::uint32_t programPosition = kNone;
128 std::vector<std::uint32_t> children;
129 /// Its order's ops, parsed when the order changes.
130 std::vector<XformOp> ops;
131 bool resets = false;
132 /// Whether its local was last built as a root's, relative to the origin.
133 bool rooted = false;
134 };
135 /// Slots in a device list, kept dense by swap-removal, its changed entries uploaded in runs.
136
1/2
✓ Branch 1 taken 23 times.
✗ Branch 2 not taken.
23 struct SlotList {
137 std::vector<std::uint32_t> slots;
138 std::unique_ptr<DeviceColumn> device;
139 std::vector<std::uint32_t> dirty;
140 };
141 /// Slots of a slot array changed since its last upload, each once.
142
1/2
✓ Branch 8 taken 23 times.
✗ Branch 9 not taken.
46 struct DirtySlots {
143 std::vector<std::uint32_t> slots;
144 std::vector<bool> marked;
145 4530 void Mark(std::uint32_t slot) {
146
2/2
✓ Branch 0 taken 3016 times.
✓ Branch 1 taken 1514 times.
4530 if (slot >= marked.size()) {
147 3016 marked.resize(slot + 1, false);
148 }
149
2/2
✓ Branch 1 taken 3031 times.
✓ Branch 2 taken 1499 times.
4530 if (!marked[slot]) {
150 3031 marked[slot] = true;
151 3031 slots.push_back(slot);
152 }
153 4530 }
154 };
155
156 void Replay(const ChangeSet& changes);
157 /// Refreshes the nodes whose transform values the host authored, reparenting those whose
158 /// order changed.
159 void RefreshAuthored(const ChangeSet& changes);
160 void AddNode(PrimId prim);
161 void RemoveNode(std::uint32_t slot);
162 /// Hangs a node under `parent` at the level below it, its subtree following.
163 void Attach(std::uint32_t slot, std::uint32_t parent);
164 void Detach(std::uint32_t slot);
165 /// Moves a node under `parent`, rebuilding its local when it becomes or stops being a root.
166 void Reparent(std::uint32_t slot, std::uint32_t parent);
167 void SetLevel(std::uint32_t slot, std::uint32_t level);
168 /// @return The slot of the node's nearest ancestor prim, none at a root or a stack reset.
169 std::uint32_t ParentSlot(std::uint32_t slot) const;
170 /// Hands descendants whose nearest ancestor prim is now `slot`'s over to it.
171 void AdoptDescendants(std::uint32_t slot);
172 /// Parses a node's op order.
173 void ParseOps(std::uint32_t slot);
174 /// Recomposes a node's local transform or program from its ops.
175 void Refresh(std::uint32_t slot);
176 /// Reparses and refreshes every node of a bucket.
177 void RefreshBucket(BucketId bucket);
178 void EnsureSlots(std::uint32_t slots);
179 std::uint32_t Insert(SlotList& list, std::uint32_t slot);
180 /// Takes a slot out of a list it is in at its `position` member, swap-removing.
181 /// @return Where it was.
182 std::uint32_t Unlist(SlotList& list, std::uint32_t NodeState::* position, std::uint32_t slot);
183 /// Takes a node's program out, the last program moving into its place.
184 void Unprogram(std::uint32_t slot);
185 const Binding* FindBinding(BucketId bucket, std::optional<Token> attribute) const;
186 /// A prim's transform as the host composes it, or as the program the device evaluates.
187 struct Built {
188 bool onDevice = false;
189 SplitRows local{};
190 Program program{};
191 std::uint32_t length = 0;
192 };
193 Built BuildProgram(std::uint32_t slot) const;
194 void UploadDirty();
195 /// Follows the scene's changes on the host and stages their uploads.
196 void Prepare();
197 void DispatchLocals(VkCommandBuffer commands) const;
198 void DispatchLevel(VkCommandBuffer commands, const SlotList& level) const;
199 /// The graph's handles of what an update reads and writes.
200 struct GraphColumns;
201 GraphColumns ImportColumns(graph::Graph& graph) const;
202 void AddLocalPass(graph::Graph& graph, GraphColumns& columns) const;
203 void AddLevelPasses(graph::Graph& graph, GraphColumns& columns) const;
204
205 DeviceContext& context_;
206 Arenas& arenas_;
207 const Scene& scene_;
208 Uploader uploader_;
209 ComputeKernel local_;
210 ComputeKernel compose_;
211 std::uint64_t cursor_ = 0;
212 glm::dvec3 origin_{0.0};
213 std::vector<Binding> bindings_;
214 PrimRows rows_;
215 std::vector<NodeState> nodes_;
216 std::vector<SlotList> levels_;
217 SlotList programmed_;
218 // Host copies of the slot arrays and the programmed list's programs, and the entries changed
219 // since the last upload.
220 std::vector<std::uint32_t> parents_;
221 std::vector<SplitRows> locals_;
222 std::vector<Program> programs_;
223 std::vector<std::uint32_t> lengths_;
224 DirtySlots dirtyParents_;
225 DirtySlots dirtyLocals_;
226 DirtySlots dirtyPrograms_;
227 std::unique_ptr<DeviceColumn> parentColumn_;
228 std::unique_ptr<DeviceColumn> localColumn_;
229 std::unique_ptr<DeviceColumn> programColumn_;
230 std::unique_ptr<DeviceColumn> lengthColumn_;
231 std::unique_ptr<DeviceColumn> worlds_;
232 };
233
234 } // namespace ernie
235