diff --git a/core/os/os.cpp b/core/os/os.cpp index a84a76bbd..53c880681 100644 --- a/core/os/os.cpp +++ b/core/os/os.cpp @@ -30,11 +30,12 @@ #include "os.h" +#include "core/config/project_settings.h" +#include "core/input/input.h" +#include "core/io/json.h" #include "core/os/dir_access.h" #include "core/os/file_access.h" -#include "core/input/input.h" #include "core/os/midi_driver.h" -#include "core/config/project_settings.h" #include "core/version_generated.gen.h" #include "servers/audio_server.h" @@ -971,6 +972,58 @@ void OS::add_frame_delay(bool p_can_draw) { } } +void OS::set_use_benchmark(bool p_use_benchmark) { + use_benchmark = p_use_benchmark; +} + +bool OS::is_use_benchmark_set() { + return use_benchmark; +} + +void OS::set_benchmark_file(const String &p_benchmark_file) { + benchmark_file = p_benchmark_file; +} + +String OS::get_benchmark_file() { + return benchmark_file; +} + +void OS::benchmark_begin_measure(const String &p_what) { +#ifdef TOOLS_ENABLED + start_benchmark_from[p_what] = OS::get_singleton()->get_ticks_usec(); +#endif +} +void OS::benchmark_end_measure(const String &p_what) { +#ifdef TOOLS_ENABLED + uint64_t total = OS::get_singleton()->get_ticks_usec() - start_benchmark_from[p_what]; + double total_f = double(total) / double(1000000); + + startup_benchmark_json[p_what] = total_f; +#endif +} + +void OS::benchmark_dump() { +#ifdef TOOLS_ENABLED + if (!use_benchmark) { + return; + } + if (!benchmark_file.empty()) { + FileAccess *f = FileAccess::open(benchmark_file, FileAccess::WRITE); + if (f) { + f->store_string(JSON::print(startup_benchmark_json, "\t", false)); + } + } else { + List keys; + startup_benchmark_json.get_key_list(&keys); + print_line("BENCHMARK:"); + for (List::Element *E = keys.front(); E; E = E->next()) { + Variant &K = E->get(); + print_line("\t-" + K.operator String() + ": " + startup_benchmark_json[K] + " sec."); + } + } +#endif +} + OS::OS() { void *volatile stack_bottom; diff --git a/core/os/os.h b/core/os/os.h index 58ae63b86..79452edac 100644 --- a/core/os/os.h +++ b/core/os/os.h @@ -74,6 +74,12 @@ class OS { bool restart_on_exit; List restart_commandline; + // For tracking benchmark data + bool use_benchmark = false; + String benchmark_file; + HashMap start_benchmark_from; + Dictionary startup_benchmark_json; + protected: void _set_logger(CompositeLogger *p_logger); @@ -649,7 +655,17 @@ public: return Vector(); } + // For recording / measuring benchmark data. Only enabled with tools + void set_use_benchmark(bool p_use_benchmark); + bool is_use_benchmark_set(); + void set_benchmark_file(const String &p_benchmark_file); + String get_benchmark_file(); + virtual void benchmark_begin_measure(const String &p_what); + virtual void benchmark_end_measure(const String &p_what); + virtual void benchmark_dump(); + virtual void process_and_drop_events() {} + OS(); virtual ~OS(); }; diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp index 179fb325a..6c618ccea 100644 --- a/core/register_core_types.cpp +++ b/core/register_core_types.cpp @@ -111,6 +111,8 @@ extern void register_variant_methods(); extern void unregister_variant_methods(); void register_core_types() { + OS::get_singleton()->benchmark_begin_measure("register_core_types"); + MemoryPool::setup(); StringName::setup(); @@ -241,6 +243,8 @@ void register_core_types() { _plogger = memnew(_PLogger); thread_pool = memnew(ThreadPool); + + OS::get_singleton()->benchmark_end_measure("register_core_types"); } void register_core_settings() { @@ -298,6 +302,8 @@ void register_core_singletons() { } void unregister_core_types() { + OS::get_singleton()->benchmark_begin_measure("unregister_core_types"); + memdelete(_resource_loader); memdelete(_resource_saver); memdelete(_os); @@ -349,4 +355,6 @@ void unregister_core_types() { StringName::cleanup(); MemoryPool::cleanup(); + + OS::get_singleton()->benchmark_end_measure("unregister_core_types"); } diff --git a/editor/editor_fonts.cpp b/editor/editor_fonts.cpp index 8ce537408..156b981c9 100644 --- a/editor/editor_fonts.cpp +++ b/editor/editor_fonts.cpp @@ -32,12 +32,13 @@ #include "builtin_fonts.gen.h" #include "core/os/dir_access.h" +#include "core/os/memory.h" +#include "core/os/os.h" +#include "core/string/ustring.h" +#include "core/variant/variant.h" #include "editor_scale.h" #include "editor_settings.h" #include "scene/resources/dynamic_font.h" -#include "core/os/memory.h" -#include "core/string/ustring.h" -#include "core/variant/variant.h" #include "scene/resources/font.h" #include "scene/resources/theme.h" @@ -104,6 +105,8 @@ MAKE_FALLBACKS(m_name); void editor_register_fonts(Ref p_theme) { + OS::get_singleton()->benchmark_begin_measure("editor_register_fonts"); + DirAccess *dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); /* Custom font */ @@ -289,4 +292,6 @@ void editor_register_fonts(Ref p_theme) { MAKE_SOURCE_FONT(df_text_editor_status_code, default_font_size); p_theme->set_font("status_source", "EditorFonts", df_text_editor_status_code); + + OS::get_singleton()->benchmark_end_measure("editor_register_fonts"); } diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 1780d697d..ee5aee2df 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -876,8 +876,13 @@ void EditorNode::_sources_changed(bool p_exist) { _load_docks(); if (defer_load_scene != "") { + OS::get_singleton()->benchmark_begin_measure("editor_load_scene"); + load_scene(defer_load_scene); defer_load_scene = ""; + + OS::get_singleton()->benchmark_end_measure("editor_load_scene"); + OS::get_singleton()->benchmark_dump(); } } } @@ -3873,6 +3878,8 @@ bool EditorNode::is_scene_in_use(const String &p_path) { } void EditorNode::register_editor_types() { + OS::get_singleton()->benchmark_begin_measure("register_editor_types"); + ResourceLoader::set_timestamp_on_load(true); ResourceSaver::set_timestamp_on_save(true); @@ -3905,12 +3912,18 @@ void EditorNode::register_editor_types() { // FIXME: Is this stuff obsolete, or should it be ported to new APIs? ClassDB::register_class(); //ClassDB::register_type(); + + OS::get_singleton()->benchmark_end_measure("register_editor_types"); } void EditorNode::unregister_editor_types() { + OS::get_singleton()->benchmark_begin_measure("unregister_editor_types"); + _init_callbacks.clear(); EditorResourcePicker::clear_caches(); + + OS::get_singleton()->benchmark_end_measure("unregister_editor_types"); } void EditorNode::stop_child_process() { @@ -5827,6 +5840,8 @@ int EditorNode::execute_and_show_output(const String &p_title, const String &p_p } EditorNode::EditorNode() { + OS::get_singleton()->benchmark_begin_measure("editor"); + EditorPropertyNameProcessor *epnp = memnew(EditorPropertyNameProcessor); add_child(epnp); @@ -7159,6 +7174,8 @@ EditorNode::EditorNode() { about = memnew(EditorAbout); add_child(about); + + OS::get_singleton()->benchmark_end_measure("editor"); } EditorNode::~EditorNode() { diff --git a/editor/editor_themes.cpp b/editor/editor_themes.cpp index a82ded466..5467c916d 100644 --- a/editor/editor_themes.cpp +++ b/editor/editor_themes.cpp @@ -41,6 +41,7 @@ #include "core/math/math_funcs.h" #include "core/math/vector2.h" #include "core/os/memory.h" +#include "core/os/os.h" #include "core/string/string_name.h" #include "core/string/ustring.h" #include "core/typedefs.h" @@ -151,6 +152,8 @@ static Ref editor_generate_icon(int p_index, bool p_convert_color, #endif void editor_register_and_generate_icons(Ref p_theme, bool p_dark_theme = true, int p_thumb_size = 32, bool p_only_thumbs = false) { + OS::get_singleton()->benchmark_begin_measure("editor_register_and_generate_icons_" + String((p_only_thumbs ? "with_only_thumbs" : "all"))); + #ifdef MODULE_SVG_ENABLED // The default icon theme is designed to be used for a dark theme. // This dictionary stores color codes to convert to other colors @@ -307,9 +310,13 @@ void editor_register_and_generate_icons(Ref p_theme, bool p_dark_theme = #else WARN_PRINT("SVG support disabled, editor icons won't be rendered."); #endif + + OS::get_singleton()->benchmark_end_measure("editor_register_and_generate_icons_" + String((p_only_thumbs ? "with_only_thumbs" : "all"))); } Ref create_editor_theme(const Ref p_theme) { + OS::get_singleton()->benchmark_begin_measure("create_editor_theme"); + Ref theme = Ref(memnew(Theme)); const float default_contrast = 0.25; @@ -1447,10 +1454,14 @@ Ref create_editor_theme(const Ref p_theme) { setting->load_text_editor_theme(); } + OS::get_singleton()->benchmark_end_measure("create_editor_theme"); + return theme; } Ref create_custom_theme(const Ref p_theme) { + OS::get_singleton()->benchmark_begin_measure("create_custom_theme"); + Ref theme = create_editor_theme(p_theme); const String custom_theme_path = EditorSettings::get_singleton()->get("interface/theme/custom_theme"); @@ -1461,6 +1472,8 @@ Ref create_custom_theme(const Ref p_theme) { } } + OS::get_singleton()->benchmark_end_measure("create_custom_theme"); + return theme; } diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 70049e804..a27f55422 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -2391,6 +2391,8 @@ void ProjectManager::_version_button_pressed() { } ProjectManager::ProjectManager() { + OS::get_singleton()->benchmark_begin_measure("project_manager"); + // load settings if (!EditorSettings::get_singleton()) { EditorSettings::create(); @@ -2781,6 +2783,8 @@ ProjectManager::ProjectManager() { about = memnew(EditorAbout); add_child(about); + + OS::get_singleton()->benchmark_end_measure("project_manager"); } ProjectManager::~ProjectManager() { diff --git a/main/main.cpp b/main/main.cpp index a565b0ebf..2c402f85b 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -361,6 +361,8 @@ void Main::print_help(const char *p_binary) { OS::get_singleton()->print(" --export-pack Same as --export, but only export the game pack for the given preset. The extension determines whether it will be in PCK or ZIP format.\n"); OS::get_singleton()->print(" --doctool [] Dump the engine API reference to the given (defaults to current dir) in XML format, merging if existing files are found.\n"); OS::get_singleton()->print(" --no-docbase Disallow dumping the base types (used with --doctool).\n"); + OS::get_singleton()->print(" --benchmark Benchmark the run time and print it to console.\n"); + OS::get_singleton()->print(" --benchmark-file Benchmark the run time and save it to a given file in JSON format. The path should be absolute.\n"); #ifdef DEBUG_METHODS_ENABLED OS::get_singleton()->print(" --gdnative-generate-json-api Generate JSON dump of the Pandemonium API for GDNative bindings.\n"); #endif @@ -411,9 +413,14 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph OS::get_singleton()->initialize_core(); + // Benchmark tracking must be done after `OS::get_singleton()->initialize_core()` as on some + // platforms, it's used to set up the time utilities. + OS::get_singleton()->benchmark_begin_measure("startup_begin"); + engine = memnew(Engine); MAIN_PRINT("Main: Initialize CORE"); + OS::get_singleton()->benchmark_begin_measure("core"); register_core_types(); register_core_driver_types(); @@ -908,6 +915,20 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph OS::get_singleton()->disable_crash_handler(); } else if (I->get() == "--skip-breakpoints") { skip_breakpoints = true; + } else if (I->get() == "--benchmark") { + OS::get_singleton()->set_use_benchmark(true); + } else if (I->get() == "--benchmark-file") { + if (I->next()) { + OS::get_singleton()->set_use_benchmark(true); + String benchmark_file = I->next()->get(); + OS::get_singleton()->set_benchmark_file(benchmark_file); + N = I->next()->next(); + } else { + OS::get_singleton()->print("Missing argument for --startup-benchmark-file .\n"); + OS::get_singleton()->print("Missing argument for --benchmark-file .\n"); + goto error; + } + } else { main_args.push_back(I->get()); } @@ -1283,6 +1304,8 @@ Error Main::setup(const char *execpath, int argc, char *argv[], bool p_second_ph return setup2(); } + OS::get_singleton()->benchmark_end_measure("core"); + return OK; error: @@ -2179,6 +2202,9 @@ bool Main::start() { } } + OS::get_singleton()->benchmark_end_measure("startup_begin"); + OS::get_singleton()->benchmark_dump(); + return true; } @@ -2423,6 +2449,8 @@ void Main::force_redraw() { * The order matters as some of those steps are linked with each other. */ void Main::cleanup(bool p_force) { + OS::get_singleton()->benchmark_begin_measure("Main::cleanup"); + if (!p_force) { ERR_FAIL_COND(!_start_success); } @@ -2545,6 +2573,9 @@ void Main::cleanup(bool p_force) { unregister_core_driver_types(); unregister_core_types(); + OS::get_singleton()->benchmark_end_measure("Main::cleanup"); + OS::get_singleton()->benchmark_dump(); + OS::get_singleton()->finalize_core(); #ifdef RID_HANDLES_ENABLED diff --git a/misc/dist/shell/_pandemonium.zsh-completion b/misc/dist/shell/_pandemonium.zsh-completion index 6a6357355..e87be67ac 100644 --- a/misc/dist/shell/_pandemonium.zsh-completion +++ b/misc/dist/shell/_pandemonium.zsh-completion @@ -72,5 +72,7 @@ _arguments \ '--export-pack[same as --export, but only export the game pack for the given preset]:export preset name' \ '--doctool[dump the engine API reference to the given path in XML format, merging if existing files are found]:path to base Pandemonium build directory:_dirs' \ '--no-docbase[disallow dumping the base types (used with --doctool)]' \ + '--benchmark[benchmark the run time and print it to console]' \ + '--benchmark-file[benchmark the run time and save it to a given file in JSON format]:path to output JSON file' \ '--gdnative-generate-json-api[generate JSON dump of the Pandemonium API for GDNative bindings]' \ '--test[run a unit test]:unit test name' diff --git a/misc/dist/shell/pandemonium.bash-completion b/misc/dist/shell/pandemonium.bash-completion index 5cd693fbd..c1c02ddfc 100644 --- a/misc/dist/shell/pandemonium.bash-completion +++ b/misc/dist/shell/pandemonium.bash-completion @@ -76,6 +76,8 @@ _complete_pandemonium_options() { --doctool --no-docbase --gdnative-generate-json-api +--benchmark +--benchmark-file --test " -- "$1")) } diff --git a/misc/dist/shell/pandemonium.fish b/misc/dist/shell/pandemonium.fish index 4b07a852b..085df429a 100644 --- a/misc/dist/shell/pandemonium.fish +++ b/misc/dist/shell/pandemonium.fish @@ -88,4 +88,6 @@ complete -c pandemonium -l doctool -d "Dump the engine API reference to the give complete -c pandemonium -l no-docbase -d "Disallow dumping the base types (used with --doctool)" complete -c pandemonium -l build-solutions -d "Build the scripting solutions (e.g. for C# projects)" complete -c pandemonium -l gdnative-generate-json-api -d "Generate JSON dump of the Godot API for GDNative bindings" +complete -c pandemonium -l benchmark -d "Benchmark the run time and print it to console" +complete -c pandemonium -l benchmark-file -d "Benchmark the run time and save it to a given file in JSON format" -x complete -c pandemonium -l test -d "Run a unit test" -x diff --git a/platform/android/java/editor/src/main/java/net/relintai/pandemonium/editor/PandemoniumEditor.kt b/platform/android/java/editor/src/main/java/net/relintai/pandemonium/editor/PandemoniumEditor.kt index 1de73cf02..e47000dd1 100644 --- a/platform/android/java/editor/src/main/java/net/relintai/pandemonium/editor/PandemoniumEditor.kt +++ b/platform/android/java/editor/src/main/java/net/relintai/pandemonium/editor/PandemoniumEditor.kt @@ -108,6 +108,9 @@ open class PandemoniumEditor : FullScreenPandemoniumApp() { if (args != null && args.isNotEmpty()) { commandLineParams.addAll(listOf(*args)) } + if (BuildConfig.BUILD_TYPE == "dev") { + commandLineParams.add("--benchmark") + } } override fun getCommandLine() = commandLineParams @@ -117,7 +120,7 @@ open class PandemoniumEditor : FullScreenPandemoniumApp() { var targetClass: Class<*> = PandemoniumGame::class.java var instanceId = GAME_ID - // Whether we should launch the new godot instance in an adjacent window + // Whether we should launch the new pandemonium instance in an adjacent window // https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT var launchAdjacent = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && (isInMultiWindowMode || isLargeScreen) @@ -220,12 +223,12 @@ open class PandemoniumEditor : FullScreenPandemoniumApp() { protected open fun overrideOrientationRequest() = true /** - * Enable long press gestures for the Godot Android editor. + * Enable long press gestures for the Pandemonium Android editor. */ protected open fun enableLongPressGestures() = true /** - * Enable pan and scale gestures for the Godot Android editor. + * Enable pan and scale gestures for the Pandemonium Android editor. */ protected open fun enablePanAndScaleGestures() = true diff --git a/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/Pandemonium.java b/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/Pandemonium.java index d03f44b86..ee85230d6 100644 --- a/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/Pandemonium.java +++ b/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/Pandemonium.java @@ -38,6 +38,7 @@ import net.relintai.pandemonium.pandemonium.io.directory.DirectoryAccessHandler; import net.relintai.pandemonium.pandemonium.io.file.FileAccessHandler; import net.relintai.pandemonium.pandemonium.plugin.PandemoniumPlugin; import net.relintai.pandemonium.pandemonium.plugin.PandemoniumPluginRegistry; +import net.relintai.pandemonium.pandemonium.utils.BenchmarkUtils; import net.relintai.pandemonium.pandemonium.utils.PandemoniumNetUtils; import net.relintai.pandemonium.pandemonium.utils.PermissionsUtil; @@ -264,6 +265,8 @@ public class Pandemonium extends Fragment implements SensorEventListener, IDownl public PandemoniumIO io; public PandemoniumNetUtils netUtils; + private DirectoryAccessHandler directoryAccessHandler; + private FileAccessHandler fileAccessHandler; static SingletonBase[] singletons = new SingletonBase[MAX_SINGLETONS]; static int singleton_count = 0; @@ -608,7 +611,7 @@ public class Pandemonium extends Fragment implements SensorEventListener, IDownl } return cmdline; } catch (Exception e) { - e.printStackTrace(); + // The _cl_ file can be missing with no adverse effect return new String[0]; } } @@ -669,8 +672,8 @@ public class Pandemonium extends Fragment implements SensorEventListener, IDownl netUtils = new PandemoniumNetUtils(activity); Context context = getContext(); - DirectoryAccessHandler directoryAccessHandler = new DirectoryAccessHandler(context); - FileAccessHandler fileAccessHandler = new FileAccessHandler(context); + directoryAccessHandler = new DirectoryAccessHandler(context); + fileAccessHandler = new FileAccessHandler(context); mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); @@ -693,6 +696,8 @@ public class Pandemonium extends Fragment implements SensorEventListener, IDownl @Override public void onCreate(Bundle icicle) { + BenchmarkUtils.beginBenchmarkMeasure("Pandemonium::onCreate"); + super.onCreate(icicle); final Activity activity = getActivity(); @@ -742,6 +747,17 @@ public class Pandemonium extends Fragment implements SensorEventListener, IDownl editor.apply(); i++; + } else if (command_line[i].equals("--benchmark")) { + BenchmarkUtils.setUseBenchmark(true); + new_args.add(command_line[i]); + } else if (has_extra && command_line[i].equals("--benchmark-file")) { + BenchmarkUtils.setUseBenchmark(true); + new_args.add(command_line[i]); + + // Retrieve the filepath + BenchmarkUtils.setBenchmarkFile(command_line[i + 1]); + new_args.add(command_line[i + 1]); + i++; } else if (command_line[i].trim().length() != 0) { new_args.add(command_line[i]); } @@ -812,6 +828,7 @@ public class Pandemonium extends Fragment implements SensorEventListener, IDownl mCurrentIntent = activity.getIntent(); initializePandemonium(); + BenchmarkUtils.endBenchmarkMeasure("Pandemonium::onCreate"); } @Override @@ -1028,22 +1045,6 @@ public class Pandemonium extends Fragment implements SensorEventListener, IDownl // Do something here if sensor accuracy changes. } - /* - @Override public boolean dispatchKeyEvent(KeyEvent event) { - - if (event.getKeyCode()==KeyEvent.KEYCODE_BACK) { - - System.out.printf("** BACK REQUEST!\n"); - - PandemoniumLib.quit(); - return true; - } - System.out.printf("** OTHER KEY!\n"); - - return false; - } - */ - public void onBackPressed() { boolean shouldQuit = true; @@ -1250,6 +1251,16 @@ public class Pandemonium extends Fragment implements SensorEventListener, IDownl mView.initInputDevices(); } + @Keep + public DirectoryAccessHandler getDirectoryAccessHandler() { + return directoryAccessHandler; + } + + @Keep + public FileAccessHandler getFileAccessHandler() { + return fileAccessHandler; + } + @Keep private int createNewPandemoniumInstance(String[] args) { if (pandemoniumHost != null) { @@ -1258,4 +1269,19 @@ public class Pandemonium extends Fragment implements SensorEventListener, IDownl return 0; } + + @Keep + private void beginBenchmarkMeasure(String label) { + BenchmarkUtils.beginBenchmarkMeasure(label); + } + + @Keep + private void endBenchmarkMeasure(String label) { + BenchmarkUtils.endBenchmarkMeasure(label); + } + + @Keep + private void dumpBenchmark(String benchmarkFile) { + BenchmarkUtils.dumpBenchmark(fileAccessHandler, benchmarkFile); + } } diff --git a/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/PandemoniumView.java b/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/PandemoniumView.java index fcd16a572..611336ea3 100644 --- a/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/PandemoniumView.java +++ b/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/PandemoniumView.java @@ -39,8 +39,13 @@ import net.relintai.pandemonium.pandemonium.config.RegularFallbackConfigChooser; import android.annotation.SuppressLint; import android.content.Context; +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.PixelFormat; import android.os.Build; +import android.text.TextUtils; +import android.util.SparseArray; import android.view.KeyEvent; import android.view.MotionEvent; @@ -48,6 +53,8 @@ import android.view.PointerIcon; import androidx.annotation.Keep; +import java.io.InputStream; + import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLContext; @@ -76,6 +83,7 @@ public class PandemoniumView extends PandemoniumGLSurfaceView { private final Pandemonium pandemonium; private final PandemoniumInputHandler inputHandler; private final PandemoniumRenderer pandemoniumRenderer; + private final SparseArray customPointerIcons = new SparseArray<>(); private EGLConfigChooser eglConfigChooser; private EGLContextFactory eglContextFactory; @@ -146,13 +154,48 @@ public class PandemoniumView extends PandemoniumGLSurfaceView { inputHandler.onPointerCaptureChange(false); } + /** + * Used to configure the PointerIcon for the given type. + * + * Called from JNI + */ + @Keep + public void configurePointerIcon(int pointerType, String imagePath, float hotSpotX, float hotSpotY) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + try { + Bitmap bitmap = null; + if (!TextUtils.isEmpty(imagePath)) { + if (pandemonium.getDirectoryAccessHandler().filesystemFileExists(imagePath)) { + // Try to load the bitmap from the file system + bitmap = BitmapFactory.decodeFile(imagePath); + } else if (pandemonium.getDirectoryAccessHandler().assetsFileExists(imagePath)) { + // Try to load the bitmap from the assets directory + AssetManager am = getContext().getAssets(); + InputStream imageInputStream = am.open(imagePath); + bitmap = BitmapFactory.decodeStream(imageInputStream); + } + } + + PointerIcon customPointerIcon = PointerIcon.create(bitmap, hotSpotX, hotSpotY); + customPointerIcons.put(pointerType, customPointerIcon); + } catch (Exception e) { + // Reset the custom pointer icon + customPointerIcons.delete(pointerType); + } + } + } + /** * Called from JNI to change the pointer icon */ @Keep private void setPointerIcon(int pointerType) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - setPointerIcon(PointerIcon.getSystemIcon(getContext(), pointerType)); + PointerIcon pointerIcon = customPointerIcons.get(pointerType); + if (pointerIcon == null) { + pointerIcon = PointerIcon.getSystemIcon(getContext(), pointerType); + } + setPointerIcon(pointerIcon); } } diff --git a/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/io/directory/DirectoryAccessHandler.kt b/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/io/directory/DirectoryAccessHandler.kt index 8cdfecd06..2d5758d78 100644 --- a/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/io/directory/DirectoryAccessHandler.kt +++ b/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/io/directory/DirectoryAccessHandler.kt @@ -79,6 +79,9 @@ class DirectoryAccessHandler(context: Context) { private val assetsDirAccess = AssetsDirectoryAccess(context) private val fileSystemDirAccess = FilesystemDirectoryAccess(context) + fun assetsFileExists(assetsPath: String) = assetsDirAccess.fileExists(assetsPath) + fun filesystemFileExists(path: String) = fileSystemDirAccess.fileExists(path) + private fun hasDirId(accessType: AccessType, dirId: Int): Boolean { return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId) diff --git a/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/io/file/FileAccessHandler.kt index 79f9f885a..c5bb5c6e9 100644 --- a/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/io/file/FileAccessHandler.kt +++ b/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/io/file/FileAccessHandler.kt @@ -46,7 +46,7 @@ class FileAccessHandler(val context: Context) { private val TAG = FileAccessHandler::class.java.simpleName private const val FILE_NOT_FOUND_ERROR_ID = -1 - private const val INVALID_FILE_ID = 0 + internal const val INVALID_FILE_ID = 0 private const val STARTING_FILE_ID = 1 internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean { @@ -96,6 +96,11 @@ class FileAccessHandler(val context: Context) { private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0 fun fileOpen(path: String?, modeFlags: Int): Int { + val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID + return fileOpen(path, accessFlag) + } + + internal fun fileOpen(path: String?, accessFlag: FileAccessFlags): Int { val storageScope = storageScopeIdentifier.identifyStorageScope(path) if (storageScope == StorageScope.UNKNOWN) { @@ -103,7 +108,6 @@ class FileAccessHandler(val context: Context) { } try { - val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID files.put(++lastFileId, dataAccess) diff --git a/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/utils/BenchmarkUtils.kt b/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/utils/BenchmarkUtils.kt new file mode 100644 index 000000000..80d5d5174 --- /dev/null +++ b/platform/android/java/lib/src/net/relintai/pandemonium/pandemonium/utils/BenchmarkUtils.kt @@ -0,0 +1,123 @@ +/**************************************************************************/ +/* BenchmarkUtils.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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. */ +/**************************************************************************/ + +@file:JvmName("BenchmarkUtils") + +package net.relintai.pandemonium.pandemonium.utils + +import android.os.Build +import android.os.SystemClock +import android.os.Trace +import android.util.Log +import net.relintai.pandemonium.pandemonium.BuildConfig +import net.relintai.pandemonium.pandemonium.io.file.FileAccessFlags +import net.relintai.pandemonium.pandemonium.io.file.FileAccessHandler +import org.json.JSONObject +import java.nio.ByteBuffer +import java.util.concurrent.ConcurrentSkipListMap + + +/** + * Contains benchmark related utilities methods + */ +private const val TAG = "PandemoniumBenchmark" + +var useBenchmark = false +var benchmarkFile = "" + +private val startBenchmarkFrom = ConcurrentSkipListMap() +private val benchmarkTracker = ConcurrentSkipListMap() + +/** + * Start measuring and tracing the execution of a given section of code using the given label. + * + * Must be followed by a call to [endBenchmarkMeasure]. + * + * Note: Only enabled on 'editorDev' build variant. + */ +fun beginBenchmarkMeasure(label: String) { + if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { + return + } + startBenchmarkFrom[label] = SystemClock.elapsedRealtime() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Trace.beginAsyncSection(label, 0) + } +} + +/** + * End measuring and tracing of the section of code with the given label. + * + * Must be preceded by a call [beginBenchmarkMeasure] + * + * Note: Only enabled on 'editorDev' build variant. + */ +fun endBenchmarkMeasure(label: String) { + if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { + return + } + val startTime = startBenchmarkFrom[label] ?: return + val total = SystemClock.elapsedRealtime() - startTime + benchmarkTracker[label] = total / 1000.0 + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Trace.endAsyncSection(label, 0) + } +} + +/** + * Dump the benchmark measurements. + * If [filepath] is valid, the data is also written in json format to the specified file. + * + * Note: Only enabled on 'editorDev' build variant. + */ +@JvmOverloads +fun dumpBenchmark(fileAccessHandler: FileAccessHandler?, filepath: String? = benchmarkFile) { + if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { + return + } + if (!useBenchmark) { + return + } + + val printOut = + benchmarkTracker.map { "\t- ${it.key} : ${it.value} sec." }.joinToString("\n") + Log.i(TAG, "BENCHMARK:\n$printOut") + + if (fileAccessHandler != null && !filepath.isNullOrBlank()) { + val fileId = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE) + if (fileId != FileAccessHandler.INVALID_FILE_ID) { + val jsonOutput = JSONObject(benchmarkTracker.toMap()).toString(4) + fileAccessHandler.fileWrite(fileId, ByteBuffer.wrap(jsonOutput.toByteArray())) + fileAccessHandler.fileClose(fileId) + } + } +} \ No newline at end of file diff --git a/platform/android/java_pandemonium_view_wrapper.cpp b/platform/android/java_pandemonium_view_wrapper.cpp index 88687a2ae..cd4292e17 100644 --- a/platform/android/java_pandemonium_view_wrapper.cpp +++ b/platform/android/java_pandemonium_view_wrapper.cpp @@ -40,6 +40,7 @@ PandemoniumJavaViewWrapper::PandemoniumJavaViewWrapper(jobject pandemonium_view) int android_device_api_level = android_get_device_api_level(); if (android_device_api_level >= __ANDROID_API_N__) { + _configure_pointer_icon = env->GetMethodID(_cls, "configurePointerIcon", "(ILjava/lang/String;FF)V"); _set_pointer_icon = env->GetMethodID(_cls, "setPointerIcon", "(I)V"); } if (android_device_api_level >= __ANDROID_API_O__) { @@ -49,7 +50,7 @@ PandemoniumJavaViewWrapper::PandemoniumJavaViewWrapper(jobject pandemonium_view) } bool PandemoniumJavaViewWrapper::can_update_pointer_icon() const { - return _set_pointer_icon != nullptr; + return _configure_pointer_icon != nullptr && _set_pointer_icon != nullptr; } bool PandemoniumJavaViewWrapper::can_capture_pointer() const { @@ -74,6 +75,16 @@ void PandemoniumJavaViewWrapper::release_pointer_capture() { } } +void PandemoniumJavaViewWrapper::configure_pointer_icon(int pointer_type, const String &image_path, const Vector2 &p_hotspot) { + if (_configure_pointer_icon != nullptr) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + + jstring jImagePath = env->NewStringUTF(image_path.utf8().get_data()); + env->CallVoidMethod(_pandemonium_view, _configure_pointer_icon, pointer_type, jImagePath, p_hotspot.x, p_hotspot.y); + } +} + void PandemoniumJavaViewWrapper::set_pointer_icon(int pointer_type) { if (_set_pointer_icon != nullptr) { JNIEnv *env = get_jni_env(); diff --git a/platform/android/java_pandemonium_view_wrapper.h b/platform/android/java_pandemonium_view_wrapper.h index e225117c8..a7aebb124 100644 --- a/platform/android/java_pandemonium_view_wrapper.h +++ b/platform/android/java_pandemonium_view_wrapper.h @@ -31,6 +31,8 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ +#include "core/math/vector2.h" + #include #include @@ -44,6 +46,8 @@ private: jmethodID _request_pointer_capture = 0; jmethodID _release_pointer_capture = 0; + + jmethodID _configure_pointer_icon = 0; jmethodID _set_pointer_icon = 0; public: @@ -54,6 +58,8 @@ public: void request_pointer_capture(); void release_pointer_capture(); + + void configure_pointer_icon(int pointer_type, const String &image_path, const Vector2 &p_hotspot); void set_pointer_icon(int pointer_type); ~PandemoniumJavaViewWrapper(); diff --git a/platform/android/java_pandemonium_wrapper.cpp b/platform/android/java_pandemonium_wrapper.cpp index 127d82eb7..9206f68bb 100644 --- a/platform/android/java_pandemonium_wrapper.cpp +++ b/platform/android/java_pandemonium_wrapper.cpp @@ -83,6 +83,9 @@ PandemoniumJavaWrapper::PandemoniumJavaWrapper(JNIEnv *p_env, jobject p_activity _create_new_pandemonium_instance = p_env->GetMethodID(pandemonium_class, "createNewPandemoniumInstance", "([Ljava/lang/String;)I"); _request_framebuffer_swap = p_env->GetMethodID(pandemonium_class, "requestFramebufferSwap", "()V"); _get_render_view = p_env->GetMethodID(pandemonium_class, "getRenderView", "()Lnet/relintai/pandemonium/pandemonium/PandemoniumView;"); + _begin_benchmark_measure = p_env->GetMethodID(pandemonium_class, "beginBenchmarkMeasure", "(Ljava/lang/String;)V"); + _end_benchmark_measure = p_env->GetMethodID(pandemonium_class, "endBenchmarkMeasure", "(Ljava/lang/String;)V"); + _dump_benchmark = p_env->GetMethodID(pandemonium_class, "dumpBenchmark", "(Ljava/lang/String;)V"); // get some Activity method pointers... _get_class_loader = p_env->GetMethodID(activity_class, "getClassLoader", "()Ljava/lang/ClassLoader;"); @@ -425,4 +428,31 @@ void PandemoniumJavaWrapper::request_framebuffer_swap() { env->CallVoidMethod(pandemonium_instance, _request_framebuffer_swap); } +} + +void PandemoniumJavaWrapper::begin_benchmark_measure(const String &p_label) { + if (_begin_benchmark_measure) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + jstring j_label = env->NewStringUTF(p_label.utf8().get_data()); + env->CallVoidMethod(pandemonium_instance, _begin_benchmark_measure, j_label); + } +} + +void PandemoniumJavaWrapper::end_benchmark_measure(const String &p_label) { + if (_end_benchmark_measure) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + jstring j_label = env->NewStringUTF(p_label.utf8().get_data()); + env->CallVoidMethod(pandemonium_instance, _end_benchmark_measure, j_label); + } +} + +void PandemoniumJavaWrapper::dump_benchmark(const String &benchmark_file) { + if (_dump_benchmark) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL(env); + jstring j_benchmark_file = env->NewStringUTF(benchmark_file.utf8().get_data()); + env->CallVoidMethod(pandemonium_instance, _dump_benchmark, j_benchmark_file); + } } \ No newline at end of file diff --git a/platform/android/java_pandemonium_wrapper.h b/platform/android/java_pandemonium_wrapper.h index ebd3276e4..36369525d 100644 --- a/platform/android/java_pandemonium_wrapper.h +++ b/platform/android/java_pandemonium_wrapper.h @@ -77,6 +77,9 @@ private: jmethodID _create_new_pandemonium_instance = 0; jmethodID _request_framebuffer_swap = 0; jmethodID _get_render_view = 0; + jmethodID _begin_benchmark_measure = 0; + jmethodID _end_benchmark_measure = 0; + jmethodID _dump_benchmark = 0; public: PandemoniumJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_pandemonium_instance); @@ -116,6 +119,9 @@ public: String get_input_fallback_mapping(); int create_new_pandemonium_instance(List args); void request_framebuffer_swap(); + void begin_benchmark_measure(const String &p_label); + void end_benchmark_measure(const String &p_label); + void dump_benchmark(const String &benchmark_file); }; #endif /* !JAVA_PANDEMONIUM_WRAPPER_H */ diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 75e58cd06..9edf6ec97 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -264,11 +264,11 @@ OS::MouseMode OS_Android::get_mouse_mode() const { return mouse_mode; } -void OS_Android::set_cursor_shape(CursorShape p_shape) { +void OS_Android::_set_cursor_shape_helper(CursorShape p_shape, bool force) { if (!pandemonium_java->get_pandemonium_view()->can_update_pointer_icon()) { return; } - if (cursor_shape == p_shape) { + if (cursor_shape == p_shape && !force) { return; } @@ -278,6 +278,19 @@ void OS_Android::set_cursor_shape(CursorShape p_shape) { } } +void OS_Android::set_cursor_shape(CursorShape p_shape) { + _set_cursor_shape_helper(p_shape); +} + +void OS_Android::set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) { + String cursor_path = p_cursor.is_valid() ? p_cursor->get_path() : ""; + if (!cursor_path.empty()) { + cursor_path = ProjectSettings::get_singleton()->globalize_path(cursor_path); + } + pandemonium_java->get_pandemonium_view()->configure_pointer_icon(android_cursors[cursor_shape], cursor_path, p_hotspot); + _set_cursor_shape_helper(p_shape, true); +} + OS::CursorShape OS_Android::get_cursor_shape() const { return cursor_shape; } @@ -636,6 +649,27 @@ String OS_Android::get_config_path() const { return get_user_data_dir().plus_file("config"); } +void OS_Android::benchmark_begin_measure(const String &p_what) { +#ifdef TOOLS_ENABLED + pandemonium_java->begin_benchmark_measure(p_what); +#endif +} + +void OS_Android::benchmark_end_measure(const String &p_what) { +#ifdef TOOLS_ENABLED + pandemonium_java->end_benchmark_measure(p_what); +#endif +} + +void OS_Android::benchmark_dump() { +#ifdef TOOLS_ENABLED + if (!is_use_benchmark_set()) { + return; + } + pandemonium_java->dump_benchmark(get_benchmark_file()); +#endif +} + bool OS_Android::_check_internal_feature_support(const String &p_feature) { if (p_feature == "mobile") { //TODO support etc2 only if GLES3 driver is selected diff --git a/platform/android/os_android.h b/platform/android/os_android.h index 534592761..c095ddb2b 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -125,8 +125,11 @@ public: virtual Error open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path = false); + void _set_cursor_shape_helper(CursorShape p_shape, bool force = false); virtual void set_cursor_shape(CursorShape p_shape); virtual CursorShape get_cursor_shape() const; + virtual void set_custom_mouse_cursor(const RES &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot); + virtual void set_mouse_mode(MouseMode p_mode); virtual MouseMode get_mouse_mode() const; @@ -212,6 +215,10 @@ public: virtual Error execute(const String &p_path, const List &p_arguments, bool p_blocking = true, ProcessID *r_child_id = nullptr, String *r_pipe = nullptr, int *r_exitcode = nullptr, bool read_stderr = false, Mutex *p_pipe_mutex = nullptr, bool p_open_console = false); virtual Error kill(const ProcessID &p_pid); + virtual void benchmark_begin_measure(const String &p_what); + virtual void benchmark_end_measure(const String &p_what); + virtual void benchmark_dump(); + void swap_buffers(); virtual bool _check_internal_feature_support(const String &p_feature);