| … | … | @@ -131,15 +131,6 @@ static bool _bind_context_or_defer(ProcEvalContext &p_ctx, int p_node_idx, |
| 131 | 131 | return false; |
| 132 | 132 | } |
| 133 | 133 | |
| 134 | | // Per-node time spent in the LAZY path. The value cone is deferred and then run |
| 135 | | // from materialize(), so every one of its nodes is charged to whichever geometry |
| 136 | | // node happened to ask -- "CPU node [SetPosition]: 11.4 s" is the whole cone, |
| 137 | | // and says nothing about which node in it to thread. |
| 138 | | static HashMap<String, double> &_lazy_ms() { |
| 139 | | static HashMap<String, double> acc; |
| 140 | | return acc; |
| 141 | | } |
| 142 | | |
| 143 | 134 | SocketValue ProcEvalContext::materialize(const SocketValue &p_val, |
| 144 | 135 | const Ref<ProcGeometry> &p_geo) { |
| 145 | 136 | if (!p_val.is_lazy || graph == nullptr || p_val.lazy_node < 0) { |
| … | … | @@ -153,14 +144,7 @@ SocketValue ProcEvalContext::materialize(const SocketValue &p_val, |
| 153 | 144 | // Restoring the previous context keeps nesting honest. |
| 154 | 145 | Ref<ProcGeometry> prev = context_geometry; |
| 155 | 146 | context_geometry = p_geo; |
| 156 | | { |
| 157 | | const uint64_t _t0 = Time::get_singleton()->get_ticks_usec(); |
| 158 | | producer->execute(*this, p_val.lazy_node); |
| 159 | | const uint64_t _t1 = Time::get_singleton()->get_ticks_usec(); |
| 160 | | HashMap<String, double> &acc = _lazy_ms(); |
| 161 | | const String k = String(producer->get_node_type()); |
| 162 | | acc[k] = (acc.has(k) ? acc[k] : 0.0) + (double)(_t1 - _t0) / 1000.0; |
| 163 | | } |
| 147 | producer->execute(*this, p_val.lazy_node); |
| 164 | 148 | context_geometry = prev; |
| 165 | 149 | int slot = p_val.lazy_node * max_output_sockets + p_val.lazy_socket; |
| 166 | 150 | if (slot < 0 || slot >= output_values.size()) { |
| … | … | @@ -1480,6 +1464,151 @@ static Ref<ProcGeometry> _to_godot_frame(const Ref<ProcGeometry> &p_geo, |
| 1480 | 1464 | return p_geo; |
| 1481 | 1465 | } |
| 1482 | 1466 | |
| 1467 | |
| 1468 | // ── WHICH NODES CAN REACH THE OUTPUT, GIVEN WHAT THE SWITCHES DECIDE ───────── |
| 1469 | // |
| 1470 | // The executor ran every node in topological order. On the bridged voronoi |
| 1471 | // mountain that meant computing MeshToSDFGrid and GridToMesh -- 4.0 s of an |
| 1472 | // 8.3 s evaluation -- and then throwing the result away, because the graph's |
| 1473 | // Solidify input is 0 and the geometry Switch takes the other branch. Blender |
| 1474 | // never touches it: its geometry nodes are a demand-driven lazy-function graph, |
| 1475 | // and node_geo_switch.cc declares both branches lf::ValueUsage::Maybe, reads the |
| 1476 | // condition, then calls params.set_input_unused() on the one it does not want, |
| 1477 | // so the scheduler never runs that subgraph at all. |
| 1478 | // |
| 1479 | // This is the same decision made statically. Conditions are settled first -- |
| 1480 | // each Switch's condition sub-cone is executed on its own, which is cheap |
| 1481 | // because a condition that depended on the heavy branch would be a cycle -- and |
| 1482 | // then liveness is walked backwards from the output, entering a Switch only |
| 1483 | // through the branch it actually takes. |
| 1484 | // |
| 1485 | // ⚠ A VARYING CONDITION NEEDS BOTH BRANCHES, and Blender says so in the same |
| 1486 | // file: execute_field() requests both when the condition is a field, because |
| 1487 | // the choice is then per element. A Switch whose condition is not a single |
| 1488 | // value is left alone here for exactly that reason. |
| 1489 | // |
| 1490 | // Nodes that cannot reach the output at all drop out of the same walk, so the |
| 1491 | // dead-code case costs nothing extra. |
| 1492 | static void _collect_input_cone(const ProcNodeGraph &p_graph, int p_node, int p_socket, |
| 1493 | Vector<bool> &r_live) { |
| 1494 | const int src = p_graph.get_input_source_node(p_node, p_socket); |
| 1495 | if (src < 0 || src >= r_live.size() || r_live[src]) { |
| 1496 | return; |
| 1497 | } |
| 1498 | r_live.write[src] = true; |
| 1499 | Ref<ProcNode> n = p_graph.get_node(src); |
| 1500 | if (n.is_null()) { |
| 1501 | return; |
| 1502 | } |
| 1503 | for (int s = 0; s < n->get_input_count(); s++) { |
| 1504 | _collect_input_cone(p_graph, src, s, r_live); |
| 1505 | } |
| 1506 | } |
| 1507 | |
| 1508 | Vector<bool> GraphExecutor::_compute_live_set(const ProcNodeGraph &p_graph, |
| 1509 | ExecutionPlan &p_plan, ProcEvalContext &p_ctx) { |
| 1510 | const int count = p_graph.get_node_count(); |
| 1511 | Vector<bool> live; |
| 1512 | live.resize(count); |
| 1513 | for (int i = 0; i < count; i++) { |
| 1514 | live.write[i] = false; |
| 1515 | } |
| 1516 | if (p_plan.output_node < 0 || p_plan.output_node >= count) { |
| 1517 | for (int i = 0; i < count; i++) { |
| 1518 | live.write[i] = true; |
| 1519 | } |
| 1520 | return live; |
| 1521 | } |
| 1522 | |
| 1523 | // 1. Settle every Switch's condition, by running only what feeds it. |
| 1524 | HashMap<int, int> taken; // switch node -> the input socket to follow |
| 1525 | for (int i = 0; i < count; i++) { |
| 1526 | Ref<ProcNode> n = p_graph.get_node(i); |
| 1527 | if (n.is_null() || n->get_node_type() != StringName("Switch")) { |
| 1528 | continue; |
| 1529 | } |
| 1530 | Vector<bool> cone; |
| 1531 | cone.resize(count); |
| 1532 | for (int k = 0; k < count; k++) { |
| 1533 | cone.write[k] = false; |
| 1534 | } |
| 1535 | _collect_input_cone(p_graph, i, 0, cone); |
| 1536 | if (p_graph.get_input_source_node(i, 0) < 0) { |
| 1537 | // Unconnected: the socket default decides, and nothing needs running. |
| 1538 | const bool c = (bool)n->get_input_default(0); |
| 1539 | taken[i] = c ? 2 : 1; |
| 1540 | continue; |
| 1541 | } |
| 1542 | // A zone body cannot be evaluated out of order, so leave that Switch be. |
| 1543 | bool in_zone = false; |
| 1544 | for (int k = 0; k < count && !in_zone; k++) { |
| 1545 | if (cone[k] && k < p_plan.node_zone_map.size() && p_plan.node_zone_map[k] >= 0) { |
| 1546 | in_zone = true; |
| 1547 | } |
| 1548 | } |
| 1549 | if (in_zone) { |
| 1550 | continue; |
| 1551 | } |
| 1552 | for (int o = 0; o < p_plan.execution_order.size(); o++) { |
| 1553 | const int idx = p_plan.execution_order[o]; |
| 1554 | if (idx < 0 || idx >= count || !cone[idx]) { |
| 1555 | continue; |
| 1556 | } |
| 1557 | Ref<ProcNode> cn = p_graph.get_node(idx); |
| 1558 | Ref<ProcGeometry> prev; |
| 1559 | bool bound = false; |
| 1560 | if (cn.is_valid() && !_bind_context_or_defer(p_ctx, idx, cn, prev, bound)) { |
| 1561 | cn->execute(p_ctx, idx); |
| 1562 | if (bound) { |
| 1563 | p_ctx.context_geometry = prev; |
| 1564 | } |
| 1565 | } |
| 1566 | } |
| 1567 | const SocketValue cond = p_ctx.read_input(i, 0); |
| 1568 | if (cond.is_field || cond.is_lazy) { |
| 1569 | continue; // per element: both branches are live, as in Blender |
| 1570 | } |
| 1571 | taken[i] = cond.to_bool() ? 2 : 1; |
| 1572 | } |
| 1573 | |
| 1574 | // 2. Liveness backwards from the output, through the taken branch only. |
| 1575 | Vector<int> stack; |
| 1576 | live.write[p_plan.output_node] = true; |
| 1577 | stack.push_back(p_plan.output_node); |
| 1578 | while (!stack.is_empty()) { |
| 1579 | const int idx = stack[stack.size() - 1]; |
| 1580 | stack.remove_at(stack.size() - 1); |
| 1581 | Ref<ProcNode> n = p_graph.get_node(idx); |
| 1582 | if (n.is_null()) { |
| 1583 | continue; |
| 1584 | } |
| 1585 | const bool is_switch = n->get_node_type() == StringName("Switch") && taken.has(idx); |
| 1586 | for (int s = 0; s < n->get_input_count(); s++) { |
| 1587 | if (is_switch && s != 0 && s != taken[idx]) { |
| 1588 | continue; // the branch this Switch does not take |
| 1589 | } |
| 1590 | const int src = p_graph.get_input_source_node(idx, s); |
| 1591 | if (src < 0 || src >= count || live[src]) { |
| 1592 | continue; |
| 1593 | } |
| 1594 | live.write[src] = true; |
| 1595 | stack.push_back(src); |
| 1596 | } |
| 1597 | } |
| 1598 | |
| 1599 | // A zone's body is reached through its pairing, not its links, so anything |
| 1600 | // inside a live zone stays live. |
| 1601 | for (int i = 0; i < count; i++) { |
| 1602 | if (i < p_plan.node_zone_map.size() && p_plan.node_zone_map[i] >= 0) { |
| 1603 | const ProcZoneInfo &z = p_plan.zones[p_plan.node_zone_map[i]]; |
| 1604 | if (z.output_node >= 0 && z.output_node < count && live[z.output_node]) { |
| 1605 | live.write[i] = true; |
| 1606 | } |
| 1607 | } |
| 1608 | } |
| 1609 | return live; |
| 1610 | } |
| 1611 | |
| 1483 | 1612 | Ref<ProcGeometry> GraphExecutor::execute( |
| 1484 | 1613 | const ProcNodeGraph &p_graph, |
| 1485 | 1614 | ExecutionPlan &p_plan, |
| … | … | @@ -1527,10 +1656,19 @@ Ref<ProcGeometry> GraphExecutor::execute( |
| 1527 | 1656 | gpu_available = true; |
| 1528 | 1657 | } |
| 1529 | 1658 | |
| 1659 | // What the output can actually reach, once the switches have decided. |
| 1660 | const Vector<bool> live = _compute_live_set(p_graph, p_plan, ctx); |
| 1661 | |
| 1530 | 1662 | // Execute nodes in topological order |
| 1531 | 1663 | for (int i = 0; i < p_plan.execution_order.size(); i++) { |
| 1532 | 1664 | int node_idx = p_plan.execution_order[i]; |
| 1533 | 1665 | |
| 1666 | // Not on any path to the output: Blender's scheduler would never have |
| 1667 | // been asked for it. |
| 1668 | if (node_idx >= 0 && node_idx < live.size() && !live[node_idx]) { |
| 1669 | continue; |
| 1670 | } |
| 1671 | |
| 1534 | 1672 | // Skip zone body nodes (executed by zone handler when output node is reached) |
| 1535 | 1673 | if (node_idx < p_plan.node_zone_map.size() && p_plan.node_zone_map[node_idx] >= 0) { |
| 1536 | 1674 | int zone_idx = p_plan.node_zone_map[node_idx]; |
| … | … | @@ -1584,16 +1722,6 @@ Ref<ProcGeometry> GraphExecutor::execute( |
| 1584 | 1722 | } |
| 1585 | 1723 | } |
| 1586 | 1724 | |
| 1587 | | { |
| 1588 | | HashMap<String, double> &acc = _lazy_ms(); |
| 1589 | | for (const KeyValue<String, double> &kv : acc) { |
| 1590 | | if (kv.value > 0.5) { |
| 1591 | | UtilityFunctions::print(" LAZY node [", kv.key, "]: ", kv.value, "ms"); |
| 1592 | | } |
| 1593 | | } |
| 1594 | | acc.clear(); |
| 1595 | | } |
| 1596 | | |
| 1597 | 1725 | // Free per-evaluation GPU buffers (pipelines persist in ExecutionPlan for reuse) |
| 1598 | 1726 | ctx.gpu_buffers.free_all(); |
| 1599 | 1727 | |