diff options
Diffstat (limited to 'platform')
94 files changed, 2467 insertions, 1014 deletions
diff --git a/platform/android/SCsub b/platform/android/SCsub index bc1b5e9200..8c88b419b3 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -95,25 +95,18 @@ if lib_arch_dir != "": else: gradle_process = ["./gradlew"] - if env["target"] != "editor" and env["dev_build"]: - subprocess.run( - gradle_process - + [ - "generateDevTemplate", - "--quiet", - ], - cwd="platform/android/java", - ) - else: - # Android editor with `dev_build=yes` is handled by the `generateGodotEditor` task. - subprocess.run( - gradle_process - + [ - "generateGodotEditor" if env["target"] == "editor" else "generateGodotTemplates", - "--quiet", - ], - cwd="platform/android/java", - ) + gradle_process += [ + "generateGodotEditor" if env["target"] == "editor" else "generateGodotTemplates", + "--quiet", + ] + + if env["debug_symbols"]: + gradle_process += ["-PdoNotStrip=true"] + + subprocess.run( + gradle_process, + cwd="platform/android/java", + ) if env["generate_apk"]: generate_apk_command = env_android.Command("generate_apk", [], generate_apk) diff --git a/platform/android/audio_driver_opensl.cpp b/platform/android/audio_driver_opensl.cpp index 51e89c720d..ef9c51db07 100644 --- a/platform/android/audio_driver_opensl.cpp +++ b/platform/android/audio_driver_opensl.cpp @@ -268,6 +268,10 @@ Error AudioDriverOpenSL::init_input_device() { } Error AudioDriverOpenSL::input_start() { + if (recordItf || recordBufferQueueItf) { + return ERR_ALREADY_IN_USE; + } + if (OS::get_singleton()->request_permission("RECORD_AUDIO")) { return init_input_device(); } @@ -277,6 +281,10 @@ Error AudioDriverOpenSL::input_start() { } Error AudioDriverOpenSL::input_stop() { + if (!recordItf || !recordBufferQueueItf) { + return ERR_CANT_OPEN; + } + SLuint32 state; SLresult res = (*recordItf)->GetRecordState(recordItf, &state); ERR_FAIL_COND_V(res != SL_RESULT_SUCCESS, ERR_CANT_OPEN); @@ -313,13 +321,36 @@ void AudioDriverOpenSL::unlock() { } void AudioDriverOpenSL::finish() { - (*sl)->Destroy(sl); + if (recordItf) { + (*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_STOPPED); + recordItf = nullptr; + } + if (recorder) { + (*recorder)->Destroy(recorder); + recorder = nullptr; + } + if (playItf) { + (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_STOPPED); + playItf = nullptr; + } + if (player) { + (*player)->Destroy(player); + player = nullptr; + } + if (OutputMix) { + (*OutputMix)->Destroy(OutputMix); + OutputMix = nullptr; + } + if (sl) { + (*sl)->Destroy(sl); + sl = nullptr; + } } void AudioDriverOpenSL::set_pause(bool p_pause) { pause = p_pause; - if (active) { + if (active && playItf) { if (pause) { (*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PAUSED); } else { diff --git a/platform/android/audio_driver_opensl.h b/platform/android/audio_driver_opensl.h index 6ea0f77def..bcd173826a 100644 --- a/platform/android/audio_driver_opensl.h +++ b/platform/android/audio_driver_opensl.h @@ -54,15 +54,15 @@ class AudioDriverOpenSL : public AudioDriver { Vector<int16_t> rec_buffer; - SLPlayItf playItf; - SLRecordItf recordItf; - SLObjectItf sl; - SLEngineItf EngineItf; - SLObjectItf OutputMix; - SLObjectItf player; - SLObjectItf recorder; - SLAndroidSimpleBufferQueueItf bufferQueueItf; - SLAndroidSimpleBufferQueueItf recordBufferQueueItf; + SLPlayItf playItf = nullptr; + SLRecordItf recordItf = nullptr; + SLObjectItf sl = nullptr; + SLEngineItf EngineItf = nullptr; + SLObjectItf OutputMix = nullptr; + SLObjectItf player = nullptr; + SLObjectItf recorder = nullptr; + SLAndroidSimpleBufferQueueItf bufferQueueItf = nullptr; + SLAndroidSimpleBufferQueueItf recordBufferQueueItf = nullptr; SLDataSource audioSource; SLDataFormat_PCM pcm; SLDataSink audioSink; diff --git a/platform/android/detect.py b/platform/android/detect.py index 0b182aca90..0a10754e24 100644 --- a/platform/android/detect.py +++ b/platform/android/detect.py @@ -190,6 +190,8 @@ def configure(env: "SConsEnvironment"): env.Append(CCFLAGS=["-mfix-cortex-a53-835769"]) env.Append(CPPDEFINES=["__ARM_ARCH_8A__"]) + env.Append(CCFLAGS=["-ffp-contract=off"]) + # Link flags env.Append(LINKFLAGS="-Wl,--gc-sections -Wl,--no-undefined -Wl,-z,now".split()) diff --git a/platform/android/display_server_android.cpp b/platform/android/display_server_android.cpp index 06b304dcde..8dc0e869d0 100644 --- a/platform/android/display_server_android.cpp +++ b/platform/android/display_server_android.cpp @@ -651,7 +651,6 @@ DisplayServerAndroid::DisplayServerAndroid(const String &p_rendering_driver, Dis #endif Input::get_singleton()->set_event_dispatch_function(_dispatch_input_events); - Input::get_singleton()->set_use_input_buffering(true); // Needed because events will come directly from the UI thread r_error = OK; } diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 479158c91f..689360aef6 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -441,6 +441,7 @@ void EditorExportPlatformAndroid::_update_preset_status() { } else { has_runnable_preset.clear(); } + devices_changed.set(); } #endif @@ -2322,7 +2323,8 @@ static bool has_valid_keystore_credentials(String &r_error_str, const String &p_ args.push_back(p_password); args.push_back("-alias"); args.push_back(p_username); - Error error = OS::get_singleton()->execute("keytool", args, &output, nullptr, true); + String keytool_path = EditorExportPlatformAndroid::get_keytool_path(); + Error error = OS::get_singleton()->execute(keytool_path, args, &output, nullptr, true); String keytool_error = "keytool error:"; bool valid = output.substr(0, keytool_error.length()) != keytool_error; @@ -3268,18 +3270,17 @@ Error EditorExportPlatformAndroid::export_project_helper(const Ref<EditorExportP } List<String> copy_args; - String copy_command; - if (export_format == EXPORT_FORMAT_AAB) { - copy_command = vformat("copyAndRename%sAab", build_type); - } else if (export_format == EXPORT_FORMAT_APK) { - copy_command = vformat("copyAndRename%sApk", build_type); - } - + String copy_command = "copyAndRenameBinary"; copy_args.push_back(copy_command); copy_args.push_back("-p"); // argument to specify the start directory. copy_args.push_back(build_path); // start directory. + copy_args.push_back("-Pexport_build_type=" + build_type.to_lower()); + + String export_format_arg = export_format == EXPORT_FORMAT_AAB ? "aab" : "apk"; + copy_args.push_back("-Pexport_format=" + export_format_arg); + String export_filename = p_path.get_file(); String export_path = p_path.get_base_dir(); if (export_path.is_relative_path()) { diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 01d5d9ef92..05b4f379b3 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -211,70 +211,24 @@ android { } } -task copyAndRenameDebugApk(type: Copy) { +task copyAndRenameBinary(type: Copy) { // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files // and directories. Otherwise this check may cause permissions access failures on Windows // machines. doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") - from "$buildDir/outputs/apk/debug/android_debug.apk" - into getExportPath() - rename "android_debug.apk", getExportFilename() -} + String exportPath = getExportPath() + String exportFilename = getExportFilename() + String exportBuildType = getExportBuildType() + String exportFormat = getExportFormat() -task copyAndRenameDevApk(type: Copy) { - // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files - // and directories. Otherwise this check may cause permissions access failures on Windows - // machines. - doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") - - from "$buildDir/outputs/apk/dev/android_dev.apk" - into getExportPath() - rename "android_dev.apk", getExportFilename() -} - -task copyAndRenameReleaseApk(type: Copy) { - // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files - // and directories. Otherwise this check may cause permissions access failures on Windows - // machines. - doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") - - from "$buildDir/outputs/apk/release/android_release.apk" - into getExportPath() - rename "android_release.apk", getExportFilename() -} - -task copyAndRenameDebugAab(type: Copy) { - // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files - // and directories. Otherwise this check may cause permissions access failures on Windows - // machines. - doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") - - from "$buildDir/outputs/bundle/debug/build-debug.aab" - into getExportPath() - rename "build-debug.aab", getExportFilename() -} - -task copyAndRenameDevAab(type: Copy) { - // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files - // and directories. Otherwise this check may cause permissions access failures on Windows - // machines. - doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") - - from "$buildDir/outputs/bundle/dev/build-dev.aab" - into getExportPath() - rename "build-dev.aab", getExportFilename() -} - -task copyAndRenameReleaseAab(type: Copy) { - // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files - // and directories. Otherwise this check may cause permissions access failures on Windows - // machines. - doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") + boolean isAab = exportFormat == "aab" + String sourceFilepath = isAab ? "$buildDir/outputs/bundle/$exportBuildType/build-${exportBuildType}.aab" : "$buildDir/outputs/apk/$exportBuildType/android_${exportBuildType}.apk" + String sourceFilename = isAab ? "build-${exportBuildType}.aab" : "android_${exportBuildType}.apk" - from "$buildDir/outputs/bundle/release/build-release.aab" - into getExportPath() - rename "build-release.aab", getExportFilename() + from sourceFilepath + into exportPath + rename sourceFilename, exportFilename } /** diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index eb9ad9de05..611a9c4a40 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -224,6 +224,22 @@ ext.getExportFilename = { return exportFilename } +ext.getExportBuildType = { + String exportBuildType = project.hasProperty("export_build_type") ? project.property("export_build_type") : "" + if (exportBuildType == null || exportBuildType.isEmpty()) { + exportBuildType = "debug" + } + return exportBuildType +} + +ext.getExportFormat = { + String exportFormat = project.hasProperty("export_format") ? project.property("export_format") : "" + if (exportFormat == null || exportFormat.isEmpty()) { + exportFormat = "apk" + } + return exportFormat +} + /** * Parse the project properties for the 'plugins_maven_repos' property and return the list * of maven repos. diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index b91b023ce6..771bda6948 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -35,116 +35,17 @@ ext { // `./gradlew generateGodotTemplates` build command instead after running the `scons` command(s). // The {selectedAbis} values must be from the {supportedAbis} values. selectedAbis = ["arm64"] -} -def rootDir = "../../.." -def binDir = "$rootDir/bin/" -def androidEditorBuildsDir = "$binDir/android_editor_builds/" + rootDir = "../../.." + binDir = "$rootDir/bin/" + androidEditorBuildsDir = "$binDir/android_editor_builds/" +} def getSconsTaskName(String flavor, String buildType, String abi) { return "compileGodotNativeLibs" + flavor.capitalize() + buildType.capitalize() + abi.capitalize() } /** - * Copy the generated 'android_debug.apk' binary template into the Godot bin directory. - * Depends on the app build task to ensure the binary is generated prior to copying. - */ -task copyDebugBinaryToBin(type: Copy) { - dependsOn ':app:assembleDebug' - from('app/build/outputs/apk/debug') - into(binDir) - include('android_debug.apk') -} - -/** - * Copy the generated 'android_dev.apk' binary template into the Godot bin directory. - * Depends on the app build task to ensure the binary is generated prior to copying. - */ -task copyDevBinaryToBin(type: Copy) { - dependsOn ':app:assembleDev' - from('app/build/outputs/apk/dev') - into(binDir) - include('android_dev.apk') -} - -/** - * Copy the generated 'android_release.apk' binary template into the Godot bin directory. - * Depends on the app build task to ensure the binary is generated prior to copying. - */ -task copyReleaseBinaryToBin(type: Copy) { - dependsOn ':app:assembleRelease' - from('app/build/outputs/apk/release') - into(binDir) - include('android_release.apk') -} - -/** - * Copy the Godot android library archive debug file into the app module debug libs directory. - * Depends on the library build task to ensure the AAR file is generated prior to copying. - */ -task copyDebugAARToAppModule(type: Copy) { - dependsOn ':lib:assembleTemplateDebug' - from('lib/build/outputs/aar') - into('app/libs/debug') - include('godot-lib.template_debug.aar') -} - -/** - * Copy the Godot android library archive debug file into the root bin directory. - * Depends on the library build task to ensure the AAR file is generated prior to copying. - */ -task copyDebugAARToBin(type: Copy) { - dependsOn ':lib:assembleTemplateDebug' - from('lib/build/outputs/aar') - into(binDir) - include('godot-lib.template_debug.aar') -} - -/** - * Copy the Godot android library archive dev file into the app module dev libs directory. - * Depends on the library build task to ensure the AAR file is generated prior to copying. - */ -task copyDevAARToAppModule(type: Copy) { - dependsOn ':lib:assembleTemplateDev' - from('lib/build/outputs/aar') - into('app/libs/dev') - include('godot-lib.template_debug.dev.aar') -} - -/** - * Copy the Godot android library archive dev file into the root bin directory. - * Depends on the library build task to ensure the AAR file is generated prior to copying. - */ -task copyDevAARToBin(type: Copy) { - dependsOn ':lib:assembleTemplateDev' - from('lib/build/outputs/aar') - into(binDir) - include('godot-lib.template_debug.dev.aar') -} - -/** - * Copy the Godot android library archive release file into the app module release libs directory. - * Depends on the library build task to ensure the AAR file is generated prior to copying. - */ -task copyReleaseAARToAppModule(type: Copy) { - dependsOn ':lib:assembleTemplateRelease' - from('lib/build/outputs/aar') - into('app/libs/release') - include('godot-lib.template_release.aar') -} - -/** - * Copy the Godot android library archive release file into the root bin directory. - * Depends on the library build task to ensure the AAR file is generated prior to copying. - */ -task copyReleaseAARToBin(type: Copy) { - dependsOn ':lib:assembleTemplateRelease' - from('lib/build/outputs/aar') - into(binDir) - include('godot-lib.template_release.aar') -} - -/** * Generate Godot gradle build template by zipping the source files from the app directory, as well * as the AAR files generated by 'copyDebugAAR', 'copyDevAAR' and 'copyReleaseAAR'. * The zip file also includes some gradle tools to enable gradle builds from the Godot Editor. @@ -197,7 +98,7 @@ def generateBuildTasks(String flavor = "template") { throw new GradleException("Invalid build flavor: $flavor") } - def tasks = [] + def buildTasks = [] // Only build the apks and aar files for which we have native shared libraries unless we intend // to run the scons build tasks. @@ -206,72 +107,93 @@ def generateBuildTasks(String flavor = "template") { String libsDir = isTemplate ? "lib/libs/" : "lib/libs/tools/" for (String target : supportedFlavorsBuildTypes[flavor]) { File targetLibs = new File(libsDir + target) + + String targetSuffix = target + if (target == "dev") { + targetSuffix = "debug.dev" + } + if (!excludeSconsBuildTasks || (targetLibs != null && targetLibs.isDirectory() && targetLibs.listFiles() != null && targetLibs.listFiles().length > 0)) { + String capitalizedTarget = target.capitalize() if (isTemplate) { - // Copy the generated aar library files to the build directory. - tasks += "copy${capitalizedTarget}AARToAppModule" - // Copy the generated aar library files to the bin directory. - tasks += "copy${capitalizedTarget}AARToBin" - // Copy the prebuilt binary templates to the bin directory. - tasks += "copy${capitalizedTarget}BinaryToBin" + // Copy the Godot android library archive file into the app module libs directory. + // Depends on the library build task to ensure the AAR file is generated prior to copying. + String copyAARTaskName = "copy${capitalizedTarget}AARToAppModule" + if (tasks.findByName(copyAARTaskName) != null) { + buildTasks += tasks.getByName(copyAARTaskName) + } else { + buildTasks += tasks.create(name: copyAARTaskName, type: Copy) { + dependsOn ":lib:assembleTemplate${capitalizedTarget}" + from('lib/build/outputs/aar') + include("godot-lib.template_${targetSuffix}.aar") + into("app/libs/${target}") + } + } + + // Copy the Godot android library archive file into the root bin directory. + // Depends on the library build task to ensure the AAR file is generated prior to copying. + String copyAARToBinTaskName = "copy${capitalizedTarget}AARToBin" + if (tasks.findByName(copyAARToBinTaskName) != null) { + buildTasks += tasks.getByName(copyAARToBinTaskName) + } else { + buildTasks += tasks.create(name: copyAARToBinTaskName, type: Copy) { + dependsOn ":lib:assembleTemplate${capitalizedTarget}" + from('lib/build/outputs/aar') + include("godot-lib.template_${targetSuffix}.aar") + into(binDir) + } + } + + // Copy the generated binary template into the Godot bin directory. + // Depends on the app build task to ensure the binary is generated prior to copying. + String copyBinaryTaskName = "copy${capitalizedTarget}BinaryToBin" + if (tasks.findByName(copyBinaryTaskName) != null) { + buildTasks += tasks.getByName(copyBinaryTaskName) + } else { + buildTasks += tasks.create(name: copyBinaryTaskName, type: Copy) { + dependsOn ":app:assemble${capitalizedTarget}" + from("app/build/outputs/apk/${target}") + into(binDir) + include("android_${target}.apk") + } + } } else { // Copy the generated editor apk to the bin directory. - tasks += "copyEditor${capitalizedTarget}ApkToBin" + String copyEditorApkTaskName = "copyEditor${capitalizedTarget}ApkToBin" + if (tasks.findByName(copyEditorApkTaskName) != null) { + buildTasks += tasks.getByName(copyEditorApkTaskName) + } else { + buildTasks += tasks.create(name: copyEditorApkTaskName, type: Copy) { + dependsOn ":editor:assemble${capitalizedTarget}" + from("editor/build/outputs/apk/${target}") + into(androidEditorBuildsDir) + include("android_editor-${target}*.apk") + } + } + // Copy the generated editor aab to the bin directory. - tasks += "copyEditor${capitalizedTarget}AabToBin" + String copyEditorAabTaskName = "copyEditor${capitalizedTarget}AabToBin" + if (tasks.findByName(copyEditorAabTaskName) != null) { + buildTasks += tasks.getByName(copyEditorAabTaskName) + } else { + buildTasks += tasks.create(name: copyEditorAabTaskName, type: Copy) { + dependsOn ":editor:bundle${capitalizedTarget}" + from("editor/build/outputs/bundle/${target}") + into(androidEditorBuildsDir) + include("android_editor-${target}*.aab") + } + } } } else { logger.lifecycle("No native shared libs for target $target. Skipping build.") } } - return tasks -} - -task copyEditorReleaseApkToBin(type: Copy) { - dependsOn ':editor:assembleRelease' - from('editor/build/outputs/apk/release') - into(androidEditorBuildsDir) - include('android_editor-release*.apk') -} - -task copyEditorReleaseAabToBin(type: Copy) { - dependsOn ':editor:bundleRelease' - from('editor/build/outputs/bundle/release') - into(androidEditorBuildsDir) - include('android_editor-release*.aab') -} - -task copyEditorDebugApkToBin(type: Copy) { - dependsOn ':editor:assembleDebug' - from('editor/build/outputs/apk/debug') - into(androidEditorBuildsDir) - include('android_editor-debug.apk') -} - -task copyEditorDebugAabToBin(type: Copy) { - dependsOn ':editor:bundleDebug' - from('editor/build/outputs/bundle/debug') - into(androidEditorBuildsDir) - include('android_editor-debug.aab') -} - -task copyEditorDevApkToBin(type: Copy) { - dependsOn ':editor:assembleDev' - from('editor/build/outputs/apk/dev') - into(androidEditorBuildsDir) - include('android_editor-dev.apk') -} - -task copyEditorDevAabToBin(type: Copy) { - dependsOn ':editor:bundleDev' - from('editor/build/outputs/bundle/dev') - into(androidEditorBuildsDir) - include('android_editor-dev.aab') + return buildTasks } /** @@ -301,7 +223,7 @@ task generateGodotTemplates { */ task generateDevTemplate { // add parameter to set symbols to true - gradle.startParameter.projectProperties += [doNotStrip: "true"] + project.ext.doNotStrip = "true" gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() dependsOn = generateBuildTasks("template") diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt index dad397de61..7c11d69609 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt @@ -117,10 +117,6 @@ open class GodotEditor : GodotActivity() { val longPressEnabled = enableLongPressGestures() val panScaleEnabled = enablePanAndScaleGestures() - val useInputBuffering = useInputBuffering() - val useAccumulatedInput = useAccumulatedInput() - GodotLib.updateInputDispatchSettings(useAccumulatedInput, useInputBuffering) - checkForProjectPermissionsToEnable() runOnUiThread { @@ -128,7 +124,6 @@ open class GodotEditor : GodotActivity() { godotFragment?.godot?.renderView?.inputHandler?.apply { enableLongPress(longPressEnabled) enablePanningAndScalingGestures(panScaleEnabled) - enableInputDispatchToRenderThread(!useInputBuffering && !useAccumulatedInput) } } } @@ -208,7 +203,14 @@ open class GodotEditor : GodotActivity() { } if (editorWindowInfo.windowClassName == javaClass.name) { Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") - ProcessPhoenix.triggerRebirth(this, newInstance) + val godot = godot + if (godot != null) { + godot.destroyAndKillProcess { + ProcessPhoenix.triggerRebirth(this, newInstance) + } + } else { + ProcessPhoenix.triggerRebirth(this, newInstance) + } } else { Log.d(TAG, "Starting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") newInstance.putExtra(EXTRA_NEW_LAUNCH, true) @@ -280,13 +282,6 @@ open class GodotEditor : GodotActivity() { java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_pan_and_scale_gestures")) /** - * Use input buffering for the Godot Android editor. - */ - protected open fun useInputBuffering() = java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/editor/android/use_input_buffering")) - - protected open fun useAccumulatedInput() = java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/editor/android/use_accumulated_input")) - - /** * Whether we should launch the new godot instance in an adjacent window * @see https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT */ diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt index f50b5577c3..2bcfba559c 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt @@ -45,10 +45,6 @@ class GodotGame : GodotEditor() { override fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) - override fun useInputBuffering() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/buffering/android/use_input_buffering")) - - override fun useAccumulatedInput() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/buffering/android/use_accumulated_input")) - override fun checkForProjectPermissionsToEnable() { // Nothing to do.. by the time we get here, the project permissions will have already // been requested by the Editor window. diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index c188a97ca5..111cd48405 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -39,8 +39,6 @@ import android.content.res.Configuration import android.content.res.Resources import android.graphics.Color import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener import android.hardware.SensorManager import android.os.* import android.util.Log @@ -53,6 +51,7 @@ import androidx.core.view.WindowInsetsAnimationCompat import androidx.core.view.WindowInsetsCompat import com.google.android.vending.expansion.downloader.* import org.godotengine.godot.input.GodotEditText +import org.godotengine.godot.input.GodotInputHandler import org.godotengine.godot.io.directory.DirectoryAccessHandler import org.godotengine.godot.io.file.FileAccessHandler import org.godotengine.godot.plugin.GodotPluginRegistry @@ -73,6 +72,8 @@ import java.io.InputStream import java.lang.Exception import java.security.MessageDigest import java.util.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference /** * Core component used to interface with the native layer of the engine. @@ -80,7 +81,7 @@ import java.util.* * Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its * lifecycle methods are properly invoked. */ -class Godot(private val context: Context) : SensorEventListener { +class Godot(private val context: Context) { private companion object { private val TAG = Godot::class.java.simpleName @@ -98,15 +99,23 @@ class Godot(private val context: Context) : SensorEventListener { private val pluginRegistry: GodotPluginRegistry by lazy { GodotPluginRegistry.getPluginRegistry() } + + private val accelerometer_enabled = AtomicBoolean(false) private val mAccelerometer: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) } + + private val gravity_enabled = AtomicBoolean(false) private val mGravity: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) } + + private val magnetometer_enabled = AtomicBoolean(false) private val mMagnetometer: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) } + + private val gyroscope_enabled = AtomicBoolean(false) private val mGyroscope: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) } @@ -126,6 +135,12 @@ class Godot(private val context: Context) : SensorEventListener { val fileAccessHandler = FileAccessHandler(context) val netUtils = GodotNetUtils(context) private val commandLineFileParser = CommandLineFileParser() + private val godotInputHandler = GodotInputHandler(context, this) + + /** + * Task to run when the engine terminates. + */ + private val runOnTerminate = AtomicReference<Runnable>() /** * Tracks whether [onCreate] was completed successfully. @@ -148,6 +163,17 @@ class Godot(private val context: Context) : SensorEventListener { private var renderViewInitialized = false private var primaryHost: GodotHost? = null + /** + * Tracks whether we're in the RESUMED lifecycle state. + * See [onResume] and [onPause] + */ + private var resumed = false + + /** + * Tracks whether [onGodotSetupCompleted] fired. + */ + private val godotMainLoopStarted = AtomicBoolean(false) + var io: GodotIO? = null private var commandLine : MutableList<String> = ArrayList<String>() @@ -410,10 +436,10 @@ class Godot(private val context: Context) : SensorEventListener { if (!meetsVulkanRequirements(activity.packageManager)) { throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message)) } - GodotVulkanRenderView(host, this) + GodotVulkanRenderView(host, this, godotInputHandler) } else { // Fallback to openGl - GodotGLRenderView(host, this, xrMode, useDebugOpengl) + GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl) } if (host == primaryHost) { @@ -514,23 +540,13 @@ class Godot(private val context: Context) : SensorEventListener { fun onResume(host: GodotHost) { Log.v(TAG, "OnResume: $host") + resumed = true if (host != primaryHost) { return } renderView?.onActivityResumed() - if (mAccelerometer != null) { - mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME) - } - if (mGravity != null) { - mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME) - } - if (mMagnetometer != null) { - mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME) - } - if (mGyroscope != null) { - mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME) - } + registerSensorsIfNeeded() if (useImmersive) { val window = requireActivity().window window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or @@ -545,14 +561,34 @@ class Godot(private val context: Context) : SensorEventListener { } } + private fun registerSensorsIfNeeded() { + if (!resumed || !godotMainLoopStarted.get()) { + return + } + + if (accelerometer_enabled.get() && mAccelerometer != null) { + mSensorManager.registerListener(godotInputHandler, mAccelerometer, SensorManager.SENSOR_DELAY_GAME) + } + if (gravity_enabled.get() && mGravity != null) { + mSensorManager.registerListener(godotInputHandler, mGravity, SensorManager.SENSOR_DELAY_GAME) + } + if (magnetometer_enabled.get() && mMagnetometer != null) { + mSensorManager.registerListener(godotInputHandler, mMagnetometer, SensorManager.SENSOR_DELAY_GAME) + } + if (gyroscope_enabled.get() && mGyroscope != null) { + mSensorManager.registerListener(godotInputHandler, mGyroscope, SensorManager.SENSOR_DELAY_GAME) + } + } + fun onPause(host: GodotHost) { Log.v(TAG, "OnPause: $host") + resumed = false if (host != primaryHost) { return } renderView?.onActivityPaused() - mSensorManager.unregisterListener(this) + mSensorManager.unregisterListener(godotInputHandler) for (plugin in pluginRegistry.allPlugins) { plugin.onMainPause() } @@ -577,10 +613,7 @@ class Godot(private val context: Context) : SensorEventListener { plugin.onMainDestroy() } - runOnRenderThread { - GodotLib.ondestroy() - forceQuit() - } + renderView?.onActivityDestroyed() } /** @@ -628,26 +661,19 @@ class Godot(private val context: Context) : SensorEventListener { private fun onGodotSetupCompleted() { Log.v(TAG, "OnGodotSetupCompleted") - if (!isEditorBuild()) { - // These properties are defined after Godot setup completion, so we retrieve them here. - val longPressEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click")) - val panScaleEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) - val rotaryInputAxisValue = GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis") - - val useInputBuffering = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/buffering/android/use_input_buffering")) - val useAccumulatedInput = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/buffering/android/use_accumulated_input")) - GodotLib.updateInputDispatchSettings(useAccumulatedInput, useInputBuffering) - - runOnUiThread { - renderView?.inputHandler?.apply { - enableLongPress(longPressEnabled) - enablePanningAndScalingGestures(panScaleEnabled) - enableInputDispatchToRenderThread(!useInputBuffering && !useAccumulatedInput) - try { - setRotaryInputAxis(Integer.parseInt(rotaryInputAxisValue)) - } catch (e: NumberFormatException) { - Log.w(TAG, e) - } + // These properties are defined after Godot setup completion, so we retrieve them here. + val longPressEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click")) + val panScaleEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) + val rotaryInputAxisValue = GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis") + + runOnUiThread { + renderView?.inputHandler?.apply { + enableLongPress(longPressEnabled) + enablePanningAndScalingGestures(panScaleEnabled) + try { + setRotaryInputAxis(Integer.parseInt(rotaryInputAxisValue)) + } catch (e: NumberFormatException) { + Log.w(TAG, e) } } } @@ -663,6 +689,16 @@ class Godot(private val context: Context) : SensorEventListener { */ private fun onGodotMainLoopStarted() { Log.v(TAG, "OnGodotMainLoopStarted") + godotMainLoopStarted.set(true) + + accelerometer_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_accelerometer"))) + gravity_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gravity"))) + gyroscope_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gyroscope"))) + magnetometer_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_magnetometer"))) + + runOnUiThread { + registerSensorsIfNeeded() + } for (plugin in pluginRegistry.allPlugins) { plugin.onGodotMainLoopStarted() @@ -670,6 +706,15 @@ class Godot(private val context: Context) : SensorEventListener { primaryHost?.onGodotMainLoopStarted() } + /** + * Invoked on the render thread when the engine is about to terminate. + */ + @Keep + private fun onGodotTerminating() { + Log.v(TAG, "OnGodotTerminating") + runOnTerminate.get()?.run() + } + private fun restart() { primaryHost?.onGodotRestartRequested(this) } @@ -805,8 +850,28 @@ class Godot(private val context: Context) : SensorEventListener { mClipboard.setPrimaryClip(clip) } - fun forceQuit() { - forceQuit(0) + /** + * Destroys the Godot Engine and kill the process it's running in. + */ + @JvmOverloads + fun destroyAndKillProcess(destroyRunnable: Runnable? = null) { + val host = primaryHost + val activity = host?.activity + if (host == null || activity == null) { + // Run the destroyRunnable right away as we are about to force quit. + destroyRunnable?.run() + + // Fallback to force quit + forceQuit(0) + return + } + + // Store the destroyRunnable so it can be run when the engine is terminating + runOnTerminate.set(destroyRunnable) + + runOnUiThread { + onDestroy(host) + } } @Keep @@ -821,11 +886,7 @@ class Godot(private val context: Context) : SensorEventListener { } ?: return false } - fun onBackPressed(host: GodotHost) { - if (host != primaryHost) { - return - } - + fun onBackPressed() { var shouldQuit = true for (plugin in pluginRegistry.allPlugins) { if (plugin.onMainBackPressed()) { @@ -837,77 +898,6 @@ class Godot(private val context: Context) : SensorEventListener { } } - private fun getRotatedValues(values: FloatArray?): FloatArray? { - if (values == null || values.size != 3) { - return null - } - val rotatedValues = FloatArray(3) - when (windowManager.defaultDisplay.rotation) { - Surface.ROTATION_0 -> { - rotatedValues[0] = values[0] - rotatedValues[1] = values[1] - rotatedValues[2] = values[2] - } - Surface.ROTATION_90 -> { - rotatedValues[0] = -values[1] - rotatedValues[1] = values[0] - rotatedValues[2] = values[2] - } - Surface.ROTATION_180 -> { - rotatedValues[0] = -values[0] - rotatedValues[1] = -values[1] - rotatedValues[2] = values[2] - } - Surface.ROTATION_270 -> { - rotatedValues[0] = values[1] - rotatedValues[1] = -values[0] - rotatedValues[2] = values[2] - } - } - return rotatedValues - } - - override fun onSensorChanged(event: SensorEvent) { - if (renderView == null) { - return - } - - val rotatedValues = getRotatedValues(event.values) - - when (event.sensor.type) { - Sensor.TYPE_ACCELEROMETER -> { - rotatedValues?.let { - renderView?.queueOnRenderThread { - GodotLib.accelerometer(-it[0], -it[1], -it[2]) - } - } - } - Sensor.TYPE_GRAVITY -> { - rotatedValues?.let { - renderView?.queueOnRenderThread { - GodotLib.gravity(-it[0], -it[1], -it[2]) - } - } - } - Sensor.TYPE_MAGNETIC_FIELD -> { - rotatedValues?.let { - renderView?.queueOnRenderThread { - GodotLib.magnetometer(-it[0], -it[1], -it[2]) - } - } - } - Sensor.TYPE_GYROSCOPE -> { - rotatedValues?.let { - renderView?.queueOnRenderThread { - GodotLib.gyroscope(it[0], it[1], it[2]) - } - } - } - } - } - - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} - /** * Used by the native code (java_godot_wrapper.h) to vibrate the device. * @param durationMs @@ -1042,7 +1032,7 @@ class Godot(private val context: Context) : SensorEventListener { @Keep private fun initInputDevices() { - renderView?.initInputDevices() + godotInputHandler.initInputDevices() } @Keep diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt index 4c5e857b7a..913e3d04c5 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt @@ -85,12 +85,8 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { protected open fun getGodotAppLayout() = R.layout.godot_app_layout override fun onDestroy() { - Log.v(TAG, "Destroying Godot app...") + Log.v(TAG, "Destroying GodotActivity $this...") super.onDestroy() - - godotFragment?.let { - terminateGodotInstance(it.godot) - } } override fun onGodotForceQuit(instance: Godot) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java index 1612ddd0b3..fdda766594 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java @@ -187,7 +187,12 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH final Activity activity = getActivity(); mCurrentIntent = activity.getIntent(); - godot = new Godot(requireContext()); + if (parentHost != null) { + godot = parentHost.getGodot(); + } + if (godot == null) { + godot = new Godot(requireContext()); + } performEngineInitialization(); BenchmarkUtils.endBenchmarkMeasure("Startup", "GodotFragment::onCreate"); } @@ -209,7 +214,7 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH final String errorMessage = TextUtils.isEmpty(e.getMessage()) ? getString(R.string.error_engine_setup_message) : e.getMessage(); - godot.alert(errorMessage, getString(R.string.text_error_title), godot::forceQuit); + godot.alert(errorMessage, getString(R.string.text_error_title), godot::destroyAndKillProcess); } catch (IllegalArgumentException ignored) { final Activity activity = getActivity(); Intent notifierIntent = new Intent(activity, activity.getClass()); @@ -325,7 +330,7 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH } public void onBackPressed() { - godot.onBackPressed(this); + godot.onBackPressed(); } /** diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java index 81043ce782..15a811ce83 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -42,7 +42,6 @@ import org.godotengine.godot.xr.regular.RegularContextFactory; import org.godotengine.godot.xr.regular.RegularFallbackConfigChooser; import android.annotation.SuppressLint; -import android.content.Context; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -77,19 +76,19 @@ import java.io.InputStream; * that matches it exactly (with regards to red/green/blue/alpha channels * bit depths). Failure to do so would result in an EGL_BAD_MATCH error. */ -public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView { +class GodotGLRenderView extends GLSurfaceView implements GodotRenderView { private final GodotHost host; private final Godot godot; private final GodotInputHandler inputHandler; private final GodotRenderer godotRenderer; private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>(); - public GodotGLRenderView(GodotHost host, Godot godot, XRMode xrMode, boolean useDebugOpengl) { + public GodotGLRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler, XRMode xrMode, boolean useDebugOpengl) { super(host.getActivity()); this.host = host; this.godot = godot; - this.inputHandler = new GodotInputHandler(this); + this.inputHandler = inputHandler; this.godotRenderer = new GodotRenderer(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); @@ -103,11 +102,6 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView } @Override - public void initInputDevices() { - this.inputHandler.initInputDevices(); - } - - @Override public void queueOnRenderThread(Runnable event) { queueEvent(event); } @@ -141,8 +135,8 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView } @Override - public void onBackPressed() { - godot.onBackPressed(host); + public void onActivityDestroyed() { + requestRenderThreadExitAndWait(); } @Override diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index 37e889daf7..909daf05c9 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -242,9 +242,8 @@ public class GodotLib { public static native void onRendererPaused(); /** - * Invoked on the GL thread to update the input dispatch settings - * @param useAccumulatedInput True to use accumulated input, false otherwise - * @param useInputBuffering True to use input buffering, false otherwise + * @return true if input must be dispatched from the render thread. If false, input is + * dispatched from the UI thread. */ - public static native void updateInputDispatchSettings(boolean useAccumulatedInput, boolean useInputBuffering); + public static native boolean shouldDispatchInputToRenderThread(); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java index 5b2f9f57c7..30821eaa8e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java @@ -37,13 +37,14 @@ import android.view.SurfaceView; public interface GodotRenderView { SurfaceView getView(); - void initInputDevices(); - /** * Starts the thread that will drive Godot's rendering. */ void startRenderer(); + /** + * Queues a runnable to be run on the rendering thread. + */ void queueOnRenderThread(Runnable event); void onActivityPaused(); @@ -54,7 +55,7 @@ public interface GodotRenderView { void onActivityStarted(); - void onBackPressed(); + void onActivityDestroyed(); GodotInputHandler getInputHandler(); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index a1ee9bd6b4..d5b05913d8 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -50,19 +50,19 @@ import androidx.annotation.Keep; import java.io.InputStream; -public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView { +class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView { private final GodotHost host; private final Godot godot; private final GodotInputHandler mInputHandler; private final VkRenderer mRenderer; private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>(); - public GodotVulkanRenderView(GodotHost host, Godot godot) { + public GodotVulkanRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler) { super(host.getActivity()); this.host = host; this.godot = godot; - mInputHandler = new GodotInputHandler(this); + mInputHandler = inputHandler; mRenderer = new VkRenderer(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); @@ -81,11 +81,6 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV } @Override - public void initInputDevices() { - mInputHandler.initInputDevices(); - } - - @Override public void queueOnRenderThread(Runnable event) { queueOnVkThread(event); } @@ -119,8 +114,8 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV } @Override - public void onBackPressed() { - godot.onBackPressed(host); + public void onActivityDestroyed() { + requestRenderThreadExitAndWait(); } @Override diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java index c9421a3257..6a4e9da699 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java @@ -595,6 +595,15 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback protected final void resumeGLThread() { mGLThread.onResume(); } + + /** + * Requests the render thread to exit and block until it does. + */ + protected final void requestRenderThreadExitAndWait() { + if (mGLThread != null) { + mGLThread.requestExitAndWait(); + } + } // -- GODOT end -- /** @@ -783,6 +792,11 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback * @return true if the buffers should be swapped, false otherwise. */ boolean onDrawFrame(GL10 gl); + + /** + * Invoked when the render thread is in the process of shutting down. + */ + void onRenderThreadExiting(); // -- GODOT end -- } @@ -1621,6 +1635,12 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback * clean-up everything... */ synchronized (sGLThreadManager) { + Log.d("GLThread", "Exiting render thread"); + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mRenderer.onRenderThreadExiting(); + } + stopEglSurfaceLocked(); stopEglContextLocked(); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java index 9d44d8826c..7e5e262b2d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java @@ -34,6 +34,8 @@ import org.godotengine.godot.GodotLib; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPluginRegistry; +import android.util.Log; + import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; @@ -41,6 +43,8 @@ import javax.microedition.khronos.opengles.GL10; * Godot's GL renderer implementation. */ public class GodotRenderer implements GLSurfaceView.Renderer { + private final String TAG = GodotRenderer.class.getSimpleName(); + private final GodotPluginRegistry pluginRegistry; private boolean activityJustResumed = false; @@ -62,6 +66,12 @@ public class GodotRenderer implements GLSurfaceView.Renderer { return swapBuffers; } + @Override + public void onRenderThreadExiting() { + Log.d(TAG, "Destroying Godot Engine"); + GodotLib.ondestroy(); + } + public void onSurfaceChanged(GL10 gl, int width, int height) { GodotLib.resize(null, width, height); for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt index 4cd3bd8db9..2929a0a0b0 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt @@ -76,7 +76,10 @@ internal class GodotGestureHandler(private val inputHandler: GodotInputHandler) } override fun onLongPress(event: MotionEvent) { - contextClickRouter(event) + val toolType = GodotInputHandler.getEventToolType(event) + if (toolType != MotionEvent.TOOL_TYPE_MOUSE) { + contextClickRouter(event) + } } private fun contextClickRouter(event: MotionEvent) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java index 889618914d..fb41cd00c0 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -32,10 +32,14 @@ package org.godotengine.godot.input; import static org.godotengine.godot.utils.GLUtils.DEBUG; +import org.godotengine.godot.Godot; import org.godotengine.godot.GodotLib; import org.godotengine.godot.GodotRenderView; import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; import android.hardware.input.InputManager; import android.os.Build; import android.util.Log; @@ -46,6 +50,10 @@ import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ScaleGestureDetector; +import android.view.Surface; +import android.view.WindowManager; + +import androidx.annotation.NonNull; import java.util.Collections; import java.util.HashSet; @@ -54,7 +62,7 @@ import java.util.Set; /** * Handles input related events for the {@link GodotRenderView} view. */ -public class GodotInputHandler implements InputManager.InputDeviceListener { +public class GodotInputHandler implements InputManager.InputDeviceListener, SensorEventListener { private static final String TAG = GodotInputHandler.class.getSimpleName(); private static final int ROTARY_INPUT_VERTICAL_AXIS = 1; @@ -64,8 +72,9 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { private final SparseArray<Joystick> mJoysticksDevices = new SparseArray<>(4); private final HashSet<Integer> mHardwareKeyboardIds = new HashSet<>(); - private final GodotRenderView mRenderView; + private final Godot godot; private final InputManager mInputManager; + private final WindowManager windowManager; private final GestureDetector gestureDetector; private final ScaleGestureDetector scaleGestureDetector; private final GodotGestureHandler godotGestureHandler; @@ -77,14 +86,13 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { private int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS; - private boolean dispatchInputToRenderThread = false; - - public GodotInputHandler(GodotRenderView godotView) { - final Context context = godotView.getView().getContext(); - mRenderView = godotView; + public GodotInputHandler(Context context, Godot godot) { + this.godot = godot; mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE); mInputManager.registerInputDeviceListener(this, null); + windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); + this.godotGestureHandler = new GodotGestureHandler(this); this.gestureDetector = new GestureDetector(context, godotGestureHandler); this.gestureDetector.setIsLongpressEnabled(false); @@ -111,19 +119,11 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } /** - * Specifies whether input should be dispatch on the UI thread or on the Render thread. - * @param enable true to dispatch input on the Render thread, false to dispatch input on the UI thread - */ - public void enableInputDispatchToRenderThread(boolean enable) { - this.dispatchInputToRenderThread = enable; - } - - /** * @return true if input must be dispatched from the render thread. If false, input is * dispatched from the UI thread. */ private boolean shouldDispatchInputToRenderThread() { - return dispatchInputToRenderThread; + return GodotLib.shouldDispatchInputToRenderThread(); } /** @@ -184,7 +184,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { public boolean onKeyDown(final int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { - mRenderView.onBackPressed(); + godot.onBackPressed(); // press 'back' button should not terminate program //normal handle 'back' event in game logic return true; @@ -472,7 +472,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return button; } - private static int getEventToolType(MotionEvent event) { + static int getEventToolType(MotionEvent event) { return event.getPointerCount() > 0 ? event.getToolType(0) : MotionEvent.TOOL_TYPE_UNKNOWN; } @@ -517,7 +517,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return handleTouchEvent(event, eventActionOverride, doubleTap); } - private static float getEventTiltX(MotionEvent event) { + static float getEventTiltX(MotionEvent event) { // Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise. final float orientation = event.getOrientation(); @@ -530,7 +530,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return (float)-Math.sin(orientation) * tiltMult; } - private static float getEventTiltY(MotionEvent event) { + static float getEventTiltY(MotionEvent event) { // Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise. final float orientation = event.getOrientation(); @@ -589,6 +589,11 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) { + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return false; + } + // Fix the buttonsMask switch (eventAction) { case MotionEvent.ACTION_CANCEL: @@ -604,7 +609,6 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { break; } - final int updatedButtonsMask = buttonsMask; // We don't handle ACTION_BUTTON_PRESS and ACTION_BUTTON_RELEASE events as they typically // follow ACTION_DOWN and ACTION_UP events. As such, handling them would result in duplicate // stream of events to the engine. @@ -617,11 +621,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { case MotionEvent.ACTION_HOVER_MOVE: case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_SCROLL: { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.dispatchMouseEvent(eventAction, updatedButtonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY)); - } else { - GodotLib.dispatchMouseEvent(eventAction, updatedButtonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY); - } + runnable.setMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY); + dispatchInputEventRunnable(runnable); return true; } } @@ -637,22 +638,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { - final int pointerCount = event.getPointerCount(); - if (pointerCount == 0) { + if (event.getPointerCount() == 0) { return true; } - final float[] positions = new float[pointerCount * 6]; // pointerId1, x1, y1, pressure1, tiltX1, tiltY1, pointerId2, etc... - - for (int i = 0; i < pointerCount; i++) { - positions[i * 6 + 0] = event.getPointerId(i); - positions[i * 6 + 1] = event.getX(i); - positions[i * 6 + 2] = event.getY(i); - positions[i * 6 + 3] = event.getPressure(i); - positions[i * 6 + 4] = getEventTiltX(event); - positions[i * 6 + 5] = getEventTiltY(event); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return false; } - final int actionPointerId = event.getPointerId(event.getActionIndex()); switch (eventActionOverride) { case MotionEvent.ACTION_DOWN: @@ -661,11 +654,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_POINTER_DOWN: { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap)); - } else { - GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap); - } + runnable.setTouchEvent(event, eventActionOverride, doubleTap); + dispatchInputEventRunnable(runnable); return true; } } @@ -673,58 +663,128 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } void handleMagnifyEvent(float x, float y, float factor) { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.magnify(x, y, factor)); - } else { - GodotLib.magnify(x, y, factor); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; } + + runnable.setMagnifyEvent(x, y, factor); + dispatchInputEventRunnable(runnable); } void handlePanEvent(float x, float y, float deltaX, float deltaY) { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.pan(x, y, deltaX, deltaY)); - } else { - GodotLib.pan(x, y, deltaX, deltaY); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; } + + runnable.setPanEvent(x, y, deltaX, deltaY); + dispatchInputEventRunnable(runnable); } private void handleJoystickButtonEvent(int device, int button, boolean pressed) { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.joybutton(device, button, pressed)); - } else { - GodotLib.joybutton(device, button, pressed); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; } + + runnable.setJoystickButtonEvent(device, button, pressed); + dispatchInputEventRunnable(runnable); } private void handleJoystickAxisEvent(int device, int axis, float value) { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.joyaxis(device, axis, value)); - } else { - GodotLib.joyaxis(device, axis, value); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; } + + runnable.setJoystickAxisEvent(device, axis, value); + dispatchInputEventRunnable(runnable); } private void handleJoystickHatEvent(int device, int hatX, int hatY) { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.joyhat(device, hatX, hatY)); - } else { - GodotLib.joyhat(device, hatX, hatY); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; } + + runnable.setJoystickHatEvent(device, hatX, hatY); + dispatchInputEventRunnable(runnable); } private void handleJoystickConnectionChangedEvent(int device, boolean connected, String name) { - if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.joyconnectionchanged(device, connected, name)); - } else { - GodotLib.joyconnectionchanged(device, connected, name); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; } + + runnable.setJoystickConnectionChangedEvent(device, connected, name); + dispatchInputEventRunnable(runnable); } void handleKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) { + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; + } + + runnable.setKeyEvent(physicalKeycode, unicode, keyLabel, pressed, echo); + dispatchInputEventRunnable(runnable); + } + + private void dispatchInputEventRunnable(@NonNull InputEventRunnable runnable) { if (shouldDispatchInputToRenderThread()) { - mRenderView.queueOnRenderThread(() -> GodotLib.key(physicalKeycode, unicode, keyLabel, pressed, echo)); + godot.runOnRenderThread(runnable); } else { - GodotLib.key(physicalKeycode, unicode, keyLabel, pressed, echo); + runnable.run(); + } + } + + @Override + public void onSensorChanged(SensorEvent event) { + final float[] values = event.values; + if (values == null || values.length != 3) { + return; + } + + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; + } + + float rotatedValue0 = 0f; + float rotatedValue1 = 0f; + float rotatedValue2 = 0f; + switch (windowManager.getDefaultDisplay().getRotation()) { + case Surface.ROTATION_0: + rotatedValue0 = values[0]; + rotatedValue1 = values[1]; + rotatedValue2 = values[2]; + break; + + case Surface.ROTATION_90: + rotatedValue0 = -values[1]; + rotatedValue1 = values[0]; + rotatedValue2 = values[2]; + break; + + case Surface.ROTATION_180: + rotatedValue0 = -values[0]; + rotatedValue1 = -values[1]; + rotatedValue2 = values[2]; + break; + + case Surface.ROTATION_270: + rotatedValue0 = values[1]; + rotatedValue1 = -values[0]; + rotatedValue2 = values[2]; + break; } + + runnable.setSensorEvent(event.sensor.getType(), rotatedValue0, rotatedValue1, rotatedValue2); + godot.runOnRenderThread(runnable); } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java new file mode 100644 index 0000000000..a282791b2e --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java @@ -0,0 +1,353 @@ +/**************************************************************************/ +/* InputEventRunnable.java */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +package org.godotengine.godot.input; + +import org.godotengine.godot.GodotLib; + +import android.hardware.Sensor; +import android.util.Log; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Pools; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Used to dispatch input events. + * + * This is a specialized version of @{@link Runnable} which allows to allocate a finite pool of + * objects for input events dispatching, thus avoid the creation (and garbage collection) of + * spurious @{@link Runnable} objects. + */ +final class InputEventRunnable implements Runnable { + private static final String TAG = InputEventRunnable.class.getSimpleName(); + + private static final int MAX_TOUCH_POINTER_COUNT = 10; // assuming 10 fingers as max supported concurrent touch pointers + + private static final Pools.Pool<InputEventRunnable> POOL = new Pools.Pool<>() { + private static final int MAX_POOL_SIZE = 120 * 10; // up to 120Hz input events rate for up to 5 secs (ANR limit) * 2 + + private final ArrayBlockingQueue<InputEventRunnable> queue = new ArrayBlockingQueue<>(MAX_POOL_SIZE); + private final AtomicInteger createdCount = new AtomicInteger(); + + @Nullable + @Override + public InputEventRunnable acquire() { + InputEventRunnable instance = queue.poll(); + if (instance == null) { + int creationCount = createdCount.incrementAndGet(); + if (creationCount <= MAX_POOL_SIZE) { + instance = new InputEventRunnable(creationCount - 1); + } + } + + return instance; + } + + @Override + public boolean release(@NonNull InputEventRunnable instance) { + return queue.offer(instance); + } + }; + + @Nullable + static InputEventRunnable obtain() { + InputEventRunnable runnable = POOL.acquire(); + if (runnable == null) { + Log.w(TAG, "Input event pool is at capacity"); + } + return runnable; + } + + /** + * Used to track when this instance was created and added to the pool. Primarily used for + * debug purposes. + */ + private final int creationRank; + + private InputEventRunnable(int creationRank) { + this.creationRank = creationRank; + } + + /** + * Set of supported input events. + */ + private enum EventType { + MOUSE, + TOUCH, + MAGNIFY, + PAN, + JOYSTICK_BUTTON, + JOYSTICK_AXIS, + JOYSTICK_HAT, + JOYSTICK_CONNECTION_CHANGED, + KEY, + SENSOR + } + + private EventType currentEventType = null; + + // common event fields + private float eventX; + private float eventY; + private float eventDeltaX; + private float eventDeltaY; + private boolean eventPressed; + + // common touch / mouse fields + private int eventAction; + private boolean doubleTap; + + // Mouse event fields and setter + private int buttonsMask; + private boolean sourceMouseRelative; + private float pressure; + private float tiltX; + private float tiltY; + void setMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) { + this.currentEventType = EventType.MOUSE; + this.eventAction = eventAction; + this.buttonsMask = buttonsMask; + this.eventX = x; + this.eventY = y; + this.eventDeltaX = deltaX; + this.eventDeltaY = deltaY; + this.doubleTap = doubleClick; + this.sourceMouseRelative = sourceMouseRelative; + this.pressure = pressure; + this.tiltX = tiltX; + this.tiltY = tiltY; + } + + // Touch event fields and setter + private int actionPointerId; + private int pointerCount; + private final float[] positions = new float[MAX_TOUCH_POINTER_COUNT * 6]; // pointerId1, x1, y1, pressure1, tiltX1, tiltY1, pointerId2, etc... + void setTouchEvent(MotionEvent event, int eventAction, boolean doubleTap) { + this.currentEventType = EventType.TOUCH; + this.eventAction = eventAction; + this.doubleTap = doubleTap; + this.actionPointerId = event.getPointerId(event.getActionIndex()); + this.pointerCount = Math.min(event.getPointerCount(), MAX_TOUCH_POINTER_COUNT); + for (int i = 0; i < pointerCount; i++) { + positions[i * 6 + 0] = event.getPointerId(i); + positions[i * 6 + 1] = event.getX(i); + positions[i * 6 + 2] = event.getY(i); + positions[i * 6 + 3] = event.getPressure(i); + positions[i * 6 + 4] = GodotInputHandler.getEventTiltX(event); + positions[i * 6 + 5] = GodotInputHandler.getEventTiltY(event); + } + } + + // Magnify event fields and setter + private float magnifyFactor; + void setMagnifyEvent(float x, float y, float factor) { + this.currentEventType = EventType.MAGNIFY; + this.eventX = x; + this.eventY = y; + this.magnifyFactor = factor; + } + + // Pan event setter + void setPanEvent(float x, float y, float deltaX, float deltaY) { + this.currentEventType = EventType.PAN; + this.eventX = x; + this.eventY = y; + this.eventDeltaX = deltaX; + this.eventDeltaY = deltaY; + } + + // common joystick field + private int joystickDevice; + + // Joystick button event fields and setter + private int button; + void setJoystickButtonEvent(int device, int button, boolean pressed) { + this.currentEventType = EventType.JOYSTICK_BUTTON; + this.joystickDevice = device; + this.button = button; + this.eventPressed = pressed; + } + + // Joystick axis event fields and setter + private int axis; + private float value; + void setJoystickAxisEvent(int device, int axis, float value) { + this.currentEventType = EventType.JOYSTICK_AXIS; + this.joystickDevice = device; + this.axis = axis; + this.value = value; + } + + // Joystick hat event fields and setter + private int hatX; + private int hatY; + void setJoystickHatEvent(int device, int hatX, int hatY) { + this.currentEventType = EventType.JOYSTICK_HAT; + this.joystickDevice = device; + this.hatX = hatX; + this.hatY = hatY; + } + + // Joystick connection changed event fields and setter + private boolean connected; + private String joystickName; + void setJoystickConnectionChangedEvent(int device, boolean connected, String name) { + this.currentEventType = EventType.JOYSTICK_CONNECTION_CHANGED; + this.joystickDevice = device; + this.connected = connected; + this.joystickName = name; + } + + // Key event fields and setter + private int physicalKeycode; + private int unicode; + private int keyLabel; + private boolean echo; + void setKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) { + this.currentEventType = EventType.KEY; + this.physicalKeycode = physicalKeycode; + this.unicode = unicode; + this.keyLabel = keyLabel; + this.eventPressed = pressed; + this.echo = echo; + } + + // Sensor event fields and setter + private int sensorType; + private float rotatedValue0; + private float rotatedValue1; + private float rotatedValue2; + void setSensorEvent(int sensorType, float rotatedValue0, float rotatedValue1, float rotatedValue2) { + this.currentEventType = EventType.SENSOR; + this.sensorType = sensorType; + this.rotatedValue0 = rotatedValue0; + this.rotatedValue1 = rotatedValue1; + this.rotatedValue2 = rotatedValue2; + } + + @Override + public void run() { + try { + if (currentEventType == null) { + Log.w(TAG, "Invalid event type"); + return; + } + + switch (currentEventType) { + case MOUSE: + GodotLib.dispatchMouseEvent( + eventAction, + buttonsMask, + eventX, + eventY, + eventDeltaX, + eventDeltaY, + doubleTap, + sourceMouseRelative, + pressure, + tiltX, + tiltY); + break; + + case TOUCH: + GodotLib.dispatchTouchEvent( + eventAction, + actionPointerId, + pointerCount, + positions, + doubleTap); + break; + + case MAGNIFY: + GodotLib.magnify(eventX, eventY, magnifyFactor); + break; + + case PAN: + GodotLib.pan(eventX, eventY, eventDeltaX, eventDeltaY); + break; + + case JOYSTICK_BUTTON: + GodotLib.joybutton(joystickDevice, button, eventPressed); + break; + + case JOYSTICK_AXIS: + GodotLib.joyaxis(joystickDevice, axis, value); + break; + + case JOYSTICK_HAT: + GodotLib.joyhat(joystickDevice, hatX, hatY); + break; + + case JOYSTICK_CONNECTION_CHANGED: + GodotLib.joyconnectionchanged(joystickDevice, connected, joystickName); + break; + + case KEY: + GodotLib.key(physicalKeycode, unicode, keyLabel, eventPressed, echo); + break; + + case SENSOR: + switch (sensorType) { + case Sensor.TYPE_ACCELEROMETER: + GodotLib.accelerometer(-rotatedValue0, -rotatedValue1, -rotatedValue2); + break; + + case Sensor.TYPE_GRAVITY: + GodotLib.gravity(-rotatedValue0, -rotatedValue1, -rotatedValue2); + break; + + case Sensor.TYPE_MAGNETIC_FIELD: + GodotLib.magnetometer(-rotatedValue0, -rotatedValue1, -rotatedValue2); + break; + + case Sensor.TYPE_GYROSCOPE: + GodotLib.gyroscope(rotatedValue0, rotatedValue1, rotatedValue2); + break; + } + break; + } + } finally { + recycle(); + } + } + + /** + * Release the current instance back to the pool + */ + private void recycle() { + currentEventType = null; + POOL.release(this); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt index f2c0577c21..d0b8a8dffa 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt @@ -53,7 +53,7 @@ internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAcc fun fileLastModified(filepath: String): Long { return try { - File(filepath).lastModified() + File(filepath).lastModified() / 1000L } catch (e: SecurityException) { 0L } diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt index 5410eed727..146fc04da4 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt @@ -203,7 +203,7 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi } val dataItem = result[0] - return dataItem.dateModified.toLong() + return dataItem.dateModified.toLong() / 1000L } fun rename(context: Context, from: String, to: String): Boolean { diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java index 711bca02e7..8976dd65db 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java +++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java @@ -43,6 +43,7 @@ import androidx.annotation.Nullable; import java.lang.reflect.Constructor; import java.util.Collection; +import java.util.Collections; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -82,6 +83,9 @@ public final class GodotPluginRegistry { * Retrieve the full set of loaded plugins. */ public Collection<GodotPlugin> getAllPlugins() { + if (registry.isEmpty()) { + return Collections.emptyList(); + } return registry.values(); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt index 6f09f51d4c..a93a7dbe09 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt @@ -31,11 +31,9 @@ @file:JvmName("VkRenderer") package org.godotengine.godot.vulkan +import android.util.Log import android.view.Surface - -import org.godotengine.godot.Godot import org.godotengine.godot.GodotLib -import org.godotengine.godot.plugin.GodotPlugin import org.godotengine.godot.plugin.GodotPluginRegistry /** @@ -52,6 +50,11 @@ import org.godotengine.godot.plugin.GodotPluginRegistry * @see [VkSurfaceView.startRenderer] */ internal class VkRenderer { + + companion object { + private val TAG = VkRenderer::class.java.simpleName + } + private val pluginRegistry: GodotPluginRegistry = GodotPluginRegistry.getPluginRegistry() /** @@ -101,8 +104,10 @@ internal class VkRenderer { } /** - * Called when the rendering thread is destroyed and used as signal to tear down the Vulkan logic. + * Invoked when the render thread is in the process of shutting down. */ - fun onVkDestroy() { + fun onRenderThreadExiting() { + Log.d(TAG, "Destroying Godot Engine") + GodotLib.ondestroy() } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt index 791b425444..9e30de6a15 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt @@ -113,12 +113,10 @@ open internal class VkSurfaceView(context: Context) : SurfaceView(context), Surf } /** - * Tear down the rendering thread. - * - * Must not be called before a [VkRenderer] has been set. + * Requests the render thread to exit and block until it does. */ - fun onDestroy() { - vkThread.blockingExit() + fun requestRenderThreadExitAndWait() { + vkThread.requestExitAndWait() } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt index 8c0065b31e..c7cb97d911 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt @@ -75,6 +75,9 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk private fun threadExiting() { lock.withLock { + Log.d(TAG, "Exiting render thread") + vkRenderer.onRenderThreadExiting() + exited = true lockCondition.signalAll() } @@ -93,7 +96,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk /** * Request the thread to exit and block until it's done. */ - fun blockingExit() { + fun requestExitAndWait() { lock.withLock { shouldExit = true lockCondition.signalAll() @@ -171,7 +174,6 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk while (true) { // Code path for exiting the thread loop. if (shouldExit) { - vkRenderer.onVkDestroy() return } diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index 87d4281c5a..a4a425f685 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -51,6 +51,7 @@ #include "core/config/project_settings.h" #include "core/input/input.h" #include "main/main.h" +#include "servers/xr_server.h" #ifdef TOOLS_ENABLED #include "editor/editor_settings.h" @@ -83,6 +84,10 @@ static Vector3 magnetometer; static Vector3 gyroscope; static void _terminate(JNIEnv *env, bool p_restart = false) { + if (step.get() == STEP_TERMINATED) { + return; + } + step.set(STEP_TERMINATED); // Ensure no further steps are attempted and no further events are sent // lets cleanup @@ -114,6 +119,7 @@ static void _terminate(JNIEnv *env, bool p_restart = false) { NetSocketAndroid::terminate(); if (godot_java) { + godot_java->on_godot_terminating(env); if (!restart_on_cleanup) { if (p_restart) { godot_java->restart(env); @@ -265,7 +271,18 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, } if (step.get() == STEP_SHOW_LOGO) { - Main::setup_boot_logo(); + bool xr_enabled; + if (XRServer::get_xr_mode() == XRServer::XRMODE_DEFAULT) { + xr_enabled = GLOBAL_GET("xr/shaders/enabled"); + } else { + xr_enabled = XRServer::get_xr_mode() == XRServer::XRMODE_ON; + } + // Unlike PCVR, there's no additional 2D screen onto which to render the boot logo, + // so we skip this step if xr is enabled. + if (!xr_enabled) { + Main::setup_boot_logo(); + } + step.increment(); return true; } @@ -550,10 +567,11 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIE } } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_updateInputDispatchSettings(JNIEnv *env, jclass clazz, jboolean p_use_accumulated_input, jboolean p_use_input_buffering) { - if (Input::get_singleton()) { - Input::get_singleton()->set_use_accumulated_input(p_use_accumulated_input); - Input::get_singleton()->set_use_input_buffering(p_use_input_buffering); +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz) { + Input *input = Input::get_singleton(); + if (input) { + return !input->is_agile_input_event_flushing(); } + return false; } } diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index 852c475e7e..d027da31fa 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -69,7 +69,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResu JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz); -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_updateInputDispatchSettings(JNIEnv *env, jclass clazz, jboolean p_use_accumulated_input, jboolean p_use_input_buffering); +JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz); } #endif // JAVA_GODOT_LIB_JNI_H diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp index 6e7f5ef5a1..91bf7b48a6 100644 --- a/platform/android/java_godot_wrapper.cpp +++ b/platform/android/java_godot_wrapper.cpp @@ -76,6 +76,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_ _get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;"); _on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V"); _on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V"); + _on_godot_terminating = p_env->GetMethodID(godot_class, "onGodotTerminating", "()V"); _create_new_godot_instance = p_env->GetMethodID(godot_class, "createNewGodotInstance", "([Ljava/lang/String;)I"); _get_render_view = p_env->GetMethodID(godot_class, "getRenderView", "()Lorg/godotengine/godot/GodotRenderView;"); _begin_benchmark_measure = p_env->GetMethodID(godot_class, "nativeBeginBenchmarkMeasure", "(Ljava/lang/String;Ljava/lang/String;)V"); @@ -136,6 +137,16 @@ void GodotJavaWrapper::on_godot_main_loop_started(JNIEnv *p_env) { } } +void GodotJavaWrapper::on_godot_terminating(JNIEnv *p_env) { + if (_on_godot_terminating) { + if (p_env == nullptr) { + p_env = get_jni_env(); + } + ERR_FAIL_NULL(p_env); + p_env->CallVoidMethod(godot_instance, _on_godot_terminating); + } +} + void GodotJavaWrapper::restart(JNIEnv *p_env) { if (_restart) { if (p_env == nullptr) { @@ -202,25 +213,27 @@ bool GodotJavaWrapper::has_get_clipboard() { } String GodotJavaWrapper::get_clipboard() { + String clipboard; if (_get_clipboard) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_clipboard); - return jstring_to_string(s, env); - } else { - return String(); + clipboard = jstring_to_string(s, env); + env->DeleteLocalRef(s); } + return clipboard; } String GodotJavaWrapper::get_input_fallback_mapping() { + String input_fallback_mapping; if (_get_input_fallback_mapping) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, String()); jstring fallback_mapping = (jstring)env->CallObjectMethod(godot_instance, _get_input_fallback_mapping); - return jstring_to_string(fallback_mapping, env); - } else { - return String(); + input_fallback_mapping = jstring_to_string(fallback_mapping, env); + env->DeleteLocalRef(fallback_mapping); } + return input_fallback_mapping; } bool GodotJavaWrapper::has_set_clipboard() { @@ -313,14 +326,15 @@ Vector<String> GodotJavaWrapper::get_gdextension_list_config_file() const { } String GodotJavaWrapper::get_ca_certificates() const { + String ca_certificates; if (_get_ca_certificates) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, String()); jstring s = (jstring)env->CallObjectMethod(godot_instance, _get_ca_certificates); - return jstring_to_string(s, env); - } else { - return String(); + ca_certificates = jstring_to_string(s, env); + env->DeleteLocalRef(s); } + return ca_certificates; } void GodotJavaWrapper::init_input_devices() { diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h index e86391d4e3..358cf3261d 100644 --- a/platform/android/java_godot_wrapper.h +++ b/platform/android/java_godot_wrapper.h @@ -68,6 +68,7 @@ private: jmethodID _get_input_fallback_mapping = nullptr; jmethodID _on_godot_setup_completed = nullptr; jmethodID _on_godot_main_loop_started = nullptr; + jmethodID _on_godot_terminating = nullptr; jmethodID _create_new_godot_instance = nullptr; jmethodID _get_render_view = nullptr; jmethodID _begin_benchmark_measure = nullptr; @@ -85,6 +86,7 @@ public: void on_godot_setup_completed(JNIEnv *p_env = nullptr); void on_godot_main_loop_started(JNIEnv *p_env = nullptr); + void on_godot_terminating(JNIEnv *p_env = nullptr); void restart(JNIEnv *p_env = nullptr); bool force_quit(JNIEnv *p_env = nullptr, int p_instance_id = 0); void set_keep_screen_on(bool p_enabled); diff --git a/platform/ios/detect.py b/platform/ios/detect.py index 53b367a0a7..ecd1f3d32d 100644 --- a/platform/ios/detect.py +++ b/platform/ios/detect.py @@ -51,6 +51,7 @@ def get_flags(): "arch": "arm64", "target": "template_debug", "use_volk": False, + "metal": True, "supported": ["mono"], "builtin_pcre2_with_jit": False, } @@ -154,8 +155,22 @@ def configure(env: "SConsEnvironment"): env.Prepend(CPPPATH=["#platform/ios"]) env.Append(CPPDEFINES=["IOS_ENABLED", "UNIX_ENABLED", "COREAUDIO_ENABLED"]) + if env["metal"] and env["arch"] != "arm64": + # Only supported on arm64, so skip it for x86_64 builds. + env["metal"] = False + + if env["metal"]: + env.AppendUnique(CPPDEFINES=["METAL_ENABLED", "RD_ENABLED"]) + env.Prepend( + CPPPATH=[ + "$IOS_SDK_PATH/System/Library/Frameworks/Metal.framework/Headers", + "$IOS_SDK_PATH/System/Library/Frameworks/QuartzCore.framework/Headers", + ] + ) + env.Prepend(CPPPATH=["#thirdparty/spirv-cross"]) + if env["vulkan"]: - env.Append(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"]) + env.AppendUnique(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"]) if env["opengl3"]: env.Append(CPPDEFINES=["GLES3_ENABLED", "GLES_SILENCE_DEPRECATION"]) diff --git a/platform/ios/display_server_ios.h b/platform/ios/display_server_ios.h index 4dded5aa29..bbb758074d 100644 --- a/platform/ios/display_server_ios.h +++ b/platform/ios/display_server_ios.h @@ -47,6 +47,10 @@ #include <vulkan/vulkan.h> #endif #endif // VULKAN_ENABLED + +#if defined(METAL_ENABLED) +#include "drivers/metal/rendering_context_driver_metal.h" +#endif // METAL_ENABLED #endif // RD_ENABLED #if defined(GLES3_ENABLED) diff --git a/platform/ios/display_server_ios.mm b/platform/ios/display_server_ios.mm index 802fbefc0d..5a027e0196 100644 --- a/platform/ios/display_server_ios.mm +++ b/platform/ios/display_server_ios.mm @@ -73,6 +73,13 @@ DisplayServerIOS::DisplayServerIOS(const String &p_rendering_driver, WindowMode #ifdef VULKAN_ENABLED RenderingContextDriverVulkanIOS::WindowPlatformData vulkan; #endif +#ifdef METAL_ENABLED +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" + // Eliminate "RenderingContextDriverMetal is only available on iOS 14.0 or newer". + RenderingContextDriverMetal::WindowPlatformData metal; +#pragma clang diagnostic pop +#endif } wpd; #if defined(VULKAN_ENABLED) @@ -85,7 +92,19 @@ DisplayServerIOS::DisplayServerIOS(const String &p_rendering_driver, WindowMode rendering_context = memnew(RenderingContextDriverVulkanIOS); } #endif - +#ifdef METAL_ENABLED + if (rendering_driver == "metal") { + if (@available(iOS 14.0, *)) { + layer = [AppDelegate.viewController.godotView initializeRenderingForDriver:@"metal"]; + wpd.metal.layer = (CAMetalLayer *)layer; + rendering_context = memnew(RenderingContextDriverMetal); + } else { + OS::get_singleton()->alert("Metal is only supported on iOS 14.0 and later."); + r_error = ERR_UNAVAILABLE; + return; + } + } +#endif if (rendering_context) { if (rendering_context->initialize() != OK) { ERR_PRINT(vformat("Failed to initialize %s context", rendering_driver)); @@ -172,6 +191,11 @@ Vector<String> DisplayServerIOS::get_rendering_drivers_func() { #if defined(VULKAN_ENABLED) drivers.push_back("vulkan"); #endif +#if defined(METAL_ENABLED) + if (@available(ios 14.0, *)) { + drivers.push_back("metal"); + } +#endif #if defined(GLES3_ENABLED) drivers.push_back("opengl3"); #endif diff --git a/platform/ios/doc_classes/EditorExportPlatformIOS.xml b/platform/ios/doc_classes/EditorExportPlatformIOS.xml index 87994ef22b..1d4a944dc4 100644 --- a/platform/ios/doc_classes/EditorExportPlatformIOS.xml +++ b/platform/ios/doc_classes/EditorExportPlatformIOS.xml @@ -151,16 +151,16 @@ Indicates whether your app uses advertising data for tracking. </member> <member name="privacy/collected_data/audio_data/collected" type="bool" setter="" getter=""> - Indicates whether your app collects audio data data. + Indicates whether your app collects audio data. </member> <member name="privacy/collected_data/audio_data/collection_purposes" type="int" setter="" getter=""> The reasons your app collects audio data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. </member> <member name="privacy/collected_data/audio_data/linked_to_user" type="bool" setter="" getter=""> - Indicates whether your app links audio data data to the user's identity. + Indicates whether your app links audio data to the user's identity. </member> <member name="privacy/collected_data/audio_data/used_for_tracking" type="bool" setter="" getter=""> - Indicates whether your app uses audio data data for tracking. + Indicates whether your app uses audio data for tracking. </member> <member name="privacy/collected_data/browsing_history/collected" type="bool" setter="" getter=""> Indicates whether your app collects browsing history. diff --git a/platform/ios/export/export_plugin.cpp b/platform/ios/export/export_plugin.cpp index 490f73a36d..e4b5392c4e 100644 --- a/platform/ios/export/export_plugin.cpp +++ b/platform/ios/export/export_plugin.cpp @@ -282,6 +282,7 @@ void EditorExportPlatformIOS::get_export_options(List<ExportOption> *r_options) r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/short_version", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/version", PROPERTY_HINT_PLACEHOLDER_TEXT, "Leave empty to use project version"), "")); + // TODO(sgc): set to iOS 14.0 for Metal r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/min_ios_version"), "12.0")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/additional_plist_content", PROPERTY_HINT_MULTILINE_TEXT), "")); @@ -1628,7 +1629,7 @@ Error EditorExportPlatformIOS::_copy_asset(const Ref<EditorExportPreset> &p_pres asset_path = asset_path.path_join(framework_name); destination_dir = p_out_dir.path_join(asset_path); - destination = destination_dir.path_join(file_name); + destination = destination_dir; // Convert to framework and copy. Error err = _convert_to_framework(p_asset, destination, p_preset->get("application/bundle_identifier")); @@ -2656,6 +2657,13 @@ bool EditorExportPlatformIOS::has_valid_export_configuration(const Ref<EditorExp } } + if (GLOBAL_GET("rendering/rendering_device/driver.ios") == "metal") { + float version = p_preset->get("application/min_ios_version").operator String().to_float(); + if (version < 14.0) { + err += TTR("Metal renderer require iOS 14+.") + "\n"; + } + } + if (!err.is_empty()) { r_error = err; } @@ -2971,6 +2979,7 @@ void EditorExportPlatformIOS::_update_preset_status() { } else { has_runnable_preset.clear(); } + devices_changed.set(); } #endif diff --git a/platform/ios/godot_app_delegate.h b/platform/ios/godot_app_delegate.h index a9bfcbb0b2..85dc6bb390 100644 --- a/platform/ios/godot_app_delegate.h +++ b/platform/ios/godot_app_delegate.h @@ -32,7 +32,7 @@ typedef NSObject<UIApplicationDelegate> ApplicationDelegateService; -@interface GodotApplicalitionDelegate : NSObject <UIApplicationDelegate> +@interface GodotApplicationDelegate : NSObject <UIApplicationDelegate> @property(class, readonly, strong) NSArray<ApplicationDelegateService *> *services; diff --git a/platform/ios/godot_app_delegate.m b/platform/ios/godot_app_delegate.m index 74e8705bc3..53e53cd0c6 100644 --- a/platform/ios/godot_app_delegate.m +++ b/platform/ios/godot_app_delegate.m @@ -32,11 +32,11 @@ #import "app_delegate.h" -@interface GodotApplicalitionDelegate () +@interface GodotApplicationDelegate () @end -@implementation GodotApplicalitionDelegate +@implementation GodotApplicationDelegate static NSMutableArray<ApplicationDelegateService *> *services = nil; diff --git a/platform/ios/godot_view.mm b/platform/ios/godot_view.mm index 1dddc9306e..552c4c262c 100644 --- a/platform/ios/godot_view.mm +++ b/platform/ios/godot_view.mm @@ -71,7 +71,7 @@ static const float earth_gravity = 9.80665; CALayer<DisplayLayer> *layer; - if ([driverName isEqualToString:@"vulkan"]) { + if ([driverName isEqualToString:@"vulkan"] || [driverName isEqualToString:@"metal"]) { #if defined(TARGET_OS_SIMULATOR) && TARGET_OS_SIMULATOR if (@available(iOS 13, *)) { layer = [GodotMetalLayer layer]; @@ -441,6 +441,9 @@ static const float earth_gravity = 9.80665; UIInterfaceOrientation interfaceOrientation = UIInterfaceOrientationUnknown; +#if __IPHONE_OS_VERSION_MAX_ALLOWED < 140000 + interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation]; +#else if (@available(iOS 13, *)) { interfaceOrientation = [UIApplication sharedApplication].delegate.window.windowScene.interfaceOrientation; #if !defined(TARGET_OS_SIMULATOR) || !TARGET_OS_SIMULATOR @@ -448,6 +451,7 @@ static const float earth_gravity = 9.80665; interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation]; #endif } +#endif switch (interfaceOrientation) { case UIInterfaceOrientationLandscapeLeft: { diff --git a/platform/ios/keyboard_input_view.mm b/platform/ios/keyboard_input_view.mm index 8b614662b7..4067701a41 100644 --- a/platform/ios/keyboard_input_view.mm +++ b/platform/ios/keyboard_input_view.mm @@ -149,23 +149,18 @@ return; } + NSString *substringToDelete = nil; if (self.previousSelectedRange.length == 0) { - // We are deleting all text before cursor if no range was selected. - // This way any inserted or changed text will be updated. - NSString *substringToDelete = [self.previousText substringToIndex:self.previousSelectedRange.location]; - [self deleteText:substringToDelete.length]; + // Get previous text to delete. + substringToDelete = [self.previousText substringToIndex:self.previousSelectedRange.location]; } else { - // If text was previously selected - // we are sending only one `backspace`. - // It will remove all text from text input. + // If text was previously selected we are sending only one `backspace`. It will remove all text from text input. [self deleteText:1]; } - NSString *substringToEnter; - + NSString *substringToEnter = nil; if (self.selectedRange.length == 0) { - // If previous cursor had a selection - // we have to calculate an inserted text. + // If previous cursor had a selection we have to calculate an inserted text. if (self.previousSelectedRange.length != 0) { NSInteger rangeEnd = self.selectedRange.location + self.selectedRange.length; NSInteger rangeStart = MIN(self.previousSelectedRange.location, self.selectedRange.location); @@ -187,7 +182,18 @@ substringToEnter = [self.text substringWithRange:self.selectedRange]; } - [self enterText:substringToEnter]; + NSInteger skip = 0; + if (substringToDelete != nil) { + for (NSInteger i = 0; i < MIN([substringToDelete length], [substringToEnter length]); i++) { + if ([substringToDelete characterAtIndex:i] == [substringToEnter characterAtIndex:i]) { + skip++; + } else { + break; + } + } + [self deleteText:[substringToDelete length] - skip]; // Delete changed part of previous text. + } + [self enterText:[substringToEnter substringFromIndex:skip]]; // Enter changed part of new text. self.previousText = self.text; self.previousSelectedRange = self.selectedRange; diff --git a/platform/ios/main.m b/platform/ios/main.m index 33b1034d98..89a00c9ae9 100644 --- a/platform/ios/main.m +++ b/platform/ios/main.m @@ -46,7 +46,7 @@ int main(int argc, char *argv[]) { gargv = argv; @autoreleasepool { - NSString *className = NSStringFromClass([GodotApplicalitionDelegate class]); + NSString *className = NSStringFromClass([GodotApplicationDelegate class]); UIApplicationMain(argc, argv, nil, className); } return 0; diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index 303a88ab26..d1de760f34 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -179,6 +179,8 @@ def configure(env: "SConsEnvironment"): env.Append(CCFLAGS=["-fsanitize-recover=memory"]) env.Append(LINKFLAGS=["-fsanitize=memory"]) + env.Append(CCFLAGS=["-ffp-contract=off"]) + # LTO if env["lto"] == "auto": # Full LTO for production. diff --git a/platform/linuxbsd/export/export_plugin.cpp b/platform/linuxbsd/export/export_plugin.cpp index 936adddda3..0032b898d2 100644 --- a/platform/linuxbsd/export/export_plugin.cpp +++ b/platform/linuxbsd/export/export_plugin.cpp @@ -61,6 +61,20 @@ Error EditorExportPlatformLinuxBSD::_export_debug_script(const Ref<EditorExportP } Error EditorExportPlatformLinuxBSD::export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) { + String custom_debug = p_preset->get("custom_template/debug"); + String custom_release = p_preset->get("custom_template/release"); + String arch = p_preset->get("binary_format/architecture"); + + String template_path = p_debug ? custom_debug : custom_release; + template_path = template_path.strip_edges(); + if (!template_path.is_empty()) { + String exe_arch = _get_exe_arch(template_path); + if (arch != exe_arch) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Mismatching custom export template executable architecture, found \"%s\", expected \"%s\"."), exe_arch, arch)); + return ERR_CANT_CREATE; + } + } + bool export_as_zip = p_path.ends_with("zip"); String pkg_name; @@ -205,8 +219,76 @@ bool EditorExportPlatformLinuxBSD::is_executable(const String &p_path) const { return is_elf(p_path) || is_shebang(p_path); } +bool EditorExportPlatformLinuxBSD::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const { + String err; + bool valid = EditorExportPlatformPC::has_valid_export_configuration(p_preset, err, r_missing_templates, p_debug); + + String custom_debug = p_preset->get("custom_template/debug").operator String().strip_edges(); + String custom_release = p_preset->get("custom_template/release").operator String().strip_edges(); + String arch = p_preset->get("binary_format/architecture"); + + if (!custom_debug.is_empty() && FileAccess::exists(custom_debug)) { + String exe_arch = _get_exe_arch(custom_debug); + if (arch != exe_arch) { + err += vformat(TTR("Mismatching custom debug export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch) + "\n"; + } + } + if (!custom_release.is_empty() && FileAccess::exists(custom_release)) { + String exe_arch = _get_exe_arch(custom_release); + if (arch != exe_arch) { + err += vformat(TTR("Mismatching custom release export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch) + "\n"; + } + } + + if (!err.is_empty()) { + r_error = err; + } + + return valid; +} + +String EditorExportPlatformLinuxBSD::_get_exe_arch(const String &p_path) const { + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); + if (f.is_null()) { + return "invalid"; + } + + // Read and check ELF magic number. + { + uint32_t magic = f->get_32(); + if (magic != 0x464c457f) { // 0x7F + "ELF" + return "invalid"; + } + } + + // Process header. + int64_t header_pos = f->get_position(); + f->seek(header_pos + 14); + uint16_t machine = f->get_16(); + f->close(); + + switch (machine) { + case 0x0003: + return "x86_32"; + case 0x003e: + return "x86_64"; + case 0x0014: + return "ppc32"; + case 0x0015: + return "ppc64"; + case 0x0028: + return "arm32"; + case 0x00b7: + return "arm64"; + case 0x00f3: + return "rv64"; + default: + return "unknown"; + } +} + Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) { - // Patch the header of the "pck" section in the ELF file so that it corresponds to the embedded data + // Patch the header of the "pck" section in the ELF file so that it corresponds to the embedded data. Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ_WRITE); if (f.is_null()) { @@ -214,7 +296,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int return ERR_CANT_OPEN; } - // Read and check ELF magic number + // Read and check ELF magic number. { uint32_t magic = f->get_32(); if (magic != 0x464c457f) { // 0x7F + "ELF" @@ -223,7 +305,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int } } - // Read program architecture bits from class field + // Read program architecture bits from class field. int bits = f->get_8() * 32; @@ -231,7 +313,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int add_message(EXPORT_MESSAGE_ERROR, TTR("PCK Embedding"), TTR("32-bit executables cannot have embedded data >= 4 GiB.")); } - // Get info about the section header table + // Get info about the section header table. int64_t section_table_pos; int64_t section_header_size; @@ -249,13 +331,13 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int int num_sections = f->get_16(); int string_section_idx = f->get_16(); - // Load the strings table + // Load the strings table. uint8_t *strings; { - // Jump to the strings section header + // Jump to the strings section header. f->seek(section_table_pos + string_section_idx * section_header_size); - // Read strings data size and offset + // Read strings data size and offset. int64_t string_data_pos; int64_t string_data_size; if (bits == 32) { @@ -268,7 +350,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int string_data_size = f->get_64(); } - // Read strings data + // Read strings data. f->seek(string_data_pos); strings = (uint8_t *)memalloc(string_data_size); if (!strings) { @@ -277,7 +359,7 @@ Error EditorExportPlatformLinuxBSD::fixup_embedded_pck(const String &p_path, int f->get_buffer(strings, string_data_size); } - // Search for the "pck" section + // Search for the "pck" section. bool found = false; for (int i = 0; i < num_sections; ++i) { diff --git a/platform/linuxbsd/export/export_plugin.h b/platform/linuxbsd/export/export_plugin.h index 21bd81ed2f..bbc55b82ce 100644 --- a/platform/linuxbsd/export/export_plugin.h +++ b/platform/linuxbsd/export/export_plugin.h @@ -69,11 +69,13 @@ class EditorExportPlatformLinuxBSD : public EditorExportPlatformPC { bool is_shebang(const String &p_path) const; Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path); + String _get_exe_arch(const String &p_path) const; public: virtual void get_export_options(List<ExportOption> *r_options) const override; virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const override; virtual bool get_export_option_visibility(const EditorExportPreset *p_preset, const String &p_option) const override; + virtual bool has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug = false) const override; virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; virtual String get_template_file_name(const String &p_target, const String &p_arch) const override; virtual Error fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) override; diff --git a/platform/linuxbsd/joypad_linux.cpp b/platform/linuxbsd/joypad_linux.cpp index 3534c1afee..a67428b9a4 100644 --- a/platform/linuxbsd/joypad_linux.cpp +++ b/platform/linuxbsd/joypad_linux.cpp @@ -374,6 +374,12 @@ void JoypadLinux::open_joypad(const char *p_path) { name = namebuf; } + for (const String &word : name.to_lower().split(" ")) { + if (banned_words.has(word)) { + return; + } + } + if (ioctl(fd, EVIOCGID, &inpid) < 0) { close(fd); return; diff --git a/platform/linuxbsd/joypad_linux.h b/platform/linuxbsd/joypad_linux.h index 26a9908d4e..bf24d8e5a5 100644 --- a/platform/linuxbsd/joypad_linux.h +++ b/platform/linuxbsd/joypad_linux.h @@ -94,6 +94,21 @@ private: Vector<String> attached_devices; + // List of lowercase words that will prevent the controller from being recognized if its name matches. + // This is done to prevent trackpads, graphics tablets and motherboard LED controllers from being + // recognized as controllers (and taking up controller ID slots as a result). + // Only whole words are matched within the controller name string. The match is case-insensitive. + const Vector<String> banned_words = { + "touchpad", // Matches e.g. "SynPS/2 Synaptics TouchPad", "Sony Interactive Entertainment DualSense Wireless Controller Touchpad" + "trackpad", + "clickpad", + "keyboard", // Matches e.g. "PG-90215 Keyboard", "Usb Keyboard Usb Keyboard Consumer Control" + "mouse", // Matches e.g. "Mouse passthrough" + "pen", // Matches e.g. "Wacom One by Wacom S Pen" + "finger", // Matches e.g. "Wacom HID 495F Finger" + "led", // Matches e.g. "ASRock LED Controller" + }; + static void monitor_joypads_thread_func(void *p_user); void monitor_joypads_thread_run(); diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp index adc9beed66..93096fcdcc 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.cpp +++ b/platform/linuxbsd/wayland/display_server_wayland.cpp @@ -1238,7 +1238,7 @@ void DisplayServerWayland::process_events() { } else { try_suspend(); } - } else if (wayland_thread.get_reset_frame()) { + } else if (!wayland_thread.is_suspended() || wayland_thread.get_reset_frame()) { // At last, a sign of life! We're no longer suspended. suspended = false; } diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp index 1ef17657b6..ab13105d18 100644 --- a/platform/linuxbsd/wayland/wayland_thread.cpp +++ b/platform/linuxbsd/wayland/wayland_thread.cpp @@ -1263,23 +1263,25 @@ void WaylandThread::_wl_seat_on_capabilities(void *data, struct wl_seat *wl_seat // Pointer handling. if (capabilities & WL_SEAT_CAPABILITY_POINTER) { - ss->cursor_surface = wl_compositor_create_surface(ss->registry->wl_compositor); - wl_surface_commit(ss->cursor_surface); + if (!ss->wl_pointer) { + ss->cursor_surface = wl_compositor_create_surface(ss->registry->wl_compositor); + wl_surface_commit(ss->cursor_surface); - ss->wl_pointer = wl_seat_get_pointer(wl_seat); - wl_pointer_add_listener(ss->wl_pointer, &wl_pointer_listener, ss); + ss->wl_pointer = wl_seat_get_pointer(wl_seat); + wl_pointer_add_listener(ss->wl_pointer, &wl_pointer_listener, ss); - if (ss->registry->wp_relative_pointer_manager) { - ss->wp_relative_pointer = zwp_relative_pointer_manager_v1_get_relative_pointer(ss->registry->wp_relative_pointer_manager, ss->wl_pointer); - zwp_relative_pointer_v1_add_listener(ss->wp_relative_pointer, &wp_relative_pointer_listener, ss); - } + if (ss->registry->wp_relative_pointer_manager) { + ss->wp_relative_pointer = zwp_relative_pointer_manager_v1_get_relative_pointer(ss->registry->wp_relative_pointer_manager, ss->wl_pointer); + zwp_relative_pointer_v1_add_listener(ss->wp_relative_pointer, &wp_relative_pointer_listener, ss); + } - if (ss->registry->wp_pointer_gestures) { - ss->wp_pointer_gesture_pinch = zwp_pointer_gestures_v1_get_pinch_gesture(ss->registry->wp_pointer_gestures, ss->wl_pointer); - zwp_pointer_gesture_pinch_v1_add_listener(ss->wp_pointer_gesture_pinch, &wp_pointer_gesture_pinch_listener, ss); - } + if (ss->registry->wp_pointer_gestures) { + ss->wp_pointer_gesture_pinch = zwp_pointer_gestures_v1_get_pinch_gesture(ss->registry->wp_pointer_gestures, ss->wl_pointer); + zwp_pointer_gesture_pinch_v1_add_listener(ss->wp_pointer_gesture_pinch, &wp_pointer_gesture_pinch_listener, ss); + } - // TODO: Constrain new pointers if the global mouse mode is constrained. + // TODO: Constrain new pointers if the global mouse mode is constrained. + } } else { if (ss->cursor_frame_callback) { // Just in case. I got bitten by weird race-like conditions already. @@ -1317,11 +1319,13 @@ void WaylandThread::_wl_seat_on_capabilities(void *data, struct wl_seat *wl_seat // Keyboard handling. if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { - ss->xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); - ERR_FAIL_NULL(ss->xkb_context); + if (!ss->wl_keyboard) { + ss->xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + ERR_FAIL_NULL(ss->xkb_context); - ss->wl_keyboard = wl_seat_get_keyboard(wl_seat); - wl_keyboard_add_listener(ss->wl_keyboard, &wl_keyboard_listener, ss); + ss->wl_keyboard = wl_seat_get_keyboard(wl_seat); + wl_keyboard_add_listener(ss->wl_keyboard, &wl_keyboard_listener, ss); + } } else { if (ss->xkb_context) { xkb_context_unref(ss->xkb_context); @@ -2047,11 +2051,21 @@ void WaylandThread::_wp_relative_pointer_on_relative_motion(void *data, struct z SeatState *ss = (SeatState *)data; ERR_FAIL_NULL(ss); + if (!ss->pointed_surface) { + // We're probably on a decoration or some other third-party thing. + return; + } + PointerData &pd = ss->pointer_data_buffer; + WindowState *ws = wl_surface_get_window_state(ss->pointed_surface); + ERR_FAIL_NULL(ws); + pd.relative_motion.x = wl_fixed_to_double(dx); pd.relative_motion.y = wl_fixed_to_double(dy); + pd.relative_motion *= window_state_get_scale_factor(ws); + pd.relative_motion_time = uptime_lo; } @@ -2244,13 +2258,11 @@ void WaylandThread::_wp_tablet_tool_on_done(void *data, struct zwp_tablet_tool_v void WaylandThread::_wp_tablet_tool_on_removed(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) { TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); - if (!ts) { return; } SeatState *ss = wl_seat_get_seat_state(ts->wl_seat); - if (!ss) { return; } @@ -2270,14 +2282,17 @@ void WaylandThread::_wp_tablet_tool_on_removed(void *data, struct zwp_tablet_too } void WaylandThread::_wp_tablet_tool_on_proximity_in(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial, struct zwp_tablet_v2 *tablet, struct wl_surface *surface) { - TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); + if (!surface || !wl_proxy_is_godot((struct wl_proxy *)surface)) { + // We're probably on a decoration or something. + return; + } + TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); if (!ts) { return; } SeatState *ss = wl_seat_get_seat_state(ts->wl_seat); - if (!ss) { return; } @@ -2299,13 +2314,12 @@ void WaylandThread::_wp_tablet_tool_on_proximity_in(void *data, struct zwp_table void WaylandThread::_wp_tablet_tool_on_proximity_out(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) { TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); - - if (!ts) { + if (!ts || !ts->data_pending.proximal_surface) { + // Not our stuff, we don't care. return; } SeatState *ss = wl_seat_get_seat_state(ts->wl_seat); - if (!ss) { return; } @@ -2326,7 +2340,6 @@ void WaylandThread::_wp_tablet_tool_on_proximity_out(void *data, struct zwp_tabl void WaylandThread::_wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial) { TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); - if (!ts) { return; } @@ -2344,7 +2357,6 @@ void WaylandThread::_wp_tablet_tool_on_down(void *data, struct zwp_tablet_tool_v void WaylandThread::_wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2) { TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); - if (!ts) { return; } @@ -2360,11 +2372,15 @@ void WaylandThread::_wp_tablet_tool_on_up(void *data, struct zwp_tablet_tool_v2 void WaylandThread::_wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t x, wl_fixed_t y) { TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); - if (!ts) { return; } + if (!ts->data_pending.proximal_surface) { + // We're probably on a decoration or some other third-party thing. + return; + } + WindowState *ws = wl_surface_get_window_state(ts->data_pending.proximal_surface); ERR_FAIL_NULL(ws); @@ -2381,7 +2397,6 @@ void WaylandThread::_wp_tablet_tool_on_motion(void *data, struct zwp_tablet_tool void WaylandThread::_wp_tablet_tool_on_pressure(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t pressure) { TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); - if (!ts) { return; } @@ -2395,7 +2410,6 @@ void WaylandThread::_wp_tablet_tool_on_distance(void *data, struct zwp_tablet_to void WaylandThread::_wp_tablet_tool_on_tilt(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, wl_fixed_t tilt_x, wl_fixed_t tilt_y) { TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); - if (!ts) { return; } @@ -2420,7 +2434,6 @@ void WaylandThread::_wp_tablet_tool_on_wheel(void *data, struct zwp_tablet_tool_ void WaylandThread::_wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t serial, uint32_t button, uint32_t state) { TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); - if (!ts) { return; } @@ -2456,13 +2469,11 @@ void WaylandThread::_wp_tablet_tool_on_button(void *data, struct zwp_tablet_tool void WaylandThread::_wp_tablet_tool_on_frame(void *data, struct zwp_tablet_tool_v2 *wp_tablet_tool_v2, uint32_t time) { TabletToolState *ts = wp_tablet_tool_get_state(wp_tablet_tool_v2); - if (!ts) { return; } SeatState *ss = wl_seat_get_seat_state(ts->wl_seat); - if (!ss) { return; } @@ -3240,6 +3251,8 @@ void WaylandThread::window_create(DisplayServer::WindowID p_window_id, int p_wid zxdg_exported_v1_add_listener(ws.xdg_exported, &xdg_exported_listener, &ws); } + wl_surface_commit(ws.wl_surface); + // Wait for the surface to be configured before continuing. wl_display_roundtrip(wl_display); } diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index edf3a40ccb..8a2f83be2d 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -1519,7 +1519,7 @@ Color DisplayServerX11::screen_get_pixel(const Point2i &p_position) const { if (image) { XColor c; c.pixel = XGetPixel(image, 0, 0); - XFree(image); + XDestroyImage(image); XQueryColor(x11_display, XDefaultColormap(x11_display, i), &c); color = Color(float(c.red) / 65535.0, float(c.green) / 65535.0, float(c.blue) / 65535.0, 1.0); break; @@ -1637,11 +1637,12 @@ Ref<Image> DisplayServerX11::screen_get_image(int p_screen) const { } } } else { - XFree(image); - ERR_FAIL_V_MSG(Ref<Image>(), vformat("XImage with RGB mask %x %x %x and depth %d is not supported.", (uint64_t)image->red_mask, (uint64_t)image->green_mask, (uint64_t)image->blue_mask, (int64_t)image->bits_per_pixel)); + String msg = vformat("XImage with RGB mask %x %x %x and depth %d is not supported.", (uint64_t)image->red_mask, (uint64_t)image->green_mask, (uint64_t)image->blue_mask, (int64_t)image->bits_per_pixel); + XDestroyImage(image); + ERR_FAIL_V_MSG(Ref<Image>(), msg); } img = Image::create_from_data(width, height, false, Image::FORMAT_RGBA8, img_data); - XFree(image); + XDestroyImage(image); } return img; @@ -1734,7 +1735,7 @@ Vector<DisplayServer::WindowID> DisplayServerX11::get_window_list() const { return ret; } -DisplayServer::WindowID DisplayServerX11::create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect) { +DisplayServer::WindowID DisplayServerX11::create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect, bool p_exclusive, WindowID p_transient_parent) { _THREAD_SAFE_METHOD_ WindowID id = _create_window(p_mode, p_vsync_mode, p_flags, p_rect); @@ -1748,6 +1749,11 @@ DisplayServer::WindowID DisplayServerX11::create_sub_window(WindowMode p_mode, V rendering_device->screen_create(id); } #endif + + if (p_transient_parent != INVALID_WINDOW_ID) { + window_set_transient(id, p_transient_parent); + } + return id; } @@ -4964,6 +4970,23 @@ void DisplayServerX11::process_events() { pos = Point2i(windows[focused_window_id].size.width / 2, windows[focused_window_id].size.height / 2); } + BitField<MouseButtonMask> last_button_state = 0; + if (event.xmotion.state & Button1Mask) { + last_button_state.set_flag(MouseButtonMask::LEFT); + } + if (event.xmotion.state & Button2Mask) { + last_button_state.set_flag(MouseButtonMask::MIDDLE); + } + if (event.xmotion.state & Button3Mask) { + last_button_state.set_flag(MouseButtonMask::RIGHT); + } + if (event.xmotion.state & Button4Mask) { + last_button_state.set_flag(MouseButtonMask::MB_XBUTTON1); + } + if (event.xmotion.state & Button5Mask) { + last_button_state.set_flag(MouseButtonMask::MB_XBUTTON2); + } + Ref<InputEventMouseMotion> mm; mm.instantiate(); @@ -4971,13 +4994,13 @@ void DisplayServerX11::process_events() { if (xi.pressure_supported) { mm->set_pressure(xi.pressure); } else { - mm->set_pressure(bool(mouse_get_button_state().has_flag(MouseButtonMask::LEFT)) ? 1.0f : 0.0f); + mm->set_pressure(bool(last_button_state.has_flag(MouseButtonMask::LEFT)) ? 1.0f : 0.0f); } mm->set_tilt(xi.tilt); mm->set_pen_inverted(xi.pen_inverted); _get_key_modifier_state(event.xmotion.state, mm); - mm->set_button_mask(mouse_get_button_state()); + mm->set_button_mask(last_button_state); mm->set_position(pos); mm->set_global_position(pos); mm->set_velocity(Input::get_singleton()->get_last_mouse_velocity()); diff --git a/platform/linuxbsd/x11/display_server_x11.h b/platform/linuxbsd/x11/display_server_x11.h index 341ba5f079..0cbfbe51ef 100644 --- a/platform/linuxbsd/x11/display_server_x11.h +++ b/platform/linuxbsd/x11/display_server_x11.h @@ -438,7 +438,7 @@ public: virtual Vector<DisplayServer::WindowID> get_window_list() const override; - virtual WindowID create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i()) override; + virtual WindowID create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i(), bool p_exclusive = false, WindowID p_transient_parent = INVALID_WINDOW_ID) override; virtual void show_window(WindowID p_id) override; virtual void delete_sub_window(WindowID p_id) override; diff --git a/platform/macos/SCsub b/platform/macos/SCsub index c965e875c1..a10262c524 100644 --- a/platform/macos/SCsub +++ b/platform/macos/SCsub @@ -23,10 +23,10 @@ def generate_bundle(target, source, env): prefix += ".double" # Lipo editor executable. - target_bin = lipo(bin_dir + "/" + prefix, env.extra_suffix) + target_bin = lipo(bin_dir + "/" + prefix, env.extra_suffix + env.module_version_string) # Assemble .app bundle and update version info. - app_dir = Dir("#bin/" + (prefix + env.extra_suffix).replace(".", "_") + ".app").abspath + app_dir = Dir("#bin/" + (prefix + env.extra_suffix + env.module_version_string).replace(".", "_") + ".app").abspath templ = Dir("#misc/dist/macos_tools.app").abspath if os.path.exists(app_dir): shutil.rmtree(app_dir) @@ -35,6 +35,8 @@ def generate_bundle(target, source, env): os.mkdir(app_dir + "/Contents/MacOS") if target_bin != "": shutil.copy(target_bin, app_dir + "/Contents/MacOS/Godot") + if "mono" in env.module_version_string: + shutil.copytree(Dir("#bin/GodotSharp").abspath, app_dir + "/Contents/Resources/GodotSharp") version = get_build_version(False) short_version = get_build_version(True) with open(Dir("#misc/dist/macos").abspath + "/editor_info_plist.template", "rt", encoding="utf-8") as fin: @@ -76,8 +78,8 @@ def generate_bundle(target, source, env): dbg_prefix += ".double" # Lipo template executables. - rel_target_bin = lipo(bin_dir + "/" + rel_prefix, env.extra_suffix) - dbg_target_bin = lipo(bin_dir + "/" + dbg_prefix, env.extra_suffix) + rel_target_bin = lipo(bin_dir + "/" + rel_prefix, env.extra_suffix + env.module_version_string) + dbg_target_bin = lipo(bin_dir + "/" + dbg_prefix, env.extra_suffix + env.module_version_string) # Assemble .app bundle. app_dir = Dir("#bin/macos_template.app").abspath @@ -93,7 +95,7 @@ def generate_bundle(target, source, env): shutil.copy(dbg_target_bin, app_dir + "/Contents/MacOS/godot_macos_debug.universal") # ZIP .app bundle. - zip_dir = Dir("#bin/" + (app_prefix + env.extra_suffix).replace(".", "_")).abspath + zip_dir = Dir("#bin/" + (app_prefix + env.extra_suffix + env.module_version_string).replace(".", "_")).abspath shutil.make_archive(zip_dir, "zip", root_dir=bin_dir, base_dir="macos_template.app") shutil.rmtree(app_dir) diff --git a/platform/macos/detect.py b/platform/macos/detect.py index 70cb00c6ff..bfd18bea99 100644 --- a/platform/macos/detect.py +++ b/platform/macos/detect.py @@ -56,6 +56,7 @@ def get_flags(): return { "arch": detect_arch(), "use_volk": False, + "metal": True, "supported": ["mono"], } @@ -96,6 +97,8 @@ def configure(env: "SConsEnvironment"): env.Append(CCFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.13"]) env.Append(LINKFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.13"]) + env.Append(CCFLAGS=["-ffp-contract=off"]) + cc_version = get_compiler_version(env) cc_version_major = cc_version["apple_major"] cc_version_minor = cc_version["apple_minor"] @@ -237,9 +240,22 @@ def configure(env: "SConsEnvironment"): env.Append(LINKFLAGS=["-rpath", "@executable_path/../Frameworks", "-rpath", "@executable_path"]) + if env["metal"] and env["arch"] != "arm64": + # Only supported on arm64, so skip it for x86_64 builds. + env["metal"] = False + + extra_frameworks = set() + + if env["metal"]: + env.AppendUnique(CPPDEFINES=["METAL_ENABLED", "RD_ENABLED"]) + extra_frameworks.add("Metal") + extra_frameworks.add("MetalKit") + env.Prepend(CPPPATH=["#thirdparty/spirv-cross"]) + if env["vulkan"]: - env.Append(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"]) - env.Append(LINKFLAGS=["-framework", "Metal", "-framework", "IOSurface"]) + env.AppendUnique(CPPDEFINES=["VULKAN_ENABLED", "RD_ENABLED"]) + extra_frameworks.add("Metal") + extra_frameworks.add("IOSurface") if not env["use_volk"]: env.Append(LINKFLAGS=["-lMoltenVK"]) @@ -258,3 +274,7 @@ def configure(env: "SConsEnvironment"): "MoltenVK SDK installation directory not found, use 'vulkan_sdk_path' SCons parameter to specify SDK path." ) sys.exit(255) + + if len(extra_frameworks) > 0: + frameworks = [item for key in extra_frameworks for item in ["-framework", key]] + env.Append(LINKFLAGS=frameworks) diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h index b4741dc08f..97af6d0a5a 100644 --- a/platform/macos/display_server_macos.h +++ b/platform/macos/display_server_macos.h @@ -47,6 +47,9 @@ #if defined(VULKAN_ENABLED) #include "rendering_context_driver_vulkan_macos.h" #endif // VULKAN_ENABLED +#if defined(METAL_ENABLED) +#include "drivers/metal/rendering_context_driver_metal.h" +#endif #endif // RD_ENABLED #define BitMap _QDBitMap // Suppress deprecated QuickDraw definition. @@ -326,7 +329,7 @@ public: virtual Vector<int> get_window_list() const override; - virtual WindowID create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i()) override; + virtual WindowID create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i(), bool p_exclusive = false, WindowID p_transient_parent = INVALID_WINDOW_ID) override; virtual void show_window(WindowID p_id) override; virtual void delete_sub_window(WindowID p_id) override; diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index a1a91345ac..3e0a5efe52 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -139,12 +139,20 @@ DisplayServerMacOS::WindowID DisplayServerMacOS::_create_window(WindowMode p_mod #ifdef VULKAN_ENABLED RenderingContextDriverVulkanMacOS::WindowPlatformData vulkan; #endif +#ifdef METAL_ENABLED + RenderingContextDriverMetal::WindowPlatformData metal; +#endif } wpd; #ifdef VULKAN_ENABLED if (rendering_driver == "vulkan") { wpd.vulkan.layer_ptr = (CAMetalLayer *const *)&layer; } #endif +#ifdef METAL_ENABLED + if (rendering_driver == "metal") { + wpd.metal.layer = (CAMetalLayer *)layer; + } +#endif Error err = rendering_context->window_create(window_id_counter, &wpd); ERR_FAIL_COND_V_MSG(err != OK, INVALID_WINDOW_ID, vformat("Can't create a %s context", rendering_driver)); @@ -568,23 +576,7 @@ void DisplayServerMacOS::menu_callback(id p_sender) { } GodotMenuItem *value = [p_sender representedObject]; - if (value) { - if (value->max_states > 0) { - value->state++; - if (value->state >= value->max_states) { - value->state = 0; - } - } - - if (value->checkable_type == CHECKABLE_TYPE_CHECK_BOX) { - if ([p_sender state] == NSControlStateValueOff) { - [p_sender setState:NSControlStateValueOn]; - } else { - [p_sender setState:NSControlStateValueOff]; - } - } - if (value->callback.is_valid()) { MenuCall mc; mc.tag = value->meta; @@ -1730,7 +1722,7 @@ Vector<DisplayServer::WindowID> DisplayServerMacOS::get_window_list() const { return ret; } -DisplayServer::WindowID DisplayServerMacOS::create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect) { +DisplayServer::WindowID DisplayServerMacOS::create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect, bool p_exclusive, WindowID p_transient_parent) { _THREAD_SAFE_METHOD_ WindowID id = _create_window(p_mode, p_vsync_mode, p_rect); @@ -1744,6 +1736,12 @@ DisplayServer::WindowID DisplayServerMacOS::create_sub_window(WindowMode p_mode, rendering_device->screen_create(id); } #endif + + window_set_exclusive(id, p_exclusive); + if (p_transient_parent != INVALID_WINDOW_ID) { + window_set_transient(id, p_transient_parent); + } + return id; } @@ -2342,7 +2340,7 @@ void DisplayServerMacOS::window_set_window_buttons_offset(const Vector2i &p_offs wd.wb_offset = p_offset / scale; wd.wb_offset = wd.wb_offset.maxi(12); if (wd.window_button_view) { - [wd.window_button_view setOffset:NSMakePoint(wd.wb_offset.x, wd.wb_offset.y)]; + [(GodotButtonView *)wd.window_button_view setOffset:NSMakePoint(wd.wb_offset.x, wd.wb_offset.y)]; } } @@ -2710,7 +2708,7 @@ void DisplayServerMacOS::window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_ gl_manager_legacy->set_use_vsync(p_vsync_mode != DisplayServer::VSYNC_DISABLED); } #endif -#if defined(VULKAN_ENABLED) +#if defined(RD_ENABLED) if (rendering_context) { rendering_context->window_set_vsync_mode(p_window, p_vsync_mode); } @@ -2727,7 +2725,7 @@ DisplayServer::VSyncMode DisplayServerMacOS::window_get_vsync_mode(WindowID p_wi return (gl_manager_legacy->is_using_vsync() ? DisplayServer::VSyncMode::VSYNC_ENABLED : DisplayServer::VSyncMode::VSYNC_DISABLED); } #endif -#if defined(VULKAN_ENABLED) +#if defined(RD_ENABLED) if (rendering_context) { return rendering_context->window_get_vsync_mode(p_window); } @@ -3311,6 +3309,9 @@ Vector<String> DisplayServerMacOS::get_rendering_drivers_func() { #if defined(VULKAN_ENABLED) drivers.push_back("vulkan"); #endif +#if defined(METAL_ENABLED) + drivers.push_back("metal"); +#endif #if defined(GLES3_ENABLED) drivers.push_back("opengl3"); drivers.push_back("opengl3_angle"); @@ -3633,6 +3634,11 @@ DisplayServerMacOS::DisplayServerMacOS(const String &p_rendering_driver, WindowM rendering_context = memnew(RenderingContextDriverVulkanMacOS); } #endif +#if defined(METAL_ENABLED) + if (rendering_driver == "metal") { + rendering_context = memnew(RenderingContextDriverMetal); + } +#endif if (rendering_context) { if (rendering_context->initialize() != OK) { diff --git a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml index 92ade4b77a..34ad52bbf6 100644 --- a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml +++ b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml @@ -224,16 +224,16 @@ Indicates whether your app uses advertising data for tracking. </member> <member name="privacy/collected_data/audio_data/collected" type="bool" setter="" getter=""> - Indicates whether your app collects audio data data. + Indicates whether your app collects audio data. </member> <member name="privacy/collected_data/audio_data/collection_purposes" type="int" setter="" getter=""> The reasons your app collects audio data. See [url=https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests]Describing data use in privacy manifests[/url]. </member> <member name="privacy/collected_data/audio_data/linked_to_user" type="bool" setter="" getter=""> - Indicates whether your app links audio data data to the user's identity. + Indicates whether your app links audio data to the user's identity. </member> <member name="privacy/collected_data/audio_data/used_for_tracking" type="bool" setter="" getter=""> - Indicates whether your app uses audio data data for tracking. + Indicates whether your app uses audio data for tracking. </member> <member name="privacy/collected_data/browsing_history/collected" type="bool" setter="" getter=""> Indicates whether your app collects browsing history. diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp index 73e2f2d45b..290b0082fc 100644 --- a/platform/macos/export/export_plugin.cpp +++ b/platform/macos/export/export_plugin.cpp @@ -141,7 +141,7 @@ String EditorExportPlatformMacOS::get_export_option_warning(const EditorExportPr if (p_name == "codesign/codesign") { if (dist_type == 2) { - if (codesign_tool == 2 && Engine::get_singleton()->has_singleton("GodotSharp")) { + if (codesign_tool == 2 && ClassDB::class_exists("CSharpScript")) { return TTR("'rcodesign' doesn't support signing applications with embedded dynamic libraries (GDExtension or .NET)."); } if (codesign_tool == 0) { @@ -333,7 +333,7 @@ bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportP } // These entitlements are required to run managed code, and are always enabled in Mono builds. - if (Engine::get_singleton()->has_singleton("GodotSharp")) { + if (ClassDB::class_exists("CSharpScript")) { if (p_option == "codesign/entitlements/allow_jit_code_execution" || p_option == "codesign/entitlements/allow_unsigned_executable_memory" || p_option == "codesign/entitlements/allow_dyld_environment_variables") { return false; } @@ -458,6 +458,7 @@ void EditorExportPlatformMacOS::get_export_options(List<ExportOption> *r_options r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/additional_plist_content", PROPERTY_HINT_MULTILINE_TEXT), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/platform_build"), "14C18")); + // TODO(sgc): Need to set appropriate version when using Metal r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_version"), "13.1")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_build"), "22C55")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "xcode/sdk_name"), "macosx13.1")); @@ -1064,7 +1065,7 @@ Error EditorExportPlatformMacOS::_notarize(const Ref<EditorExportPreset> &p_pres return OK; } -Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn, bool p_set_id) { +void EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn, bool p_set_id) { int codesign_tool = p_preset->get("codesign/codesign"); switch (codesign_tool) { case 1: { // built-in ad-hoc @@ -1074,7 +1075,7 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre Error err = CodeSign::codesign(false, true, p_path, p_ent_path, error_msg); if (err != OK) { add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Built-in CodeSign failed with error \"%s\"."), error_msg)); - return Error::FAILED; + return; } #else add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Built-in CodeSign require regex module.")); @@ -1086,13 +1087,13 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre String rcodesign = EDITOR_GET("export/macos/rcodesign").operator String(); if (rcodesign.is_empty()) { add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xrcodesign path is not set. Configure rcodesign path in the Editor Settings (Export > macOS > rcodesign).")); - return Error::FAILED; + return; } List<String> args; args.push_back("sign"); - if (p_path.get_extension() != "dmg") { + if (!p_ent_path.is_empty()) { args.push_back("--entitlements-xml-path"); args.push_back(p_ent_path); } @@ -1124,13 +1125,13 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre Error err = OS::get_singleton()->execute(rcodesign, args, &str, &exitcode, true); if (err != OK) { add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start rcodesign executable.")); - return err; + return; } if (exitcode != 0) { print_line("rcodesign (" + p_path + "):\n" + str); add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details.")); - return Error::FAILED; + return; } else { print_verbose("rcodesign (" + p_path + "):\n" + str); } @@ -1141,7 +1142,7 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre if (!FileAccess::exists("/usr/bin/codesign") && !FileAccess::exists("/bin/codesign")) { add_message(EXPORT_MESSAGE_ERROR, TTR("Code Signing"), TTR("Xcode command line tools are not installed.")); - return Error::FAILED; + return; } bool ad_hoc = (p_preset->get("codesign/identity") == "" || p_preset->get("codesign/identity") == "-"); @@ -1153,7 +1154,7 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre args.push_back("runtime"); } - if (p_path.get_extension() != "dmg") { + if (!p_ent_path.is_empty()) { args.push_back("--entitlements"); args.push_back(p_ent_path); } @@ -1190,13 +1191,13 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre Error err = OS::get_singleton()->execute("codesign", args, &str, &exitcode, true); if (err != OK) { add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Could not start codesign executable, make sure Xcode command line tools are installed.")); - return err; + return; } if (exitcode != 0) { print_line("codesign (" + p_path + "):\n" + str); add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), TTR("Code signing failed, see editor log for details.")); - return Error::FAILED; + return; } else { print_verbose("codesign (" + p_path + "):\n" + str); } @@ -1205,14 +1206,13 @@ Error EditorExportPlatformMacOS::_code_sign(const Ref<EditorExportPreset> &p_pre default: { }; } - - return OK; } -Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, +void EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code) { static Vector<String> extensions_to_sign; + bool sandbox = p_preset->get("codesign/entitlements/app_sandbox/enabled"); if (extensions_to_sign.is_empty()) { extensions_to_sign.push_back("dylib"); extensions_to_sign.push_back("framework"); @@ -1223,7 +1223,8 @@ Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPres Ref<DirAccess> dir_access{ DirAccess::open(p_path, &dir_access_error) }; if (dir_access_error != OK) { - return dir_access_error; + add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Cannot sign directory %s."), p_path)); + return; } dir_access->list_dir_begin(); @@ -1237,44 +1238,35 @@ Error EditorExportPlatformMacOS::_code_sign_directory(const Ref<EditorExportPres } if (extensions_to_sign.has(current_file.get_extension())) { - String ent_path = p_ent_path; + String ent_path; bool set_bundle_id = false; - if (FileAccess::exists(current_file_path)) { + if (sandbox && FileAccess::exists(current_file_path)) { int ftype = MachO::get_filetype(current_file_path); if (ftype == 2 || ftype == 5) { ent_path = p_helper_ent_path; set_bundle_id = true; } } - Error code_sign_error{ _code_sign(p_preset, current_file_path, ent_path, false, set_bundle_id) }; - if (code_sign_error != OK) { - return code_sign_error; - } + _code_sign(p_preset, current_file_path, ent_path, false, set_bundle_id); if (is_executable(current_file_path)) { // chmod with 0755 if the file is executable. FileAccess::set_unix_permissions(current_file_path, 0755); } } else if (dir_access->current_is_dir()) { - Error code_sign_error{ _code_sign_directory(p_preset, current_file_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code) }; - if (code_sign_error != OK) { - return code_sign_error; - } + _code_sign_directory(p_preset, current_file_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code); } else if (p_should_error_on_non_code) { add_message(EXPORT_MESSAGE_WARNING, TTR("Code Signing"), vformat(TTR("Cannot sign file %s."), current_file)); - return Error::FAILED; } current_file = dir_access->get_next(); } - - return OK; } Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path, const String &p_in_app_path, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, const String &p_helper_ent_path, - bool p_should_error_on_non_code_sign) { + bool p_should_error_on_non_code_sign, bool p_sandbox) { static Vector<String> extensions_to_sign; if (extensions_to_sign.is_empty()) { @@ -1363,19 +1355,19 @@ Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access if (err == OK && p_sign_enabled) { if (dir_access->dir_exists(p_src_path) && p_src_path.get_extension().is_empty()) { // If it is a directory, find and sign all dynamic libraries. - err = _code_sign_directory(p_preset, p_in_app_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code_sign); + _code_sign_directory(p_preset, p_in_app_path, p_ent_path, p_helper_ent_path, p_should_error_on_non_code_sign); } else { if (extensions_to_sign.has(p_in_app_path.get_extension())) { - String ent_path = p_ent_path; + String ent_path; bool set_bundle_id = false; - if (FileAccess::exists(p_in_app_path)) { + if (p_sandbox && FileAccess::exists(p_in_app_path)) { int ftype = MachO::get_filetype(p_in_app_path); if (ftype == 2 || ftype == 5) { ent_path = p_helper_ent_path; set_bundle_id = true; } } - err = _code_sign(p_preset, p_in_app_path, ent_path, false, set_bundle_id); + _code_sign(p_preset, p_in_app_path, ent_path, false, set_bundle_id); } if (dir_access->file_exists(p_in_app_path) && is_executable(p_in_app_path)) { // chmod with 0755 if the file is executable. @@ -1389,13 +1381,13 @@ Error EditorExportPlatformMacOS::_copy_and_sign_files(Ref<DirAccess> &dir_access Error EditorExportPlatformMacOS::_export_macos_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin, const String &p_app_path_name, Ref<DirAccess> &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, - const String &p_ent_path, const String &p_helper_ent_path) { + const String &p_ent_path, const String &p_helper_ent_path, bool p_sandbox) { Error error{ OK }; const Vector<String> &macos_plugins{ p_editor_export_plugin->get_macos_plugin_files() }; for (int i = 0; i < macos_plugins.size(); ++i) { String src_path{ ProjectSettings::get_singleton()->globalize_path(macos_plugins[i]) }; String path_in_app{ p_app_path_name + "/Contents/PlugIns/" + src_path.get_file() }; - error = _copy_and_sign_files(dir_access, src_path, path_in_app, p_sign_enabled, p_preset, p_ent_path, p_helper_ent_path, false); + error = _copy_and_sign_files(dir_access, src_path, path_in_app, p_sign_enabled, p_preset, p_ent_path, p_helper_ent_path, false, p_sandbox); if (error != OK) { break; } @@ -1988,7 +1980,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p ent_f->store_line("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"); ent_f->store_line("<plist version=\"1.0\">"); ent_f->store_line("<dict>"); - if (Engine::get_singleton()->has_singleton("GodotSharp")) { + if (ClassDB::class_exists("CSharpScript")) { // These entitlements are required to run managed code, and are always enabled in Mono builds. ent_f->store_line("<key>com.apple.security.cs.allow-jit</key>"); ent_f->store_line("<true/>"); @@ -2156,7 +2148,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p String hlp_path = helpers[i]; err = da->copy(hlp_path, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file()); if (err == OK && sign_enabled) { - err = _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path, false, true); + _code_sign(p_preset, tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), hlp_ent_path, false, true); } FileAccess::set_unix_permissions(tmp_app_path_name + "/Contents/Helpers/" + hlp_path.get_file(), 0755); } @@ -2168,11 +2160,11 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p String src_path = ProjectSettings::get_singleton()->globalize_path(shared_objects[i].path); if (shared_objects[i].target.is_empty()) { String path_in_app = tmp_app_path_name + "/Contents/Frameworks/" + src_path.get_file(); - err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, hlp_ent_path, true); + err = _copy_and_sign_files(da, src_path, path_in_app, sign_enabled, p_preset, ent_path, hlp_ent_path, true, sandbox); } else { String path_in_app = tmp_app_path_name.path_join(shared_objects[i].target); tmp_app_dir->make_dir_recursive(path_in_app); - err = _copy_and_sign_files(da, src_path, path_in_app.path_join(src_path.get_file()), sign_enabled, p_preset, ent_path, hlp_ent_path, false); + err = _copy_and_sign_files(da, src_path, path_in_app.path_join(src_path.get_file()), sign_enabled, p_preset, ent_path, hlp_ent_path, false, sandbox); } if (err != OK) { break; @@ -2181,7 +2173,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p Vector<Ref<EditorExportPlugin>> export_plugins{ EditorExport::get_singleton()->get_export_plugins() }; for (int i = 0; i < export_plugins.size(); ++i) { - err = _export_macos_plugins_for(export_plugins[i], tmp_app_path_name, da, sign_enabled, p_preset, ent_path, hlp_ent_path); + err = _export_macos_plugins_for(export_plugins[i], tmp_app_path_name, da, sign_enabled, p_preset, ent_path, hlp_ent_path, sandbox); if (err != OK) { break; } @@ -2201,7 +2193,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p if (ep.step(TTR("Code signing bundle"), 2)) { return ERR_SKIP; } - err = _code_sign(p_preset, tmp_app_path_name, ent_path, true, false); + _code_sign(p_preset, tmp_app_path_name, ent_path, true, false); } String noto_path = p_path; @@ -2219,7 +2211,7 @@ Error EditorExportPlatformMacOS::export_project(const Ref<EditorExportPreset> &p if (ep.step(TTR("Code signing DMG"), 3)) { return ERR_SKIP; } - err = _code_sign(p_preset, p_path, ent_path, false, false); + _code_sign(p_preset, p_path, ent_path, false, false); } } else if (export_format == "pkg") { // Create a Installer. diff --git a/platform/macos/export/export_plugin.h b/platform/macos/export/export_plugin.h index 6134d756b9..062a2e5f95 100644 --- a/platform/macos/export/export_plugin.h +++ b/platform/macos/export/export_plugin.h @@ -90,14 +90,14 @@ class EditorExportPlatformMacOS : public EditorExportPlatform { void _make_icon(const Ref<EditorExportPreset> &p_preset, const Ref<Image> &p_icon, Vector<uint8_t> &p_data); Error _notarize(const Ref<EditorExportPreset> &p_preset, const String &p_path); - Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn = true, bool p_set_id = false); - Error _code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code = true); + void _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, bool p_warn = true, bool p_set_id = false); + void _code_sign_directory(const Ref<EditorExportPreset> &p_preset, const String &p_path, const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code = true); Error _copy_and_sign_files(Ref<DirAccess> &dir_access, const String &p_src_path, const String &p_in_app_path, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, const String &p_ent_path, const String &p_helper_ent_path, - bool p_should_error_on_non_code_sign); + bool p_should_error_on_non_code_sign, bool p_sandbox); Error _export_macos_plugins_for(Ref<EditorExportPlugin> p_editor_export_plugin, const String &p_app_path_name, Ref<DirAccess> &dir_access, bool p_sign_enabled, const Ref<EditorExportPreset> &p_preset, - const String &p_ent_path, const String &p_helper_ent_path); + const String &p_ent_path, const String &p_helper_ent_path, bool p_sandbox); Error _create_dmg(const String &p_dmg_path, const String &p_pkg_name, const String &p_app_path_name); Error _create_pkg(const Ref<EditorExportPreset> &p_preset, const String &p_pkg_path, const String &p_app_path_name); Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path); diff --git a/platform/macos/gl_manager_macos_legacy.h b/platform/macos/gl_manager_macos_legacy.h index af9be8f5ba..383c5c3306 100644 --- a/platform/macos/gl_manager_macos_legacy.h +++ b/platform/macos/gl_manager_macos_legacy.h @@ -62,6 +62,7 @@ class GLManagerLegacy_MacOS { Error create_context(GLWindow &win); + bool framework_loaded = false; bool use_vsync = false; CGLEnablePtr CGLEnable = nullptr; CGLSetParameterPtr CGLSetParameter = nullptr; diff --git a/platform/macos/gl_manager_macos_legacy.mm b/platform/macos/gl_manager_macos_legacy.mm index 6ce3831d9c..a0d037144e 100644 --- a/platform/macos/gl_manager_macos_legacy.mm +++ b/platform/macos/gl_manager_macos_legacy.mm @@ -32,6 +32,7 @@ #if defined(MACOS_ENABLED) && defined(GLES3_ENABLED) +#include <dlfcn.h> #include <stdio.h> #include <stdlib.h> @@ -156,7 +157,7 @@ void GLManagerLegacy_MacOS::window_set_per_pixel_transparency_enabled(DisplaySer } Error GLManagerLegacy_MacOS::initialize() { - return OK; + return framework_loaded ? OK : ERR_CANT_CREATE; } void GLManagerLegacy_MacOS::set_use_vsync(bool p_use) { @@ -186,12 +187,17 @@ NSOpenGLContext *GLManagerLegacy_MacOS::get_context(DisplayServer::WindowID p_wi } GLManagerLegacy_MacOS::GLManagerLegacy_MacOS() { - CFBundleRef framework = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.opengl")); - CFBundleLoadExecutable(framework); - - CGLEnable = (CGLEnablePtr)CFBundleGetFunctionPointerForName(framework, CFSTR("CGLEnable")); - CGLSetParameter = (CGLSetParameterPtr)CFBundleGetFunctionPointerForName(framework, CFSTR("CGLSetParameter")); - CGLGetCurrentContext = (CGLGetCurrentContextPtr)CFBundleGetFunctionPointerForName(framework, CFSTR("CGLGetCurrentContext")); + NSBundle *framework = [NSBundle bundleWithPath:@"/System/Library/Frameworks/OpenGL.framework"]; + if (framework) { + void *library_handle = dlopen([framework.executablePath UTF8String], RTLD_NOW); + if (library_handle) { + CGLEnable = (CGLEnablePtr)dlsym(library_handle, "CGLEnable"); + CGLSetParameter = (CGLSetParameterPtr)dlsym(library_handle, "CGLSetParameter"); + CGLGetCurrentContext = (CGLGetCurrentContextPtr)dlsym(library_handle, "CGLGetCurrentContext"); + + framework_loaded = CGLEnable && CGLSetParameter && CGLGetCurrentContext; + } + } } GLManagerLegacy_MacOS::~GLManagerLegacy_MacOS() { diff --git a/platform/macos/godot_content_view.mm b/platform/macos/godot_content_view.mm index 77f3a28ae7..7d43ac9fe6 100644 --- a/platform/macos/godot_content_view.mm +++ b/platform/macos/godot_content_view.mm @@ -329,8 +329,9 @@ Callable::CallError ce; wd.drop_files_callback.callp((const Variant **)&v_args, 1, ret, ce); if (ce.error != Callable::CallError::CALL_OK) { - ERR_PRINT(vformat("Failed to execute drop files callback: %s.", Variant::get_callable_error_text(wd.drop_files_callback, v_args, 1, ce))); + ERR_FAIL_V_MSG(NO, vformat("Failed to execute drop files callback: %s.", Variant::get_callable_error_text(wd.drop_files_callback, v_args, 1, ce))); } + return YES; } return NO; diff --git a/platform/macos/godot_menu_item.h b/platform/macos/godot_menu_item.h index b6e2d41c08..e1af317259 100644 --- a/platform/macos/godot_menu_item.h +++ b/platform/macos/godot_menu_item.h @@ -52,6 +52,7 @@ enum GlobalMenuCheckType { Callable hover_callback; Variant meta; GlobalMenuCheckType checkable_type; + bool checked; int max_states; int state; Ref<Image> img; diff --git a/platform/macos/godot_menu_item.mm b/platform/macos/godot_menu_item.mm index 30dac9be9b..479542113a 100644 --- a/platform/macos/godot_menu_item.mm +++ b/platform/macos/godot_menu_item.mm @@ -31,4 +31,18 @@ #include "godot_menu_item.h" @implementation GodotMenuItem + +- (id)init { + self = [super init]; + + self->callback = Callable(); + self->key_callback = Callable(); + self->checkable_type = GlobalMenuCheckType::CHECKABLE_TYPE_NONE; + self->checked = false; + self->max_states = 0; + self->state = 0; + + return self; +} + @end diff --git a/platform/macos/joypad_macos.mm b/platform/macos/joypad_macos.mm index 8cd5cdd9f2..beb32d9129 100644 --- a/platform/macos/joypad_macos.mm +++ b/platform/macos/joypad_macos.mm @@ -228,7 +228,7 @@ void JoypadMacOS::joypad_vibration_stop(Joypad *p_joypad, uint64_t p_timestamp) @property(assign, nonatomic) BOOL isObserving; @property(assign, nonatomic) BOOL isProcessing; @property(strong, nonatomic) NSMutableDictionary<NSNumber *, Joypad *> *connectedJoypads; -@property(strong, nonatomic) NSMutableArray<Joypad *> *joypadsQueue; +@property(strong, nonatomic) NSMutableArray<GCController *> *joypadsQueue; @end @@ -364,8 +364,7 @@ void JoypadMacOS::joypad_vibration_stop(Joypad *p_joypad, uint64_t p_timestamp) if ([[self getAllKeysForController:controller] count] > 0) { print_verbose("Controller is already registered."); } else if (!self.isProcessing) { - Joypad *joypad = [[Joypad alloc] init:controller]; - [self.joypadsQueue addObject:joypad]; + [self.joypadsQueue addObject:controller]; } else { [self addMacOSJoypad:controller]; } diff --git a/platform/macos/native_menu_macos.mm b/platform/macos/native_menu_macos.mm index 1ae1137ca0..802d58dc26 100644 --- a/platform/macos/native_menu_macos.mm +++ b/platform/macos/native_menu_macos.mm @@ -373,12 +373,7 @@ int NativeMenuMacOS::add_submenu_item(const RID &p_rid, const String &p_label, c menu_item = [md->menu insertItemWithTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()] action:nil keyEquivalent:@"" atIndex:p_index]; GodotMenuItem *obj = [[GodotMenuItem alloc] init]; - obj->callback = Callable(); - obj->key_callback = Callable(); obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; - obj->max_states = 0; - obj->state = 0; [menu_item setRepresentedObject:obj]; [md_sub->menu setTitle:[NSString stringWithUTF8String:p_label.utf8().get_data()]]; @@ -417,9 +412,6 @@ int NativeMenuMacOS::add_item(const RID &p_rid, const String &p_label, const Cal obj->callback = p_callback; obj->key_callback = p_key_callback; obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; - obj->max_states = 0; - obj->state = 0; [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } @@ -438,8 +430,6 @@ int NativeMenuMacOS::add_check_item(const RID &p_rid, const String &p_label, con obj->key_callback = p_key_callback; obj->meta = p_tag; obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX; - obj->max_states = 0; - obj->state = 0; [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } @@ -457,9 +447,6 @@ int NativeMenuMacOS::add_icon_item(const RID &p_rid, const Ref<Texture2D> &p_ico obj->callback = p_callback; obj->key_callback = p_key_callback; obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; - obj->max_states = 0; - obj->state = 0; DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); if (ds && p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0 && p_icon->get_image().is_valid()) { obj->img = p_icon->get_image(); @@ -489,8 +476,6 @@ int NativeMenuMacOS::add_icon_check_item(const RID &p_rid, const Ref<Texture2D> obj->key_callback = p_key_callback; obj->meta = p_tag; obj->checkable_type = CHECKABLE_TYPE_CHECK_BOX; - obj->max_states = 0; - obj->state = 0; DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); if (ds && p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0 && p_icon->get_image().is_valid()) { obj->img = p_icon->get_image(); @@ -520,8 +505,6 @@ int NativeMenuMacOS::add_radio_check_item(const RID &p_rid, const String &p_labe obj->key_callback = p_key_callback; obj->meta = p_tag; obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; - obj->max_states = 0; - obj->state = 0; [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; [menu_item setRepresentedObject:obj]; } @@ -540,8 +523,6 @@ int NativeMenuMacOS::add_icon_radio_check_item(const RID &p_rid, const Ref<Textu obj->key_callback = p_key_callback; obj->meta = p_tag; obj->checkable_type = CHECKABLE_TYPE_RADIO_BUTTON; - obj->max_states = 0; - obj->state = 0; DisplayServerMacOS *ds = (DisplayServerMacOS *)DisplayServer::get_singleton(); if (ds && p_icon.is_valid() && p_icon->get_width() > 0 && p_icon->get_height() > 0 && p_icon->get_image().is_valid()) { obj->img = p_icon->get_image(); @@ -570,7 +551,6 @@ int NativeMenuMacOS::add_multistate_item(const RID &p_rid, const String &p_label obj->callback = p_callback; obj->key_callback = p_key_callback; obj->meta = p_tag; - obj->checkable_type = CHECKABLE_TYPE_NONE; obj->max_states = p_max_states; obj->state = p_default_state; [menu_item setKeyEquivalentModifierMask:KeyMappingMacOS::keycode_get_native_mask(p_accel)]; @@ -640,7 +620,10 @@ bool NativeMenuMacOS::is_item_checked(const RID &p_rid, int p_idx) const { ERR_FAIL_COND_V(p_idx >= item_start + item_count, false); const NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; if (menu_item) { - return ([menu_item state] == NSControlStateValueOn); + const GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + return obj->checked; + } } return false; } @@ -958,10 +941,14 @@ void NativeMenuMacOS::set_item_checked(const RID &p_rid, int p_idx, bool p_check ERR_FAIL_COND(p_idx >= item_start + item_count); NSMenuItem *menu_item = [md->menu itemAtIndex:p_idx]; if (menu_item) { - if (p_checked) { - [menu_item setState:NSControlStateValueOn]; - } else { - [menu_item setState:NSControlStateValueOff]; + GodotMenuItem *obj = [menu_item representedObject]; + if (obj) { + obj->checked = p_checked; + if (p_checked) { + [menu_item setState:NSControlStateValueOn]; + } else { + [menu_item setState:NSControlStateValueOff]; + } } } } diff --git a/platform/macos/os_macos.h b/platform/macos/os_macos.h index 912a682a6b..303fc112bf 100644 --- a/platform/macos/os_macos.h +++ b/platform/macos/os_macos.h @@ -109,6 +109,7 @@ public: virtual String get_executable_path() const override; virtual Error create_process(const String &p_path, const List<String> &p_arguments, ProcessID *r_child_id = nullptr, bool p_open_console = false) override; virtual Error create_instance(const List<String> &p_arguments, ProcessID *r_child_id = nullptr) override; + virtual bool is_process_running(const ProcessID &p_pid) const override; virtual String get_unique_id() const override; virtual String get_processor_name() const override; diff --git a/platform/macos/os_macos.mm b/platform/macos/os_macos.mm index 9f0bea5951..3a82514766 100644 --- a/platform/macos/os_macos.mm +++ b/platform/macos/os_macos.mm @@ -666,6 +666,15 @@ Error OS_MacOS::create_instance(const List<String> &p_arguments, ProcessID *r_ch } } +bool OS_MacOS::is_process_running(const ProcessID &p_pid) const { + NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:(pid_t)p_pid]; + if (!app) { + return OS_Unix::is_process_running(p_pid); + } + + return ![app isTerminated]; +} + String OS_MacOS::get_unique_id() const { static String serial_number; diff --git a/platform/web/audio_driver_web.cpp b/platform/web/audio_driver_web.cpp index dd986e650c..5e046d7050 100644 --- a/platform/web/audio_driver_web.cpp +++ b/platform/web/audio_driver_web.cpp @@ -33,6 +33,8 @@ #include "godot_audio.h" #include "core/config/project_settings.h" +#include "core/object/object.h" +#include "scene/main/node.h" #include "servers/audio/audio_stream.h" #include <emscripten.h> @@ -51,6 +53,21 @@ void AudioDriverWeb::_latency_update_callback(float p_latency) { AudioDriverWeb::audio_context.output_latency = p_latency; } +void AudioDriverWeb::_sample_playback_finished_callback(const char *p_playback_object_id) { + const ObjectID playback_id = ObjectID(String::to_int(p_playback_object_id)); + + Object *playback_object = ObjectDB::get_instance(playback_id); + if (playback_object == nullptr) { + return; + } + Ref<AudioSamplePlayback> playback = Object::cast_to<AudioSamplePlayback>(playback_object); + if (playback.is_null()) { + return; + } + + AudioServer::get_singleton()->stop_sample_playback(playback); +} + void AudioDriverWeb::_audio_driver_process(int p_from, int p_samples) { int32_t *stream_buffer = reinterpret_cast<int32_t *>(output_rb); const int max_samples = memarr_len(output_rb); @@ -132,6 +149,9 @@ Error AudioDriverWeb::init() { if (!input_rb) { return ERR_OUT_OF_MEMORY; } + + godot_audio_sample_set_finished_callback(&_sample_playback_finished_callback); + return OK; } @@ -292,6 +312,11 @@ bool AudioDriverWeb::is_sample_playback_active(const Ref<AudioSamplePlayback> &p return godot_audio_sample_is_active(itos(p_playback->get_instance_id()).utf8().get_data()) != 0; } +double AudioDriverWeb::get_sample_playback_position(const Ref<AudioSamplePlayback> &p_playback) { + ERR_FAIL_COND_V_MSG(p_playback.is_null(), false, "Parameter p_playback is null."); + return godot_audio_get_sample_playback_position(itos(p_playback->get_instance_id()).utf8().get_data()); +} + void AudioDriverWeb::update_sample_playback_pitch_scale(const Ref<AudioSamplePlayback> &p_playback, float p_pitch_scale) { ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null."); godot_audio_sample_update_pitch_scale( diff --git a/platform/web/audio_driver_web.h b/platform/web/audio_driver_web.h index 298ad90fae..d352fa4692 100644 --- a/platform/web/audio_driver_web.h +++ b/platform/web/audio_driver_web.h @@ -58,6 +58,7 @@ private: WASM_EXPORT static void _state_change_callback(int p_state); WASM_EXPORT static void _latency_update_callback(float p_latency); + WASM_EXPORT static void _sample_playback_finished_callback(const char *p_playback_object_id); static AudioDriverWeb *singleton; @@ -95,6 +96,7 @@ public: virtual void stop_sample_playback(const Ref<AudioSamplePlayback> &p_playback) override; virtual void set_sample_playback_pause(const Ref<AudioSamplePlayback> &p_playback, bool p_paused) override; virtual bool is_sample_playback_active(const Ref<AudioSamplePlayback> &p_playback) override; + virtual double get_sample_playback_position(const Ref<AudioSamplePlayback> &p_playback) override; virtual void update_sample_playback_pitch_scale(const Ref<AudioSamplePlayback> &p_playback, float p_pitch_scale = 0.0f) override; virtual void set_sample_playback_bus_volumes_linear(const Ref<AudioSamplePlayback> &p_playback, const HashMap<StringName, Vector<AudioFrame>> &p_bus_volumes) override; diff --git a/platform/web/detect.py b/platform/web/detect.py index cb4dac1125..bf75c2f9fc 100644 --- a/platform/web/detect.py +++ b/platform/web/detect.py @@ -78,6 +78,7 @@ def get_flags(): # -Os reduces file size by around 5 MiB over -O3. -Oz only saves about # 100 KiB over -Os, which does not justify the negative impact on # run-time performance. + # Note that this overrides the "auto" behavior for target/dev_build. "optimize": "size", } @@ -226,6 +227,11 @@ def configure(env: "SConsEnvironment"): env.Append(LINKFLAGS=["-sDEFAULT_PTHREAD_STACK_SIZE=%sKB" % env["default_pthread_stack_size"]]) env.Append(LINKFLAGS=["-sPTHREAD_POOL_SIZE=8"]) env.Append(LINKFLAGS=["-sWASM_MEM_MAX=2048MB"]) + if not env["dlink_enabled"]: + # Workaround https://github.com/emscripten-core/emscripten/issues/21844#issuecomment-2116936414. + # Not needed (and potentially dangerous) when dlink_enabled=yes, since we set EXPORT_ALL=1 in that case. + env.Append(LINKFLAGS=["-sEXPORTED_FUNCTIONS=['__emscripten_thread_crashed','_main']"]) + elif env["proxy_to_pthread"]: print_warning('"threads=no" support requires "proxy_to_pthread=no", disabling proxy to pthread.') env["proxy_to_pthread"] = False diff --git a/platform/web/display_server_web.cpp b/platform/web/display_server_web.cpp index 40de4e523b..4e55cc137a 100644 --- a/platform/web/display_server_web.cpp +++ b/platform/web/display_server_web.cpp @@ -902,8 +902,10 @@ void DisplayServerWeb::process_joypads() { for (int b = 0; b < s_btns_num; b++) { // Buttons 6 and 7 in the standard mapping need to be // axis to be handled as JoyAxis::TRIGGER by Godot. - if (s_standard && (b == 6 || b == 7)) { - input->joy_axis(idx, (JoyAxis)b, s_btns[b]); + if (s_standard && (b == 6)) { + input->joy_axis(idx, JoyAxis::TRIGGER_LEFT, s_btns[b]); + } else if (s_standard && (b == 7)) { + input->joy_axis(idx, JoyAxis::TRIGGER_RIGHT, s_btns[b]); } else { input->joy_button(idx, (JoyButton)b, s_btns[b]); } diff --git a/platform/web/emscripten_helpers.py b/platform/web/emscripten_helpers.py index 2cee3e8110..8fcabb21c7 100644 --- a/platform/web/emscripten_helpers.py +++ b/platform/web/emscripten_helpers.py @@ -51,11 +51,13 @@ def create_template_zip(env, js, wasm, worker, side): js, wasm, "#platform/web/js/libs/audio.worklet.js", + "#platform/web/js/libs/audio.position.worklet.js", ] out_files = [ zip_dir.File(binary_name + ".js"), zip_dir.File(binary_name + ".wasm"), zip_dir.File(binary_name + ".audio.worklet.js"), + zip_dir.File(binary_name + ".audio.position.worklet.js"), ] if env["threads"]: in_files.append(worker) @@ -74,6 +76,7 @@ def create_template_zip(env, js, wasm, worker, side): "offline.html", "godot.editor.js", "godot.editor.audio.worklet.js", + "godot.editor.audio.position.worklet.js", "logo.svg", "favicon.png", ] diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp index d83e465e8e..d8c1b6033d 100644 --- a/platform/web/export/export_plugin.cpp +++ b/platform/web/export/export_plugin.cpp @@ -242,6 +242,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese } cache_files.push_back(name + ".worker.js"); cache_files.push_back(name + ".audio.worklet.js"); + cache_files.push_back(name + ".audio.position.worklet.js"); replaces["___GODOT_CACHE___"] = Variant(cache_files).to_json_string(); // Heavy files that are cached on demand. @@ -835,6 +836,7 @@ Error EditorExportPlatformWeb::_export_project(const Ref<EditorExportPreset> &p_ DirAccess::remove_file_or_error(basepath + ".js"); DirAccess::remove_file_or_error(basepath + ".worker.js"); DirAccess::remove_file_or_error(basepath + ".audio.worklet.js"); + DirAccess::remove_file_or_error(basepath + ".audio.position.worklet.js"); DirAccess::remove_file_or_error(basepath + ".service.worker.js"); DirAccess::remove_file_or_error(basepath + ".pck"); DirAccess::remove_file_or_error(basepath + ".png"); diff --git a/platform/web/godot_audio.h b/platform/web/godot_audio.h index 8bebbcf7de..4961ebd2bb 100644 --- a/platform/web/godot_audio.h +++ b/platform/web/godot_audio.h @@ -55,8 +55,10 @@ extern void godot_audio_sample_start(const char *p_playback_object_id, const cha extern void godot_audio_sample_stop(const char *p_playback_object_id); extern void godot_audio_sample_set_pause(const char *p_playback_object_id, bool p_pause); extern int godot_audio_sample_is_active(const char *p_playback_object_id); +extern double godot_audio_get_sample_playback_position(const char *p_playback_object_id); extern void godot_audio_sample_update_pitch_scale(const char *p_playback_object_id, float p_pitch_scale); extern void godot_audio_sample_set_volumes_linear(const char *p_playback_object_id, int *p_buses_buf, int p_buses_size, float *p_volumes_buf, int p_volumes_size); +extern void godot_audio_sample_set_finished_callback(void (*p_callback)(const char *)); extern void godot_audio_sample_bus_set_count(int p_count); extern void godot_audio_sample_bus_remove(int p_index); diff --git a/platform/web/js/engine/config.js b/platform/web/js/engine/config.js index 8c4e1b1b24..61b488cf81 100644 --- a/platform/web/js/engine/config.js +++ b/platform/web/js/engine/config.js @@ -299,6 +299,8 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused- return `${loadPath}.worker.js`; } else if (path.endsWith('.audio.worklet.js')) { return `${loadPath}.audio.worklet.js`; + } else if (path.endsWith('.audio.position.worklet.js')) { + return `${loadPath}.audio.position.worklet.js`; } else if (path.endsWith('.js')) { return `${loadPath}.js`; } else if (path in gdext) { diff --git a/platform/web/js/libs/audio.position.worklet.js b/platform/web/js/libs/audio.position.worklet.js new file mode 100644 index 0000000000..bf3ac4ae2d --- /dev/null +++ b/platform/web/js/libs/audio.position.worklet.js @@ -0,0 +1,50 @@ +/**************************************************************************/ +/* godot.audio.position.worklet.js */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +class GodotPositionReportingProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.position = 0; + } + + process(inputs, _outputs, _parameters) { + if (inputs.length > 0) { + const input = inputs[0]; + if (input.length > 0) { + this.position += input[0].length; + this.port.postMessage({ 'type': 'position', 'data': this.position }); + return true; + } + } + return true; + } +} + +registerProcessor('godot-position-reporting-processor', GodotPositionReportingProcessor); diff --git a/platform/web/js/libs/library_godot_audio.js b/platform/web/js/libs/library_godot_audio.js index 531dbdaeab..0ba6eed464 100644 --- a/platform/web/js/libs/library_godot_audio.js +++ b/platform/web/js/libs/library_godot_audio.js @@ -77,7 +77,7 @@ class Sample { * Creates a `Sample` based on the params. Will register it to the * `GodotAudio.samples` registry. * @param {SampleParams} params Base params - * @param {SampleOptions} [options={}] Optional params + * @param {SampleOptions} [options={{}}] Optional params * @returns {Sample} */ static create(params, options = {}) { @@ -98,8 +98,7 @@ class Sample { /** * `Sample` constructor. * @param {SampleParams} params Base params - * @param {SampleOptions} [options={}] Optional params - * @constructor + * @param {SampleOptions} [options={{}}] Optional params */ constructor(params, options = {}) { /** @type {string} */ @@ -154,7 +153,7 @@ class Sample { if (this._audioBuffer == null) { throw new Error('couldn\'t duplicate a null audioBuffer'); } - /** @type {Float32Array[]} */ + /** @type {Array<Float32Array>} */ const channels = new Array(this._audioBuffer.numberOfChannels); for (let i = 0; i < this._audioBuffer.numberOfChannels; i++) { const channel = new Float32Array(this._audioBuffer.getChannelData(i)); @@ -189,7 +188,6 @@ class SampleNodeBus { /** * `SampleNodeBus` constructor. * @param {Bus} bus The bus related to the new `SampleNodeBus`. - * @constructor */ constructor(bus) { const NUMBER_OF_WEB_CHANNELS = 6; @@ -332,6 +330,7 @@ class SampleNodeBus { * startTime?: number * loopMode?: LoopMode * volume?: Float32Array + * start?: boolean * }} SampleNodeOptions */ @@ -413,8 +412,7 @@ class SampleNode { /** * @param {SampleNodeParams} params Base params - * @param {SampleNodeOptions} [options={}] Optional params - * @constructor + * @param {SampleNodeOptions} [options={{}}] Optional params */ constructor(params, options = {}) { /** @type {string} */ @@ -424,9 +422,15 @@ class SampleNode { /** @type {number} */ this.offset = options.offset ?? 0; /** @type {number} */ + this._playbackPosition = options.offset; + /** @type {number} */ this.startTime = options.startTime ?? 0; /** @type {boolean} */ this.isPaused = false; + /** @type {boolean} */ + this.isStarted = false; + /** @type {boolean} */ + this.isCanceled = false; /** @type {number} */ this.pauseTime = 0; /** @type {number} */ @@ -441,8 +445,10 @@ class SampleNode { this._sampleNodeBuses = new Map(); /** @type {AudioBufferSourceNode | null} */ this._source = GodotAudio.ctx.createBufferSource(); - /** @type {AudioBufferSourceNode["onended"]} */ + this._onended = null; + /** @type {AudioWorkletNode | null} */ + this._positionWorklet = null; this.setPlaybackRate(options.playbackRate ?? 44100); this._source.buffer = this.getSample().getAudioBuffer(); @@ -452,6 +458,8 @@ class SampleNode { const bus = GodotAudio.Bus.getBus(params.busIndex); const sampleNodeBus = this.getSampleNodeBus(bus); sampleNodeBus.setVolume(options.volume); + + this.connectPositionWorklet(options.start); } /** @@ -463,6 +471,14 @@ class SampleNode { } /** + * Gets the playback position. + * @returns {number} + */ + getPlaybackPosition() { + return this._playbackPosition; + } + + /** * Sets the playback rate. * @param {number} val Value to set. * @returns {void} @@ -511,8 +527,12 @@ class SampleNode { * @returns {void} */ start() { + if (this.isStarted) { + return; + } this._resetSourceStartTime(); this._source.start(this.startTime, this.offset); + this.isStarted = true; } /** @@ -558,7 +578,7 @@ class SampleNode { /** * Sets the volumes of the `SampleNode` for each buses passed in parameters. - * @param {Bus[]} buses + * @param {Array<Bus>} buses * @param {Float32Array} volumes */ setVolumes(buses, volumes) { @@ -588,17 +608,73 @@ class SampleNode { } /** + * Sets up and connects the source to the GodotPositionReportingProcessor + * If the worklet module is not loaded in, it will be added + */ + connectPositionWorklet(start) { + try { + this._positionWorklet = this.createPositionWorklet(); + this._source.connect(this._positionWorklet); + if (start) { + this.start(); + } + } catch (error) { + if (error?.name !== 'InvalidStateError') { + throw error; + } + const path = GodotConfig.locate_file('godot.audio.position.worklet.js'); + GodotAudio.ctx.audioWorklet + .addModule(path) + .then(() => { + if (!this.isCanceled) { + this._positionWorklet = this.createPositionWorklet(); + this._source.connect(this._positionWorklet); + if (start) { + this.start(); + } + } + }).catch((addModuleError) => { + GodotRuntime.error('Failed to create PositionWorklet.', addModuleError); + }); + } + } + + /** + * Creates the AudioWorkletProcessor used to track playback position. + * @returns {AudioWorkletNode} + */ + createPositionWorklet() { + const worklet = new AudioWorkletNode( + GodotAudio.ctx, + 'godot-position-reporting-processor' + ); + worklet.port.onmessage = (event) => { + switch (event.data['type']) { + case 'position': + this._playbackPosition = (parseInt(event.data.data, 10) / this.getSample().sampleRate) + this.offset; + break; + default: + // Do nothing. + } + }; + return worklet; + } + + /** * Clears the `SampleNode`. * @returns {void} */ clear() { + this.isCanceled = true; this.isPaused = false; this.pauseTime = 0; if (this._source != null) { this._source.removeEventListener('ended', this._onended); this._onended = null; - this._source.stop(); + if (this.isStarted) { + this._source.stop(); + } this._source.disconnect(); this._source = null; } @@ -608,6 +684,12 @@ class SampleNode { } this._sampleNodeBuses.clear(); + if (this._positionWorklet) { + this._positionWorklet.disconnect(); + this._positionWorklet.port.onmessage = null; + this._positionWorklet = null; + } + GodotAudio.SampleNode.delete(this.id); } @@ -633,7 +715,9 @@ class SampleNode { * @returns {void} */ _restart() { - this._source.disconnect(); + if (this._source != null) { + this._source.disconnect(); + } this._source = GodotAudio.ctx.createBufferSource(); this._source.buffer = this.getSample().getAudioBuffer(); @@ -646,7 +730,9 @@ class SampleNode { const pauseTime = this.isPaused ? this.pauseTime : 0; + this.connectPositionWorklet(); this._source.start(this.startTime, this.offset + pauseTime); + this.isStarted = true; } /** @@ -687,9 +773,15 @@ class SampleNode { } switch (self.getSample().loopMode) { - case 'disabled': + case 'disabled': { + const id = this.id; self.stop(); - break; + if (GodotAudio.sampleFinishedCallback != null) { + const idCharPtr = GodotRuntime.allocString(id); + GodotAudio.sampleFinishedCallback(idCharPtr); + GodotRuntime.free(idCharPtr); + } + } break; case 'forward': case 'backward': self.restart(); @@ -812,7 +904,6 @@ class Bus { /** * `Bus` constructor. - * @constructor */ constructor() { /** @type {Set<SampleNode>} */ @@ -856,7 +947,10 @@ class Bus { * @returns {void} */ setVolumeDb(val) { - this._gainNode.gain.value = GodotAudio.db_to_linear(val); + const linear = GodotAudio.db_to_linear(val); + if (isFinite(linear)) { + this._gainNode.gain.value = linear; + } } /** @@ -979,7 +1073,6 @@ class Bus { GodotAudio.buses = GodotAudio.buses.filter((v) => v !== this); } - /** @type {Bus["prototype"]["_syncSampleNodes"]} */ _syncSampleNodes() { const sampleNodes = Array.from(this._sampleNodes); for (let i = 0; i < sampleNodes.length; i++) { @@ -1080,7 +1173,7 @@ const _GodotAudio = { // `Bus` class /** * Registry of `Bus`es. - * @type {Bus[]} + * @type {Array<Bus>} */ buses: null, /** @@ -1090,6 +1183,12 @@ const _GodotAudio = { busSolo: null, Bus, + /** + * Callback to signal that a sample has finished. + * @type {(playbackObjectIdPtr: number) => void | null} + */ + sampleFinishedCallback: null, + /** @type {AudioContext} */ ctx: null, input: null, @@ -1250,7 +1349,7 @@ const _GodotAudio = { startOptions ) { GodotAudio.SampleNode.stopSampleNode(playbackObjectId); - const sampleNode = GodotAudio.SampleNode.create( + GodotAudio.SampleNode.create( { busIndex, id: playbackObjectId, @@ -1258,7 +1357,6 @@ const _GodotAudio = { }, startOptions ); - sampleNode.start(); }, /** @@ -1297,7 +1395,7 @@ const _GodotAudio = { /** * Triggered when a sample node volumes need to be updated. * @param {string} playbackObjectId Id of the sample playback - * @param {number[]} busIndexes Indexes of the buses that need to be updated + * @param {Array<number>} busIndexes Indexes of the buses that need to be updated * @param {Float32Array} volumes Array of the volumes * @returns {void} */ @@ -1578,6 +1676,7 @@ const _GodotAudio = { offset, volume, playbackRate: 1, + start: true, }; GodotAudio.start_sample( playbackObjectId, @@ -1623,6 +1722,22 @@ const _GodotAudio = { return Number(GodotAudio.sampleNodes.has(playbackObjectId)); }, + godot_audio_get_sample_playback_position__proxy: 'sync', + godot_audio_get_sample_playback_position__sig: 'di', + /** + * Returns the position of the playback position. + * @param {number} playbackObjectIdStrPtr Playback object id pointer + * @returns {number} + */ + godot_audio_get_sample_playback_position: function (playbackObjectIdStrPtr) { + const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr); + const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(playbackObjectId); + if (sampleNode == null) { + return 0; + } + return sampleNode.getPlaybackPosition(); + }, + godot_audio_sample_update_pitch_scale__proxy: 'sync', godot_audio_sample_update_pitch_scale__sig: 'vii', /** @@ -1764,6 +1879,17 @@ const _GodotAudio = { godot_audio_sample_bus_set_mute: function (bus, enable) { GodotAudio.set_sample_bus_mute(bus, Boolean(enable)); }, + + godot_audio_sample_set_finished_callback__proxy: 'sync', + godot_audio_sample_set_finished_callback__sig: 'vi', + /** + * Sets the finished callback + * @param {Number} callbackPtr Finished callback pointer + * @returns {void} + */ + godot_audio_sample_set_finished_callback: function (callbackPtr) { + GodotAudio.sampleFinishedCallback = GodotRuntime.get_func(callbackPtr); + }, }; autoAddDeps(_GodotAudio, '$GodotAudio'); diff --git a/platform/web/js/libs/library_godot_input.js b/platform/web/js/libs/library_godot_input.js index 7ea89d553f..6e3b97023d 100644 --- a/platform/web/js/libs/library_godot_input.js +++ b/platform/web/js/libs/library_godot_input.js @@ -112,6 +112,7 @@ const GodotIME = { ime.style.top = '0px'; ime.style.width = '100%'; ime.style.height = '40px'; + ime.style.pointerEvents = 'none'; ime.style.display = 'none'; ime.contentEditable = 'true'; diff --git a/platform/web/serve.py b/platform/web/serve.py index f0b0ec9622..4e1521449b 100755 --- a/platform/web/serve.py +++ b/platform/web/serve.py @@ -6,7 +6,7 @@ import os import socket import subprocess import sys -from http.server import HTTPServer, SimpleHTTPRequestHandler, test # type: ignore +from http.server import HTTPServer, SimpleHTTPRequestHandler from pathlib import Path @@ -38,12 +38,24 @@ def shell_open(url): def serve(root, port, run_browser): os.chdir(root) + address = ("", port) + httpd = DualStackServer(address, CORSRequestHandler) + + url = f"http://127.0.0.1:{port}" if run_browser: # Open the served page in the user's default browser. - print("Opening the served URL in the default browser (use `--no-browser` or `-n` to disable this).") - shell_open(f"http://127.0.0.1:{port}") + print(f"Opening the served URL in the default browser (use `--no-browser` or `-n` to disable this): {url}") + shell_open(url) + else: + print(f"Serving at: {url}") - test(CORSRequestHandler, DualStackServer, port=port) + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nKeyboard interrupt received, stopping server.") + finally: + # Clean-up server + httpd.server_close() if __name__ == "__main__": diff --git a/platform/windows/SCsub b/platform/windows/SCsub index f2fb8616ae..f8ed8b73f5 100644 --- a/platform/windows/SCsub +++ b/platform/windows/SCsub @@ -108,18 +108,6 @@ if env["d3d12"]: # Used in cases where we can have multiple archs side-by-side. arch_bin_dir = "#bin/" + env["arch"] - # DXC - if env["dxc_path"] != "" and os.path.exists(env["dxc_path"]): - dxc_dll = "dxil.dll" - # Whether this one is loaded from arch-specific directory or not can be determined at runtime. - # Let's copy to both and let the user decide the distribution model. - for v in ["#bin", arch_bin_dir]: - env.Command( - v + "/" + dxc_dll, - env["dxc_path"] + "/bin/" + dxc_arch_subdir + "/" + dxc_dll, - Copy("$TARGET", "$SOURCE"), - ) - # Agility SDK if env["agility_sdk_path"] != "" and os.path.exists(env["agility_sdk_path"]): agility_dlls = ["D3D12Core.dll", "d3d12SDKLayers.dll"] diff --git a/platform/windows/detect.py b/platform/windows/detect.py index fee306a25c..b4830c5908 100644 --- a/platform/windows/detect.py +++ b/platform/windows/detect.py @@ -214,11 +214,6 @@ def get_opts(): os.path.join(d3d12_deps_folder, "mesa"), ), ( - "dxc_path", - "Path to the DirectX Shader Compiler distribution (required for D3D12)", - os.path.join(d3d12_deps_folder, "dxc"), - ), - ( "agility_sdk_path", "Path to the Agility SDK distribution (optional for D3D12)", os.path.join(d3d12_deps_folder, "agility_sdk"), @@ -306,7 +301,6 @@ def setup_msvc_manual(env: "SConsEnvironment"): print("Using VCVARS-determined MSVC, arch %s" % (env_arch)) -# FIXME: Likely overwrites command-line options for the msvc compiler. See #91883. def setup_msvc_auto(env: "SConsEnvironment"): """Set up MSVC using SCons's auto-detection logic""" @@ -339,6 +333,12 @@ def setup_msvc_auto(env: "SConsEnvironment"): env.Tool("msvc") env.Tool("mssdk") # we want the MS SDK + # Re-add potentially overwritten flags. + env.AppendUnique(CCFLAGS=env.get("ccflags", "").split()) + env.AppendUnique(CXXFLAGS=env.get("cxxflags", "").split()) + env.AppendUnique(CFLAGS=env.get("cflags", "").split()) + env.AppendUnique(RCFLAGS=env.get("rcflags", "").split()) + # Note: actual compiler version can be found in env['MSVC_VERSION'], e.g. "14.1" for VS2015 print("Using SCons-detected MSVC version %s, arch %s" % (env["MSVC_VERSION"], env["arch"])) @@ -467,6 +467,8 @@ def configure_msvc(env: "SConsEnvironment", vcvars_msvc_config): if env["arch"] == "x86_32": env["x86_libtheora_opt_vc"] = True + env.Append(CCFLAGS=["/fp:strict"]) + env.AppendUnique(CCFLAGS=["/Gd", "/GR", "/nologo"]) env.AppendUnique(CCFLAGS=["/utf-8"]) # Force to use Unicode encoding. env.AppendUnique(CXXFLAGS=["/TP"]) # assume all sources are C++ @@ -644,7 +646,8 @@ def configure_mingw(env: "SConsEnvironment"): # TODO: Re-evaluate the need for this / streamline with common config. if env["target"] == "template_release": - env.Append(CCFLAGS=["-msse2"]) + if env["arch"] != "arm64": + env.Append(CCFLAGS=["-msse2"]) elif env.dev_build: # Allow big objects. It's supposed not to have drawbacks but seems to break # GCC LTO, so enabling for debug builds only (which are not built with LTO @@ -674,6 +677,8 @@ def configure_mingw(env: "SConsEnvironment"): if env["arch"] in ["x86_32", "x86_64"]: env["x86_libtheora_opt_gcc"] = True + env.Append(CCFLAGS=["-ffp-contract=off"]) + mingw_bin_prefix = get_mingw_bin_prefix(env["mingw_prefix"], env["arch"]) if env["use_llvm"]: diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 8d26a705a9..635e8326e2 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -38,6 +38,7 @@ #include "core/version.h" #include "drivers/png/png_driver_common.h" #include "main/main.h" +#include "scene/resources/texture.h" #if defined(VULKAN_ENABLED) #include "rendering_context_driver_vulkan_windows.h" @@ -249,6 +250,14 @@ void DisplayServerWindows::tts_stop() { tts->stop(); } +Error DisplayServerWindows::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) { + return _file_dialog_with_options_show(p_title, p_current_directory, String(), p_filename, p_show_hidden, p_mode, p_filters, TypedArray<Dictionary>(), p_callback, false); +} + +Error DisplayServerWindows::file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) { + return _file_dialog_with_options_show(p_title, p_current_directory, p_root, p_filename, p_show_hidden, p_mode, p_filters, p_options, p_callback, true); +} + // Silence warning due to a COM API weirdness. #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push @@ -376,22 +385,85 @@ public: #pragma GCC diagnostic pop #endif -Error DisplayServerWindows::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) { - return _file_dialog_with_options_show(p_title, p_current_directory, String(), p_filename, p_show_hidden, p_mode, p_filters, TypedArray<Dictionary>(), p_callback, false); +LRESULT CALLBACK WndProcFileDialog(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + DisplayServerWindows *ds_win = static_cast<DisplayServerWindows *>(DisplayServer::get_singleton()); + if (ds_win) { + return ds_win->WndProcFileDialog(hWnd, uMsg, wParam, lParam); + } else { + return DefWindowProcW(hWnd, uMsg, wParam, lParam); + } } -Error DisplayServerWindows::file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) { - return _file_dialog_with_options_show(p_title, p_current_directory, p_root, p_filename, p_show_hidden, p_mode, p_filters, p_options, p_callback, true); +LRESULT DisplayServerWindows::WndProcFileDialog(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + MutexLock lock(file_dialog_mutex); + if (file_dialog_wnd.has(hWnd)) { + if (file_dialog_wnd[hWnd]->close_requested.is_set()) { + IPropertyStore *prop_store; + HRESULT hr = SHGetPropertyStoreForWindow(hWnd, IID_IPropertyStore, (void **)&prop_store); + if (hr == S_OK) { + PROPVARIANT val; + PropVariantInit(&val); + prop_store->SetValue(PKEY_AppUserModel_ID, val); + prop_store->Release(); + } + DestroyWindow(hWnd); + file_dialog_wnd.erase(hWnd); + } + } + return DefWindowProcW(hWnd, uMsg, wParam, lParam); } -Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb) { - _THREAD_SAFE_METHOD_ +void DisplayServerWindows::_thread_fd_monitor(void *p_ud) { + DisplayServerWindows *ds = static_cast<DisplayServerWindows *>(get_singleton()); + FileDialogData *fd = (FileDialogData *)p_ud; - ERR_FAIL_INDEX_V(int(p_mode), FILE_DIALOG_MODE_SAVE_MAX, FAILED); + if (fd->mode < 0 && fd->mode >= DisplayServer::FILE_DIALOG_MODE_SAVE_MAX) { + fd->finished.set(); + return; + } + CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + int64_t x = fd->wrect.position.x; + int64_t y = fd->wrect.position.y; + int64_t w = fd->wrect.size.x; + int64_t h = fd->wrect.size.y; + + WNDCLASSW wc = {}; + wc.lpfnWndProc = (WNDPROC)::WndProcFileDialog; + wc.hInstance = GetModuleHandle(nullptr); + wc.lpszClassName = L"Engine File Dialog"; + RegisterClassW(&wc); + + HWND hwnd_dialog = CreateWindowExW(WS_EX_APPWINDOW, L"Engine File Dialog", L"", WS_OVERLAPPEDWINDOW, x, y, w, h, nullptr, nullptr, GetModuleHandle(nullptr), nullptr); + if (hwnd_dialog) { + { + MutexLock lock(ds->file_dialog_mutex); + ds->file_dialog_wnd[hwnd_dialog] = fd; + } + + HICON mainwindow_icon = (HICON)SendMessage(fd->hwnd_owner, WM_GETICON, ICON_SMALL, 0); + if (mainwindow_icon) { + SendMessage(hwnd_dialog, WM_SETICON, ICON_SMALL, (LPARAM)mainwindow_icon); + } + mainwindow_icon = (HICON)SendMessage(fd->hwnd_owner, WM_GETICON, ICON_BIG, 0); + if (mainwindow_icon) { + SendMessage(hwnd_dialog, WM_SETICON, ICON_BIG, (LPARAM)mainwindow_icon); + } + IPropertyStore *prop_store; + HRESULT hr = SHGetPropertyStoreForWindow(hwnd_dialog, IID_IPropertyStore, (void **)&prop_store); + if (hr == S_OK) { + PROPVARIANT val; + InitPropVariantFromString((PCWSTR)fd->appid.utf16().get_data(), &val); + prop_store->SetValue(PKEY_AppUserModel_ID, val); + prop_store->Release(); + } + } + + SetCurrentProcessExplicitAppUserModelID((PCWSTR)fd->appid.utf16().get_data()); Vector<Char16String> filter_names; Vector<Char16String> filter_exts; - for (const String &E : p_filters) { + for (const String &E : fd->filters) { Vector<String> tokens = E.split(";"); if (tokens.size() >= 1) { String flt = tokens[0].strip_edges(); @@ -424,11 +496,9 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title filters.push_back({ (LPCWSTR)filter_names[i].ptr(), (LPCWSTR)filter_exts[i].ptr() }); } - WindowID prev_focus = last_focused_window; - HRESULT hr = S_OK; IFileDialog *pfd = nullptr; - if (p_mode == FILE_DIALOG_MODE_SAVE_FILE) { + if (fd->mode == DisplayServer::FILE_DIALOG_MODE_SAVE_FILE) { hr = CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_INPROC_SERVER, IID_IFileSaveDialog, (void **)&pfd); } else { hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_IFileOpenDialog, (void **)&pfd); @@ -444,40 +514,32 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title IFileDialogCustomize *pfdc = nullptr; hr = pfd->QueryInterface(IID_PPV_ARGS(&pfdc)); - for (int i = 0; i < p_options.size(); i++) { - const Dictionary &item = p_options[i]; + for (int i = 0; i < fd->options.size(); i++) { + const Dictionary &item = fd->options[i]; if (!item.has("name") || !item.has("values") || !item.has("default")) { continue; } - const String &name = item["name"]; - const Vector<String> &options = item["values"]; - int default_idx = item["default"]; - - event_handler->add_option(pfdc, name, options, default_idx); + event_handler->add_option(pfdc, item["name"], item["values"], item["default_idx"]); } - event_handler->set_root(p_root); + event_handler->set_root(fd->root); pfdc->Release(); DWORD flags; pfd->GetOptions(&flags); - if (p_mode == FILE_DIALOG_MODE_OPEN_FILES) { + if (fd->mode == DisplayServer::FILE_DIALOG_MODE_OPEN_FILES) { flags |= FOS_ALLOWMULTISELECT; } - if (p_mode == FILE_DIALOG_MODE_OPEN_DIR) { + if (fd->mode == DisplayServer::FILE_DIALOG_MODE_OPEN_DIR) { flags |= FOS_PICKFOLDERS; } - if (p_show_hidden) { + if (fd->show_hidden) { flags |= FOS_FORCESHOWHIDDEN; } pfd->SetOptions(flags | FOS_FORCEFILESYSTEM); - pfd->SetTitle((LPCWSTR)p_title.utf16().ptr()); + pfd->SetTitle((LPCWSTR)fd->title.utf16().ptr()); - String dir = ProjectSettings::get_singleton()->globalize_path(p_current_directory); - if (dir == ".") { - dir = OS::get_singleton()->get_executable_path().get_base_dir(); - } - dir = dir.replace("/", "\\"); + String dir = fd->current_directory.replace("/", "\\"); IShellItem *shellitem = nullptr; hr = SHCreateItemFromParsingName((LPCWSTR)dir.utf16().ptr(), nullptr, IID_IShellItem, (void **)&shellitem); @@ -486,16 +548,11 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title pfd->SetFolder(shellitem); } - pfd->SetFileName((LPCWSTR)p_filename.utf16().ptr()); + pfd->SetFileName((LPCWSTR)fd->filename.utf16().ptr()); pfd->SetFileTypes(filters.size(), filters.ptr()); pfd->SetFileTypeIndex(0); - WindowID window_id = _get_focused_window_or_popup(); - if (!windows.has(window_id)) { - window_id = MAIN_WINDOW_ID; - } - - hr = pfd->Show(windows[window_id].hWnd); + hr = pfd->Show(hwnd_dialog); pfd->Unadvise(cookie); Dictionary options = event_handler->get_selected(); @@ -512,7 +569,7 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title if (SUCCEEDED(hr)) { Vector<String> file_names; - if (p_mode == FILE_DIALOG_MODE_OPEN_FILES) { + if (fd->mode == DisplayServer::FILE_DIALOG_MODE_OPEN_FILES) { IShellItemArray *results; hr = static_cast<IFileOpenDialog *>(pfd)->GetResults(&results); if (SUCCEEDED(hr)) { @@ -545,73 +602,148 @@ Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title result->Release(); } } - if (p_callback.is_valid()) { - if (p_options_in_cb) { + if (fd->callback.is_valid()) { + if (fd->options_in_cb) { Variant v_result = true; Variant v_files = file_names; Variant v_index = index; Variant v_opt = options; - Variant ret; - Callable::CallError ce; - const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt }; + const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt }; - p_callback.callp(args, 4, ret, ce); - if (ce.error != Callable::CallError::CALL_OK) { - ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 4, ce))); - } + fd->callback.call_deferredp(cb_args, 4); } else { Variant v_result = true; Variant v_files = file_names; Variant v_index = index; - Variant ret; - Callable::CallError ce; - const Variant *args[3] = { &v_result, &v_files, &v_index }; + const Variant *cb_args[3] = { &v_result, &v_files, &v_index }; - p_callback.callp(args, 3, ret, ce); - if (ce.error != Callable::CallError::CALL_OK) { - ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 3, ce))); - } + fd->callback.call_deferredp(cb_args, 3); } } } else { - if (p_callback.is_valid()) { - if (p_options_in_cb) { + if (fd->callback.is_valid()) { + if (fd->options_in_cb) { Variant v_result = false; Variant v_files = Vector<String>(); - Variant v_index = index; - Variant v_opt = options; - Variant ret; - Callable::CallError ce; - const Variant *args[4] = { &v_result, &v_files, &v_index, &v_opt }; + Variant v_index = 0; + Variant v_opt = Dictionary(); + const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt }; - p_callback.callp(args, 4, ret, ce); - if (ce.error != Callable::CallError::CALL_OK) { - ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 4, ce))); - } + fd->callback.call_deferredp(cb_args, 4); } else { Variant v_result = false; Variant v_files = Vector<String>(); - Variant v_index = index; - Variant ret; - Callable::CallError ce; - const Variant *args[3] = { &v_result, &v_files, &v_index }; + Variant v_index = 0; + const Variant *cb_args[3] = { &v_result, &v_files, &v_index }; - p_callback.callp(args, 3, ret, ce); - if (ce.error != Callable::CallError::CALL_OK) { - ERR_PRINT(vformat("Failed to execute file dialogs callback: %s.", Variant::get_callable_error_text(p_callback, args, 3, ce))); - } + fd->callback.call_deferredp(cb_args, 3); } } } pfd->Release(); - if (prev_focus != INVALID_WINDOW_ID) { - callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(prev_focus); + } else { + if (fd->callback.is_valid()) { + if (fd->options_in_cb) { + Variant v_result = false; + Variant v_files = Vector<String>(); + Variant v_index = 0; + Variant v_opt = Dictionary(); + const Variant *cb_args[4] = { &v_result, &v_files, &v_index, &v_opt }; + + fd->callback.call_deferredp(cb_args, 4); + } else { + Variant v_result = false; + Variant v_files = Vector<String>(); + Variant v_index = 0; + const Variant *cb_args[3] = { &v_result, &v_files, &v_index }; + + fd->callback.call_deferredp(cb_args, 3); + } + } + } + { + MutexLock lock(ds->file_dialog_mutex); + if (hwnd_dialog && ds->file_dialog_wnd.has(hwnd_dialog)) { + IPropertyStore *prop_store; + hr = SHGetPropertyStoreForWindow(hwnd_dialog, IID_IPropertyStore, (void **)&prop_store); + if (hr == S_OK) { + PROPVARIANT val; + PropVariantInit(&val); + prop_store->SetValue(PKEY_AppUserModel_ID, val); + prop_store->Release(); + } + DestroyWindow(hwnd_dialog); + ds->file_dialog_wnd.erase(hwnd_dialog); } + } + UnregisterClassW(L"Engine File Dialog", GetModuleHandle(nullptr)); + CoUninitialize(); + + fd->finished.set(); + + if (fd->window_id != INVALID_WINDOW_ID) { + callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd->window_id); + } +} - return OK; +Error DisplayServerWindows::_file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb) { + _THREAD_SAFE_METHOD_ + + ERR_FAIL_INDEX_V(int(p_mode), FILE_DIALOG_MODE_SAVE_MAX, FAILED); + + WindowID window_id = _get_focused_window_or_popup(); + if (!windows.has(window_id)) { + window_id = MAIN_WINDOW_ID; + } + String appname; + if (Engine::get_singleton()->is_editor_hint()) { + appname = "Godot.GodotEditor." + String(VERSION_BRANCH); } else { - return ERR_CANT_OPEN; + String name = GLOBAL_GET("application/config/name"); + String version = GLOBAL_GET("application/config/version"); + if (version.is_empty()) { + version = "0"; + } + String clean_app_name = name.to_pascal_case(); + for (int i = 0; i < clean_app_name.length(); i++) { + if (!is_ascii_alphanumeric_char(clean_app_name[i]) && clean_app_name[i] != '_' && clean_app_name[i] != '.') { + clean_app_name[i] = '_'; + } + } + clean_app_name = clean_app_name.substr(0, 120 - version.length()).trim_suffix("."); + appname = "Godot." + clean_app_name + "." + version; + } + + FileDialogData *fd = memnew(FileDialogData); + if (window_id != INVALID_WINDOW_ID) { + fd->hwnd_owner = windows[window_id].hWnd; + RECT crect; + GetWindowRect(fd->hwnd_owner, &crect); + fd->wrect = Rect2i(crect.left, crect.top, crect.right - crect.left, crect.bottom - crect.top); + } else { + fd->hwnd_owner = 0; + fd->wrect = Rect2i(CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT); } + fd->appid = appname; + fd->title = p_title; + fd->current_directory = p_current_directory; + fd->root = p_root; + fd->filename = p_filename; + fd->show_hidden = p_show_hidden; + fd->mode = p_mode; + fd->window_id = window_id; + fd->filters = p_filters; + fd->options = p_options; + fd->callback = p_callback; + fd->options_in_cb = p_options_in_cb; + fd->finished.clear(); + fd->close_requested.clear(); + + fd->listener_thread.start(DisplayServerWindows::_thread_fd_monitor, fd); + + file_dialogs.push_back(fd); + + return OK; } void DisplayServerWindows::mouse_set_mode(MouseMode p_mode) { @@ -1305,10 +1437,10 @@ DisplayServer::WindowID DisplayServerWindows::get_window_at_screen_position(cons return INVALID_WINDOW_ID; } -DisplayServer::WindowID DisplayServerWindows::create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect) { +DisplayServer::WindowID DisplayServerWindows::create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect, bool p_exclusive, WindowID p_transient_parent) { _THREAD_SAFE_METHOD_ - WindowID window_id = _create_window(p_mode, p_vsync_mode, p_flags, p_rect); + WindowID window_id = _create_window(p_mode, p_vsync_mode, p_flags, p_rect, p_exclusive, p_transient_parent); ERR_FAIL_COND_V_MSG(window_id == INVALID_WINDOW_ID, INVALID_WINDOW_ID, "Failed to create sub window."); WindowData &wd = windows[window_id]; @@ -1332,13 +1464,15 @@ DisplayServer::WindowID DisplayServerWindows::create_sub_window(WindowMode p_mod wd.is_popup = true; } if (p_flags & WINDOW_FLAG_TRANSPARENT_BIT) { - DWM_BLURBEHIND bb; - ZeroMemory(&bb, sizeof(bb)); - HRGN hRgn = CreateRectRgn(0, 0, -1, -1); - bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION; - bb.hRgnBlur = hRgn; - bb.fEnable = TRUE; - DwmEnableBlurBehindWindow(wd.hWnd, &bb); + if (OS::get_singleton()->is_layered_allowed()) { + DWM_BLURBEHIND bb; + ZeroMemory(&bb, sizeof(bb)); + HRGN hRgn = CreateRectRgn(0, 0, -1, -1); + bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION; + bb.hRgnBlur = hRgn; + bb.fEnable = TRUE; + DwmEnableBlurBehindWindow(wd.hWnd, &bb); + } wd.layered_window = true; } @@ -2008,7 +2142,7 @@ void DisplayServerWindows::window_set_mode(WindowMode p_mode, WindowID p_window) } if (p_mode == WINDOW_MODE_WINDOWED) { - ShowWindow(wd.hWnd, SW_RESTORE); + ShowWindow(wd.hWnd, SW_NORMAL); wd.maximized = false; wd.minimized = false; } @@ -2118,28 +2252,29 @@ void DisplayServerWindows::window_set_flag(WindowFlags p_flag, bool p_enabled, W } break; case WINDOW_FLAG_TRANSPARENT: { if (p_enabled) { - //enable per-pixel alpha - - DWM_BLURBEHIND bb; - ZeroMemory(&bb, sizeof(bb)); - HRGN hRgn = CreateRectRgn(0, 0, -1, -1); - bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION; - bb.hRgnBlur = hRgn; - bb.fEnable = TRUE; - DwmEnableBlurBehindWindow(wd.hWnd, &bb); - + // Enable per-pixel alpha. + if (OS::get_singleton()->is_layered_allowed()) { + DWM_BLURBEHIND bb; + ZeroMemory(&bb, sizeof(bb)); + HRGN hRgn = CreateRectRgn(0, 0, -1, -1); + bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION; + bb.hRgnBlur = hRgn; + bb.fEnable = TRUE; + DwmEnableBlurBehindWindow(wd.hWnd, &bb); + } wd.layered_window = true; } else { - //disable per-pixel alpha + // Disable per-pixel alpha. wd.layered_window = false; - - DWM_BLURBEHIND bb; - ZeroMemory(&bb, sizeof(bb)); - HRGN hRgn = CreateRectRgn(0, 0, -1, -1); - bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION; - bb.hRgnBlur = hRgn; - bb.fEnable = FALSE; - DwmEnableBlurBehindWindow(wd.hWnd, &bb); + if (OS::get_singleton()->is_layered_allowed()) { + DWM_BLURBEHIND bb; + ZeroMemory(&bb, sizeof(bb)); + HRGN hRgn = CreateRectRgn(0, 0, -1, -1); + bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION; + bb.hRgnBlur = hRgn; + bb.fEnable = FALSE; + DwmEnableBlurBehindWindow(wd.hWnd, &bb); + } } } break; case WINDOW_FLAG_NO_FOCUS: { @@ -2910,24 +3045,67 @@ Key DisplayServerWindows::keyboard_get_label_from_physical(Key p_keycode) const return p_keycode; } -String _get_full_layout_name_from_registry(HKL p_layout) { - String id = "SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts\\" + String::num_int64((int64_t)p_layout, 16, false).lpad(8, "0"); +String DisplayServerWindows::_get_keyboard_layout_display_name(const String &p_klid) const { String ret; + HKEY key; + if (RegOpenKeyW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", &key) != ERROR_SUCCESS) { + return String(); + } - HKEY hkey; - WCHAR layout_text[1024]; - memset(layout_text, 0, 1024 * sizeof(WCHAR)); - - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, (LPCWSTR)(id.utf16().get_data()), 0, KEY_QUERY_VALUE, &hkey) != ERROR_SUCCESS) { - return ret; + WCHAR buffer[MAX_PATH] = {}; + DWORD buffer_size = MAX_PATH; + if (RegGetValueW(key, (LPCWSTR)p_klid.utf16().get_data(), L"Layout Display Name", RRF_RT_REG_SZ, nullptr, buffer, &buffer_size) == ERROR_SUCCESS) { + if (load_indirect_string) { + if (load_indirect_string(buffer, buffer, buffer_size, nullptr) == S_OK) { + ret = String::utf16((const char16_t *)buffer, buffer_size); + } + } + } else { + if (RegGetValueW(key, (LPCWSTR)p_klid.utf16().get_data(), L"Layout Text", RRF_RT_REG_SZ, nullptr, buffer, &buffer_size) == ERROR_SUCCESS) { + ret = String::utf16((const char16_t *)buffer, buffer_size); + } } - DWORD buffer = 1024; - DWORD vtype = REG_SZ; - if (RegQueryValueExW(hkey, L"Layout Text", nullptr, &vtype, (LPBYTE)layout_text, &buffer) == ERROR_SUCCESS) { - ret = String::utf16((const char16_t *)layout_text); + RegCloseKey(key); + return ret; +} + +String DisplayServerWindows::_get_klid(HKL p_hkl) const { + String ret; + + WORD device = HIWORD(p_hkl); + if ((device & 0xf000) == 0xf000) { + WORD layout_id = device & 0x0fff; + + HKEY key; + if (RegOpenKeyW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", &key) != ERROR_SUCCESS) { + return String(); + } + + DWORD index = 0; + wchar_t klid_buffer[KL_NAMELENGTH]; + DWORD klid_buffer_size = KL_NAMELENGTH; + while (RegEnumKeyExW(key, index, klid_buffer, &klid_buffer_size, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) { + wchar_t layout_id_buf[MAX_PATH] = {}; + DWORD layout_id_size = MAX_PATH; + if (RegGetValueW(key, klid_buffer, L"Layout Id", RRF_RT_REG_SZ, nullptr, layout_id_buf, &layout_id_size) == ERROR_SUCCESS) { + if (layout_id == String::utf16((char16_t *)layout_id_buf, layout_id_size).hex_to_int()) { + ret = String::utf16((const char16_t *)klid_buffer, klid_buffer_size).lpad(8, "0"); + break; + } + } + klid_buffer_size = KL_NAMELENGTH; + ++index; + } + + RegCloseKey(key); + } else { + if (device == 0) { + device = LOWORD(p_hkl); + } + ret = (String::num_uint64((uint64_t)device, 16, false)).lpad(8, "0"); } - RegCloseKey(hkey); + return ret; } @@ -2939,7 +3117,7 @@ String DisplayServerWindows::keyboard_get_layout_name(int p_index) const { HKL *layouts = (HKL *)memalloc(layout_count * sizeof(HKL)); GetKeyboardLayoutList(layout_count, layouts); - String ret = _get_full_layout_name_from_registry(layouts[p_index]); // Try reading full name from Windows registry, fallback to locale name if failed (e.g. on Wine). + String ret = _get_keyboard_layout_display_name(_get_klid(layouts[p_index])); // Try reading full name from Windows registry, fallback to locale name if failed (e.g. on Wine). if (ret.is_empty()) { WCHAR buf[LOCALE_NAME_MAX_LENGTH]; memset(buf, 0, LOCALE_NAME_MAX_LENGTH * sizeof(WCHAR)); @@ -2975,6 +3153,21 @@ void DisplayServerWindows::process_events() { _process_key_events(); Input::get_singleton()->flush_buffered_events(); } + + LocalVector<List<FileDialogData *>::Element *> to_remove; + for (List<FileDialogData *>::Element *E = file_dialogs.front(); E; E = E->next()) { + FileDialogData *fd = E->get(); + if (fd->finished.is_set()) { + if (fd->listener_thread.is_started()) { + fd->listener_thread.wait_to_finish(); + } + to_remove.push_back(E); + } + } + for (List<FileDialogData *>::Element *E : to_remove) { + memdelete(E->get()); + E->erase(); + } } void DisplayServerWindows::force_process_and_drop_events() { @@ -3807,9 +4000,9 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA case WM_ACTIVATE: { // Activation can happen just after the window has been created, even before the callbacks are set. // Therefore, it's safer to defer the delivery of the event. - if (!windows[window_id].activate_timer_id) { - windows[window_id].activate_timer_id = SetTimer(windows[window_id].hWnd, 1, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); - } + // It's important to set an nIDEvent different from the SetTimer for move_timer_id because + // if the same nIDEvent is passed, the timer is replaced and the same timer_id is returned. + windows[window_id].activate_timer_id = SetTimer(windows[window_id].hWnd, DisplayServerWindows::TIMER_ID_WINDOW_ACTIVATION, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); windows[window_id].activate_state = GET_WM_ACTIVATE_STATE(wParam, lParam); return 0; } break; @@ -4160,6 +4353,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA mm->set_relative_screen_position(mm->get_relative()); old_x = mm->get_position().x; old_y = mm->get_position().y; + if (windows[window_id].window_focused || window_get_active_popup() == window_id) { Input::get_singleton()->parse_input_event(mm); } @@ -4186,13 +4380,128 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA break; } + pointer_button[GET_POINTERID_WPARAM(wParam)] = MouseButton::NONE; windows[window_id].block_mm = true; return 0; } break; case WM_POINTERLEAVE: { + pointer_button[GET_POINTERID_WPARAM(wParam)] = MouseButton::NONE; windows[window_id].block_mm = false; return 0; } break; + case WM_POINTERDOWN: + case WM_POINTERUP: { + if (mouse_mode == MOUSE_MODE_CAPTURED && use_raw_input) { + break; + } + + if ((tablet_get_current_driver() != "winink") || !winink_available) { + break; + } + + uint32_t pointer_id = LOWORD(wParam); + POINTER_INPUT_TYPE pointer_type = PT_POINTER; + if (!win8p_GetPointerType(pointer_id, &pointer_type)) { + break; + } + + if (pointer_type != PT_PEN) { + break; + } + + Ref<InputEventMouseButton> mb; + mb.instantiate(); + mb->set_window_id(window_id); + + BitField<MouseButtonMask> last_button_state = 0; + if (IS_POINTER_FIRSTBUTTON_WPARAM(wParam)) { + last_button_state.set_flag(MouseButtonMask::LEFT); + mb->set_button_index(MouseButton::LEFT); + } + if (IS_POINTER_SECONDBUTTON_WPARAM(wParam)) { + last_button_state.set_flag(MouseButtonMask::RIGHT); + mb->set_button_index(MouseButton::RIGHT); + } + if (IS_POINTER_THIRDBUTTON_WPARAM(wParam)) { + last_button_state.set_flag(MouseButtonMask::MIDDLE); + mb->set_button_index(MouseButton::MIDDLE); + } + if (IS_POINTER_FOURTHBUTTON_WPARAM(wParam)) { + last_button_state.set_flag(MouseButtonMask::MB_XBUTTON1); + mb->set_button_index(MouseButton::MB_XBUTTON1); + } + if (IS_POINTER_FIFTHBUTTON_WPARAM(wParam)) { + last_button_state.set_flag(MouseButtonMask::MB_XBUTTON2); + mb->set_button_index(MouseButton::MB_XBUTTON2); + } + mb->set_button_mask(last_button_state); + + const BitField<WinKeyModifierMask> &mods = _get_mods(); + mb->set_ctrl_pressed(mods.has_flag(WinKeyModifierMask::CTRL)); + mb->set_shift_pressed(mods.has_flag(WinKeyModifierMask::SHIFT)); + mb->set_alt_pressed(mods.has_flag(WinKeyModifierMask::ALT)); + mb->set_meta_pressed(mods.has_flag(WinKeyModifierMask::META)); + + POINT coords; // Client coords. + coords.x = GET_X_LPARAM(lParam); + coords.y = GET_Y_LPARAM(lParam); + + // Note: Handle popup closing here, since mouse event is not emulated and hook will not be called. + uint64_t delta = OS::get_singleton()->get_ticks_msec() - time_since_popup; + if (delta > 250) { + Point2i pos = Point2i(coords.x, coords.y) - _get_screens_origin(); + List<WindowID>::Element *C = nullptr; + List<WindowID>::Element *E = popup_list.back(); + // Find top popup to close. + while (E) { + // Popup window area. + Rect2i win_rect = Rect2i(window_get_position_with_decorations(E->get()), window_get_size_with_decorations(E->get())); + // Area of the parent window, which responsible for opening sub-menu. + Rect2i safe_rect = window_get_popup_safe_rect(E->get()); + if (win_rect.has_point(pos)) { + break; + } else if (safe_rect != Rect2i() && safe_rect.has_point(pos)) { + break; + } else { + C = E; + E = E->prev(); + } + } + if (C) { + _send_window_event(windows[C->get()], DisplayServerWindows::WINDOW_EVENT_CLOSE_REQUEST); + } + } + + int64_t pen_id = GET_POINTERID_WPARAM(wParam); + if (uMsg == WM_POINTERDOWN) { + mb->set_pressed(true); + if (pointer_down_time.has(pen_id) && (pointer_prev_button[pen_id] == mb->get_button_index()) && (ABS(coords.y - pointer_last_pos[pen_id].y) < GetSystemMetrics(SM_CYDOUBLECLK)) && GetMessageTime() - pointer_down_time[pen_id] < (LONG)GetDoubleClickTime()) { + mb->set_double_click(true); + pointer_down_time[pen_id] = 0; + } else { + pointer_down_time[pen_id] = GetMessageTime(); + pointer_prev_button[pen_id] = mb->get_button_index(); + pointer_last_pos[pen_id] = Vector2(coords.x, coords.y); + } + pointer_button[pen_id] = mb->get_button_index(); + } else { + if (!pointer_button.has(pen_id)) { + return 0; + } + mb->set_pressed(false); + mb->set_button_index(pointer_button[pen_id]); + pointer_button[pen_id] = MouseButton::NONE; + } + + ScreenToClient(windows[window_id].hWnd, &coords); + + mb->set_position(Vector2(coords.x, coords.y)); + mb->set_global_position(Vector2(coords.x, coords.y)); + + Input::get_singleton()->parse_input_event(mb); + + return 0; + } break; case WM_POINTERUPDATE: { if (mouse_mode == MOUSE_MODE_CAPTURED && use_raw_input) { break; @@ -4270,7 +4579,23 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA mm->set_alt_pressed(mods.has_flag(WinKeyModifierMask::ALT)); mm->set_meta_pressed(mods.has_flag(WinKeyModifierMask::META)); - mm->set_button_mask(mouse_get_button_state()); + BitField<MouseButtonMask> last_button_state = 0; + if (IS_POINTER_FIRSTBUTTON_WPARAM(wParam)) { + last_button_state.set_flag(MouseButtonMask::LEFT); + } + if (IS_POINTER_SECONDBUTTON_WPARAM(wParam)) { + last_button_state.set_flag(MouseButtonMask::RIGHT); + } + if (IS_POINTER_THIRDBUTTON_WPARAM(wParam)) { + last_button_state.set_flag(MouseButtonMask::MIDDLE); + } + if (IS_POINTER_FOURTHBUTTON_WPARAM(wParam)) { + last_button_state.set_flag(MouseButtonMask::MB_XBUTTON1); + } + if (IS_POINTER_FIFTHBUTTON_WPARAM(wParam)) { + last_button_state.set_flag(MouseButtonMask::MB_XBUTTON2); + } + mm->set_button_mask(last_button_state); POINT coords; // Client coords. coords.x = GET_X_LPARAM(lParam); @@ -4441,6 +4766,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA mm->set_position(mm->get_position() - window_get_position(receiving_window_id) + window_get_position(window_id)); mm->set_global_position(mm->get_position()); } + Input::get_singleton()->parse_input_event(mm); } break; @@ -4685,16 +5011,16 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA rect_changed = true; } #if defined(RD_ENABLED) - if (rendering_context && window.context_created) { + if (window.create_completed && rendering_context && window.context_created) { // Note: Trigger resize event to update swapchains when window is minimized/restored, even if size is not changed. rendering_context->window_set_size(window_id, window.width, window.height); } #endif #if defined(GLES3_ENABLED) - if (gl_manager_native) { + if (window.create_completed && gl_manager_native) { gl_manager_native->window_resize(window_id, window.width, window.height); } - if (gl_manager_angle) { + if (window.create_completed && gl_manager_angle) { gl_manager_angle->window_resize(window_id, window.width, window.height); } #endif @@ -4727,7 +5053,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA case WM_ENTERSIZEMOVE: { Input::get_singleton()->release_pressed_events(); - windows[window_id].move_timer_id = SetTimer(windows[window_id].hWnd, 1, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); + windows[window_id].move_timer_id = SetTimer(windows[window_id].hWnd, DisplayServerWindows::TIMER_ID_MOVE_REDRAW, USER_TIMER_MINIMUM, (TIMERPROC) nullptr); } break; case WM_EXITSIZEMOVE: { KillTimer(windows[window_id].hWnd, windows[window_id].move_timer_id); @@ -5159,7 +5485,7 @@ void DisplayServerWindows::_update_tablet_ctx(const String &p_old_driver, const } } -DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect) { +DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect, bool p_exclusive, WindowID p_transient_parent) { DWORD dwExStyle; DWORD dwStyle; @@ -5209,6 +5535,20 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, WindowID id = window_id_counter; { + WindowData *wd_transient_parent = nullptr; + HWND owner_hwnd = nullptr; + if (p_transient_parent != INVALID_WINDOW_ID) { + if (!windows.has(p_transient_parent)) { + ERR_PRINT("Condition \"!windows.has(p_transient_parent)\" is true."); + p_transient_parent = INVALID_WINDOW_ID; + } else { + wd_transient_parent = &windows[p_transient_parent]; + if (p_exclusive) { + owner_hwnd = wd_transient_parent->hWnd; + } + } + } + WindowData &wd = windows[id]; wd.hWnd = CreateWindowExW( @@ -5219,7 +5559,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, WindowRect.top, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top, - nullptr, + owner_hwnd, nullptr, hInstance, // tunnel the WindowData we need to handle creation message @@ -5241,11 +5581,20 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, wd.pre_fs_valid = true; } + wd.exclusive = p_exclusive; + if (wd_transient_parent) { + wd.transient_parent = p_transient_parent; + wd_transient_parent->transient_children.insert(id); + } + if (is_dark_mode_supported() && dark_title_available) { BOOL value = is_dark_mode(); ::DwmSetWindowAttribute(wd.hWnd, use_legacy_dark_mode_before_20H1 ? DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 : DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value)); } + RECT real_client_rect; + GetClientRect(wd.hWnd, &real_client_rect); + #ifdef RD_ENABLED if (rendering_context) { union { @@ -5275,7 +5624,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, return INVALID_WINDOW_ID; } - rendering_context->window_set_size(id, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top); + rendering_context->window_set_size(id, real_client_rect.right - real_client_rect.left, real_client_rect.bottom - real_client_rect.top); rendering_context->window_set_vsync_mode(id, p_vsync_mode); wd.context_created = true; } @@ -5283,7 +5632,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, #ifdef GLES3_ENABLED if (gl_manager_native) { - if (gl_manager_native->window_create(id, wd.hWnd, hInstance, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top) != OK) { + if (gl_manager_native->window_create(id, wd.hWnd, hInstance, real_client_rect.right - real_client_rect.left, real_client_rect.bottom - real_client_rect.top) != OK) { memdelete(gl_manager_native); gl_manager_native = nullptr; windows.erase(id); @@ -5293,7 +5642,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, } if (gl_manager_angle) { - if (gl_manager_angle->window_create(id, nullptr, wd.hWnd, WindowRect.right - WindowRect.left, WindowRect.bottom - WindowRect.top) != OK) { + if (gl_manager_angle->window_create(id, nullptr, wd.hWnd, real_client_rect.right - real_client_rect.left, real_client_rect.bottom - real_client_rect.top) != OK) { memdelete(gl_manager_angle); gl_manager_angle = nullptr; windows.erase(id); @@ -5355,7 +5704,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, PROPVARIANT val; String appname; if (Engine::get_singleton()->is_editor_hint()) { - appname = "Godot.GodotEditor." + String(VERSION_BRANCH); + appname = "Godot.GodotEditor." + String(VERSION_FULL_CONFIG); } else { String name = GLOBAL_GET("application/config/name"); String version = GLOBAL_GET("application/config/version"); @@ -5402,12 +5751,15 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, SetWindowPos(wd.hWnd, HWND_TOP, srect.position.x, srect.position.y, srect.size.width, srect.size.height, SWP_NOZORDER | SWP_NOACTIVATE); } + wd.create_completed = true; window_id_counter++; } return id; } +BitField<DisplayServerWindows::DriverID> DisplayServerWindows::tested_drivers = 0; + // WinTab API. bool DisplayServerWindows::wintab_available = false; WTOpenPtr DisplayServerWindows::wintab_WTOpen = nullptr; @@ -5432,6 +5784,9 @@ GetPointerPenInfoPtr DisplayServerWindows::win8p_GetPointerPenInfo = nullptr; LogicalToPhysicalPointForPerMonitorDPIPtr DisplayServerWindows::win81p_LogicalToPhysicalPointForPerMonitorDPI = nullptr; PhysicalToLogicalPointForPerMonitorDPIPtr DisplayServerWindows::win81p_PhysicalToLogicalPointForPerMonitorDPI = nullptr; +// Shell API, +SHLoadIndirectStringPtr DisplayServerWindows::load_indirect_string = nullptr; + Vector2i _get_device_ids(const String &p_device_name) { if (p_device_name.is_empty()) { return Vector2i(); @@ -5494,12 +5849,6 @@ Vector2i _get_device_ids(const String &p_device_name) { return ids; } -typedef enum _SHC_PROCESS_DPI_AWARENESS { - SHC_PROCESS_DPI_UNAWARE = 0, - SHC_PROCESS_SYSTEM_DPI_AWARE = 1, - SHC_PROCESS_PER_MONITOR_DPI_AWARE = 2 -} SHC_PROCESS_DPI_AWARENESS; - bool DisplayServerWindows::is_dark_mode_supported() const { return ux_theme_available; } @@ -5567,6 +5916,8 @@ void DisplayServerWindows::tablet_set_current_driver(const String &p_driver) { DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, Error &r_error) { KeyMappingWindows::initialize(); + tested_drivers.clear(); + drop_events = false; key_event_pos = 0; @@ -5605,6 +5956,12 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win FreeLibrary(nt_lib); } + // Load Shell API. + HMODULE shellapi_lib = LoadLibraryW(L"shlwapi.dll"); + if (shellapi_lib) { + load_indirect_string = (SHLoadIndirectStringPtr)GetProcAddress(shellapi_lib, "SHLoadIndirectString"); + } + // Load UXTheme, available on Windows 10+ only. if (os_ver.dwBuildNumber >= 10240) { HMODULE ux_theme_lib = LoadLibraryW(L"uxtheme.dll"); @@ -5729,7 +6086,6 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win wc.lpszClassName = L"Engine"; if (!RegisterClassExW(&wc)) { - MessageBoxW(nullptr, L"Failed To Register The Window Class.", L"ERROR", MB_OK | MB_ICONEXCLAMATION); r_error = ERR_UNAVAILABLE; return; } @@ -5740,33 +6096,83 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win #if defined(VULKAN_ENABLED) if (rendering_driver == "vulkan") { rendering_context = memnew(RenderingContextDriverVulkanWindows); + tested_drivers.set_flag(DRIVER_ID_RD_VULKAN); } #endif #if defined(D3D12_ENABLED) if (rendering_driver == "d3d12") { rendering_context = memnew(RenderingContextDriverD3D12); + tested_drivers.set_flag(DRIVER_ID_RD_D3D12); } #endif if (rendering_context) { if (rendering_context->initialize() != OK) { - memdelete(rendering_context); - rendering_context = nullptr; - r_error = ERR_UNAVAILABLE; - return; + bool failed = true; +#if defined(VULKAN_ENABLED) + bool fallback_to_vulkan = GLOBAL_GET("rendering/rendering_device/fallback_to_vulkan"); + if (failed && fallback_to_vulkan && rendering_driver != "vulkan") { + memdelete(rendering_context); + rendering_context = memnew(RenderingContextDriverVulkanWindows); + tested_drivers.set_flag(DRIVER_ID_RD_VULKAN); + if (rendering_context->initialize() == OK) { + WARN_PRINT("Your video card drivers seem not to support Direct3D 12, switching to Vulkan."); + rendering_driver = "vulkan"; + failed = false; + } + } +#endif +#if defined(D3D12_ENABLED) + bool fallback_to_d3d12 = GLOBAL_GET("rendering/rendering_device/fallback_to_d3d12"); + if (failed && fallback_to_d3d12 && rendering_driver != "d3d12") { + memdelete(rendering_context); + rendering_context = memnew(RenderingContextDriverD3D12); + tested_drivers.set_flag(DRIVER_ID_RD_D3D12); + if (rendering_context->initialize() == OK) { + WARN_PRINT("Your video card drivers seem not to support Vulkan, switching to Direct3D 12."); + rendering_driver = "d3d12"; + failed = false; + } + } +#endif + if (failed) { + memdelete(rendering_context); + rendering_context = nullptr; + r_error = ERR_UNAVAILABLE; + return; + } } } #endif // Init context and rendering device #if defined(GLES3_ENABLED) -#if defined(__arm__) || defined(__aarch64__) || defined(_M_ARM) || defined(_M_ARM64) - // There's no native OpenGL drivers on Windows for ARM, switch to ANGLE over DX. + bool fallback = GLOBAL_GET("rendering/gl_compatibility/fallback_to_angle"); + bool show_warning = true; + if (rendering_driver == "opengl3") { - rendering_driver = "opengl3_angle"; + // There's no native OpenGL drivers on Windows for ARM, always enable fallback. +#if defined(__arm__) || defined(__aarch64__) || defined(_M_ARM) || defined(_M_ARM64) + fallback = true; + show_warning = false; +#else + typedef BOOL(WINAPI * IsWow64Process2Ptr)(HANDLE, USHORT *, USHORT *); + + IsWow64Process2Ptr IsWow64Process2 = (IsWow64Process2Ptr)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "IsWow64Process2"); + if (IsWow64Process2) { + USHORT process_arch = 0; + USHORT machine_arch = 0; + if (!IsWow64Process2(GetCurrentProcess(), &process_arch, &machine_arch)) { + machine_arch = 0; + } + if (machine_arch == 0xAA64) { + fallback = true; + show_warning = false; + } + } +#endif } -#elif defined(EGL_STATIC) - bool fallback = GLOBAL_GET("rendering/gl_compatibility/fallback_to_angle"); + if (fallback && (rendering_driver == "opengl3")) { Dictionary gl_info = detect_wgl(); @@ -5792,14 +6198,17 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } if (force_angle || (gl_info["version"].operator int() < 30003)) { - WARN_PRINT("Your video card drivers seem not to support the required OpenGL 3.3 version, switching to ANGLE."); + tested_drivers.set_flag(DRIVER_ID_COMPAT_OPENGL3); + if (show_warning) { + WARN_PRINT("Your video card drivers seem not to support the required OpenGL 3.3 version, switching to ANGLE."); + } rendering_driver = "opengl3_angle"; } } -#endif if (rendering_driver == "opengl3") { gl_manager_native = memnew(GLManagerNative_Windows); + tested_drivers.set_flag(DRIVER_ID_COMPAT_OPENGL3); if (gl_manager_native->initialize() != OK) { memdelete(gl_manager_native); @@ -5812,6 +6221,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } if (rendering_driver == "opengl3_angle") { gl_manager_angle = memnew(GLManagerANGLE_Windows); + tested_drivers.set_flag(DRIVER_ID_COMPAT_ANGLE_D3D11); if (gl_manager_angle->initialize() != OK) { memdelete(gl_manager_angle); @@ -5826,7 +6236,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win String appname; if (Engine::get_singleton()->is_editor_hint()) { - appname = "Godot.GodotEditor." + String(VERSION_BRANCH); + appname = "Godot.GodotEditor." + String(VERSION_FULL_CONFIG); } else { String name = GLOBAL_GET("application/config/name"); String version = GLOBAL_GET("application/config/version"); @@ -5841,6 +6251,17 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win } clean_app_name = clean_app_name.substr(0, 120 - version.length()).trim_suffix("."); appname = "Godot." + clean_app_name + "." + version; + +#ifndef TOOLS_ENABLED + // Set for exported projects only. + HKEY key; + if (RegOpenKeyW(HKEY_CURRENT_USER_LOCAL_SETTINGS, L"Software\\Microsoft\\Windows\\Shell\\MuiCache", &key) == ERROR_SUCCESS) { + Char16String cs_name = name.utf16(); + String value_name = OS::get_singleton()->get_executable_path().replace("/", "\\") + ".FriendlyAppName"; + RegSetValueExW(key, (LPCWSTR)value_name.utf16().get_data(), 0, REG_SZ, (const BYTE *)cs_name.get_data(), cs_name.size() * sizeof(WCHAR)); + RegCloseKey(key); + } +#endif } SetCurrentProcessExplicitAppUserModelID((PCWSTR)appname.utf16().get_data()); @@ -5857,7 +6278,7 @@ DisplayServerWindows::DisplayServerWindows(const String &p_rendering_driver, Win window_position = scr_rect.position + (scr_rect.size - p_resolution) / 2; } - WindowID main_window = _create_window(p_mode, p_vsync_mode, p_flags, Rect2i(window_position, p_resolution)); + WindowID main_window = _create_window(p_mode, p_vsync_mode, p_flags, Rect2i(window_position, p_resolution), false, INVALID_WINDOW_ID); ERR_FAIL_COND_MSG(main_window == INVALID_WINDOW_ID, "Failed to create main window."); joypad = new JoypadWindows(&windows[MAIN_WINDOW_ID].hWnd); @@ -5934,32 +6355,41 @@ Vector<String> DisplayServerWindows::get_rendering_drivers_func() { DisplayServer *DisplayServerWindows::create_func(const String &p_rendering_driver, WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Vector2i *p_position, const Vector2i &p_resolution, int p_screen, Context p_context, Error &r_error) { DisplayServer *ds = memnew(DisplayServerWindows(p_rendering_driver, p_mode, p_vsync_mode, p_flags, p_position, p_resolution, p_screen, p_context, r_error)); if (r_error != OK) { - if (p_rendering_driver == "vulkan") { - String executable_name = OS::get_singleton()->get_executable_path().get_file(); - OS::get_singleton()->alert( - vformat("Your video card drivers seem not to support the required Vulkan version.\n\n" - "If possible, consider updating your video card drivers or using the OpenGL 3 driver.\n\n" - "You can enable the OpenGL 3 driver by starting the engine from the\n" - "command line with the command:\n\n \"%s\" --rendering-driver opengl3\n\n" - "If you have recently updated your video card drivers, try rebooting.", - executable_name), - "Unable to initialize Vulkan video driver"); - } else if (p_rendering_driver == "d3d12") { + if (tested_drivers == 0) { + OS::get_singleton()->alert("Failed to register the window class.", "Unable to initialize DisplayServer"); + } else if (tested_drivers.has_flag(DRIVER_ID_RD_VULKAN) || tested_drivers.has_flag(DRIVER_ID_RD_D3D12)) { + Vector<String> drivers; + if (tested_drivers.has_flag(DRIVER_ID_RD_VULKAN)) { + drivers.push_back("Vulkan"); + } + if (tested_drivers.has_flag(DRIVER_ID_RD_D3D12)) { + drivers.push_back("Direct3D 12"); + } String executable_name = OS::get_singleton()->get_executable_path().get_file(); OS::get_singleton()->alert( - vformat("Your video card drivers seem not to support the required DirectX 12 version.\n\n" + vformat("Your video card drivers seem not to support the required %s version.\n\n" "If possible, consider updating your video card drivers or using the OpenGL 3 driver.\n\n" "You can enable the OpenGL 3 driver by starting the engine from the\n" "command line with the command:\n\n \"%s\" --rendering-driver opengl3\n\n" "If you have recently updated your video card drivers, try rebooting.", + String(" or ").join(drivers), executable_name), - "Unable to initialize DirectX 12 video driver"); + "Unable to initialize video driver"); } else { + Vector<String> drivers; + if (tested_drivers.has_flag(DRIVER_ID_COMPAT_OPENGL3)) { + drivers.push_back("OpenGL 3.3"); + } + if (tested_drivers.has_flag(DRIVER_ID_COMPAT_ANGLE_D3D11)) { + drivers.push_back("Direct3D 11"); + } OS::get_singleton()->alert( - "Your video card drivers seem not to support the required OpenGL 3.3 version.\n\n" - "If possible, consider updating your video card drivers.\n\n" - "If you have recently updated your video card drivers, try rebooting.", - "Unable to initialize OpenGL video driver"); + vformat( + "Your video card drivers seem not to support the required %s version.\n\n" + "If possible, consider updating your video card drivers.\n\n" + "If you have recently updated your video card drivers, try rebooting.", + String(" or ").join(drivers)), + "Unable to initialize video driver"); } } return ds; @@ -5970,6 +6400,20 @@ void DisplayServerWindows::register_windows_driver() { } DisplayServerWindows::~DisplayServerWindows() { + LocalVector<List<FileDialogData *>::Element *> to_remove; + for (List<FileDialogData *>::Element *E = file_dialogs.front(); E; E = E->next()) { + FileDialogData *fd = E->get(); + if (fd->listener_thread.is_started()) { + fd->close_requested.set(); + fd->listener_thread.wait_to_finish(); + } + to_remove.push_back(E); + } + for (List<FileDialogData *>::Element *E : to_remove) { + memdelete(E->get()); + E->erase(); + } + delete joypad; touch_state.clear(); diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 382f18c239..8a2f9f81a6 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -207,6 +207,50 @@ typedef UINT32 PEN_MASK; #define POINTER_MESSAGE_FLAG_FIRSTBUTTON 0x00000010 #endif +#ifndef POINTER_MESSAGE_FLAG_SECONDBUTTON +#define POINTER_MESSAGE_FLAG_SECONDBUTTON 0x00000020 +#endif + +#ifndef POINTER_MESSAGE_FLAG_THIRDBUTTON +#define POINTER_MESSAGE_FLAG_THIRDBUTTON 0x00000040 +#endif + +#ifndef POINTER_MESSAGE_FLAG_FOURTHBUTTON +#define POINTER_MESSAGE_FLAG_FOURTHBUTTON 0x00000080 +#endif + +#ifndef POINTER_MESSAGE_FLAG_FIFTHBUTTON +#define POINTER_MESSAGE_FLAG_FIFTHBUTTON 0x00000100 +#endif + +#ifndef IS_POINTER_FLAG_SET_WPARAM +#define IS_POINTER_FLAG_SET_WPARAM(wParam, flag) (((DWORD)HIWORD(wParam) & (flag)) == (flag)) +#endif + +#ifndef IS_POINTER_FIRSTBUTTON_WPARAM +#define IS_POINTER_FIRSTBUTTON_WPARAM(wParam) IS_POINTER_FLAG_SET_WPARAM(wParam, POINTER_MESSAGE_FLAG_FIRSTBUTTON) +#endif + +#ifndef IS_POINTER_SECONDBUTTON_WPARAM +#define IS_POINTER_SECONDBUTTON_WPARAM(wParam) IS_POINTER_FLAG_SET_WPARAM(wParam, POINTER_MESSAGE_FLAG_SECONDBUTTON) +#endif + +#ifndef IS_POINTER_THIRDBUTTON_WPARAM +#define IS_POINTER_THIRDBUTTON_WPARAM(wParam) IS_POINTER_FLAG_SET_WPARAM(wParam, POINTER_MESSAGE_FLAG_THIRDBUTTON) +#endif + +#ifndef IS_POINTER_FOURTHBUTTON_WPARAM +#define IS_POINTER_FOURTHBUTTON_WPARAM(wParam) IS_POINTER_FLAG_SET_WPARAM(wParam, POINTER_MESSAGE_FLAG_FOURTHBUTTON) +#endif + +#ifndef IS_POINTER_FIFTHBUTTON_WPARAM +#define IS_POINTER_FIFTHBUTTON_WPARAM(wParam) IS_POINTER_FLAG_SET_WPARAM(wParam, POINTER_MESSAGE_FLAG_FIFTHBUTTON) +#endif + +#ifndef GET_POINTERID_WPARAM +#define GET_POINTERID_WPARAM(wParam) (LOWORD(wParam)) +#endif + #if WINVER < 0x0602 enum tagPOINTER_INPUT_TYPE { PT_POINTER = 0x00000001, @@ -274,10 +318,19 @@ typedef struct tagPOINTER_PEN_INFO { #define WM_POINTERLEAVE 0x024A #endif +#ifndef WM_POINTERDOWN +#define WM_POINTERDOWN 0x0246 +#endif + +#ifndef WM_POINTERUP +#define WM_POINTERUP 0x0247 +#endif + typedef BOOL(WINAPI *GetPointerTypePtr)(uint32_t p_id, POINTER_INPUT_TYPE *p_type); typedef BOOL(WINAPI *GetPointerPenInfoPtr)(uint32_t p_id, POINTER_PEN_INFO *p_pen_info); typedef BOOL(WINAPI *LogicalToPhysicalPointForPerMonitorDPIPtr)(HWND hwnd, LPPOINT lpPoint); typedef BOOL(WINAPI *PhysicalToLogicalPointForPerMonitorDPIPtr)(HWND hwnd, LPPOINT lpPoint); +typedef HRESULT(WINAPI *SHLoadIndirectStringPtr)(PCWSTR pszSource, PWSTR pszOutBuf, UINT cchOutBuf, void **ppvReserved); typedef struct { BYTE bWidth; // Width, in pixels, of the image @@ -297,6 +350,12 @@ typedef struct { ICONDIRENTRY idEntries[1]; // An entry for each image (idCount of 'em) } ICONDIR, *LPICONDIR; +typedef enum _SHC_PROCESS_DPI_AWARENESS { + SHC_PROCESS_DPI_UNAWARE = 0, + SHC_PROCESS_SYSTEM_DPI_AWARE = 1, + SHC_PROCESS_PER_MONITOR_DPI_AWARE = 2, +} SHC_PROCESS_DPI_AWARENESS; + class DisplayServerWindows : public DisplayServer { // No need to register with GDCLASS, it's platform-specific and nothing is added. @@ -328,10 +387,26 @@ class DisplayServerWindows : public DisplayServer { static LogicalToPhysicalPointForPerMonitorDPIPtr win81p_LogicalToPhysicalPointForPerMonitorDPI; static PhysicalToLogicalPointForPerMonitorDPIPtr win81p_PhysicalToLogicalPointForPerMonitorDPI; + // Shell API + static SHLoadIndirectStringPtr load_indirect_string; + void _update_tablet_ctx(const String &p_old_driver, const String &p_new_driver); String tablet_driver; Vector<String> tablet_drivers; + enum DriverID { + DRIVER_ID_COMPAT_OPENGL3 = 1 << 0, + DRIVER_ID_COMPAT_ANGLE_D3D11 = 1 << 1, + DRIVER_ID_RD_VULKAN = 1 << 2, + DRIVER_ID_RD_D3D12 = 1 << 3, + }; + static BitField<DriverID> tested_drivers; + + enum TimerID { + TIMER_ID_MOVE_REDRAW = 1, + TIMER_ID_WINDOW_ACTIVATION = 2, + }; + enum { KEY_EVENT_BUFFER_SIZE = 512 }; @@ -380,6 +455,7 @@ class DisplayServerWindows : public DisplayServer { Vector<Vector2> mpath; + bool create_completed = false; bool pre_fs_valid = false; RECT pre_fs_rect; bool maximized = false; @@ -456,7 +532,7 @@ class DisplayServerWindows : public DisplayServer { uint64_t time_since_popup = 0; Ref<Image> icon; - WindowID _create_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect); + WindowID _create_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect, bool p_exclusive, WindowID p_transient_parent); WindowID window_id_counter = MAIN_WINDOW_ID; RBMap<WindowID, WindowData> windows; @@ -474,6 +550,36 @@ class DisplayServerWindows : public DisplayServer { IndicatorID indicator_id_counter = 0; HashMap<IndicatorID, IndicatorData> indicators; + struct FileDialogData { + HWND hwnd_owner = 0; + Rect2i wrect; + String appid; + String title; + String current_directory; + String root; + String filename; + bool show_hidden = false; + DisplayServer::FileDialogMode mode = FileDialogMode::FILE_DIALOG_MODE_OPEN_ANY; + Vector<String> filters; + TypedArray<Dictionary> options; + WindowID window_id = DisplayServer::INVALID_WINDOW_ID; + Callable callback; + bool options_in_cb = false; + Thread listener_thread; + SafeFlag close_requested; + SafeFlag finished; + }; + Mutex file_dialog_mutex; + List<FileDialogData *> file_dialogs; + HashMap<HWND, FileDialogData *> file_dialog_wnd; + + static void _thread_fd_monitor(void *p_ud); + + HashMap<int64_t, MouseButton> pointer_prev_button; + HashMap<int64_t, MouseButton> pointer_button; + HashMap<int64_t, LONG> pointer_down_time; + HashMap<int64_t, Vector2> pointer_last_pos; + void _send_window_event(const WindowData &wd, WindowEvent p_event); void _get_window_style(bool p_main_window, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, bool p_maximized, bool p_maximized_fs, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex); @@ -526,7 +632,11 @@ class DisplayServerWindows : public DisplayServer { Error _file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback, bool p_options_in_cb); + String _get_keyboard_layout_display_name(const String &p_klid) const; + String _get_klid(HKL p_hkl) const; + public: + LRESULT WndProcFileDialog(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); LRESULT MouseProc(int code, WPARAM wParam, LPARAM lParam); @@ -583,7 +693,7 @@ public: virtual Vector<DisplayServer::WindowID> get_window_list() const override; - virtual WindowID create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i()) override; + virtual WindowID create_sub_window(WindowMode p_mode, VSyncMode p_vsync_mode, uint32_t p_flags, const Rect2i &p_rect = Rect2i(), bool p_exclusive = false, WindowID p_transient_parent = INVALID_WINDOW_ID) override; virtual void show_window(WindowID p_window) override; virtual void delete_sub_window(WindowID p_window) override; diff --git a/platform/windows/doc_classes/EditorExportPlatformWindows.xml b/platform/windows/doc_classes/EditorExportPlatformWindows.xml index 06b272c10e..9e2db756ce 100644 --- a/platform/windows/doc_classes/EditorExportPlatformWindows.xml +++ b/platform/windows/doc_classes/EditorExportPlatformWindows.xml @@ -26,7 +26,7 @@ If set to [code]1[/code], ANGLE libraries are exported with the exported application. If set to [code]0[/code], ANGLE libraries are exported only if [member ProjectSettings.rendering/gl_compatibility/driver] is set to [code]"opengl3_angle"[/code]. </member> <member name="application/export_d3d12" type="int" setter="" getter=""> - If set to [code]1[/code], Direct3D 12 runtime (DXIL, Agility SDK, PIX) libraries are exported with the exported application. If set to [code]0[/code], Direct3D 12 libraries are exported only if [member ProjectSettings.rendering/rendering_device/driver] is set to [code]"d3d12"[/code]. + If set to [code]1[/code], the Direct3D 12 runtime libraries (Agility SDK, PIX) are exported with the exported application. If set to [code]0[/code], Direct3D 12 libraries are exported only if [member ProjectSettings.rendering/rendering_device/driver] is set to [code]"d3d12"[/code]. </member> <member name="application/file_description" type="String" setter="" getter=""> File description to be presented to users. Required. See [url=https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block]StringFileInfo[/url]. diff --git a/platform/windows/export/export_plugin.cpp b/platform/windows/export/export_plugin.cpp index 6ce9d27dc5..b465bd4ecd 100644 --- a/platform/windows/export/export_plugin.cpp +++ b/platform/windows/export/export_plugin.cpp @@ -187,6 +187,12 @@ Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset> template_path = template_path.strip_edges(); if (template_path.is_empty()) { template_path = find_export_template(get_template_file_name(p_debug ? "debug" : "release", arch)); + } else { + String exe_arch = _get_exe_arch(template_path); + if (arch != exe_arch) { + add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Templates"), vformat(TTR("Mismatching custom export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch)); + return ERR_CANT_CREATE; + } } int export_angle = p_preset->get("application/export_angle"); @@ -208,18 +214,14 @@ Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset> int export_d3d12 = p_preset->get("application/export_d3d12"); bool agility_sdk_multiarch = p_preset->get("application/d3d12_agility_sdk_multiarch"); - bool include_dxil_libs = false; + bool include_d3d12_extra_libs = false; if (export_d3d12 == 0) { - include_dxil_libs = (String(GLOBAL_GET("rendering/rendering_device/driver.windows")) == "d3d12") && (String(GLOBAL_GET("rendering/renderer/rendering_method")) != "gl_compatibility"); + include_d3d12_extra_libs = (String(GLOBAL_GET("rendering/rendering_device/driver.windows")) == "d3d12") && (String(GLOBAL_GET("rendering/renderer/rendering_method")) != "gl_compatibility"); } else if (export_d3d12 == 1) { - include_dxil_libs = true; + include_d3d12_extra_libs = true; } - if (include_dxil_libs) { + if (include_d3d12_extra_libs) { Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); - if (da->file_exists(template_path.get_base_dir().path_join("dxil." + arch + ".dll"))) { - da->make_dir_recursive(p_path.get_base_dir().path_join(arch)); - da->copy(template_path.get_base_dir().path_join("dxil." + arch + ".dll"), p_path.get_base_dir().path_join(arch).path_join("dxil.dll"), get_chmod_flags()); - } if (da->file_exists(template_path.get_base_dir().path_join("D3D12Core." + arch + ".dll"))) { if (agility_sdk_multiarch) { da->make_dir_recursive(p_path.get_base_dir().path_join(arch)); @@ -757,9 +759,26 @@ Error EditorExportPlatformWindows::_code_sign(const Ref<EditorExportPreset> &p_p } bool EditorExportPlatformWindows::has_valid_export_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error, bool &r_missing_templates, bool p_debug) const { - String err = ""; + String err; bool valid = EditorExportPlatformPC::has_valid_export_configuration(p_preset, err, r_missing_templates, p_debug); + String custom_debug = p_preset->get("custom_template/debug").operator String().strip_edges(); + String custom_release = p_preset->get("custom_template/release").operator String().strip_edges(); + String arch = p_preset->get("binary_format/architecture"); + + if (!custom_debug.is_empty() && FileAccess::exists(custom_debug)) { + String exe_arch = _get_exe_arch(custom_debug); + if (arch != exe_arch) { + err += vformat(TTR("Mismatching custom debug export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch) + "\n"; + } + } + if (!custom_release.is_empty() && FileAccess::exists(custom_release)) { + String exe_arch = _get_exe_arch(custom_release); + if (arch != exe_arch) { + err += vformat(TTR("Mismatching custom release export template executable architecture: found \"%s\", expected \"%s\"."), exe_arch, arch) + "\n"; + } + } + String rcedit_path = EDITOR_GET("export/windows/rcedit"); if (p_preset->get("application/modify_resources") && rcedit_path.is_empty()) { err += TTR("The rcedit tool must be configured in the Editor Settings (Export > Windows > rcedit) to change the icon or app information data.") + "\n"; @@ -773,7 +792,7 @@ bool EditorExportPlatformWindows::has_valid_export_configuration(const Ref<Edito } bool EditorExportPlatformWindows::has_valid_project_configuration(const Ref<EditorExportPreset> &p_preset, String &r_error) const { - String err = ""; + String err; bool valid = true; List<ExportOption> options; @@ -797,6 +816,43 @@ bool EditorExportPlatformWindows::has_valid_project_configuration(const Ref<Edit return valid; } +String EditorExportPlatformWindows::_get_exe_arch(const String &p_path) const { + Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ); + if (f.is_null()) { + return "invalid"; + } + + // Jump to the PE header and check the magic number. + { + f->seek(0x3c); + uint32_t pe_pos = f->get_32(); + + f->seek(pe_pos); + uint32_t magic = f->get_32(); + if (magic != 0x00004550) { + return "invalid"; + } + } + + // Process header. + uint16_t machine = f->get_16(); + f->close(); + + switch (machine) { + case 0x014c: + return "x86_32"; + case 0x8664: + return "x86_64"; + case 0x01c0: + case 0x01c4: + return "arm32"; + case 0xaa64: + return "arm64"; + default: + return "unknown"; + } +} + Error EditorExportPlatformWindows::fixup_embedded_pck(const String &p_path, int64_t p_embedded_start, int64_t p_embedded_size) { // Patch the header of the "pck" section in the PE file so that it corresponds to the embedded data diff --git a/platform/windows/export/export_plugin.h b/platform/windows/export/export_plugin.h index c644b1f9e1..6ccb4a15a7 100644 --- a/platform/windows/export/export_plugin.h +++ b/platform/windows/export/export_plugin.h @@ -73,6 +73,8 @@ class EditorExportPlatformWindows : public EditorExportPlatformPC { Error _rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path, bool p_console_icon); Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path); + String _get_exe_arch(const String &p_path) const; + public: virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override; virtual Error modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) override; diff --git a/platform/windows/gl_manager_windows_angle.cpp b/platform/windows/gl_manager_windows_angle.cpp index 3086edc7f2..c52564676f 100644 --- a/platform/windows/gl_manager_windows_angle.cpp +++ b/platform/windows/gl_manager_windows_angle.cpp @@ -67,4 +67,9 @@ Vector<EGLint> GLManagerANGLE_Windows::_get_platform_context_attribs() const { return ret; } +void GLManagerANGLE_Windows::window_resize(DisplayServer::WindowID p_window_id, int p_width, int p_height) { + window_make_current(p_window_id); + eglWaitNative(EGL_CORE_NATIVE_ENGINE); +} + #endif // WINDOWS_ENABLED && GLES3_ENABLED diff --git a/platform/windows/gl_manager_windows_angle.h b/platform/windows/gl_manager_windows_angle.h index d8dc651cfd..f43a6fbe02 100644 --- a/platform/windows/gl_manager_windows_angle.h +++ b/platform/windows/gl_manager_windows_angle.h @@ -50,7 +50,7 @@ private: virtual Vector<EGLint> _get_platform_context_attribs() const override; public: - void window_resize(DisplayServer::WindowID p_window_id, int p_width, int p_height) {} + void window_resize(DisplayServer::WindowID p_window_id, int p_width, int p_height); GLManagerANGLE_Windows(){}; ~GLManagerANGLE_Windows(){}; diff --git a/platform/windows/gl_manager_windows_native.cpp b/platform/windows/gl_manager_windows_native.cpp index c8d7534e26..8590c46d12 100644 --- a/platform/windows/gl_manager_windows_native.cpp +++ b/platform/windows/gl_manager_windows_native.cpp @@ -76,6 +76,8 @@ static String format_error_message(DWORD id) { const int OGL_THREAD_CONTROL_ID = 0x20C1221E; const int OGL_THREAD_CONTROL_DISABLE = 0x00000002; const int OGL_THREAD_CONTROL_ENABLE = 0x00000001; +const int VRR_MODE_ID = 0x1194F158; +const int VRR_MODE_FULLSCREEN_ONLY = 0x1; typedef int(__cdecl *NvAPI_Initialize_t)(); typedef int(__cdecl *NvAPI_Unload_t)(); @@ -104,10 +106,12 @@ static bool nvapi_err_check(const char *msg, int status) { return true; } -// On windows we have to disable threaded optimization when using NVIDIA graphics cards -// to avoid stuttering, see https://stackoverflow.com/questions/36959508/nvidia-graphics-driver-causing-noticeable-frame-stuttering/37632948 -// also see https://github.com/Ryujinx/Ryujinx/blob/master/src/Ryujinx.Common/GraphicsDriver/NVThreadedOptimization.cs -void GLManagerNative_Windows::_nvapi_disable_threaded_optimization() { +// On windows we have to customize the NVIDIA application profile: +// * disable threaded optimization when using NVIDIA cards to avoid stuttering, see +// https://stackoverflow.com/questions/36959508/nvidia-graphics-driver-causing-noticeable-frame-stuttering/37632948 +// https://github.com/Ryujinx/Ryujinx/blob/master/src/Ryujinx.Common/GraphicsDriver/NVThreadedOptimization.cs +// * disable G-SYNC in windowed mode, as it results in unstable editor refresh rates +void GLManagerNative_Windows::_nvapi_setup_profile() { HMODULE nvapi = nullptr; #ifdef _WIN64 nvapi = LoadLibraryA("nvapi64.dll"); @@ -239,21 +243,29 @@ void GLManagerNative_Windows::_nvapi_disable_threaded_optimization() { } } - NVDRS_SETTING setting; - setting.version = NVDRS_SETTING_VER; - setting.settingId = OGL_THREAD_CONTROL_ID; - setting.settingType = NVDRS_DWORD_TYPE; - setting.settingLocation = NVDRS_CURRENT_PROFILE_LOCATION; - setting.isCurrentPredefined = 0; - setting.isPredefinedValid = 0; + NVDRS_SETTING ogl_thread_control_setting = {}; + ogl_thread_control_setting.version = NVDRS_SETTING_VER; + ogl_thread_control_setting.settingId = OGL_THREAD_CONTROL_ID; + ogl_thread_control_setting.settingType = NVDRS_DWORD_TYPE; int thread_control_val = OGL_THREAD_CONTROL_DISABLE; if (!GLOBAL_GET("rendering/gl_compatibility/nvidia_disable_threaded_optimization")) { thread_control_val = OGL_THREAD_CONTROL_ENABLE; } - setting.u32CurrentValue = thread_control_val; - setting.u32PredefinedValue = thread_control_val; + ogl_thread_control_setting.u32CurrentValue = thread_control_val; - if (!nvapi_err_check("NVAPI: Error calling NvAPI_DRS_SetSetting", NvAPI_DRS_SetSetting(session_handle, profile_handle, &setting))) { + if (!nvapi_err_check("NVAPI: Error calling NvAPI_DRS_SetSetting", NvAPI_DRS_SetSetting(session_handle, profile_handle, &ogl_thread_control_setting))) { + NvAPI_DRS_DestroySession(session_handle); + NvAPI_Unload(); + return; + } + + NVDRS_SETTING vrr_mode_setting = {}; + vrr_mode_setting.version = NVDRS_SETTING_VER; + vrr_mode_setting.settingId = VRR_MODE_ID; + vrr_mode_setting.settingType = NVDRS_DWORD_TYPE; + vrr_mode_setting.u32CurrentValue = VRR_MODE_FULLSCREEN_ONLY; + + if (!nvapi_err_check("NVAPI: Error calling NvAPI_DRS_SetSetting", NvAPI_DRS_SetSetting(session_handle, profile_handle, &vrr_mode_setting))) { NvAPI_DRS_DestroySession(session_handle); NvAPI_Unload(); return; @@ -270,6 +282,7 @@ void GLManagerNative_Windows::_nvapi_disable_threaded_optimization() { } else { print_verbose("NVAPI: Enabled OpenGL threaded optimization successfully"); } + print_verbose("NVAPI: Disabled G-SYNC for windowed mode successfully"); NvAPI_DRS_DestroySession(session_handle); } @@ -495,7 +508,7 @@ void GLManagerNative_Windows::swap_buffers() { } Error GLManagerNative_Windows::initialize() { - _nvapi_disable_threaded_optimization(); + _nvapi_setup_profile(); return OK; } diff --git a/platform/windows/gl_manager_windows_native.h b/platform/windows/gl_manager_windows_native.h index b4e2a3acdf..532092ae74 100644 --- a/platform/windows/gl_manager_windows_native.h +++ b/platform/windows/gl_manager_windows_native.h @@ -78,7 +78,7 @@ private: int glx_minor, glx_major; private: - void _nvapi_disable_threaded_optimization(); + void _nvapi_setup_profile(); int _find_or_create_display(GLWindow &win); Error _create_context(GLWindow &win, GLDisplay &gl_display); diff --git a/platform/windows/native_menu_windows.cpp b/platform/windows/native_menu_windows.cpp index d9dc28e9d9..fde55918e4 100644 --- a/platform/windows/native_menu_windows.cpp +++ b/platform/windows/native_menu_windows.cpp @@ -81,22 +81,6 @@ void NativeMenuWindows::_menu_activate(HMENU p_menu, int p_index) const { if (GetMenuItemInfoW(md->menu, p_index, true, &item)) { MenuItemData *item_data = (MenuItemData *)item.dwItemData; if (item_data) { - if (item_data->max_states > 0) { - item_data->state++; - if (item_data->state >= item_data->max_states) { - item_data->state = 0; - } - } - - if (item_data->checkable_type == CHECKABLE_TYPE_CHECK_BOX) { - if ((item.fState & MFS_CHECKED) == MFS_CHECKED) { - item.fState &= ~MFS_CHECKED; - } else { - item.fState |= MFS_CHECKED; - } - SetMenuItemInfoW(md->menu, p_index, true, &item); - } - if (item_data->callback.is_valid()) { Variant ret; Callable::CallError ce; @@ -619,9 +603,12 @@ bool NativeMenuWindows::is_item_checked(const RID &p_rid, int p_idx) const { MENUITEMINFOW item; ZeroMemory(&item, sizeof(item)); item.cbSize = sizeof(item); - item.fMask = MIIM_STATE; + item.fMask = MIIM_STATE | MIIM_DATA; if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { - return (item.fState & MFS_CHECKED) == MFS_CHECKED; + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + return item_data->checked; + } } return false; } @@ -861,12 +848,16 @@ void NativeMenuWindows::set_item_checked(const RID &p_rid, int p_idx, bool p_che MENUITEMINFOW item; ZeroMemory(&item, sizeof(item)); item.cbSize = sizeof(item); - item.fMask = MIIM_STATE; + item.fMask = MIIM_STATE | MIIM_DATA; if (GetMenuItemInfoW(md->menu, p_idx, true, &item)) { - if (p_checked) { - item.fState |= MFS_CHECKED; - } else { - item.fState &= ~MFS_CHECKED; + MenuItemData *item_data = (MenuItemData *)item.dwItemData; + if (item_data) { + item_data->checked = p_checked; + if (p_checked) { + item.fState |= MFS_CHECKED; + } else { + item.fState &= ~MFS_CHECKED; + } } SetMenuItemInfoW(md->menu, p_idx, true, &item); } diff --git a/platform/windows/native_menu_windows.h b/platform/windows/native_menu_windows.h index 5c4aaa52c8..235a4b332a 100644 --- a/platform/windows/native_menu_windows.h +++ b/platform/windows/native_menu_windows.h @@ -51,6 +51,7 @@ class NativeMenuWindows : public NativeMenu { Callable callback; Variant meta; GlobalMenuCheckType checkable_type; + bool checked = false; int max_states = 0; int state = 0; Ref<Image> img; diff --git a/platform/windows/os_windows.cpp b/platform/windows/os_windows.cpp index 157702655e..40b265785f 100644 --- a/platform/windows/os_windows.cpp +++ b/platform/windows/os_windows.cpp @@ -379,6 +379,8 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha //this code exists so gdextension can load .dll files from within the executable path path = get_executable_path().get_base_dir().path_join(p_path.get_file()); } + // Path to load from may be different from original if we make copies. + String load_path = path; ERR_FAIL_COND_V(!FileAccess::exists(path), ERR_FILE_NOT_FOUND); @@ -387,25 +389,22 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha if (p_data != nullptr && p_data->generate_temp_files) { // Copy the file to the same directory as the original with a prefix in the name. // This is so relative path to dependencies are satisfied. - String copy_path = path.get_base_dir().path_join("~" + path.get_file()); + load_path = path.get_base_dir().path_join("~" + path.get_file()); // If there's a left-over copy (possibly from a crash) then delete it first. - if (FileAccess::exists(copy_path)) { - DirAccess::remove_absolute(copy_path); + if (FileAccess::exists(load_path)) { + DirAccess::remove_absolute(load_path); } - Error copy_err = DirAccess::copy_absolute(path, copy_path); + Error copy_err = DirAccess::copy_absolute(path, load_path); if (copy_err) { ERR_PRINT("Error copying library: " + path); return ERR_CANT_CREATE; } - FileAccess::set_hidden_attribute(copy_path, true); + FileAccess::set_hidden_attribute(load_path, true); - // Save the copied path so it can be deleted later. - path = copy_path; - - Error pdb_err = WindowsUtils::copy_and_rename_pdb(path); + Error pdb_err = WindowsUtils::copy_and_rename_pdb(load_path); if (pdb_err != OK && pdb_err != ERR_SKIP) { WARN_PRINT(vformat("Failed to rename the PDB file. The original PDB file for '%s' will be loaded.", path)); } @@ -421,21 +420,21 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha DLL_DIRECTORY_COOKIE cookie = nullptr; if (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) { - cookie = add_dll_directory((LPCWSTR)(path.get_base_dir().utf16().get_data())); + cookie = add_dll_directory((LPCWSTR)(load_path.get_base_dir().utf16().get_data())); } - p_library_handle = (void *)LoadLibraryExW((LPCWSTR)(path.utf16().get_data()), nullptr, (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0); + p_library_handle = (void *)LoadLibraryExW((LPCWSTR)(load_path.utf16().get_data()), nullptr, (p_data != nullptr && p_data->also_set_library_path && has_dll_directory_api) ? LOAD_LIBRARY_SEARCH_DEFAULT_DIRS : 0); if (!p_library_handle) { if (p_data != nullptr && p_data->generate_temp_files) { - DirAccess::remove_absolute(path); + DirAccess::remove_absolute(load_path); } #ifdef DEBUG_ENABLED DWORD err_code = GetLastError(); - HashSet<String> checekd_libs; + HashSet<String> checked_libs; HashSet<String> missing_libs; - debug_dynamic_library_check_dependencies(path, path, checekd_libs, missing_libs); + debug_dynamic_library_check_dependencies(load_path, load_path, checked_libs, missing_libs); if (!missing_libs.is_empty()) { String missing; for (const String &E : missing_libs) { @@ -464,7 +463,8 @@ Error OS_Windows::open_dynamic_library(const String &p_path, void *&p_library_ha } if (p_data != nullptr && p_data->generate_temp_files) { - temp_libraries[p_library_handle] = path; + // Save the copied path so it can be deleted later. + temp_libraries[p_library_handle] = load_path; } return OK; @@ -1634,26 +1634,6 @@ String OS_Windows::get_locale() const { return "en"; } -// We need this because GetSystemInfo() is unreliable on WOW64 -// see https://msdn.microsoft.com/en-us/library/windows/desktop/ms724381(v=vs.85).aspx -// Taken from MSDN -typedef BOOL(WINAPI *LPFN_ISWOW64PROCESS)(HANDLE, PBOOL); -LPFN_ISWOW64PROCESS fnIsWow64Process; - -BOOL is_wow64() { - BOOL wow64 = FALSE; - - fnIsWow64Process = (LPFN_ISWOW64PROCESS)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "IsWow64Process"); - - if (fnIsWow64Process) { - if (!fnIsWow64Process(GetCurrentProcess(), &wow64)) { - wow64 = FALSE; - } - } - - return wow64; -} - String OS_Windows::get_processor_name() const { const String id = "Hardware\\Description\\System\\CentralProcessor\\0"; |