Implemented execution map to optimize out nodes locally
- Added optional optimization of execution map - Added visualization of skipped nodes in the editor debug toolmaster
parent
30e33849fe
commit
03bfc09e99
|
@ -1,6 +1,7 @@
|
|||
#include "voxel_graph_editor.h"
|
||||
#include "../../generators/graph/voxel_generator_graph.h"
|
||||
#include "../../generators/graph/voxel_graph_node_db.h"
|
||||
#include "../../terrain/voxel_node.h"
|
||||
#include "../../util/macros.h"
|
||||
|
||||
#include <core/core_string_names.h>
|
||||
|
@ -69,13 +70,15 @@ public:
|
|||
|
||||
enabled_checkbox = memnew(CheckBox);
|
||||
enabled_checkbox->set_text(TTR("Enabled"));
|
||||
enabled_checkbox->connect("toggled", this, "_on_enabled_checkbox_toggled");
|
||||
vb->add_child(enabled_checkbox);
|
||||
|
||||
Label *tip = memnew(Label);
|
||||
// TODO Had to use `\n` and disable autowrap, otherwise the popup height becomes crazy high
|
||||
// See https://github.com/godotengine/godot/issues/47005
|
||||
tip->set_text(TTR("When enabled, hover node output labels to\ninspect their "
|
||||
"estimated range within the\nconfigured area."));
|
||||
"estimated range within the\nconfigured area.\n"
|
||||
"Nodes that may be optimized out locally will be greyed out."));
|
||||
//tip->set_autowrap(true);
|
||||
tip->set_modulate(Color(1.f, 1.f, 1.f, 0.8f));
|
||||
vb->add_child(tip);
|
||||
|
@ -107,7 +110,15 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
static void add_row(String text, SpinBox *&sb, GridContainer *parent, float defval) {
|
||||
void _on_enabled_checkbox_toggled(bool enabled) {
|
||||
emit_signal("analysis_toggled", enabled);
|
||||
}
|
||||
|
||||
void _on_area_spinbox_value_changed(float value) {
|
||||
emit_signal("area_changed");
|
||||
}
|
||||
|
||||
void add_row(String text, SpinBox *&sb, GridContainer *parent, float defval) {
|
||||
sb = memnew(SpinBox);
|
||||
sb->set_min(-99999.f);
|
||||
sb->set_max(99999.f);
|
||||
|
@ -117,6 +128,17 @@ private:
|
|||
label->set_text(text);
|
||||
parent->add_child(label);
|
||||
parent->add_child(sb);
|
||||
sb->connect("value_changed", this, "_on_area_spinbox_value_changed");
|
||||
}
|
||||
|
||||
static void _bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("_on_enabled_checkbox_toggled", "enabled"),
|
||||
&VoxelRangeAnalysisDialog::_on_enabled_checkbox_toggled);
|
||||
ClassDB::bind_method(D_METHOD("_on_area_spinbox_value_changed", "value"),
|
||||
&VoxelRangeAnalysisDialog::_on_area_spinbox_value_changed);
|
||||
|
||||
ADD_SIGNAL(MethodInfo("analysis_toggled", PropertyInfo(Variant::BOOL, "enabled")));
|
||||
ADD_SIGNAL(MethodInfo("area_changed"));
|
||||
}
|
||||
|
||||
CheckBox *enabled_checkbox;
|
||||
|
@ -190,6 +212,8 @@ VoxelGraphEditor::VoxelGraphEditor() {
|
|||
add_child(_context_menu);
|
||||
|
||||
_range_analysis_dialog = memnew(VoxelRangeAnalysisDialog);
|
||||
_range_analysis_dialog->connect("analysis_toggled", this, "_on_range_analysis_toggled");
|
||||
_range_analysis_dialog->connect("area_changed", this, "_on_range_analysis_area_changed");
|
||||
add_child(_range_analysis_dialog);
|
||||
}
|
||||
|
||||
|
@ -210,6 +234,8 @@ void VoxelGraphEditor::set_graph(Ref<VoxelGeneratorGraph> graph) {
|
|||
_graph->connect(VoxelGeneratorGraph::SIGNAL_NODE_NAME_CHANGED, this, "_on_graph_node_name_changed");
|
||||
}
|
||||
|
||||
_debug_renderer.clear();
|
||||
|
||||
build_gui_from_graph();
|
||||
}
|
||||
|
||||
|
@ -217,6 +243,17 @@ void VoxelGraphEditor::set_undo_redo(UndoRedo *undo_redo) {
|
|||
_undo_redo = undo_redo;
|
||||
}
|
||||
|
||||
void VoxelGraphEditor::set_voxel_node(VoxelNode *node) {
|
||||
_voxel_node = node;
|
||||
if (_voxel_node == nullptr) {
|
||||
PRINT_VERBOSE("Reference node for VoxelGraph previews: null");
|
||||
_debug_renderer.set_world(nullptr);
|
||||
} else {
|
||||
PRINT_VERBOSE(String("Reference node for VoxelGraph previews: {0}").format(varray(node->get_path())));
|
||||
_debug_renderer.set_world(_voxel_node->get_world().ptr());
|
||||
}
|
||||
}
|
||||
|
||||
void VoxelGraphEditor::_notification(int p_what) {
|
||||
switch (p_what) {
|
||||
case NOTIFICATION_INTERNAL_PROCESS:
|
||||
|
@ -603,12 +640,24 @@ void VoxelGraphEditor::_check_nothing_selected() {
|
|||
}
|
||||
}
|
||||
|
||||
void reset_modulates(GraphEdit &graph_edit) {
|
||||
for (int child_index = 0; child_index < graph_edit.get_child_count(); ++child_index) {
|
||||
VoxelGraphEditorNode *node_view =
|
||||
Object::cast_to<VoxelGraphEditorNode>(graph_edit.get_child(child_index));
|
||||
if (node_view == nullptr) {
|
||||
continue;
|
||||
}
|
||||
node_view->set_modulate(Color(1, 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
void VoxelGraphEditor::update_previews() {
|
||||
if (_graph.is_null()) {
|
||||
return;
|
||||
}
|
||||
|
||||
clear_range_analysis_tooltips();
|
||||
reset_modulates(*_graph_edit);
|
||||
|
||||
uint64_t time_before = OS::get_singleton()->get_ticks_usec();
|
||||
|
||||
|
@ -640,11 +689,12 @@ void VoxelGraphEditor::update_range_analysis_previews() {
|
|||
ERR_FAIL_COND(!_graph->is_good());
|
||||
|
||||
const AABB aabb = _range_analysis_dialog->get_aabb();
|
||||
|
||||
_graph->analyze_range(aabb.position, aabb.position + aabb.size);
|
||||
_graph->analyze_range(aabb.position, aabb.position + aabb.size, true, true);
|
||||
|
||||
const VoxelGraphRuntime::State &state = _graph->get_last_state_from_current_thread();
|
||||
|
||||
const Color greyed_out_color(1, 1, 1, 0.5);
|
||||
|
||||
for (int child_index = 0; child_index < _graph_edit->get_child_count(); ++child_index) {
|
||||
VoxelGraphEditorNode *node_view =
|
||||
Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_child(child_index));
|
||||
|
@ -652,6 +702,14 @@ void VoxelGraphEditor::update_range_analysis_previews() {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (node_view->output_labels.size() == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assume the node won't run for now
|
||||
// TODO Would be nice if GraphEdit's minimap would take such coloring into account...
|
||||
node_view->set_modulate(greyed_out_color);
|
||||
|
||||
for (int port_index = 0; port_index < node_view->output_labels.size(); ++port_index) {
|
||||
ProgramGraph::PortLocation loc;
|
||||
loc.node_id = node_view->node_id;
|
||||
|
@ -665,6 +723,34 @@ void VoxelGraphEditor::update_range_analysis_previews() {
|
|||
label->set_tooltip(String("Min: {0}\nMax: {1}").format(varray(range.min, range.max)));
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight only nodes that will actually run
|
||||
ArraySlice<const int> execution_map = state.get_debug_execution_map();
|
||||
for (unsigned int i = 0; i < execution_map.size(); ++i) {
|
||||
String node_view_path = node_to_gui_name(execution_map[i]);
|
||||
VoxelGraphEditorNode *node_view =
|
||||
Object::cast_to<VoxelGraphEditorNode>(_graph_edit->get_node(node_view_path));
|
||||
node_view->set_modulate(Color(1, 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
void VoxelGraphEditor::update_range_analysis_gizmo() {
|
||||
if (!_range_analysis_dialog->is_analysis_enabled()) {
|
||||
_debug_renderer.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_voxel_node == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Transform parent_transform = _voxel_node->get_global_transform();
|
||||
const AABB aabb = _range_analysis_dialog->get_aabb();
|
||||
_debug_renderer.begin();
|
||||
_debug_renderer.draw_box(
|
||||
parent_transform * Transform(Basis().scaled(aabb.size), aabb.position),
|
||||
VoxelDebug::ID_VOXEL_GRAPH_DEBUG_BOUNDS);
|
||||
_debug_renderer.end();
|
||||
}
|
||||
|
||||
void VoxelGraphEditor::update_slice_previews() {
|
||||
|
@ -819,6 +905,16 @@ void VoxelGraphEditor::_on_analyze_range_button_pressed() {
|
|||
_range_analysis_dialog->popup_centered_minsize();
|
||||
}
|
||||
|
||||
void VoxelGraphEditor::_on_range_analysis_toggled(bool enabled) {
|
||||
schedule_preview_update();
|
||||
update_range_analysis_gizmo();
|
||||
}
|
||||
|
||||
void VoxelGraphEditor::_on_range_analysis_area_changed() {
|
||||
schedule_preview_update();
|
||||
update_range_analysis_gizmo();
|
||||
}
|
||||
|
||||
void VoxelGraphEditor::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("_on_graph_edit_gui_input", "event"), &VoxelGraphEditor::_on_graph_edit_gui_input);
|
||||
ClassDB::bind_method(
|
||||
|
@ -841,6 +937,10 @@ void VoxelGraphEditor::_bind_methods() {
|
|||
ClassDB::bind_method(D_METHOD("_on_profile_button_pressed"), &VoxelGraphEditor::_on_profile_button_pressed);
|
||||
ClassDB::bind_method(D_METHOD("_on_analyze_range_button_pressed"),
|
||||
&VoxelGraphEditor::_on_analyze_range_button_pressed);
|
||||
ClassDB::bind_method(D_METHOD("_on_range_analysis_toggled", "enabled"),
|
||||
&VoxelGraphEditor::_on_range_analysis_toggled);
|
||||
ClassDB::bind_method(D_METHOD("_on_range_analysis_area_changed"),
|
||||
&VoxelGraphEditor::_on_range_analysis_area_changed);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("_check_nothing_selected"), &VoxelGraphEditor::_check_nothing_selected);
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#ifndef VOXEL_GRAPH_EDITOR_H
|
||||
#define VOXEL_GRAPH_EDITOR_H
|
||||
|
||||
#include "../voxel_debug.h"
|
||||
#include <scene/gui/control.h>
|
||||
|
||||
class VoxelGeneratorGraph;
|
||||
|
@ -9,6 +10,8 @@ class PopupMenu;
|
|||
class AcceptDialog;
|
||||
class UndoRedo;
|
||||
class VoxelRangeAnalysisDialog;
|
||||
class VoxelNode;
|
||||
class Spatial;
|
||||
|
||||
class VoxelGraphEditor : public Control {
|
||||
GDCLASS(VoxelGraphEditor, Control)
|
||||
|
@ -22,6 +25,7 @@ public:
|
|||
inline Ref<VoxelGeneratorGraph> get_graph() const { return _graph; }
|
||||
|
||||
void set_undo_redo(UndoRedo *undo_redo);
|
||||
void set_voxel_node(VoxelNode *node);
|
||||
|
||||
private:
|
||||
void _notification(int p_what);
|
||||
|
@ -37,6 +41,7 @@ private:
|
|||
void update_previews();
|
||||
void update_slice_previews();
|
||||
void update_range_analysis_previews();
|
||||
void update_range_analysis_gizmo();
|
||||
void clear_range_analysis_tooltips();
|
||||
|
||||
void _on_graph_edit_gui_input(Ref<InputEvent> event);
|
||||
|
@ -52,6 +57,8 @@ private:
|
|||
void _on_graph_changed();
|
||||
void _on_graph_node_name_changed(int node_id);
|
||||
void _on_analyze_range_button_pressed();
|
||||
void _on_range_analysis_toggled(bool enabled);
|
||||
void _on_range_analysis_area_changed();
|
||||
|
||||
void _check_nothing_selected();
|
||||
|
||||
|
@ -67,6 +74,8 @@ private:
|
|||
Vector2 _click_position;
|
||||
bool _nothing_selected_check_scheduled = false;
|
||||
float _time_before_preview_update = 0.f;
|
||||
Spatial *_voxel_node = nullptr;
|
||||
VoxelDebug::DebugRenderer _debug_renderer;
|
||||
};
|
||||
|
||||
#endif // VOXEL_GRAPH_EDITOR_H
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
#include "voxel_graph_editor_plugin.h"
|
||||
#include "../../generators/graph/voxel_generator_graph.h"
|
||||
#include "../../terrain/voxel_node.h"
|
||||
#include "voxel_graph_editor.h"
|
||||
#include "voxel_graph_node_inspector_wrapper.h"
|
||||
|
||||
#include <editor/editor_data.h>
|
||||
#include <editor/editor_scale.h>
|
||||
|
||||
VoxelGraphEditorPlugin::VoxelGraphEditorPlugin(EditorNode *p_node) {
|
||||
|
@ -41,6 +43,19 @@ void VoxelGraphEditorPlugin::edit(Object *p_object) {
|
|||
Ref<VoxelGeneratorGraph> graph(graph_ptr);
|
||||
_graph_editor->set_undo_redo(&get_undo_redo()); // UndoRedo isn't available in constructor
|
||||
_graph_editor->set_graph(graph);
|
||||
|
||||
VoxelNode *voxel_node = nullptr;
|
||||
Array selected_nodes = get_editor_interface()->get_selection()->get_selected_nodes();
|
||||
for (int i = 0; i < selected_nodes.size(); ++i) {
|
||||
Node *node = selected_nodes[i];
|
||||
ERR_FAIL_COND(node == nullptr);
|
||||
VoxelNode *vn = Object::cast_to<VoxelNode>(node);
|
||||
if (vn != nullptr && vn->get_generator() == graph_ptr) {
|
||||
voxel_node = vn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_graph_editor->set_voxel_node(voxel_node);
|
||||
}
|
||||
|
||||
void VoxelGraphEditorPlugin::make_visible(bool visible) {
|
||||
|
@ -50,6 +65,7 @@ void VoxelGraphEditorPlugin::make_visible(bool visible) {
|
|||
|
||||
} else {
|
||||
_bottom_panel_button->hide();
|
||||
_graph_editor->set_voxel_node(nullptr);
|
||||
|
||||
// TODO Awful hack to handle the nonsense happening in `_on_graph_editor_node_selected`
|
||||
if (!_deferred_visibility_scheduled) {
|
||||
|
|
|
@ -21,6 +21,8 @@ static Color get_color(ColorID id) {
|
|||
return Color(1, 1, 1);
|
||||
case ID_OCTREE_BOUNDS:
|
||||
return Color(0.5, 0.5, 0.5);
|
||||
case ID_VOXEL_GRAPH_DEBUG_BOUNDS:
|
||||
return Color(1.0, 1.0, 0.0);
|
||||
default:
|
||||
CRASH_NOW_MSG("Unexpected index");
|
||||
}
|
||||
|
@ -170,6 +172,7 @@ void DebugRenderer::begin() {
|
|||
}
|
||||
|
||||
void DebugRenderer::draw_box(Transform t, ColorID color) {
|
||||
// Pick an existing item, or create one
|
||||
DebugRendererItem *item;
|
||||
if (_current >= _items.size()) {
|
||||
item = memnew(DebugRendererItem);
|
||||
|
@ -188,6 +191,7 @@ void DebugRenderer::draw_box(Transform t, ColorID color) {
|
|||
|
||||
void DebugRenderer::end() {
|
||||
CRASH_COND(!_inside_block);
|
||||
// Hide exceeding items
|
||||
for (unsigned int i = _current; i < _items.size(); ++i) {
|
||||
DebugRendererItem *item = _items[i];
|
||||
item->set_visible(false);
|
||||
|
|
|
@ -13,6 +13,7 @@ namespace VoxelDebug {
|
|||
enum ColorID {
|
||||
ID_VOXEL_BOUNDS = 0,
|
||||
ID_OCTREE_BOUNDS,
|
||||
ID_VOXEL_GRAPH_DEBUG_BOUNDS,
|
||||
ID_COUNT
|
||||
};
|
||||
|
||||
|
|
|
@ -207,7 +207,7 @@ void ProgramGraph::find_terminal_nodes(std::vector<uint32_t> &node_ids) const {
|
|||
}
|
||||
}
|
||||
|
||||
void ProgramGraph::find_dependencies(std::vector<uint32_t> nodes_to_process, std::vector<uint32_t> &order) const {
|
||||
void ProgramGraph::find_dependencies(std::vector<uint32_t> nodes_to_process, std::vector<uint32_t> &out_order) const {
|
||||
// Finds dependencies of the given nodes, and returns them in the order they should be processed
|
||||
std::unordered_set<uint32_t> visited_nodes;
|
||||
|
||||
|
@ -233,7 +233,7 @@ void ProgramGraph::find_dependencies(std::vector<uint32_t> nodes_to_process, std
|
|||
|
||||
if (nodes_to_process_begin == nodes_to_process.size()) {
|
||||
// No ancestor to visit, process the node
|
||||
order.push_back(node->id);
|
||||
out_order.push_back(node->id);
|
||||
visited_nodes.insert(node->id);
|
||||
nodes_to_process.pop_back();
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ public:
|
|||
bool disconnect(PortLocation src, PortLocation dst);
|
||||
|
||||
bool has_path(uint32_t p_src_node_id, uint32_t p_dst_node_id) const;
|
||||
void find_dependencies(std::vector<uint32_t> nodes_to_process, std::vector<uint32_t> &order) const;
|
||||
void find_dependencies(std::vector<uint32_t> nodes_to_process, std::vector<uint32_t> &out_order) const;
|
||||
void find_immediate_dependencies(uint32_t node_id, std::vector<uint32_t> &deps) const;
|
||||
void find_depth_first(uint32_t start_node_id, std::vector<uint32_t> &order) const;
|
||||
void find_terminal_nodes(std::vector<uint32_t> &node_ids) const;
|
||||
|
|
|
@ -270,6 +270,9 @@ void VoxelGeneratorGraph::generate_block(VoxelBlockRequest &input) {
|
|||
|
||||
// The section may have the surface in it, we have to calculate it
|
||||
|
||||
// Optimize out branches of the graph that won't contribute to the result
|
||||
runtime->generate_optimized_execution_map(cache.state, false);
|
||||
|
||||
{
|
||||
unsigned int i = 0;
|
||||
for (int rz = rmin.z, gz = gmin.z; rz < rmax.z; ++rz, gz += stride) {
|
||||
|
@ -286,7 +289,7 @@ void VoxelGeneratorGraph::generate_block(VoxelBlockRequest &input) {
|
|||
|
||||
y_cache.fill(gy);
|
||||
|
||||
runtime->generate_set(cache.state, x_cache, y_cache, z_cache, slice_cache, ry != rmin.y);
|
||||
runtime->generate_set(cache.state, x_cache, y_cache, z_cache, slice_cache, ry != rmin.y, true);
|
||||
|
||||
// TODO Flatten this further
|
||||
{
|
||||
|
@ -332,7 +335,7 @@ void VoxelGeneratorGraph::generate_set(ArraySlice<float> in_x, ArraySlice<float>
|
|||
ERR_FAIL_COND(_runtime == nullptr || !_runtime->has_output());
|
||||
Cache &cache = _cache;
|
||||
_runtime->prepare_state(cache.state, in_x.size());
|
||||
_runtime->generate_set(cache.state, in_x, in_y, in_z, out_sdf, false);
|
||||
_runtime->generate_set(cache.state, in_x, in_y, in_z, out_sdf, false, false);
|
||||
}
|
||||
|
||||
const VoxelGraphRuntime::State &VoxelGeneratorGraph::get_last_state_from_current_thread() {
|
||||
|
@ -348,6 +351,12 @@ bool VoxelGeneratorGraph::try_get_output_port_address(ProgramGraph::PortLocation
|
|||
return res;
|
||||
}
|
||||
|
||||
void VoxelGeneratorGraph::find_dependencies(uint32_t node_id, std::vector<uint32_t> &out_dependencies) const {
|
||||
std::vector<uint32_t> dst;
|
||||
dst.push_back(node_id);
|
||||
_graph.find_dependencies(dst, out_dependencies);
|
||||
}
|
||||
|
||||
inline Vector3 get_3d_pos_from_panorama_uv(Vector2 uv) {
|
||||
const float xa = -Math_TAU * uv.x - Math_PI;
|
||||
const float ya = -Math_PI * (uv.y - 0.5f);
|
||||
|
@ -444,7 +453,7 @@ void VoxelGeneratorGraph::bake_sphere_bumpmap(Ref<Image> im, float ref_radius, f
|
|||
}
|
||||
|
||||
runtime->generate_set(state,
|
||||
to_slice(x_coords), to_slice(y_coords), to_slice(z_coords), to_slice(sdf_values), false);
|
||||
to_slice(x_coords), to_slice(y_coords), to_slice(z_coords), to_slice(sdf_values), false, false);
|
||||
|
||||
// Calculate final pixels
|
||||
im->lock();
|
||||
|
@ -541,8 +550,9 @@ void VoxelGeneratorGraph::bake_sphere_normalmap(Ref<Image> im, float ref_radius,
|
|||
++i;
|
||||
}
|
||||
}
|
||||
// TODO Perform range analysis on the range of coordinates, it might still yield performance benefits
|
||||
runtime->generate_set(state,
|
||||
to_slice(x_coords), to_slice(y_coords), to_slice(z_coords), to_slice(sdf_values_p), false);
|
||||
to_slice(x_coords), to_slice(y_coords), to_slice(z_coords), to_slice(sdf_values_p), false, false);
|
||||
|
||||
// Get neighbors along X
|
||||
i = 0;
|
||||
|
@ -557,7 +567,7 @@ void VoxelGeneratorGraph::bake_sphere_normalmap(Ref<Image> im, float ref_radius,
|
|||
}
|
||||
}
|
||||
runtime->generate_set(state,
|
||||
to_slice(x_coords), to_slice(y_coords), to_slice(z_coords), to_slice(sdf_values_px), false);
|
||||
to_slice(x_coords), to_slice(y_coords), to_slice(z_coords), to_slice(sdf_values_px), false, false);
|
||||
|
||||
// Get neighbors along Y
|
||||
i = 0;
|
||||
|
@ -572,7 +582,7 @@ void VoxelGeneratorGraph::bake_sphere_normalmap(Ref<Image> im, float ref_radius,
|
|||
}
|
||||
}
|
||||
runtime->generate_set(state,
|
||||
to_slice(x_coords), to_slice(y_coords), to_slice(z_coords), to_slice(sdf_values_py), false);
|
||||
to_slice(x_coords), to_slice(y_coords), to_slice(z_coords), to_slice(sdf_values_py), false, false);
|
||||
|
||||
// TODO This is probably invalid due to the distortion, may need to use another approach.
|
||||
// Compute the 3D normal from gradient, then project it?
|
||||
|
@ -626,10 +636,11 @@ float VoxelGeneratorGraph::generate_single(const Vector3i &position) {
|
|||
ERR_FAIL_COND_V(runtime == nullptr || !runtime->has_output(), 0.f);
|
||||
Cache &cache = _cache;
|
||||
runtime->prepare_state(cache.state, 1);
|
||||
return runtime->generate_single(cache.state, position.to_vec3());
|
||||
return runtime->generate_single(cache.state, position.to_vec3(), false);
|
||||
}
|
||||
|
||||
Interval VoxelGeneratorGraph::analyze_range(Vector3i min_pos, Vector3i max_pos) const {
|
||||
Interval VoxelGeneratorGraph::analyze_range(Vector3i min_pos, Vector3i max_pos,
|
||||
bool optimize_execution_map, bool debug) const {
|
||||
std::shared_ptr<const VoxelGraphRuntime> runtime;
|
||||
{
|
||||
RWLockRead rlock(_runtime_lock);
|
||||
|
@ -639,7 +650,11 @@ Interval VoxelGeneratorGraph::analyze_range(Vector3i min_pos, Vector3i max_pos)
|
|||
Cache &cache = _cache;
|
||||
// Note, buffer size is irrelevant here, because range analysis doesn't use buffers
|
||||
runtime->prepare_state(cache.state, 1);
|
||||
return runtime->analyze_range(cache.state, min_pos, max_pos);
|
||||
Interval res = runtime->analyze_range(cache.state, min_pos, max_pos);
|
||||
if (optimize_execution_map) {
|
||||
runtime->generate_optimized_execution_map(cache.state, debug);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Ref<Resource> VoxelGeneratorGraph::duplicate(bool p_subresources) const {
|
||||
|
@ -821,7 +836,7 @@ float VoxelGeneratorGraph::debug_measure_microseconds_per_voxel(bool singular) {
|
|||
for (uint32_t z = 0; z < cube_size; ++z) {
|
||||
for (uint32_t y = 0; y < cube_size; ++y) {
|
||||
for (uint32_t x = 0; x < cube_size; ++x) {
|
||||
runtime->generate_single(cache.state, Vector3i(x, y, z).to_vec3());
|
||||
runtime->generate_single(cache.state, Vector3i(x, y, z).to_vec3(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -849,7 +864,7 @@ float VoxelGeneratorGraph::debug_measure_microseconds_per_voxel(bool singular) {
|
|||
profiling_clock.restart();
|
||||
|
||||
for (uint32_t y = 0; y < cube_size; ++y) {
|
||||
runtime->generate_set(cache.state, sx, sy, sz, sdst, false);
|
||||
runtime->generate_set(cache.state, sx, sy, sz, sdst, false, false);
|
||||
}
|
||||
|
||||
elapsed_us += profiling_clock.restart();
|
||||
|
@ -944,7 +959,7 @@ float VoxelGeneratorGraph::_b_generate_single(Vector3 pos) {
|
|||
}
|
||||
|
||||
Vector2 VoxelGeneratorGraph::_b_analyze_range(Vector3 min_pos, Vector3 max_pos) const {
|
||||
const Interval r = analyze_range(Vector3i::from_floored(min_pos), Vector3i::from_floored(max_pos));
|
||||
const Interval r = analyze_range(Vector3i::from_floored(min_pos), Vector3i::from_floored(max_pos), false, false);
|
||||
return Vector2(r.min, r.max);
|
||||
}
|
||||
|
||||
|
|
|
@ -118,13 +118,16 @@ public:
|
|||
void generate_set(ArraySlice<float> in_x, ArraySlice<float> in_y, ArraySlice<float> in_z,
|
||||
ArraySlice<float> out_sdf);
|
||||
|
||||
Interval analyze_range(Vector3i min_pos, Vector3i max_pos) const;
|
||||
Interval analyze_range(Vector3i min_pos, Vector3i max_pos, bool optimize_execution_map, bool debug) const;
|
||||
void generate_optimized_execution_map();
|
||||
|
||||
// Returns state from the last generator used in the current thread
|
||||
static const VoxelGraphRuntime::State &get_last_state_from_current_thread();
|
||||
|
||||
bool try_get_output_port_address(ProgramGraph::PortLocation port, uint32_t &out_address) const;
|
||||
|
||||
void find_dependencies(uint32_t node_id, std::vector<uint32_t> &out_dependencies) const;
|
||||
|
||||
// Debug
|
||||
|
||||
float debug_measure_microseconds_per_voxel(bool singular);
|
||||
|
|
|
@ -601,6 +601,7 @@ VoxelGraphNodeDB::VoxelGraphNodeDB() {
|
|||
t.inputs.push_back(Port("b"));
|
||||
t.inputs.push_back(Port("ratio"));
|
||||
t.outputs.push_back(Port("out"));
|
||||
// TODO Add a `clamp` parameter? It helps optimization
|
||||
t.process_buffer_func = [](ProcessBufferContext &ctx) {
|
||||
const VoxelGraphRuntime::Buffer &a = ctx.get_input(0);
|
||||
const VoxelGraphRuntime::Buffer &b = ctx.get_input(1);
|
||||
|
@ -636,6 +637,15 @@ VoxelGraphNodeDB::VoxelGraphNodeDB() {
|
|||
// Note: if I call this `t` like I use to do with `lerp`, GCC complains it shadows `t` from the outer scope.
|
||||
// Even though this lambda does not capture anything from the outer scope :shrug:
|
||||
const Interval r = ctx.get_input(2);
|
||||
if (r.is_single_value()) {
|
||||
if (r.min == 1.f) {
|
||||
// a will be ignored
|
||||
ctx.ignore_input(0);
|
||||
} else if (r.min == 0.f) {
|
||||
// b will be ignored
|
||||
ctx.ignore_input(1);
|
||||
}
|
||||
}
|
||||
ctx.set_output(0, lerp(a, b, r));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ VoxelGraphRuntime::CompilationResult VoxelGraphRuntime::_compile(const ProgramGr
|
|||
|
||||
std::vector<uint32_t> order;
|
||||
std::vector<uint32_t> terminal_nodes;
|
||||
std::unordered_map<uint32_t, uint32_t> node_id_to_dependency_graph;
|
||||
|
||||
graph.find_terminal_nodes(terminal_nodes);
|
||||
|
||||
|
@ -162,35 +163,48 @@ VoxelGraphRuntime::CompilationResult VoxelGraphRuntime::_compile(const ProgramGr
|
|||
//#endif
|
||||
|
||||
struct MemoryHelper {
|
||||
std::vector<uint16_t> &bindings;
|
||||
std::vector<Constant> &constants;
|
||||
std::vector<BufferSpec> &buffer_specs;
|
||||
unsigned int next_address = 0;
|
||||
|
||||
uint16_t add_binding() {
|
||||
const unsigned int a = next_address;
|
||||
++next_address;
|
||||
bindings.push_back(a);
|
||||
BufferSpec bs;
|
||||
bs.address = a;
|
||||
bs.is_binding = true;
|
||||
bs.is_constant = false;
|
||||
bs.users_count = 0;
|
||||
buffer_specs.push_back(bs);
|
||||
return a;
|
||||
}
|
||||
|
||||
uint16_t add_var() {
|
||||
const unsigned int a = next_address;
|
||||
++next_address;
|
||||
BufferSpec bs;
|
||||
bs.address = a;
|
||||
bs.is_binding = false;
|
||||
bs.is_constant = false;
|
||||
bs.users_count = 0;
|
||||
buffer_specs.push_back(bs);
|
||||
return a;
|
||||
}
|
||||
|
||||
uint16_t add_constant(float v) {
|
||||
const unsigned int a = next_address;
|
||||
++next_address;
|
||||
Constant c;
|
||||
c.address = a;
|
||||
c.value = v;
|
||||
constants.push_back(c);
|
||||
BufferSpec bs;
|
||||
bs.address = a;
|
||||
bs.constant_value = v;
|
||||
bs.is_binding = false;
|
||||
bs.is_constant = true;
|
||||
bs.users_count = 0;
|
||||
buffer_specs.push_back(bs);
|
||||
return a;
|
||||
}
|
||||
};
|
||||
|
||||
MemoryHelper mem{ _program.bindings, _program.constants };
|
||||
MemoryHelper mem{ _program.buffer_specs };
|
||||
|
||||
// Main inputs X, Y, Z
|
||||
_program.x_input_address = mem.add_binding();
|
||||
|
@ -211,9 +225,19 @@ VoxelGraphRuntime::CompilationResult VoxelGraphRuntime::_compile(const ProgramGr
|
|||
CRASH_COND(node->outputs.size() != type.outputs.size());
|
||||
|
||||
if (order_index == xzy_start_index) {
|
||||
_program.xzy_start = operations.size();
|
||||
_program.xzy_start_op_address = operations.size();
|
||||
}
|
||||
|
||||
const unsigned int dg_node_index = _program.dependency_graph.nodes.size();
|
||||
_program.dependency_graph.nodes.push_back(DependencyGraph::Node());
|
||||
DependencyGraph::Node &dg_node = _program.dependency_graph.nodes.back();
|
||||
dg_node.is_output = false;
|
||||
dg_node.op_address = operations.size();
|
||||
dg_node.first_dependency = _program.dependency_graph.dependencies.size();
|
||||
dg_node.end_dependency = dg_node.first_dependency;
|
||||
dg_node.debug_node_id = node_id;
|
||||
node_id_to_dependency_graph.insert(std::make_pair(node_id, dg_node_index));
|
||||
|
||||
// We still hardcode some of the nodes. Maybe we can abstract them too one day.
|
||||
switch (node->type_id) {
|
||||
case VoxelGeneratorGraph::NODE_CONSTANT: {
|
||||
|
@ -251,6 +275,18 @@ VoxelGraphRuntime::CompilationResult VoxelGraphRuntime::_compile(const ProgramGr
|
|||
// Previous node ports must have been registered
|
||||
CRASH_COND(aptr == nullptr);
|
||||
_program.sdf_output_address = *aptr;
|
||||
|
||||
BufferSpec &bs = _program.buffer_specs[*aptr];
|
||||
++bs.users_count;
|
||||
|
||||
// Register dependency
|
||||
auto it = node_id_to_dependency_graph.find(src_port.node_id);
|
||||
CRASH_COND(it == node_id_to_dependency_graph.end());
|
||||
CRASH_COND(it->second >= _program.dependency_graph.nodes.size());
|
||||
_program.dependency_graph.dependencies.push_back(it->second);
|
||||
++dg_node.end_dependency;
|
||||
dg_node.is_output = true;
|
||||
_program.sdf_output_node_index = dg_node_index;
|
||||
}
|
||||
continue;
|
||||
|
||||
|
@ -259,7 +295,14 @@ VoxelGraphRuntime::CompilationResult VoxelGraphRuntime::_compile(const ProgramGr
|
|||
};
|
||||
|
||||
// Add actual operation
|
||||
|
||||
CRASH_COND(node->type_id > 0xff);
|
||||
|
||||
if (order_index == xzy_start_index) {
|
||||
_program.xzy_start_execution_map_index = _program.default_execution_map.size();
|
||||
}
|
||||
_program.default_execution_map.push_back(operations.size());
|
||||
|
||||
append(operations, static_cast<uint8_t>(node->type_id));
|
||||
|
||||
// Inputs and outputs use a convention so we can have generic code for them.
|
||||
|
@ -281,9 +324,19 @@ VoxelGraphRuntime::CompilationResult VoxelGraphRuntime::_compile(const ProgramGr
|
|||
// Previous node ports must have been registered
|
||||
CRASH_COND(aptr == nullptr);
|
||||
a = *aptr;
|
||||
|
||||
// Register dependency
|
||||
auto it = node_id_to_dependency_graph.find(src_port.node_id);
|
||||
CRASH_COND(it == node_id_to_dependency_graph.end());
|
||||
CRASH_COND(it->second >= _program.dependency_graph.nodes.size());
|
||||
_program.dependency_graph.dependencies.push_back(it->second);
|
||||
++dg_node.end_dependency;
|
||||
}
|
||||
|
||||
append(operations, a);
|
||||
|
||||
BufferSpec &bs = _program.buffer_specs[a];
|
||||
++bs.users_count;
|
||||
}
|
||||
|
||||
// Add outputs
|
||||
|
@ -365,13 +418,182 @@ VoxelGraphRuntime::CompilationResult VoxelGraphRuntime::_compile(const ProgramGr
|
|||
return result;
|
||||
}
|
||||
|
||||
float VoxelGraphRuntime::generate_single(State &state, Vector3 position) const {
|
||||
static ArraySlice<const uint16_t> get_outputs_from_op_address(
|
||||
ArraySlice<const uint8_t> &operations, uint16_t op_address) {
|
||||
|
||||
const uint8_t opid = operations[op_address];
|
||||
const VoxelGraphNodeDB::NodeType &node_type = VoxelGraphNodeDB::get_singleton()->get_type(opid);
|
||||
|
||||
const uint32_t inputs_size = node_type.inputs.size() * sizeof(uint16_t);
|
||||
const uint32_t outputs_size = node_type.outputs.size() * sizeof(uint16_t);
|
||||
|
||||
// The +1 is for `opid`
|
||||
return operations.sub(op_address + 1 + inputs_size, outputs_size).reinterpret_cast_to<const uint16_t>();
|
||||
}
|
||||
|
||||
bool VoxelGraphRuntime::is_operation_constant(const State &state, uint16_t op_address) const {
|
||||
ArraySlice<const uint16_t> outputs = get_outputs_from_op_address(to_slice_const(_program.operations), op_address);
|
||||
|
||||
for (unsigned int i = 0; i < outputs.size(); ++i) {
|
||||
const uint16_t output_address = outputs[i];
|
||||
const Buffer &buffer = state.get_buffer(output_address);
|
||||
if (!(buffer.is_constant ||
|
||||
state.get_range(output_address).is_single_value() ||
|
||||
buffer.local_users_count == 0)) {
|
||||
// At least one of the outputs cannot be predicted in the current area
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Generates a list of adresses for the operations to execute,
|
||||
// skipping those that are deemed constant by the last range analysis.
|
||||
// If a non-constant operation only contributes to a constant one, it will also be skipped.
|
||||
// This has the effect of optimizing locally at runtime without relying on explicit conditionals.
|
||||
// It can be useful for biomes, where some branches become constant when not used in the final blending.
|
||||
void VoxelGraphRuntime::generate_execution_map(const State &state,
|
||||
std::vector<uint16_t> &execution_map, unsigned int &out_mapped_xzy_start,
|
||||
std::vector<int> *debug_execution_map) const {
|
||||
|
||||
VOXEL_PROFILE_SCOPE();
|
||||
|
||||
// Range analysis results must have been computed
|
||||
ERR_FAIL_COND(state.ranges.size() == 0);
|
||||
|
||||
const Program &program = _program;
|
||||
const DependencyGraph &graph = program.dependency_graph;
|
||||
|
||||
static thread_local std::vector<uint16_t> to_process;
|
||||
to_process.clear();
|
||||
to_process.push_back(program.sdf_output_node_index);
|
||||
|
||||
enum ProcessResult {
|
||||
NOT_PROCESSED,
|
||||
SKIPPABLE,
|
||||
REQUIRED
|
||||
};
|
||||
|
||||
static thread_local std::vector<ProcessResult> results;
|
||||
results.clear();
|
||||
results.resize(graph.nodes.size(), NOT_PROCESSED);
|
||||
|
||||
while (to_process.size() != 0) {
|
||||
const uint32_t node_index = to_process.back();
|
||||
const unsigned int to_process_previous_size = to_process.size();
|
||||
|
||||
// Check needed because Godot never compiles with `_DEBUG`...
|
||||
#ifdef DEBUG_ENABLED
|
||||
CRASH_COND(node_index >= graph.nodes.size());
|
||||
#endif
|
||||
const DependencyGraph::Node &node = graph.nodes[node_index];
|
||||
|
||||
// Ignore outputs because they are not present in the operations list
|
||||
if (!node.is_output && is_operation_constant(state, node.op_address)) {
|
||||
// Skip this operation for now.
|
||||
// If no other dependency reaches it, it will be effectively skipped in the result.
|
||||
to_process.pop_back();
|
||||
results[node_index] = SKIPPABLE;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (uint32_t i = node.first_dependency; i < node.end_dependency; ++i) {
|
||||
const uint32_t dep_node_index = graph.dependencies[i];
|
||||
if (results[dep_node_index] != NOT_PROCESSED) {
|
||||
// Already processed
|
||||
continue;
|
||||
}
|
||||
to_process.push_back(dep_node_index);
|
||||
}
|
||||
|
||||
if (to_process_previous_size == to_process.size()) {
|
||||
to_process.pop_back();
|
||||
results[node_index] = REQUIRED;
|
||||
}
|
||||
}
|
||||
|
||||
if (debug_execution_map != nullptr) {
|
||||
debug_execution_map->clear();
|
||||
for (unsigned int node_index = 0; node_index < graph.nodes.size(); ++node_index) {
|
||||
const ProcessResult res = results[node_index];
|
||||
const DependencyGraph::Node &node = graph.nodes[node_index];
|
||||
if (res == REQUIRED) {
|
||||
debug_execution_map->push_back(node.debug_node_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
execution_map.clear();
|
||||
|
||||
ArraySlice<const uint8_t> operations(program.operations.data(), 0, program.operations.size());
|
||||
bool xzy_start_not_assigned = true;
|
||||
|
||||
// Now we have to fill buffers with the local constants we may have found.
|
||||
// We iterate nodes primarily because we have to preserve a certain order relative to outer loop optimization.
|
||||
for (unsigned int node_index = 0; node_index < graph.nodes.size(); ++node_index) {
|
||||
const ProcessResult res = results[node_index];
|
||||
const DependencyGraph::Node &node = graph.nodes[node_index];
|
||||
|
||||
if (node.is_output) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (res) {
|
||||
case NOT_PROCESSED:
|
||||
continue;
|
||||
|
||||
case SKIPPABLE: {
|
||||
const ArraySlice<const uint16_t> outputs = get_outputs_from_op_address(operations, node.op_address);
|
||||
|
||||
for (unsigned int i = 0; i < outputs.size(); ++i) {
|
||||
const uint16_t output_address = outputs[i];
|
||||
const Buffer &buffer = state.get_buffer(output_address);
|
||||
|
||||
if (buffer.is_constant) {
|
||||
// Already assigned at prepare-time
|
||||
continue;
|
||||
}
|
||||
|
||||
CRASH_COND(buffer.is_binding);
|
||||
|
||||
// The node is considered skippable, which means its outputs are either locally constant or unused.
|
||||
// Unused buffers can be left as-is, but local constants must be filled in.
|
||||
if (buffer.local_users_count > 0) {
|
||||
const Interval range = state.ranges[output_address];
|
||||
CRASH_COND(!range.is_single_value());
|
||||
const float v = range.min;
|
||||
for (unsigned int j = 0; j < buffer.size; ++j) {
|
||||
buffer.data[j] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case REQUIRED:
|
||||
if (xzy_start_not_assigned && node.op_address >= program.xzy_start_op_address) {
|
||||
// This should be correct as long as the list of nodes in the graph follows the same re-ordered
|
||||
// optimization done in `compile()` such that all nodes not depending on Y come first
|
||||
out_mapped_xzy_start = execution_map.size();
|
||||
xzy_start_not_assigned = false;
|
||||
}
|
||||
execution_map.push_back(node.op_address);
|
||||
break;
|
||||
|
||||
default:
|
||||
CRASH_NOW();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float VoxelGraphRuntime::generate_single(State &state, Vector3 position, bool use_execution_map) const {
|
||||
float output;
|
||||
generate_set(state,
|
||||
ArraySlice<float>(&position.x, 1),
|
||||
ArraySlice<float>(&position.y, 1),
|
||||
ArraySlice<float>(&position.z, 1),
|
||||
ArraySlice<float>(&output, 1), false);
|
||||
ArraySlice<float>(&output, 1), false, use_execution_map);
|
||||
return output;
|
||||
}
|
||||
|
||||
|
@ -385,17 +607,21 @@ void VoxelGraphRuntime::prepare_state(State &state, unsigned int buffer_size) co
|
|||
ArraySlice<Buffer> buffers(state.buffers, 0, state.buffers.size());
|
||||
state.buffer_size = buffer_size;
|
||||
|
||||
for (auto it = _program.bindings.begin(); it != _program.bindings.end(); ++it) {
|
||||
const uint16_t a = *it;
|
||||
Buffer &b = buffers[a];
|
||||
if (b.is_binding) {
|
||||
// Forgot to unbind?
|
||||
CRASH_COND(b.data != nullptr);
|
||||
} else if (b.data != nullptr) {
|
||||
// Deallocate this buffer if it wasnt a binding and contained something
|
||||
memdelete(b.data);
|
||||
for (auto it = _program.buffer_specs.cbegin(); it != _program.buffer_specs.cend(); ++it) {
|
||||
const BufferSpec &buffer_spec = *it;
|
||||
Buffer &buffer = buffers[buffer_spec.address];
|
||||
|
||||
if (buffer_spec.is_binding) {
|
||||
if (buffer.is_binding) {
|
||||
// Forgot to unbind?
|
||||
CRASH_COND(buffer.data != nullptr);
|
||||
} else if (buffer.data != nullptr) {
|
||||
// Deallocate this buffer if it wasnt a binding and contained something
|
||||
memdelete(buffer.data);
|
||||
}
|
||||
}
|
||||
b.is_binding = true;
|
||||
|
||||
buffer.is_binding = buffer_spec.is_binding;
|
||||
}
|
||||
|
||||
// Allocate more buffers if needed
|
||||
|
@ -442,19 +668,23 @@ void VoxelGraphRuntime::prepare_state(State &state, unsigned int buffer_size) co
|
|||
state.ranges.resize(_program.buffer_count);
|
||||
|
||||
// Always reset constants because we don't know if we'll run the same program as before...
|
||||
for (auto it = _program.constants.begin(); it != _program.constants.end(); ++it) {
|
||||
const Constant &c = *it;
|
||||
Buffer &buffer = buffers[c.address];
|
||||
buffer.is_constant = true;
|
||||
buffer.constant_value = c.value;
|
||||
CRASH_COND(buffer.size > buffer.capacity);
|
||||
for (unsigned int j = 0; j < buffer_size; ++j) {
|
||||
buffer.data[j] = c.value;
|
||||
for (auto it = _program.buffer_specs.cbegin(); it != _program.buffer_specs.cend(); ++it) {
|
||||
const BufferSpec &bs = *it;
|
||||
Buffer &buffer = buffers[bs.address];
|
||||
if (bs.is_constant) {
|
||||
buffer.is_constant = true;
|
||||
buffer.constant_value = bs.constant_value;
|
||||
CRASH_COND(buffer.size > buffer.capacity);
|
||||
for (unsigned int j = 0; j < buffer_size; ++j) {
|
||||
buffer.data[j] = bs.constant_value;
|
||||
}
|
||||
CRASH_COND(bs.address >= state.ranges.size());
|
||||
state.ranges[bs.address] = Interval::from_single_value(bs.constant_value);
|
||||
}
|
||||
CRASH_COND(c.address >= state.ranges.size());
|
||||
state.ranges[c.address] = Interval::from_single_value(c.value);
|
||||
}
|
||||
|
||||
state.execution_map.clear();
|
||||
|
||||
/*if (use_range_analysis) {
|
||||
// TODO To be really worth it, we may need a runtime graph traversal pass,
|
||||
// where we build an execution map of nodes that are worthy 🔨
|
||||
|
@ -471,7 +701,8 @@ void VoxelGraphRuntime::prepare_state(State &state, unsigned int buffer_size) co
|
|||
|
||||
void VoxelGraphRuntime::generate_set(State &state,
|
||||
ArraySlice<float> in_x, ArraySlice<float> in_y, ArraySlice<float> in_z,
|
||||
ArraySlice<float> out_sdf, bool skip_xz) const {
|
||||
ArraySlice<float> out_sdf, bool skip_xz, bool use_execution_map) const {
|
||||
|
||||
// I don't like putting private helper functions in headers.
|
||||
struct L {
|
||||
static inline void bind_buffer(ArraySlice<Buffer> buffers, int a, ArraySlice<float> d) {
|
||||
|
@ -503,6 +734,8 @@ void VoxelGraphRuntime::generate_set(State &state,
|
|||
ERR_FAIL_COND(state.buffers.size() == 0);
|
||||
ERR_FAIL_COND(state.buffer_size < buffer_size);
|
||||
ERR_FAIL_COND(state.buffers[0].size < buffer_size);
|
||||
ERR_FAIL_COND(use_execution_map && state.execution_map.size() == 0);
|
||||
ERR_FAIL_COND(use_execution_map && state.execution_map_xzy_start_index >= state.execution_map.size());
|
||||
#ifdef DEBUG_ENABLED
|
||||
for (size_t i = 0; i < state.buffers.size(); ++i) {
|
||||
const Buffer &b = state.buffers[i];
|
||||
|
@ -529,19 +762,21 @@ void VoxelGraphRuntime::generate_set(State &state,
|
|||
L::bind_buffer(buffers, _program.z_input_address, in_z);
|
||||
}
|
||||
|
||||
uint32_t pc = skip_xz ? _program.xzy_start : 0;
|
||||
|
||||
// STL is unreadable on debug builds of Godot, because _DEBUG isn't defined
|
||||
//#ifdef DEBUG_ENABLED
|
||||
// const size_t memory_size = memory.size();
|
||||
// const size_t program_size = _program.size();
|
||||
// const float *memory_raw = memory.data();
|
||||
// const uint8_t *program_raw = (const uint8_t *)_program.data();
|
||||
//#endif
|
||||
|
||||
const ArraySlice<const uint8_t> operations(_program.operations.data(), 0, _program.operations.size());
|
||||
|
||||
while (pc < operations.size()) {
|
||||
ArraySlice<const uint16_t> execution_map = use_execution_map ?
|
||||
to_slice_const(state.execution_map) :
|
||||
to_slice_const(_program.default_execution_map);
|
||||
if (skip_xz) {
|
||||
const unsigned int offset = use_execution_map ?
|
||||
state.execution_map_xzy_start_index :
|
||||
_program.xzy_start_execution_map_index;
|
||||
execution_map = execution_map.sub(offset);
|
||||
}
|
||||
|
||||
for (unsigned int execution_map_index = 0; execution_map_index < execution_map.size(); ++execution_map_index) {
|
||||
unsigned int pc = execution_map[execution_map_index];
|
||||
|
||||
const uint8_t opid = operations[pc++];
|
||||
const VoxelGraphNodeDB::NodeType &node_type = VoxelGraphNodeDB::get_singleton()->get_type(opid);
|
||||
|
||||
|
@ -559,18 +794,7 @@ void VoxelGraphRuntime::generate_set(State &state,
|
|||
ArraySlice<const uint8_t> params;
|
||||
if (params_size > 0) {
|
||||
params = operations.sub(pc, params_size);
|
||||
pc += params_size;
|
||||
}
|
||||
|
||||
// TODO May be replaced by execution mapping?
|
||||
// Skip node if all its outputs are constant
|
||||
bool all_outputs_constant = true;
|
||||
for (uint32_t i = 0; i < outputs.size(); ++i) {
|
||||
const Buffer &buffer = buffers[outputs[i]];
|
||||
all_outputs_constant &= buffer.is_constant;
|
||||
}
|
||||
if (all_outputs_constant) {
|
||||
continue;
|
||||
//pc += params_size;
|
||||
}
|
||||
|
||||
ERR_FAIL_COND(node_type.process_buffer_func == nullptr);
|
||||
|
@ -600,12 +824,22 @@ void VoxelGraphRuntime::generate_set(State &state,
|
|||
|
||||
// TODO Accept float bounds
|
||||
Interval VoxelGraphRuntime::analyze_range(State &state, Vector3i min_pos, Vector3i max_pos) const {
|
||||
VOXEL_PROFILE_SCOPE();
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
ERR_FAIL_COND_V_MSG(!has_output(), Interval(), "The graph has no SDF output");
|
||||
ERR_FAIL_COND_V(state.ranges.size() != _program.buffer_count, Interval());
|
||||
#endif
|
||||
|
||||
ArraySlice<Interval> ranges(state.ranges, 0, state.ranges.size());
|
||||
ArraySlice<Buffer> buffers(state.buffers, 0, state.buffers.size());
|
||||
|
||||
// Reset users count, as they might be decreased during the analysis
|
||||
for (auto it = _program.buffer_specs.cbegin(); it != _program.buffer_specs.cend(); ++it) {
|
||||
const BufferSpec &bs = *it;
|
||||
Buffer &b = buffers[bs.address];
|
||||
b.local_users_count = bs.users_count;
|
||||
}
|
||||
|
||||
ranges[_program.x_input_address] = Interval(min_pos.x, max_pos.x);
|
||||
ranges[_program.y_input_address] = Interval(min_pos.y, max_pos.y);
|
||||
|
@ -613,6 +847,8 @@ Interval VoxelGraphRuntime::analyze_range(State &state, Vector3i min_pos, Vector
|
|||
|
||||
const ArraySlice<const uint8_t> operations(_program.operations.data(), 0, _program.operations.size());
|
||||
|
||||
// Here operations must all be analyzed, because we do this as a broad-phase.
|
||||
// Only narrow-phase may skip some operations eventually.
|
||||
uint32_t pc = 0;
|
||||
while (pc < operations.size()) {
|
||||
const uint8_t opid = operations[pc++];
|
||||
|
@ -636,7 +872,7 @@ Interval VoxelGraphRuntime::analyze_range(State &state, Vector3i min_pos, Vector
|
|||
}
|
||||
|
||||
ERR_FAIL_COND_V(node_type.range_analysis_func == nullptr, Interval());
|
||||
RangeAnalysisContext ctx(inputs, outputs, params, ranges);
|
||||
RangeAnalysisContext ctx(inputs, outputs, params, ranges, buffers);
|
||||
node_type.range_analysis_func(ctx);
|
||||
|
||||
#ifdef VOXEL_DEBUG_GRAPH_PROG_SENTINEL
|
||||
|
|
|
@ -19,6 +19,7 @@ public:
|
|||
};
|
||||
|
||||
struct Buffer {
|
||||
// Values of the buffer. Must contain at least `size` values.
|
||||
// TODO Consider wrapping this in debug mode. It is one of the rare cases I didnt do it.
|
||||
// I spent an hour debugging memory corruption which originated from an overrun while accessing this data.
|
||||
float *data = nullptr;
|
||||
|
@ -26,9 +27,15 @@ public:
|
|||
// All buffers have the same available count, size is here only for convenience.
|
||||
unsigned int size;
|
||||
unsigned int capacity;
|
||||
// Constant value of the buffer, if it is a compile-time constant
|
||||
float constant_value;
|
||||
// Is the buffer holding a compile-time constant
|
||||
bool is_constant;
|
||||
// Is the buffer a user input/output
|
||||
bool is_binding = false;
|
||||
// How many operations are using this buffer as input.
|
||||
// This value is only relevant when using optimized execution mapping.
|
||||
unsigned int local_users_count;
|
||||
};
|
||||
|
||||
// Contains the data the program will modify while it runs.
|
||||
|
@ -60,11 +67,23 @@ public:
|
|||
ranges.clear();
|
||||
}
|
||||
|
||||
ArraySlice<const int> get_debug_execution_map() const {
|
||||
return to_slice_const(debug_execution_map);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class VoxelGraphRuntime;
|
||||
|
||||
std::vector<Interval> ranges;
|
||||
std::vector<Buffer> buffers;
|
||||
|
||||
// Stores operation addresses
|
||||
std::vector<uint16_t> execution_map;
|
||||
|
||||
// Stores node IDs referring to the user-facing graph
|
||||
std::vector<int> debug_execution_map;
|
||||
|
||||
unsigned int execution_map_xzy_start_index;
|
||||
unsigned int buffer_size = 0;
|
||||
unsigned int buffer_capacity = 0;
|
||||
};
|
||||
|
@ -80,13 +99,24 @@ public:
|
|||
// If none of these change, you can keep re-using it.
|
||||
void prepare_state(State &state, unsigned int buffer_size) const;
|
||||
|
||||
float generate_single(State &state, Vector3 position) const;
|
||||
float generate_single(State &state, Vector3 position, bool use_execution_map) const;
|
||||
|
||||
void generate_set(State &state, ArraySlice<float> in_x, ArraySlice<float> in_y, ArraySlice<float> in_z,
|
||||
ArraySlice<float> out_sdf, bool skip_xz) const;
|
||||
ArraySlice<float> out_sdf, bool skip_xz, bool use_execution_map) const;
|
||||
|
||||
// Analyzes a specific region of inputs to find out what ranges of outputs we can expect.
|
||||
// It can be used to speed up calls to `generate_set` thanks to execution mapping,
|
||||
// so that operations can be optimized out if they don't contribute to the result.
|
||||
Interval analyze_range(State &state, Vector3i min_pos, Vector3i max_pos) const;
|
||||
|
||||
// Call this after `analyze_range` if you intend to actually generate a set or single values in the area.
|
||||
// This allows to use the execution map optimization, until you choose another area.
|
||||
// (i.e when using this, querying values outside of the analyzed area may be invalid)
|
||||
inline void generate_optimized_execution_map(State &state, bool debug) const {
|
||||
generate_execution_map(state, state.execution_map, state.execution_map_xzy_start_index,
|
||||
debug ? &state.debug_execution_map : nullptr);
|
||||
}
|
||||
|
||||
inline bool has_output() const {
|
||||
return _program.sdf_output_address != -1;
|
||||
}
|
||||
|
@ -220,9 +250,11 @@ public:
|
|||
const ArraySlice<const uint16_t> inputs,
|
||||
const ArraySlice<const uint16_t> outputs,
|
||||
const ArraySlice<const uint8_t> params,
|
||||
ArraySlice<Interval> ranges) :
|
||||
ArraySlice<Interval> ranges,
|
||||
ArraySlice<Buffer> buffers) :
|
||||
_ProcessContext(inputs, outputs, params),
|
||||
_ranges(ranges) {}
|
||||
_ranges(ranges),
|
||||
_buffers(buffers) {}
|
||||
|
||||
inline const Interval get_input(uint32_t i) const {
|
||||
const uint32_t address = get_input_address(i);
|
||||
|
@ -234,8 +266,15 @@ public:
|
|||
_ranges[address] = r;
|
||||
}
|
||||
|
||||
inline void ignore_input(uint32_t i) {
|
||||
const uint32_t address = get_input_address(i);
|
||||
Buffer &b = _buffers[address];
|
||||
--b.local_users_count;
|
||||
}
|
||||
|
||||
private:
|
||||
ArraySlice<Interval> _ranges;
|
||||
ArraySlice<Buffer> _buffers;
|
||||
};
|
||||
|
||||
typedef void (*CompileFunc)(CompileContext &);
|
||||
|
@ -245,38 +284,118 @@ public:
|
|||
private:
|
||||
CompilationResult _compile(const ProgramGraph &graph, bool debug);
|
||||
|
||||
struct Constant {
|
||||
unsigned int address;
|
||||
float value = 0;
|
||||
void VoxelGraphRuntime::generate_execution_map(const State &state,
|
||||
std::vector<uint16_t> &execution_map, unsigned int &out_mapped_xzy_start,
|
||||
std::vector<int> *debug_execution_map) const;
|
||||
|
||||
bool is_operation_constant(const State &state, uint16_t op_address) const;
|
||||
|
||||
struct BufferSpec {
|
||||
// Index the buffer should be stored at
|
||||
uint16_t address;
|
||||
// How many nodes use this buffer as input
|
||||
uint16_t users_count;
|
||||
// Value of the compile-time constant, if any
|
||||
float constant_value;
|
||||
// Is the buffer constant at compile time
|
||||
bool is_constant;
|
||||
// Is the buffer a user input/output
|
||||
bool is_binding;
|
||||
};
|
||||
|
||||
struct DependencyGraph {
|
||||
struct Node {
|
||||
uint16_t first_dependency;
|
||||
uint16_t end_dependency;
|
||||
uint16_t op_address;
|
||||
bool is_output;
|
||||
int debug_node_id;
|
||||
};
|
||||
|
||||
// Indexes to the `nodes` array
|
||||
std::vector<uint16_t> dependencies;
|
||||
// Nodes in the same order they would be in the default execution map
|
||||
std::vector<Node> nodes;
|
||||
|
||||
inline void clear() {
|
||||
dependencies.clear();
|
||||
nodes.clear();
|
||||
}
|
||||
};
|
||||
|
||||
// Precalculated program data.
|
||||
// Remains constant and read-only after compilation.
|
||||
struct Program {
|
||||
// Serialized operations and arguments.
|
||||
// They come up as series of <opid><inputs><outputs><parameters_size><parameters>.
|
||||
// They should be laid out in the same order they will be run in, although it's not absolutely required.
|
||||
// It's better to have it ordered because memory access will be more predictable.
|
||||
std::vector<uint8_t> operations;
|
||||
|
||||
// Describes dependencies between operations. It is generated at compile time.
|
||||
// It is used to perform dynamic optimization in case some operations can be predicted as constant.
|
||||
DependencyGraph dependency_graph;
|
||||
|
||||
// List of indexes within `operations` describing which order they should be run into by default.
|
||||
// It's used because sometimes we may want to override with a simplified execution map dynamically.
|
||||
// When we don't, we use the default one so the code doesn't have to change.
|
||||
std::vector<uint16_t> default_execution_map;
|
||||
|
||||
// Heap-allocated parameters data, when too large to fit in `operations`.
|
||||
// We keep a reference to them so they can be freed when the program is cleared.
|
||||
std::vector<HeapResource> heap_resources;
|
||||
|
||||
// Heap-allocated parameters data, when too large to fit in `operations`.
|
||||
// We keep a reference to them so they won't be freed until the program is cleared.
|
||||
std::vector<Ref<Reference> > ref_resources;
|
||||
std::vector<Constant> constants;
|
||||
std::vector<uint16_t> bindings;
|
||||
uint32_t xzy_start;
|
||||
|
||||
// Describes the list of buffers to prepare in `State` before the program can be run
|
||||
std::vector<BufferSpec> buffer_specs;
|
||||
|
||||
// Address in `operations` from which operations will depend on Y. Operations before never depend on it.
|
||||
// It is used to optimize away calculations that would otherwise be the same in planar terrain use cases.
|
||||
uint32_t xzy_start_op_address;
|
||||
uint32_t xzy_start_execution_map_index;
|
||||
|
||||
// Note: the following buffers are allocated by the user.
|
||||
// They are mapped temporarily into the same array of buffers inside `State`,
|
||||
// so we won't need specific code to handle them. This requires knowing at which index they are reserved.
|
||||
// They must be all assigned for the program to run correctly.
|
||||
//
|
||||
// Address within the State's array of buffers where the X input may be.
|
||||
int x_input_address = -1;
|
||||
// Address within the State's array of buffers where the Y input may be.
|
||||
int y_input_address = -1;
|
||||
// Address within the State's array of buffers where the Z input may be.
|
||||
int z_input_address = -1;
|
||||
// Address within the State's array of buffers where the SDF output may be.
|
||||
int sdf_output_address = -1;
|
||||
int sdf_output_node_index = -1;
|
||||
|
||||
// Maximum amount of buffers this program will need to do a full run.
|
||||
// Buffers are needed to hold values of arguments and outputs for each operation.
|
||||
unsigned int buffer_count = 0;
|
||||
|
||||
// Associates a high-level port to its corresponding address within the compiled program.
|
||||
// This is used for debugging intermediate values.
|
||||
HashMap<ProgramGraph::PortLocation, uint16_t, ProgramGraph::PortLocationHasher> output_port_addresses;
|
||||
|
||||
// Result of the last compilation attempt. The program should not be run if it failed.
|
||||
CompilationResult compilation_result;
|
||||
|
||||
void clear() {
|
||||
operations.clear();
|
||||
constants.clear();
|
||||
bindings.clear();
|
||||
// Address in the program from which operations will depend on Y.
|
||||
xzy_start = 0;
|
||||
buffer_specs.clear();
|
||||
xzy_start_execution_map_index = 0;
|
||||
xzy_start_op_address = 0;
|
||||
default_execution_map.clear();
|
||||
output_port_addresses.clear();
|
||||
dependency_graph.clear();
|
||||
sdf_output_address = -1;
|
||||
x_input_address = -1;
|
||||
y_input_address = -1;
|
||||
z_input_address = -1;
|
||||
sdf_output_node_index = -1;
|
||||
compilation_result = CompilationResult();
|
||||
for (auto it = heap_resources.begin(); it != heap_resources.end(); ++it) {
|
||||
HeapResource &r = *it;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
#ifndef VOXEL_NODE_H
|
||||
#define VOXEL_NODE_H
|
||||
|
||||
#include <scene/3d/spatial.h>
|
||||
#include "../generators/voxel_generator.h"
|
||||
#include "../meshers/voxel_mesher.h"
|
||||
#include "../streams/voxel_stream.h"
|
||||
|
||||
class VoxelMesher;
|
||||
class VoxelStream;
|
||||
class VoxelGenerator;
|
||||
#include <scene/3d/spatial.h>
|
||||
|
||||
// Base class for voxel volumes
|
||||
class VoxelNode : public Spatial {
|
||||
|
|
Loading…
Reference in New Issue