diff options
28 files changed, 2122 insertions, 27 deletions
diff --git a/SConstruct b/SConstruct index 4e0f95a6c2..0fd9326e1c 100644 --- a/SConstruct +++ b/SConstruct @@ -195,7 +195,7 @@ opts.Add("extra_suffix", "Custom extra suffix added to the base filename of all  opts.Add(BoolVariable("vsproj", "Generate a Visual Studio solution", False))  opts.Add(BoolVariable("disable_3d", "Disable 3D nodes for a smaller executable", False))  opts.Add(BoolVariable("disable_advanced_gui", "Disable advanced GUI nodes and behaviors", False)) -opts.Add("disable_classes", "Disable given classes (comma separated)", "") +opts.Add("build_feature_profile", "Path to a file containing a feature build profile", "")  opts.Add(BoolVariable("modules_enabled_by_default", "If no, disable all modules except ones explicitly enabled", True))  opts.Add(BoolVariable("no_editor_splash", "Don't use the custom splash screen for the editor", True))  opts.Add("system_certs_path", "Use this path as SSL certificates default for editor (for package maintainers)", "") @@ -752,7 +752,27 @@ if selected_platform in platform_list:      if env["tools"]:          env.Append(CPPDEFINES=["TOOLS_ENABLED"]) -    methods.write_disabled_classes(env["disable_classes"].split(",")) + +    disabled_classes = [] + +    if env["build_feature_profile"] != "": +        print("Using build feature profile: " + env["build_feature_profile"]) +        import json + +        try: +            ft = json.load(open(env["build_feature_profile"])) +            if "disabled_classes" in ft: +                disabled_classes = ft["disabled_classes"] +            if "disabled_build_options" in ft: +                dbo = ft["disabled_build_options"] +                for c in dbo: +                    env[c] = dbo[c] +        except: +            print("Error opening feature build profile: " + env["build_feature_profile"]) +            Exit(255) + +    methods.write_disabled_classes(disabled_classes) +      if env["disable_3d"]:          if env["tools"]:              print( diff --git a/core/io/resource_format_binary.cpp b/core/io/resource_format_binary.cpp index b1c50e829c..0f4bc1e19c 100644 --- a/core/io/resource_format_binary.cpp +++ b/core/io/resource_format_binary.cpp @@ -865,6 +865,22 @@ String ResourceLoaderBinary::get_unicode_string() {  	return s;  } +void ResourceLoaderBinary::get_classes_used(Ref<FileAccess> p_f, HashSet<StringName> *p_classes) { +	open(p_f, false, true); +	if (error) { +		return; +	} + +	for (int i = 0; i < internal_resources.size(); i++) { +		p_f->seek(internal_resources[i].offset); +		String t = get_unicode_string(); +		ERR_FAIL_COND(p_f->get_error() != OK); +		if (t != String()) { +			p_classes->insert(t); +		} +	} +} +  void ResourceLoaderBinary::get_dependencies(Ref<FileAccess> p_f, List<String> *p_dependencies, bool p_add_types) {  	open(p_f, false, true);  	if (error) { @@ -1337,6 +1353,16 @@ Error ResourceFormatLoaderBinary::rename_dependencies(const String &p_path, cons  	return OK;  } +void ResourceFormatLoaderBinary::get_classes_used(const String &p_path, HashSet<StringName> *r_classes) { +	Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); +	ERR_FAIL_COND_MSG(f.is_null(), "Cannot open file '" + p_path + "'."); + +	ResourceLoaderBinary loader; +	loader.local_path = ProjectSettings::get_singleton()->localize_path(p_path); +	loader.res_path = loader.local_path; +	loader.get_classes_used(f, r_classes); +} +  String ResourceFormatLoaderBinary::get_resource_type(const String &p_path) const {  	Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);  	if (f.is_null()) { diff --git a/core/io/resource_format_binary.h b/core/io/resource_format_binary.h index 5da880ddb8..2b043302fd 100644 --- a/core/io/resource_format_binary.h +++ b/core/io/resource_format_binary.h @@ -101,6 +101,7 @@ public:  	void open(Ref<FileAccess> p_f, bool p_no_resources = false, bool p_keep_uuid_paths = false);  	String recognize(Ref<FileAccess> p_f);  	void get_dependencies(Ref<FileAccess> p_f, List<String> *p_dependencies, bool p_add_types); +	void get_classes_used(Ref<FileAccess> p_f, HashSet<StringName> *p_classes);  	ResourceLoaderBinary() {}  }; @@ -112,6 +113,7 @@ public:  	virtual void get_recognized_extensions(List<String> *p_extensions) const;  	virtual bool handles_type(const String &p_type) const;  	virtual String get_resource_type(const String &p_path) const; +	virtual void get_classes_used(const String &p_path, HashSet<StringName> *r_classes);  	virtual ResourceUID::ID get_resource_uid(const String &p_path) const;  	virtual void get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types = false);  	virtual Error rename_dependencies(const String &p_path, const HashMap<String, String> &p_map); diff --git a/core/io/resource_importer.cpp b/core/io/resource_importer.cpp index 934cb780e6..e059fc842b 100644 --- a/core/io/resource_importer.cpp +++ b/core/io/resource_importer.cpp @@ -352,6 +352,16 @@ Variant ResourceFormatImporter::get_resource_metadata(const String &p_path) cons  	return pat.metadata;  } +void ResourceFormatImporter::get_classes_used(const String &p_path, HashSet<StringName> *r_classes) { +	PathAndType pat; +	Error err = _get_path_and_type(p_path, pat); + +	if (err != OK) { +		return; +	} + +	ResourceLoader::get_classes_used(pat.path, r_classes); +}  void ResourceFormatImporter::get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types) {  	PathAndType pat; diff --git a/core/io/resource_importer.h b/core/io/resource_importer.h index 0c7909df06..d0ea98b598 100644 --- a/core/io/resource_importer.h +++ b/core/io/resource_importer.h @@ -65,12 +65,12 @@ public:  	virtual bool handles_type(const String &p_type) const;  	virtual String get_resource_type(const String &p_path) const;  	virtual ResourceUID::ID get_resource_uid(const String &p_path) const; -  	virtual Variant get_resource_metadata(const String &p_path) const;  	virtual bool is_import_valid(const String &p_path) const;  	virtual void get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types = false);  	virtual bool is_imported(const String &p_path) const { return recognize_path(p_path); }  	virtual String get_import_group_file(const String &p_path) const; +	virtual void get_classes_used(const String &p_path, HashSet<StringName> *r_classes);  	virtual bool exists(const String &p_path) const;  	virtual int get_import_order(const String &p_path) const; diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp index 2cd455475c..fc4177004b 100644 --- a/core/io/resource_loader.cpp +++ b/core/io/resource_loader.cpp @@ -76,6 +76,21 @@ bool ResourceFormatLoader::handles_type(const String &p_type) const {  	return false;  } +void ResourceFormatLoader::get_classes_used(const String &p_path, HashSet<StringName> *r_classes) { +	Vector<String> ret; +	if (GDVIRTUAL_CALL(_get_classes_used, p_path, ret)) { +		for (int i = 0; i < ret.size(); i++) { +			r_classes->insert(ret[i]); +		} +		return; +	} + +	String res = get_resource_type(p_path); +	if (!res.is_empty()) { +		r_classes->insert(res); +	} +} +  String ResourceFormatLoader::get_resource_type(const String &p_path) const {  	String ret; @@ -180,6 +195,7 @@ void ResourceFormatLoader::_bind_methods() {  	GDVIRTUAL_BIND(_get_dependencies, "path", "add_types");  	GDVIRTUAL_BIND(_rename_dependencies, "path", "renames");  	GDVIRTUAL_BIND(_exists, "path"); +	GDVIRTUAL_BIND(_get_classes_used, "path");  	GDVIRTUAL_BIND(_load, "path", "original_path", "use_sub_threads", "cache_mode");  } @@ -730,6 +746,18 @@ Error ResourceLoader::rename_dependencies(const String &p_path, const HashMap<St  	return OK; // ??  } +void ResourceLoader::get_classes_used(const String &p_path, HashSet<StringName> *r_classes) { +	String local_path = _validate_local_path(p_path); + +	for (int i = 0; i < loader_count; i++) { +		if (!loader[i]->recognize_path(local_path)) { +			continue; +		} + +		return loader[i]->get_classes_used(p_path, r_classes); +	} +} +  String ResourceLoader::get_resource_type(const String &p_path) {  	String local_path = _validate_local_path(p_path); diff --git a/core/io/resource_loader.h b/core/io/resource_loader.h index 815dd1dd72..91ba930176 100644 --- a/core/io/resource_loader.h +++ b/core/io/resource_loader.h @@ -55,6 +55,7 @@ protected:  	GDVIRTUAL1RC(String, _get_resource_type, String)  	GDVIRTUAL1RC(ResourceUID::ID, _get_resource_uid, String)  	GDVIRTUAL2RC(Vector<String>, _get_dependencies, String, bool) +	GDVIRTUAL1RC(Vector<String>, _get_classes_used, String)  	GDVIRTUAL2RC(int64_t, _rename_dependencies, String, Dictionary)  	GDVIRTUAL1RC(bool, _exists, String) @@ -67,6 +68,7 @@ public:  	virtual void get_recognized_extensions_for_type(const String &p_type, List<String> *p_extensions) const;  	virtual bool recognize_path(const String &p_path, const String &p_for_type = String()) const;  	virtual bool handles_type(const String &p_type) const; +	virtual void get_classes_used(const String &p_path, HashSet<StringName> *r_classes);  	virtual String get_resource_type(const String &p_path) const;  	virtual ResourceUID::ID get_resource_uid(const String &p_path) const;  	virtual void get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types = false); @@ -170,6 +172,7 @@ public:  	static void get_recognized_extensions_for_type(const String &p_type, List<String> *p_extensions);  	static void add_resource_format_loader(Ref<ResourceFormatLoader> p_format_loader, bool p_at_front = false);  	static void remove_resource_format_loader(Ref<ResourceFormatLoader> p_format_loader); +	static void get_classes_used(const String &p_path, HashSet<StringName> *r_classes);  	static String get_resource_type(const String &p_path);  	static ResourceUID::ID get_resource_uid(const String &p_path);  	static void get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types = false); diff --git a/core/object/class_db.cpp b/core/object/class_db.cpp index ac008dad88..d67315f20d 100644 --- a/core/object/class_db.cpp +++ b/core/object/class_db.cpp @@ -305,6 +305,13 @@ void ClassDB::add_compatibility_class(const StringName &p_class, const StringNam  	compat_classes[p_class] = p_fallback;  } +StringName ClassDB::get_compatibility_class(const StringName &p_class) { +	if (compat_classes.has(p_class)) { +		return compat_classes[p_class]; +	} +	return StringName(); +} +  Object *ClassDB::instantiate(const StringName &p_class) {  	ClassInfo *ti;  	{ diff --git a/core/object/class_db.h b/core/object/class_db.h index 1d26eb18f1..8b6a260d86 100644 --- a/core/object/class_db.h +++ b/core/object/class_db.h @@ -357,6 +357,7 @@ public:  	static bool is_resource_extension(const StringName &p_extension);  	static void add_compatibility_class(const StringName &p_class, const StringName &p_fallback); +	static StringName get_compatibility_class(const StringName &p_class);  	static void set_current_api(APIType p_api);  	static APIType get_current_api(); @@ -418,16 +419,16 @@ _FORCE_INLINE_ Vector<Error> errarray(P... p_args) {  #endif -#define GDREGISTER_CLASS(m_class)                    \ -	if (!GD_IS_DEFINED(ClassDB_Disable_##m_class)) { \ -		::ClassDB::register_class<m_class>();        \ +#define GDREGISTER_CLASS(m_class)             \ +	if (m_class::_class_is_enabled) {         \ +		::ClassDB::register_class<m_class>(); \  	} -#define GDREGISTER_VIRTUAL_CLASS(m_class)            \ -	if (!GD_IS_DEFINED(ClassDB_Disable_##m_class)) { \ -		::ClassDB::register_class<m_class>(true);    \ +#define GDREGISTER_VIRTUAL_CLASS(m_class)         \ +	if (m_class::_class_is_enabled) {             \ +		::ClassDB::register_class<m_class>(true); \  	}  #define GDREGISTER_ABSTRACT_CLASS(m_class)             \ -	if (!GD_IS_DEFINED(ClassDB_Disable_##m_class)) {   \ +	if (m_class::_class_is_enabled) {                  \  		::ClassDB::register_abstract_class<m_class>(); \  	} diff --git a/core/object/object.h b/core/object/object.h index 705d6451dc..17f75a4e1d 100644 --- a/core/object/object.h +++ b/core/object/object.h @@ -358,6 +358,7 @@ private:  	friend class ::ClassDB;                                                                                                                      \                                                                                                                                                   \  public:                                                                                                                                          \ +	static constexpr bool _class_is_enabled = !bool(GD_IS_DEFINED(ClassDB_Disable_##m_class)) && m_inherits::_class_is_enabled;                  \  	virtual String get_class() const override {                                                                                                  \  		if (_get_extension()) {                                                                                                                  \  			return _get_extension()->class_name.operator String();                                                                               \ @@ -667,6 +668,8 @@ public: // Should be protected, but bug in clang++.  	_FORCE_INLINE_ static void register_custom_data_to_otdb() {}  public: +	static constexpr bool _class_is_enabled = true; +  	void notify_property_list_changed();  	static void *get_class_ptr_static() { diff --git a/core/object/worker_thread_pool.cpp b/core/object/worker_thread_pool.cpp new file mode 100644 index 0000000000..c276802f99 --- /dev/null +++ b/core/object/worker_thread_pool.cpp @@ -0,0 +1,463 @@ +/*************************************************************************/ +/*  worker_thread_pool.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 "worker_thread_pool.h" + +#include "core/os/os.h" + +WorkerThreadPool *WorkerThreadPool::singleton = nullptr; + +void WorkerThreadPool::_process_task_queue() { +	task_mutex.lock(); +	Task *task = task_queue.first()->self(); +	task_queue.remove(task_queue.first()); +	task_mutex.unlock(); +	_process_task(task); +} + +void WorkerThreadPool::_process_task(Task *p_task) { +	bool low_priority = p_task->low_priority; + +	if (p_task->group) { +		// Handling a group +		bool do_post = false; +		if (p_task->native_group_func) { +			while (true) { +				uint32_t work_index = p_task->group->index.postincrement(); +				if (work_index >= p_task->group->max) { +					do_post = work_index == p_task->group->max; // First one reaching max handles semaphore and clean-up. +					break; +				} +				p_task->native_group_func(p_task->native_func_userdata, work_index); +			} + +		} else { +			Callable::CallError ce; +			Variant ret; +			Variant arg; +			Variant *argptr = &arg; +			while (true) { +				uint32_t work_index = p_task->group->index.postincrement(); +				if (work_index >= p_task->group->max) { +					do_post = work_index == p_task->group->max; // First one reaching max handles semaphore and clean-up. +					break; +				} +				arg = work_index; +				p_task->callable.call((const Variant **)&argptr, 1, ret, ce); +			} +		} + +		if (low_priority && use_native_low_priority_threads) { +			p_task->completed = true; +			p_task->done_semaphore.post(); +			if (do_post) { +				p_task->group->completed.set_to(true); +			} +		} else { +			if (do_post) { +				p_task->group->done_semaphore.post(); +				p_task->group->completed.set_to(true); +			} +			uint32_t max_users = p_task->group->tasks_used + 1; // Add 1 because the thread waiting for it is also user. Read before to avoid another thread freeing task after increment. +			uint32_t finished_users = p_task->group->finished.increment(); + +			if (finished_users == max_users) { +				// Get rid of the group, because nobody else is using it. +				task_mutex.lock(); +				group_allocator.free(p_task->group); +				task_mutex.unlock(); +			} + +			// For groups, tasks get rid of themselves. + +			task_mutex.lock(); +			task_allocator.free(p_task); +			task_mutex.unlock(); +		} +	} else { +		if (p_task->native_func) { +			p_task->native_func(p_task->native_func_userdata); +		} else { +			Callable::CallError ce; +			Variant ret; +			p_task->callable.call(nullptr, 0, ret, ce); +		} + +		p_task->completed = true; +		p_task->done_semaphore.post(); +	} + +	if (!use_native_low_priority_threads && low_priority) { +		// A low prioriry task was freed, so see if we can move a pending one to the high priority queue. +		bool post = false; +		task_mutex.lock(); +		if (low_priority_task_queue.first()) { +			Task *low_prio_task = low_priority_task_queue.first()->self(); +			low_priority_task_queue.remove(low_priority_task_queue.first()); +			task_queue.add_last(&low_prio_task->task_elem); +			post = true; +		} else { +			low_priority_threads_used.decrement(); +		} +		task_mutex.lock(); +		if (post) { +			task_available_semaphore.post(); +		} +	} +} + +void WorkerThreadPool::_thread_function(void *p_user) { +	while (true) { +		singleton->task_available_semaphore.wait(); +		if (singleton->exit_threads.is_set()) { +			break; +		} +		singleton->_process_task_queue(); +	} +} + +void WorkerThreadPool::_native_low_priority_thread_function(void *p_user) { +	Task *task = (Task *)p_user; +	singleton->_process_task(task); +} + +void WorkerThreadPool::_post_task(Task *p_task, bool p_high_priority) { +	task_mutex.lock(); +	p_task->low_priority = !p_high_priority; +	if (!p_high_priority && use_native_low_priority_threads) { +		task_mutex.unlock(); +		p_task->low_priority_thread = native_thread_allocator.alloc(); +		p_task->low_priority_thread->start(_native_low_priority_thread_function, p_task); // Pask task directly to thread. + +	} else if (p_high_priority || low_priority_threads_used.get() < max_low_priority_threads) { +		task_queue.add_last(&p_task->task_elem); +		if (!p_high_priority) { +			low_priority_threads_used.increment(); +		} +		task_mutex.unlock(); +		task_available_semaphore.post(); +	} else { +		// Too many threads using low priority, must go to queue. +		low_priority_task_queue.add_last(&p_task->task_elem); +		task_mutex.unlock(); +	} +} + +WorkerThreadPool::TaskID WorkerThreadPool::add_native_task(void (*p_func)(void *), void *p_userdata, bool p_high_priority, const String &p_description) { +	task_mutex.lock(); +	// Get a free task +	Task *task = task_allocator.alloc(); +	TaskID id = last_task++; +	task->native_func = p_func; +	task->native_func_userdata = p_userdata; +	task->description = p_description; +	tasks.insert(id, task); +	task_mutex.unlock(); + +	_post_task(task, p_high_priority); + +	return id; +} + +WorkerThreadPool::TaskID WorkerThreadPool::add_task(const Callable &p_action, bool p_high_priority, const String &p_description) { +	task_mutex.lock(); +	// Get a free task +	Task *task = task_allocator.alloc(); +	TaskID id = last_task++; +	task->callable = p_action; +	task->description = p_description; +	tasks.insert(id, task); +	task_mutex.unlock(); + +	_post_task(task, p_high_priority); + +	return id; +} + +bool WorkerThreadPool::is_task_completed(TaskID p_task_id) const { +	task_mutex.lock(); +	const Task *const *taskp = tasks.getptr(p_task_id); +	if (!taskp) { +		task_mutex.unlock(); +		ERR_FAIL_V_MSG(false, "Invalid Task ID"); // Invalid task +	} + +	bool completed = (*taskp)->completed; +	task_mutex.unlock(); + +	return completed; +} + +void WorkerThreadPool::wait_for_task_completion(TaskID p_task_id) { +	task_mutex.lock(); +	Task **taskp = tasks.getptr(p_task_id); +	if (!taskp) { +		task_mutex.unlock(); +		ERR_FAIL_MSG("Invalid Task ID"); // Invalid task +	} +	Task *task = *taskp; + +	if (task->waiting) { +		String description = task->description; +		task_mutex.unlock(); +		if (description.is_empty()) { +			ERR_FAIL_MSG("Another thread is waiting on this task: " + itos(p_task_id)); // Invalid task +		} else { +			ERR_FAIL_MSG("Another thread is waiting on this task: " + description + " (" + itos(p_task_id) + ")"); // Invalid task +		} +	} + +	task->waiting = true; + +	task_mutex.unlock(); + +	if (use_native_low_priority_threads && task->low_priority) { +		task->low_priority_thread->wait_to_finish(); +		native_thread_allocator.free(task->low_priority_thread); +	} else { +		int *index = thread_ids.getptr(Thread::get_caller_id()); + +		if (index) { +			// We are an actual process thread, we must not be blocked so continue processing stuff if available. +			while (true) { +				if (task->done_semaphore.try_wait()) { +					// If done, exit +					break; +				} +				if (task_available_semaphore.try_wait()) { +					// Solve tasks while they are around. +					_process_task_queue(); +					continue; +				} +				OS::get_singleton()->delay_usec(1); // Microsleep, this could be converted to waiting for multiple objects in supported platforms for a bit more performance. +			} +		} else { +			task->done_semaphore.wait(); +		} +	} + +	task_mutex.lock(); +	tasks.erase(p_task_id); +	task_allocator.free(task); +	task_mutex.unlock(); +} + +WorkerThreadPool::GroupID WorkerThreadPool::add_native_group_task(void (*p_func)(void *, uint32_t), void *p_userdata, int p_elements, int p_tasks, bool p_high_priority, const String &p_description) { +	ERR_FAIL_COND_V(p_elements <= 0, INVALID_TASK_ID); +	if (p_tasks < 0) { +		p_tasks = threads.size(); +	} + +	task_mutex.lock(); +	Group *group = group_allocator.alloc(); +	GroupID id = last_task++; +	group->max = p_elements; +	group->self = id; +	group->tasks_used = p_tasks; +	Task **tasks_posted = (Task **)alloca(sizeof(Task *) * p_tasks); +	for (int i = 0; i < p_tasks; i++) { +		Task *task = task_allocator.alloc(); +		task->native_group_func = p_func; +		task->native_func_userdata = p_userdata; +		task->description = p_description; +		task->group = group; +		tasks_posted[i] = task; +		// No task ID is used. +	} +	groups[id] = group; +	task_mutex.unlock(); + +	if (!p_high_priority && use_native_low_priority_threads) { +		group->low_priority_native_tasks.resize(p_tasks); +	} + +	for (int i = 0; i < p_tasks; i++) { +		_post_task(tasks_posted[i], p_high_priority); +		if (!p_high_priority && use_native_low_priority_threads) { +			group->low_priority_native_tasks[i] = tasks_posted[i]; +		} +	} + +	return id; +} + +WorkerThreadPool::GroupID WorkerThreadPool::add_group_task(const Callable &p_action, int p_elements, int p_tasks, bool p_high_priority, const String &p_description) { +	ERR_FAIL_COND_V(p_elements <= 0, INVALID_TASK_ID); +	if (p_tasks < 0) { +		p_tasks = threads.size(); +	} + +	task_mutex.lock(); +	Group *group = group_allocator.alloc(); +	GroupID id = last_task++; +	group->max = p_elements; +	group->self = id; +	group->tasks_used = p_tasks; +	Task **tasks_posted = (Task **)alloca(sizeof(Task *) * p_tasks); +	for (int i = 0; i < p_tasks; i++) { +		Task *task = task_allocator.alloc(); +		task->callable = p_action; +		task->description = p_description; +		task->group = group; +		tasks_posted[i] = task; +		// No task ID is used. +	} +	groups[id] = group; +	task_mutex.unlock(); + +	if (!p_high_priority && use_native_low_priority_threads) { +		group->low_priority_native_tasks.resize(p_tasks); +	} + +	for (int i = 0; i < p_tasks; i++) { +		_post_task(tasks_posted[i], p_high_priority); +		if (!p_high_priority && use_native_low_priority_threads) { +			group->low_priority_native_tasks[i] = tasks_posted[i]; +		} +	} +	return id; +} + +bool WorkerThreadPool::is_group_task_completed(GroupID p_group) const { +	task_mutex.lock(); +	const Group *const *groupp = groups.getptr(p_group); +	if (!groupp) { +		task_mutex.unlock(); +		ERR_FAIL_V_MSG(false, "Invalid Group ID"); +	} +	bool completed = (*groupp)->completed.is_set(); +	task_mutex.unlock(); +	return completed; +} + +void WorkerThreadPool::wait_for_group_task_completion(GroupID p_group) { +	task_mutex.lock(); +	Group **groupp = groups.getptr(p_group); +	task_mutex.unlock(); +	if (!groupp) { +		ERR_FAIL_MSG("Invalid Group ID"); +	} +	Group *group = *groupp; + +	if (group->low_priority_native_tasks.size() > 0) { +		for (uint32_t i = 0; i < group->low_priority_native_tasks.size(); i++) { +			group->low_priority_native_tasks[i]->low_priority_thread->wait_to_finish(); +			native_thread_allocator.free(group->low_priority_native_tasks[i]->low_priority_thread); +			task_mutex.lock(); +			task_allocator.free(group->low_priority_native_tasks[i]); +			task_mutex.unlock(); +		} + +		task_mutex.lock(); +		group_allocator.free(group); +		task_mutex.unlock(); +	} else { +		group->done_semaphore.wait(); + +		uint32_t max_users = group->tasks_used + 1; // Add 1 because the thread waiting for it is also user. Read before to avoid another thread freeing task after increment. +		uint32_t finished_users = group->finished.increment(); // fetch happens before inc, so increment later. + +		if (finished_users == max_users) { +			// All tasks using this group are gone (finished before the group), so clear the gorup too. +			task_mutex.lock(); +			group_allocator.free(group); +			task_mutex.unlock(); +		} +	} + +	groups.erase(p_group); // Threads do not access this, so safe to erase here. +} + +void WorkerThreadPool::init(int p_thread_count, bool p_use_native_threads_low_priority, float p_low_priority_task_ratio) { +	ERR_FAIL_COND(threads.size() > 0); +	if (p_thread_count < 0) { +		p_thread_count = OS::get_singleton()->get_default_thread_pool_size(); +	} + +	if (p_use_native_threads_low_priority) { +		max_low_priority_threads = 0; +	} else { +		max_low_priority_threads = CLAMP(p_thread_count * p_low_priority_task_ratio, 1, p_thread_count); +	} + +	use_native_low_priority_threads = p_use_native_threads_low_priority; + +	threads.resize(p_thread_count); + +	for (uint32_t i = 0; i < threads.size(); i++) { +		threads[i].index = i; +		threads[i].thread.start(&WorkerThreadPool::_thread_function, &threads[i]); +		thread_ids.insert(threads[i].thread.get_id(), i); +	} +} + +void WorkerThreadPool::finish() { +	if (threads.size() == 0) { +		return; +	} + +	task_mutex.lock(); +	SelfList<Task> *E = low_priority_task_queue.first(); +	while (E) { +		print_error("Task waiting was never re-claimed: " + E->self()->description); +		E = E->next(); +	} +	task_mutex.unlock(); + +	exit_threads.set_to(true); + +	for (uint32_t i = 0; i < threads.size(); i++) { +		task_available_semaphore.post(); +	} + +	for (uint32_t i = 0; i < threads.size(); i++) { +		threads[i].thread.wait_to_finish(); +	} + +	threads.clear(); +} + +void WorkerThreadPool::_bind_methods() { +	ClassDB::bind_method(D_METHOD("add_task", "action", "high_priority", "description"), &WorkerThreadPool::add_task, DEFVAL(false), DEFVAL(String())); +	ClassDB::bind_method(D_METHOD("is_task_completed", "task_id"), &WorkerThreadPool::is_task_completed); +	ClassDB::bind_method(D_METHOD("wait_for_task_completion", "task_id"), &WorkerThreadPool::wait_for_task_completion); + +	ClassDB::bind_method(D_METHOD("add_group_task", "action", "elements", "tasks_needed", "high_priority", "description"), &WorkerThreadPool::add_group_task, DEFVAL(-1), DEFVAL(false), DEFVAL(String())); +	ClassDB::bind_method(D_METHOD("is_group_task_completed", "group_id"), &WorkerThreadPool::is_group_task_completed); +	ClassDB::bind_method(D_METHOD("wait_for_group_task_completion", "group_id"), &WorkerThreadPool::wait_for_group_task_completion); +} + +WorkerThreadPool::WorkerThreadPool() { +	singleton = this; +} + +WorkerThreadPool::~WorkerThreadPool() { +	finish(); +} diff --git a/core/object/worker_thread_pool.h b/core/object/worker_thread_pool.h new file mode 100644 index 0000000000..dfb0050605 --- /dev/null +++ b/core/object/worker_thread_pool.h @@ -0,0 +1,146 @@ +/*************************************************************************/ +/*  worker_thread_pool.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 WORKER_THREAD_POOL_H +#define WORKER_THREAD_POOL_H + +#include "core/os/memory.h" +#include "core/os/os.h" +#include "core/os/semaphore.h" +#include "core/os/thread.h" +#include "core/templates/local_vector.h" +#include "core/templates/paged_allocator.h" +#include "core/templates/rid.h" +#include "core/templates/safe_refcount.h" + +class WorkerThreadPool : public Object { +	GDCLASS(WorkerThreadPool, Object) +public: +	enum { +		INVALID_TASK_ID = -1 +	}; + +	typedef int64_t TaskID; +	typedef int64_t GroupID; + +private: +	struct Task; + +	struct Group { +		GroupID self; +		SafeNumeric<uint32_t> index; +		uint32_t max = 0; +		Semaphore done_semaphore; +		SafeFlag completed; +		SafeNumeric<uint32_t> finished; +		uint32_t tasks_used = 0; +		TightLocalVector<Task *> low_priority_native_tasks; +	}; + +	struct Task { +		Callable callable; +		void (*native_func)(void *) = nullptr; +		void (*native_group_func)(void *, uint32_t) = nullptr; +		void *native_func_userdata = nullptr; +		String description; +		Semaphore done_semaphore; +		bool completed = false; +		Group *group = nullptr; +		SelfList<Task> task_elem; +		bool waiting = false; // Waiting for completion +		bool low_priority = false; +		Thread *low_priority_thread = nullptr; +		Task() : +				task_elem(this) {} +	}; + +	PagedAllocator<Task> task_allocator; +	PagedAllocator<Group> group_allocator; +	PagedAllocator<Thread> native_thread_allocator; + +	SelfList<Task>::List low_priority_task_queue; +	SelfList<Task>::List task_queue; + +	Mutex task_mutex; +	Semaphore task_available_semaphore; + +	struct ThreadData { +		uint32_t index; +		Thread thread; +	}; + +	TightLocalVector<ThreadData> threads; +	SafeFlag exit_threads; + +	HashMap<Thread::ID, int> thread_ids; +	HashMap<TaskID, Task *> tasks; +	HashMap<GroupID, Group *> groups; + +	bool use_native_low_priority_threads = false; +	uint32_t max_low_priority_threads = 0; +	SafeNumeric<uint32_t> low_priority_threads_used; + +	uint64_t last_task = 1; + +	static void _thread_function(void *p_user); +	static void _native_low_priority_thread_function(void *p_user); + +	void _process_task_queue(); +	void _process_task(Task *task); + +	void _post_task(Task *p_task, bool p_high_priority); + +	static WorkerThreadPool *singleton; + +protected: +	static void _bind_methods(); + +public: +	TaskID add_native_task(void (*p_func)(void *), void *p_userdata, bool p_high_priority = false, const String &p_description = String()); +	TaskID add_task(const Callable &p_action, bool p_high_priority = false, const String &p_description = String()); + +	bool is_task_completed(TaskID p_task_id) const; +	void wait_for_task_completion(TaskID p_task_id); + +	GroupID add_native_group_task(void (*p_func)(void *, uint32_t), void *p_userdata, int p_elements, int p_tasks = -1, bool p_high_priority = false, const String &p_description = String()); +	GroupID add_group_task(const Callable &p_action, int p_elements, int p_tasks = -1, bool p_high_priority = false, const String &p_description = String()); +	bool is_group_task_completed(GroupID p_group) const; +	void wait_for_group_task_completion(GroupID p_group); + +	_FORCE_INLINE_ int get_thread_count() const { return threads.size(); } + +	static WorkerThreadPool *get_singleton() { return singleton; } +	void init(int p_thread_count = -1, bool p_use_native_threads_low_priority = true, float p_low_priority_task_ratio = 0.3); +	void finish(); +	WorkerThreadPool(); +	~WorkerThreadPool(); +}; + +#endif // WORKER_THREAD_POOL_H diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp index e60d325f74..aab4b9a580 100644 --- a/core/register_core_types.cpp +++ b/core/register_core_types.cpp @@ -74,6 +74,7 @@  #include "core/object/class_db.h"  #include "core/object/script_language_extension.h"  #include "core/object/undo_redo.h" +#include "core/object/worker_thread_pool.h"  #include "core/os/main_loop.h"  #include "core/os/time.h"  #include "core/string/optimized_translation.h" @@ -101,6 +102,8 @@ static IP *ip = nullptr;  static core_bind::Geometry2D *_geometry_2d = nullptr;  static core_bind::Geometry3D *_geometry_3d = nullptr; +static WorkerThreadPool *worker_thread_pool = nullptr; +  extern Mutex _global_mutex;  static NativeExtensionManager *native_extension_manager = nullptr; @@ -189,6 +192,8 @@ void register_core_types() {  	GDREGISTER_CLASS(PacketPeerUDP);  	GDREGISTER_CLASS(UDPServer); +	GDREGISTER_ABSTRACT_CLASS(WorkerThreadPool); +  	ClassDB::register_custom_instance_class<HTTPClient>();  	// Crypto @@ -271,6 +276,8 @@ void register_core_types() {  	GDREGISTER_NATIVE_STRUCT(AudioFrame, "float left;float right");  	GDREGISTER_NATIVE_STRUCT(ScriptLanguageExtensionProfilingInfo, "StringName signature;uint64_t call_count;uint64_t total_time;uint64_t self_time"); + +	worker_thread_pool = memnew(WorkerThreadPool);  }  void register_core_settings() { @@ -279,9 +286,18 @@ void register_core_settings() {  	ProjectSettings::get_singleton()->set_custom_property_info("network/limits/tcp/connect_timeout_seconds", PropertyInfo(Variant::INT, "network/limits/tcp/connect_timeout_seconds", PROPERTY_HINT_RANGE, "1,1800,1"));  	GLOBAL_DEF_RST("network/limits/packet_peer_stream/max_buffer_po2", (16));  	ProjectSettings::get_singleton()->set_custom_property_info("network/limits/packet_peer_stream/max_buffer_po2", PropertyInfo(Variant::INT, "network/limits/packet_peer_stream/max_buffer_po2", PROPERTY_HINT_RANGE, "0,64,1,or_greater")); -  	GLOBAL_DEF("network/ssl/certificate_bundle_override", "");  	ProjectSettings::get_singleton()->set_custom_property_info("network/ssl/certificate_bundle_override", PropertyInfo(Variant::STRING, "network/ssl/certificate_bundle_override", PROPERTY_HINT_FILE, "*.crt")); + +	int worker_threads = GLOBAL_DEF("threading/worker_pool/max_threads", -1); +	bool low_priority_use_system_threads = GLOBAL_DEF("threading/worker_pool/use_system_threads_for_low_priority_tasks", true); +	float low_property_ratio = GLOBAL_DEF("threading/worker_pool/low_priority_thread_ratio", 0.3); + +	if (Engine::get_singleton()->is_editor_hint() || Engine::get_singleton()->is_project_manager_hint()) { +		worker_thread_pool->init(); +	} else { +		worker_thread_pool->init(worker_threads, low_priority_use_system_threads, low_property_ratio); +	}  }  void register_core_singletons() { @@ -319,6 +335,7 @@ void register_core_singletons() {  	Engine::get_singleton()->add_singleton(Engine::Singleton("Time", Time::get_singleton()));  	Engine::get_singleton()->add_singleton(Engine::Singleton("NativeExtensionManager", NativeExtensionManager::get_singleton()));  	Engine::get_singleton()->add_singleton(Engine::Singleton("ResourceUID", ResourceUID::get_singleton())); +	Engine::get_singleton()->add_singleton(Engine::Singleton("WorkerThreadPool", worker_thread_pool));  }  void register_core_extensions() { @@ -350,6 +367,8 @@ void unregister_core_types() {  	memdelete(_geometry_2d);  	memdelete(_geometry_3d); +	memdelete(worker_thread_pool); +  	ResourceLoader::remove_resource_format_loader(resource_format_image);  	resource_format_image.unref(); diff --git a/core/templates/safe_refcount.h b/core/templates/safe_refcount.h index 3148283dca..1f6551762e 100644 --- a/core/templates/safe_refcount.h +++ b/core/templates/safe_refcount.h @@ -111,6 +111,7 @@ public:  			if (tmp >= p_value) {  				return tmp; // already greater, or equal  			} +  			if (value.compare_exchange_weak(tmp, p_value, std::memory_order_acq_rel)) {  				return p_value;  			} diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml index 7acec9e63b..fbeda641ad 100644 --- a/doc/classes/@GlobalScope.xml +++ b/doc/classes/@GlobalScope.xml @@ -1222,6 +1222,8 @@  		<member name="VisualScriptCustomNodes" type="VisualScriptCustomNodes" setter="" getter="">  			The [VisualScriptCustomNodes] singleton.  		</member> +		<member name="WorkerThreadPool" type="WorkerThreadPool" setter="" getter=""> +		</member>  		<member name="XRServer" type="XRServer" setter="" getter="">  			The [XRServer] singleton.  		</member> diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index 898d34b385..f7567133cd 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -1986,6 +1986,12 @@  		</member>  		<member name="rendering/vulkan/staging_buffer/texture_upload_region_size_px" type="int" setter="" getter="" default="64">  		</member> +		<member name="threading/worker_pool/low_priority_thread_ratio" type="float" setter="" getter="" default="0.3"> +		</member> +		<member name="threading/worker_pool/max_threads" type="int" setter="" getter="" default="-1"> +		</member> +		<member name="threading/worker_pool/use_system_threads_for_low_priority_tasks" type="bool" setter="" getter="" default="true"> +		</member>  		<member name="xr/openxr/default_action_map" type="String" setter="" getter="" default=""res://openxr_action_map.tres"">  			Action map configuration to load by default.  		</member> diff --git a/doc/classes/ResourceFormatLoader.xml b/doc/classes/ResourceFormatLoader.xml index 36b64f5a86..fef94b5f3b 100644 --- a/doc/classes/ResourceFormatLoader.xml +++ b/doc/classes/ResourceFormatLoader.xml @@ -17,6 +17,12 @@  			<description>  			</description>  		</method> +		<method name="_get_classes_used" qualifiers="virtual const"> +			<return type="PackedStringArray" /> +			<argument index="0" name="path" type="String" /> +			<description> +			</description> +		</method>  		<method name="_get_dependencies" qualifiers="virtual const">  			<return type="PackedStringArray" />  			<argument index="0" name="path" type="String" /> diff --git a/doc/classes/WorkerThreadPool.xml b/doc/classes/WorkerThreadPool.xml new file mode 100644 index 0000000000..22eabf9012 --- /dev/null +++ b/doc/classes/WorkerThreadPool.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="WorkerThreadPool" inherits="Object" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> +	<brief_description> +	</brief_description> +	<description> +	</description> +	<tutorials> +	</tutorials> +	<methods> +		<method name="add_group_task"> +			<return type="int" /> +			<argument index="0" name="action" type="Callable" /> +			<argument index="1" name="elements" type="int" /> +			<argument index="2" name="tasks_needed" type="int" default="-1" /> +			<argument index="3" name="high_priority" type="bool" default="false" /> +			<argument index="4" name="description" type="String" default="""" /> +			<description> +			</description> +		</method> +		<method name="add_task"> +			<return type="int" /> +			<argument index="0" name="action" type="Callable" /> +			<argument index="1" name="high_priority" type="bool" default="false" /> +			<argument index="2" name="description" type="String" default="""" /> +			<description> +			</description> +		</method> +		<method name="is_group_task_completed" qualifiers="const"> +			<return type="bool" /> +			<argument index="0" name="group_id" type="int" /> +			<description> +			</description> +		</method> +		<method name="is_task_completed" qualifiers="const"> +			<return type="bool" /> +			<argument index="0" name="task_id" type="int" /> +			<description> +			</description> +		</method> +		<method name="wait_for_group_task_completion"> +			<return type="void" /> +			<argument index="0" name="group_id" type="int" /> +			<description> +			</description> +		</method> +		<method name="wait_for_task_completion"> +			<return type="void" /> +			<argument index="0" name="task_id" type="int" /> +			<description> +			</description> +		</method> +	</methods> +</class> diff --git a/editor/editor_build_profile.cpp b/editor/editor_build_profile.cpp new file mode 100644 index 0000000000..6a5604290f --- /dev/null +++ b/editor/editor_build_profile.cpp @@ -0,0 +1,797 @@ +/*************************************************************************/ +/*  editor_build_profile.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_build_profile.h" + +#include "core/io/dir_access.h" +#include "core/io/json.h" +#include "editor/editor_file_dialog.h" +#include "editor/editor_file_system.h" +#include "editor/editor_node.h" +#include "editor/editor_property_name_processor.h" +#include "editor/editor_scale.h" +#include "editor/editor_settings.h" + +const char *EditorBuildProfile::build_option_identifiers[BUILD_OPTION_MAX] = { +	// This maps to SCons build options. +	"disable_3d", +	"disable_2d_physics", +	"disable_3d_physics", +	"disable_navigation", +	"openxr", +	"opengl3", +	"vulkan", +}; + +const bool EditorBuildProfile::build_option_disable_values[BUILD_OPTION_MAX] = { +	// This maps to SCons build options. +	true, +	true, +	true, +	true, +	false, +	false, +	false +}; + +void EditorBuildProfile::set_disable_class(const StringName &p_class, bool p_disabled) { +	if (p_disabled) { +		disabled_classes.insert(p_class); +	} else { +		disabled_classes.erase(p_class); +	} +} + +bool EditorBuildProfile::is_class_disabled(const StringName &p_class) const { +	if (p_class == StringName()) { +		return false; +	} +	return disabled_classes.has(p_class) || is_class_disabled(ClassDB::get_parent_class_nocheck(p_class)); +} + +void EditorBuildProfile::set_item_collapsed(const StringName &p_class, bool p_collapsed) { +	if (p_collapsed) { +		collapsed_classes.insert(p_class); +	} else { +		collapsed_classes.erase(p_class); +	} +} + +bool EditorBuildProfile::is_item_collapsed(const StringName &p_class) const { +	return collapsed_classes.has(p_class); +} + +void EditorBuildProfile::set_disable_build_option(BuildOption p_build_option, bool p_disable) { +	ERR_FAIL_INDEX(p_build_option, BUILD_OPTION_MAX); +	build_options_disabled[p_build_option] = p_disable; +} + +void EditorBuildProfile::clear_disabled_classes() { +	disabled_classes.clear(); +	collapsed_classes.clear(); +} + +bool EditorBuildProfile::is_build_option_disabled(BuildOption p_build_option) const { +	ERR_FAIL_INDEX_V(p_build_option, BUILD_OPTION_MAX, false); +	return build_options_disabled[p_build_option]; +} + +bool EditorBuildProfile::get_build_option_disable_value(BuildOption p_build_option) { +	ERR_FAIL_INDEX_V(p_build_option, BUILD_OPTION_MAX, false); +	return build_option_disable_values[p_build_option]; +} + +void EditorBuildProfile::set_force_detect_classes(const String &p_classes) { +	force_detect_classes = p_classes; +} + +String EditorBuildProfile::get_force_detect_classes() const { +	return force_detect_classes; +} + +String EditorBuildProfile::get_build_option_name(BuildOption p_build_option) { +	ERR_FAIL_INDEX_V(p_build_option, BUILD_OPTION_MAX, String()); +	const char *build_option_names[BUILD_OPTION_MAX] = { +		TTRC("3D Engine"), +		TTRC("2D Physics"), +		TTRC("3D Physics"), +		TTRC("Navigation"), +		TTRC("XR"), +		TTRC("RenderingDevice"), +		TTRC("OpenGL"), +		TTRC("Vulkan"), +	}; +	return TTRGET(build_option_names[p_build_option]); +} + +String EditorBuildProfile::get_build_option_description(BuildOption p_build_option) { +	ERR_FAIL_INDEX_V(p_build_option, BUILD_OPTION_MAX, String()); + +	const char *build_option_descriptions[BUILD_OPTION_MAX] = { +		TTRC("3D Nodes as well as RenderingServer access to 3D features."), +		TTRC("2D Physics nodes and PhysicsServer2D."), +		TTRC("3D Physics nodes and PhysicsServer3D."), +		TTRC("Navigation, both 2D and 3D."), +		TTRC("XR (AR and VR)."), +		TTRC("RenderingDevice based rendering (if disabled, the OpenGL back-end is required)."), +		TTRC("OpenGL back-end (if disabled, the RenderingDevice back-end is required)."), +		TTRC("Vulkan back-end of RenderingDevice."), +	}; + +	return TTRGET(build_option_descriptions[p_build_option]); +} + +Error EditorBuildProfile::save_to_file(const String &p_path) { +	Dictionary data; +	data["type"] = "build_profile"; +	Array dis_classes; +	for (const StringName &E : disabled_classes) { +		dis_classes.push_back(String(E)); +	} +	dis_classes.sort(); +	data["disabled_classes"] = dis_classes; + +	Dictionary dis_build_options; +	for (int i = 0; i < BUILD_OPTION_MAX; i++) { +		if (build_options_disabled[i]) { +			dis_build_options[build_option_identifiers[i]] = build_option_disable_values[i]; +		} +	} + +	data["disabled_build_options"] = dis_build_options; + +	if (!force_detect_classes.is_empty()) { +		data["force_detect_classes"] = force_detect_classes; +	} + +	Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE); +	ERR_FAIL_COND_V_MSG(f.is_null(), ERR_CANT_CREATE, "Cannot create file '" + p_path + "'."); + +	JSON json; +	String text = json.stringify(data, "\t"); +	f->store_string(text); +	return OK; +} + +Error EditorBuildProfile::load_from_file(const String &p_path) { +	Error err; +	String text = FileAccess::get_file_as_string(p_path, &err); +	if (err != OK) { +		return err; +	} + +	JSON json; +	err = json.parse(text); +	if (err != OK) { +		ERR_PRINT("Error parsing '" + p_path + "' on line " + itos(json.get_error_line()) + ": " + json.get_error_message()); +		return ERR_PARSE_ERROR; +	} + +	Dictionary data = json.get_data(); + +	if (!data.has("type") || String(data["type"]) != "build_profile") { +		ERR_PRINT("Error parsing '" + p_path + "', it's not a build profile."); +		return ERR_PARSE_ERROR; +	} + +	disabled_classes.clear(); + +	if (data.has("disabled_classes")) { +		Array disabled_classes_arr = data["disabled_classes"]; +		for (int i = 0; i < disabled_classes_arr.size(); i++) { +			disabled_classes.insert(disabled_classes_arr[i]); +		} +	} + +	for (int i = 0; i < BUILD_OPTION_MAX; i++) { +		build_options_disabled[i] = false; +	} + +	if (data.has("disabled_build_options")) { +		Dictionary disabled_build_options_arr = data["disabled_build_options"]; +		List<Variant> keys; +		disabled_build_options_arr.get_key_list(&keys); + +		for (const Variant &K : keys) { +			String key = K; + +			for (int i = 0; i < BUILD_OPTION_MAX; i++) { +				String f = build_option_identifiers[i]; +				if (f == key) { +					build_options_disabled[i] = true; +					break; +				} +			} +		} +	} + +	if (data.has("force_detect_classes")) { +		force_detect_classes = data["force_detect_classes"]; +	} + +	return OK; +} + +void EditorBuildProfile::_bind_methods() { +	ClassDB::bind_method(D_METHOD("set_disable_class", "class_name", "disable"), &EditorBuildProfile::set_disable_class); +	ClassDB::bind_method(D_METHOD("is_class_disabled", "class_name"), &EditorBuildProfile::is_class_disabled); + +	ClassDB::bind_method(D_METHOD("set_disable_build_option", "build_option", "disable"), &EditorBuildProfile::set_disable_build_option); +	ClassDB::bind_method(D_METHOD("is_build_option_disabled", "build_option"), &EditorBuildProfile::is_build_option_disabled); + +	ClassDB::bind_method(D_METHOD("get_build_option_name", "build_option"), &EditorBuildProfile::_get_build_option_name); + +	ClassDB::bind_method(D_METHOD("save_to_file", "path"), &EditorBuildProfile::save_to_file); +	ClassDB::bind_method(D_METHOD("load_from_file", "path"), &EditorBuildProfile::load_from_file); + +	BIND_ENUM_CONSTANT(BUILD_OPTION_3D); +	BIND_ENUM_CONSTANT(BUILD_OPTION_PHYSICS_2D); +	BIND_ENUM_CONSTANT(BUILD_OPTION_PHYSICS_3D); +	BIND_ENUM_CONSTANT(BUILD_OPTION_NAVIGATION); +	BIND_ENUM_CONSTANT(BUILD_OPTION_XR); +	BIND_ENUM_CONSTANT(BUILD_OPTION_RENDERING_DEVICE); +	BIND_ENUM_CONSTANT(BUILD_OPTION_OPENGL); +	BIND_ENUM_CONSTANT(BUILD_OPTION_VULKAN); +	BIND_ENUM_CONSTANT(BUILD_OPTION_MAX); +} + +EditorBuildProfile::EditorBuildProfile() {} + +////////////////////////// + +void EditorBuildProfileManager::_notification(int p_what) { +	switch (p_what) { +		case NOTIFICATION_READY: { +			String last_file = EditorSettings::get_singleton()->get_project_metadata("build_profile", "last_file_path", ""); +			if (!last_file.is_empty()) { +				_import_profile(last_file); +			} +			if (edited.is_null()) { +				edited.instantiate(); +				_update_edited_profile(); +			} + +		} break; +	} +} + +void EditorBuildProfileManager::_profile_action(int p_action) { +	last_action = Action(p_action); + +	switch (p_action) { +		case ACTION_RESET: { +			confirm_dialog->set_text("Reset the edited profile?"); +			confirm_dialog->popup_centered(); +		} break; +		case ACTION_LOAD: { +			import_profile->popup_file_dialog(); +		} break; +		case ACTION_SAVE: { +			if (!profile_path->get_text().is_empty()) { +				Error err = edited->save_to_file(profile_path->get_text()); +				if (err != OK) { +					EditorNode::get_singleton()->show_warning(TTR("File saving failed.")); +				} +				break; +			} +			[[fallthrough]]; +		} +		case ACTION_SAVE_AS: { +			export_profile->popup_file_dialog(); +			export_profile->set_current_file(profile_path->get_text()); +		} break; +		case ACTION_NEW: { +			confirm_dialog->set_text("Create a new profile?"); +			confirm_dialog->popup_centered(); +		} break; +		case ACTION_DETECT: { +			confirm_dialog->set_text("This will scan all files in the current project to detect used classes."); +			confirm_dialog->popup_centered(); +		} break; +		case ACTION_MAX: { +		} break; +	} +} + +void EditorBuildProfileManager::_find_files(EditorFileSystemDirectory *p_dir, const HashMap<String, DetectedFile> &p_cache, HashMap<String, DetectedFile> &r_detected) { +	if (p_dir == nullptr) { +		return; +	} + +	for (int i = 0; i < p_dir->get_file_count(); i++) { +		String p = p_dir->get_file_path(i); + +		uint64_t timestamp = 0; +		String md5; + +		if (p_cache.has(p)) { +			const DetectedFile &cache = p_cache[p]; +			// Check if timestamp and MD5 match. +			timestamp = FileAccess::get_modified_time(p); +			bool cache_valid = true; +			if (cache.timestamp != timestamp) { +				md5 = FileAccess::get_md5(p); +				if (md5 != cache.md5) { +					cache_valid = false; +				} +			} + +			if (cache_valid) { +				r_detected.insert(p, cache); +				continue; +			} +		} + +		// Not cached, or cache invalid. + +		DetectedFile cache; + +		HashSet<StringName> classes; +		ResourceLoader::get_classes_used(p, &classes); + +		for (const StringName &E : classes) { +			cache.classes.push_back(E); +		} + +		if (md5.is_empty()) { +			cache.timestamp = FileAccess::get_modified_time(p); +			cache.md5 = FileAccess::get_md5(p); +		} else { +			cache.timestamp = timestamp; +			cache.md5 = md5; +		} + +		r_detected.insert(p, cache); +	} + +	for (int i = 0; i < p_dir->get_subdir_count(); i++) { +		_find_files(p_dir->get_subdir(i), p_cache, r_detected); +	} +} + +void EditorBuildProfileManager::_detect_classes() { +	HashMap<String, DetectedFile> previous_file_cache; + +	Ref<FileAccess> f = FileAccess::open("res://.godot/editor/used_class_cache", FileAccess::READ); +	if (f.is_valid()) { +		while (!f->eof_reached()) { +			String l = f->get_line(); +			Vector<String> fields = l.split("::"); +			if (fields.size() == 4) { +				String path = fields[0]; +				DetectedFile df; +				df.timestamp = fields[1].to_int(); +				df.md5 = fields[2]; +				df.classes = fields[3].split(","); +				previous_file_cache.insert(path, df); +			} +		} +		f.unref(); +	} + +	HashMap<String, DetectedFile> updated_file_cache; + +	_find_files(EditorFileSystem::get_singleton()->get_filesystem(), previous_file_cache, updated_file_cache); + +	HashSet<StringName> used_classes; + +	// Find classes and update the disk cache in the process. +	f = FileAccess::open("res://.godot/editor/used_class_cache", FileAccess::WRITE); + +	for (const KeyValue<String, DetectedFile> &E : updated_file_cache) { +		String l = E.key + "::" + itos(E.value.timestamp) + "::" + E.value.md5 + "::"; +		for (int i = 0; i < E.value.classes.size(); i++) { +			String c = E.value.classes[i]; +			if (i > 0) { +				l += ","; +			} +			l += c; +			used_classes.insert(c); +		} +		f->store_line(l); +	} + +	f.unref(); + +	// Add forced ones. + +	Vector<String> force_detect = edited->get_force_detect_classes().split(","); +	for (int i = 0; i < force_detect.size(); i++) { +		String c = force_detect[i].strip_edges(); +		if (c.is_empty()) { +			continue; +		} +		used_classes.insert(c); +	} + +	// Filter all classes to discard inherited ones. + +	HashSet<StringName> all_used_classes; + +	for (const StringName &E : used_classes) { +		StringName c = E; +		if (!ClassDB::class_exists(c)) { +			// Maybe this is an old class that got replaced? try getting compat class. +			c = ClassDB::get_compatibility_class(c); +			if (!c) { +				// No luck, skip. +				continue; +			} +		} +		while (c) { +			all_used_classes.insert(c); +			c = ClassDB::get_parent_class(c); +		} +	} + +	edited->clear_disabled_classes(); + +	List<StringName> all_classes; +	ClassDB::get_class_list(&all_classes); + +	for (const StringName &E : all_classes) { +		if (all_used_classes.has(E)) { +			// This class is valid, do nothing. +			continue; +		} + +		StringName p = ClassDB::get_parent_class(E); +		if (!p || all_used_classes.has(p)) { +			// If no parent, or if the parent is enabled, then add to disabled classes. +			// This way we avoid disabling redundant classes. +			edited->set_disable_class(E, true); +		} +	} +} + +void EditorBuildProfileManager::_action_confirm() { +	switch (last_action) { +		case ACTION_RESET: { +			edited.instantiate(); +			_update_edited_profile(); +		} break; +		case ACTION_LOAD: { +		} break; +		case ACTION_SAVE: { +		} break; +		case ACTION_SAVE_AS: { +		} break; +		case ACTION_NEW: { +			profile_path->set_text(""); +			edited.instantiate(); +			_update_edited_profile(); +		} break; +		case ACTION_DETECT: { +			_detect_classes(); +			_update_edited_profile(); +		} break; +		case ACTION_MAX: { +		} break; +	} +} + +void EditorBuildProfileManager::_fill_classes_from(TreeItem *p_parent, const String &p_class, const String &p_selected) { +	TreeItem *class_item = class_list->create_item(p_parent); +	class_item->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); +	class_item->set_icon(0, EditorNode::get_singleton()->get_class_icon(p_class, "Node")); +	String text = p_class; + +	bool disabled = edited->is_class_disabled(p_class); +	if (disabled) { +		class_item->set_custom_color(0, class_list->get_theme_color(SNAME("disabled_font_color"), SNAME("Editor"))); +	} + +	class_item->set_text(0, text); +	class_item->set_editable(0, true); +	class_item->set_selectable(0, true); +	class_item->set_metadata(0, p_class); + +	bool collapsed = edited->is_item_collapsed(p_class); +	class_item->set_collapsed(collapsed); + +	if (p_class == p_selected) { +		class_item->select(0); +	} +	if (disabled) { +		// Class disabled, do nothing else (do not show further). +		return; +	} + +	class_item->set_checked(0, true); // If it's not disabled, its checked. + +	List<StringName> child_classes; +	ClassDB::get_direct_inheriters_from_class(p_class, &child_classes); +	child_classes.sort_custom<StringName::AlphCompare>(); + +	for (const StringName &name : child_classes) { +		if (String(name).begins_with("Editor") || ClassDB::get_api_type(name) != ClassDB::API_CORE) { +			continue; +		} +		_fill_classes_from(class_item, name, p_selected); +	} +} + +void EditorBuildProfileManager::_class_list_item_selected() { +	if (updating_build_options) { +		return; +	} + +	TreeItem *item = class_list->get_selected(); +	if (!item) { +		return; +	} + +	Variant md = item->get_metadata(0); +	if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { +		String class_name = md; +		String class_description; + +		DocTools *dd = EditorHelp::get_doc_data(); +		HashMap<String, DocData::ClassDoc>::Iterator E = dd->class_list.find(class_name); +		if (E) { +			class_description = DTR(E->value.brief_description); +		} + +		description_bit->set_text(class_description); +	} else if (md.get_type() == Variant::INT) { +		int build_option_id = md; +		String build_option_description = EditorBuildProfile::get_build_option_description(EditorBuildProfile::BuildOption(build_option_id)); + +		description_bit->set_text(TTRGET(build_option_description)); +		return; +	} else { +		return; +	} +} + +void EditorBuildProfileManager::_class_list_item_edited() { +	if (updating_build_options) { +		return; +	} + +	TreeItem *item = class_list->get_edited(); +	if (!item) { +		return; +	} + +	bool checked = item->is_checked(0); + +	Variant md = item->get_metadata(0); +	if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { +		String class_selected = md; +		edited->set_disable_class(class_selected, !checked); +		_update_edited_profile(); +	} else if (md.get_type() == Variant::INT) { +		int build_option_selected = md; +		edited->set_disable_build_option(EditorBuildProfile::BuildOption(build_option_selected), !checked); +	} +} + +void EditorBuildProfileManager::_class_list_item_collapsed(Object *p_item) { +	if (updating_build_options) { +		return; +	} + +	TreeItem *item = Object::cast_to<TreeItem>(p_item); +	if (!item) { +		return; +	} + +	Variant md = item->get_metadata(0); +	if (md.get_type() != Variant::STRING && md.get_type() != Variant::STRING_NAME) { +		return; +	} + +	String class_name = md; +	bool collapsed = item->is_collapsed(); +	edited->set_item_collapsed(class_name, collapsed); +} + +void EditorBuildProfileManager::_update_edited_profile() { +	String class_selected; +	int build_option_selected = -1; + +	if (class_list->get_selected()) { +		Variant md = class_list->get_selected()->get_metadata(0); +		if (md.get_type() == Variant::STRING || md.get_type() == Variant::STRING_NAME) { +			class_selected = md; +		} else if (md.get_type() == Variant::INT) { +			build_option_selected = md; +		} +	} + +	class_list->clear(); + +	updating_build_options = true; + +	TreeItem *root = class_list->create_item(); + +	TreeItem *build_options = class_list->create_item(root); +	build_options->set_text(0, TTR("General Features:")); +	for (int i = 0; i < EditorBuildProfile::BUILD_OPTION_MAX; i++) { +		TreeItem *build_option; +		build_option = class_list->create_item(build_options); + +		build_option->set_cell_mode(0, TreeItem::CELL_MODE_CHECK); +		build_option->set_text(0, EditorBuildProfile::get_build_option_name(EditorBuildProfile::BuildOption(i))); +		build_option->set_selectable(0, true); +		build_option->set_editable(0, true); +		build_option->set_metadata(0, i); +		if (!edited->is_build_option_disabled(EditorBuildProfile::BuildOption(i))) { +			build_option->set_checked(0, true); +		} + +		if (i == build_option_selected) { +			build_option->select(0); +		} +	} + +	TreeItem *classes = class_list->create_item(root); +	classes->set_text(0, TTR("Nodes and Classes:")); + +	_fill_classes_from(classes, "Node", class_selected); +	_fill_classes_from(classes, "Resource", class_selected); + +	force_detect_classes->set_text(edited->get_force_detect_classes()); + +	updating_build_options = false; + +	_class_list_item_selected(); +} + +void EditorBuildProfileManager::_force_detect_classes_changed(const String &p_text) { +	if (updating_build_options) { +		return; +	} +	edited->set_force_detect_classes(force_detect_classes->get_text()); +} + +void EditorBuildProfileManager::_import_profile(const String &p_path) { +	Ref<EditorBuildProfile> profile; +	profile.instantiate(); +	Error err = profile->load_from_file(p_path); +	String basefile = p_path.get_file(); +	if (err != OK) { +		EditorNode::get_singleton()->show_warning(vformat(TTR("File '%s' format is invalid, import aborted."), basefile)); +		return; +	} + +	profile_path->set_text(p_path); +	EditorSettings::get_singleton()->set_project_metadata("build_profile", "last_file_path", p_path); + +	edited = profile; +	_update_edited_profile(); +} + +void EditorBuildProfileManager::_export_profile(const String &p_path) { +	ERR_FAIL_COND(edited.is_null()); +	Error err = edited->save_to_file(p_path); +	if (err != OK) { +		EditorNode::get_singleton()->show_warning(vformat(TTR("Error saving profile to path: '%s'."), p_path)); +	} else { +		profile_path->set_text(p_path); +		EditorSettings::get_singleton()->set_project_metadata("build_profile", "last_file_path", p_path); +	} +} + +Ref<EditorBuildProfile> EditorBuildProfileManager::get_current_profile() { +	return edited; +} + +EditorBuildProfileManager *EditorBuildProfileManager::singleton = nullptr; + +void EditorBuildProfileManager::_bind_methods() { +	ClassDB::bind_method("_update_selected_profile", &EditorBuildProfileManager::_update_edited_profile); +} + +EditorBuildProfileManager::EditorBuildProfileManager() { +	VBoxContainer *main_vbc = memnew(VBoxContainer); +	add_child(main_vbc); + +	HBoxContainer *path_hbc = memnew(HBoxContainer); +	profile_path = memnew(LineEdit); +	path_hbc->add_child(profile_path); +	profile_path->set_editable(true); +	profile_path->set_h_size_flags(Control::SIZE_EXPAND_FILL); + +	profile_actions[ACTION_NEW] = memnew(Button(TTR("New"))); +	path_hbc->add_child(profile_actions[ACTION_NEW]); +	profile_actions[ACTION_NEW]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action), varray(ACTION_NEW)); + +	profile_actions[ACTION_LOAD] = memnew(Button(TTR("Load"))); +	path_hbc->add_child(profile_actions[ACTION_LOAD]); +	profile_actions[ACTION_LOAD]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action), varray(ACTION_LOAD)); + +	profile_actions[ACTION_SAVE] = memnew(Button(TTR("Save"))); +	path_hbc->add_child(profile_actions[ACTION_SAVE]); +	profile_actions[ACTION_SAVE]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action), varray(ACTION_SAVE)); + +	profile_actions[ACTION_SAVE_AS] = memnew(Button(TTR("Save As"))); +	path_hbc->add_child(profile_actions[ACTION_SAVE_AS]); +	profile_actions[ACTION_SAVE_AS]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action), varray(ACTION_SAVE_AS)); + +	main_vbc->add_margin_child(TTR("Profile:"), path_hbc); + +	main_vbc->add_child(memnew(HSeparator)); + +	HBoxContainer *profiles_hbc = memnew(HBoxContainer); + +	profile_actions[ACTION_RESET] = memnew(Button(TTR("Reset to Defaults"))); +	profiles_hbc->add_child(profile_actions[ACTION_RESET]); +	profile_actions[ACTION_RESET]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action), varray(ACTION_RESET)); + +	profile_actions[ACTION_DETECT] = memnew(Button(TTR("Detect from Project"))); +	profiles_hbc->add_child(profile_actions[ACTION_DETECT]); +	profile_actions[ACTION_DETECT]->connect("pressed", callable_mp(this, &EditorBuildProfileManager::_profile_action), varray(ACTION_DETECT)); + +	main_vbc->add_margin_child(TTR("Actions:"), profiles_hbc); + +	class_list = memnew(Tree); +	class_list->set_hide_root(true); +	class_list->set_edit_checkbox_cell_only_when_checkbox_is_pressed(true); +	class_list->connect("cell_selected", callable_mp(this, &EditorBuildProfileManager::_class_list_item_selected)); +	class_list->connect("item_edited", callable_mp(this, &EditorBuildProfileManager::_class_list_item_edited), varray(), CONNECT_DEFERRED); +	class_list->connect("item_collapsed", callable_mp(this, &EditorBuildProfileManager::_class_list_item_collapsed)); +	// It will be displayed once the user creates or chooses a profile. +	main_vbc->add_margin_child(TTR("Configure Engine Build Profile:"), class_list, true); + +	description_bit = memnew(EditorHelpBit); +	description_bit->set_custom_minimum_size(Size2(0, 80) * EDSCALE); +	main_vbc->add_margin_child(TTR("Description:"), description_bit, false); + +	confirm_dialog = memnew(ConfirmationDialog); +	add_child(confirm_dialog); +	confirm_dialog->set_title(TTR("Please Confirm:")); +	confirm_dialog->connect("confirmed", callable_mp(this, &EditorBuildProfileManager::_action_confirm)); + +	import_profile = memnew(EditorFileDialog); +	add_child(import_profile); +	import_profile->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE); +	import_profile->add_filter("*.build", TTR("Egine Build Profile")); +	import_profile->connect("files_selected", callable_mp(this, &EditorBuildProfileManager::_import_profile)); +	import_profile->set_title(TTR("Load Profile")); +	import_profile->set_access(EditorFileDialog::ACCESS_FILESYSTEM); + +	export_profile = memnew(EditorFileDialog); +	add_child(export_profile); +	export_profile->set_file_mode(EditorFileDialog::FILE_MODE_SAVE_FILE); +	export_profile->add_filter("*.build", TTR("Egine Build Profile")); +	export_profile->connect("file_selected", callable_mp(this, &EditorBuildProfileManager::_export_profile)); +	export_profile->set_title(TTR("Export Profile")); +	export_profile->set_access(EditorFileDialog::ACCESS_FILESYSTEM); + +	force_detect_classes = memnew(LineEdit); +	main_vbc->add_margin_child(TTR("Forced classes on detect:"), force_detect_classes); +	force_detect_classes->connect("text_changed", callable_mp(this, &EditorBuildProfileManager::_force_detect_classes_changed)); + +	set_title(TTR("Edit Build Configuration Profile")); + +	singleton = this; +} diff --git a/editor/editor_build_profile.h b/editor/editor_build_profile.h new file mode 100644 index 0000000000..bb6494b8c9 --- /dev/null +++ b/editor/editor_build_profile.h @@ -0,0 +1,173 @@ +/*************************************************************************/ +/*  editor_build_profile.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_BUILD_PROFILE_H +#define EDITOR_BUILD_PROFILE_H + +#include "core/io/file_access.h" +#include "core/object/ref_counted.h" +#include "editor/editor_help.h" +#include "scene/gui/dialogs.h" +#include "scene/gui/option_button.h" +#include "scene/gui/separator.h" +#include "scene/gui/split_container.h" +#include "scene/gui/tree.h" + +class EditorBuildProfile : public RefCounted { +	GDCLASS(EditorBuildProfile, RefCounted); + +public: +	enum BuildOption { +		BUILD_OPTION_3D, +		BUILD_OPTION_PHYSICS_2D, +		BUILD_OPTION_PHYSICS_3D, +		BUILD_OPTION_NAVIGATION, +		BUILD_OPTION_XR, +		BUILD_OPTION_RENDERING_DEVICE, +		BUILD_OPTION_OPENGL, +		BUILD_OPTION_VULKAN, +		BUILD_OPTION_MAX +	}; + +private: +	HashSet<StringName> disabled_classes; + +	HashSet<StringName> collapsed_classes; + +	String force_detect_classes; + +	bool build_options_disabled[BUILD_OPTION_MAX] = {}; +	static const char *build_option_identifiers[BUILD_OPTION_MAX]; +	static const bool build_option_disable_values[BUILD_OPTION_MAX]; + +	String _get_build_option_name(BuildOption p_build_option) { return get_build_option_name(p_build_option); } + +protected: +	static void _bind_methods(); + +public: +	void set_disable_class(const StringName &p_class, bool p_disabled); +	bool is_class_disabled(const StringName &p_class) const; + +	void set_item_collapsed(const StringName &p_class, bool p_collapsed); +	bool is_item_collapsed(const StringName &p_class) const; + +	void set_disable_build_option(BuildOption p_build_option, bool p_disable); +	bool is_build_option_disabled(BuildOption p_build_option) const; + +	void set_force_detect_classes(const String &p_classes); +	String get_force_detect_classes() const; + +	void clear_disabled_classes(); + +	Error save_to_file(const String &p_path); +	Error load_from_file(const String &p_path); + +	static String get_build_option_name(BuildOption p_build_option); +	static String get_build_option_description(BuildOption p_build_option); +	static bool get_build_option_disable_value(BuildOption p_build_option); + +	EditorBuildProfile(); +}; + +VARIANT_ENUM_CAST(EditorBuildProfile::BuildOption) + +class EditorFileSystemDirectory; + +class EditorBuildProfileManager : public AcceptDialog { +	GDCLASS(EditorBuildProfileManager, AcceptDialog); + +	enum Action { +		ACTION_NEW, +		ACTION_RESET, +		ACTION_LOAD, +		ACTION_SAVE, +		ACTION_SAVE_AS, +		ACTION_DETECT, +		ACTION_MAX +	}; + +	Action last_action = ACTION_NEW; + +	ConfirmationDialog *confirm_dialog = nullptr; +	Button *profile_actions[ACTION_MAX]; + +	Tree *class_list = nullptr; +	EditorHelpBit *description_bit = nullptr; + +	EditorFileDialog *import_profile = nullptr; +	EditorFileDialog *export_profile = nullptr; + +	LineEdit *profile_path = nullptr; + +	LineEdit *force_detect_classes = nullptr; + +	void _profile_action(int p_action); +	void _action_confirm(); + +	void _update_edited_profile(); +	void _fill_classes_from(TreeItem *p_parent, const String &p_class, const String &p_selected); + +	Ref<EditorBuildProfile> edited; + +	void _import_profile(const String &p_path); +	void _export_profile(const String &p_path); + +	bool updating_build_options = false; + +	void _class_list_item_selected(); +	void _class_list_item_edited(); +	void _class_list_item_collapsed(Object *p_item); +	void _detect_classes(); + +	void _force_detect_classes_changed(const String &p_text); + +	struct DetectedFile { +		uint32_t timestamp = 0; +		String md5; +		Vector<String> classes; +	}; + +	void _find_files(EditorFileSystemDirectory *p_dir, const HashMap<String, DetectedFile> &p_cache, HashMap<String, DetectedFile> &r_detected); + +	static EditorBuildProfileManager *singleton; + +protected: +	static void _bind_methods(); +	void _notification(int p_what); + +public: +	Ref<EditorBuildProfile> get_current_profile(); + +	static EditorBuildProfileManager *get_singleton() { return singleton; } +	EditorBuildProfileManager(); +}; + +#endif // EDITOR_BUILD_PROFILE_H diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 166dcf19c8..68aad71ca2 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -75,6 +75,7 @@  #include "editor/dependency_editor.h"  #include "editor/editor_about.h"  #include "editor/editor_audio_buses.h" +#include "editor/editor_build_profile.h"  #include "editor/editor_command_palette.h"  #include "editor/editor_data.h"  #include "editor/editor_export.h" @@ -2805,6 +2806,9 @@ void EditorNode::_menu_option_confirm(int p_option, bool p_confirmed) {  				}  			}  		} break; +		case TOOLS_BUILD_PROFILE_MANAGER: { +			build_profile_manager->popup_centered_clamped(Size2(700, 800) * EDSCALE, 0.8); +		} break;  		case RUN_USER_DATA_FOLDER: {  			// Ensure_user_data_dir() to prevent the edge case: "Open User Data Folder" won't work after the project was renamed in ProjectSettingsEditor unless the project is saved.  			OS::get_singleton()->ensure_user_data_dir(); @@ -6451,6 +6455,10 @@ EditorNode::EditorNode() {  	feature_profile_manager = memnew(EditorFeatureProfileManager);  	gui_base->add_child(feature_profile_manager); + +	build_profile_manager = memnew(EditorBuildProfileManager); +	gui_base->add_child(build_profile_manager); +  	about = memnew(EditorAbout);  	gui_base->add_child(about);  	feature_profile_manager->connect("current_feature_profile_changed", callable_mp(this, &EditorNode::_feature_profile_changed)); @@ -6543,6 +6551,10 @@ EditorNode::EditorNode() {  	p->add_item(TTR("Install Android Build Template..."), FILE_INSTALL_ANDROID_SOURCE);  	p->add_item(TTR("Open User Data Folder"), RUN_USER_DATA_FOLDER); +	p->add_separator(); +	p->add_item(TTR("Customize Engine Build Configuration..."), TOOLS_BUILD_PROFILE_MANAGER); +	p->add_separator(); +  	plugin_config_dialog = memnew(PluginConfigDialog);  	plugin_config_dialog->connect("plugin_ready", callable_mp(this, &EditorNode::_on_plugin_ready));  	gui_base->add_child(plugin_config_dialog); @@ -7190,6 +7202,7 @@ EditorNode::EditorNode() {  		vshader_convert.instantiate();  		resource_conversion_plugins.push_back(vshader_convert);  	} +  	update_spinner_step_msec = OS::get_singleton()->get_ticks_msec();  	update_spinner_step_frame = Engine::get_singleton()->get_frames_drawn(); diff --git a/editor/editor_node.h b/editor/editor_node.h index 07d565314d..08f5bfdc4a 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -95,6 +95,7 @@ class TabContainer;  class TextureProgressBar;  class VSplitContainer;  class Window; +class EditorBuildProfileManager;  class EditorNode : public Node {  	GDCLASS(EditorNode, Node); @@ -163,6 +164,7 @@ private:  		EDIT_REDO,  		EDIT_RELOAD_SAVED_SCENE,  		TOOLS_ORPHAN_RESOURCES, +		TOOLS_BUILD_PROFILE_MANAGER,  		TOOLS_CUSTOM,  		RESOURCE_SAVE,  		RESOURCE_SAVE_AS, @@ -377,6 +379,7 @@ private:  	EditorFileDialog *file = nullptr;  	ExportTemplateManager *export_template_manager = nullptr;  	EditorFeatureProfileManager *feature_profile_manager = nullptr; +	EditorBuildProfileManager *build_profile_manager = nullptr;  	EditorFileDialog *file_templates = nullptr;  	EditorFileDialog *file_export_lib = nullptr;  	EditorFileDialog *file_script = nullptr; diff --git a/main/main.cpp b/main/main.cpp index 4902c72ee8..7d803d901d 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -410,6 +410,8 @@ Error Main::test_setup() {  			String("Please include this when reporting the bug on https://github.com/godotengine/godot/issues"));  	GLOBAL_DEF_RST("rendering/occlusion_culling/bvh_build_quality", 2); +	register_core_settings(); //here globals are present +  	translation_server = memnew(TranslationServer);  	tsman = memnew(TextServerManager); diff --git a/modules/gdscript/gdscript.cpp b/modules/gdscript/gdscript.cpp index e74314389d..964c1133ff 100644 --- a/modules/gdscript/gdscript.cpp +++ b/modules/gdscript/gdscript.cpp @@ -870,7 +870,7 @@ Error GDScript::reload(bool p_keep_state) {  		}  		// TODO: Show all error messages.  		_err_print_error("GDScript::reload", path.is_empty() ? "built-in" : (const char *)path.utf8().get_data(), parser.get_errors().front()->get().line, ("Parse Error: " + parser.get_errors().front()->get().message).utf8().get_data(), false, ERR_HANDLER_SCRIPT); -		ERR_FAIL_V(ERR_PARSE_ERROR); +		return ERR_PARSE_ERROR;  	}  	GDScriptAnalyzer analyzer(&parser); @@ -886,7 +886,7 @@ Error GDScript::reload(bool p_keep_state) {  			_err_print_error("GDScript::reload", path.is_empty() ? "built-in" : (const char *)path.utf8().get_data(), e->get().line, ("Parse Error: " + e->get().message).utf8().get_data(), false, ERR_HANDLER_SCRIPT);  			e = e->next();  		} -		ERR_FAIL_V(ERR_PARSE_ERROR); +		return ERR_PARSE_ERROR;  	}  	bool can_run = ScriptServer::is_scripting_enabled() || parser.is_tool(); @@ -904,7 +904,7 @@ Error GDScript::reload(bool p_keep_state) {  				GDScriptLanguage::get_singleton()->debug_break_parse(_get_debug_path(), compiler.get_error_line(), "Parser Error: " + compiler.get_error());  			}  			_err_print_error("GDScript::reload", path.is_empty() ? "built-in" : (const char *)path.utf8().get_data(), compiler.get_error_line(), ("Compile Error: " + compiler.get_error()).utf8().get_data(), false, ERR_HANDLER_SCRIPT); -			ERR_FAIL_V(ERR_COMPILATION_FAILED); +			return ERR_COMPILATION_FAILED;  		} else {  			return err;  		} diff --git a/scene/resources/resource_format_text.cpp b/scene/resources/resource_format_text.cpp index 66afb001fb..100e8ea7c6 100644 --- a/scene/resources/resource_format_text.cpp +++ b/scene/resources/resource_format_text.cpp @@ -65,14 +65,18 @@ Error ResourceLoaderText::_parse_sub_resource_dummy(DummyReadData *p_data, Varia  		return ERR_PARSE_ERROR;  	} -	String unique_id = token.value; +	if (p_data->no_placeholders) { +		r_res.unref(); +	} else { +		String unique_id = token.value; -	if (!p_data->resource_map.has(unique_id)) { -		r_err_str = "Found unique_id reference before mapping, sub-resources stored out of order in resource file"; -		return ERR_PARSE_ERROR; -	} +		if (!p_data->resource_map.has(unique_id)) { +			r_err_str = "Found unique_id reference before mapping, sub-resources stored out of order in resource file"; +			return ERR_PARSE_ERROR; +		} -	r_res = p_data->resource_map[unique_id]; +		r_res = p_data->resource_map[unique_id]; +	}  	VariantParser::get_token(p_stream, token, line, r_err_str);  	if (token.type != VariantParser::TK_PARENTHESIS_CLOSE) { @@ -91,11 +95,15 @@ Error ResourceLoaderText::_parse_ext_resource_dummy(DummyReadData *p_data, Varia  		return ERR_PARSE_ERROR;  	} -	String id = token.value; +	if (p_data->no_placeholders) { +		r_res.unref(); +	} else { +		String id = token.value; -	ERR_FAIL_COND_V(!p_data->rev_external_resources.has(id), ERR_PARSE_ERROR); +		ERR_FAIL_COND_V(!p_data->rev_external_resources.has(id), ERR_PARSE_ERROR); -	r_res = p_data->rev_external_resources[id]; +		r_res = p_data->rev_external_resources[id]; +	}  	VariantParser::get_token(p_stream, token, line, r_err_str);  	if (token.type != VariantParser::TK_PARENTHESIS_CLOSE) { @@ -1066,7 +1074,7 @@ static void bs_save_unicode_string(Ref<FileAccess> p_f, const String &p_string,  	p_f->store_buffer((const uint8_t *)utf8.get_data(), utf8.length() + 1);  } -Error ResourceLoaderText::save_as_binary(Ref<FileAccess> p_f, const String &p_path) { +Error ResourceLoaderText::save_as_binary(const String &p_path) {  	if (error) {  		return error;  	} @@ -1271,7 +1279,7 @@ Error ResourceLoaderText::save_as_binary(Ref<FileAccess> p_f, const String &p_pa  		}  		if (next_tag.name == "node") { -			//this is a node, must save one more! +			// This is a node, must save one more!  			if (!is_scene) {  				error_text += "found the 'node' tag on a resource file!"; @@ -1346,6 +1354,126 @@ Error ResourceLoaderText::save_as_binary(Ref<FileAccess> p_f, const String &p_pa  	return OK;  } +Error ResourceLoaderText::get_classes_used(HashSet<StringName> *r_classes) { +	if (error) { +		return error; +	} + +	ignore_resource_parsing = true; + +	DummyReadData dummy_read; +	dummy_read.no_placeholders = true; +	VariantParser::ResourceParser rp; +	rp.ext_func = _parse_ext_resource_dummys; +	rp.sub_func = _parse_sub_resource_dummys; +	rp.userdata = &dummy_read; + +	while (next_tag.name == "ext_resource") { +		error = VariantParser::parse_tag(&stream, lines, error_text, next_tag, &rp); + +		if (error) { +			_printerr(); +			return error; +		} +	} + +	while (next_tag.name == "sub_resource" || next_tag.name == "resource") { +		if (next_tag.name == "sub_resource") { +			if (!next_tag.fields.has("type")) { +				error = ERR_FILE_CORRUPT; +				error_text = "Missing 'type' in external resource tag"; +				_printerr(); +				return error; +			} + +			r_classes->insert(next_tag.fields["type"]); + +		} else { +			r_classes->insert(next_tag.fields["res_type"]); +		} + +		while (true) { +			String assign; +			Variant value; + +			error = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, &rp); + +			if (error) { +				if (error == ERR_FILE_EOF) { +					return OK; +				} + +				_printerr(); +				return error; +			} + +			if (!assign.is_empty()) { +				continue; +			} else if (!next_tag.name.is_empty()) { +				error = OK; +				break; +			} else { +				error = ERR_FILE_CORRUPT; +				error_text = "Premature end of file while parsing [sub_resource]"; +				_printerr(); +				return error; +			} +		} +	} + +	while (next_tag.name == "node") { +		// This is a node, must save one more! + +		if (!is_scene) { +			error_text += "found the 'node' tag on a resource file!"; +			_printerr(); +			error = ERR_FILE_CORRUPT; +			return error; +		} + +		if (!next_tag.fields.has("type")) { +			error = ERR_FILE_CORRUPT; +			error_text = "Missing 'type' in external resource tag"; +			_printerr(); +			return error; +		} + +		r_classes->insert(next_tag.fields["type"]); + +		while (true) { +			String assign; +			Variant value; + +			error = VariantParser::parse_tag_assign_eof(&stream, lines, error_text, next_tag, assign, value, &rp); + +			if (error) { +				if (error == ERR_FILE_MISSING_DEPENDENCIES) { +					// Resource loading error, just skip it. +				} else if (error != ERR_FILE_EOF) { +					_printerr(); +					return error; +				} else { +					return OK; +				} +			} + +			if (!assign.is_empty()) { +				continue; +			} else if (!next_tag.name.is_empty()) { +				error = OK; +				break; +			} else { +				error = ERR_FILE_CORRUPT; +				error_text = "Premature end of file while parsing [sub_resource]"; +				_printerr(); +				return error; +			} +		} +	} + +	return OK; +} +  String ResourceLoaderText::recognize(Ref<FileAccess> p_f) {  	error = OK; @@ -1473,6 +1601,26 @@ bool ResourceFormatLoaderText::handles_type(const String &p_type) const {  	return true;  } +void ResourceFormatLoaderText::get_classes_used(const String &p_path, HashSet<StringName> *r_classes) { +	String ext = p_path.get_extension().to_lower(); +	if (ext == "tscn") { +		r_classes->insert("PackedScene"); +	} + +	// ...for anything else must test... + +	Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); +	if (f.is_null()) { +		return; // Could not read. +	} + +	ResourceLoaderText loader; +	loader.local_path = ProjectSettings::get_singleton()->localize_path(p_path); +	loader.res_path = loader.local_path; +	loader.open(f); +	loader.get_classes_used(r_classes); +} +  String ResourceFormatLoaderText::get_resource_type(const String &p_path) const {  	String ext = p_path.get_extension().to_lower();  	if (ext == "tscn") { @@ -1561,7 +1709,7 @@ Error ResourceFormatLoaderText::convert_file_to_binary(const String &p_src_path,  	loader.local_path = ProjectSettings::get_singleton()->localize_path(path);  	loader.res_path = loader.local_path;  	loader.open(f); -	return loader.save_as_binary(f, p_dst_path); +	return loader.save_as_binary(p_dst_path);  }  /*****************************************************************************************************/ diff --git a/scene/resources/resource_format_text.h b/scene/resources/resource_format_text.h index 5c6a937bf2..69bb40502f 100644 --- a/scene/resources/resource_format_text.h +++ b/scene/resources/resource_format_text.h @@ -90,6 +90,7 @@ class ResourceLoaderText {  	};  	struct DummyReadData { +		bool no_placeholders = false;  		HashMap<Ref<Resource>, int> external_resources;  		HashMap<String, Ref<Resource>> rev_external_resources;  		HashMap<Ref<Resource>, int> resource_index_map; @@ -125,8 +126,9 @@ public:  	ResourceUID::ID get_uid(Ref<FileAccess> p_f);  	void get_dependencies(Ref<FileAccess> p_f, List<String> *p_dependencies, bool p_add_types);  	Error rename_dependencies(Ref<FileAccess> p_f, const String &p_path, const HashMap<String, String> &p_map); +	Error get_classes_used(HashSet<StringName> *r_classes); -	Error save_as_binary(Ref<FileAccess> p_f, const String &p_path); +	Error save_as_binary(const String &p_path);  	ResourceLoaderText();  }; @@ -137,6 +139,8 @@ public:  	virtual void get_recognized_extensions_for_type(const String &p_type, List<String> *p_extensions) const;  	virtual void get_recognized_extensions(List<String> *p_extensions) const;  	virtual bool handles_type(const String &p_type) const; +	virtual void get_classes_used(const String &p_path, HashSet<StringName> *r_classes); +  	virtual String get_resource_type(const String &p_path) const;  	virtual ResourceUID::ID get_resource_uid(const String &p_path) const;  	virtual void get_dependencies(const String &p_path, List<String> *p_dependencies, bool p_add_types = false); diff --git a/tests/core/threads/test_worker_thread_pool.h b/tests/core/threads/test_worker_thread_pool.h new file mode 100644 index 0000000000..641b293c8a --- /dev/null +++ b/tests/core/threads/test_worker_thread_pool.h @@ -0,0 +1,158 @@ +/*************************************************************************/ +/*  test_worker_thread_pool.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 TEST_WORKER_THREAD_POOL_H +#define TEST_WORKER_THREAD_POOL_H + +#include "core/object/worker_thread_pool.h" + +#include "tests/test_macros.h" + +namespace TestWorkerThreadPool { + +int u32scmp(const char32_t *l, const char32_t *r) { +	for (; *l == *r && *l && *r; l++, r++) { +		// Continue. +	} +	return *l - *r; +} + +static void static_test(void *p_arg) { +	SafeNumeric<uint32_t> *counter = (SafeNumeric<uint32_t> *)p_arg; +	counter->increment(); +} + +static SafeNumeric<uint32_t> callable_counter; + +static void static_callable_test() { +	callable_counter.increment(); +} + +TEST_CASE("[WorkerThreadPool] Process 256 threads using native task") { +	const int count = 256; +	SafeNumeric<uint32_t> counter; +	WorkerThreadPool::TaskID tasks[count]; +	for (int i = 0; i < count; i++) { +		tasks[i] = WorkerThreadPool::get_singleton()->add_native_task(static_test, &counter, true); +	} +	for (int i = 0; i < count; i++) { +		WorkerThreadPool::get_singleton()->wait_for_task_completion(tasks[i]); +	} + +	CHECK(counter.get() == count); +} + +TEST_CASE("[WorkerThreadPool] Process 256 threads using native low priority") { +	const int count = 256; +	SafeNumeric<uint32_t> counter = SafeNumeric<uint32_t>(0); +	WorkerThreadPool::TaskID tasks[count]; +	for (int i = 0; i < count; i++) { +		tasks[i] = WorkerThreadPool::get_singleton()->add_native_task(static_test, &counter, false); +	} +	for (int i = 0; i < count; i++) { +		WorkerThreadPool::get_singleton()->wait_for_task_completion(tasks[i]); +	} + +	CHECK(counter.get() == count); +} + +TEST_CASE("[WorkerThreadPool] Process 256 threads using callable") { +	const int count = 256; +	WorkerThreadPool::TaskID tasks[count]; +	callable_counter.set(0); +	for (int i = 0; i < count; i++) { +		tasks[i] = WorkerThreadPool::get_singleton()->add_task(callable_mp_static(static_callable_test), true); +	} +	for (int i = 0; i < count; i++) { +		WorkerThreadPool::get_singleton()->wait_for_task_completion(tasks[i]); +	} + +	CHECK(callable_counter.get() == count); +} + +TEST_CASE("[WorkerThreadPool] Process 256 threads using callable low priority") { +	const int count = 256; +	WorkerThreadPool::TaskID tasks[count]; +	callable_counter.set(0); +	for (int i = 0; i < count; i++) { +		tasks[i] = WorkerThreadPool::get_singleton()->add_task(callable_mp_static(static_callable_test), false); +	} +	for (int i = 0; i < count; i++) { +		WorkerThreadPool::get_singleton()->wait_for_task_completion(tasks[i]); +	} + +	CHECK(callable_counter.get() == count); +} + +static void static_group_test(void *p_arg, uint32_t p_index) { +	SafeNumeric<uint32_t> *counter = (SafeNumeric<uint32_t> *)p_arg; +	counter->exchange_if_greater(p_index); +} + +TEST_CASE("[WorkerThreadPool] Process 256 elements on native task group") { +	const int count = 256; +	SafeNumeric<uint32_t> counter; +	WorkerThreadPool::GroupID group = WorkerThreadPool::get_singleton()->add_native_group_task(static_group_test, &counter, count, -1, true); +	WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group); +	CHECK(counter.get() == count - 1); +} + +TEST_CASE("[WorkerThreadPool] Process 256 elements on native task group low priority") { +	const int count = 256; +	SafeNumeric<uint32_t> counter; +	WorkerThreadPool::GroupID group = WorkerThreadPool::get_singleton()->add_native_group_task(static_group_test, &counter, count, -1, false); +	WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group); +	CHECK(counter.get() == count - 1); +} + +static SafeNumeric<uint32_t> callable_group_counter; + +static void static_callable_group_test(uint32_t p_index) { +	callable_group_counter.exchange_if_greater(p_index); +} + +TEST_CASE("[WorkerThreadPool] Process 256 elements on native task group") { +	const int count = 256; +	WorkerThreadPool::GroupID group = WorkerThreadPool::get_singleton()->add_group_task(callable_mp_static(static_callable_group_test), count, -1, true); +	WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group); +	CHECK(callable_group_counter.get() == count - 1); +} + +TEST_CASE("[WorkerThreadPool] Process 256 elements on native task group low priority") { +	const int count = 256; +	callable_group_counter.set(0); +	WorkerThreadPool::GroupID group = WorkerThreadPool::get_singleton()->add_group_task(callable_mp_static(static_callable_group_test), count, -1, false); +	WorkerThreadPool::get_singleton()->wait_for_group_task_completion(group); +	CHECK(callable_group_counter.get() == count - 1); +} + +} // namespace TestWorkerThreadPool + +#endif // TEST_WORKER_THREAD_POOL_H diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 79cda7e512..e71f2edba3 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -70,6 +70,7 @@  #include "tests/core/test_crypto.h"  #include "tests/core/test_hashing_context.h"  #include "tests/core/test_time.h" +#include "tests/core/threads/test_worker_thread_pool.h"  #include "tests/core/variant/test_array.h"  #include "tests/core/variant/test_dictionary.h"  #include "tests/core/variant/test_variant.h"  |