diff options
-rw-r--r-- | doc/classes/ShaderInclude.xml | 13 | ||||
-rw-r--r-- | editor/code_editor.cpp | 4 | ||||
-rw-r--r-- | editor/code_editor.h | 3 | ||||
-rw-r--r-- | editor/editor_asset_installer.cpp | 1 | ||||
-rw-r--r-- | editor/filesystem_dock.cpp | 4 | ||||
-rw-r--r-- | editor/plugins/shader_editor_plugin.cpp | 338 | ||||
-rw-r--r-- | editor/plugins/shader_editor_plugin.h | 24 | ||||
-rw-r--r-- | editor/shader_create_dialog.cpp | 227 | ||||
-rw-r--r-- | editor/shader_create_dialog.h | 11 | ||||
-rw-r--r-- | scene/register_scene_types.cpp | 17 | ||||
-rw-r--r-- | scene/resources/shader.cpp | 22 | ||||
-rw-r--r-- | scene/resources/shader.h | 3 | ||||
-rw-r--r-- | scene/resources/shader_include.cpp | 139 | ||||
-rw-r--r-- | scene/resources/shader_include.h | 71 | ||||
-rw-r--r-- | servers/rendering/shader_language.cpp | 217 | ||||
-rw-r--r-- | servers/rendering/shader_language.h | 8 | ||||
-rw-r--r-- | servers/rendering/shader_preprocessor.cpp | 1027 | ||||
-rw-r--r-- | servers/rendering/shader_preprocessor.h | 196 |
18 files changed, 2127 insertions, 198 deletions
diff --git a/doc/classes/ShaderInclude.xml b/doc/classes/ShaderInclude.xml new file mode 100644 index 0000000000..40072a933b --- /dev/null +++ b/doc/classes/ShaderInclude.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="ShaderInclude" inherits="Resource" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> + <brief_description> + </brief_description> + <description> + </description> + <tutorials> + </tutorials> + <members> + <member name="code" type="String" setter="set_code" getter="get_code" default=""""> + </member> + </members> +</class> diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index 3a0edf301d..fd331503ca 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -1594,6 +1594,10 @@ void CodeTextEditor::set_error_pos(int p_line, int p_column) { error_column = p_column; } +Point2i CodeTextEditor::get_error_pos() const { + return Point2i(error_line, error_column); +} + void CodeTextEditor::goto_error() { if (!error->get_text().is_empty()) { if (text_editor->get_line_count() != error_line) { diff --git a/editor/code_editor.h b/editor/code_editor.h index e2441cec2b..49679cc700 100644 --- a/editor/code_editor.h +++ b/editor/code_editor.h @@ -253,13 +253,14 @@ public: void update_editor_settings(); void set_error(const String &p_error); void set_error_pos(int p_line, int p_column); + Point2i get_error_pos() const; void update_line_and_column() { _line_col_changed(); } CodeEdit *get_text_editor() { return text_editor; } FindReplaceBar *get_find_replace_bar() { return find_replace_bar; } void set_find_replace_bar(FindReplaceBar *p_bar); void remove_find_replace_bar(); virtual void apply_code() {} - void goto_error(); + virtual void goto_error(); void toggle_bookmark(); void goto_next_bookmark(); diff --git a/editor/editor_asset_installer.cpp b/editor/editor_asset_installer.cpp index 3d4bee4b4e..8fa486408e 100644 --- a/editor/editor_asset_installer.cpp +++ b/editor/editor_asset_installer.cpp @@ -112,6 +112,7 @@ void EditorAssetInstaller::open(const String &p_path, int p_depth) { extension_guess["glb"] = tree->get_theme_icon(SNAME("PackedScene"), SNAME("EditorIcons")); extension_guess["gdshader"] = tree->get_theme_icon(SNAME("Shader"), SNAME("EditorIcons")); + extension_guess["gdshaderinc"] = tree->get_theme_icon(SNAME("TextFile"), SNAME("EditorIcons")); extension_guess["gd"] = tree->get_theme_icon(SNAME("GDScript"), SNAME("EditorIcons")); if (Engine::get_singleton()->has_singleton("GodotSharp")) { extension_guess["cs"] = tree->get_theme_icon(SNAME("CSharpScript"), SNAME("EditorIcons")); diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp index fe6e6044a4..6240ac86ad 100644 --- a/editor/filesystem_dock.cpp +++ b/editor/filesystem_dock.cpp @@ -2034,6 +2034,10 @@ void FileSystemDock::_resource_created() { make_shader_dialog->config(fpath.plus_file("new_shader"), false, false, 1); make_shader_dialog->popup_centered(); return; + } else if (type_name == "ShaderInclude") { + make_shader_dialog->config(fpath.plus_file("new_shader_include"), false, false, 2); + make_shader_dialog->popup_centered(); + return; } Variant c = new_resource_dialog->instance_selected(); diff --git a/editor/plugins/shader_editor_plugin.cpp b/editor/plugins/shader_editor_plugin.cpp index 70b8c3aaa7..9a20b0087e 100644 --- a/editor/plugins/shader_editor_plugin.cpp +++ b/editor/plugins/shader_editor_plugin.cpp @@ -72,15 +72,42 @@ Ref<Shader> ShaderTextEditor::get_edited_shader() const { return shader; } +Ref<ShaderInclude> ShaderTextEditor::get_edited_shader_include() const { + return shader_inc; +} + void ShaderTextEditor::set_edited_shader(const Ref<Shader> &p_shader) { + set_edited_shader(p_shader, p_shader->get_code()); +} + +void ShaderTextEditor::set_edited_shader(const Ref<Shader> &p_shader, const String &p_code) { if (shader == p_shader) { return; } shader = p_shader; + shader_inc = Ref<ShaderInclude>(); + + set_edited_code(p_code); +} + +void ShaderTextEditor::set_edited_shader_include(const Ref<ShaderInclude> &p_shader_inc) { + set_edited_shader_include(p_shader_inc, p_shader_inc->get_code()); +} +void ShaderTextEditor::set_edited_shader_include(const Ref<ShaderInclude> &p_shader_inc, const String &p_code) { + if (shader_inc == p_shader_inc) { + return; + } + shader_inc = p_shader_inc; + shader = Ref<Shader>(); + + set_edited_code(p_code); +} + +void ShaderTextEditor::set_edited_code(const String &p_code) { _load_theme_settings(); - get_text_editor()->set_text(p_shader->get_code()); + get_text_editor()->set_text(p_code); get_text_editor()->clear_undo_history(); get_text_editor()->call_deferred(SNAME("set_h_scroll"), 0); get_text_editor()->call_deferred(SNAME("set_v_scroll"), 0); @@ -132,11 +159,12 @@ void ShaderTextEditor::_load_theme_settings() { syntax_highlighter->clear_keyword_colors(); - List<String> keywords; - ShaderLanguage::get_keyword_list(&keywords); const Color keyword_color = EDITOR_GET("text_editor/theme/highlighting/keyword_color"); const Color control_flow_keyword_color = EDITOR_GET("text_editor/theme/highlighting/control_flow_keyword_color"); + List<String> keywords; + ShaderLanguage::get_keyword_list(&keywords); + for (const String &E : keywords) { if (ShaderLanguage::is_control_flow_keyword(E)) { syntax_highlighter->add_keyword_color(E, control_flow_keyword_color); @@ -145,6 +173,13 @@ void ShaderTextEditor::_load_theme_settings() { } } + List<String> pp_keywords; + ShaderLanguage::get_preprocessor_keyword_list(&pp_keywords, false); + + for (const String &E : pp_keywords) { + syntax_highlighter->add_keyword_color(E, keyword_color); + } + // Colorize built-ins like `COLOR` differently to make them easier // to distinguish from keywords at a quick glance. @@ -191,8 +226,12 @@ void ShaderTextEditor::_load_theme_settings() { text_editor->add_auto_brace_completion_pair("/*", "*/"); } + // Colorize preprocessor include strings. + const Color string_color = EDITOR_GET("text_editor/theme/highlighting/string_color"); + syntax_highlighter->add_color_region("\"", "\"", string_color, false); + if (warnings_panel) { - // Warnings panel + // Warnings panel. warnings_panel->add_theme_font_override("normal_font", EditorNode::get_singleton()->get_gui_base()->get_theme_font(SNAME("main"), SNAME("EditorFonts"))); warnings_panel->add_theme_font_size_override("normal_font_size", EditorNode::get_singleton()->get_gui_base()->get_theme_font_size(SNAME("main_size"), SNAME("EditorFonts"))); } @@ -227,39 +266,72 @@ static ShaderLanguage::DataType _get_global_variable_type(const StringName &p_va } void ShaderTextEditor::_code_complete_script(const String &p_code, List<ScriptLanguage::CodeCompletionOption> *r_options) { - _check_shader_mode(); - ShaderLanguage sl; String calltip; - ShaderLanguage::ShaderCompileInfo info; + info.global_variable_type_func = _get_global_variable_type; + + Ref<ShaderInclude> inc = shader_inc; + if (shader.is_null()) { + info.is_include = true; + + sl.complete(p_code, info, r_options, calltip); + get_text_editor()->set_code_hint(calltip); + return; + } + _check_shader_mode(); info.functions = ShaderTypes::get_singleton()->get_functions(RenderingServer::ShaderMode(shader->get_mode())); info.render_modes = ShaderTypes::get_singleton()->get_modes(RenderingServer::ShaderMode(shader->get_mode())); info.shader_types = ShaderTypes::get_singleton()->get_types(); - info.global_variable_type_func = _get_global_variable_type; sl.complete(p_code, info, r_options, calltip); - get_text_editor()->set_code_hint(calltip); } void ShaderTextEditor::_validate_script() { - _check_shader_mode(); - - String code = get_text_editor()->get_text(); - //List<StringName> params; - //shader->get_param_list(¶ms); + String code; - ShaderLanguage::ShaderCompileInfo info; - info.functions = ShaderTypes::get_singleton()->get_functions(RenderingServer::ShaderMode(shader->get_mode())); - info.render_modes = ShaderTypes::get_singleton()->get_modes(RenderingServer::ShaderMode(shader->get_mode())); - info.shader_types = ShaderTypes::get_singleton()->get_types(); - info.global_variable_type_func = _get_global_variable_type; + if (shader.is_valid()) { + _check_shader_mode(); + code = shader->get_code(); + } else { + code = shader_inc->get_code(); + } ShaderLanguage sl; sl.enable_warning_checking(saved_warnings_enabled); - sl.set_warning_flags(saved_warning_flags); + uint32_t flags = saved_warning_flags; + if (shader.is_null()) { + if (flags & ShaderWarning::UNUSED_CONSTANT) { + flags &= ~(ShaderWarning::UNUSED_CONSTANT); + } + if (flags & ShaderWarning::UNUSED_FUNCTION) { + flags &= ~(ShaderWarning::UNUSED_FUNCTION); + } + if (flags & ShaderWarning::UNUSED_STRUCT) { + flags &= ~(ShaderWarning::UNUSED_STRUCT); + } + if (flags & ShaderWarning::UNUSED_UNIFORM) { + flags &= ~(ShaderWarning::UNUSED_UNIFORM); + } + if (flags & ShaderWarning::UNUSED_VARYING) { + flags &= ~(ShaderWarning::UNUSED_VARYING); + } + } + sl.set_warning_flags(flags); + + ShaderLanguage::ShaderCompileInfo info; + info.global_variable_type_func = _get_global_variable_type; + + if (shader.is_null()) { + info.is_include = true; + } else { + Shader::Mode mode = shader->get_mode(); + info.functions = ShaderTypes::get_singleton()->get_functions(RenderingServer::ShaderMode(mode)); + info.render_modes = ShaderTypes::get_singleton()->get_modes(RenderingServer::ShaderMode(mode)); + info.shader_types = ShaderTypes::get_singleton()->get_types(); + } last_compile_result = sl.compile(code, info); @@ -524,15 +596,23 @@ void ShaderEditor::_update_warnings(bool p_validate) { } void ShaderEditor::_check_for_external_edit() { - if (shader.is_null() || !shader.is_valid()) { + bool use_autoreload = bool(EDITOR_GET("text_editor/behavior/files/auto_reload_scripts_on_external_change")); + + if (shader_inc.is_valid()) { + if (shader_inc->get_last_modified_time() != FileAccess::get_modified_time(shader_inc->get_path())) { + if (use_autoreload) { + _reload_shader_include_from_disk(); + } else { + disk_changed->call_deferred(SNAME("popup_centered")); + } + } return; } - if (shader->is_built_in()) { + if (shader.is_null() || shader->is_built_in()) { return; } - bool use_autoreload = bool(EDITOR_GET("text_editor/behavior/files/auto_reload_scripts_on_external_change")); if (shader->get_last_modified_time() != FileAccess::get_modified_time(shader->get_path())) { if (use_autoreload) { _reload_shader_from_disk(); @@ -551,6 +631,23 @@ void ShaderEditor::_reload_shader_from_disk() { shader_editor->reload_text(); } +void ShaderEditor::_reload_shader_include_from_disk() { + Ref<ShaderInclude> rel_shader_include = ResourceLoader::load(shader_inc->get_path(), shader_inc->get_class(), ResourceFormatLoader::CACHE_MODE_IGNORE); + ERR_FAIL_COND(!rel_shader_include.is_valid()); + + shader_inc->set_code(rel_shader_include->get_code()); + shader_inc->set_last_modified_time(rel_shader_include->get_last_modified_time()); + shader_editor->reload_text(); +} + +void ShaderEditor::_reload() { + if (shader.is_valid()) { + _reload_shader_from_disk(); + } else if (shader_inc.is_valid()) { + _reload_shader_include_from_disk(); + } +} + void ShaderEditor::edit(const Ref<Shader> &p_shader) { if (p_shader.is_null() || !p_shader->is_text_shader()) { return; @@ -561,37 +658,73 @@ void ShaderEditor::edit(const Ref<Shader> &p_shader) { } shader = p_shader; + shader_inc = Ref<ShaderInclude>(); + + shader_editor->set_edited_shader(shader); +} + +void ShaderEditor::edit(const Ref<ShaderInclude> &p_shader_inc) { + if (p_shader_inc.is_null()) { + return; + } - shader_editor->set_edited_shader(p_shader); + if (shader_inc == p_shader_inc) { + return; + } - //vertex_editor->set_edited_shader(shader,ShaderLanguage::SHADER_MATERIAL_VERTEX); - // see if already has it + shader_inc = p_shader_inc; + shader = Ref<Shader>(); + + shader_editor->set_edited_shader_include(p_shader_inc); } void ShaderEditor::save_external_data(const String &p_str) { - if (shader.is_null()) { + if (shader.is_null() && shader_inc.is_null()) { disk_changed->hide(); return; } apply_shaders(); - if (!shader->is_built_in()) { - //external shader, save it + + Ref<Shader> edited_shader = shader_editor->get_edited_shader(); + if (edited_shader.is_valid()) { + ResourceSaver::save(edited_shader->get_path(), edited_shader); + } + if (shader.is_valid() && shader != edited_shader) { ResourceSaver::save(shader->get_path(), shader); } + Ref<ShaderInclude> edited_shader_inc = shader_editor->get_edited_shader_include(); + if (edited_shader_inc.is_valid()) { + ResourceSaver::save(edited_shader_inc->get_path(), edited_shader_inc); + } + if (shader_inc.is_valid() && shader_inc != edited_shader_inc) { + ResourceSaver::save(shader_inc->get_path(), shader_inc); + } + disk_changed->hide(); } +void ShaderEditor::validate_script() { + shader_editor->_validate_script(); +} + void ShaderEditor::apply_shaders() { + String editor_code = shader_editor->get_text_editor()->get_text(); if (shader.is_valid()) { String shader_code = shader->get_code(); - String editor_code = shader_editor->get_text_editor()->get_text(); if (shader_code != editor_code) { shader->set_code(editor_code); shader->set_edited(true); } } + if (shader_inc.is_valid()) { + String shader_inc_code = shader_inc->get_code(); + if (shader_inc_code != editor_code) { + shader_inc->set_code(editor_code); + shader_inc->set_edited(true); + } + } } void ShaderEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) { @@ -704,6 +837,7 @@ ShaderEditor::ShaderEditor() { _update_warnings(false); shader_editor = memnew(ShaderTextEditor); + shader_editor->set_v_size_flags(SIZE_EXPAND_FILL); shader_editor->add_theme_constant_override("separation", 0); shader_editor->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); @@ -829,7 +963,7 @@ ShaderEditor::ShaderEditor() { dl->set_text(TTR("This shader has been modified on disk.\nWhat action should be taken?")); vbc->add_child(dl); - disk_changed->connect("confirmed", callable_mp(this, &ShaderEditor::_reload_shader_from_disk)); + disk_changed->connect("confirmed", callable_mp(this, &ShaderEditor::_reload)); disk_changed->set_ok_button_text(TTR("Reload")); disk_changed->add_button(TTR("Resave"), !DisplayServer::get_singleton()->get_swap_cancel_ok(), "resave"); @@ -844,19 +978,37 @@ void ShaderEditorPlugin::_update_shader_list() { shader_list->clear(); for (uint32_t i = 0; i < edited_shaders.size(); i++) { String text; - String path = edited_shaders[i].shader->get_path(); - String _class = edited_shaders[i].shader->get_class(); + String path; + String _class; + String shader_name; + if (edited_shaders[i].shader.is_valid()) { + Ref<Shader> shader = edited_shaders[i].shader; + + path = shader->get_path(); + _class = shader->get_class(); + shader_name = shader->get_name(); + } else { + Ref<ShaderInclude> shader_inc = edited_shaders[i].shader_inc; + + path = shader_inc->get_path(); + _class = shader_inc->get_class(); + shader_name = shader_inc->get_name(); + } if (path.is_resource_file()) { text = path.get_file(); - } else if (edited_shaders[i].shader->get_name() != "") { - text = edited_shaders[i].shader->get_name(); + } else if (shader_name != "") { + text = shader_name; } else { - text = _class + ":" + itos(edited_shaders[i].shader->get_instance_id()); + if (edited_shaders[i].shader.is_valid()) { + text = _class + ":" + itos(edited_shaders[i].shader->get_instance_id()); + } else { + text = _class + ":" + itos(edited_shaders[i].shader_inc->get_instance_id()); + } } if (!shader_list->has_theme_icon(_class, SNAME("EditorIcons"))) { - _class = "Resource"; + _class = "TextFile"; } Ref<Texture2D> icon = shader_list->get_theme_icon(_class, SNAME("EditorIcons")); @@ -874,35 +1026,50 @@ void ShaderEditorPlugin::_update_shader_list() { } void ShaderEditorPlugin::edit(Object *p_object) { - Shader *s = Object::cast_to<Shader>(p_object); - for (uint32_t i = 0; i < edited_shaders.size(); i++) { - if (edited_shaders[i].shader.ptr() == s) { - // Exists, select. - shader_tabs->set_current_tab(i); - shader_list->select(i); - return; - } - } - // Add. EditedShader es; - es.shader = Ref<Shader>(s); - Ref<VisualShader> vs = es.shader; - if (vs.is_valid()) { - es.visual_shader_editor = memnew(VisualShaderEditor); - shader_tabs->add_child(es.visual_shader_editor); - es.visual_shader_editor->edit(vs.ptr()); - } else { + + ShaderInclude *si = Object::cast_to<ShaderInclude>(p_object); + if (si != nullptr) { + for (uint32_t i = 0; i < edited_shaders.size(); i++) { + if (edited_shaders[i].shader_inc.ptr() == si) { + shader_tabs->set_current_tab(i); + shader_list->select(i); + return; + } + } + es.shader_inc = Ref<ShaderInclude>(si); es.shader_editor = memnew(ShaderEditor); + es.shader_editor->edit(si); shader_tabs->add_child(es.shader_editor); - es.shader_editor->edit(s); + } else { + Shader *s = Object::cast_to<Shader>(p_object); + for (uint32_t i = 0; i < edited_shaders.size(); i++) { + if (edited_shaders[i].shader.ptr() == s) { + shader_tabs->set_current_tab(i); + shader_list->select(i); + return; + } + } + es.shader = Ref<Shader>(s); + Ref<VisualShader> vs = es.shader; + if (vs.is_valid()) { + es.visual_shader_editor = memnew(VisualShaderEditor); + es.visual_shader_editor->edit(vs.ptr()); + shader_tabs->add_child(es.visual_shader_editor); + } else { + es.shader_editor = memnew(ShaderEditor); + es.shader_editor->edit(s); + shader_tabs->add_child(es.shader_editor); + } } + shader_tabs->set_current_tab(shader_tabs->get_tab_count() - 1); edited_shaders.push_back(es); _update_shader_list(); } bool ShaderEditorPlugin::handles(Object *p_object) const { - return Object::cast_to<Shader>(p_object) != nullptr; + return Object::cast_to<Shader>(p_object) != nullptr || Object::cast_to<ShaderInclude>(p_object) != nullptr; } void ShaderEditorPlugin::make_visible(bool p_visible) { @@ -949,6 +1116,9 @@ void ShaderEditorPlugin::apply_changes() { } void ShaderEditorPlugin::_shader_selected(int p_index) { + if (edited_shaders[p_index].shader_editor) { + edited_shaders[p_index].shader_editor->validate_script(); + } shader_tabs->set_current_tab(p_index); } @@ -975,31 +1145,56 @@ void ShaderEditorPlugin::_resource_saved(Object *obj) { void ShaderEditorPlugin::_menu_item_pressed(int p_index) { switch (p_index) { case FILE_NEW: { - String base_path = FileSystemDock::get_singleton()->get_current_path(); + String base_path = FileSystemDock::get_singleton()->get_current_path().get_base_dir(); shader_create_dialog->config(base_path.plus_file("new_shader"), false, false, 0); shader_create_dialog->popup_centered(); } break; + case FILE_NEW_INCLUDE: { + String base_path = FileSystemDock::get_singleton()->get_current_path().get_base_dir(); + shader_create_dialog->config(base_path.plus_file("new_shader"), false, false, 2); + shader_create_dialog->popup_centered(); + } break; case FILE_OPEN: { InspectorDock::get_singleton()->open_resource("Shader"); } break; + case FILE_OPEN_INCLUDE: { + InspectorDock::get_singleton()->open_resource("ShaderInclude"); + } break; case FILE_SAVE: { int index = shader_tabs->get_current_tab(); ERR_FAIL_INDEX(index, shader_tabs->get_tab_count()); - EditorNode::get_singleton()->save_resource(edited_shaders[index].shader); + if (edited_shaders[index].shader.is_valid()) { + EditorNode::get_singleton()->save_resource(edited_shaders[index].shader); + } else { + EditorNode::get_singleton()->save_resource(edited_shaders[index].shader_inc); + } } break; case FILE_SAVE_AS: { int index = shader_tabs->get_current_tab(); ERR_FAIL_INDEX(index, shader_tabs->get_tab_count()); - String path = edited_shaders[index].shader->get_path(); - if (!path.is_resource_file()) { - path = ""; + String path; + if (edited_shaders[index].shader.is_valid()) { + path = edited_shaders[index].shader->get_path(); + if (!path.is_resource_file()) { + path = ""; + } + EditorNode::get_singleton()->save_resource_as(edited_shaders[index].shader, path); + } else { + path = edited_shaders[index].shader_inc->get_path(); + if (!path.is_resource_file()) { + path = ""; + } + EditorNode::get_singleton()->save_resource_as(edited_shaders[index].shader_inc, path); } - EditorNode::get_singleton()->save_resource_as(edited_shaders[index].shader, path); } break; case FILE_INSPECT: { int index = shader_tabs->get_current_tab(); ERR_FAIL_INDEX(index, shader_tabs->get_tab_count()); - EditorNode::get_singleton()->push_item(edited_shaders[index].shader.ptr()); + if (edited_shaders[index].shader.is_valid()) { + EditorNode::get_singleton()->push_item(edited_shaders[index].shader.ptr()); + } else { + EditorNode::get_singleton()->push_item(edited_shaders[index].shader_inc.ptr()); + } } break; case FILE_CLOSE: { _close_shader(shader_tabs->get_current_tab()); @@ -1011,6 +1206,10 @@ void ShaderEditorPlugin::_shader_created(Ref<Shader> p_shader) { EditorNode::get_singleton()->push_item(p_shader.ptr()); } +void ShaderEditorPlugin::_shader_include_created(Ref<ShaderInclude> p_shader_inc) { + EditorNode::get_singleton()->push_item(p_shader_inc.ptr()); +} + ShaderEditorPlugin::ShaderEditorPlugin() { main_split = memnew(HSplitContainer); @@ -1021,14 +1220,16 @@ ShaderEditorPlugin::ShaderEditorPlugin() { file_menu = memnew(MenuButton); file_menu->set_text(TTR("File")); file_menu->get_popup()->add_item(TTR("New Shader"), FILE_NEW); + file_menu->get_popup()->add_item(TTR("New Shader Include"), FILE_NEW_INCLUDE); file_menu->get_popup()->add_separator(); - file_menu->get_popup()->add_item(TTR("Load Shader"), FILE_OPEN); - file_menu->get_popup()->add_item(TTR("Save Shader"), FILE_SAVE); - file_menu->get_popup()->add_item(TTR("Save Shader As"), FILE_SAVE_AS); + file_menu->get_popup()->add_item(TTR("Load Shader File"), FILE_OPEN); + file_menu->get_popup()->add_item(TTR("Load Shader Include File"), FILE_OPEN_INCLUDE); + file_menu->get_popup()->add_item(TTR("Save File"), FILE_SAVE); + file_menu->get_popup()->add_item(TTR("Save File As"), FILE_SAVE_AS); file_menu->get_popup()->add_separator(); - file_menu->get_popup()->add_item(TTR("Open Shader in Inspector"), FILE_INSPECT); + file_menu->get_popup()->add_item(TTR("Open File in Inspector"), FILE_INSPECT); file_menu->get_popup()->add_separator(); - file_menu->get_popup()->add_item(TTR("Close Shader"), FILE_CLOSE); + file_menu->get_popup()->add_item(TTR("Close File"), FILE_CLOSE); file_menu->get_popup()->connect("id_pressed", callable_mp(this, &ShaderEditorPlugin::_menu_item_pressed)); file_hb->add_child(file_menu); @@ -1060,6 +1261,7 @@ ShaderEditorPlugin::ShaderEditorPlugin() { shader_create_dialog = memnew(ShaderCreateDialog); vb->add_child(shader_create_dialog); shader_create_dialog->connect("shader_created", callable_mp(this, &ShaderEditorPlugin::_shader_created)); + shader_create_dialog->connect("shader_include_created", callable_mp(this, &ShaderEditorPlugin::_shader_include_created)); } ShaderEditorPlugin::~ShaderEditorPlugin() { diff --git a/editor/plugins/shader_editor_plugin.h b/editor/plugins/shader_editor_plugin.h index 2e0dbe0d60..c9cd75e451 100644 --- a/editor/plugins/shader_editor_plugin.h +++ b/editor/plugins/shader_editor_plugin.h @@ -40,6 +40,7 @@ #include "scene/gui/text_edit.h" #include "scene/main/timer.h" #include "scene/resources/shader.h" +#include "scene/resources/shader_include.h" #include "servers/rendering/shader_warnings.h" class ItemList; @@ -59,6 +60,7 @@ class ShaderTextEditor : public CodeTextEditor { Ref<CodeHighlighter> syntax_highlighter; RichTextLabel *warnings_panel = nullptr; Ref<Shader> shader; + Ref<ShaderInclude> shader_inc; List<ShaderWarning> warnings; Error last_compile_result = Error::OK; @@ -79,7 +81,14 @@ public: void set_warnings_panel(RichTextLabel *p_warnings_panel); Ref<Shader> get_edited_shader() const; + Ref<ShaderInclude> get_edited_shader_include() const; + void set_edited_shader(const Ref<Shader> &p_shader); + void set_edited_shader(const Ref<Shader> &p_shader, const String &p_code); + void set_edited_shader_include(const Ref<ShaderInclude> &p_include); + void set_edited_shader_include(const Ref<ShaderInclude> &p_include, const String &p_code); + void set_edited_code(const String &p_code); + ShaderTextEditor(); }; @@ -129,12 +138,15 @@ class ShaderEditor : public PanelContainer { void _menu_option(int p_option); mutable Ref<Shader> shader; + mutable Ref<ShaderInclude> shader_inc; void _editor_settings_changed(); void _project_settings_changed(); void _check_for_external_edit(); void _reload_shader_from_disk(); + void _reload_shader_include_from_disk(); + void _reload(); void _show_warnings_panel(bool p_show); void _warning_clicked(Variant p_line); void _update_warnings(bool p_validate); @@ -143,21 +155,21 @@ protected: void _notification(int p_what); static void _bind_methods(); void _make_context_menu(bool p_selection, Vector2 p_position); - void _text_edit_gui_input(const Ref<InputEvent> &ev); + void _text_edit_gui_input(const Ref<InputEvent> &p_ev); void _update_bookmark_list(); void _bookmark_item_pressed(int p_idx); public: void apply_shaders(); - void ensure_select_current(); void edit(const Ref<Shader> &p_shader); - + void edit(const Ref<ShaderInclude> &p_shader_inc); void goto_line_selection(int p_line, int p_begin, int p_end); + void save_external_data(const String &p_str = ""); + void validate_script(); virtual Size2 get_minimum_size() const override { return Size2(0, 200); } - void save_external_data(const String &p_str = ""); ShaderEditor(); }; @@ -167,6 +179,7 @@ class ShaderEditorPlugin : public EditorPlugin { struct EditedShader { Ref<Shader> shader; + Ref<ShaderInclude> shader_inc; ShaderEditor *shader_editor = nullptr; VisualShaderEditor *visual_shader_editor = nullptr; }; @@ -175,7 +188,9 @@ class ShaderEditorPlugin : public EditorPlugin { enum { FILE_NEW, + FILE_NEW_INCLUDE, FILE_OPEN, + FILE_OPEN_INCLUDE, FILE_SAVE, FILE_SAVE_AS, FILE_INSPECT, @@ -199,6 +214,7 @@ class ShaderEditorPlugin : public EditorPlugin { void _close_shader(int p_index); void _shader_created(Ref<Shader> p_shader); + void _shader_include_created(Ref<ShaderInclude> p_shader_inc); public: virtual String get_name() const override { return "Shader"; } diff --git a/editor/shader_create_dialog.cpp b/editor/shader_create_dialog.cpp index 28e1e9bf22..7ae03ee96f 100644 --- a/editor/shader_create_dialog.cpp +++ b/editor/shader_create_dialog.cpp @@ -33,6 +33,7 @@ #include "core/config/project_settings.h" #include "editor/editor_file_dialog.h" #include "editor/editor_scale.h" +#include "scene/resources/shader_include.h" #include "scene/resources/visual_shader.h" #include "servers/rendering/shader_types.h" @@ -43,15 +44,15 @@ void ShaderCreateDialog::_notification(int p_what) { String last_lang = EditorSettings::get_singleton()->get_project_metadata("shader_setup", "last_selected_language", ""); if (!last_lang.is_empty()) { - for (int i = 0; i < language_menu->get_item_count(); i++) { - if (language_menu->get_item_text(i) == last_lang) { - language_menu->select(i); - current_language = i; + for (int i = 0; i < type_menu->get_item_count(); i++) { + if (type_menu->get_item_text(i) == last_lang) { + type_menu->select(i); + current_type = i; break; } } } else { - language_menu->select(default_language); + type_menu->select(default_type); } current_mode = EditorSettings::get_singleton()->get_project_metadata("shader_setup", "last_selected_mode", 0); @@ -67,12 +68,17 @@ void ShaderCreateDialog::_notification(int p_what) { void ShaderCreateDialog::_update_theme() { Ref<Texture2D> shader_icon = gc->get_theme_icon(SNAME("Shader"), SNAME("EditorIcons")); if (shader_icon.is_valid()) { - language_menu->set_item_icon(0, shader_icon); + type_menu->set_item_icon(0, shader_icon); } Ref<Texture2D> visual_shader_icon = gc->get_theme_icon(SNAME("VisualShader"), SNAME("EditorIcons")); if (visual_shader_icon.is_valid()) { - language_menu->set_item_icon(1, visual_shader_icon); + type_menu->set_item_icon(1, visual_shader_icon); + } + + Ref<Texture2D> include_icon = gc->get_theme_icon(SNAME("TextFile"), SNAME("EditorIcons")); + if (include_icon.is_valid()) { + type_menu->set_item_icon(2, include_icon); } path_button->set_icon(get_theme_icon(SNAME("Folder"), SNAME("EditorIcons"))); @@ -80,7 +86,7 @@ void ShaderCreateDialog::_update_theme() { } void ShaderCreateDialog::_update_language_info() { - language_data.clear(); + type_data.clear(); for (int i = 0; i < SHADER_TYPE_MAX; i++) { ShaderTypeData data; @@ -88,12 +94,15 @@ void ShaderCreateDialog::_update_language_info() { data.use_templates = true; data.extensions.push_back("gdshader"); data.default_extension = "gdshader"; + } else if (i == int(SHADER_TYPE_INC)) { + data.extensions.push_back("gdshaderinc"); + data.default_extension = "gdshaderinc"; } else { data.default_extension = "tres"; } data.extensions.push_back("res"); data.extensions.push_back("tres"); - language_data.push_back(data); + type_data.push_back(data); } } @@ -136,69 +145,97 @@ void ShaderCreateDialog::ok_pressed() { void ShaderCreateDialog::_create_new() { Ref<Resource> shader; - - if (language_menu->get_selected() == int(SHADER_TYPE_TEXT)) { - Ref<Shader> text_shader; - text_shader.instantiate(); - shader = text_shader; - - StringBuilder code; - code += vformat("shader_type %s;\n", mode_menu->get_text().replace(" ", "").camelcase_to_underscore()); - - if (current_template == 0) { // Default template. - code += "\n"; - switch (current_mode) { - case Shader::MODE_SPATIAL: - code += "void fragment() {\n"; - code += "\t// Place fragment code here.\n"; - code += "}\n"; - break; - case Shader::MODE_CANVAS_ITEM: - code += "void fragment() {\n"; - code += "\t// Place fragment code here.\n"; - code += "}\n"; - break; - case Shader::MODE_PARTICLES: - code += "void start() {\n"; - code += "\t// Place start code here.\n"; - code += "}\n"; - code += "\n"; - code += "void process() {\n"; - code += "\t// Place process code here.\n"; - code += "}\n"; - break; - case Shader::MODE_SKY: - code += "void sky() {\n"; - code += "\t// Place sky code here.\n"; - code += "}\n"; - break; - case Shader::MODE_FOG: - code += "void fog() {\n"; - code += "\t// Place fog code here.\n"; - code += "}\n"; - break; + Ref<Resource> shader_inc; + + switch (type_menu->get_selected()) { + case SHADER_TYPE_TEXT: { + Ref<Shader> text_shader; + text_shader.instantiate(); + shader = text_shader; + + StringBuilder code; + code += vformat("shader_type %s;\n", mode_menu->get_text().replace(" ", "").camelcase_to_underscore()); + + if (current_template == 0) { // Default template. + code += "\n"; + switch (current_mode) { + case Shader::MODE_SPATIAL: + code += "void fragment() {\n"; + code += "\t// Place fragment code here.\n"; + code += "}\n"; + break; + case Shader::MODE_CANVAS_ITEM: + code += "void fragment() {\n"; + code += "\t// Place fragment code here.\n"; + code += "}\n"; + break; + case Shader::MODE_PARTICLES: + code += "void start() {\n"; + code += "\t// Place start code here.\n"; + code += "}\n"; + code += "\n"; + code += "void process() {\n"; + code += "\t// Place process code here.\n"; + code += "}\n"; + break; + case Shader::MODE_SKY: + code += "void sky() {\n"; + code += "\t// Place sky code here.\n"; + code += "}\n"; + break; + case Shader::MODE_FOG: + code += "void fog() {\n"; + code += "\t// Place fog code here.\n"; + code += "}\n"; + break; + } } - } - text_shader->set_code(code.as_string()); - } else { - Ref<VisualShader> visual_shader; - visual_shader.instantiate(); - shader = visual_shader; - visual_shader->set_mode(Shader::Mode(current_mode)); + text_shader->set_code(code.as_string()); + } break; + case SHADER_TYPE_VISUAL: { + Ref<VisualShader> visual_shader; + visual_shader.instantiate(); + shader = visual_shader; + visual_shader->set_mode(Shader::Mode(current_mode)); + } break; + case SHADER_TYPE_INC: { + Ref<ShaderInclude> include; + include.instantiate(); + shader_inc = include; + } break; + default: { + } break; } - if (!is_built_in) { + if (shader.is_null()) { String lpath = ProjectSettings::get_singleton()->localize_path(file_path->get_text()); - shader->set_path(lpath); - Error err = ResourceSaver::save(lpath, shader, ResourceSaver::FLAG_CHANGE_PATH); - if (err != OK) { - alert->set_text(TTR("Error - Could not create shader in filesystem.")); + shader_inc->set_path(lpath); + + Error error = ResourceSaver::save(lpath, shader_inc, ResourceSaver::FLAG_CHANGE_PATH); + if (error != OK) { + alert->set_text(TTR("Error - Could not create shader include in filesystem.")); alert->popup_centered(); return; } + + emit_signal(SNAME("shader_include_created"), shader_inc); + } else { + if (!is_built_in) { + String lpath = ProjectSettings::get_singleton()->localize_path(file_path->get_text()); + shader->set_path(lpath); + + Error error = ResourceSaver::save(lpath, shader, ResourceSaver::FLAG_CHANGE_PATH); + if (error != OK) { + alert->set_text(TTR("Error - Could not create shader in filesystem.")); + alert->popup_centered(); + return; + } + } + + emit_signal(SNAME("shader_created"), shader); } - emit_signal(SNAME("shader_created"), shader); + file_path->set_text(file_path->get_text().get_base_dir()); hide(); } @@ -215,9 +252,9 @@ void ShaderCreateDialog::_load_exist() { hide(); } -void ShaderCreateDialog::_language_changed(int p_language) { - current_language = p_language; - ShaderTypeData data = language_data[p_language]; +void ShaderCreateDialog::_type_changed(int p_language) { + current_type = p_language; + ShaderTypeData data = type_data[p_language]; String selected_ext = "." + data.default_extension; String path = file_path->get_text(); @@ -238,6 +275,8 @@ void ShaderCreateDialog::_language_changed(int p_language) { _path_changed(path); file_path->set_text(path); + type_menu->set_item_disabled(int(SHADER_TYPE_INC), load_enabled); + mode_menu->set_disabled(p_language == SHADER_TYPE_INC); template_menu->set_disabled(!data.use_templates); template_menu->clear(); @@ -253,7 +292,7 @@ void ShaderCreateDialog::_language_changed(int p_language) { template_menu->add_item(TTR("N/A")); } - EditorSettings::get_singleton()->set_project_metadata("shader_setup", "last_selected_language", language_menu->get_item_text(language_menu->get_selected())); + EditorSettings::get_singleton()->set_project_metadata("shader_setup", "last_selected_language", type_menu->get_item_text(type_menu->get_selected())); _update_dialog(); } @@ -275,7 +314,7 @@ void ShaderCreateDialog::_browse_path() { file_browse->set_disable_overwrite_warning(true); file_browse->clear_filters(); - List<String> extensions = language_data[language_menu->get_selected()].extensions; + List<String> extensions = type_data[type_menu->get_selected()].extensions; for (const String &E : extensions) { file_browse->add_filter("*." + E); @@ -330,8 +369,8 @@ void ShaderCreateDialog::_path_submitted(const String &p_path) { void ShaderCreateDialog::config(const String &p_base_path, bool p_built_in_enabled, bool p_load_enabled, int p_preferred_type, int p_preferred_mode) { if (!p_base_path.is_empty()) { initial_base_path = p_base_path.get_basename(); - file_path->set_text(initial_base_path + "." + language_data[language_menu->get_selected()].default_extension); - current_language = language_menu->get_selected(); + file_path->set_text(initial_base_path + "." + type_data[type_menu->get_selected()].default_extension); + current_type = type_menu->get_selected(); } else { initial_base_path = ""; file_path->set_text(""); @@ -342,8 +381,8 @@ void ShaderCreateDialog::config(const String &p_base_path, bool p_built_in_enabl load_enabled = p_load_enabled; if (p_preferred_type > -1) { - language_menu->select(p_preferred_type); - _language_changed(p_preferred_type); + type_menu->select(p_preferred_type); + _type_changed(p_preferred_type); } if (p_preferred_mode > -1) { @@ -351,7 +390,7 @@ void ShaderCreateDialog::config(const String &p_base_path, bool p_built_in_enabl _mode_changed(p_preferred_mode); } - _language_changed(current_language); + _type_changed(current_type); _path_changed(file_path->get_text()); } @@ -384,14 +423,14 @@ String ShaderCreateDialog::_validate_path(const String &p_path) { HashSet<String> extensions; for (int i = 0; i < SHADER_TYPE_MAX; i++) { - for (const String &ext : language_data[i].extensions) { + for (const String &ext : type_data[i].extensions) { if (!extensions.has(ext)) { extensions.insert(ext); } } } - ShaderTypeData data = language_data[language_menu->get_selected()]; + ShaderTypeData data = type_data[type_menu->get_selected()]; bool found = false; bool match = false; @@ -399,8 +438,8 @@ String ShaderCreateDialog::_validate_path(const String &p_path) { for (const String &ext : extensions) { if (ext.nocasecmp_to(extension) == 0) { found = true; - for (const String &lang_ext : language_data[current_language].extensions) { - if (lang_ext.nocasecmp_to(extension) == 0) { + for (const String &type_ext : type_data[current_type].extensions) { + if (type_ext.nocasecmp_to(extension) == 0) { match = true; break; } @@ -504,6 +543,7 @@ void ShaderCreateDialog::_bind_methods() { ClassDB::bind_method(D_METHOD("config", "path", "built_in_enabled", "load_enabled"), &ShaderCreateDialog::config, DEFVAL(true), DEFVAL(true)); ADD_SIGNAL(MethodInfo("shader_created", PropertyInfo(Variant::OBJECT, "shader", PROPERTY_HINT_RESOURCE_TYPE, "Shader"))); + ADD_SIGNAL(MethodInfo("shader_include_created", PropertyInfo(Variant::OBJECT, "shader_include", PROPERTY_HINT_RESOURCE_TYPE, "ShaderInclude"))); } ShaderCreateDialog::ShaderCreateDialog() { @@ -547,24 +587,27 @@ ShaderCreateDialog::ShaderCreateDialog() { vb->add_child(status_panel); add_child(vb); - // Language. + // Type. - language_menu = memnew(OptionButton); - language_menu->set_custom_minimum_size(Size2(250, 0) * EDSCALE); - language_menu->set_h_size_flags(Control::SIZE_EXPAND_FILL); - gc->add_child(memnew(Label(TTR("Language:")))); - gc->add_child(language_menu); + type_menu = memnew(OptionButton); + type_menu->set_custom_minimum_size(Size2(250, 0) * EDSCALE); + type_menu->set_h_size_flags(Control::SIZE_EXPAND_FILL); + gc->add_child(memnew(Label(TTR("Type:")))); + gc->add_child(type_menu); for (int i = 0; i < SHADER_TYPE_MAX; i++) { - String language; + String type; bool invalid = false; switch (i) { case SHADER_TYPE_TEXT: - language = "Shader"; - default_language = i; + type = "Shader"; + default_type = i; break; case SHADER_TYPE_VISUAL: - language = "VisualShader"; + type = "VisualShader"; + break; + case SHADER_TYPE_INC: + type = "ShaderInclude"; break; case SHADER_TYPE_MAX: invalid = true; @@ -576,13 +619,13 @@ ShaderCreateDialog::ShaderCreateDialog() { if (invalid) { continue; } - language_menu->add_item(language); + type_menu->add_item(type); } - if (default_language >= 0) { - language_menu->select(default_language); + if (default_type >= 0) { + type_menu->select(default_type); } - current_language = default_language; - language_menu->connect("item_selected", callable_mp(this, &ShaderCreateDialog::_language_changed)); + current_type = default_type; + type_menu->connect("item_selected", callable_mp(this, &ShaderCreateDialog::_type_changed)); // Modes. diff --git a/editor/shader_create_dialog.h b/editor/shader_create_dialog.h index 6737ce4f10..44bd866fbd 100644 --- a/editor/shader_create_dialog.h +++ b/editor/shader_create_dialog.h @@ -47,6 +47,7 @@ class ShaderCreateDialog : public ConfirmationDialog { enum ShaderType { SHADER_TYPE_TEXT, SHADER_TYPE_VISUAL, + SHADER_TYPE_INC, SHADER_TYPE_MAX, }; @@ -56,14 +57,14 @@ class ShaderCreateDialog : public ConfirmationDialog { bool use_templates = false; }; - List<ShaderTypeData> language_data; + List<ShaderTypeData> type_data; GridContainer *gc = nullptr; Label *error_label = nullptr; Label *path_error_label = nullptr; Label *builtin_warning_label = nullptr; PanelContainer *status_panel = nullptr; - OptionButton *language_menu = nullptr; + OptionButton *type_menu = nullptr; OptionButton *mode_menu = nullptr; OptionButton *template_menu = nullptr; CheckBox *internal = nullptr; @@ -79,8 +80,8 @@ class ShaderCreateDialog : public ConfirmationDialog { bool built_in_enabled = true; bool load_enabled = false; bool re_check_path = false; - int current_language = -1; - int default_language = -1; + int current_type = -1; + int default_type = -1; int current_mode = 0; int current_template = 0; @@ -89,7 +90,7 @@ class ShaderCreateDialog : public ConfirmationDialog { void _path_hbox_sorted(); void _path_changed(const String &p_path = String()); void _path_submitted(const String &p_path = String()); - void _language_changed(int p_language = 0); + void _type_changed(int p_type = 0); void _built_in_toggled(bool p_enabled); void _template_changed(int p_template = 0); void _mode_changed(int p_mode = 0); diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index b1ef3d0f6f..09a283ea53 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -174,6 +174,7 @@ #include "scene/resources/segment_shape_2d.h" #include "scene/resources/separation_ray_shape_2d.h" #include "scene/resources/separation_ray_shape_3d.h" +#include "scene/resources/shader_include.h" #include "scene/resources/skeleton_modification_2d.h" #include "scene/resources/skeleton_modification_2d_ccdik.h" #include "scene/resources/skeleton_modification_2d_fabrik.h" @@ -273,6 +274,9 @@ static Ref<ResourceFormatLoaderCompressedTexture3D> resource_loader_texture_3d; static Ref<ResourceFormatSaverShader> resource_saver_shader; static Ref<ResourceFormatLoaderShader> resource_loader_shader; +static Ref<ResourceFormatSaverShaderInclude> resource_saver_shader_include; +static Ref<ResourceFormatLoaderShaderInclude> resource_loader_shader_include; + void register_scene_types() { SceneStringNames::create(); @@ -301,6 +305,12 @@ void register_scene_types() { resource_loader_shader.instantiate(); ResourceLoader::add_resource_format_loader(resource_loader_shader, true); + resource_saver_shader_include.instantiate(); + ResourceSaver::add_resource_format_saver(resource_saver_shader_include, true); + + resource_loader_shader_include.instantiate(); + ResourceLoader::add_resource_format_loader(resource_loader_shader_include, true); + OS::get_singleton()->yield(); // may take time to init GDREGISTER_CLASS(Object); @@ -569,6 +579,7 @@ void register_scene_types() { GDREGISTER_CLASS(Shader); GDREGISTER_CLASS(VisualShader); + GDREGISTER_CLASS(ShaderInclude); GDREGISTER_ABSTRACT_CLASS(VisualShaderNode); GDREGISTER_CLASS(VisualShaderNodeCustom); GDREGISTER_CLASS(VisualShaderNodeInput); @@ -1185,6 +1196,12 @@ void unregister_scene_types() { ResourceLoader::remove_resource_format_loader(resource_loader_shader); resource_loader_shader.unref(); + ResourceSaver::remove_resource_format_saver(resource_saver_shader_include); + resource_saver_shader_include.unref(); + + ResourceLoader::remove_resource_format_loader(resource_loader_shader_include); + resource_loader_shader_include.unref(); + // StandardMaterial3D is not initialised when 3D is disabled, so it shouldn't be cleaned up either #ifndef _3D_DISABLED BaseMaterial3D::finish_shaders(); diff --git a/scene/resources/shader.cpp b/scene/resources/shader.cpp index d49157b1b8..e7f8e6ae11 100644 --- a/scene/resources/shader.cpp +++ b/scene/resources/shader.cpp @@ -40,8 +40,28 @@ Shader::Mode Shader::get_mode() const { return mode; } +void Shader::_dependency_changed() { + RenderingServer::get_singleton()->shader_set_code(shader, RenderingServer::get_singleton()->shader_get_code(shader)); + params_cache_dirty = true; + + emit_changed(); +} + void Shader::set_code(const String &p_code) { - String type = ShaderLanguage::get_shader_type(p_code); + HashSet<Ref<ShaderInclude>> new_include_dependencies; + + for (Ref<ShaderInclude> E : include_dependencies) { + E->disconnect(SNAME("changed"), callable_mp(this, &Shader::_dependency_changed)); + } + + String type = ShaderLanguage::get_shader_type_and_dependencies(p_code, &new_include_dependencies); + + // This ensures previous include resources are not freed and then re-loaded during parse (which would make compiling slower) + include_dependencies = new_include_dependencies; + + for (Ref<ShaderInclude> E : include_dependencies) { + E->connect(SNAME("changed"), callable_mp(this, &Shader::_dependency_changed)); + } if (type == "canvas_item") { mode = MODE_CANVAS_ITEM; diff --git a/scene/resources/shader.h b/scene/resources/shader.h index 11c9f60ce8..dc3fe8e619 100644 --- a/scene/resources/shader.h +++ b/scene/resources/shader.h @@ -35,6 +35,7 @@ #include "core/io/resource_loader.h" #include "core/io/resource_saver.h" #include "scene/resources/texture.h" +#include "shader_include.h" class Shader : public Resource { GDCLASS(Shader, Resource); @@ -53,6 +54,7 @@ public: private: RID shader; Mode mode = MODE_SPATIAL; + HashSet<Ref<ShaderInclude>> include_dependencies; // hack the name of performance // shaders keep a list of ShaderMaterial -> RenderingServer name translations, to make @@ -61,6 +63,7 @@ private: mutable HashMap<StringName, StringName> params_cache; //map a shader param to a material param.. HashMap<StringName, HashMap<int, Ref<Texture2D>>> default_textures; + void _dependency_changed(); virtual void _update_shader() const; //used for visual shader protected: static void _bind_methods(); diff --git a/scene/resources/shader_include.cpp b/scene/resources/shader_include.cpp new file mode 100644 index 0000000000..2d4f18534d --- /dev/null +++ b/scene/resources/shader_include.cpp @@ -0,0 +1,139 @@ +/*************************************************************************/ +/* shader_include.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "shader_include.h" +#include "servers/rendering/shader_language.h" + +void ShaderInclude::_dependency_changed() { + emit_changed(); +} + +void ShaderInclude::set_code(const String &p_code) { + HashSet<Ref<ShaderInclude>> new_dependencies; + code = p_code; + + for (Ref<ShaderInclude> E : dependencies) { + E->disconnect(SNAME("changed"), callable_mp(this, &ShaderInclude::_dependency_changed)); + } + + ShaderLanguage::get_shader_dependencies(p_code, &new_dependencies); + + // This ensures previous include resources are not freed and then re-loaded during parse (which would make compiling slower) + dependencies = new_dependencies; + + for (Ref<ShaderInclude> E : dependencies) { + E->connect(SNAME("changed"), callable_mp(this, &ShaderInclude::_dependency_changed)); + } + + emit_changed(); +} + +String ShaderInclude::get_code() const { + return code; +} + +void ShaderInclude::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_code", "code"), &ShaderInclude::set_code); + ClassDB::bind_method(D_METHOD("get_code"), &ShaderInclude::get_code); + + ADD_PROPERTY(PropertyInfo(Variant::STRING, "code", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_code", "get_code"); +} + +// ResourceFormatLoaderShaderInclude + +Ref<Resource> ResourceFormatLoaderShaderInclude::load(const String &p_path, const String &p_original_path, Error *r_error, bool p_use_sub_threads, float *r_progress, CacheMode p_cache_mode) { + if (r_error) { + *r_error = ERR_FILE_CANT_OPEN; + } + + Ref<ShaderInclude> shader_inc; + shader_inc.instantiate(); + + Vector<uint8_t> buffer = FileAccess::get_file_as_array(p_path); + + String str; + str.parse_utf8((const char *)buffer.ptr(), buffer.size()); + + shader_inc->set_code(str); + + if (r_error) { + *r_error = OK; + } + + return shader_inc; +} + +void ResourceFormatLoaderShaderInclude::get_recognized_extensions(List<String> *p_extensions) const { + p_extensions->push_back("gdshaderinc"); +} + +bool ResourceFormatLoaderShaderInclude::handles_type(const String &p_type) const { + return (p_type == "ShaderInclude"); +} + +String ResourceFormatLoaderShaderInclude::get_resource_type(const String &p_path) const { + String extension = p_path.get_extension().to_lower(); + if (extension == "gdshaderinc") { + return "ShaderInclude"; + } + return ""; +} + +// ResourceFormatSaverShaderInclude + +Error ResourceFormatSaverShaderInclude::save(const String &p_path, const Ref<Resource> &p_resource, uint32_t p_flags) { + Ref<ShaderInclude> shader_inc = p_resource; + ERR_FAIL_COND_V(shader_inc.is_null(), ERR_INVALID_PARAMETER); + + String source = shader_inc->get_code(); + + Error error; + Ref<FileAccess> file = FileAccess::open(p_path, FileAccess::WRITE, &error); + + ERR_FAIL_COND_V_MSG(error, error, "Cannot save shader include '" + p_path + "'."); + + file->store_string(source); + if (file->get_error() != OK && file->get_error() != ERR_FILE_EOF) { + return ERR_CANT_CREATE; + } + + return OK; +} + +void ResourceFormatSaverShaderInclude::get_recognized_extensions(const Ref<Resource> &p_resource, List<String> *p_extensions) const { + const ShaderInclude *shader_inc = Object::cast_to<ShaderInclude>(*p_resource); + if (shader_inc != nullptr) { + p_extensions->push_back("gdshaderinc"); + } +} + +bool ResourceFormatSaverShaderInclude::recognize(const Ref<Resource> &p_resource) const { + return p_resource->get_class_name() == "ShaderInclude"; //only shader, not inherited +} diff --git a/scene/resources/shader_include.h b/scene/resources/shader_include.h new file mode 100644 index 0000000000..6f0deeef4e --- /dev/null +++ b/scene/resources/shader_include.h @@ -0,0 +1,71 @@ +/*************************************************************************/ +/* shader_include.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef SHADER_INCLUDE_H +#define SHADER_INCLUDE_H + +#include "core/io/resource.h" +#include "core/io/resource_loader.h" +#include "core/io/resource_saver.h" +#include "core/templates/hash_set.h" + +class ShaderInclude : public Resource { + GDCLASS(ShaderInclude, Resource); + OBJ_SAVE_TYPE(ShaderInclude); + +private: + String code; + HashSet<Ref<ShaderInclude>> dependencies; + void _dependency_changed(); + +protected: + static void _bind_methods(); + +public: + void set_code(const String &p_text); + String get_code() const; +}; + +class ResourceFormatLoaderShaderInclude : public ResourceFormatLoader { +public: + virtual Ref<Resource> load(const String &p_path, const String &p_original_path = "", Error *r_error = nullptr, bool p_use_sub_threads = false, float *r_progress = nullptr, CacheMode p_cache_mode = CACHE_MODE_REUSE); + virtual void get_recognized_extensions(List<String> *p_extensions) const; + virtual bool handles_type(const String &p_type) const; + virtual String get_resource_type(const String &p_path) const; +}; + +class ResourceFormatSaverShaderInclude : public ResourceFormatSaver { +public: + virtual Error save(const String &p_path, const Ref<Resource> &p_resource, uint32_t p_flags = 0); + virtual void get_recognized_extensions(const Ref<Resource> &p_resource, List<String> *p_extensions) const; + virtual bool recognize(const Ref<Resource> &p_resource) const; +}; + +#endif // SHADER_INCLUDE_H diff --git a/servers/rendering/shader_language.cpp b/servers/rendering/shader_language.cpp index ad9b51ac0c..f4de32d14b 100644 --- a/servers/rendering/shader_language.cpp +++ b/servers/rendering/shader_language.cpp @@ -33,6 +33,7 @@ #include "core/os/os.h" #include "core/string/print_string.h" #include "servers/rendering_server.h" +#include "shader_preprocessor.h" #define HAS_WARNING(flag) (warning_flags & flag) @@ -4118,6 +4119,10 @@ void ShaderLanguage::get_keyword_list(List<String> *r_keywords) { } } +void ShaderLanguage::get_preprocessor_keyword_list(List<String> *r_keywords, bool p_include_shader_keywords) { + ShaderPreprocessor::get_keyword_list(r_keywords, p_include_shader_keywords); +} + bool ShaderLanguage::is_control_flow_keyword(String p_keyword) { return p_keyword == "break" || p_keyword == "case" || @@ -7677,35 +7682,60 @@ Error ShaderLanguage::_validate_precision(DataType p_type, DataPrecision p_preci return OK; } -Error ShaderLanguage::_parse_shader(const HashMap<StringName, FunctionInfo> &p_functions, const Vector<ModeInfo> &p_render_modes, const HashSet<String> &p_shader_types) { - Token tk = _get_token(); +Error ShaderLanguage::_preprocess_shader(const String &p_code, String &r_result, int *r_completion_type) { + Error error = OK; + + ShaderPreprocessor processor(p_code); + processor.preprocess(r_result); + + ShaderPreprocessor::State *state = processor.get_state(); + if (!state->error.is_empty()) { + error_line = state->error_line; + error_set = true; + error_str = state->error; + error = FAILED; + } + + if (r_completion_type != nullptr) { + *r_completion_type = (int)state->completion_type; + } + + return error; +} + +Error ShaderLanguage::_parse_shader(const HashMap<StringName, FunctionInfo> &p_functions, const Vector<ModeInfo> &p_render_modes, const HashSet<String> &p_shader_types, bool p_is_include) { + Token tk; TkPos prev_pos; Token next; - if (tk.type != TK_SHADER_TYPE) { - _set_error(vformat(RTR("Expected '%s' at the beginning of shader. Valid types are: %s."), "shader_type", _get_shader_type_list(p_shader_types))); - return ERR_PARSE_ERROR; - } + if (!p_is_include) { + tk = _get_token(); + + if (tk.type != TK_SHADER_TYPE) { + _set_error(vformat(RTR("Expected '%s' at the beginning of shader. Valid types are: %s."), "shader_type", _get_shader_type_list(p_shader_types))); + return ERR_PARSE_ERROR; + } #ifdef DEBUG_ENABLED - keyword_completion_context = CF_UNSPECIFIED; + keyword_completion_context = CF_UNSPECIFIED; #endif // DEBUG_ENABLED - _get_completable_identifier(nullptr, COMPLETION_SHADER_TYPE, shader_type_identifier); - if (shader_type_identifier == StringName()) { - _set_error(vformat(RTR("Expected an identifier after '%s', indicating the type of shader. Valid types are: %s."), "shader_type", _get_shader_type_list(p_shader_types))); - return ERR_PARSE_ERROR; - } - if (!p_shader_types.has(shader_type_identifier)) { - _set_error(vformat(RTR("Invalid shader type. Valid types are: %s"), _get_shader_type_list(p_shader_types))); - return ERR_PARSE_ERROR; - } - prev_pos = _get_tkpos(); - tk = _get_token(); + _get_completable_identifier(nullptr, COMPLETION_SHADER_TYPE, shader_type_identifier); + if (shader_type_identifier == StringName()) { + _set_error(vformat(RTR("Expected an identifier after '%s', indicating the type of shader. Valid types are: %s."), "shader_type", _get_shader_type_list(p_shader_types))); + return ERR_PARSE_ERROR; + } + if (!p_shader_types.has(shader_type_identifier)) { + _set_error(vformat(RTR("Invalid shader type. Valid types are: %s"), _get_shader_type_list(p_shader_types))); + return ERR_PARSE_ERROR; + } + prev_pos = _get_tkpos(); + tk = _get_token(); - if (tk.type != TK_SEMICOLON) { - _set_tkpos(prev_pos); - _set_expected_after_error(";", "shader_type " + String(shader_type_identifier)); - return ERR_PARSE_ERROR; + if (tk.type != TK_SEMICOLON) { + _set_tkpos(prev_pos); + _set_expected_after_error(";", "shader_type " + String(shader_type_identifier)); + return ERR_PARSE_ERROR; + } } #ifdef DEBUG_ENABLED @@ -9470,6 +9500,97 @@ String ShaderLanguage::get_shader_type(const String &p_code) { return String(); } +void ShaderLanguage::get_shader_dependencies(const String &p_code, HashSet<Ref<ShaderInclude>> *r_dependencies) { + bool reading_inc = false; + String cur_identifier; + + for (int i = _get_first_ident_pos(p_code); i < p_code.length(); i++) { + if (p_code[i] == ';') { + continue; + + } else if (p_code[i] <= 32) { + if (cur_identifier == "#include") { + reading_inc = true; + cur_identifier = String(); + } else { + if (reading_inc) { + String path = cur_identifier; + if (path.begins_with("\"") && path.ends_with("\"")) { + path = path.substr(1, path.length() - 2); + if (!path.begins_with("res://")) { + path = path.insert(0, "res://"); + } + Ref<ShaderInclude> inc = ResourceLoader::load(path); + if (inc.is_valid()) { + r_dependencies->insert(inc); + } + } + reading_inc = false; + } + } + } else { + cur_identifier += String::chr(p_code[i]); + } + } +} + +String ShaderLanguage::get_shader_type_and_dependencies(const String &p_code, HashSet<Ref<ShaderInclude>> *r_dependencies) { + bool read_type = true; + bool reading_type = false; + bool reading_inc = false; + String type; + + String cur_identifier; + + for (int i = _get_first_ident_pos(p_code); i < p_code.length(); i++) { + if (p_code[i] == ';') { + continue; + + } else if (p_code[i] <= 32) { + if (!cur_identifier.is_empty()) { + if (read_type) { + if (!reading_type) { + if (cur_identifier == "shader_type") { + reading_type = true; + cur_identifier = String(); + } + } else { + type = cur_identifier; + read_type = false; + cur_identifier = String(); + } + } else if (cur_identifier == "#include") { + reading_inc = true; + cur_identifier = String(); + } else { + if (reading_inc) { + String path = cur_identifier; + if (path.begins_with("\"") && path.ends_with("\"")) { + path = path.substr(1, path.length() - 2); + if (!path.begins_with("res://")) { + path = path.insert(0, "res://"); + } + Ref<ShaderInclude> inc = ResourceLoader::load(path); + if (inc.is_valid()) { + r_dependencies->insert(inc); + } + } + reading_inc = false; + } + } + } + } else { + cur_identifier += String::chr(p_code[i]); + } + } + + if (reading_type) { + return type; + } + + return String(); +} + #ifdef DEBUG_ENABLED void ShaderLanguage::_check_warning_accums() { for (const KeyValue<ShaderWarning::Code, HashMap<StringName, HashMap<StringName, Usage>> *> &E : warnings_check_map2) { @@ -9509,14 +9630,21 @@ uint32_t ShaderLanguage::get_warning_flags() const { Error ShaderLanguage::compile(const String &p_code, const ShaderCompileInfo &p_info) { clear(); - code = p_code; + Error err = _preprocess_shader(p_code, code); + if (err != OK) { + return err; + } + + // Clear after preprocessing. Because preprocess uses the resource loader, it means if this instance is held in a singleton, it can have a changed state after. + clear(); + global_var_get_type_func = p_info.global_variable_type_func; varying_function_names = p_info.varying_function_names; nodes = nullptr; shader = alloc_node<ShaderNode>(); - Error err = _parse_shader(p_info.functions, p_info.render_modes, p_info.shader_types); + err = _parse_shader(p_info.functions, p_info.render_modes, p_info.shader_types, p_info.is_include); #ifdef DEBUG_ENABLED if (check_warnings) { @@ -9533,14 +9661,51 @@ Error ShaderLanguage::compile(const String &p_code, const ShaderCompileInfo &p_i Error ShaderLanguage::complete(const String &p_code, const ShaderCompileInfo &p_info, List<ScriptLanguage::CodeCompletionOption> *r_options, String &r_call_hint) { clear(); - code = p_code; + int preprocessor_completion_type; + Error error = _preprocess_shader(p_code, code, &preprocessor_completion_type); + + switch (preprocessor_completion_type) { + case ShaderPreprocessor::COMPLETION_TYPE_DIRECTIVE: { + static List<String> options; + + if (options.is_empty()) { + ShaderPreprocessor::get_keyword_list(&options, true); + } + + for (const String &E : options) { + ScriptLanguage::CodeCompletionOption option(E, ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); + r_options->push_back(option); + } + + return OK; + } break; + case ShaderPreprocessor::COMPLETION_TYPE_PRAGMA: { + static List<String> options; + + if (options.is_empty()) { + ShaderPreprocessor::get_pragma_list(&options); + } + + for (const String &E : options) { + ScriptLanguage::CodeCompletionOption option(E, ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT); + r_options->push_back(option); + } + + return OK; + } break; + } + + if (error != OK) { + return error; + } + varying_function_names = p_info.varying_function_names; nodes = nullptr; global_var_get_type_func = p_info.global_variable_type_func; shader = alloc_node<ShaderNode>(); - _parse_shader(p_info.functions, p_info.render_modes, p_info.shader_types); + _parse_shader(p_info.functions, p_info.render_modes, p_info.shader_types, p_info.is_include); #ifdef DEBUG_ENABLED // Adds context keywords. diff --git a/servers/rendering/shader_language.h b/servers/rendering/shader_language.h index 2b147fbeb1..59d679fd98 100644 --- a/servers/rendering/shader_language.h +++ b/servers/rendering/shader_language.h @@ -38,6 +38,7 @@ #include "core/templates/rb_map.h" #include "core/typedefs.h" #include "core/variant/variant.h" +#include "scene/resources/shader_include.h" #ifdef DEBUG_ENABLED #include "shader_warnings.h" @@ -776,6 +777,7 @@ public: static uint32_t get_datatype_size(DataType p_type); static void get_keyword_list(List<String> *r_keywords); + static void get_preprocessor_keyword_list(List<String> *r_keywords, bool p_include_shader_keywords); static bool is_control_flow_keyword(String p_keyword); static void get_builtin_funcs(List<String> *r_keywords); @@ -1070,7 +1072,8 @@ private: String _get_shader_type_list(const HashSet<String> &p_shader_types) const; String _get_qualifier_str(ArgumentQualifier p_qualifier) const; - Error _parse_shader(const HashMap<StringName, FunctionInfo> &p_functions, const Vector<ModeInfo> &p_render_modes, const HashSet<String> &p_shader_types); + Error _preprocess_shader(const String &p_code, String &r_result, int *r_completion_type = nullptr); + Error _parse_shader(const HashMap<StringName, FunctionInfo> &p_functions, const Vector<ModeInfo> &p_render_modes, const HashSet<String> &p_shader_types, bool p_is_include); Error _find_last_flow_op_in_block(BlockNode *p_block, FlowOperation p_op); Error _find_last_flow_op_in_op(ControlFlowNode *p_flow, FlowOperation p_op); @@ -1091,6 +1094,8 @@ public: void clear(); static String get_shader_type(const String &p_code); + static void get_shader_dependencies(const String &p_code, HashSet<Ref<ShaderInclude>> *r_dependencies); + static String get_shader_type_and_dependencies(const String &p_code, HashSet<Ref<ShaderInclude>> *r_dependencies); struct ShaderCompileInfo { HashMap<StringName, FunctionInfo> functions; @@ -1098,6 +1103,7 @@ public: VaryingFunctionNames varying_function_names = VaryingFunctionNames(); HashSet<String> shader_types; GlobalVariableGetTypeFunc global_variable_type_func = nullptr; + bool is_include = false; }; Error compile(const String &p_code, const ShaderCompileInfo &p_info); diff --git a/servers/rendering/shader_preprocessor.cpp b/servers/rendering/shader_preprocessor.cpp new file mode 100644 index 0000000000..3890c63e9f --- /dev/null +++ b/servers/rendering/shader_preprocessor.cpp @@ -0,0 +1,1027 @@ +/*************************************************************************/ +/* shader_preprocessor.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#include "shader_preprocessor.h" +#include "core/math/expression.h" + +const char32_t CURSOR = 0xFFFF; + +// Tokenizer + +void ShaderPreprocessor::Tokenizer::add_generated(const ShaderPreprocessor::Token &p_t) { + generated.push_back(p_t); +} + +char32_t ShaderPreprocessor::Tokenizer::next() { + if (index < size) { + return code[index++]; + } + return 0; +} + +int ShaderPreprocessor::Tokenizer::get_line() const { + return line; +} + +int ShaderPreprocessor::Tokenizer::get_index() const { + return index; +} + +void ShaderPreprocessor::Tokenizer::get_and_clear_generated(Vector<ShaderPreprocessor::Token> *r_out) { + for (int i = 0; i < generated.size(); i++) { + r_out->push_back(generated[i]); + } + generated.clear(); +} + +void ShaderPreprocessor::Tokenizer::backtrack(char32_t p_what) { + while (index >= 0) { + char32_t c = code[index]; + if (c == p_what) { + break; + } + index--; + } +} + +char32_t ShaderPreprocessor::Tokenizer::peek() { + if (index < size) { + return code[index]; + } + return 0; +} + +LocalVector<ShaderPreprocessor::Token> ShaderPreprocessor::Tokenizer::advance(char32_t p_what) { + LocalVector<ShaderPreprocessor::Token> tokens; + + while (index < size) { + char32_t c = code[index++]; + + tokens.push_back(ShaderPreprocessor::Token(c, line)); + + if (c == '\n') { + add_generated(ShaderPreprocessor::Token('\n', line)); + line++; + } + + if (c == p_what || c == 0) { + return tokens; + } + } + return LocalVector<ShaderPreprocessor::Token>(); +} + +void ShaderPreprocessor::Tokenizer::skip_whitespace() { + while (is_char_space(peek())) { + next(); + } +} + +String ShaderPreprocessor::Tokenizer::get_identifier(bool *r_is_cursor, bool p_started) { + if (r_is_cursor != nullptr) { + *r_is_cursor = false; + } + + LocalVector<char32_t> text; + + while (true) { + char32_t c = peek(); + if (is_char_end(c) || c == '(' || c == ')' || c == ',' || c == ';') { + break; + } + + if (is_whitespace(c) && p_started) { + break; + } + if (!is_whitespace(c)) { + p_started = true; + } + + char32_t n = next(); + if (n == CURSOR) { + if (r_is_cursor != nullptr) { + *r_is_cursor = true; + } + } else { + if (p_started) { + text.push_back(n); + } + } + } + + String id = vector_to_string(text); + if (!id.is_valid_identifier()) { + return ""; + } + + return id; +} + +String ShaderPreprocessor::Tokenizer::peek_identifier() { + const int original = index; + String id = get_identifier(); + index = original; + return id; +} + +ShaderPreprocessor::Token ShaderPreprocessor::Tokenizer::get_token() { + while (index < size) { + const char32_t c = code[index++]; + const Token t = ShaderPreprocessor::Token(c, line); + + switch (c) { + case ' ': + case '\t': + skip_whitespace(); + return ShaderPreprocessor::Token(' ', line); + case '\n': + line++; + return t; + default: + return t; + } + } + return ShaderPreprocessor::Token(char32_t(0), line); +} + +ShaderPreprocessor::Tokenizer::Tokenizer(const String &p_code) { + code = p_code; + line = 0; + index = 0; + size = code.size(); +} + +// ShaderPreprocessor::CommentRemover + +String ShaderPreprocessor::CommentRemover::get_error() const { + if (comments_open != 0) { + return "Block comment mismatch"; + } + return ""; +} + +int ShaderPreprocessor::CommentRemover::get_error_line() const { + if (comments_open != 0) { + return comment_line_open; + } + return -1; +} + +char32_t ShaderPreprocessor::CommentRemover::peek() const { + if (index < code.size()) { + return code[index]; + } + return 0; +} + +bool ShaderPreprocessor::CommentRemover::advance(char32_t p_what) { + while (index < code.size()) { + char32_t c = code[index++]; + + if (c == '\n') { + line++; + stripped.push_back('\n'); + } + + if (c == p_what) { + return true; + } + } + return false; +} + +String ShaderPreprocessor::CommentRemover::strip() { + stripped.clear(); + index = 0; + line = 0; + comment_line_open = 0; + comments_open = 0; + strings_open = 0; + + while (index < code.size()) { + char32_t c = code[index++]; + + if (c == CURSOR) { + // Cursor. Maintain. + stripped.push_back(c); + } else if (c == '"') { + if (strings_open <= 0) { + strings_open++; + } else { + strings_open--; + } + stripped.push_back(c); + } else if (c == '/' && strings_open == 0) { + char32_t p = peek(); + if (p == '/') { // Single line comment. + advance('\n'); + } else if (p == '*') { // Start of a block comment. + index++; + comment_line_open = line; + comments_open++; + while (advance('*')) { + if (peek() == '/') { // End of a block comment. + comments_open--; + index++; + break; + } + } + } else { + stripped.push_back(c); + } + } else if (c == '*' && strings_open == 0) { + if (peek() == '/') { // Unmatched end of a block comment. + comment_line_open = line; + comments_open--; + } else { + stripped.push_back(c); + } + } else if (c == '\n') { + line++; + stripped.push_back(c); + } else { + stripped.push_back(c); + } + } + return vector_to_string(stripped); +} + +ShaderPreprocessor::CommentRemover::CommentRemover(const String &p_code) { + code = p_code; + index = 0; + line = 0; + comment_line_open = 0; + comments_open = 0; + strings_open = 0; +} + +// ShaderPreprocessor::Token + +ShaderPreprocessor::Token::Token() { + text = 0; + line = -1; +} + +ShaderPreprocessor::Token::Token(char32_t p_text, int p_line) { + text = p_text; + line = p_line; +} + +// ShaderPreprocessor + +bool ShaderPreprocessor::is_char_word(char32_t p_char) { + if ((p_char >= '0' && p_char <= '9') || + (p_char >= 'a' && p_char <= 'z') || + (p_char >= 'A' && p_char <= 'Z') || + p_char == '_') { + return true; + } + + return false; +} + +bool ShaderPreprocessor::is_char_space(char32_t p_char) { + return p_char == ' ' || p_char == '\t'; +} + +bool ShaderPreprocessor::is_char_end(char32_t p_char) { + return p_char == '\n' || p_char == 0; +} + +String ShaderPreprocessor::vector_to_string(const LocalVector<char32_t> &p_v, int p_start, int p_end) { + const int stop = (p_end == -1) ? p_v.size() : p_end; + const int count = stop - p_start; + + String result; + result.resize(count + 1); + for (int i = 0; i < count; i++) { + result[i] = p_v[p_start + i]; + } + result[count] = 0; // Ensure string is null terminated for length() to work. + return result; +} + +String ShaderPreprocessor::tokens_to_string(const LocalVector<Token> &p_tokens) { + LocalVector<char32_t> result; + for (uint32_t i = 0; i < p_tokens.size(); i++) { + result.push_back(p_tokens[i].text); + } + return vector_to_string(result); +} + +void ShaderPreprocessor::process_directive(Tokenizer *p_tokenizer) { + bool is_cursor; + String directive = p_tokenizer->get_identifier(&is_cursor, true); + if (is_cursor) { + state->completion_type = COMPLETION_TYPE_DIRECTIVE; + } + + if (directive == "if") { + if (check_directive_before_type(p_tokenizer, directive)) { + process_if(p_tokenizer); + } + } else if (directive == "ifdef") { + if (check_directive_before_type(p_tokenizer, directive)) { + process_ifdef(p_tokenizer); + } + } else if (directive == "ifndef") { + if (check_directive_before_type(p_tokenizer, directive)) { + process_ifndef(p_tokenizer); + } + } else if (directive == "else") { + if (check_directive_before_type(p_tokenizer, directive)) { + process_else(p_tokenizer); + } + } else if (directive == "endif") { + if (check_directive_before_type(p_tokenizer, directive)) { + process_endif(p_tokenizer); + } + } else if (directive == "define") { + if (check_directive_before_type(p_tokenizer, directive)) { + process_define(p_tokenizer); + } + } else if (directive == "undef") { + if (check_directive_before_type(p_tokenizer, directive)) { + process_undef(p_tokenizer); + } + } else if (directive == "include") { + if (check_directive_before_type(p_tokenizer, directive)) { + process_include(p_tokenizer); + } + } else if (directive == "pragma") { + if (check_directive_before_type(p_tokenizer, directive)) { + process_pragma(p_tokenizer); + } + } else { + set_error(RTR("Unknown directive."), p_tokenizer->get_line()); + } +} + +void ShaderPreprocessor::process_define(Tokenizer *p_tokenizer) { + const int line = p_tokenizer->get_line(); + + String label = p_tokenizer->get_identifier(); + if (label.is_empty()) { + set_error(RTR("Invalid macro name."), line); + return; + } + + if (state->defines.has(label)) { + set_error(RTR("Macro redefinition."), line); + return; + } + + if (p_tokenizer->peek() == '(') { + // Macro has arguments. + p_tokenizer->get_token(); + + Vector<String> args; + while (true) { + String name = p_tokenizer->get_identifier(); + if (name.is_empty()) { + set_error(RTR("Invalid argument name."), line); + return; + } + args.push_back(name); + + p_tokenizer->skip_whitespace(); + char32_t next = p_tokenizer->get_token().text; + if (next == ')') { + break; + } else if (next != ',') { + set_error(RTR("Expected a comma in the macro argument list."), line); + return; + } + } + + Define *define = memnew(Define); + define->arguments = args; + define->body = tokens_to_string(p_tokenizer->advance('\n')).strip_edges(); + state->defines[label] = define; + } else { + // Simple substitution macro. + Define *define = memnew(Define); + define->body = tokens_to_string(p_tokenizer->advance('\n')).strip_edges(); + state->defines[label] = define; + } +} + +void ShaderPreprocessor::process_else(Tokenizer *p_tokenizer) { + if (state->skip_stack_else.is_empty()) { + set_error(RTR("Unmatched else."), p_tokenizer->get_line()); + return; + } + p_tokenizer->advance('\n'); + + bool skip = state->skip_stack_else[state->skip_stack_else.size() - 1]; + state->skip_stack_else.remove_at(state->skip_stack_else.size() - 1); + + Vector<SkippedCondition *> vec = state->skipped_conditions[state->current_include]; + int index = vec.size() - 1; + if (index >= 0) { + SkippedCondition *cond = vec[index]; + if (cond->end_line == -1) { + cond->end_line = p_tokenizer->get_line(); + } + } + + if (skip) { + Vector<String> ends; + ends.push_back("endif"); + next_directive(p_tokenizer, ends); + } +} + +void ShaderPreprocessor::process_endif(Tokenizer *p_tokenizer) { + state->condition_depth--; + if (state->condition_depth < 0) { + set_error(RTR("Unmatched endif."), p_tokenizer->get_line()); + return; + } + + Vector<SkippedCondition *> vec = state->skipped_conditions[state->current_include]; + int index = vec.size() - 1; + if (index >= 0) { + SkippedCondition *cond = vec[index]; + if (cond->end_line == -1) { + cond->end_line = p_tokenizer->get_line(); + } + } + + p_tokenizer->advance('\n'); +} + +void ShaderPreprocessor::process_if(Tokenizer *p_tokenizer) { + int line = p_tokenizer->get_line(); + + String body = tokens_to_string(p_tokenizer->advance('\n')).strip_edges(); + if (body.is_empty()) { + set_error(RTR("Missing condition."), line); + return; + } + + Error error = expand_macros(body, line, body); + if (error != OK) { + return; + } + + Expression expression; + Vector<String> names; + error = expression.parse(body, names); + if (error != OK) { + set_error(expression.get_error_text(), line); + return; + } + + Variant v = expression.execute(Array(), nullptr, false); + if (v.get_type() == Variant::NIL) { + set_error(RTR("Condition evaluation error."), line); + return; + } + + bool success = v.booleanize(); + start_branch_condition(p_tokenizer, success); +} + +void ShaderPreprocessor::process_ifdef(Tokenizer *p_tokenizer) { + const int line = p_tokenizer->get_line(); + + String label = p_tokenizer->get_identifier(); + if (label.is_empty()) { + set_error(RTR("Invalid macro name."), line); + return; + } + + p_tokenizer->skip_whitespace(); + if (!is_char_end(p_tokenizer->peek())) { + set_error(RTR("Invalid ifdef."), line); + return; + } + p_tokenizer->advance('\n'); + + bool success = state->defines.has(label); + start_branch_condition(p_tokenizer, success); +} + +void ShaderPreprocessor::process_ifndef(Tokenizer *p_tokenizer) { + const int line = p_tokenizer->get_line(); + + String label = p_tokenizer->get_identifier(); + if (label.is_empty()) { + set_error(RTR("Invalid macro name."), line); + return; + } + + p_tokenizer->skip_whitespace(); + if (!is_char_end(p_tokenizer->peek())) { + set_error(RTR("Invalid ifndef."), line); + return; + } + p_tokenizer->advance('\n'); + + bool success = !state->defines.has(label); + start_branch_condition(p_tokenizer, success); +} + +void ShaderPreprocessor::process_include(Tokenizer *p_tokenizer) { + const int line = p_tokenizer->get_line(); + + p_tokenizer->advance('"'); + String path = tokens_to_string(p_tokenizer->advance('"')); + path = path.substr(0, path.length() - 1); + p_tokenizer->skip_whitespace(); + + if (path.is_empty() || !is_char_end(p_tokenizer->peek())) { + set_error(RTR("Invalid path."), line); + return; + } + + Ref<Resource> res = ResourceLoader::load(path); + if (res.is_null()) { + set_error(RTR("Shader include load failed. Does the shader include exist? Is there a cyclic dependency?"), line); + return; + } + + Ref<ShaderInclude> shader_inc = Object::cast_to<ShaderInclude>(*res); + if (shader_inc.is_null()) { + set_error(RTR("Shader include resource type is wrong."), line); + return; + } + + String included = shader_inc->get_code(); + if (!included.is_empty()) { + uint64_t code_hash = included.hash64(); + if (state->cyclic_include_hashes.find(code_hash)) { + set_error(RTR("Cyclic include found."), line); + return; + } + } + + const String real_path = shader_inc->get_path(); + if (state->includes.has(real_path)) { + // Already included, skip. + // This is a valid check because 2 separate include paths could use some + // of the same shared functions from a common shader include. + return; + } + + // Mark as included. + state->includes.insert(real_path); + + state->include_depth++; + if (state->include_depth > 25) { + set_error(RTR("Shader max include depth exceeded."), line); + return; + } + + String old_include = state->current_include; + state->current_include = real_path; + ShaderPreprocessor processor(included); + + int prev_condition_depth = state->condition_depth; + state->condition_depth = 0; + + String result; + processor.preprocess(state, result); + add_to_output(result); + + // Reset to last include if there are no errors. We want to use this as context. + if (state->error.is_empty()) { + state->current_include = old_include; + } else { + return; + } + + state->include_depth--; + state->condition_depth = prev_condition_depth; +} + +void ShaderPreprocessor::process_pragma(Tokenizer *p_tokenizer) { + const int line = p_tokenizer->get_line(); + + bool is_cursor; + const String label = p_tokenizer->get_identifier(&is_cursor); + if (is_cursor) { + state->completion_type = COMPLETION_TYPE_PRAGMA; + } + + if (label.is_empty()) { + set_error(RTR("Invalid pragma directive."), line); + return; + } + + // Rxplicitly handle pragma values here. + // If more pragma options are created, then refactor into a more defined structure. + if (label == "disable_preprocessor") { + state->disabled = true; + } else { + set_error(RTR("Invalid pragma directive."), line); + return; + } + + p_tokenizer->advance('\n'); +} + +void ShaderPreprocessor::process_undef(Tokenizer *p_tokenizer) { + const int line = p_tokenizer->get_line(); + const String label = p_tokenizer->get_identifier(); + if (label.is_empty() || !state->defines.has(label)) { + set_error(RTR("Invalid name."), line); + return; + } + + p_tokenizer->skip_whitespace(); + if (!is_char_end(p_tokenizer->peek())) { + set_error(RTR("Invalid undef."), line); + return; + } + + memdelete(state->defines[label]); + state->defines.erase(label); +} + +void ShaderPreprocessor::start_branch_condition(Tokenizer *p_tokenizer, bool p_success) { + state->condition_depth++; + + if (p_success) { + state->skip_stack_else.push_back(true); + } else { + SkippedCondition *cond = memnew(SkippedCondition()); + cond->start_line = p_tokenizer->get_line(); + state->skipped_conditions[state->current_include].push_back(cond); + + Vector<String> ends; + ends.push_back("else"); + ends.push_back("endif"); + if (next_directive(p_tokenizer, ends) == "else") { + state->skip_stack_else.push_back(false); + } else { + state->skip_stack_else.push_back(true); + } + } +} + +void ShaderPreprocessor::expand_output_macros(int p_start, int p_line_number) { + String line = vector_to_string(output, p_start, output.size()); + + Error error = expand_macros(line, p_line_number - 1, line); // We are already on next line, so -1. + if (error != OK) { + return; + } + + output.resize(p_start); + + add_to_output(line); +} + +Error ShaderPreprocessor::expand_macros(const String &p_string, int p_line, String &r_expanded) { + Vector<Pair<String, Define *>> active_defines; + active_defines.resize(state->defines.size()); + int index = 0; + for (const RBMap<String, Define *>::Element *E = state->defines.front(); E; E = E->next()) { + active_defines.set(index++, Pair<String, Define *>(E->key(), E->get())); + } + + return expand_macros(p_string, p_line, active_defines, r_expanded); +} + +Error ShaderPreprocessor::expand_macros(const String &p_string, int p_line, Vector<Pair<String, Define *>> p_defines, String &r_expanded) { + r_expanded = p_string; + // When expanding macros we must only evaluate them once. + // Later we continue expanding but with the already + // evaluated macros removed. + for (int i = 0; i < p_defines.size(); i++) { + Pair<String, Define *> define_pair = p_defines[i]; + + Error error = expand_macros_once(r_expanded, p_line, define_pair, r_expanded); + if (error != OK) { + return error; + } + + // Remove expanded macro and recursively replace remaining. + p_defines.remove_at(i); + return expand_macros(r_expanded, p_line, p_defines, r_expanded); + } + + return OK; +} + +Error ShaderPreprocessor::expand_macros_once(const String &p_line, int p_line_number, Pair<String, Define *> p_define_pair, String &r_expanded) { + String result = p_line; + + const String &key = p_define_pair.first; + const Define *define = p_define_pair.second; + + int index_start = 0; + int index = 0; + while (find_match(result, key, index, index_start)) { + String body = define->body; + if (define->arguments.size() > 0) { + // Complex macro with arguments. + int args_start = index + key.length(); + int args_end = p_line.find(")", args_start); + if (args_start == -1 || args_end == -1) { + set_error(RTR("Missing macro argument parenthesis."), p_line_number); + return FAILED; + } + + String values = result.substr(args_start + 1, args_end - (args_start + 1)); + Vector<String> args = values.split(","); + if (args.size() != define->arguments.size()) { + set_error(RTR("Invalid macro argument count."), p_line_number); + return FAILED; + } + + // Insert macro arguments into the body. + for (int i = 0; i < args.size(); i++) { + String arg_name = define->arguments[i]; + int arg_index_start = 0; + int arg_index = 0; + while (find_match(body, arg_name, arg_index, arg_index_start)) { + body = body.substr(0, arg_index) + args[i] + body.substr(arg_index + arg_name.length(), body.length() - (arg_index + arg_name.length())); + // Manually reset arg_index_start to where the arg value of the define finishes. + // This ensures we don't skip the other args of this macro in the string. + arg_index_start = arg_index + args[i].length() + 1; + } + } + + result = result.substr(0, index) + " " + body + " " + result.substr(args_end + 1, result.length()); + } else { + result = result.substr(0, index) + body + result.substr(index + key.length(), result.length() - (index + key.length())); + // Manually reset index_start to where the body value of the define finishes. + // This ensures we don't skip another instance of this macro in the string. + index_start = index + body.length() + 1; + break; + } + } + r_expanded = result; + return OK; +} + +bool ShaderPreprocessor::find_match(const String &p_string, const String &p_value, int &r_index, int &r_index_start) { + // Looks for value in string and then determines if the boundaries + // are non-word characters. This method semi-emulates \b in regex. + r_index = p_string.find(p_value, r_index_start); + while (r_index > -1) { + if (r_index > 0) { + if (is_char_word(p_string[r_index - 1])) { + r_index_start = r_index + 1; + r_index = p_string.find(p_value, r_index_start); + continue; + } + } + + if (r_index + p_value.length() < p_string.length()) { + if (is_char_word(p_string[r_index + p_value.length()])) { + r_index_start = r_index + p_value.length() + 1; + r_index = p_string.find(p_value, r_index_start); + continue; + } + } + + // Return and shift index start automatically for next call. + r_index_start = r_index + p_value.length() + 1; + return true; + } + + return false; +} + +String ShaderPreprocessor::next_directive(Tokenizer *p_tokenizer, const Vector<String> &p_directives) { + const int line = p_tokenizer->get_line(); + int nesting = 0; + + while (true) { + p_tokenizer->advance('#'); + + String id = p_tokenizer->peek_identifier(); + if (id.is_empty()) { + break; + } + + if (nesting == 0) { + for (int i = 0; i < p_directives.size(); i++) { + if (p_directives[i] == id) { + p_tokenizer->backtrack('#'); + return id; + } + } + } + + if (id == "ifdef" || id == "ifndef" || id == "if") { + nesting++; + } else if (id == "endif") { + nesting--; + } + } + + set_error(RTR("Can't find matching branch directive."), line); + return ""; +} + +void ShaderPreprocessor::add_to_output(const String &p_str) { + for (int i = 0; i < p_str.length(); i++) { + output.push_back(p_str[i]); + } +} + +void ShaderPreprocessor::set_error(const String &p_error, int p_line) { + if (state->error.is_empty()) { + state->error = p_error; + state->error_line = p_line + 1; + } +} + +bool ShaderPreprocessor::check_directive_before_type(Tokenizer *p_tokenizer, const String &p_directive) { + if (p_tokenizer->get_index() < state->shader_type_pos) { + set_error(vformat(RTR("`#%s` may not be defined before `shader_type`."), p_directive), p_tokenizer->get_line()); + return false; + } + return true; +} + +ShaderPreprocessor::State *ShaderPreprocessor::create_state() { + State *new_state = memnew(State); + + String platform = OS::get_singleton()->get_name().replace(" ", "_").to_upper(); + new_state->defines[platform] = create_define("true"); + + Engine *engine = Engine::get_singleton(); + new_state->defines["EDITOR"] = create_define(engine->is_editor_hint() ? "true" : "false"); + + return new_state; +} + +ShaderPreprocessor::Define *ShaderPreprocessor::create_define(const String &p_body) { + ShaderPreprocessor::Define *define = memnew(Define); + define->body = p_body; + return define; +} + +void ShaderPreprocessor::clear() { + if (state_owner && state != nullptr) { + for (const RBMap<String, Define *>::Element *E = state->defines.front(); E; E = E->next()) { + memdelete(E->get()); + } + + for (const RBMap<String, Vector<SkippedCondition *>>::Element *E = state->skipped_conditions.front(); E; E = E->next()) { + for (SkippedCondition *condition : E->get()) { + memdelete(condition); + } + } + + memdelete(state); + } + state_owner = false; + state = nullptr; +} + +Error ShaderPreprocessor::preprocess(State *p_state, String &r_result) { + clear(); + + output.clear(); + + state = p_state; + if (state == nullptr) { + state = create_state(); + state_owner = true; + } + + CommentRemover remover(code); + String stripped = remover.strip(); + String error = remover.get_error(); + if (!error.is_empty()) { + set_error(error, remover.get_error_line()); + return FAILED; + } + + // Track code hashes to prevent cyclic include. + uint64_t code_hash = code.hash64(); + state->cyclic_include_hashes.push_back(code_hash); + + Tokenizer p_tokenizer(stripped); + int last_size = 0; + bool has_symbols_before_directive = false; + + while (true) { + const Token &t = p_tokenizer.get_token(); + + if (t.text == 0) { + break; + } + + if (state->disabled) { + // Preprocessor was disabled. + // Read the rest of the file into the output. + output.push_back(t.text); + continue; + } else { + // Add autogenerated tokens. + Vector<Token> generated; + p_tokenizer.get_and_clear_generated(&generated); + for (int i = 0; i < generated.size(); i++) { + output.push_back(generated[i].text); + } + } + + if (t.text == '#') { + if (has_symbols_before_directive) { + set_error(RTR("Invalid symbols placed before directive."), p_tokenizer.get_line()); + state->cyclic_include_hashes.erase(code_hash); // Remove this hash. + return FAILED; + } + process_directive(&p_tokenizer); + } else { + if (is_char_end(t.text)) { + expand_output_macros(last_size, p_tokenizer.get_line()); + last_size = output.size(); + has_symbols_before_directive = false; + } else if (!is_char_space(t.text)) { + has_symbols_before_directive = true; + } + output.push_back(t.text); + } + + if (!state->error.is_empty()) { + state->cyclic_include_hashes.erase(code_hash); // Remove this hash. + return FAILED; + } + } + state->cyclic_include_hashes.erase(code_hash); // Remove this hash. + + if (!state->disabled) { + if (state->condition_depth != 0) { + set_error(RTR("Unmatched conditional statement."), p_tokenizer.line); + return FAILED; + } + + expand_output_macros(last_size, p_tokenizer.get_line()); + } + + r_result = vector_to_string(output); + + return OK; +} + +Error ShaderPreprocessor::preprocess(String &r_result) { + return preprocess(nullptr, r_result); +} + +ShaderPreprocessor::State *ShaderPreprocessor::get_state() { + return state; +} + +void ShaderPreprocessor::get_keyword_list(List<String> *r_keywords, bool p_include_shader_keywords) { + r_keywords->push_back("define"); + if (p_include_shader_keywords) { + r_keywords->push_back("else"); + } + r_keywords->push_back("endif"); + if (p_include_shader_keywords) { + r_keywords->push_back("if"); + } + r_keywords->push_back("ifdef"); + r_keywords->push_back("ifndef"); + r_keywords->push_back("include"); + r_keywords->push_back("pragma"); + r_keywords->push_back("undef"); +} + +void ShaderPreprocessor::get_pragma_list(List<String> *r_pragmas) { + r_pragmas->push_back("disable_preprocessor"); +} + +ShaderPreprocessor::ShaderPreprocessor(const String &p_code) : + code(p_code) { +} + +ShaderPreprocessor::~ShaderPreprocessor() { + clear(); +} diff --git a/servers/rendering/shader_preprocessor.h b/servers/rendering/shader_preprocessor.h new file mode 100644 index 0000000000..1cbab8e36f --- /dev/null +++ b/servers/rendering/shader_preprocessor.h @@ -0,0 +1,196 @@ +/*************************************************************************/ +/* shader_preprocessor.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef SHADER_PREPROCESSOR_H +#define SHADER_PREPROCESSOR_H + +#include "core/string/ustring.h" +#include "core/templates/list.h" +#include "core/templates/local_vector.h" +#include "core/templates/rb_map.h" +#include "core/templates/rb_set.h" +#include "core/typedefs.h" + +#include "core/io/resource_loader.h" +#include "core/os/os.h" +#include "scene/resources/shader.h" +#include "scene/resources/shader_include.h" + +class ShaderPreprocessor { +private: + struct Token { + char32_t text; + int line; + + Token(); + Token(char32_t p_text, int p_line); + }; + + // The real preprocessor that understands basic shader and preprocessor language syntax. + class Tokenizer { + public: + String code; + int line; + int index; + int size; + Vector<Token> generated; + + private: + void add_generated(const Token &p_t); + char32_t next(); + + public: + int get_line() const; + int get_index() const; + char32_t peek(); + + void get_and_clear_generated(Vector<Token> *r_out); + void backtrack(char32_t p_what); + LocalVector<Token> advance(char32_t p_what); + void skip_whitespace(); + String get_identifier(bool *r_is_cursor = nullptr, bool p_started = false); + String peek_identifier(); + Token get_token(); + + Tokenizer(const String &p_code); + }; + + class CommentRemover { + private: + LocalVector<char32_t> stripped; + String code; + int index; + int line; + int comment_line_open; + int comments_open; + int strings_open; + + public: + String get_error() const; + int get_error_line() const; + char32_t peek() const; + + bool advance(char32_t p_what); + String strip(); + + CommentRemover(const String &p_code); + }; + + struct Define { + Vector<String> arguments; + String body; + }; + + struct SkippedCondition { + int start_line = -1; + int end_line = -1; + }; + +public: + enum CompletionType { + COMPLETION_TYPE_NONE, + COMPLETION_TYPE_DIRECTIVE, + COMPLETION_TYPE_PRAGMA_DIRECTIVE, + COMPLETION_TYPE_PRAGMA, + }; + + struct State { + RBMap<String, Define *> defines; + Vector<bool> skip_stack_else; + int condition_depth = 0; + RBSet<String> includes; + List<uint64_t> cyclic_include_hashes; // Holds code hash of includes. + int include_depth = 0; + String current_include; + String current_shader_type; + int shader_type_pos = -1; + String error; + int error_line = -1; + RBMap<String, Vector<SkippedCondition *>> skipped_conditions; + bool disabled = false; + CompletionType completion_type = COMPLETION_TYPE_NONE; + }; + +private: + String code; + LocalVector<char32_t> output; + State *state = nullptr; + bool state_owner = false; + +private: + static bool is_char_word(char32_t p_char); + static bool is_char_space(char32_t p_char); + static bool is_char_end(char32_t p_char); + static String vector_to_string(const LocalVector<char32_t> &p_v, int p_start = 0, int p_end = -1); + static String tokens_to_string(const LocalVector<Token> &p_tokens); + + void process_directive(Tokenizer *p_tokenizer); + void process_define(Tokenizer *p_tokenizer); + void process_else(Tokenizer *p_tokenizer); + void process_endif(Tokenizer *p_tokenizer); + void process_if(Tokenizer *p_tokenizer); + void process_ifdef(Tokenizer *p_tokenizer); + void process_ifndef(Tokenizer *p_tokenizer); + void process_include(Tokenizer *p_tokenizer); + void process_pragma(Tokenizer *p_tokenizer); + void process_undef(Tokenizer *p_tokenizer); + + void start_branch_condition(Tokenizer *p_tokenizer, bool p_success); + + void expand_output_macros(int p_start, int p_line); + Error expand_macros(const String &p_string, int p_line, String &r_result); + Error expand_macros(const String &p_string, int p_line, Vector<Pair<String, Define *>> p_defines, String &r_result); + Error expand_macros_once(const String &p_line, int p_line_number, Pair<String, Define *> p_define_pair, String &r_expanded); + bool find_match(const String &p_string, const String &p_value, int &r_index, int &r_index_start); + + String next_directive(Tokenizer *p_tokenizer, const Vector<String> &p_directives); + void add_to_output(const String &p_str); + void set_error(const String &p_error, int p_line); + bool check_directive_before_type(Tokenizer *p_tokenizer, const String &p_directive); + + static State *create_state(); + static Define *create_define(const String &p_body); + + void clear(); + +public: + Error preprocess(State *p_state, String &r_result); + Error preprocess(String &r_result); + + State *get_state(); + + static void get_keyword_list(List<String> *r_keywords, bool p_include_shader_keywords = false); + static void get_pragma_list(List<String> *r_pragmas); + + ShaderPreprocessor(const String &p_code); + ~ShaderPreprocessor(); +}; + +#endif // SHADER_PREPROCESSOR_H |