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 | 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 | 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 | 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 | 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 | 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 | 46 | struct DirtySlots { | |
| 143 | std::vector<std::uint32_t> slots; | ||
| 144 | std::vector<bool> marked; | ||
| 145 | 4530 | void Mark(std::uint32_t slot) { | |
| 146 | 4530 | if (slot >= marked.size()) { | |
| 147 | 3016 | marked.resize(slot + 1, false); | |
| 148 | } | ||
| 149 | 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 |