diff options
Diffstat (limited to 'modules')
33 files changed, 2075 insertions, 122 deletions
diff --git a/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h b/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h index 5af46bc752..66fd27b449 100644 --- a/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h +++ b/modules/gltf/editor/editor_scene_exporter_gltf_plugin.h @@ -36,6 +36,8 @@ #include "editor/editor_plugin.h" #include "editor_scene_importer_gltf.h" +class EditorFileDialog; + class SceneExporterGLTFPlugin : public EditorPlugin { GDCLASS(SceneExporterGLTFPlugin, EditorPlugin); diff --git a/modules/gridmap/editor/grid_map_editor_plugin.h b/modules/gridmap/editor/grid_map_editor_plugin.h index 6fd38d9445..91f14690ca 100644 --- a/modules/gridmap/editor/grid_map_editor_plugin.h +++ b/modules/gridmap/editor/grid_map_editor_plugin.h @@ -35,11 +35,14 @@ #include "../grid_map.h" #include "editor/editor_plugin.h" +#include "scene/gui/box_container.h" #include "scene/gui/item_list.h" #include "scene/gui/slider.h" #include "scene/gui/spin_box.h" +class ConfirmationDialog; class EditorUndoRedoManager; +class MenuButton; class Node3DEditorPlugin; class GridMapEditor : public VBoxContainer { diff --git a/modules/lightmapper_rd/register_types.cpp b/modules/lightmapper_rd/register_types.cpp index 0e0330c1a1..ed223e1faa 100644 --- a/modules/lightmapper_rd/register_types.cpp +++ b/modules/lightmapper_rd/register_types.cpp @@ -57,6 +57,7 @@ void initialize_lightmapper_rd_module(ModuleInitializationLevel p_level) { GLOBAL_DEF("rendering/lightmapping/bake_quality/high_quality_probe_ray_count", 512); GLOBAL_DEF("rendering/lightmapping/bake_quality/ultra_quality_probe_ray_count", 2048); GLOBAL_DEF("rendering/lightmapping/bake_performance/max_rays_per_probe_pass", 64); + GLOBAL_DEF("rendering/lightmapping/primitive_meshes/texel_size", 0.2); #ifndef _3D_DISABLED GDREGISTER_CLASS(LightmapperRD); Lightmapper::create_gpu = create_lightmapper_rd; diff --git a/modules/multiplayer/editor/editor_network_profiler.cpp b/modules/multiplayer/editor/editor_network_profiler.cpp new file mode 100644 index 0000000000..a7e5b80b66 --- /dev/null +++ b/modules/multiplayer/editor/editor_network_profiler.cpp @@ -0,0 +1,197 @@ +/*************************************************************************/ +/* editor_network_profiler.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 "editor_network_profiler.h" + +#include "core/os/os.h" +#include "editor/editor_scale.h" +#include "editor/editor_settings.h" + +void EditorNetworkProfiler::_bind_methods() { + ADD_SIGNAL(MethodInfo("enable_profiling", PropertyInfo(Variant::BOOL, "enable"))); +} + +void EditorNetworkProfiler::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: { + activate->set_icon(get_theme_icon(SNAME("Play"), SNAME("EditorIcons"))); + clear_button->set_icon(get_theme_icon(SNAME("Clear"), SNAME("EditorIcons"))); + incoming_bandwidth_text->set_right_icon(get_theme_icon(SNAME("ArrowDown"), SNAME("EditorIcons"))); + outgoing_bandwidth_text->set_right_icon(get_theme_icon(SNAME("ArrowUp"), SNAME("EditorIcons"))); + + // This needs to be done here to set the faded color when the profiler is first opened + incoming_bandwidth_text->add_theme_color_override("font_uneditable_color", get_theme_color(SNAME("font_color"), SNAME("Editor")) * Color(1, 1, 1, 0.5)); + outgoing_bandwidth_text->add_theme_color_override("font_uneditable_color", get_theme_color(SNAME("font_color"), SNAME("Editor")) * Color(1, 1, 1, 0.5)); + } break; + } +} + +void EditorNetworkProfiler::_update_frame() { + counters_display->clear(); + + TreeItem *root = counters_display->create_item(); + + for (const KeyValue<ObjectID, RPCNodeInfo> &E : nodes_data) { + TreeItem *node = counters_display->create_item(root); + + for (int j = 0; j < counters_display->get_columns(); ++j) { + node->set_text_alignment(j, j > 0 ? HORIZONTAL_ALIGNMENT_RIGHT : HORIZONTAL_ALIGNMENT_LEFT); + } + + node->set_text(0, E.value.node_path); + node->set_text(1, E.value.incoming_rpc == 0 ? "-" : itos(E.value.incoming_rpc)); + node->set_text(2, E.value.outgoing_rpc == 0 ? "-" : itos(E.value.outgoing_rpc)); + } +} + +void EditorNetworkProfiler::_activate_pressed() { + if (activate->is_pressed()) { + activate->set_icon(get_theme_icon(SNAME("Stop"), SNAME("EditorIcons"))); + activate->set_text(TTR("Stop")); + } else { + activate->set_icon(get_theme_icon(SNAME("Play"), SNAME("EditorIcons"))); + activate->set_text(TTR("Start")); + } + emit_signal(SNAME("enable_profiling"), activate->is_pressed()); +} + +void EditorNetworkProfiler::_clear_pressed() { + nodes_data.clear(); + set_bandwidth(0, 0); + if (frame_delay->is_stopped()) { + frame_delay->set_wait_time(0.1); + frame_delay->start(); + } +} + +void EditorNetworkProfiler::add_node_frame_data(const RPCNodeInfo p_frame) { + if (!nodes_data.has(p_frame.node)) { + nodes_data.insert(p_frame.node, p_frame); + } else { + nodes_data[p_frame.node].incoming_rpc += p_frame.incoming_rpc; + nodes_data[p_frame.node].outgoing_rpc += p_frame.outgoing_rpc; + } + + if (frame_delay->is_stopped()) { + frame_delay->set_wait_time(0.1); + frame_delay->start(); + } +} + +void EditorNetworkProfiler::set_bandwidth(int p_incoming, int p_outgoing) { + incoming_bandwidth_text->set_text(vformat(TTR("%s/s"), String::humanize_size(p_incoming))); + outgoing_bandwidth_text->set_text(vformat(TTR("%s/s"), String::humanize_size(p_outgoing))); + + // Make labels more prominent when the bandwidth is greater than 0 to attract user attention + incoming_bandwidth_text->add_theme_color_override( + "font_uneditable_color", + get_theme_color(SNAME("font_color"), SNAME("Editor")) * Color(1, 1, 1, p_incoming > 0 ? 1 : 0.5)); + outgoing_bandwidth_text->add_theme_color_override( + "font_uneditable_color", + get_theme_color(SNAME("font_color"), SNAME("Editor")) * Color(1, 1, 1, p_outgoing > 0 ? 1 : 0.5)); +} + +bool EditorNetworkProfiler::is_profiling() { + return activate->is_pressed(); +} + +EditorNetworkProfiler::EditorNetworkProfiler() { + HBoxContainer *hb = memnew(HBoxContainer); + hb->add_theme_constant_override("separation", 8 * EDSCALE); + add_child(hb); + + activate = memnew(Button); + activate->set_toggle_mode(true); + activate->set_text(TTR("Start")); + activate->connect("pressed", callable_mp(this, &EditorNetworkProfiler::_activate_pressed)); + hb->add_child(activate); + + clear_button = memnew(Button); + clear_button->set_text(TTR("Clear")); + clear_button->connect("pressed", callable_mp(this, &EditorNetworkProfiler::_clear_pressed)); + hb->add_child(clear_button); + + hb->add_spacer(); + + Label *lb = memnew(Label); + lb->set_text(TTR("Down")); + hb->add_child(lb); + + incoming_bandwidth_text = memnew(LineEdit); + incoming_bandwidth_text->set_editable(false); + incoming_bandwidth_text->set_custom_minimum_size(Size2(120, 0) * EDSCALE); + incoming_bandwidth_text->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + hb->add_child(incoming_bandwidth_text); + + Control *down_up_spacer = memnew(Control); + down_up_spacer->set_custom_minimum_size(Size2(30, 0) * EDSCALE); + hb->add_child(down_up_spacer); + + lb = memnew(Label); + lb->set_text(TTR("Up")); + hb->add_child(lb); + + outgoing_bandwidth_text = memnew(LineEdit); + outgoing_bandwidth_text->set_editable(false); + outgoing_bandwidth_text->set_custom_minimum_size(Size2(120, 0) * EDSCALE); + outgoing_bandwidth_text->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + hb->add_child(outgoing_bandwidth_text); + + // Set initial texts in the incoming/outgoing bandwidth labels + set_bandwidth(0, 0); + + counters_display = memnew(Tree); + counters_display->set_custom_minimum_size(Size2(300, 0) * EDSCALE); + counters_display->set_v_size_flags(SIZE_EXPAND_FILL); + counters_display->set_hide_folding(true); + counters_display->set_hide_root(true); + counters_display->set_columns(3); + counters_display->set_column_titles_visible(true); + counters_display->set_column_title(0, TTR("Node")); + counters_display->set_column_expand(0, true); + counters_display->set_column_clip_content(0, true); + counters_display->set_column_custom_minimum_width(0, 60 * EDSCALE); + counters_display->set_column_title(1, TTR("Incoming RPC")); + counters_display->set_column_expand(1, false); + counters_display->set_column_clip_content(1, true); + counters_display->set_column_custom_minimum_width(1, 120 * EDSCALE); + counters_display->set_column_title(2, TTR("Outgoing RPC")); + counters_display->set_column_expand(2, false); + counters_display->set_column_clip_content(2, true); + counters_display->set_column_custom_minimum_width(2, 120 * EDSCALE); + add_child(counters_display); + + frame_delay = memnew(Timer); + frame_delay->set_wait_time(0.1); + frame_delay->set_one_shot(true); + add_child(frame_delay); + frame_delay->connect("timeout", callable_mp(this, &EditorNetworkProfiler::_update_frame)); +} diff --git a/modules/multiplayer/editor/editor_network_profiler.h b/modules/multiplayer/editor/editor_network_profiler.h new file mode 100644 index 0000000000..98d12e3c0a --- /dev/null +++ b/modules/multiplayer/editor/editor_network_profiler.h @@ -0,0 +1,76 @@ +/*************************************************************************/ +/* editor_network_profiler.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 EDITOR_NETWORK_PROFILER_H +#define EDITOR_NETWORK_PROFILER_H + +#include "scene/debugger/scene_debugger.h" +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/label.h" +#include "scene/gui/split_container.h" +#include "scene/gui/tree.h" + +#include "../multiplayer_debugger.h" + +class EditorNetworkProfiler : public VBoxContainer { + GDCLASS(EditorNetworkProfiler, VBoxContainer) + +private: + using RPCNodeInfo = MultiplayerDebugger::RPCNodeInfo; + + Button *activate = nullptr; + Button *clear_button = nullptr; + Tree *counters_display = nullptr; + LineEdit *incoming_bandwidth_text = nullptr; + LineEdit *outgoing_bandwidth_text = nullptr; + + Timer *frame_delay = nullptr; + + HashMap<ObjectID, RPCNodeInfo> nodes_data; + + void _update_frame(); + + void _activate_pressed(); + void _clear_pressed(); + +protected: + void _notification(int p_what); + static void _bind_methods(); + +public: + void add_node_frame_data(const RPCNodeInfo p_frame); + void set_bandwidth(int p_incoming, int p_outgoing); + bool is_profiling(); + + EditorNetworkProfiler(); +}; + +#endif // EDITOR_NETWORK_PROFILER_H diff --git a/modules/multiplayer/editor/multiplayer_editor_plugin.cpp b/modules/multiplayer/editor/multiplayer_editor_plugin.cpp new file mode 100644 index 0000000000..00b1537827 --- /dev/null +++ b/modules/multiplayer/editor/multiplayer_editor_plugin.cpp @@ -0,0 +1,140 @@ +/*************************************************************************/ +/* multiplayer_editor_plugin.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 "multiplayer_editor_plugin.h" + +#include "../multiplayer_synchronizer.h" +#include "editor_network_profiler.h" +#include "replication_editor.h" + +#include "editor/editor_node.h" + +bool MultiplayerEditorDebugger::has_capture(const String &p_capture) const { + return p_capture == "multiplayer"; +} + +bool MultiplayerEditorDebugger::capture(const String &p_message, const Array &p_data, int p_session) { + ERR_FAIL_COND_V(!profilers.has(p_session), false); + EditorNetworkProfiler *profiler = profilers[p_session]; + if (p_message == "multiplayer:rpc") { + MultiplayerDebugger::RPCFrame frame; + frame.deserialize(p_data); + for (int i = 0; i < frame.infos.size(); i++) { + profiler->add_node_frame_data(frame.infos[i]); + } + return true; + + } else if (p_message == "multiplayer:bandwidth") { + ERR_FAIL_COND_V(p_data.size() < 2, false); + profiler->set_bandwidth(p_data[0], p_data[1]); + return true; + } + return false; +} + +void MultiplayerEditorDebugger::_profiler_activate(bool p_enable, int p_session_id) { + Ref<EditorDebuggerSession> session = get_session(p_session_id); + ERR_FAIL_COND(session.is_null()); + session->toggle_profiler("multiplayer", p_enable); + session->toggle_profiler("rpc", p_enable); +} + +void MultiplayerEditorDebugger::setup_session(int p_session_id) { + Ref<EditorDebuggerSession> session = get_session(p_session_id); + ERR_FAIL_COND(session.is_null()); + EditorNetworkProfiler *profiler = memnew(EditorNetworkProfiler); + profiler->connect("enable_profiling", callable_mp(this, &MultiplayerEditorDebugger::_profiler_activate).bind(p_session_id)); + profiler->set_name(TTR("Network Profiler")); + session->add_session_tab(profiler); + profilers[p_session_id] = profiler; +} + +MultiplayerEditorPlugin::MultiplayerEditorPlugin() { + repl_editor = memnew(ReplicationEditor); + button = EditorNode::get_singleton()->add_bottom_panel_item(TTR("Replication"), repl_editor); + button->hide(); + repl_editor->get_pin()->connect("pressed", callable_mp(this, &MultiplayerEditorPlugin::_pinned)); + debugger.instantiate(); +} + +MultiplayerEditorPlugin::~MultiplayerEditorPlugin() { +} + +void MultiplayerEditorPlugin::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + get_tree()->connect("node_removed", callable_mp(this, &MultiplayerEditorPlugin::_node_removed)); + add_debugger_plugin(debugger); + } break; + case NOTIFICATION_EXIT_TREE: { + remove_debugger_plugin(debugger); + } + } +} + +void MultiplayerEditorPlugin::_node_removed(Node *p_node) { + if (p_node && p_node == repl_editor->get_current()) { + repl_editor->edit(nullptr); + if (repl_editor->is_visible_in_tree()) { + EditorNode::get_singleton()->hide_bottom_panel(); + } + button->hide(); + repl_editor->get_pin()->set_pressed(false); + } +} + +void MultiplayerEditorPlugin::_pinned() { + if (!repl_editor->get_pin()->is_pressed()) { + if (repl_editor->is_visible_in_tree()) { + EditorNode::get_singleton()->hide_bottom_panel(); + } + button->hide(); + } +} + +void MultiplayerEditorPlugin::edit(Object *p_object) { + repl_editor->edit(Object::cast_to<MultiplayerSynchronizer>(p_object)); +} + +bool MultiplayerEditorPlugin::handles(Object *p_object) const { + return p_object->is_class("MultiplayerSynchronizer"); +} + +void MultiplayerEditorPlugin::make_visible(bool p_visible) { + if (p_visible) { + button->show(); + EditorNode::get_singleton()->make_bottom_panel_item_visible(repl_editor); + } else if (!repl_editor->get_pin()->is_pressed()) { + if (repl_editor->is_visible_in_tree()) { + EditorNode::get_singleton()->hide_bottom_panel(); + } + button->hide(); + } +} diff --git a/modules/multiplayer/editor/multiplayer_editor_plugin.h b/modules/multiplayer/editor/multiplayer_editor_plugin.h new file mode 100644 index 0000000000..6d1514cdb1 --- /dev/null +++ b/modules/multiplayer/editor/multiplayer_editor_plugin.h @@ -0,0 +1,81 @@ +/*************************************************************************/ +/* multiplayer_editor_plugin.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 MULTIPLAYER_EDITOR_PLUGIN_H +#define MULTIPLAYER_EDITOR_PLUGIN_H + +#include "editor/editor_plugin.h" + +#include "editor/plugins/editor_debugger_plugin.h" + +class EditorNetworkProfiler; +class MultiplayerEditorDebugger : public EditorDebuggerPlugin { + GDCLASS(MultiplayerEditorDebugger, EditorDebuggerPlugin); + +private: + HashMap<int, EditorNetworkProfiler *> profilers; + + void _profiler_activate(bool p_enable, int p_session_id); + +public: + virtual bool has_capture(const String &p_capture) const override; + virtual bool capture(const String &p_message, const Array &p_data, int p_index) override; + virtual void setup_session(int p_session_id) override; + + MultiplayerEditorDebugger() {} +}; + +class ReplicationEditor; + +class MultiplayerEditorPlugin : public EditorPlugin { + GDCLASS(MultiplayerEditorPlugin, EditorPlugin); + +private: + Button *button = nullptr; + ReplicationEditor *repl_editor = nullptr; + Ref<MultiplayerEditorDebugger> debugger; + + void _node_removed(Node *p_node); + + void _pinned(); + +protected: + void _notification(int p_what); + +public: + virtual void edit(Object *p_object) override; + virtual bool handles(Object *p_object) const override; + virtual void make_visible(bool p_visible) override; + + MultiplayerEditorPlugin(); + ~MultiplayerEditorPlugin(); +}; + +#endif // MULTIPLAYER_EDITOR_PLUGIN_H diff --git a/modules/multiplayer/editor/replication_editor_plugin.cpp b/modules/multiplayer/editor/replication_editor.cpp index aee5f5b483..4ba9cd14f0 100644 --- a/modules/multiplayer/editor/replication_editor_plugin.cpp +++ b/modules/multiplayer/editor/replication_editor.cpp @@ -1,5 +1,5 @@ /*************************************************************************/ -/* replication_editor_plugin.cpp */ +/* replication_editor.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,15 +28,17 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "replication_editor_plugin.h" +#include "replication_editor.h" + +#include "../multiplayer_synchronizer.h" #include "editor/editor_node.h" #include "editor/editor_scale.h" #include "editor/editor_settings.h" #include "editor/editor_undo_redo_manager.h" #include "editor/inspector_dock.h" +#include "editor/property_selector.h" #include "editor/scene_tree_editor.h" -#include "modules/multiplayer/multiplayer_synchronizer.h" #include "scene/gui/dialogs.h" #include "scene/gui/separator.h" #include "scene/gui/tree.h" @@ -140,7 +142,7 @@ void ReplicationEditor::_add_sync_property(String p_path) { return; } - Ref<EditorUndoRedoManager> undo_redo = EditorNode::get_singleton()->get_undo_redo(); + Ref<EditorUndoRedoManager> &undo_redo = EditorNode::get_singleton()->get_undo_redo(); undo_redo->create_action(TTR("Add property to synchronizer")); if (config.is_null()) { @@ -492,62 +494,3 @@ void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn, item->set_checked(2, p_sync); item->set_editable(2, true); } - -/// ReplicationEditorPlugin -ReplicationEditorPlugin::ReplicationEditorPlugin() { - repl_editor = memnew(ReplicationEditor); - button = EditorNode::get_singleton()->add_bottom_panel_item(TTR("Replication"), repl_editor); - button->hide(); - repl_editor->get_pin()->connect("pressed", callable_mp(this, &ReplicationEditorPlugin::_pinned)); -} - -ReplicationEditorPlugin::~ReplicationEditorPlugin() { -} - -void ReplicationEditorPlugin::_notification(int p_what) { - switch (p_what) { - case NOTIFICATION_ENTER_TREE: { - get_tree()->connect("node_removed", callable_mp(this, &ReplicationEditorPlugin::_node_removed)); - } break; - } -} - -void ReplicationEditorPlugin::_node_removed(Node *p_node) { - if (p_node && p_node == repl_editor->get_current()) { - repl_editor->edit(nullptr); - if (repl_editor->is_visible_in_tree()) { - EditorNode::get_singleton()->hide_bottom_panel(); - } - button->hide(); - repl_editor->get_pin()->set_pressed(false); - } -} - -void ReplicationEditorPlugin::_pinned() { - if (!repl_editor->get_pin()->is_pressed()) { - if (repl_editor->is_visible_in_tree()) { - EditorNode::get_singleton()->hide_bottom_panel(); - } - button->hide(); - } -} - -void ReplicationEditorPlugin::edit(Object *p_object) { - repl_editor->edit(Object::cast_to<MultiplayerSynchronizer>(p_object)); -} - -bool ReplicationEditorPlugin::handles(Object *p_object) const { - return p_object->is_class("MultiplayerSynchronizer"); -} - -void ReplicationEditorPlugin::make_visible(bool p_visible) { - if (p_visible) { - button->show(); - EditorNode::get_singleton()->make_bottom_panel_item_visible(repl_editor); - } else if (!repl_editor->get_pin()->is_pressed()) { - if (repl_editor->is_visible_in_tree()) { - EditorNode::get_singleton()->hide_bottom_panel(); - } - button->hide(); - } -} diff --git a/modules/multiplayer/editor/replication_editor_plugin.h b/modules/multiplayer/editor/replication_editor.h index e60e49cc25..8a48e8dbe7 100644 --- a/modules/multiplayer/editor/replication_editor_plugin.h +++ b/modules/multiplayer/editor/replication_editor.h @@ -1,5 +1,5 @@ /*************************************************************************/ -/* replication_editor_plugin.h */ +/* replication_editor.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,21 +28,21 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef REPLICATION_EDITOR_PLUGIN_H -#define REPLICATION_EDITOR_PLUGIN_H +#ifndef REPLICATION_EDITOR_H +#define REPLICATION_EDITOR_H #include "editor/editor_plugin.h" - -#include "editor/editor_spin_slider.h" -#include "editor/property_selector.h" - -#include "../scene_replication_config.h" +#include "modules/multiplayer/scene_replication_config.h" +#include "scene/gui/box_container.h" class ConfirmationDialog; class MultiplayerSynchronizer; -class SceneTreeDialog; +class AcceptDialog; +class LineEdit; class Tree; class TreeItem; +class PropertySelector; +class SceneTreeDialog; class ReplicationEditor : public VBoxContainer { GDCLASS(ReplicationEditor, VBoxContainer); @@ -105,27 +105,4 @@ public: ~ReplicationEditor() {} }; -class ReplicationEditorPlugin : public EditorPlugin { - GDCLASS(ReplicationEditorPlugin, EditorPlugin); - -private: - Button *button = nullptr; - ReplicationEditor *repl_editor = nullptr; - - void _node_removed(Node *p_node); - - void _pinned(); - -protected: - void _notification(int p_what); - -public: - virtual void edit(Object *p_object) override; - virtual bool handles(Object *p_object) const override; - virtual void make_visible(bool p_visible) override; - - ReplicationEditorPlugin(); - ~ReplicationEditorPlugin(); -}; - -#endif // REPLICATION_EDITOR_PLUGIN_H +#endif // REPLICATION_EDITOR_H diff --git a/modules/multiplayer/multiplayer_debugger.cpp b/modules/multiplayer/multiplayer_debugger.cpp new file mode 100644 index 0000000000..3d22af04dc --- /dev/null +++ b/modules/multiplayer/multiplayer_debugger.cpp @@ -0,0 +1,194 @@ +/*************************************************************************/ +/* multiplayer_debugger.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 "multiplayer_debugger.h" + +#include "core/debugger/engine_debugger.h" +#include "scene/main/node.h" + +List<Ref<EngineProfiler>> multiplayer_profilers; + +void MultiplayerDebugger::initialize() { + Ref<BandwidthProfiler> bandwidth; + bandwidth.instantiate(); + bandwidth->bind("multiplayer"); + multiplayer_profilers.push_back(bandwidth); + + Ref<RPCProfiler> rpc_profiler; + rpc_profiler.instantiate(); + rpc_profiler->bind("rpc"); + multiplayer_profilers.push_back(rpc_profiler); +} + +void MultiplayerDebugger::deinitialize() { + multiplayer_profilers.clear(); +} + +// BandwidthProfiler + +int MultiplayerDebugger::BandwidthProfiler::bandwidth_usage(const Vector<BandwidthFrame> &p_buffer, int p_pointer) { + ERR_FAIL_COND_V(p_buffer.size() == 0, 0); + int total_bandwidth = 0; + + uint64_t timestamp = OS::get_singleton()->get_ticks_msec(); + uint64_t final_timestamp = timestamp - 1000; + + int i = (p_pointer + p_buffer.size() - 1) % p_buffer.size(); + + while (i != p_pointer && p_buffer[i].packet_size > 0) { + if (p_buffer[i].timestamp < final_timestamp) { + return total_bandwidth; + } + total_bandwidth += p_buffer[i].packet_size; + i = (i + p_buffer.size() - 1) % p_buffer.size(); + } + + ERR_FAIL_COND_V_MSG(i == p_pointer, total_bandwidth, "Reached the end of the bandwidth profiler buffer, values might be inaccurate."); + return total_bandwidth; +} + +void MultiplayerDebugger::BandwidthProfiler::toggle(bool p_enable, const Array &p_opts) { + if (!p_enable) { + bandwidth_in.clear(); + bandwidth_out.clear(); + } else { + bandwidth_in_ptr = 0; + bandwidth_in.resize(16384); // ~128kB + for (int i = 0; i < bandwidth_in.size(); ++i) { + bandwidth_in.write[i].packet_size = -1; + } + bandwidth_out_ptr = 0; + bandwidth_out.resize(16384); // ~128kB + for (int i = 0; i < bandwidth_out.size(); ++i) { + bandwidth_out.write[i].packet_size = -1; + } + } +} + +void MultiplayerDebugger::BandwidthProfiler::add(const Array &p_data) { + ERR_FAIL_COND(p_data.size() < 3); + const String inout = p_data[0]; + int time = p_data[1]; + int size = p_data[2]; + if (inout == "in") { + bandwidth_in.write[bandwidth_in_ptr].timestamp = time; + bandwidth_in.write[bandwidth_in_ptr].packet_size = size; + bandwidth_in_ptr = (bandwidth_in_ptr + 1) % bandwidth_in.size(); + } else if (inout == "out") { + bandwidth_out.write[bandwidth_out_ptr].timestamp = time; + bandwidth_out.write[bandwidth_out_ptr].packet_size = size; + bandwidth_out_ptr = (bandwidth_out_ptr + 1) % bandwidth_out.size(); + } +} + +void MultiplayerDebugger::BandwidthProfiler::tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time) { + uint64_t pt = OS::get_singleton()->get_ticks_msec(); + if (pt - last_bandwidth_time > 200) { + last_bandwidth_time = pt; + int incoming_bandwidth = bandwidth_usage(bandwidth_in, bandwidth_in_ptr); + int outgoing_bandwidth = bandwidth_usage(bandwidth_out, bandwidth_out_ptr); + + Array arr; + arr.push_back(incoming_bandwidth); + arr.push_back(outgoing_bandwidth); + EngineDebugger::get_singleton()->send_message("multiplayer:bandwidth", arr); + } +} + +// RPCProfiler + +Array MultiplayerDebugger::RPCFrame::serialize() { + Array arr; + arr.push_back(infos.size() * 4); + for (int i = 0; i < infos.size(); ++i) { + arr.push_back(uint64_t(infos[i].node)); + arr.push_back(infos[i].node_path); + arr.push_back(infos[i].incoming_rpc); + arr.push_back(infos[i].outgoing_rpc); + } + return arr; +} + +bool MultiplayerDebugger::RPCFrame::deserialize(const Array &p_arr) { + ERR_FAIL_COND_V(p_arr.size() < 1, false); + uint32_t size = p_arr[0]; + ERR_FAIL_COND_V(size % 4, false); + ERR_FAIL_COND_V((uint32_t)p_arr.size() != size + 1, false); + infos.resize(size / 4); + int idx = 1; + for (uint32_t i = 0; i < size / 4; ++i) { + infos.write[i].node = uint64_t(p_arr[idx]); + infos.write[i].node_path = p_arr[idx + 1]; + infos.write[i].incoming_rpc = p_arr[idx + 2]; + infos.write[i].outgoing_rpc = p_arr[idx + 3]; + } + return true; +} + +void MultiplayerDebugger::RPCProfiler::init_node(const ObjectID p_node) { + if (rpc_node_data.has(p_node)) { + return; + } + rpc_node_data.insert(p_node, RPCNodeInfo()); + rpc_node_data[p_node].node = p_node; + rpc_node_data[p_node].node_path = Object::cast_to<Node>(ObjectDB::get_instance(p_node))->get_path(); + rpc_node_data[p_node].incoming_rpc = 0; + rpc_node_data[p_node].outgoing_rpc = 0; +} + +void MultiplayerDebugger::RPCProfiler::toggle(bool p_enable, const Array &p_opts) { + rpc_node_data.clear(); +} + +void MultiplayerDebugger::RPCProfiler::add(const Array &p_data) { + ERR_FAIL_COND(p_data.size() < 2); + const ObjectID id = p_data[0]; + const String what = p_data[1]; + init_node(id); + RPCNodeInfo &info = rpc_node_data[id]; + if (what == "rpc_in") { + info.incoming_rpc++; + } else if (what == "rpc_out") { + info.outgoing_rpc++; + } +} + +void MultiplayerDebugger::RPCProfiler::tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time) { + uint64_t pt = OS::get_singleton()->get_ticks_msec(); + if (pt - last_profile_time > 100) { + last_profile_time = pt; + RPCFrame frame; + for (const KeyValue<ObjectID, RPCNodeInfo> &E : rpc_node_data) { + frame.infos.push_back(E.value); + } + rpc_node_data.clear(); + EngineDebugger::get_singleton()->send_message("multiplayer:rpc", frame.serialize()); + } +} diff --git a/modules/multiplayer/multiplayer_debugger.h b/modules/multiplayer/multiplayer_debugger.h new file mode 100644 index 0000000000..4efd1da016 --- /dev/null +++ b/modules/multiplayer/multiplayer_debugger.h @@ -0,0 +1,95 @@ +/*************************************************************************/ +/* multiplayer_debugger.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 MULTIPLAYER_DEBUGGER_H +#define MULTIPLAYER_DEBUGGER_H + +#include "core/debugger/engine_profiler.h" + +#include "core/os/os.h" + +class MultiplayerDebugger { +public: + struct RPCNodeInfo { + ObjectID node; + String node_path; + int incoming_rpc = 0; + int outgoing_rpc = 0; + }; + + struct RPCFrame { + Vector<RPCNodeInfo> infos; + + Array serialize(); + bool deserialize(const Array &p_arr); + }; + +private: + class BandwidthProfiler : public EngineProfiler { + protected: + struct BandwidthFrame { + uint32_t timestamp; + int packet_size; + }; + + int bandwidth_in_ptr = 0; + Vector<BandwidthFrame> bandwidth_in; + int bandwidth_out_ptr = 0; + Vector<BandwidthFrame> bandwidth_out; + uint64_t last_bandwidth_time = 0; + + int bandwidth_usage(const Vector<BandwidthFrame> &p_buffer, int p_pointer); + + public: + void toggle(bool p_enable, const Array &p_opts); + void add(const Array &p_data); + void tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time); + }; + + class RPCProfiler : public EngineProfiler { + public: + private: + HashMap<ObjectID, RPCNodeInfo> rpc_node_data; + uint64_t last_profile_time = 0; + + void init_node(const ObjectID p_node); + + public: + void toggle(bool p_enable, const Array &p_opts); + void add(const Array &p_data); + void tick(double p_frame_time, double p_process_time, double p_physics_time, double p_physics_frame_time); + }; + +public: + static void initialize(); + static void deinitialize(); +}; + +#endif // MULTIPLAYER_DEBUGGER_H diff --git a/modules/multiplayer/register_types.cpp b/modules/multiplayer/register_types.cpp index a2c524da80..2bf1041029 100644 --- a/modules/multiplayer/register_types.cpp +++ b/modules/multiplayer/register_types.cpp @@ -36,9 +36,10 @@ #include "scene_replication_interface.h" #include "scene_rpc_interface.h" +#include "multiplayer_debugger.h" + #ifdef TOOLS_ENABLED -#include "editor/editor_plugin.h" -#include "editor/replication_editor_plugin.h" +#include "editor/multiplayer_editor_plugin.h" #endif void initialize_multiplayer_module(ModuleInitializationLevel p_level) { @@ -48,13 +49,15 @@ void initialize_multiplayer_module(ModuleInitializationLevel p_level) { GDREGISTER_CLASS(MultiplayerSynchronizer); GDREGISTER_CLASS(SceneMultiplayer); MultiplayerAPI::set_default_interface("SceneMultiplayer"); + MultiplayerDebugger::initialize(); } #ifdef TOOLS_ENABLED if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) { - EditorPlugins::add_by_type<ReplicationEditorPlugin>(); + EditorPlugins::add_by_type<MultiplayerEditorPlugin>(); } #endif } void uninitialize_multiplayer_module(ModuleInitializationLevel p_level) { + MultiplayerDebugger::deinitialize(); } diff --git a/modules/navigation/editor/navigation_mesh_editor_plugin.h b/modules/navigation/editor/navigation_mesh_editor_plugin.h index bc9e4185b7..b7bde98131 100644 --- a/modules/navigation/editor/navigation_mesh_editor_plugin.h +++ b/modules/navigation/editor/navigation_mesh_editor_plugin.h @@ -35,6 +35,8 @@ #include "editor/editor_plugin.h" +class AcceptDialog; +class HBoxContainer; class NavigationRegion3D; class NavigationMeshEditor : public Control { diff --git a/modules/noise/editor/noise_editor_plugin.cpp b/modules/noise/editor/noise_editor_plugin.cpp index e8e73e4fd9..47f5f8f819 100644 --- a/modules/noise/editor/noise_editor_plugin.cpp +++ b/modules/noise/editor/noise_editor_plugin.cpp @@ -32,7 +32,9 @@ #ifdef TOOLS_ENABLED +#include "editor/editor_inspector.h" #include "editor/editor_scale.h" +#include "scene/gui/texture_rect.h" #include "modules/noise/noise.h" #include "modules/noise/noise_texture_2d.h" diff --git a/modules/openxr/extensions/openxr_opengl_extension.cpp b/modules/openxr/extensions/openxr_opengl_extension.cpp index a25727f885..ee69144123 100644 --- a/modules/openxr/extensions/openxr_opengl_extension.cpp +++ b/modules/openxr/extensions/openxr_opengl_extension.cpp @@ -151,19 +151,6 @@ void *OpenXROpenGLExtension::set_session_create_and_get_next_pointer(void *p_nex graphics_binding_gl.glxContext = (GLXContext)glxcontext_handle; graphics_binding_gl.glxDrawable = (GLXDrawable)glxdrawable_handle; - if (graphics_binding_gl.xDisplay == nullptr) { - print_line("OpenXR Failed to get xDisplay from Godot, using XOpenDisplay(nullptr)"); - graphics_binding_gl.xDisplay = XOpenDisplay(nullptr); - } - if (graphics_binding_gl.glxContext == nullptr) { - print_line("OpenXR Failed to get glxContext from Godot, using glXGetCurrentContext()"); - graphics_binding_gl.glxContext = glXGetCurrentContext(); - } - if (graphics_binding_gl.glxDrawable == 0) { - print_line("OpenXR Failed to get glxDrawable from Godot, using glXGetCurrentDrawable()"); - graphics_binding_gl.glxDrawable = glXGetCurrentDrawable(); - } - // spec says to use proper values but runtimes don't care graphics_binding_gl.visualid = 0; graphics_binding_gl.glxFBConfig = 0; diff --git a/modules/openxr/openxr_interface.cpp b/modules/openxr/openxr_interface.cpp index be6b7e4411..77660eb6f0 100644 --- a/modules/openxr/openxr_interface.cpp +++ b/modules/openxr/openxr_interface.cpp @@ -666,6 +666,7 @@ Transform3D OpenXRInterface::get_camera_transform() { Transform3D OpenXRInterface::get_transform_for_view(uint32_t p_view, const Transform3D &p_cam_transform) { XRServer *xr_server = XRServer::get_singleton(); ERR_FAIL_NULL_V(xr_server, Transform3D()); + ERR_FAIL_UNSIGNED_INDEX_V_MSG(p_view, get_view_count(), Transform3D(), "View index outside bounds."); Transform3D t; if (openxr_api && openxr_api->get_view_transform(p_view, t)) { @@ -685,6 +686,7 @@ Transform3D OpenXRInterface::get_transform_for_view(uint32_t p_view, const Trans Projection OpenXRInterface::get_projection_for_view(uint32_t p_view, double p_aspect, double p_z_near, double p_z_far) { Projection cm; + ERR_FAIL_UNSIGNED_INDEX_V_MSG(p_view, get_view_count(), cm, "View index outside bounds."); if (openxr_api) { if (openxr_api->get_view_projection(p_view, p_z_near, p_z_far, cm)) { diff --git a/modules/text_server_adv/SCsub b/modules/text_server_adv/SCsub index bd208a8429..3c7a89b705 100644 --- a/modules/text_server_adv/SCsub +++ b/modules/text_server_adv/SCsub @@ -39,6 +39,9 @@ thirdparty_obj = [] freetype_enabled = "freetype" in env.module_list msdfgen_enabled = "msdfgen" in env.module_list +if "svg" in env.module_list: + env_text_server_adv.Prepend(CPPPATH=["#thirdparty/thorvg/inc", "#thirdparty/thorvg/src/lib"]) + if env["builtin_harfbuzz"]: env_harfbuzz = env_modules.Clone() env_harfbuzz.disable_warnings() diff --git a/modules/text_server_adv/gdextension_build/SConstruct b/modules/text_server_adv/gdextension_build/SConstruct index 65b41e46ce..4a363fdd7a 100644 --- a/modules/text_server_adv/gdextension_build/SConstruct +++ b/modules/text_server_adv/gdextension_build/SConstruct @@ -23,6 +23,7 @@ opts.Add(BoolVariable("brotli_enabled", "Use Brotli library", True)) opts.Add(BoolVariable("freetype_enabled", "Use FreeType library", True)) opts.Add(BoolVariable("msdfgen_enabled", "Use MSDFgen library (require FreeType)", True)) opts.Add(BoolVariable("graphite_enabled", "Use Graphite library (require FreeType)", True)) +opts.Add(BoolVariable("thorvg_enabled", "Use ThorVG library (require FreeType)", True)) opts.Add(BoolVariable("static_icu_data", "Use built-in ICU data", True)) opts.Add(BoolVariable("verbose", "Enable verbose output for the compilation", False)) @@ -34,6 +35,79 @@ if not env["verbose"]: if env["platform"] == "windows" and not env["use_mingw"]: env.AppendUnique(CCFLAGS=["/utf-8"]) # Force to use Unicode encoding. +# ThorVG +if env["thorvg_enabled"] and env["freetype_enabled"]: + env_tvg = env.Clone() + env_tvg.disable_warnings() + + thirdparty_tvg_dir = "../../../thirdparty/thorvg/" + thirdparty_tvg_sources = [ + "src/lib/sw_engine/tvgSwFill.cpp", + "src/lib/sw_engine/tvgSwImage.cpp", + "src/lib/sw_engine/tvgSwMath.cpp", + "src/lib/sw_engine/tvgSwMemPool.cpp", + "src/lib/sw_engine/tvgSwRaster.cpp", + "src/lib/sw_engine/tvgSwRenderer.cpp", + "src/lib/sw_engine/tvgSwRle.cpp", + "src/lib/sw_engine/tvgSwShape.cpp", + "src/lib/sw_engine/tvgSwStroke.cpp", + "src/lib/tvgAccessor.cpp", + "src/lib/tvgBezier.cpp", + "src/lib/tvgCanvas.cpp", + "src/lib/tvgFill.cpp", + "src/lib/tvgGlCanvas.cpp", + "src/lib/tvgInitializer.cpp", + "src/lib/tvgLinearGradient.cpp", + "src/lib/tvgLoader.cpp", + "src/lib/tvgLzw.cpp", + "src/lib/tvgPaint.cpp", + "src/lib/tvgPicture.cpp", + "src/lib/tvgRadialGradient.cpp", + "src/lib/tvgRender.cpp", + "src/lib/tvgSaver.cpp", + "src/lib/tvgScene.cpp", + "src/lib/tvgShape.cpp", + "src/lib/tvgSwCanvas.cpp", + "src/lib/tvgTaskScheduler.cpp", + "src/loaders/external_png/tvgPngLoader.cpp", + "src/loaders/jpg/tvgJpgd.cpp", + "src/loaders/jpg/tvgJpgLoader.cpp", + "src/loaders/raw/tvgRawLoader.cpp", + "src/loaders/svg/tvgSvgCssStyle.cpp", + "src/loaders/svg/tvgSvgLoader.cpp", + "src/loaders/svg/tvgSvgPath.cpp", + "src/loaders/svg/tvgSvgSceneBuilder.cpp", + "src/loaders/svg/tvgSvgUtil.cpp", + "src/loaders/svg/tvgXmlParser.cpp", + "src/loaders/tvg/tvgTvgBinInterpreter.cpp", + "src/loaders/tvg/tvgTvgLoader.cpp", + "src/savers/tvg/tvgTvgSaver.cpp", + ] + thirdparty_tvg_sources = [thirdparty_tvg_dir + file for file in thirdparty_tvg_sources] + + env_tvg.Append( + CPPPATH=[ + "../../../thirdparty/thorvg/inc", + "../../../thirdparty/thorvg/src/lib", + "../../../thirdparty/thorvg/src/lib/sw_engine", + "../../../thirdparty/thorvg/src/loaders/external_png", + "../../../thirdparty/thorvg/src/loaders/jpg", + "../../../thirdparty/thorvg/src/loaders/raw", + "../../../thirdparty/thorvg/src/loaders/svg", + "../../../thirdparty/thorvg/src/loaders/tvg", + "../../../thirdparty/thorvg/src/savers/tvg", + "../../../thirdparty/libpng", + ] + ) + env.Append(CPPPATH=["../../../thirdparty/thorvg/inc", "../../../thirdparty/thorvg/src/lib"]) + env.Append(CPPDEFINES=["MODULE_SVG_ENABLED"]) + + lib = env_tvg.Library( + f'tvg_builtin{env["suffix"]}{env["LIBSUFFIX"]}', + thirdparty_tvg_sources, + ) + env.Append(LIBS=[lib]) + # MSDFGEN if env["msdfgen_enabled"] and env["freetype_enabled"]: env_msdfgen = env.Clone() diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index 155da3c67b..166325c551 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -41,6 +41,8 @@ using namespace godot; +#define GLOBAL_GET(m_var) ProjectSettings::get_singleton()->get(m_var) + #else // Headers for building as built-in module. @@ -50,7 +52,7 @@ using namespace godot; #include "core/string/print_string.h" #include "core/string/translation.h" -#include "modules/modules_enabled.gen.h" // For freetype, msdfgen. +#include "modules/modules_enabled.gen.h" // For freetype, msdfgen, svg. #endif @@ -69,6 +71,10 @@ using namespace godot; #include "msdfgen.h" #endif +#ifdef MODULE_SVG_ENABLED +#include "thorvg_svg_in_ot.h" +#endif + /*************************************************************************/ /* bmp_font_t HarfBuzz Bitmap font interface */ /*************************************************************************/ @@ -1346,6 +1352,9 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_f memdelete(fd); ERR_FAIL_V_MSG(false, "FreeType: Error initializing library: '" + String(FT_Error_String(error)) + "'."); } +#ifdef MODULE_SVG_ENABLED + FT_Property_Set(ft_library, "ot-svg", "svg-hooks", get_tvg_svg_in_ot_hooks()); +#endif } memset(&fd->stream, 0, sizeof(FT_StreamRec)); @@ -1888,6 +1897,9 @@ int64_t TextServerAdvanced::_font_get_face_count(const RID &p_font_rid) const { if (!ft_library) { error = FT_Init_FreeType(&ft_library); ERR_FAIL_COND_V_MSG(error != 0, false, "FreeType: Error initializing library: '" + String(FT_Error_String(error)) + "'."); +#ifdef MODULE_SVG_ENABLED + FT_Property_Set(ft_library, "ot-svg", "svg-hooks", get_tvg_svg_in_ot_hooks()); +#endif } FT_StreamRec stream; diff --git a/modules/text_server_adv/text_server_adv.h b/modules/text_server_adv/text_server_adv.h index 33fa1e117e..10fe3c2316 100644 --- a/modules/text_server_adv/text_server_adv.h +++ b/modules/text_server_adv/text_server_adv.h @@ -88,7 +88,7 @@ using namespace godot; #include "core/templates/rid_owner.h" #include "scene/resources/texture.h" -#include "modules/modules_enabled.gen.h" // For freetype, msdfgen. +#include "modules/modules_enabled.gen.h" // For freetype, msdfgen, svg. #endif @@ -117,6 +117,7 @@ using namespace godot; #include FT_ADVANCES_H #include FT_MULTIPLE_MASTERS_H #include FT_BBOX_H +#include FT_MODULE_H #include FT_CONFIG_OPTIONS_H #if !defined(FT_CONFIG_OPTION_USE_BROTLI) && !defined(_MSC_VER) #warning FreeType is configured without Brotli support, built-in fonts will not be available. diff --git a/modules/text_server_adv/thorvg_bounds_iterator.cpp b/modules/text_server_adv/thorvg_bounds_iterator.cpp new file mode 100644 index 0000000000..54a6136134 --- /dev/null +++ b/modules/text_server_adv/thorvg_bounds_iterator.cpp @@ -0,0 +1,70 @@ +/*************************************************************************/ +/* thorvg_bounds_iterator.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. */ +/*************************************************************************/ + +#ifdef GDEXTENSION +// Headers for building as GDExtension plug-in. + +#include <godot_cpp/godot.hpp> + +using namespace godot; + +#else +// Headers for building as built-in module. + +#include "core/typedefs.h" + +#include "modules/modules_enabled.gen.h" // For svg. +#endif + +#ifdef MODULE_SVG_ENABLED + +#include "thorvg_bounds_iterator.h" + +#include <tvgIteratorAccessor.h> +#include <tvgPaint.h> + +// This function uses private ThorVG API to get bounding box of top level children elements. + +void tvg_get_bounds(tvg::Picture *p_picture, float &r_min_x, float &r_min_y, float &r_max_x, float &r_max_y) { + tvg::IteratorAccessor itrAccessor; + if (tvg::Iterator *it = itrAccessor.iterator(p_picture)) { + while (const tvg::Paint *child = it->next()) { + float x = 0, y = 0, w = 0, h = 0; + child->bounds(&x, &y, &w, &h, true); + r_min_x = MIN(x, r_min_x); + r_min_y = MIN(y, r_min_y); + r_max_x = MAX(x + w, r_max_x); + r_max_y = MAX(y + h, r_max_y); + } + delete (it); + } +} + +#endif // MODULE_SVG_ENABLED diff --git a/modules/text_server_adv/thorvg_bounds_iterator.h b/modules/text_server_adv/thorvg_bounds_iterator.h new file mode 100644 index 0000000000..e54e30eaa2 --- /dev/null +++ b/modules/text_server_adv/thorvg_bounds_iterator.h @@ -0,0 +1,58 @@ +/*************************************************************************/ +/* thorvg_bounds_iterator.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 THORVG_BOUNDS_ITERATOR_H +#define THORVG_BOUNDS_ITERATOR_H + +#ifdef GDEXTENSION +// Headers for building as GDExtension plug-in. + +#include <godot_cpp/core/mutex_lock.hpp> +#include <godot_cpp/godot.hpp> + +using namespace godot; + +#else +// Headers for building as built-in module. + +#include "core/typedefs.h" + +#include "modules/modules_enabled.gen.h" // For svg. +#endif + +#ifdef MODULE_SVG_ENABLED + +#include <thorvg.h> + +void tvg_get_bounds(tvg::Picture *p_picture, float &r_min_x, float &r_min_y, float &r_max_x, float &r_max_y); + +#endif // MODULE_SVG_ENABLED + +#endif // THORVG_BOUNDS_ITERATOR_H diff --git a/modules/text_server_adv/thorvg_svg_in_ot.cpp b/modules/text_server_adv/thorvg_svg_in_ot.cpp new file mode 100644 index 0000000000..7863ab67fa --- /dev/null +++ b/modules/text_server_adv/thorvg_svg_in_ot.cpp @@ -0,0 +1,320 @@ +/*************************************************************************/ +/* thorvg_svg_in_ot.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. */ +/*************************************************************************/ + +#ifdef GDEXTENSION +// Headers for building as GDExtension plug-in. + +#include <godot_cpp/classes/xml_parser.hpp> +#include <godot_cpp/core/mutex_lock.hpp> +#include <godot_cpp/godot.hpp> +#include <godot_cpp/templates/vector.hpp> + +using namespace godot; + +#else +// Headers for building as built-in module. + +#include "core/error/error_macros.h" +#include "core/io/xml_parser.h" +#include "core/os/memory.h" +#include "core/os/os.h" +#include "core/string/ustring.h" +#include "core/typedefs.h" +#include "core/variant/variant.h" + +#include "modules/modules_enabled.gen.h" // For svg. +#endif + +#ifdef MODULE_SVG_ENABLED + +#include "thorvg_bounds_iterator.h" +#include "thorvg_svg_in_ot.h" + +#include <freetype/otsvg.h> +#include <ft2build.h> + +#include <math.h> +#include <stdlib.h> + +FT_Error tvg_svg_in_ot_init(FT_Pointer *p_state) { + *p_state = memnew(TVG_State); + + return FT_Err_Ok; +} + +void tvg_svg_in_ot_free(FT_Pointer *p_state) { + TVG_State *state = *reinterpret_cast<TVG_State **>(p_state); + memdelete(state); +} + +FT_Error tvg_svg_in_ot_preset_slot(FT_GlyphSlot p_slot, FT_Bool p_cache, FT_Pointer *p_state) { + TVG_State *state = *reinterpret_cast<TVG_State **>(p_state); + if (!state) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "SVG in OT state not initialized."); + } + MutexLock lock(state->mutex); + + FT_SVG_Document document = (FT_SVG_Document)p_slot->other; + FT_Size_Metrics metrics = document->metrics; + + GL_State &gl_state = state->glyph_map[p_slot->glyph_index]; + if (!gl_state.ready) { + Ref<XMLParser> parser; + parser.instantiate(); +#ifdef GDEXTENSION + PackedByteArray data; + data.resize(document->svg_document_length); + memcpy(data.ptrw(), document->svg_document, document->svg_document_length); + parser->open_buffer(data); +#else + parser->_open_buffer((const uint8_t *)document->svg_document, document->svg_document_length); +#endif + + float aspect = 1.0f; + String xml_body; + while (parser->read() == OK) { + if (parser->has_attribute("id")) { +#ifdef GDEXTENSION + const String &gl_name = parser->get_named_attribute_value("id"); +#else + const String &gl_name = parser->get_attribute_value("id"); +#endif + if (gl_name.begins_with("glyph")) { + int dot_pos = gl_name.find("."); + int64_t gl_idx = gl_name.substr(5, (dot_pos > 0) ? dot_pos - 5 : -1).to_int(); + if (p_slot->glyph_index != gl_idx) { + parser->skip_section(); + continue; + } + } + } + if (parser->get_node_type() == XMLParser::NODE_ELEMENT && parser->get_node_name() == "svg") { + if (parser->has_attribute("viewBox")) { +#ifdef GDEXTENSION + PackedStringArray vb = parser->get_named_attribute_value("viewBox").split(" "); +#else + Vector<String> vb = parser->get_attribute_value("viewBox").split(" "); +#endif + + if (vb.size() == 4) { + aspect = vb[2].to_float() / vb[3].to_float(); + } + } + continue; + } +#ifdef GDEXTENSION + if (parser->get_node_type() == XMLParser::NODE_ELEMENT) { + xml_body = xml_body + "<" + parser->get_node_name(); + for (int i = 0; i < parser->get_attribute_count(); i++) { + xml_body = xml_body + " " + parser->get_attribute_name(i) + "=\"" + parser->get_attribute_value(i) + "\""; + } + xml_body = xml_body + ">"; + } else if (parser->get_node_type() == XMLParser::NODE_TEXT) { + xml_body = xml_body + parser->get_node_data(); + } else if (parser->get_node_type() == XMLParser::NODE_ELEMENT_END) { + xml_body = xml_body + "</" + parser->get_node_name() + ">"; + } +#else + if (parser->get_node_type() == XMLParser::NODE_ELEMENT) { + xml_body += vformat("<%s", parser->get_node_name()); + for (int i = 0; i < parser->get_attribute_count(); i++) { + xml_body += vformat(" %s=\"%s\"", parser->get_attribute_name(i), parser->get_attribute_value(i)); + } + xml_body += ">"; + } else if (parser->get_node_type() == XMLParser::NODE_TEXT) { + xml_body += parser->get_node_data(); + } else if (parser->get_node_type() == XMLParser::NODE_ELEMENT_END) { + xml_body += vformat("</%s>", parser->get_node_name()); + } +#endif + } + String temp_xml = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 0 0\">" + xml_body; + + std::unique_ptr<tvg::Picture> picture = tvg::Picture::gen(); + tvg::Result result = picture->load(temp_xml.utf8().get_data(), temp_xml.utf8().length(), "svg+xml", false); + if (result != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "Failed to load SVG document (bounds detection)."); + } + + float min_x = INFINITY, min_y = INFINITY, max_x = -INFINITY, max_y = -INFINITY; + tvg_get_bounds(picture.get(), min_x, min_y, max_x, max_y); + + float new_h = (max_y - min_y); + float new_w = (max_x - min_x); + + if (new_h * aspect >= new_w) { + new_w = (new_h * aspect); + } else { + new_h = (new_w / aspect); + } + +#ifdef GDEXTENSION + gl_state.xml_code = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"" + rtos(min_x) + " " + rtos(min_y) + " " + rtos(new_w) + " " + rtos(new_h) + "\">" + xml_body; +#else + gl_state.xml_code = vformat("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"%f %f %f %f\">", min_x, min_y, new_w, new_h) + xml_body; +#endif + + picture = tvg::Picture::gen(); + result = picture->load(gl_state.xml_code.utf8().get_data(), gl_state.xml_code.utf8().length(), "svg+xml", false); + if (result != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "Failed to load SVG document (glyph metrics)."); + } + + float x_svg_to_out, y_svg_to_out; + x_svg_to_out = (float)metrics.x_ppem / new_w; + y_svg_to_out = (float)metrics.y_ppem / new_h; + + gl_state.m.e11 = (double)document->transform.xx / (1 << 16) * x_svg_to_out; + gl_state.m.e12 = -(double)document->transform.xy / (1 << 16) * x_svg_to_out; + gl_state.m.e21 = -(double)document->transform.yx / (1 << 16) * y_svg_to_out; + gl_state.m.e22 = (double)document->transform.yy / (1 << 16) * y_svg_to_out; + gl_state.m.e13 = (double)document->delta.x / 64 * new_w / metrics.x_ppem; + gl_state.m.e23 = -(double)document->delta.y / 64 * new_h / metrics.y_ppem; + gl_state.m.e31 = 0; + gl_state.m.e32 = 0; + gl_state.m.e33 = 1; + + result = picture->transform(gl_state.m); + if (result != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "Failed to apply transform to SVG document."); + } + + result = picture->bounds(&gl_state.x, &gl_state.y, &gl_state.w, &gl_state.h, true); + if (result != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "Failed to get SVG bounds."); + } + + gl_state.bmp_y = -min_y * gl_state.h / new_h; + gl_state.bmp_x = min_x * gl_state.w / new_w; + + gl_state.ready = true; + } + + p_slot->bitmap_left = (FT_Int)gl_state.bmp_x; + p_slot->bitmap_top = (FT_Int)gl_state.bmp_y; + + float tmp = ceil(gl_state.h); + p_slot->bitmap.rows = (unsigned int)tmp; + tmp = ceil(gl_state.w); + p_slot->bitmap.width = (unsigned int)tmp; + p_slot->bitmap.pitch = (int)p_slot->bitmap.width * 4; + p_slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA; + + float metrics_width, metrics_height; + float horiBearingX, horiBearingY; + float vertBearingX, vertBearingY; + + metrics_width = (float)gl_state.w; + metrics_height = (float)gl_state.h; + horiBearingX = (float)gl_state.x; + horiBearingY = (float)-gl_state.y; + vertBearingX = p_slot->metrics.horiBearingX / 64.0f - p_slot->metrics.horiAdvance / 64.0f / 2; + vertBearingY = (p_slot->metrics.vertAdvance / 64.0f - p_slot->metrics.height / 64.0f) / 2; + + tmp = roundf(metrics_width * 64); + p_slot->metrics.width = (FT_Pos)tmp; + tmp = roundf(metrics_height * 64); + p_slot->metrics.height = (FT_Pos)tmp; + + p_slot->metrics.horiBearingX = (FT_Pos)(horiBearingX * 64); + p_slot->metrics.horiBearingY = (FT_Pos)(horiBearingY * 64); + p_slot->metrics.vertBearingX = (FT_Pos)(vertBearingX * 64); + p_slot->metrics.vertBearingY = (FT_Pos)(vertBearingY * 64); + + if (p_slot->metrics.vertAdvance == 0) { + p_slot->metrics.vertAdvance = (FT_Pos)(metrics_height * 1.2f * 64); + } + + return FT_Err_Ok; +} + +FT_Error tvg_svg_in_ot_render(FT_GlyphSlot p_slot, FT_Pointer *p_state) { + TVG_State *state = *reinterpret_cast<TVG_State **>(p_state); + if (!state) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "SVG in OT state not initialized."); + } + MutexLock lock(state->mutex); + + if (!state->glyph_map.has(p_slot->glyph_index)) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "SVG glyph not loaded."); + } + + GL_State &gl_state = state->glyph_map[p_slot->glyph_index]; + ERR_FAIL_COND_V_MSG(!gl_state.ready, FT_Err_Invalid_SVG_Document, "SVG glyph not ready."); + + std::unique_ptr<tvg::Picture> picture = tvg::Picture::gen(); + tvg::Result res = picture->load(gl_state.xml_code.utf8().get_data(), gl_state.xml_code.utf8().length(), "svg+xml", false); + if (res != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "Failed to load SVG document (glyph rendering)."); + } + res = picture->transform(gl_state.m); + if (res != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "Failed to apply transform to SVG document."); + } + + std::unique_ptr<tvg::SwCanvas> sw_canvas = tvg::SwCanvas::gen(); + res = sw_canvas->target((uint32_t *)p_slot->bitmap.buffer, (int)p_slot->bitmap.width, (int)p_slot->bitmap.width, (int)p_slot->bitmap.rows, tvg::SwCanvas::ARGB8888_STRAIGHT); + if (res != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_Outline, "Failed to create SVG canvas."); + } + res = sw_canvas->push(std::move(picture)); + if (res != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_Outline, "Failed to set SVG canvas source."); + } + res = sw_canvas->draw(); + if (res != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_Outline, "Failed to draw to SVG canvas."); + } + res = sw_canvas->sync(); + if (res != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_Outline, "Failed to sync SVG canvas."); + } + + state->glyph_map.erase(p_slot->glyph_index); + + p_slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA; + p_slot->bitmap.num_grays = 256; + p_slot->format = FT_GLYPH_FORMAT_BITMAP; + + return FT_Err_Ok; +} + +SVG_RendererHooks tvg_svg_in_ot_hooks = { + (SVG_Lib_Init_Func)tvg_svg_in_ot_init, + (SVG_Lib_Free_Func)tvg_svg_in_ot_free, + (SVG_Lib_Render_Func)tvg_svg_in_ot_render, + (SVG_Lib_Preset_Slot_Func)tvg_svg_in_ot_preset_slot, +}; + +SVG_RendererHooks *get_tvg_svg_in_ot_hooks() { + return &tvg_svg_in_ot_hooks; +} + +#endif // MODULE_SVG_ENABLED diff --git a/modules/text_server_adv/thorvg_svg_in_ot.h b/modules/text_server_adv/thorvg_svg_in_ot.h new file mode 100644 index 0000000000..b2816193d9 --- /dev/null +++ b/modules/text_server_adv/thorvg_svg_in_ot.h @@ -0,0 +1,86 @@ +/*************************************************************************/ +/* thorvg_svg_in_ot.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 THORVG_SVG_IN_OT_H +#define THORVG_SVG_IN_OT_H + +#ifdef GDEXTENSION +// Headers for building as GDExtension plug-in. + +#include <godot_cpp/core/mutex_lock.hpp> +#include <godot_cpp/godot.hpp> +#include <godot_cpp/templates/hash_map.hpp> + +using namespace godot; + +#else +// Headers for building as built-in module. + +#include "core/os/mutex.h" +#include "core/templates/hash_map.h" +#include "core/typedefs.h" + +#include "modules/modules_enabled.gen.h" // For svg. +#endif + +#ifdef MODULE_SVG_ENABLED + +#include <freetype/freetype.h> +#include <freetype/otsvg.h> +#include <ft2build.h> +#include <thorvg.h> + +struct GL_State { + bool ready = false; + float bmp_x = 0; + float bmp_y = 0; + float x = 0; + float y = 0; + float w = 0; + float h = 0; + String xml_code; + tvg::Matrix m; +}; + +struct TVG_State { + Mutex mutex; + HashMap<uint32_t, GL_State> glyph_map; +}; + +FT_Error tvg_svg_in_ot_init(FT_Pointer *p_state); +void tvg_svg_in_ot_free(FT_Pointer *p_state); +FT_Error tvg_svg_in_ot_preset_slot(FT_GlyphSlot p_slot, FT_Bool p_cache, FT_Pointer *p_state); +FT_Error tvg_svg_in_ot_render(FT_GlyphSlot p_slot, FT_Pointer *p_state); + +SVG_RendererHooks *get_tvg_svg_in_ot_hooks(); + +#endif // MODULE_SVG_ENABLED + +#endif // THORVG_SVG_IN_OT_H diff --git a/modules/text_server_fb/SCsub b/modules/text_server_fb/SCsub index 429d2e1fdc..f1d57ec4d3 100644 --- a/modules/text_server_fb/SCsub +++ b/modules/text_server_fb/SCsub @@ -8,6 +8,9 @@ msdfgen_enabled = "msdfgen" in env.module_list env_text_server_fb = env_modules.Clone() +if "svg" in env.module_list: + env_text_server_fb.Prepend(CPPPATH=["#thirdparty/thorvg/inc", "#thirdparty/thorvg/src/lib"]) + if env["builtin_msdfgen"] and msdfgen_enabled: env_text_server_fb.Prepend(CPPPATH=["#thirdparty/msdfgen"]) diff --git a/modules/text_server_fb/gdextension_build/SConstruct b/modules/text_server_fb/gdextension_build/SConstruct index 8ed8f61a43..7b4c548a21 100644 --- a/modules/text_server_fb/gdextension_build/SConstruct +++ b/modules/text_server_fb/gdextension_build/SConstruct @@ -22,6 +22,7 @@ opts = Variables([], ARGUMENTS) opts.Add(BoolVariable("brotli_enabled", "Use Brotli library", True)) opts.Add(BoolVariable("freetype_enabled", "Use FreeType library", True)) opts.Add(BoolVariable("msdfgen_enabled", "Use MSDFgen library (require FreeType)", True)) +opts.Add(BoolVariable("thorvg_enabled", "Use ThorVG library (require FreeType)", True)) opts.Add(BoolVariable("verbose", "Enable verbose output for the compilation", False)) opts.Update(env) @@ -29,6 +30,79 @@ opts.Update(env) if not env["verbose"]: methods.no_verbose(sys, env) +# ThorVG +if env["thorvg_enabled"] and env["freetype_enabled"]: + env_tvg = env.Clone() + env_tvg.disable_warnings() + + thirdparty_tvg_dir = "../../../thirdparty/thorvg/" + thirdparty_tvg_sources = [ + "src/lib/sw_engine/tvgSwFill.cpp", + "src/lib/sw_engine/tvgSwImage.cpp", + "src/lib/sw_engine/tvgSwMath.cpp", + "src/lib/sw_engine/tvgSwMemPool.cpp", + "src/lib/sw_engine/tvgSwRaster.cpp", + "src/lib/sw_engine/tvgSwRenderer.cpp", + "src/lib/sw_engine/tvgSwRle.cpp", + "src/lib/sw_engine/tvgSwShape.cpp", + "src/lib/sw_engine/tvgSwStroke.cpp", + "src/lib/tvgAccessor.cpp", + "src/lib/tvgBezier.cpp", + "src/lib/tvgCanvas.cpp", + "src/lib/tvgFill.cpp", + "src/lib/tvgGlCanvas.cpp", + "src/lib/tvgInitializer.cpp", + "src/lib/tvgLinearGradient.cpp", + "src/lib/tvgLoader.cpp", + "src/lib/tvgLzw.cpp", + "src/lib/tvgPaint.cpp", + "src/lib/tvgPicture.cpp", + "src/lib/tvgRadialGradient.cpp", + "src/lib/tvgRender.cpp", + "src/lib/tvgSaver.cpp", + "src/lib/tvgScene.cpp", + "src/lib/tvgShape.cpp", + "src/lib/tvgSwCanvas.cpp", + "src/lib/tvgTaskScheduler.cpp", + "src/loaders/external_png/tvgPngLoader.cpp", + "src/loaders/jpg/tvgJpgd.cpp", + "src/loaders/jpg/tvgJpgLoader.cpp", + "src/loaders/raw/tvgRawLoader.cpp", + "src/loaders/svg/tvgSvgCssStyle.cpp", + "src/loaders/svg/tvgSvgLoader.cpp", + "src/loaders/svg/tvgSvgPath.cpp", + "src/loaders/svg/tvgSvgSceneBuilder.cpp", + "src/loaders/svg/tvgSvgUtil.cpp", + "src/loaders/svg/tvgXmlParser.cpp", + "src/loaders/tvg/tvgTvgBinInterpreter.cpp", + "src/loaders/tvg/tvgTvgLoader.cpp", + "src/savers/tvg/tvgTvgSaver.cpp", + ] + thirdparty_tvg_sources = [thirdparty_tvg_dir + file for file in thirdparty_tvg_sources] + + env_tvg.Append( + CPPPATH=[ + "../../../thirdparty/thorvg/inc", + "../../../thirdparty/thorvg/src/lib", + "../../../thirdparty/thorvg/src/lib/sw_engine", + "../../../thirdparty/thorvg/src/loaders/external_png", + "../../../thirdparty/thorvg/src/loaders/jpg", + "../../../thirdparty/thorvg/src/loaders/raw", + "../../../thirdparty/thorvg/src/loaders/svg", + "../../../thirdparty/thorvg/src/loaders/tvg", + "../../../thirdparty/thorvg/src/savers/tvg", + "../../../thirdparty/libpng", + ] + ) + env.Append(CPPPATH=["../../../thirdparty/thorvg/inc", "../../../thirdparty/thorvg/src/lib"]) + env.Append(CPPDEFINES=["MODULE_SVG_ENABLED"]) + + lib = env_tvg.Library( + f'tvg_builtin{env["suffix"]}{env["LIBSUFFIX"]}', + thirdparty_tvg_sources, + ) + env.Append(LIBS=[lib]) + # MSDFGEN if env["msdfgen_enabled"] and env["freetype_enabled"]: env_msdfgen = env.Clone() diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp index 999870b904..aaef9c9a3d 100644 --- a/modules/text_server_fb/text_server_fb.cpp +++ b/modules/text_server_fb/text_server_fb.cpp @@ -41,6 +41,8 @@ using namespace godot; +#define GLOBAL_GET(m_var) ProjectSettings::get_singleton()->get(m_var) + #else // Headers for building as built-in module. @@ -49,7 +51,7 @@ using namespace godot; #include "core/string/print_string.h" #include "core/string/ucaps.h" -#include "modules/modules_enabled.gen.h" // For freetype, msdfgen. +#include "modules/modules_enabled.gen.h" // For freetype, msdfgen, svg. #endif @@ -62,6 +64,10 @@ using namespace godot; #include "msdfgen.h" #endif +#ifdef MODULE_SVG_ENABLED +#include "thorvg_svg_in_ot.h" +#endif + /*************************************************************************/ #define OT_TAG(c1, c2, c3, c4) ((int32_t)((((uint32_t)(c1)&0xff) << 24) | (((uint32_t)(c2)&0xff) << 16) | (((uint32_t)(c3)&0xff) << 8) | ((uint32_t)(c4)&0xff))) @@ -771,6 +777,9 @@ _FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_f memdelete(fd); ERR_FAIL_V_MSG(false, "FreeType: Error initializing library: '" + String(FT_Error_String(error)) + "'."); } +#ifdef MODULE_SVG_ENABLED + FT_Property_Set(ft_library, "ot-svg", "svg-hooks", get_tvg_svg_in_ot_hooks()); +#endif } memset(&fd->stream, 0, sizeof(FT_StreamRec)); @@ -992,6 +1001,9 @@ int64_t TextServerFallback::_font_get_face_count(const RID &p_font_rid) const { if (!ft_library) { error = FT_Init_FreeType(&ft_library); ERR_FAIL_COND_V_MSG(error != 0, false, "FreeType: Error initializing library: '" + String(FT_Error_String(error)) + "'."); +#ifdef MODULE_SVG_ENABLED + FT_Property_Set(ft_library, "ot-svg", "svg-hooks", get_tvg_svg_in_ot_hooks()); +#endif } FT_StreamRec stream; diff --git a/modules/text_server_fb/text_server_fb.h b/modules/text_server_fb/text_server_fb.h index 151ae13904..7e0bc99618 100644 --- a/modules/text_server_fb/text_server_fb.h +++ b/modules/text_server_fb/text_server_fb.h @@ -87,7 +87,7 @@ using namespace godot; #include "core/templates/rid_owner.h" #include "scene/resources/texture.h" -#include "modules/modules_enabled.gen.h" // For freetype, msdfgen. +#include "modules/modules_enabled.gen.h" // For freetype, msdfgen, svg. #endif @@ -101,6 +101,7 @@ using namespace godot; #include FT_ADVANCES_H #include FT_MULTIPLE_MASTERS_H #include FT_BBOX_H +#include FT_MODULE_H #include FT_CONFIG_OPTIONS_H #if !defined(FT_CONFIG_OPTION_USE_BROTLI) && !defined(_MSC_VER) #warning FreeType is configured without Brotli support, built-in fonts will not be available. diff --git a/modules/text_server_fb/thorvg_bounds_iterator.cpp b/modules/text_server_fb/thorvg_bounds_iterator.cpp new file mode 100644 index 0000000000..54a6136134 --- /dev/null +++ b/modules/text_server_fb/thorvg_bounds_iterator.cpp @@ -0,0 +1,70 @@ +/*************************************************************************/ +/* thorvg_bounds_iterator.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. */ +/*************************************************************************/ + +#ifdef GDEXTENSION +// Headers for building as GDExtension plug-in. + +#include <godot_cpp/godot.hpp> + +using namespace godot; + +#else +// Headers for building as built-in module. + +#include "core/typedefs.h" + +#include "modules/modules_enabled.gen.h" // For svg. +#endif + +#ifdef MODULE_SVG_ENABLED + +#include "thorvg_bounds_iterator.h" + +#include <tvgIteratorAccessor.h> +#include <tvgPaint.h> + +// This function uses private ThorVG API to get bounding box of top level children elements. + +void tvg_get_bounds(tvg::Picture *p_picture, float &r_min_x, float &r_min_y, float &r_max_x, float &r_max_y) { + tvg::IteratorAccessor itrAccessor; + if (tvg::Iterator *it = itrAccessor.iterator(p_picture)) { + while (const tvg::Paint *child = it->next()) { + float x = 0, y = 0, w = 0, h = 0; + child->bounds(&x, &y, &w, &h, true); + r_min_x = MIN(x, r_min_x); + r_min_y = MIN(y, r_min_y); + r_max_x = MAX(x + w, r_max_x); + r_max_y = MAX(y + h, r_max_y); + } + delete (it); + } +} + +#endif // MODULE_SVG_ENABLED diff --git a/modules/text_server_fb/thorvg_bounds_iterator.h b/modules/text_server_fb/thorvg_bounds_iterator.h new file mode 100644 index 0000000000..e54e30eaa2 --- /dev/null +++ b/modules/text_server_fb/thorvg_bounds_iterator.h @@ -0,0 +1,58 @@ +/*************************************************************************/ +/* thorvg_bounds_iterator.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 THORVG_BOUNDS_ITERATOR_H +#define THORVG_BOUNDS_ITERATOR_H + +#ifdef GDEXTENSION +// Headers for building as GDExtension plug-in. + +#include <godot_cpp/core/mutex_lock.hpp> +#include <godot_cpp/godot.hpp> + +using namespace godot; + +#else +// Headers for building as built-in module. + +#include "core/typedefs.h" + +#include "modules/modules_enabled.gen.h" // For svg. +#endif + +#ifdef MODULE_SVG_ENABLED + +#include <thorvg.h> + +void tvg_get_bounds(tvg::Picture *p_picture, float &r_min_x, float &r_min_y, float &r_max_x, float &r_max_y); + +#endif // MODULE_SVG_ENABLED + +#endif // THORVG_BOUNDS_ITERATOR_H diff --git a/modules/text_server_fb/thorvg_svg_in_ot.cpp b/modules/text_server_fb/thorvg_svg_in_ot.cpp new file mode 100644 index 0000000000..7863ab67fa --- /dev/null +++ b/modules/text_server_fb/thorvg_svg_in_ot.cpp @@ -0,0 +1,320 @@ +/*************************************************************************/ +/* thorvg_svg_in_ot.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. */ +/*************************************************************************/ + +#ifdef GDEXTENSION +// Headers for building as GDExtension plug-in. + +#include <godot_cpp/classes/xml_parser.hpp> +#include <godot_cpp/core/mutex_lock.hpp> +#include <godot_cpp/godot.hpp> +#include <godot_cpp/templates/vector.hpp> + +using namespace godot; + +#else +// Headers for building as built-in module. + +#include "core/error/error_macros.h" +#include "core/io/xml_parser.h" +#include "core/os/memory.h" +#include "core/os/os.h" +#include "core/string/ustring.h" +#include "core/typedefs.h" +#include "core/variant/variant.h" + +#include "modules/modules_enabled.gen.h" // For svg. +#endif + +#ifdef MODULE_SVG_ENABLED + +#include "thorvg_bounds_iterator.h" +#include "thorvg_svg_in_ot.h" + +#include <freetype/otsvg.h> +#include <ft2build.h> + +#include <math.h> +#include <stdlib.h> + +FT_Error tvg_svg_in_ot_init(FT_Pointer *p_state) { + *p_state = memnew(TVG_State); + + return FT_Err_Ok; +} + +void tvg_svg_in_ot_free(FT_Pointer *p_state) { + TVG_State *state = *reinterpret_cast<TVG_State **>(p_state); + memdelete(state); +} + +FT_Error tvg_svg_in_ot_preset_slot(FT_GlyphSlot p_slot, FT_Bool p_cache, FT_Pointer *p_state) { + TVG_State *state = *reinterpret_cast<TVG_State **>(p_state); + if (!state) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "SVG in OT state not initialized."); + } + MutexLock lock(state->mutex); + + FT_SVG_Document document = (FT_SVG_Document)p_slot->other; + FT_Size_Metrics metrics = document->metrics; + + GL_State &gl_state = state->glyph_map[p_slot->glyph_index]; + if (!gl_state.ready) { + Ref<XMLParser> parser; + parser.instantiate(); +#ifdef GDEXTENSION + PackedByteArray data; + data.resize(document->svg_document_length); + memcpy(data.ptrw(), document->svg_document, document->svg_document_length); + parser->open_buffer(data); +#else + parser->_open_buffer((const uint8_t *)document->svg_document, document->svg_document_length); +#endif + + float aspect = 1.0f; + String xml_body; + while (parser->read() == OK) { + if (parser->has_attribute("id")) { +#ifdef GDEXTENSION + const String &gl_name = parser->get_named_attribute_value("id"); +#else + const String &gl_name = parser->get_attribute_value("id"); +#endif + if (gl_name.begins_with("glyph")) { + int dot_pos = gl_name.find("."); + int64_t gl_idx = gl_name.substr(5, (dot_pos > 0) ? dot_pos - 5 : -1).to_int(); + if (p_slot->glyph_index != gl_idx) { + parser->skip_section(); + continue; + } + } + } + if (parser->get_node_type() == XMLParser::NODE_ELEMENT && parser->get_node_name() == "svg") { + if (parser->has_attribute("viewBox")) { +#ifdef GDEXTENSION + PackedStringArray vb = parser->get_named_attribute_value("viewBox").split(" "); +#else + Vector<String> vb = parser->get_attribute_value("viewBox").split(" "); +#endif + + if (vb.size() == 4) { + aspect = vb[2].to_float() / vb[3].to_float(); + } + } + continue; + } +#ifdef GDEXTENSION + if (parser->get_node_type() == XMLParser::NODE_ELEMENT) { + xml_body = xml_body + "<" + parser->get_node_name(); + for (int i = 0; i < parser->get_attribute_count(); i++) { + xml_body = xml_body + " " + parser->get_attribute_name(i) + "=\"" + parser->get_attribute_value(i) + "\""; + } + xml_body = xml_body + ">"; + } else if (parser->get_node_type() == XMLParser::NODE_TEXT) { + xml_body = xml_body + parser->get_node_data(); + } else if (parser->get_node_type() == XMLParser::NODE_ELEMENT_END) { + xml_body = xml_body + "</" + parser->get_node_name() + ">"; + } +#else + if (parser->get_node_type() == XMLParser::NODE_ELEMENT) { + xml_body += vformat("<%s", parser->get_node_name()); + for (int i = 0; i < parser->get_attribute_count(); i++) { + xml_body += vformat(" %s=\"%s\"", parser->get_attribute_name(i), parser->get_attribute_value(i)); + } + xml_body += ">"; + } else if (parser->get_node_type() == XMLParser::NODE_TEXT) { + xml_body += parser->get_node_data(); + } else if (parser->get_node_type() == XMLParser::NODE_ELEMENT_END) { + xml_body += vformat("</%s>", parser->get_node_name()); + } +#endif + } + String temp_xml = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 0 0\">" + xml_body; + + std::unique_ptr<tvg::Picture> picture = tvg::Picture::gen(); + tvg::Result result = picture->load(temp_xml.utf8().get_data(), temp_xml.utf8().length(), "svg+xml", false); + if (result != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "Failed to load SVG document (bounds detection)."); + } + + float min_x = INFINITY, min_y = INFINITY, max_x = -INFINITY, max_y = -INFINITY; + tvg_get_bounds(picture.get(), min_x, min_y, max_x, max_y); + + float new_h = (max_y - min_y); + float new_w = (max_x - min_x); + + if (new_h * aspect >= new_w) { + new_w = (new_h * aspect); + } else { + new_h = (new_w / aspect); + } + +#ifdef GDEXTENSION + gl_state.xml_code = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"" + rtos(min_x) + " " + rtos(min_y) + " " + rtos(new_w) + " " + rtos(new_h) + "\">" + xml_body; +#else + gl_state.xml_code = vformat("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"%f %f %f %f\">", min_x, min_y, new_w, new_h) + xml_body; +#endif + + picture = tvg::Picture::gen(); + result = picture->load(gl_state.xml_code.utf8().get_data(), gl_state.xml_code.utf8().length(), "svg+xml", false); + if (result != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "Failed to load SVG document (glyph metrics)."); + } + + float x_svg_to_out, y_svg_to_out; + x_svg_to_out = (float)metrics.x_ppem / new_w; + y_svg_to_out = (float)metrics.y_ppem / new_h; + + gl_state.m.e11 = (double)document->transform.xx / (1 << 16) * x_svg_to_out; + gl_state.m.e12 = -(double)document->transform.xy / (1 << 16) * x_svg_to_out; + gl_state.m.e21 = -(double)document->transform.yx / (1 << 16) * y_svg_to_out; + gl_state.m.e22 = (double)document->transform.yy / (1 << 16) * y_svg_to_out; + gl_state.m.e13 = (double)document->delta.x / 64 * new_w / metrics.x_ppem; + gl_state.m.e23 = -(double)document->delta.y / 64 * new_h / metrics.y_ppem; + gl_state.m.e31 = 0; + gl_state.m.e32 = 0; + gl_state.m.e33 = 1; + + result = picture->transform(gl_state.m); + if (result != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "Failed to apply transform to SVG document."); + } + + result = picture->bounds(&gl_state.x, &gl_state.y, &gl_state.w, &gl_state.h, true); + if (result != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "Failed to get SVG bounds."); + } + + gl_state.bmp_y = -min_y * gl_state.h / new_h; + gl_state.bmp_x = min_x * gl_state.w / new_w; + + gl_state.ready = true; + } + + p_slot->bitmap_left = (FT_Int)gl_state.bmp_x; + p_slot->bitmap_top = (FT_Int)gl_state.bmp_y; + + float tmp = ceil(gl_state.h); + p_slot->bitmap.rows = (unsigned int)tmp; + tmp = ceil(gl_state.w); + p_slot->bitmap.width = (unsigned int)tmp; + p_slot->bitmap.pitch = (int)p_slot->bitmap.width * 4; + p_slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA; + + float metrics_width, metrics_height; + float horiBearingX, horiBearingY; + float vertBearingX, vertBearingY; + + metrics_width = (float)gl_state.w; + metrics_height = (float)gl_state.h; + horiBearingX = (float)gl_state.x; + horiBearingY = (float)-gl_state.y; + vertBearingX = p_slot->metrics.horiBearingX / 64.0f - p_slot->metrics.horiAdvance / 64.0f / 2; + vertBearingY = (p_slot->metrics.vertAdvance / 64.0f - p_slot->metrics.height / 64.0f) / 2; + + tmp = roundf(metrics_width * 64); + p_slot->metrics.width = (FT_Pos)tmp; + tmp = roundf(metrics_height * 64); + p_slot->metrics.height = (FT_Pos)tmp; + + p_slot->metrics.horiBearingX = (FT_Pos)(horiBearingX * 64); + p_slot->metrics.horiBearingY = (FT_Pos)(horiBearingY * 64); + p_slot->metrics.vertBearingX = (FT_Pos)(vertBearingX * 64); + p_slot->metrics.vertBearingY = (FT_Pos)(vertBearingY * 64); + + if (p_slot->metrics.vertAdvance == 0) { + p_slot->metrics.vertAdvance = (FT_Pos)(metrics_height * 1.2f * 64); + } + + return FT_Err_Ok; +} + +FT_Error tvg_svg_in_ot_render(FT_GlyphSlot p_slot, FT_Pointer *p_state) { + TVG_State *state = *reinterpret_cast<TVG_State **>(p_state); + if (!state) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "SVG in OT state not initialized."); + } + MutexLock lock(state->mutex); + + if (!state->glyph_map.has(p_slot->glyph_index)) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "SVG glyph not loaded."); + } + + GL_State &gl_state = state->glyph_map[p_slot->glyph_index]; + ERR_FAIL_COND_V_MSG(!gl_state.ready, FT_Err_Invalid_SVG_Document, "SVG glyph not ready."); + + std::unique_ptr<tvg::Picture> picture = tvg::Picture::gen(); + tvg::Result res = picture->load(gl_state.xml_code.utf8().get_data(), gl_state.xml_code.utf8().length(), "svg+xml", false); + if (res != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "Failed to load SVG document (glyph rendering)."); + } + res = picture->transform(gl_state.m); + if (res != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_SVG_Document, "Failed to apply transform to SVG document."); + } + + std::unique_ptr<tvg::SwCanvas> sw_canvas = tvg::SwCanvas::gen(); + res = sw_canvas->target((uint32_t *)p_slot->bitmap.buffer, (int)p_slot->bitmap.width, (int)p_slot->bitmap.width, (int)p_slot->bitmap.rows, tvg::SwCanvas::ARGB8888_STRAIGHT); + if (res != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_Outline, "Failed to create SVG canvas."); + } + res = sw_canvas->push(std::move(picture)); + if (res != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_Outline, "Failed to set SVG canvas source."); + } + res = sw_canvas->draw(); + if (res != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_Outline, "Failed to draw to SVG canvas."); + } + res = sw_canvas->sync(); + if (res != tvg::Result::Success) { + ERR_FAIL_V_MSG(FT_Err_Invalid_Outline, "Failed to sync SVG canvas."); + } + + state->glyph_map.erase(p_slot->glyph_index); + + p_slot->bitmap.pixel_mode = FT_PIXEL_MODE_BGRA; + p_slot->bitmap.num_grays = 256; + p_slot->format = FT_GLYPH_FORMAT_BITMAP; + + return FT_Err_Ok; +} + +SVG_RendererHooks tvg_svg_in_ot_hooks = { + (SVG_Lib_Init_Func)tvg_svg_in_ot_init, + (SVG_Lib_Free_Func)tvg_svg_in_ot_free, + (SVG_Lib_Render_Func)tvg_svg_in_ot_render, + (SVG_Lib_Preset_Slot_Func)tvg_svg_in_ot_preset_slot, +}; + +SVG_RendererHooks *get_tvg_svg_in_ot_hooks() { + return &tvg_svg_in_ot_hooks; +} + +#endif // MODULE_SVG_ENABLED diff --git a/modules/text_server_fb/thorvg_svg_in_ot.h b/modules/text_server_fb/thorvg_svg_in_ot.h new file mode 100644 index 0000000000..b2816193d9 --- /dev/null +++ b/modules/text_server_fb/thorvg_svg_in_ot.h @@ -0,0 +1,86 @@ +/*************************************************************************/ +/* thorvg_svg_in_ot.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 THORVG_SVG_IN_OT_H +#define THORVG_SVG_IN_OT_H + +#ifdef GDEXTENSION +// Headers for building as GDExtension plug-in. + +#include <godot_cpp/core/mutex_lock.hpp> +#include <godot_cpp/godot.hpp> +#include <godot_cpp/templates/hash_map.hpp> + +using namespace godot; + +#else +// Headers for building as built-in module. + +#include "core/os/mutex.h" +#include "core/templates/hash_map.h" +#include "core/typedefs.h" + +#include "modules/modules_enabled.gen.h" // For svg. +#endif + +#ifdef MODULE_SVG_ENABLED + +#include <freetype/freetype.h> +#include <freetype/otsvg.h> +#include <ft2build.h> +#include <thorvg.h> + +struct GL_State { + bool ready = false; + float bmp_x = 0; + float bmp_y = 0; + float x = 0; + float y = 0; + float w = 0; + float h = 0; + String xml_code; + tvg::Matrix m; +}; + +struct TVG_State { + Mutex mutex; + HashMap<uint32_t, GL_State> glyph_map; +}; + +FT_Error tvg_svg_in_ot_init(FT_Pointer *p_state); +void tvg_svg_in_ot_free(FT_Pointer *p_state); +FT_Error tvg_svg_in_ot_preset_slot(FT_GlyphSlot p_slot, FT_Bool p_cache, FT_Pointer *p_state); +FT_Error tvg_svg_in_ot_render(FT_GlyphSlot p_slot, FT_Pointer *p_state); + +SVG_RendererHooks *get_tvg_svg_in_ot_hooks(); + +#endif // MODULE_SVG_ENABLED + +#endif // THORVG_SVG_IN_OT_H diff --git a/modules/zip/zip_packer.cpp b/modules/zip/zip_packer.cpp index e62700f191..5566848087 100644 --- a/modules/zip/zip_packer.cpp +++ b/modules/zip/zip_packer.cpp @@ -46,7 +46,11 @@ Error ZIPPacker::open(String p_path, ZipAppend p_append) { Error ZIPPacker::close() { ERR_FAIL_COND_V_MSG(fa.is_null(), FAILED, "ZIPPacker cannot be closed because it is not open."); - return zipClose(zf, NULL) == ZIP_OK ? OK : FAILED; + Error err = zipClose(zf, NULL) == ZIP_OK ? OK : FAILED; + if (err == OK) { + zf = NULL; + } + return err; } Error ZIPPacker::start_file(String p_path) { @@ -79,11 +83,7 @@ Error ZIPPacker::write_file(Vector<uint8_t> p_data) { Error ZIPPacker::close_file() { ERR_FAIL_COND_V_MSG(fa.is_null(), FAILED, "ZIPPacker must be opened before use."); - Error err = zipCloseFileInZip(zf) == ZIP_OK ? OK : FAILED; - if (err == OK) { - zf = NULL; - } - return err; + return zipCloseFileInZip(zf) == ZIP_OK ? OK : FAILED; } void ZIPPacker::_bind_methods() { |