Implemented execution map to optimize out nodes locally

- Added optional optimization of execution map
- Added visualization of skipped nodes in the editor debug tool
master
Marc Gilleron 2021-03-21 18:48:00 +00:00
parent 30e33849fe
commit 03bfc09e99
13 changed files with 607 additions and 94 deletions

View File

@ -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);

View File

@ -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

View File

@ -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) {

View File

@ -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);

View File

@ -13,6 +13,7 @@ namespace VoxelDebug {
enum ColorID {
ID_VOXEL_BOUNDS = 0,
ID_OCTREE_BOUNDS,
ID_VOXEL_GRAPH_DEBUG_BOUNDS,
ID_COUNT
};

View File

@ -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();
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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);

View File

@ -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));
};
}

View File

@ -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

View File

@ -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;

View File

@ -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 {