diff --git a/.github/actions/godot-cpp-build/action.yml b/.github/actions/godot-cpp-build/action.yml new file mode 100644 index 00000000000..9596c5a8fd7 --- /dev/null +++ b/.github/actions/godot-cpp-build/action.yml @@ -0,0 +1,39 @@ +name: Build godot-cpp +description: Build godot-cpp with the provided options. + +env: + GODOT_CPP_BRANCH: 4.3 + +inputs: + bin: + description: Path to the Godot binary. + required: true + type: string + scons-flags: + description: Additional SCons flags. + type: string + scons-cache: + description: The SCons cache path. + default: ${{ github.workspace }}/.scons_cache/ + type: string + +runs: + using: composite + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + repository: godotengine/godot-cpp + ref: ${{ env.GODOT_CPP_BRANCH }} + path: godot-cpp + + - name: Extract API + shell: sh + run: ${{ inputs.bin }} --headless --dump-gdextension-interface --dump-extension-api + + - name: SCons Build + shell: sh + env: + SCONS_CACHE: ${{ inputs.scons-cache }} + run: scons --directory=./godot-cpp/test "gdextension_dir=${{ github.workspace }}" ${{ inputs.scons-flags }} diff --git a/.github/workflows/godot_cpp_test.yml b/.github/workflows/godot_cpp_test.yml deleted file mode 100644 index a1b085604eb..00000000000 --- a/.github/workflows/godot_cpp_test.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: 🪲 Godot CPP -on: - workflow_call: - -# Global Settings -env: - # Used for the cache key. Add version suffix to force clean build. - GODOT_BASE_BRANCH: master - # Used for the godot-cpp checkout. - GODOT_CPP_BRANCH: 4.3 - -jobs: - godot-cpp-tests: - runs-on: ubuntu-24.04 - name: Build and test Godot CPP - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - sparse-checkout: | - .github - misc/utility/problem-matchers.json - - - name: Checkout godot-cpp - uses: actions/checkout@v4 - with: - submodules: recursive - repository: godotengine/godot-cpp - ref: ${{ env.GODOT_CPP_BRANCH }} - path: godot-cpp - - - name: Setup Python and SCons - uses: ./.github/actions/godot-deps - - - name: Download GDExtension interface and API dump - uses: ./.github/actions/download-artifact - with: - name: godot-api-dump - path: ./godot-cpp/gdextension - - # TODO: Enable caching when godot-cpp has proper cache limiting. - - # - name: Restore Godot build cache - # uses: ./.github/actions/godot-cache-restore - # with: - # cache-name: godot-cpp - # continue-on-error: true - - - name: Build godot-cpp test extension - run: scons --directory=./godot-cpp/test target=template_debug dev_build=yes verbose=yes - - # - name: Save Godot build cache - # uses: ./.github/actions/godot-cache-save - # with: - # cache-name: godot-cpp - # continue-on-error: true diff --git a/.github/workflows/linux_builds.yml b/.github/workflows/linux_builds.yml index c0f29aeab33..3310b789ea6 100644 --- a/.github/workflows/linux_builds.yml +++ b/.github/workflows/linux_builds.yml @@ -33,7 +33,9 @@ jobs: proj-conv: true api-compat: true artifact: true - cache-limit: 1 + # Validate godot-cpp compatibility on one arbitrary editor build. + godot-cpp: true + cache-limit: 2 - name: Editor with doubles and GCC sanitizers (target=editor, tests=yes, dev_build=yes, scu_build=yes, precision=double, use_asan=yes, use_ubsan=yes, linker=gold) cache-name: linux-editor-double-sanitizers @@ -44,8 +46,6 @@ jobs: build-mono: false tests: true proj-test: true - # Generate an API dump for godot-cpp tests. - api-dump: true # Skip 2GiB artifact speeding up action. artifact: false cache-limit: 7 @@ -158,6 +158,13 @@ jobs: tests: ${{ matrix.tests }} scons-cache-limit: ${{ matrix.cache-limit }} + - name: Compilation (godot-cpp) + uses: ./.github/actions/godot-cpp-build + if: matrix.godot-cpp + with: + bin: ${{ matrix.bin }} + scons-flags: target=template_debug dev_build=yes verbose=yes + - name: Save Godot build cache uses: ./.github/actions/godot-cache-save with: @@ -187,12 +194,6 @@ jobs: with: name: ${{ matrix.cache-name }} - - name: Dump Godot API - uses: ./.github/actions/godot-api-dump - if: matrix.api-dump - with: - bin: ${{ matrix.bin }} - - name: Unit tests if: matrix.tests run: | diff --git a/.github/workflows/runner.yml b/.github/workflows/runner.yml index 937f0e895d7..c88d44a098d 100644 --- a/.github/workflows/runner.yml +++ b/.github/workflows/runner.yml @@ -44,15 +44,3 @@ jobs: name: 🌐 Web needs: static-checks uses: ./.github/workflows/web_builds.yml - - # Third stage: Run auxiliary tests using build artifacts from previous jobs. - - # Can be turned off for PRs that intentionally break compat with godot-cpp, - # until both the upstream PR and the matching godot-cpp changes are merged. - godot-cpp-test: - name: 🪲 Godot CPP - # This can be changed to depend on another platform, if we decide to use it for - # godot-cpp instead. Make sure to move the .github/actions/godot-api-dump step - # appropriately. - needs: linux-build - uses: ./.github/workflows/godot_cpp_test.yml diff --git a/core/core_bind.cpp b/core/core_bind.cpp index 14e503904f7..bd0aabde80e 100644 --- a/core/core_bind.cpp +++ b/core/core_bind.cpp @@ -115,12 +115,12 @@ PackedStringArray ResourceLoader::get_dependencies(const String &p_path) { } bool ResourceLoader::has_cached(const String &p_path) { - String local_path = ProjectSettings::get_singleton()->localize_path(p_path); + String local_path = ::ResourceLoader::_validate_local_path(p_path); return ResourceCache::has(local_path); } Ref ResourceLoader::get_cached_ref(const String &p_path) { - String local_path = ProjectSettings::get_singleton()->localize_path(p_path); + String local_path = ::ResourceLoader::_validate_local_path(p_path); return ResourceCache::get_ref(local_path); } diff --git a/core/io/resource_loader.cpp b/core/io/resource_loader.cpp index ef2802f05ae..be44d8e5c37 100644 --- a/core/io/resource_loader.cpp +++ b/core/io/resource_loader.cpp @@ -497,7 +497,7 @@ void ResourceLoader::_run_load_task(void *p_userdata) { curr_load_task = curr_load_task_backup; } -static String _validate_local_path(const String &p_path) { +String ResourceLoader::_validate_local_path(const String &p_path) { ResourceUID::ID uid = ResourceUID::get_singleton()->text_to_id(p_path); if (uid != ResourceUID::INVALID_ID) { return ResourceUID::get_singleton()->get_id_path(uid); diff --git a/core/io/resource_loader.h b/core/io/resource_loader.h index d526edd6d5e..35543f7a577 100644 --- a/core/io/resource_loader.h +++ b/core/io/resource_loader.h @@ -38,6 +38,10 @@ #include "core/object/worker_thread_pool.h" #include "core/os/thread.h" +namespace core_bind { +class ResourceLoader; +} + class ConditionVariable; template @@ -103,6 +107,7 @@ typedef void (*ResourceLoadedCallback)(Ref p_resource, const String &p class ResourceLoader { friend class LoadToken; + friend class core_bind::ResourceLoader; enum { MAX_LOADERS = 64 @@ -219,6 +224,8 @@ class ResourceLoader { static bool _ensure_load_progress(); + static String _validate_local_path(const String &p_path); + public: static Error load_threaded_request(const String &p_path, const String &p_type_hint = "", bool p_use_sub_threads = false, ResourceFormatLoader::CacheMode p_cache_mode = ResourceFormatLoader::CACHE_MODE_REUSE); static ThreadLoadStatus load_threaded_get_status(const String &p_path, float *r_progress = nullptr); diff --git a/doc/classes/CPUParticles2D.xml b/doc/classes/CPUParticles2D.xml index e52e7274006..646e025b115 100644 --- a/doc/classes/CPUParticles2D.xml +++ b/doc/classes/CPUParticles2D.xml @@ -241,6 +241,7 @@ Align Y axis of particle with the direction of its velocity. + Particle system starts as if it had already run for this many seconds. diff --git a/doc/classes/Cubemap.xml b/doc/classes/Cubemap.xml index 853dbc265a8..930e4975fe6 100644 --- a/doc/classes/Cubemap.xml +++ b/doc/classes/Cubemap.xml @@ -7,10 +7,10 @@ A cubemap is made of 6 textures organized in layers. They are typically used for faking reflections in 3D rendering (see [ReflectionProbe]). It can be used to make an object look as if it's reflecting its surroundings. This usually delivers much better performance than other reflection methods. This resource is typically used as a uniform in custom shaders. Few core Redot methods make use of [Cubemap] resources. To create such a texture file yourself, reimport your image files using the Redot Editor import presets. The expected image order is X+, X-, Y+, Y-, Z+, Z- (in Redot's coordinate system, so Y+ is "up" and Z- is "forward"). You can use one of the following templates as a base: - - [url=https://raw.githubusercontent.com/Redot-Engine/redot-docs/master/img/cubemap_template_2x3.webp]2×3 cubemap template (default layout option)[/url] - - [url=https://raw.githubusercontent.com/Redot-Engine/redot-docs/master/img/cubemap_template_3x2.webp]3×2 cubemap template[/url] - - [url=https://raw.githubusercontent.com/Redot-Engine/redot-docs/master/img/cubemap_template_1x6.webp]1×6 cubemap template[/url] - - [url=https://raw.githubusercontent.com/Redot-Engine/redot-docs/master/img/cubemap_template_6x1.webp]6×1 cubemap template[/url] + - [url=https://raw.githubusercontent.com/Redot-Engine/redot-docs/master/tutorials/assets_pipeline/img/cubemap_template_2x3.webp]2×3 cubemap template (default layout option)[/url] + - [url=https://raw.githubusercontent.com/Redot-Engine/redot-docs/master/tutorials/assets_pipeline/img/cubemap_template_3x2.webp]3×2 cubemap template[/url] + - [url=https://raw.githubusercontent.com/Redot-Engine/redot-docs/master/tutorials/assets_pipeline/img/cubemap_template_1x6.webp]1×6 cubemap template[/url] + - [url=https://raw.githubusercontent.com/Redot-Engine/redot-docs/master/tutorials/assets_pipeline/img/cubemap_template_6x1.webp]6×1 cubemap template[/url] [b]Note:[/b] Redot doesn't support using cubemaps in a [PanoramaSkyMaterial]. To use a cubemap as a skybox, convert the default [PanoramaSkyMaterial] to a [ShaderMaterial] using the [b]Convert to ShaderMaterial[/b] resource dropdown option, then replace its code with the following: [codeblock lang=text] shader_type sky; diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index e7048a06ba2..1565750cf74 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -1136,6 +1136,9 @@ - [b]Launch in PiP mode[/b] will launch the Play window directly in picture-in-picture (PiP) mode if PiP mode is supported and enabled. When maximized, the Play window will occupy the same window as the Editor. [b]Note:[/b] Only available in the Android editor. + + Overrides game embedding setting for all newly opened projects. If enabled, game embedding settings are not saved. + Specifies the picture-in-picture (PiP) mode for the Play window. - [b]Disabled:[/b] PiP is disabled for the Play window. diff --git a/drivers/gles3/storage/texture_storage.cpp b/drivers/gles3/storage/texture_storage.cpp index 37f95145979..9831e3aa0eb 100644 --- a/drivers/gles3/storage/texture_storage.cpp +++ b/drivers/gles3/storage/texture_storage.cpp @@ -1511,8 +1511,17 @@ void TextureStorage::_texture_set_data(RID p_texture, const Ref &p_image, GLenum internal_format; bool compressed = false; + bool needs_decompress = texture->resize_to_po2; + + // Support for RGTC-compressed Texture Arrays isn't mandated by GLES3/WebGL. + if (!RasterizerGLES3::is_gles_over_gl() && texture->target == GL_TEXTURE_2D_ARRAY) { + if (p_image->get_format() == Image::FORMAT_RGTC_R || p_image->get_format() == Image::FORMAT_RGTC_RG) { + needs_decompress = true; + } + } + Image::Format real_format; - Ref img = _get_gl_image_and_format(p_image, p_image->get_format(), real_format, format, internal_format, type, compressed, texture->resize_to_po2); + Ref img = _get_gl_image_and_format(p_image, p_image->get_format(), real_format, format, internal_format, type, compressed, needs_decompress); ERR_FAIL_COND(img.is_null()); if (texture->resize_to_po2) { if (p_image->is_compressed()) { diff --git a/drivers/vulkan/rendering_device_driver_vulkan.cpp b/drivers/vulkan/rendering_device_driver_vulkan.cpp index cce6d08a3ef..d530964da42 100644 --- a/drivers/vulkan/rendering_device_driver_vulkan.cpp +++ b/drivers/vulkan/rendering_device_driver_vulkan.cpp @@ -1532,10 +1532,12 @@ RDD::BufferID RenderingDeviceDriverVulkan::buffer_create(uint64_t p_size, BitFie if (is_src && !is_dst) { // Looks like a staging buffer: CPU maps, writes sequentially, then GPU copies to VRAM. alloc_create_info.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + alloc_create_info.preferredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; } if (is_dst && !is_src) { // Looks like a readback buffer: GPU copies from VRAM, then CPU maps and reads. alloc_create_info.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT; + alloc_create_info.preferredFlags |= VK_MEMORY_PROPERTY_HOST_CACHED_BIT; } alloc_create_info.requiredFlags = (VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT); } break; diff --git a/editor/code_editor.cpp b/editor/code_editor.cpp index dbfedb9fc28..470b16ad60f 100644 --- a/editor/code_editor.cpp +++ b/editor/code_editor.cpp @@ -935,12 +935,14 @@ void CodeTextEditor::_text_editor_gui_input(const Ref &p_event) { } } +#ifndef ANDROID_ENABLED Ref magnify_gesture = p_event; if (magnify_gesture.is_valid()) { _zoom_to(zoom_factor * powf(magnify_gesture->get_factor(), 0.25f)); accept_event(); return; } +#endif Ref k = p_event; diff --git a/editor/editor_run.cpp b/editor/editor_run.cpp index 059fc1d9887..439470c388d 100644 --- a/editor/editor_run.cpp +++ b/editor/editor_run.cpp @@ -106,99 +106,16 @@ Error EditorRun::run(const String &p_scene, const String &p_write_movie, const V } } - int screen = EDITOR_GET("run/window_placement/screen"); - if (screen == -5) { - // Same as editor - screen = DisplayServer::get_singleton()->window_get_current_screen(); - } else if (screen == -4) { - // Previous monitor (wrap to the other end if needed) - screen = Math::wrapi( - DisplayServer::get_singleton()->window_get_current_screen() - 1, - 0, - DisplayServer::get_singleton()->get_screen_count()); - } else if (screen == -3) { - // Next monitor (wrap to the other end if needed) - screen = Math::wrapi( - DisplayServer::get_singleton()->window_get_current_screen() + 1, - 0, - DisplayServer::get_singleton()->get_screen_count()); + WindowPlacement window_placement = get_window_placement(); + if (window_placement.position != Point2i(INT_MAX, INT_MAX)) { + args.push_back("--position"); + args.push_back(itos(window_placement.position.x) + "," + itos(window_placement.position.y)); } - Rect2 screen_rect = DisplayServer::get_singleton()->screen_get_usable_rect(screen); - - int window_placement = EDITOR_GET("run/window_placement/rect"); - if (screen_rect != Rect2()) { - Size2 window_size; - window_size.x = GLOBAL_GET("display/window/size/viewport_width"); - window_size.y = GLOBAL_GET("display/window/size/viewport_height"); - - Size2 desired_size; - desired_size.x = GLOBAL_GET("display/window/size/window_width_override"); - desired_size.y = GLOBAL_GET("display/window/size/window_height_override"); - if (desired_size.x > 0 && desired_size.y > 0) { - window_size = desired_size; - } - - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_HIDPI)) { - bool hidpi_proj = GLOBAL_GET("display/window/dpi/allow_hidpi"); - int display_scale = 1; - - if (OS::get_singleton()->is_hidpi_allowed()) { - if (hidpi_proj) { - display_scale = 1; // Both editor and project runs in hiDPI mode, do not scale. - } else { - display_scale = DisplayServer::get_singleton()->screen_get_max_scale(); // Editor is in hiDPI mode, project is not, scale down. - } - } else { - if (hidpi_proj) { - display_scale = (1.f / DisplayServer::get_singleton()->screen_get_max_scale()); // Editor is not in hiDPI mode, project is, scale up. - } else { - display_scale = 1; // Both editor and project runs in lowDPI mode, do not scale. - } - } - screen_rect.position /= display_scale; - screen_rect.size /= display_scale; - } - - switch (window_placement) { - case 0: { // top left - args.push_back("--position"); - args.push_back(itos(screen_rect.position.x) + "," + itos(screen_rect.position.y)); - } break; - case 1: { // centered - Vector2 pos = (screen_rect.position) + ((screen_rect.size - window_size) / 2).floor(); - args.push_back("--position"); - args.push_back(itos(pos.x) + "," + itos(pos.y)); - } break; - case 2: { // custom pos - Vector2 pos = EDITOR_GET("run/window_placement/rect_custom_position"); - pos += screen_rect.position; - args.push_back("--position"); - args.push_back(itos(pos.x) + "," + itos(pos.y)); - } break; - case 3: { // force maximized - Vector2 pos = screen_rect.position + screen_rect.size / 2; - args.push_back("--position"); - args.push_back(itos(pos.x) + "," + itos(pos.y)); - args.push_back("--maximized"); - } break; - case 4: { // force fullscreen - Vector2 pos = screen_rect.position + screen_rect.size / 2; - args.push_back("--position"); - args.push_back(itos(pos.x) + "," + itos(pos.y)); - args.push_back("--fullscreen"); - } break; - } - } else { - // Unable to get screen info, skip setting position. - switch (window_placement) { - case 3: { // force maximized - args.push_back("--maximized"); - } break; - case 4: { // force fullscreen - args.push_back("--fullscreen"); - } break; - } + if (window_placement.force_maximized) { + args.push_back("--maximized"); + } else if (window_placement.force_fullscreen) { + args.push_back("--fullscreen"); } List breakpoints; @@ -299,6 +216,98 @@ OS::ProcessID EditorRun::get_current_process() const { return pids.front()->get(); } +EditorRun::WindowPlacement EditorRun::get_window_placement() { + WindowPlacement placement = WindowPlacement(); + placement.screen = EDITOR_GET("run/window_placement/screen"); + if (placement.screen == -5) { + // Same as editor + placement.screen = DisplayServer::get_singleton()->window_get_current_screen(); + } else if (placement.screen == -4) { + // Previous monitor (wrap to the other end if needed) + placement.screen = Math::wrapi( + DisplayServer::get_singleton()->window_get_current_screen() - 1, + 0, + DisplayServer::get_singleton()->get_screen_count()); + } else if (placement.screen == -3) { + // Next monitor (wrap to the other end if needed) + placement.screen = Math::wrapi( + DisplayServer::get_singleton()->window_get_current_screen() + 1, + 0, + DisplayServer::get_singleton()->get_screen_count()); + } else if (placement.screen == -2) { + // Primary screen + placement.screen = DisplayServer::get_singleton()->get_primary_screen(); + } + + placement.size.x = GLOBAL_GET("display/window/size/viewport_width"); + placement.size.y = GLOBAL_GET("display/window/size/viewport_height"); + + Size2 desired_size; + desired_size.x = GLOBAL_GET("display/window/size/window_width_override"); + desired_size.y = GLOBAL_GET("display/window/size/window_height_override"); + if (desired_size.x > 0 && desired_size.y > 0) { + placement.size = desired_size; + } + + Rect2 screen_rect = DisplayServer::get_singleton()->screen_get_usable_rect(placement.screen); + + int window_placement = EDITOR_GET("run/window_placement/rect"); + if (screen_rect != Rect2()) { + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_HIDPI)) { + bool hidpi_proj = GLOBAL_GET("display/window/dpi/allow_hidpi"); + int display_scale = 1; + + if (OS::get_singleton()->is_hidpi_allowed()) { + if (hidpi_proj) { + display_scale = 1; // Both editor and project runs in hiDPI mode, do not scale. + } else { + display_scale = DisplayServer::get_singleton()->screen_get_max_scale(); // Editor is in hiDPI mode, project is not, scale down. + } + } else { + if (hidpi_proj) { + display_scale = (1.f / DisplayServer::get_singleton()->screen_get_max_scale()); // Editor is not in hiDPI mode, project is, scale up. + } else { + display_scale = 1; // Both editor and project runs in lowDPI mode, do not scale. + } + } + screen_rect.position /= display_scale; + screen_rect.size /= display_scale; + } + + switch (window_placement) { + case 0: { // top left + placement.position = screen_rect.position; + } break; + case 1: { // centered + placement.position = (screen_rect.position) + ((screen_rect.size - placement.size) / 2).floor(); + } break; + case 2: { // custom pos + Vector2 pos = EDITOR_GET("run/window_placement/rect_custom_position"); + pos += screen_rect.position; + placement.position = pos; + } break; + case 3: { // force maximized + placement.force_maximized = true; + } break; + case 4: { // force fullscreen + placement.force_fullscreen = true; + } break; + } + } else { + // Unable to get screen info, skip setting position. + switch (window_placement) { + case 3: { // force maximized + placement.force_maximized = true; + } break; + case 4: { // force fullscreen + placement.force_fullscreen = true; + } break; + } + } + + return placement; +} + EditorRun::EditorRun() { status = STATUS_STOP; running_scene = ""; diff --git a/editor/editor_run.h b/editor/editor_run.h index 55d01b95ceb..03728a2c0bb 100644 --- a/editor/editor_run.h +++ b/editor/editor_run.h @@ -47,6 +47,14 @@ class EditorRun { List pids; + struct WindowPlacement { + int screen = 0; + Point2i position = Point2i(INT_MAX, INT_MAX); + Size2i size; + bool force_maximized = false; + bool force_fullscreen = false; + }; + private: Status status; String running_scene; @@ -66,6 +74,8 @@ class EditorRun { int get_child_process_count() const { return pids.size(); } OS::ProcessID get_current_process() const; + static WindowPlacement get_window_placement(); + EditorRun(); }; diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index a88f9c8d8a3..b123c31d98f 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -942,6 +942,8 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2,Launch in PiP mode:3"; EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/android_window", 0, android_window_hints) + EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/game_embed_mode", 0, "Use Per-Project Configuration:0,Embed Game:1,Make Game Workspace Floating:2,Disabled:3"); + int default_play_window_pip_mode = 0; #ifdef ANDROID_ENABLED default_play_window_pip_mode = 2; diff --git a/editor/export/editor_export_platform.cpp b/editor/export/editor_export_platform.cpp index 0cbb5608c19..5cdf0650631 100644 --- a/editor/export/editor_export_platform.cpp +++ b/editor/export/editor_export_platform.cpp @@ -37,6 +37,7 @@ #include "core/extension/gdextension.h" #include "core/io/file_access_encrypted.h" #include "core/io/file_access_pack.h" // PACK_HEADER_MAGIC, PACK_FORMAT_VERSION +#include "core/io/resource_uid.h" #include "core/io/zip_io.h" #include "core/version.h" #include "editor/editor_file_system.h" @@ -957,8 +958,8 @@ Vector EditorExportPlatform::get_forced_export_files() { files.push_back(ProjectSettings::get_singleton()->get_global_class_list_path()); - String icon = GLOBAL_GET("application/config/icon"); - String splash = GLOBAL_GET("application/boot_splash/image"); + String icon = ResourceUID::ensure_path(GLOBAL_GET("application/config/icon")); + String splash = ResourceUID::ensure_path(GLOBAL_GET("application/boot_splash/image")); if (!icon.is_empty() && FileAccess::exists(icon)) { files.push_back(icon); } diff --git a/editor/icons/FixedSize.svg b/editor/icons/FixedSize.svg new file mode 100644 index 00000000000..1ca5752c23c --- /dev/null +++ b/editor/icons/FixedSize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/icons/KeepAspect.svg b/editor/icons/KeepAspect.svg index 1cc831563fc..f178a2aa879 100644 --- a/editor/icons/KeepAspect.svg +++ b/editor/icons/KeepAspect.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/editor/icons/Stretch.svg b/editor/icons/Stretch.svg new file mode 100644 index 00000000000..f8cb75fe864 --- /dev/null +++ b/editor/icons/Stretch.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/plugins/embedded_process.cpp b/editor/plugins/embedded_process.cpp index 95a7a2da7a9..0d49d7115a3 100644 --- a/editor/plugins/embedded_process.cpp +++ b/editor/plugins/embedded_process.cpp @@ -52,7 +52,7 @@ void EmbeddedProcess::_notification(int p_what) { Rect2i new_global_rect = get_global_rect(); if (last_global_rect != new_global_rect) { last_global_rect = new_global_rect; - _queue_update_embedded_process(); + queue_update_embedded_process(); } } break; @@ -62,7 +62,7 @@ void EmbeddedProcess::_notification(int p_what) { case NOTIFICATION_RESIZED: case NOTIFICATION_VISIBILITY_CHANGED: case NOTIFICATION_WM_POSITION_CHANGED: { - _queue_update_embedded_process(); + queue_update_embedded_process(); } break; case NOTIFICATION_THEME_CHANGED: { focus_style_box = get_theme_stylebox(SNAME("FocusViewport"), EditorStringName(EditorStyles)); @@ -79,7 +79,7 @@ void EmbeddedProcess::_notification(int p_what) { } } break; case NOTIFICATION_FOCUS_ENTER: { - _queue_update_embedded_process(); + queue_update_embedded_process(); } break; case NOTIFICATION_APPLICATION_FOCUS_IN: { application_has_focus = true; @@ -90,7 +90,7 @@ void EmbeddedProcess::_notification(int p_what) { // or if the current window is a different popup or secondary window. if (embedding_completed && current_process_id != focused_process_id && window && window->has_focus()) { grab_focus(); - _queue_update_embedded_process(); + queue_update_embedded_process(); } } } break; @@ -104,27 +104,33 @@ void EmbeddedProcess::_notification(int p_what) { void EmbeddedProcess::set_window_size(const Size2i p_window_size) { if (window_size != p_window_size) { window_size = p_window_size; - _queue_update_embedded_process(); + queue_update_embedded_process(); } } void EmbeddedProcess::set_keep_aspect(bool p_keep_aspect) { if (keep_aspect != p_keep_aspect) { keep_aspect = p_keep_aspect; - _queue_update_embedded_process(); + queue_update_embedded_process(); } } Rect2i EmbeddedProcess::_get_global_embedded_window_rect() { Rect2i control_rect = get_global_rect(); - control_rect = Rect2i(control_rect.position, control_rect.size.maxi(1)); - if (keep_aspect) { - Rect2i desired_rect = control_rect; - float ratio = MIN((float)control_rect.size.x / window_size.x, (float)control_rect.size.y / window_size.y); - desired_rect.size = Size2i(window_size.x * ratio, window_size.y * ratio).maxi(1); + control_rect = Rect2i(control_rect.position + margin_top_left, (control_rect.size - get_margins_size()).maxi(1)); + if (window_size != Size2i()) { + Rect2i desired_rect = Rect2i(); + if (!keep_aspect && control_rect.size.x >= window_size.x && control_rect.size.y >= window_size.y) { + // Fixed at the desired size. + desired_rect.size = window_size; + } else { + float ratio = MIN((float)control_rect.size.x / window_size.x, (float)control_rect.size.y / window_size.y); + desired_rect.size = Size2i(window_size.x * ratio, window_size.y * ratio).maxi(1); + } desired_rect.position = Size2i(control_rect.position.x + ((control_rect.size.x - desired_rect.size.x) / 2), control_rect.position.y + ((control_rect.size.y - desired_rect.size.y) / 2)); return desired_rect; } else { + // Stretch, use all the control area. return control_rect; } } @@ -135,8 +141,28 @@ Rect2i EmbeddedProcess::get_screen_embedded_window_rect() { rect.position += window->get_position(); } - // Removing margins to make space for the focus border style. - return Rect2i(rect.position.x + margin_top_left.x, rect.position.y + margin_top_left.y, MAX(rect.size.x - (margin_top_left.x + margin_bottom_right.x), 1), MAX(rect.size.y - (margin_top_left.y + margin_bottom_right.y), 1)); + return rect; +} + +int EmbeddedProcess::get_margin_size(Side p_side) const { + ERR_FAIL_INDEX_V((int)p_side, 4, 0); + + switch (p_side) { + case SIDE_LEFT: + return margin_top_left.x; + case SIDE_RIGHT: + return margin_bottom_right.x; + case SIDE_TOP: + return margin_top_left.y; + case SIDE_BOTTOM: + return margin_bottom_right.y; + } + + return 0; +} + +Size2 EmbeddedProcess::get_margins_size() { + return margin_top_left + margin_bottom_right; } bool EmbeddedProcess::is_embedding_in_progress() { @@ -217,7 +243,7 @@ bool EmbeddedProcess::_is_embedded_process_updatable() { return window && current_process_id != 0 && embedding_completed; } -void EmbeddedProcess::_queue_update_embedded_process() { +void EmbeddedProcess::queue_update_embedded_process() { if (updated_embedded_process_queued || !_is_embedded_process_updatable()) { return; } @@ -289,7 +315,7 @@ void EmbeddedProcess::_check_mouse_over() { // When we already have the focus and the user moves the mouse over the embedded process, // we just need to refocus the process. if (focused) { - _queue_update_embedded_process(); + queue_update_embedded_process(); } else { grab_focus(); queue_redraw(); diff --git a/editor/plugins/embedded_process.h b/editor/plugins/embedded_process.h index 891e9914091..62bbcaa0187 100644 --- a/editor/plugins/embedded_process.h +++ b/editor/plugins/embedded_process.h @@ -61,7 +61,6 @@ class EmbeddedProcess : public Control { Rect2i last_global_rect; void _try_embed_process(); - void _queue_update_embedded_process(); void _update_embedded_process(); void _timer_embedding_timeout(); void _draw(); @@ -80,8 +79,11 @@ class EmbeddedProcess : public Control { void set_window_size(const Size2i p_window_size); void set_keep_aspect(bool p_keep_aspect); + void queue_update_embedded_process(); Rect2i get_screen_embedded_window_rect(); + int get_margin_size(Side p_side) const; + Size2 get_margins_size(); bool is_embedding_in_progress(); bool is_embedding_completed(); int get_embedded_pid() const; diff --git a/editor/plugins/game_view_plugin.cpp b/editor/plugins/game_view_plugin.cpp index 03d2c0fa9d4..acd08f048b1 100644 --- a/editor/plugins/game_view_plugin.cpp +++ b/editor/plugins/game_view_plugin.cpp @@ -43,6 +43,7 @@ #include "editor/editor_main_screen.h" #include "editor/editor_node.h" #include "editor/editor_settings.h" +#include "editor/editor_string_names.h" #include "editor/gui/editor_run_bar.h" #include "editor/plugins/embedded_process.h" #include "editor/themes/editor_scale.h" @@ -224,18 +225,49 @@ void GameView::_instance_starting(int p_idx, List &r_arguments) { if (!is_feature_enabled) { return; } - if (p_idx == 0 && embed_on_play && make_floating_on_play && !window_wrapper->get_window_enabled() && EditorNode::get_singleton()->is_multi_window_enabled()) { + if (p_idx == 0 && embed_on_play && make_floating_on_play && !window_wrapper->get_window_enabled() && _get_embed_available() == EMBED_AVAILABLE) { // Set the Floating Window default title. Always considered in DEBUG mode, same as in Window::set_title. String appname = GLOBAL_GET("application/config/name"); appname = vformat("%s (DEBUG)", TranslationServer::get_singleton()->translate(appname)); window_wrapper->set_window_title(appname); - window_wrapper->restore_window_from_saved_position(floating_window_rect, floating_window_screen, floating_window_screen_rect); + _show_update_window_wrapper(); } _update_arguments_for_instance(p_idx, r_arguments); } +void GameView::_show_update_window_wrapper() { + EditorRun::WindowPlacement placement = EditorRun::get_window_placement(); + Point2 position = floating_window_rect.position; + Size2i size = floating_window_rect.size; + int screen = floating_window_screen; + + Size2 wrapped_margins_size = window_wrapper->get_margins_size(); + Point2 offset_embedded_process = embedded_process->get_global_position() - get_global_position(); + offset_embedded_process.x += embedded_process->get_margin_size(SIDE_LEFT); + offset_embedded_process.y += embedded_process->get_margin_size(SIDE_TOP); + + // Obtain the size around the embedded process control. Usually, the difference between the game view's get_size + // and the embedded control should work. However, when the control is hidden and has never been displayed, + // the size of the embedded control is not calculated. + Size2 old_min_size = embedded_process->get_custom_minimum_size(); + embedded_process->set_custom_minimum_size(Size2i()); + Size2 min_size = get_minimum_size(); + embedded_process->set_custom_minimum_size(old_min_size); + + Point2 size_diff_embedded_process = Point2(0, min_size.y) + embedded_process->get_margins_size(); + + if (placement.position != Point2i(INT_MAX, INT_MAX)) { + position = placement.position - offset_embedded_process; + screen = placement.screen; + } + if (placement.size != Size2i()) { + size = placement.size + size_diff_embedded_process + wrapped_margins_size; + } + window_wrapper->restore_window_from_saved_position(Rect2(position, size), screen, Rect2i()); +} + void GameView::_play_pressed() { if (!is_feature_enabled) { return; @@ -250,7 +282,7 @@ void GameView::_play_pressed() { screen_index_before_start = EditorNode::get_singleton()->get_editor_main_screen()->get_selected_index(); } - if (embed_on_play) { + if (embed_on_play && _get_embed_available() == EMBED_AVAILABLE) { // It's important to disable the low power mode when unfocused because otherwise // the button in the editor are not responsive and if the user moves the mouse quickly, // the mouse clicks are not registered. @@ -308,9 +340,18 @@ void GameView::_embedded_process_focused() { } } -void GameView::_project_settings_changed() { +void GameView::_editor_or_project_settings_changed() { // Update the window size and aspect ratio. _update_embed_window_size(); + + if (window_wrapper->get_window_enabled()) { + _show_update_window_wrapper(); + if (embedded_process->is_embedding_completed()) { + embedded_process->queue_update_embedded_process(); + } + } + + _update_ui(); } void GameView::_update_debugger_buttons() { @@ -362,35 +403,102 @@ void GameView::_embed_options_menu_menu_id_pressed(int p_id) { switch (p_id) { case EMBED_RUN_GAME_EMBEDDED: { embed_on_play = !embed_on_play; - EditorSettings::get_singleton()->set_project_metadata("game_view", "embed_on_play", embed_on_play); + int game_mode = EDITOR_GET("run/window_placement/game_embed_mode"); + if (game_mode == 0) { // Save only if not overridden by editor. + EditorSettings::get_singleton()->set_project_metadata("game_view", "embed_on_play", embed_on_play); + } } break; case EMBED_MAKE_FLOATING_ON_PLAY: { make_floating_on_play = !make_floating_on_play; - EditorSettings::get_singleton()->set_project_metadata("game_view", "make_floating_on_play", make_floating_on_play); + int game_mode = EDITOR_GET("run/window_placement/game_embed_mode"); + if (game_mode == 0) { // Save only if not overridden by editor. + EditorSettings::get_singleton()->set_project_metadata("game_view", "make_floating_on_play", make_floating_on_play); + } } break; } _update_embed_menu_options(); + _update_ui(); } -void GameView::_keep_aspect_button_pressed() { - embedded_process->set_keep_aspect(keep_aspect_button->is_pressed()); +void GameView::_size_mode_button_pressed(int size_mode) { + embed_size_mode = (EmbedSizeMode)size_mode; + + _update_embed_menu_options(); + _update_embed_window_size(); +} + +GameView::EmbedAvailability GameView::_get_embed_available() { + if (!DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_WINDOW_EMBEDDING)) { + return EMBED_NOT_AVAILABLE_FEATURE_NOT_SUPPORTED; + } + if (!EditorNode::get_singleton()->is_multi_window_enabled()) { + return EMBED_NOT_AVAILABLE_SINGLE_WINDOW_MODE; + } + + EditorRun::WindowPlacement placement = EditorRun::get_window_placement(); + if (placement.force_fullscreen) { + return EMBED_NOT_AVAILABLE_FULLSCREEN; + } + if (placement.force_maximized) { + return EMBED_NOT_AVAILABLE_MAXIMIZED; + } + + DisplayServer::WindowMode window_mode = (DisplayServer::WindowMode)(GLOBAL_GET("display/window/size/mode").operator int()); + if (window_mode == DisplayServer::WindowMode::WINDOW_MODE_MINIMIZED) { + return EMBED_NOT_AVAILABLE_MINIMIZED; + } + if (window_mode == DisplayServer::WindowMode::WINDOW_MODE_MAXIMIZED) { + return EMBED_NOT_AVAILABLE_MAXIMIZED; + } + if (window_mode == DisplayServer::WindowMode::WINDOW_MODE_FULLSCREEN || window_mode == DisplayServer::WindowMode::WINDOW_MODE_EXCLUSIVE_FULLSCREEN) { + return EMBED_NOT_AVAILABLE_FULLSCREEN; + } + + return EMBED_AVAILABLE; } void GameView::_update_ui() { bool show_game_size = false; - if (!DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_WINDOW_EMBEDDING)) { - state_label->set_text(TTR("Game embedding not available on your OS.")); - } else if (embedded_process->is_embedding_completed()) { - state_label->set_text(""); - show_game_size = true; - } else if (embedded_process->is_embedding_in_progress()) { - state_label->set_text(TTR("Game starting...")); - } else if (EditorRunBar::get_singleton()->is_playing()) { - state_label->set_text(TTR("Game running not embedded.")); - } else if (embed_on_play) { - state_label->set_text(TTR("Press play to start the game.")); + EmbedAvailability available = _get_embed_available(); + + switch (available) { + case EMBED_AVAILABLE: + if (embedded_process->is_embedding_completed()) { + state_label->set_text(""); + show_game_size = true; + } else if (embedded_process->is_embedding_in_progress()) { + state_label->set_text(TTR("Game starting...")); + } else if (EditorRunBar::get_singleton()->is_playing()) { + state_label->set_text(TTR("Game running not embedded.")); + } else if (embed_on_play) { + state_label->set_text(TTR("Press play to start the game.")); + } else { + state_label->set_text(TTR("Embedding is disabled.")); + } + break; + case EMBED_NOT_AVAILABLE_FEATURE_NOT_SUPPORTED: + state_label->set_text(TTR("Game embedding not available on your OS.")); + break; + case EMBED_NOT_AVAILABLE_MINIMIZED: + state_label->set_text(TTR("Game embedding not available when the game starts minimized.\nConsider overriding the window mode project setting with the editor feature tag to Windowed to use game embedding while leaving the exported project intact.")); + break; + case EMBED_NOT_AVAILABLE_MAXIMIZED: + state_label->set_text(TTR("Game embedding not available when the game starts maximized.\nConsider overriding the window mode project setting with the editor feature tag to Windowed to use game embedding while leaving the exported project intact.")); + break; + case EMBED_NOT_AVAILABLE_FULLSCREEN: + state_label->set_text(TTR("Game embedding not available when the game starts in fullscreen.\nConsider overriding the window mode project setting with the editor feature tag to Windowed to use game embedding while leaving the exported project intact.")); + break; + case EMBED_NOT_AVAILABLE_SINGLE_WINDOW_MODE: + state_label->set_text(TTR("Game embedding not available in single window mode.")); + break; + } + + if (available == EMBED_AVAILABLE) { + if (state_label->has_theme_color_override(SceneStringName(font_color))) { + state_label->remove_theme_color_override(SceneStringName(font_color)); + } } else { - state_label->set_text(TTR("Embedding is disabled.")); + state_label->add_theme_color_override(SceneStringName(font_color), state_label->get_theme_color(SNAME("warning_color"), EditorStringName(Editor))); } game_size_label->set_visible(show_game_size); @@ -401,22 +509,23 @@ void GameView::_update_embed_menu_options() { menu->set_item_checked(menu->get_item_index(EMBED_RUN_GAME_EMBEDDED), embed_on_play); menu->set_item_checked(menu->get_item_index(EMBED_MAKE_FLOATING_ON_PLAY), make_floating_on_play); - // When embed is Off or in single window mode, Make floating is not available. - menu->set_item_disabled(menu->get_item_index(EMBED_MAKE_FLOATING_ON_PLAY), !embed_on_play || !EditorNode::get_singleton()->is_multi_window_enabled()); + menu->set_item_disabled(menu->get_item_index(EMBED_MAKE_FLOATING_ON_PLAY), !embed_on_play); + + fixed_size_button->set_pressed(embed_size_mode == SIZE_MODE_FIXED); + keep_aspect_button->set_pressed(embed_size_mode == SIZE_MODE_KEEP_ASPECT); + stretch_button->set_pressed(embed_size_mode == SIZE_MODE_STRETCH); } void GameView::_update_embed_window_size() { - Size2 window_size; - window_size.x = GLOBAL_GET("display/window/size/viewport_width"); - window_size.y = GLOBAL_GET("display/window/size/viewport_height"); - - Size2 desired_size; - desired_size.x = GLOBAL_GET("display/window/size/window_width_override"); - desired_size.y = GLOBAL_GET("display/window/size/window_height_override"); - if (desired_size.x > 0 && desired_size.y > 0) { - window_size = desired_size; + if (embed_size_mode == SIZE_MODE_FIXED || embed_size_mode == SIZE_MODE_KEEP_ASPECT) { + //The embedded process control will need the desired window size. + EditorRun::WindowPlacement placement = EditorRun::get_window_placement(); + embedded_process->set_window_size(placement.size); + } else { + //Stretch... No need for the window size. + embedded_process->set_window_size(Size2i()); } - embedded_process->set_window_size(window_size); + embedded_process->set_keep_aspect(embed_size_mode == SIZE_MODE_KEEP_ASPECT); } void GameView::_hide_selection_toggled(bool p_pressed) { @@ -477,7 +586,9 @@ void GameView::_notification(int p_what) { select_mode_button[RuntimeNodeSelect::SELECT_MODE_LIST]->set_button_icon(get_editor_theme_icon(SNAME("ListSelect"))); hide_selection->set_button_icon(get_editor_theme_icon(hide_selection->is_pressed() ? SNAME("GuiVisibilityHidden") : SNAME("GuiVisibilityVisible"))); + fixed_size_button->set_button_icon(get_editor_theme_icon(SNAME("FixedSize"))); keep_aspect_button->set_button_icon(get_editor_theme_icon(SNAME("KeepAspect"))); + stretch_button->set_button_icon(get_editor_theme_icon(SNAME("Stretch"))); embed_options_menu->set_button_icon(get_editor_theme_icon(SNAME("GuiTabMenuHl"))); camera_override_button->set_button_icon(get_editor_theme_icon(SNAME("Camera"))); @@ -487,8 +598,26 @@ void GameView::_notification(int p_what) { case NOTIFICATION_READY: { if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_WINDOW_EMBEDDING)) { // Embedding available. - embed_on_play = EditorSettings::get_singleton()->get_project_metadata("game_view", "embed_on_play", true); - make_floating_on_play = EditorSettings::get_singleton()->get_project_metadata("game_view", "make_floating_on_play", true); + int game_mode = EDITOR_GET("run/window_placement/game_embed_mode"); + switch (game_mode) { + case 1: { // Embed. + embed_on_play = true; + make_floating_on_play = false; + } break; + case 2: { // Floating. + embed_on_play = true; + make_floating_on_play = true; + } break; + case 3: { // Disabled. + embed_on_play = false; + make_floating_on_play = false; + } break; + default: { + embed_on_play = EditorSettings::get_singleton()->get_project_metadata("game_view", "embed_on_play", true); + make_floating_on_play = EditorSettings::get_singleton()->get_project_metadata("game_view", "make_floating_on_play", true); + } break; + } + embed_size_mode = (EmbedSizeMode)(int)EditorSettings::get_singleton()->get_project_metadata("game_view", "embed_size_mode", SIZE_MODE_FIXED); keep_aspect_button->set_pressed(EditorSettings::get_singleton()->get_project_metadata("game_view", "keep_aspect", true)); _update_embed_menu_options(); @@ -497,15 +626,15 @@ void GameView::_notification(int p_what) { EditorRun::instance_starting_callback = _instance_starting_static; // Listen for project settings changes to update the window size and aspect ratio. - ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &GameView::_project_settings_changed)); - - embedded_process->set_keep_aspect(keep_aspect_button->is_pressed()); + ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &GameView::_editor_or_project_settings_changed)); + EditorSettings::get_singleton()->connect("settings_changed", callable_mp(this, &GameView::_editor_or_project_settings_changed)); } else { // Embedding not available. embedding_separator->hide(); embed_options_menu->hide(); + fixed_size_button->hide(); keep_aspect_button->hide(); - keep_aspect_button->hide(); + stretch_button->hide(); } _update_ui(); @@ -560,7 +689,6 @@ Dictionary GameView::get_state() const { void GameView::set_window_layout(Ref p_layout) { floating_window_rect = p_layout->get_value("GameView", "floating_window_rect", Rect2i()); floating_window_screen = p_layout->get_value("GameView", "floating_window_screen", -1); - floating_window_screen_rect = p_layout->get_value("GameView", "floating_window_screen_rect", Rect2i()); } void GameView::get_window_layout(Ref p_layout) { @@ -570,14 +698,12 @@ void GameView::get_window_layout(Ref p_layout) { p_layout->set_value("GameView", "floating_window_rect", floating_window_rect); p_layout->set_value("GameView", "floating_window_screen", floating_window_screen); - p_layout->set_value("GameView", "floating_window_screen_rect", floating_window_screen_rect); } void GameView::_update_floating_window_settings() { if (window_wrapper->get_window_enabled()) { floating_window_rect = window_wrapper->get_window_rect(); floating_window_screen = window_wrapper->get_window_screen(); - floating_window_screen_rect = DisplayServer::get_singleton()->screen_get_usable_rect(floating_window_screen); } } @@ -612,7 +738,7 @@ void GameView::_remote_window_title_changed(String title) { } void GameView::_update_arguments_for_instance(int p_idx, List &r_arguments) { - if (p_idx != 0 || !embed_on_play || !DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_WINDOW_EMBEDDING)) { + if (p_idx != 0 || !embed_on_play || _get_embed_available() != EMBED_AVAILABLE) { return; } @@ -654,14 +780,20 @@ void GameView::_update_arguments_for_instance(int p_idx, List &r_argumen r_arguments.insert_after(N, itos(rect.size.x) + "x" + itos(rect.size.y)); } -void GameView::_window_before_closing() { +void GameView::_window_close_request() { // Before the parent window closed, we close the embedded game. That prevents // the embedded game to be seen without a parent window for a fraction of second. if (EditorRunBar::get_singleton()->is_playing() && (embedded_process->is_embedding_completed() || embedded_process->is_embedding_in_progress())) { + // Try to gracefully close the window. That way, the NOTIFICATION_WM_CLOSE_REQUEST + // notification should be propagated in the game process. embedded_process->reset(); - // Call deferred to prevent the _stop_pressed callback to be executed before the wrapper window - // actually closes. - callable_mp(EditorRunBar::get_singleton(), &EditorRunBar::stop_playing).call_deferred(); + + // When the embedding is not complete, we need to kill the process. + if (embedded_process->is_embedding_in_progress()) { + // Call deferred to prevent the _stop_pressed callback to be executed before the wrapper window + // actually closes. + callable_mp(EditorRunBar::get_singleton(), &EditorRunBar::stop_playing).call_deferred(); + } } } @@ -780,12 +912,26 @@ GameView::GameView(Ref p_debugger, WindowWrapper *p_wrapper) { embedding_separator = memnew(VSeparator); main_menu_hbox->add_child(embedding_separator); + fixed_size_button = memnew(Button); + main_menu_hbox->add_child(fixed_size_button); + fixed_size_button->set_toggle_mode(true); + fixed_size_button->set_theme_type_variation("FlatButton"); + fixed_size_button->set_tooltip_text(TTR("Embedded game size is based on project settings.\nThe 'Keep Aspect' mode is used when the Game Workspace is smaller than the desired size.")); + fixed_size_button->connect(SceneStringName(pressed), callable_mp(this, &GameView::_size_mode_button_pressed).bind(SIZE_MODE_FIXED)); + keep_aspect_button = memnew(Button); main_menu_hbox->add_child(keep_aspect_button); keep_aspect_button->set_toggle_mode(true); keep_aspect_button->set_theme_type_variation("FlatButton"); keep_aspect_button->set_tooltip_text(TTR("Keep the aspect ratio of the embedded game.")); - keep_aspect_button->connect(SceneStringName(pressed), callable_mp(this, &GameView::_keep_aspect_button_pressed)); + keep_aspect_button->connect(SceneStringName(pressed), callable_mp(this, &GameView::_size_mode_button_pressed).bind(SIZE_MODE_KEEP_ASPECT)); + + stretch_button = memnew(Button); + main_menu_hbox->add_child(stretch_button); + stretch_button->set_toggle_mode(true); + stretch_button->set_theme_type_variation("FlatButton"); + stretch_button->set_tooltip_text(TTR("Embedded game size stretches to fit the Game Workspace.")); + stretch_button->connect(SceneStringName(pressed), callable_mp(this, &GameView::_size_mode_button_pressed).bind(SIZE_MODE_STRETCH)); embed_options_menu = memnew(MenuButton); main_menu_hbox->add_child(embed_options_menu); @@ -819,8 +965,14 @@ GameView::GameView(Ref p_debugger, WindowWrapper *p_wrapper) { embedded_process->connect("embedded_process_focused", callable_mp(this, &GameView::_embedded_process_focused)); embedded_process->set_custom_minimum_size(Size2i(100, 100)); + MarginContainer *state_container = memnew(MarginContainer); + state_container->add_theme_constant_override("margin_left", 8 * EDSCALE); + state_container->add_theme_constant_override("margin_right", 8 * EDSCALE); + state_container->set_anchors_and_offsets_preset(PRESET_FULL_RECT); + panel->add_child(state_container); + state_label = memnew(Label()); - panel->add_child(state_label); + state_container->add_child(state_label); state_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); state_label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); state_label->set_autowrap_mode(TextServer::AUTOWRAP_WORD); @@ -831,7 +983,8 @@ GameView::GameView(Ref p_debugger, WindowWrapper *p_wrapper) { p_debugger->connect("session_started", callable_mp(this, &GameView::_sessions_changed)); p_debugger->connect("session_stopped", callable_mp(this, &GameView::_sessions_changed)); - p_wrapper->connect("window_before_closing", callable_mp(this, &GameView::_window_before_closing)); + p_wrapper->set_override_close_request(true); + p_wrapper->connect("window_close_requested", callable_mp(this, &GameView::_window_close_request)); p_wrapper->connect("window_size_changed", callable_mp(this, &GameView::_update_floating_window_settings)); } diff --git a/editor/plugins/game_view_plugin.h b/editor/plugins/game_view_plugin.h index 799664e56ff..f869cd0229f 100644 --- a/editor/plugins/game_view_plugin.h +++ b/editor/plugins/game_view_plugin.h @@ -96,6 +96,21 @@ class GameView : public VBoxContainer { EMBED_MAKE_FLOATING_ON_PLAY, }; + enum EmbedSizeMode { + SIZE_MODE_FIXED, + SIZE_MODE_KEEP_ASPECT, + SIZE_MODE_STRETCH, + }; + + enum EmbedAvailability { + EMBED_AVAILABLE, + EMBED_NOT_AVAILABLE_FEATURE_NOT_SUPPORTED, + EMBED_NOT_AVAILABLE_MINIMIZED, + EMBED_NOT_AVAILABLE_MAXIMIZED, + EMBED_NOT_AVAILABLE_FULLSCREEN, + EMBED_NOT_AVAILABLE_SINGLE_WINDOW_MODE, + }; + inline static GameView *singleton = nullptr; Ref debugger; @@ -108,10 +123,10 @@ class GameView : public VBoxContainer { bool embed_on_play = true; bool make_floating_on_play = true; + EmbedSizeMode embed_size_mode = SIZE_MODE_FIXED; Rect2i floating_window_rect; int floating_window_screen = -1; - Rect2i floating_window_screen_rect; Button *suspend_button = nullptr; Button *next_frame_button = nullptr; @@ -125,10 +140,11 @@ class GameView : public VBoxContainer { MenuButton *camera_override_menu = nullptr; VSeparator *embedding_separator = nullptr; + Button *fixed_size_button = nullptr; Button *keep_aspect_button = nullptr; + Button *stretch_button = nullptr; MenuButton *embed_options_menu = nullptr; Label *game_size_label = nullptr; - Panel *panel = nullptr; EmbeddedProcess *embedded_process = nullptr; Label *state_label = nullptr; @@ -142,7 +158,7 @@ class GameView : public VBoxContainer { void _node_type_pressed(int p_option); void _select_mode_pressed(int p_option); void _embed_options_menu_menu_id_pressed(int p_id); - void _keep_aspect_button_pressed(); + void _size_mode_button_pressed(int size_mode); void _play_pressed(); static void _instance_starting_static(int p_idx, List &r_arguments); @@ -152,19 +168,21 @@ class GameView : public VBoxContainer { void _embedding_failed(); void _embedded_process_updated(); void _embedded_process_focused(); - void _project_settings_changed(); + void _editor_or_project_settings_changed(); + EmbedAvailability _get_embed_available(); void _update_ui(); void _update_embed_menu_options(); void _update_embed_window_size(); void _update_arguments_for_instance(int p_idx, List &r_arguments); + void _show_update_window_wrapper(); void _hide_selection_toggled(bool p_pressed); void _camera_override_button_toggled(bool p_pressed); void _camera_override_menu_id_pressed(int p_id); - void _window_before_closing(); + void _window_close_request(); void _update_floating_window_settings(); void _attach_script_debugger(); void _detach_script_debugger(); diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index dfe6ea125de..d45a2de3695 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -3388,7 +3388,7 @@ void SceneTreeDock::_new_scene_from(const String &p_file) { copy_3d->set_rotation(Vector3(0, 0, 0)); } if (reset_scale) { - copy_3d->set_scale(Vector3(0, 0, 0)); + copy_3d->set_scale(Vector3(1, 1, 1)); } } diff --git a/editor/window_wrapper.cpp b/editor/window_wrapper.cpp index 5d8212567c5..26538dc4353 100644 --- a/editor/window_wrapper.cpp +++ b/editor/window_wrapper.cpp @@ -103,12 +103,6 @@ void WindowWrapper::_set_window_enabled_with_rect(bool p_visible, const Rect2 p_ Node *parent = _get_wrapped_control_parent(); - // In the GameView plugin, we need to the the signal before the window is actually closed - // to prevent the embedded game to be seen the parent window for a fraction of a second. - if (!p_visible) { - emit_signal("window_before_closing"); - } - if (wrapped_control->get_parent() != parent) { // Move the control to the window. wrapped_control->reparent(parent, false); @@ -122,7 +116,7 @@ void WindowWrapper::_set_window_enabled_with_rect(bool p_visible, const Rect2 p_ } window->set_visible(p_visible); - if (!p_visible) { + if (!p_visible && !override_close_request) { emit_signal("window_close_requested"); } emit_signal("window_visibility_changed", p_visible); @@ -143,10 +137,17 @@ void WindowWrapper::_window_size_changed() { emit_signal(SNAME("window_size_changed")); } +void WindowWrapper::_window_close_request() { + if (override_close_request) { + emit_signal("window_close_requested"); + } else { + set_window_enabled(false); + } +} + void WindowWrapper::_bind_methods() { ADD_SIGNAL(MethodInfo("window_visibility_changed", PropertyInfo(Variant::BOOL, "visible"))); ADD_SIGNAL(MethodInfo("window_close_requested")); - ADD_SIGNAL(MethodInfo("window_before_closing")); ADD_SIGNAL(MethodInfo("window_size_changed")); } @@ -326,12 +327,24 @@ void WindowWrapper::set_margins_enabled(bool p_enabled) { } } +Size2 WindowWrapper::get_margins_size() { + if (!margins) { + return Size2(); + } + + return Size2(margins->get_margin_size(SIDE_LEFT) + margins->get_margin_size(SIDE_RIGHT), margins->get_margin_size(SIDE_TOP) + margins->get_margin_size(SIDE_RIGHT)); +} + void WindowWrapper::grab_window_focus() { if (get_window_enabled() && is_visible()) { window->grab_focus(); } } +void WindowWrapper::set_override_close_request(bool p_enabled) { + override_close_request = p_enabled; +} + WindowWrapper::WindowWrapper() { if (!EditorNode::get_singleton()->is_multi_window_enabled()) { return; @@ -344,7 +357,7 @@ WindowWrapper::WindowWrapper() { add_child(window); window->hide(); - window->connect("close_requested", callable_mp(this, &WindowWrapper::set_window_enabled).bind(false)); + window->connect("close_requested", callable_mp(this, &WindowWrapper::_window_close_request)); window->connect("size_changed", callable_mp(this, &WindowWrapper::_window_size_changed)); ShortcutBin *capturer = memnew(ShortcutBin); diff --git a/editor/window_wrapper.h b/editor/window_wrapper.h index ccb61c70905..266c1b39ea5 100644 --- a/editor/window_wrapper.h +++ b/editor/window_wrapper.h @@ -52,12 +52,15 @@ class WindowWrapper : public MarginContainer { Ref enable_shortcut; + bool override_close_request = false; + Rect2 _get_default_window_rect() const; Node *_get_wrapped_control_parent() const; void _set_window_enabled_with_rect(bool p_visible, const Rect2 p_rect); void _set_window_rect(const Rect2 p_rect); void _window_size_changed(); + void _window_close_request(); protected: static void _bind_methods(); @@ -84,8 +87,11 @@ class WindowWrapper : public MarginContainer { void set_window_title(const String &p_title); void set_margins_enabled(bool p_enabled); + Size2 get_margins_size(); void grab_window_focus(); + void set_override_close_request(bool p_enabled); + WindowWrapper(); ~WindowWrapper(); }; diff --git a/modules/navigation/3d/nav_mesh_queries_3d.cpp b/modules/navigation/3d/nav_mesh_queries_3d.cpp index fdbff65e92d..f8c4407248f 100644 --- a/modules/navigation/3d/nav_mesh_queries_3d.cpp +++ b/modules/navigation/3d/nav_mesh_queries_3d.cpp @@ -300,7 +300,7 @@ void NavMeshQueries3D::_query_task_build_path_corridor(NavMeshPathQueryTask3D &p if ((p_navigation_layers & owner->get_navigation_layers()) != 0) { Vector3 pathway[2] = { connection.pathway_start, connection.pathway_end }; const Vector3 new_entry = Geometry3D::get_closest_point_to_segment(least_cost_poly.entry, pathway); - const real_t new_traveled_distance = least_cost_poly.entry.distance_squared_to(new_entry) * poly_travel_cost + poly_enter_cost + least_cost_poly.traveled_distance; + const real_t new_traveled_distance = least_cost_poly.entry.distance_to(new_entry) * poly_travel_cost + poly_enter_cost + least_cost_poly.traveled_distance; // Check if the neighbor polygon has already been processed. gd::NavigationPoly &neighbor_poly = navigation_polys[connection.polygon->id]; @@ -312,7 +312,7 @@ void NavMeshQueries3D::_query_task_build_path_corridor(NavMeshPathQueryTask3D &p neighbor_poly.back_navigation_edge_pathway_end = connection.pathway_end; neighbor_poly.traveled_distance = new_traveled_distance; neighbor_poly.distance_to_destination = - new_entry.distance_squared_to(end_point) * + new_entry.distance_to(end_point) * owner->get_travel_cost(); neighbor_poly.entry = new_entry; diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt index b166cf1d361..00e6f7c7375 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt @@ -73,8 +73,6 @@ abstract class BaseGodotEditor : GodotActivity() { private const val WAIT_FOR_DEBUGGER = false - @JvmStatic - protected val EXTRA_COMMAND_LINE_PARAMS = "command_line_params" @JvmStatic protected val EXTRA_PIP_AVAILABLE = "pip_available" @JvmStatic @@ -132,7 +130,6 @@ abstract class BaseGodotEditor : GodotActivity() { } private val editorMessageDispatcher = EditorMessageDispatcher(this) - private val commandLineParams = ArrayList() private val editorLoadingIndicator: View? by lazy { findViewById(R.id.editor_loading_indicator) } override fun getGodotAppLayout() = R.layout.godot_editor_layout @@ -185,10 +182,6 @@ abstract class BaseGodotEditor : GodotActivity() { // requested on demand based on use cases. PermissionsUtil.requestManifestPermissions(this, getExcludedPermissions()) - val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS) - Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}") - updateCommandLineParams(params?.asList() ?: emptyList()) - editorMessageDispatcher.parseStartIntent(packageManager, intent) if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) { @@ -221,20 +214,16 @@ abstract class BaseGodotEditor : GodotActivity() { } @CallSuper - protected open fun updateCommandLineParams(args: List) { - // Update the list of command line params with the new args - commandLineParams.clear() - if (args.isNotEmpty()) { - commandLineParams.addAll(args) - } - if (BuildConfig.BUILD_TYPE == "dev") { - commandLineParams.add("--benchmark") + protected override fun updateCommandLineParams(args: Array) { + val args = if (BuildConfig.BUILD_TYPE == "dev") { + args + "--benchmark" + } else { + args } + super.updateCommandLineParams(args); } - final override fun getCommandLine() = commandLineParams - - protected fun retrieveEditorWindowInfo(args: Array): EditorWindowInfo { + protected open fun retrieveEditorWindowInfo(args: Array): EditorWindowInfo { var hasEditor = false var xrMode = XR_MODE_DEFAULT @@ -337,14 +326,7 @@ abstract class BaseGodotEditor : GodotActivity() { val newInstance = getNewGodotInstanceIntent(editorWindowInfo, args) if (editorWindowInfo.windowClassName == javaClass.name) { Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") - val godot = godot - if (godot != null) { - godot.destroyAndKillProcess { - ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance) - } - } else { - ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance) - } + triggerRebirth(activityOptions?.toBundle(), newInstance) } else { Log.d(TAG, "Starting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") newInstance.putExtra(EXTRA_NEW_LAUNCH, true) diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotXRGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotXRGame.kt index 26f589f3471..3272d4e0c9f 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotXRGame.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotXRGame.kt @@ -42,7 +42,7 @@ open class GodotXRGame: GodotGame() { override fun overrideOrientationRequest() = true - override fun updateCommandLineParams(args: List) { + override fun updateCommandLineParams(args: Array) { val updatedArgs = ArrayList() if (!args.contains(XRMode.OPENXR.cmdLineArg)) { updatedArgs.add(XRMode.OPENXR.cmdLineArg) @@ -53,7 +53,7 @@ open class GodotXRGame: GodotGame() { } updatedArgs.addAll(args) - super.updateCommandLineParams(updatedArgs) + super.updateCommandLineParams(updatedArgs.toTypedArray()) } override fun getEditorWindowInfo() = XR_RUN_GAME_INFO 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 1de172c8618..b7f512c9ee6 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -825,9 +825,26 @@ class Godot(private val context: Context) { * Returns true if `Vulkan` is used for rendering. */ private fun usesVulkan(): Boolean { - val renderer = GodotLib.getGlobal("rendering/renderer/rendering_method") - val renderingDevice = GodotLib.getGlobal("rendering/rendering_device/driver") - return ("forward_plus" == renderer || "mobile" == renderer) && "vulkan" == renderingDevice + var rendererSource = "ProjectSettings" + var renderer = GodotLib.getGlobal("rendering/renderer/rendering_method") + var renderingDeviceSource = "ProjectSettings" + var renderingDevice = GodotLib.getGlobal("rendering/rendering_device/driver") + val cmdline = getCommandLine() + var index = cmdline.indexOf("--rendering-method") + if (index > -1 && cmdline.size > index + 1) { + rendererSource = "CommandLine" + renderer = cmdline.get(index + 1) + } + index = cmdline.indexOf("--rendering-driver") + if (index > -1 && cmdline.size > index + 1) { + renderingDeviceSource = "CommandLine" + renderingDevice = cmdline.get(index + 1) + } + val result = ("forward_plus" == renderer || "mobile" == renderer) && "vulkan" == renderingDevice + Log.d(TAG, """usesVulkan(): ${result} + renderingDevice: ${renderingDevice} (${renderingDeviceSource}) + renderer: ${renderer} (${rendererSource})""") + return result } /** 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 370f37f8721..b57c3e5f033 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt @@ -33,6 +33,7 @@ package org.redotengine.godot import android.app.Activity +import android.content.ComponentName import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle @@ -54,18 +55,32 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { companion object { private val TAG = GodotActivity::class.java.simpleName + @JvmStatic + protected val EXTRA_COMMAND_LINE_PARAMS = "command_line_params" + @JvmStatic protected val EXTRA_NEW_LAUNCH = "new_launch_requested" + + // This window must not match those in BaseGodotEditor.RUN_GAME_INFO etc + @JvmStatic + private final val DEFAULT_WINDOW_ID = 664; } + private val commandLineParams = ArrayList() /** * Interaction with the [Godot] object is delegated to the [GodotFragment] class. */ protected var godotFragment: GodotFragment? = null private set + @CallSuper override fun onCreate(savedInstanceState: Bundle?) { + val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS) + Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}") + updateCommandLineParams(params ?: emptyArray()) + super.onCreate(savedInstanceState) + setContentView(getGodotAppLayout()) handleStartIntent(intent, true) @@ -81,6 +96,29 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { } } + override fun onNewGodotInstanceRequested(args: Array): Int { + Log.d(TAG, "Restarting with parameters ${args.contentToString()}") + val intent = Intent() + .setComponent(ComponentName(this, javaClass.name)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(EXTRA_COMMAND_LINE_PARAMS, args) + triggerRebirth(null, intent) + // fake 'process' id returned by create_instance() etc + return DEFAULT_WINDOW_ID; + } + + protected fun triggerRebirth(bundle: Bundle?, intent: Intent) { + // Launch a new activity + val godot = godot + if (godot != null) { + godot.destroyAndKillProcess { + ProcessPhoenix.triggerRebirth(this, bundle, intent) + } + } else { + ProcessPhoenix.triggerRebirth(this, bundle, intent) + } + } + @LayoutRes protected open fun getGodotAppLayout() = R.layout.godot_app_layout @@ -178,4 +216,15 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { protected open fun initGodotInstance(): GodotFragment { return GodotFragment() } + + @CallSuper + protected open fun updateCommandLineParams(args: Array) { + // Update the list of command line params with the new args + commandLineParams.clear() + if (args.isNotEmpty()) { + commandLineParams.addAll(args) + } + } + + final override fun getCommandLine() = commandLineParams } diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp index 12422ec3b18..8d7da05f451 100644 --- a/platform/linuxbsd/wayland/wayland_thread.cpp +++ b/platform/linuxbsd/wayland/wayland_thread.cpp @@ -4121,6 +4121,7 @@ void WaylandThread::selection_set_text(const String &p_text) { if (registry.wl_data_device_manager == nullptr) { DEBUG_LOG_WAYLAND_THREAD("Couldn't set selection, wl_data_device_manager global not available."); + return; } if (ss == nullptr) { @@ -4248,6 +4249,11 @@ void WaylandThread::primary_set_text(const String &p_text) { return; } + if (ss->wp_primary_selection_device == nullptr) { + DEBUG_LOG_WAYLAND_THREAD("Couldn't set primary selection, seat doesn't have wp_primary_selection_device."); + return; + } + ss->primary_data = p_text.to_utf8_buffer(); if (ss->wp_primary_selection_source == nullptr) { @@ -4255,10 +4261,10 @@ void WaylandThread::primary_set_text(const String &p_text) { zwp_primary_selection_source_v1_add_listener(ss->wp_primary_selection_source, &wp_primary_selection_source_listener, ss); zwp_primary_selection_source_v1_offer(ss->wp_primary_selection_source, "text/plain;charset=utf-8"); zwp_primary_selection_source_v1_offer(ss->wp_primary_selection_source, "text/plain"); - } - // TODO: Implement a good way of getting the latest serial from the user. - zwp_primary_selection_device_v1_set_selection(ss->wp_primary_selection_device, ss->wp_primary_selection_source, MAX(ss->pointer_data.button_serial, ss->last_key_pressed_serial)); + // TODO: Implement a good way of getting the latest serial from the user. + zwp_primary_selection_device_v1_set_selection(ss->wp_primary_selection_device, ss->wp_primary_selection_source, MAX(ss->pointer_data.button_serial, ss->last_key_pressed_serial)); + } // Wait for the message to get to the server before continuing, otherwise the // clipboard update might come with a delay. diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index 3f27051803d..295654b2103 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -5851,6 +5851,24 @@ Error DisplayServerX11::remove_embedded_process(OS::ProcessID p_pid) { } EmbeddedProcessData *ep = embedded_processes.get(p_pid); + + // Handle bad window errors silently because just in case the embedded window was closed. + int (*oldHandler)(Display *, XErrorEvent *) = XSetErrorHandler(&bad_window_error_handler); + + // Send the message to gracefully close the window. + XEvent ev; + memset(&ev, 0, sizeof(ev)); + ev.xclient.type = ClientMessage; + ev.xclient.window = ep->process_window; + ev.xclient.message_type = XInternAtom(x11_display, "WM_PROTOCOLS", True); + ev.xclient.format = 32; + ev.xclient.data.l[0] = XInternAtom(x11_display, "WM_DELETE_WINDOW", False); + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(x11_display, ep->process_window, False, NoEventMask, &ev); + + // Restore default error handler. + XSetErrorHandler(oldHandler); + embedded_processes.erase(p_pid); memdelete(ep); diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 81413af0e6b..9d97727b6d6 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -2977,6 +2977,9 @@ Error DisplayServerWindows::remove_embedded_process(OS::ProcessID p_pid) { EmbeddedProcessData *ep = embedded_processes.get(p_pid); + // Send a close message to gracefully close the process. + PostMessage(ep->window_handle, WM_CLOSE, 0, 0); + // This is a workaround to ensure the parent window correctly regains focus after the // embedded window is closed. When the embedded window is closed while it has focus, // the parent window (the editor) does not become active. It appears focused but is not truly activated. diff --git a/scene/2d/camera_2d.cpp b/scene/2d/camera_2d.cpp index 905f937be00..1464a13863f 100644 --- a/scene/2d/camera_2d.cpp +++ b/scene/2d/camera_2d.cpp @@ -317,7 +317,9 @@ void Camera2D::_notification(int p_what) { } if (is_physics_interpolated_and_enabled()) { _ensure_update_interpolation_data(); - _interpolation_data.xform_curr = get_camera_transform(); + if (Engine::get_singleton()->is_in_physics_frame()) { + _interpolation_data.xform_curr = get_camera_transform(); + } } } break; diff --git a/scene/2d/cpu_particles_2d.cpp b/scene/2d/cpu_particles_2d.cpp index 86e6553078a..3913bbd04d4 100644 --- a/scene/2d/cpu_particles_2d.cpp +++ b/scene/2d/cpu_particles_2d.cpp @@ -34,6 +34,7 @@ #include "cpu_particles_2d.compat.inc" #include "core/math/random_number_generator.h" +#include "core/math/transform_interpolator.h" #include "scene/2d/gpu_particles_2d.h" #include "scene/resources/atlas_texture.h" #include "scene/resources/canvas_item_material.h" @@ -98,7 +99,14 @@ void CPUParticles2D::set_lifetime_randomness(double p_random) { void CPUParticles2D::set_use_local_coordinates(bool p_enable) { local_coords = p_enable; - set_notify_transform(!p_enable); + + // Prevent sending item transforms when using global coords, + // and inform the RenderingServer to use identity mode. + set_canvas_item_use_identity_transform(!local_coords); + + // We only need NOTIFICATION_TRANSFORM_CHANGED + // when following an interpolated target. + set_notify_transform(_interpolation_data.interpolated_follow); } void CPUParticles2D::set_speed_scale(double p_scale) { @@ -230,6 +238,27 @@ void CPUParticles2D::_texture_changed() { } } +void CPUParticles2D::_refresh_interpolation_state() { + if (!is_inside_tree()) { + return; + } + + // The logic for whether to do an interpolated follow. + // This is rather complex, but basically: + // If project setting interpolation is ON and this particle system is in global mode, + // we will follow the INTERPOLATED position rather than the actual position. + // This is so that particles aren't generated AHEAD of the interpolated parent. + bool follow = !local_coords && get_tree()->is_physics_interpolation_enabled(); + + if (follow == _interpolation_data.interpolated_follow) { + return; + } + + _interpolation_data.interpolated_follow = follow; + + set_physics_process_internal(_interpolation_data.interpolated_follow); +} + Ref CPUParticles2D::get_texture() const { return texture; } @@ -602,6 +631,9 @@ void CPUParticles2D::_update_internal() { return; } + // Change update mode? + _refresh_interpolation_state(); + double delta = get_process_delta_time(); if (!active && !emitting) { set_process_internal(false); @@ -681,7 +713,11 @@ void CPUParticles2D::_particles_process(double p_delta) { Transform2D emission_xform; Transform2D velocity_xform; if (!local_coords) { - emission_xform = get_global_transform(); + if (!_interpolation_data.interpolated_follow) { + emission_xform = get_global_transform(); + } else { + TransformInterpolator::interpolate_transform_2d(_interpolation_data.global_xform_prev, _interpolation_data.global_xform_curr, emission_xform, Engine::get_singleton()->get_physics_interpolation_fraction()); + } velocity_xform = emission_xform; velocity_xform[2] = Vector2(); } @@ -1144,6 +1180,17 @@ void CPUParticles2D::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { set_process_internal(emitting); + + _refresh_interpolation_state(); + + set_physics_process_internal(emitting && _interpolation_data.interpolated_follow); + + // If we are interpolated following, then reset physics interpolation + // when first appearing. This won't be called by canvas item, as in the + // following mode, is_physics_interpolated() is actually FALSE. + if (_interpolation_data.interpolated_follow) { + notification(NOTIFICATION_RESET_PHYSICS_INTERPOLATION); + } } break; case NOTIFICATION_EXIT_TREE: { @@ -1172,37 +1219,28 @@ void CPUParticles2D::_notification(int p_what) { _update_internal(); } break; - case NOTIFICATION_TRANSFORM_CHANGED: { - inv_emission_transform = get_global_transform().affine_inverse(); - - if (!local_coords) { - int pc = particles.size(); - - float *w = particle_data.ptrw(); - const Particle *r = particles.ptr(); - float *ptr = w; - - for (int i = 0; i < pc; i++) { - Transform2D t = inv_emission_transform * r[i].transform; - - if (r[i].active) { - ptr[0] = t.columns[0][0]; - ptr[1] = t.columns[1][0]; - ptr[2] = 0; - ptr[3] = t.columns[2][0]; - ptr[4] = t.columns[0][1]; - ptr[5] = t.columns[1][1]; - ptr[6] = 0; - ptr[7] = t.columns[2][1]; - - } else { - memset(ptr, 0, sizeof(float) * 8); - } + case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: { + if (_interpolation_data.interpolated_follow) { + // Keep the interpolated follow target updated. + _interpolation_data.global_xform_prev = _interpolation_data.global_xform_curr; + _interpolation_data.global_xform_curr = get_global_transform(); + } + } break; - ptr += 16; + case NOTIFICATION_TRANSFORM_CHANGED: { + if (_interpolation_data.interpolated_follow) { + // If the transform has been updated AFTER the physics tick, keep data flowing. + if (Engine::get_singleton()->is_in_physics_frame()) { + _interpolation_data.global_xform_curr = get_global_transform(); } } } break; + + case NOTIFICATION_RESET_PHYSICS_INTERPOLATION: { + // Make sure current is up to date with any pending global transform changes. + _interpolation_data.global_xform_curr = get_global_transform_const(); + _interpolation_data.global_xform_prev = _interpolation_data.global_xform_curr; + } break; } } @@ -1561,6 +1599,12 @@ CPUParticles2D::CPUParticles2D() { set_color(Color(1, 1, 1, 1)); _update_mesh_texture(); + + // CPUParticles2D defaults to interpolation off. + // This is because the result often looks better when the particles are updated every frame. + // Note that children will need to explicitly turn back on interpolation if they want to use it, + // rather than relying on inherit mode. + set_physics_interpolation_mode(Node::PHYSICS_INTERPOLATION_MODE_OFF); } CPUParticles2D::~CPUParticles2D() { diff --git a/scene/2d/cpu_particles_2d.h b/scene/2d/cpu_particles_2d.h index 68879d7ab07..9985e9184f3 100644 --- a/scene/2d/cpu_particles_2d.h +++ b/scene/2d/cpu_particles_2d.h @@ -191,6 +191,15 @@ class CPUParticles2D : public Node2D { Mutex update_mutex; + struct InterpolationData { + // Whether this particle is non-interpolated, but following an interpolated parent. + bool interpolated_follow = false; + + // If doing interpolated follow, we need to keep these updated per tick. + Transform2D global_xform_curr; + Transform2D global_xform_prev; + } _interpolation_data; + void _update_render_thread(); void _update_mesh_texture(); @@ -199,6 +208,8 @@ class CPUParticles2D : public Node2D { void _texture_changed(); + void _refresh_interpolation_state(); + protected: static void _bind_methods(); void _notification(int p_what); diff --git a/scene/2d/gpu_particles_2d.cpp b/scene/2d/gpu_particles_2d.cpp index e11bac98bb8..7c4d6323d10 100644 --- a/scene/2d/gpu_particles_2d.cpp +++ b/scene/2d/gpu_particles_2d.cpp @@ -914,6 +914,8 @@ GPUParticles2D::GPUParticles2D() { one_shot = false; // Needed so that set_emitting doesn't access uninitialized values set_emitting(true); set_one_shot(false); + set_use_fixed_seed(false); + set_seed(0); set_amount(8); set_amount_ratio(1.0); set_lifetime(1); diff --git a/scene/2d/node_2d.cpp b/scene/2d/node_2d.cpp index ab8136418ed..7aaa84bb4d7 100644 --- a/scene/2d/node_2d.cpp +++ b/scene/2d/node_2d.cpp @@ -376,7 +376,9 @@ void Node2D::set_transform(const Transform2D &p_transform) { transform = p_transform; _set_xform_dirty(true); - RenderingServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), transform); + if (!_is_using_identity_transform()) { + RenderingServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), transform); + } _notify_transform(); } diff --git a/scene/3d/gpu_particles_3d.cpp b/scene/3d/gpu_particles_3d.cpp index 2dd9277c493..95e1c7548e9 100644 --- a/scene/3d/gpu_particles_3d.cpp +++ b/scene/3d/gpu_particles_3d.cpp @@ -94,10 +94,6 @@ void GPUParticles3D::set_one_shot(bool p_one_shot) { if (is_emitting()) { if (!one_shot) { - if (!use_fixed_seed) { - set_seed(Math::rand()); - } - RenderingServer::get_singleton()->particles_restart(particles); } } diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 9d67bbee329..246e2dceaef 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -277,7 +277,6 @@ void CodeEdit::_notification(int p_what) { } break; case NOTIFICATION_MOUSE_EXIT: { - queue_redraw(); symbol_tooltip_timer->stop(); } break; } diff --git a/scene/gui/color_picker.cpp b/scene/gui/color_picker.cpp index ba4b48c9a8b..6fb3db7c0eb 100644 --- a/scene/gui/color_picker.cpp +++ b/scene/gui/color_picker.cpp @@ -2379,13 +2379,10 @@ ColorPicker::ColorPicker() { btn_preset->set_toggle_mode(true); btn_preset->set_focus_mode(FOCUS_NONE); btn_preset->set_text_alignment(HORIZONTAL_ALIGNMENT_LEFT); + btn_preset->set_h_size_flags(SIZE_EXPAND_FILL); btn_preset->connect(SceneStringName(toggled), callable_mp(this, &ColorPicker::_show_hide_preset).bind(btn_preset, preset_container)); palette_box->add_child(btn_preset); - HBoxContainer *padding_box = memnew(HBoxContainer); - padding_box->set_h_size_flags(SIZE_EXPAND_FILL); - palette_box->add_child(padding_box); - menu_btn = memnew(Button); menu_btn->set_flat(true); menu_btn->set_tooltip_text(ETR("Show all options available.")); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index f7bb13f05d6..975c17d669a 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -1677,6 +1677,10 @@ void TextEdit::_notification(int p_what) { drag_caret_force_displayed = false; queue_redraw(); } + if (hovered_gutter != Vector2i(-1, -1)) { + hovered_gutter = Vector2i(-1, -1); + queue_redraw(); + } } break; } } @@ -1845,18 +1849,18 @@ void TextEdit::gui_input(const Ref &p_gui_input) { int col = pos.x; // Gutters. + Vector2i current_hovered_gutter = _get_hovered_gutter(mpos); + if (current_hovered_gutter != hovered_gutter) { + hovered_gutter = current_hovered_gutter; + queue_redraw(); + } + if (hovered_gutter != Vector2i(-1, -1)) { + emit_signal(SNAME("gutter_clicked"), hovered_gutter.y, hovered_gutter.x); + return; + } int left_margin = theme_cache.style_normal->get_margin(SIDE_LEFT); - for (int i = 0; i < gutters.size(); i++) { - if (!gutters[i].draw || gutters[i].width <= 0) { - continue; - } - - if (mpos.x >= left_margin && mpos.x <= left_margin + gutters[i].width) { - emit_signal(SNAME("gutter_clicked"), line, i); - return; - } - - left_margin += gutters[i].width; + if (mpos.x < left_margin + gutters_width + gutter_padding) { + return; } // Minimap. @@ -2068,27 +2072,8 @@ void TextEdit::gui_input(const Ref &p_gui_input) { } } - // Check if user is hovering a different gutter, and update if yes. - Vector2i current_hovered_gutter = Vector2i(-1, -1); - - int left_margin = theme_cache.style_normal->get_margin(SIDE_LEFT); - if (mpos.x <= left_margin + gutters_width + gutter_padding) { - int hovered_row = get_line_column_at_pos(mpos).y; - for (int i = 0; i < gutters.size(); i++) { - if (!gutters[i].draw || gutters[i].width <= 0) { - continue; - } - - if (mpos.x >= left_margin && mpos.x < left_margin + gutters[i].width) { - // We are in this gutter i's horizontal area. - current_hovered_gutter = Vector2i(i, hovered_row); - break; - } - - left_margin += gutters[i].width; - } - } - + // Update hovered gutter. + Vector2i current_hovered_gutter = _get_hovered_gutter(mpos); if (current_hovered_gutter != hovered_gutter) { hovered_gutter = current_hovered_gutter; queue_redraw(); @@ -3117,24 +3102,16 @@ void TextEdit::drop_data(const Point2 &p_point, const Variant &p_data) { } Control::CursorShape TextEdit::get_cursor_shape(const Point2 &p_pos) const { - Point2i pos = get_line_column_at_pos(p_pos); - int row = pos.y; - - int left_margin = theme_cache.style_normal->get_margin(SIDE_LEFT); - int gutter = left_margin + gutters_width; - if (p_pos.x < gutter) { - for (int i = 0; i < gutters.size(); i++) { - if (!gutters[i].draw) { - continue; - } - - if (p_pos.x >= left_margin && p_pos.x < left_margin + gutters[i].width) { - if (gutters[i].clickable || is_line_gutter_clickable(row, i)) { - return CURSOR_POINTING_HAND; - } - } - left_margin += gutters[i].width; + Vector2i current_hovered_gutter = _get_hovered_gutter(p_pos); + if (current_hovered_gutter != Vector2i(-1, -1)) { + if (gutters[current_hovered_gutter.x].clickable || is_line_gutter_clickable(current_hovered_gutter.y, current_hovered_gutter.x)) { + return CURSOR_POINTING_HAND; + } else { + return CURSOR_ARROW; } + } + int left_margin = theme_cache.style_normal->get_margin(SIDE_LEFT); + if (p_pos.x < left_margin + gutters_width + gutter_padding) { return CURSOR_ARROW; } @@ -8323,9 +8300,35 @@ void TextEdit::_update_gutter_width() { if (gutters_width > 0) { gutter_padding = 2; } + if (get_viewport()) { + hovered_gutter = _get_hovered_gutter(get_local_mouse_position()); + } queue_redraw(); } +Vector2i TextEdit::_get_hovered_gutter(const Point2 &p_mouse_pos) const { + int left_margin = theme_cache.style_normal->get_margin(SIDE_LEFT); + if (p_mouse_pos.x > left_margin + gutters_width + gutter_padding) { + return Vector2i(-1, -1); + } + int hovered_row = get_line_column_at_pos(p_mouse_pos, false).y; + if (hovered_row == -1) { + return Vector2i(-1, -1); + } + for (int i = 0; i < gutters.size(); i++) { + if (!gutters[i].draw || gutters[i].width <= 0) { + continue; + } + + if (p_mouse_pos.x >= left_margin && p_mouse_pos.x < left_margin + gutters[i].width) { + return Vector2i(i, hovered_row); + } + + left_margin += gutters[i].width; + } + return Vector2i(-1, -1); +} + /* Syntax highlighting. */ Vector> TextEdit::_get_line_syntax_highlighting(int p_line) { if (syntax_highlighter.is_null() || setting_text) { diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 735c3ba6182..d76fefbc0cd 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -567,6 +567,7 @@ class TextEdit : public Control { Vector2i hovered_gutter = Vector2i(-1, -1); // X = gutter index, Y = row. void _update_gutter_width(); + Vector2i _get_hovered_gutter(const Point2 &p_mouse_pos) const; /* Syntax highlighting. */ Ref syntax_highlighter; diff --git a/scene/gui/texture_progress_bar.cpp b/scene/gui/texture_progress_bar.cpp index 1ddc3cc565c..6da836abbf8 100644 --- a/scene/gui/texture_progress_bar.cpp +++ b/scene/gui/texture_progress_bar.cpp @@ -83,15 +83,19 @@ bool TextureProgressBar::get_nine_patch_stretch() const { Size2 TextureProgressBar::get_minimum_size() const { if (nine_patch_stretch) { return Size2(stretch_margin[SIDE_LEFT] + stretch_margin[SIDE_RIGHT], stretch_margin[SIDE_TOP] + stretch_margin[SIDE_BOTTOM]); - } else if (under.is_valid()) { - return under->get_size(); - } else if (over.is_valid()) { - return over->get_size(); - } else if (progress.is_valid()) { - return progress->get_size(); } - return Size2(1, 1); + Size2 size = Size2(1, 1); + if (under.is_valid()) { + size = size.max(under->get_size()); + } + if (progress.is_valid()) { + size = size.max(progress->get_size()); + } + if (over.is_valid()) { + size = size.max(over->get_size()); + } + return size; } void TextureProgressBar::set_progress_texture(const Ref &p_texture) { diff --git a/scene/main/canvas_item.cpp b/scene/main/canvas_item.cpp index 3c2abdcecb8..2d58e8a7c51 100644 --- a/scene/main/canvas_item.cpp +++ b/scene/main/canvas_item.cpp @@ -190,6 +190,20 @@ Transform2D CanvasItem::get_global_transform() const { return global_transform; } +// Same as get_global_transform() but no reset for `global_invalid`. +Transform2D CanvasItem::get_global_transform_const() const { + if (_is_global_invalid()) { + const CanvasItem *pi = get_parent_item(); + if (pi) { + global_transform = pi->get_global_transform_const() * get_transform(); + } else { + global_transform = get_transform(); + } + } + + return global_transform; +} + void CanvasItem::_set_global_invalid(bool p_invalid) const { if (is_group_processing()) { if (p_invalid) { @@ -1041,6 +1055,24 @@ void CanvasItem::_physics_interpolated_changed() { RenderingServer::get_singleton()->canvas_item_set_interpolated(canvas_item, is_physics_interpolated()); } +void CanvasItem::set_canvas_item_use_identity_transform(bool p_enable) { + // Prevent sending item transforms to RenderingServer when using global coords. + _set_use_identity_transform(p_enable); + + // Let RenderingServer know not to concatenate the parent transform during the render. + RenderingServer::get_singleton()->canvas_item_set_use_identity_transform(get_canvas_item(), p_enable); + + if (is_inside_tree()) { + if (p_enable) { + // Make sure item is using identity transform in server. + RenderingServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), Transform2D()); + } else { + // Make sure item transform is up to date in server if switching identity transform off. + RenderingServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), get_transform()); + } + } +} + Rect2 CanvasItem::get_viewport_rect() const { ERR_READ_THREAD_GUARD_V(Rect2()); ERR_FAIL_COND_V(!is_inside_tree(), Rect2()); diff --git a/scene/main/canvas_item.h b/scene/main/canvas_item.h index bb90a148582..f03efc001c3 100644 --- a/scene/main/canvas_item.h +++ b/scene/main/canvas_item.h @@ -172,6 +172,8 @@ class CanvasItem : public Node { void item_rect_changed(bool p_size_changed = true); + void set_canvas_item_use_identity_transform(bool p_enable); + void _notification(int p_what); static void _bind_methods(); @@ -341,6 +343,7 @@ class CanvasItem : public Node { virtual Transform2D get_transform() const = 0; virtual Transform2D get_global_transform() const; + virtual Transform2D get_global_transform_const() const; virtual Transform2D get_global_transform_with_canvas() const; virtual Transform2D get_screen_transform() const; diff --git a/scene/resources/visual_shader_nodes.cpp b/scene/resources/visual_shader_nodes.cpp index 2883e57aefa..ee2bd832cc8 100644 --- a/scene/resources/visual_shader_nodes.cpp +++ b/scene/resources/visual_shader_nodes.cpp @@ -1759,12 +1759,13 @@ String VisualShaderNodeWorldPositionFromDepth::generate_code(Shader::Mode p_mode code += " float __log_depth = textureLod(" + make_unique_id(p_type, p_id, "depth_tex") + ", " + uv + ", 0.0).x;\n"; if (!RenderingServer::get_singleton()->is_low_end()) { - code += " vec4 __depth_view = INV_PROJECTION_MATRIX * vec4(" + uv + " * 2.0 - 1.0, __log_depth, 1.0);\n"; + code += " vec4 __ndc = vec4(" + uv + " * 2.0 - 1.0, __log_depth, 1.0);\n"; } else { - code += " vec4 __depth_view = INV_PROJECTION_MATRIX * vec4(vec3(" + uv + ", __log_depth) * 2.0 - 1.0, 1.0);\n"; + code += " vec4 __ndc = vec4(vec3(" + uv + ", __log_depth) * 2.0 - 1.0, 1.0);\n"; } - code += " __depth_view.xyz /= __depth_view.w;\n"; - code += vformat(" %s = (INV_VIEW_MATRIX * __depth_view).xyz;\n", p_output_vars[0]); + code += " vec4 __position_world = INV_VIEW_MATRIX * INV_PROJECTION_MATRIX * __ndc;\n"; + code += " __position_world.xyz /= __position_world.w;\n"; + code += vformat(" %s = __position_world.xyz;\n", p_output_vars[0]); code += " }\n"; return code; diff --git a/servers/rendering/renderer_canvas_cull.cpp b/servers/rendering/renderer_canvas_cull.cpp index bbd8afccb1e..5a09aba84d6 100644 --- a/servers/rendering/renderer_canvas_cull.cpp +++ b/servers/rendering/renderer_canvas_cull.cpp @@ -72,6 +72,11 @@ void RendererCanvasCull::_dependency_deleted(const RID &p_dependency, Dependency void RendererCanvasCull::_render_canvas_item_tree(RID p_to_render_target, Canvas::ChildItem *p_child_items, int p_child_item_count, const Transform2D &p_transform, const Rect2 &p_clip_rect, const Color &p_modulate, RendererCanvasRender::Light *p_lights, RendererCanvasRender::Light *p_directional_lights, RenderingServer::CanvasItemTextureFilter p_default_filter, RenderingServer::CanvasItemTextureRepeat p_default_repeat, bool p_snap_2d_vertices_to_pixel, uint32_t p_canvas_cull_mask, RenderingMethod::RenderInfo *r_render_info) { RENDER_TIMESTAMP("Cull CanvasItem Tree"); + // This is used to avoid passing the camera transform down the rendering + // function calls, as it won't be used in 99% of cases, because the camera + // transform is normally concatenated with the item global transform. + _current_camera_transform = p_transform; + memset(z_list, 0, z_range * sizeof(RendererCanvasRender::Item *)); memset(z_last_list, 0, z_range * sizeof(RendererCanvasRender::Item *)); @@ -244,14 +249,14 @@ void RendererCanvasCull::_attach_canvas_item_for_draw(RendererCanvasCull::Item * } if (((ci->commands != nullptr || ci->visibility_notifier) && p_clip_rect.intersects(p_global_rect, true)) || ci->vp_render || ci->copy_back_buffer) { - //something to draw? + // Something to draw? if (ci->update_when_visible) { RenderingServerDefault::redraw_request(); } if (ci->commands != nullptr || ci->copy_back_buffer) { - ci->final_transform = p_transform; + ci->final_transform = !ci->use_identity_transform ? p_transform : _current_camera_transform; ci->final_modulate = p_modulate * ci->self_modulate; ci->global_rect_cache = p_global_rect; ci->global_rect_cache.position -= p_clip_rect.position; @@ -324,6 +329,10 @@ void RendererCanvasCull::_cull_canvas_item(Item *p_canvas_item, const Transform2 } } + // Always calculate final transform as if not using identity xform. + // This is so the expected transform is passed to children. + // However, if use_identity_xform is set, + // we can override the transform for rendering purposes for this item only. Transform2D self_xform; Transform2D final_xform; if (p_is_already_y_sorted) { @@ -362,7 +371,12 @@ void RendererCanvasCull::_cull_canvas_item(Item *p_canvas_item, const Transform2 ci->repeat_source_item = repeat_source_item; } - Rect2 global_rect = final_xform.xform(rect); + Rect2 global_rect; + if (!p_canvas_item->use_identity_transform) { + global_rect = final_xform.xform(rect); + } else { + global_rect = _current_camera_transform.xform(rect); + } if (repeat_source_item && (repeat_size.x || repeat_size.y)) { // Top-left repeated rect. Rect2 corner_rect = global_rect; @@ -688,6 +702,13 @@ void RendererCanvasCull::canvas_item_set_draw_behind_parent(RID p_item, bool p_e canvas_item->behind = p_enable; } +void RendererCanvasCull::canvas_item_set_use_identity_transform(RID p_item, bool p_enable) { + Item *canvas_item = canvas_item_owner.get_or_null(p_item); + ERR_FAIL_NULL(canvas_item); + + canvas_item->use_identity_transform = p_enable; +} + void RendererCanvasCull::canvas_item_set_update_when_visible(RID p_item, bool p_update) { Item *canvas_item = canvas_item_owner.get_or_null(p_item); ERR_FAIL_NULL(canvas_item); diff --git a/servers/rendering/renderer_canvas_cull.h b/servers/rendering/renderer_canvas_cull.h index 1ed3b5bd495..420e7f100ff 100644 --- a/servers/rendering/renderer_canvas_cull.h +++ b/servers/rendering/renderer_canvas_cull.h @@ -219,6 +219,8 @@ class RendererCanvasCull { RendererCanvasRender::Item **z_list; RendererCanvasRender::Item **z_last_list; + Transform2D _current_camera_transform; + public: void render_canvas(RID p_render_target, Canvas *p_canvas, const Transform2D &p_transform, RendererCanvasRender::Light *p_lights, RendererCanvasRender::Light *p_directional_lights, const Rect2 &p_clip_rect, RS::CanvasItemTextureFilter p_default_filter, RS::CanvasItemTextureRepeat p_default_repeat, bool p_snap_2d_transforms_to_pixel, bool p_snap_2d_vertices_to_pixel, uint32_t p_canvas_cull_mask, RenderingMethod::RenderInfo *r_render_info = nullptr); @@ -252,6 +254,7 @@ class RendererCanvasCull { void canvas_item_set_self_modulate(RID p_item, const Color &p_color); void canvas_item_set_draw_behind_parent(RID p_item, bool p_enable); + void canvas_item_set_use_identity_transform(RID p_item, bool p_enable); void canvas_item_set_update_when_visible(RID p_item, bool p_update); diff --git a/servers/rendering/renderer_canvas_render.h b/servers/rendering/renderer_canvas_render.h index 47f630b317e..6a2917c16ef 100644 --- a/servers/rendering/renderer_canvas_render.h +++ b/servers/rendering/renderer_canvas_render.h @@ -323,6 +323,7 @@ class RendererCanvasRender { bool update_when_visible : 1; bool on_interpolate_transform_list : 1; bool interpolated : 1; + bool use_identity_transform : 1; struct CanvasGroup { RS::CanvasGroupMode mode; @@ -488,6 +489,7 @@ class RendererCanvasRender { repeat_source = false; on_interpolate_transform_list = false; interpolated = true; + use_identity_transform = false; } virtual ~Item() { clear(); diff --git a/servers/rendering/renderer_rd/renderer_canvas_render_rd.cpp b/servers/rendering/renderer_rd/renderer_canvas_render_rd.cpp index 0e6029f286b..ea37296b07b 100644 --- a/servers/rendering/renderer_rd/renderer_canvas_render_rd.cpp +++ b/servers/rendering/renderer_rd/renderer_canvas_render_rd.cpp @@ -2973,8 +2973,23 @@ void RendererCanvasRenderRD::_uniform_set_invalidation_callback(void *p_userdata static_cast(singleton)->rid_set_to_uniform_set.erase(*key); } +void RendererCanvasRenderRD::_canvas_texture_invalidation_callback(bool p_deleted, void *p_userdata) { + KeyValue> *kv = static_cast> *>(p_userdata); + RD *rd = RD::get_singleton(); + for (RID rid : kv->value) { + // the invalidation callback will take care of clearing rid_set_to_uniform_set cache also + rd->free(rid); + } + kv->value.clear(); + if (p_deleted) { + static_cast(singleton)->canvas_texture_to_uniform_set.erase(kv->key); + } +} + void RendererCanvasRenderRD::_render_batch(RD::DrawListID p_draw_list, CanvasShaderData *p_shader_data, RenderingDevice::FramebufferFormatID p_framebuffer_format, Light *p_lights, Batch const *p_batch, RenderingMethod::RenderInfo *r_render_info) { { + RendererRD::TextureStorage *ts = RendererRD::TextureStorage::get_singleton(); + RIDSetKey key( p_batch->tex_info->state, state.canvas_instance_data_buffers[state.current_data_buffer_index].instance_buffers[p_batch->instance_buffer_index]); @@ -2994,6 +3009,19 @@ void RendererCanvasRenderRD::_render_batch(RD::DrawListID p_draw_list, CanvasSha const RIDCache::Pair *iter = rid_set_to_uniform_set.insert(key, rid); uniform_set = &iter->data; RD::get_singleton()->uniform_set_set_invalidation_callback(rid, RendererCanvasRenderRD::_uniform_set_invalidation_callback, (void *)&iter->key); + + // If this is a CanvasTexture, it must be tracked so that any changes to the diffuse, normal + // or specular channels invalidate all associated uniform sets. + if (ts->owns_canvas_texture(p_batch->tex_info->state.texture)) { + KeyValue> *kv = nullptr; + if (HashMap>::Iterator i = canvas_texture_to_uniform_set.find(p_batch->tex_info->state.texture); i == canvas_texture_to_uniform_set.end()) { + kv = &*canvas_texture_to_uniform_set.insert(p_batch->tex_info->state.texture, { *uniform_set }); + } else { + i->value.push_back(rid); + kv = &*i; + } + ts->canvas_texture_set_invalidation_callback(p_batch->tex_info->state.texture, RendererCanvasRenderRD::_canvas_texture_invalidation_callback, kv); + } } if (state.current_batch_uniform_set != *uniform_set) { diff --git a/servers/rendering/renderer_rd/renderer_canvas_render_rd.h b/servers/rendering/renderer_rd/renderer_canvas_render_rd.h index 606beb78340..a3be3e8fe23 100644 --- a/servers/rendering/renderer_rd/renderer_canvas_render_rd.h +++ b/servers/rendering/renderer_rd/renderer_canvas_render_rd.h @@ -487,9 +487,14 @@ class RendererCanvasRenderRD : public RendererCanvasRender { static void _before_evict(RendererCanvasRenderRD::RIDSetKey &p_key, RID &p_rid); static void _uniform_set_invalidation_callback(void *p_userdata); + static void _canvas_texture_invalidation_callback(bool p_deleted, void *p_userdata); typedef LRUCache, HashMapComparatorDefault, _before_evict> RIDCache; RIDCache rid_set_to_uniform_set; + /// Maps a CanvasTexture to its associated uniform sets, which must + /// be invalidated when the CanvasTexture is updated, such as changing the + /// diffuse texture. + HashMap> canvas_texture_to_uniform_set; struct Batch { // Position in the UBO measured in bytes diff --git a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp index 7be0295d2d4..c022716898d 100644 --- a/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp +++ b/servers/rendering/renderer_rd/storage_rd/texture_storage.cpp @@ -45,9 +45,15 @@ using namespace RendererRD; void TextureStorage::CanvasTexture::clear_cache() { info_cache[0] = CanvasTextureCache(); info_cache[1] = CanvasTextureCache(); + if (invalidated_callback != nullptr) { + invalidated_callback(false, invalidated_callback_userdata); + } } TextureStorage::CanvasTexture::~CanvasTexture() { + if (invalidated_callback != nullptr) { + invalidated_callback(true, invalidated_callback_userdata); + } } /////////////////////////////////////////////////////////////////////////// @@ -737,6 +743,16 @@ TextureStorage::CanvasTextureInfo TextureStorage::canvas_texture_get_info(RID p_ return res; } +void TextureStorage::canvas_texture_set_invalidation_callback(RID p_canvas_texture, InvalidationCallback p_callback, void *p_userdata) { + CanvasTexture *ct = canvas_texture_owner.get_or_null(p_canvas_texture); + if (!ct) { + return; + } + + ct->invalidated_callback = p_callback; + ct->invalidated_callback_userdata = p_userdata; +} + /* Texture API */ RID TextureStorage::texture_allocate() { diff --git a/servers/rendering/renderer_rd/storage_rd/texture_storage.h b/servers/rendering/renderer_rd/storage_rd/texture_storage.h index 56f9eb04b26..5f0b6541ba9 100644 --- a/servers/rendering/renderer_rd/storage_rd/texture_storage.h +++ b/servers/rendering/renderer_rd/storage_rd/texture_storage.h @@ -92,6 +92,8 @@ class TextureStorage : public RendererTextureStorage { _FORCE_INLINE_ bool is_null() const { return diffuse.is_null(); } }; + typedef void (*InvalidationCallback)(bool p_deleted, void *p_userdata); + private: friend class LightStorage; friend class MaterialStorage; @@ -120,6 +122,9 @@ class TextureStorage : public RendererTextureStorage { RS::CanvasItemTextureRepeat texture_repeat = RS::CANVAS_ITEM_TEXTURE_REPEAT_DEFAULT; CanvasTextureCache info_cache[2]; + InvalidationCallback invalidated_callback = nullptr; + void *invalidated_callback_userdata = nullptr; + Size2i size_cache = Size2i(1, 1); bool use_normal_cache = false; bool use_specular_cache = false; @@ -501,6 +506,7 @@ class TextureStorage : public RendererTextureStorage { virtual void canvas_texture_set_texture_repeat(RID p_item, RS::CanvasItemTextureRepeat p_repeat) override; CanvasTextureInfo canvas_texture_get_info(RID p_texture, RS::CanvasItemTextureFilter p_base_filter, RS::CanvasItemTextureRepeat p_base_repeat, bool p_use_srgb, bool p_texture_is_data); + void canvas_texture_set_invalidation_callback(RID p_canvas_texture, InvalidationCallback p_callback, void *p_userdata); /* Texture API */ diff --git a/servers/rendering/rendering_server_default.h b/servers/rendering/rendering_server_default.h index 9bbed11adfc..ac44ba5a29a 100644 --- a/servers/rendering/rendering_server_default.h +++ b/servers/rendering/rendering_server_default.h @@ -964,6 +964,7 @@ class RenderingServerDefault : public RenderingServer { FUNC2(canvas_item_set_self_modulate, RID, const Color &) FUNC2(canvas_item_set_draw_behind_parent, RID, bool) + FUNC2(canvas_item_set_use_identity_transform, RID, bool) FUNC6(canvas_item_add_line, RID, const Point2 &, const Point2 &, const Color &, float, bool) FUNC5(canvas_item_add_polyline, RID, const Vector &, const Vector &, float, bool) diff --git a/servers/rendering_server.h b/servers/rendering_server.h index 405910705b3..f2afe72e0b0 100644 --- a/servers/rendering_server.h +++ b/servers/rendering_server.h @@ -1547,6 +1547,7 @@ class RenderingServer : public Object { virtual void canvas_item_set_visibility_layer(RID p_item, uint32_t p_visibility_layer) = 0; virtual void canvas_item_set_draw_behind_parent(RID p_item, bool p_enable) = 0; + virtual void canvas_item_set_use_identity_transform(RID p_item, bool p_enabled) = 0; enum NinePatchAxisMode { NINE_PATCH_STRETCH, diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h index ebab13feb6b..ea742525e54 100644 --- a/tests/scene/test_text_edit.h +++ b/tests/scene/test_text_edit.h @@ -8153,6 +8153,93 @@ TEST_CASE("[SceneTree][TextEdit] gutters") { // Merging tested via CodeEdit gutters. } + SUBCASE("[TextEdit] gutter mouse") { + DisplayServerMock *DS = (DisplayServerMock *)(DisplayServer::get_singleton()); + // Set size for mouse input. + text_edit->set_size(Size2(200, 200)); + + text_edit->set_text("test1\ntest2\ntest3\ntest4"); + text_edit->grab_focus(); + + text_edit->add_gutter(); + text_edit->set_gutter_name(0, "test_gutter"); + text_edit->set_gutter_width(0, 10); + text_edit->set_gutter_clickable(0, true); + + text_edit->add_gutter(); + text_edit->set_gutter_name(1, "test_gutter_not_clickable"); + text_edit->set_gutter_width(1, 10); + text_edit->set_gutter_clickable(1, false); + + text_edit->add_gutter(); + CHECK(text_edit->get_gutter_count() == 3); + text_edit->set_gutter_name(2, "test_gutter_3"); + text_edit->set_gutter_width(2, 10); + text_edit->set_gutter_clickable(2, true); + + MessageQueue::get_singleton()->flush(); + const int line_height = text_edit->get_line_height(); + + // Defaults to none. + CHECK(text_edit->get_hovered_gutter() == Vector2i(-1, -1)); + CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_ARROW); + + // Hover over gutter. + SEND_GUI_MOUSE_MOTION_EVENT(Point2(5, line_height + line_height / 2), MouseButtonMask::NONE, Key::NONE); + CHECK(text_edit->get_hovered_gutter() == Vector2i(0, 1)); + SIGNAL_CHECK_FALSE("gutter_clicked"); + CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_POINTING_HAND); + + // Click on gutter. + SEND_GUI_MOUSE_BUTTON_EVENT(Point2(5, line_height / 2), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_hovered_gutter() == Vector2i(0, 0)); + SIGNAL_CHECK("gutter_clicked", build_array(build_array(0, 0))); + + // Click on gutter on another line. + SEND_GUI_MOUSE_BUTTON_EVENT(Point2(5, line_height * 3 + line_height / 2), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_hovered_gutter() == Vector2i(0, 3)); + SIGNAL_CHECK("gutter_clicked", build_array(build_array(3, 0))); + + // Unclickable gutter can be hovered. + SEND_GUI_MOUSE_MOTION_EVENT(Point2(15, line_height + line_height / 2), MouseButtonMask::NONE, Key::NONE); + CHECK(text_edit->get_hovered_gutter() == Vector2i(1, 1)); + SIGNAL_CHECK_FALSE("gutter_clicked"); + CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_ARROW); + + // Unclickable gutter can be clicked. + SEND_GUI_MOUSE_BUTTON_EVENT(Point2(15, line_height * 2 + line_height / 2), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_hovered_gutter() == Vector2i(1, 2)); + SIGNAL_CHECK("gutter_clicked", build_array(build_array(2, 1))); + CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_ARROW); + + // Hover past last line. + SEND_GUI_MOUSE_MOTION_EVENT(Point2(5, line_height * 5), MouseButtonMask::NONE, Key::NONE); + CHECK(text_edit->get_hovered_gutter() == Vector2i(-1, -1)); + SIGNAL_CHECK_FALSE("gutter_clicked"); + CHECK(DS->get_cursor_shape() == DisplayServer::CURSOR_ARROW); + + // Click on gutter past last line. + SEND_GUI_MOUSE_BUTTON_EVENT(Point2(5, line_height * 5), MouseButton::LEFT, MouseButtonMask::LEFT, Key::NONE); + CHECK(text_edit->get_hovered_gutter() == Vector2i(-1, -1)); + SIGNAL_CHECK_FALSE("gutter_clicked"); + + // Mouse exit resets hover. + SEND_GUI_MOUSE_MOTION_EVENT(Point2(5, line_height + line_height / 2), MouseButtonMask::NONE, Key::NONE); + CHECK(text_edit->get_hovered_gutter() == Vector2i(0, 1)); + SEND_GUI_MOUSE_MOTION_EVENT(Point2(-1, -1), MouseButtonMask::NONE, Key::NONE); + CHECK(text_edit->get_hovered_gutter() == Vector2i(-1, -1)); + + // Removing gutter updates hover. + SEND_GUI_MOUSE_MOTION_EVENT(Point2(25, line_height + line_height / 2), MouseButtonMask::NONE, Key::NONE); + CHECK(text_edit->get_hovered_gutter() == Vector2i(2, 1)); + text_edit->remove_gutter(2); + CHECK(text_edit->get_hovered_gutter() == Vector2i(-1, -1)); + + // Updating size updates hover. + text_edit->set_gutter_width(1, 20); + CHECK(text_edit->get_hovered_gutter() == Vector2i(1, 1)); + } + SIGNAL_UNWATCH(text_edit, "gutter_clicked"); SIGNAL_UNWATCH(text_edit, "gutter_added"); SIGNAL_UNWATCH(text_edit, "gutter_removed"); diff --git a/thirdparty/jolt_physics/Jolt/Physics/Collision/Shape/BoxShape.cpp b/thirdparty/jolt_physics/Jolt/Physics/Collision/Shape/BoxShape.cpp index dc7ea482857..fc2281252f0 100644 --- a/thirdparty/jolt_physics/Jolt/Physics/Collision/Shape/BoxShape.cpp +++ b/thirdparty/jolt_physics/Jolt/Physics/Collision/Shape/BoxShape.cpp @@ -59,7 +59,7 @@ BoxShape::BoxShape(const BoxShapeSettings &inSettings, ShapeResult &outResult) : { // Check convex radius if (inSettings.mConvexRadius < 0.0f - || inSettings.mHalfExtent.ReduceMin() <= inSettings.mConvexRadius) + || inSettings.mHalfExtent.ReduceMin() < inSettings.mConvexRadius) { outResult.SetError("Invalid convex radius"); return; @@ -278,7 +278,7 @@ void BoxShape::CollideSoftBodyVertices(Mat44Arg inCenterOfMassTransform, Vec3Arg void BoxShape::GetTrianglesStart(GetTrianglesContext &ioContext, const AABox &inBox, Vec3Arg inPositionCOM, QuatArg inRotation, Vec3Arg inScale) const { - new (&ioContext) GetTrianglesContextVertexList(inPositionCOM, inRotation, inScale, Mat44::sScale(mHalfExtent), sUnitBoxTriangles, sizeof(sUnitBoxTriangles) / sizeof(Vec3), GetMaterial()); + new (&ioContext) GetTrianglesContextVertexList(inPositionCOM, inRotation, inScale, Mat44::sScale(mHalfExtent), sUnitBoxTriangles, std::size(sUnitBoxTriangles), GetMaterial()); } int BoxShape::GetTrianglesNext(GetTrianglesContext &ioContext, int inMaxTrianglesRequested, Float3 *outTriangleVertices, const PhysicsMaterial **outMaterials) const