diff --git a/core/object/worker_thread_pool.cpp b/core/object/worker_thread_pool.cpp index 23adfdbc851..40b5aaf44ec 100644 --- a/core/object/worker_thread_pool.cpp +++ b/core/object/worker_thread_pool.cpp @@ -580,9 +580,11 @@ void WorkerThreadPool::_switch_runlevel(Runlevel p_runlevel) { runlevel = p_runlevel; memset(&runlevel_data, 0, sizeof(runlevel_data)); for (uint32_t i = 0; i < threads.size(); i++) { - threads[i].cond_var.notify_one(); + threads[i].cond_var.notify_one(); // Wakes the worker loop so it rechecks the new runlevel threads[i].signaled = true; } + // Notify the main thread even if counts already match, + // in case it's currently blocked in wait_for_usec / wait. control_cond_var.notify_all(); } diff --git a/core/os/condition_variable.h b/core/os/condition_variable.h index 7eda3d5a087..33c39442226 100644 --- a/core/os/condition_variable.h +++ b/core/os/condition_variable.h @@ -52,6 +52,8 @@ #define THREADING_NAMESPACE std #endif +#include + /// An object one or multiple threads can wait on a be notified by some other. /// Normally, you want to use a semaphore for such scenarios, but when the /// condition is something different than a count being greater than zero @@ -71,6 +73,17 @@ class ConditionVariable { condition.wait(p_lock.mutex._get_lock()); } + /// @return `true` if notified before timeout, false if timed out. + template + _ALWAYS_INLINE_ bool wait_for_usec(const MutexLock &p_lock, uint64_t p_usec) const { + return condition.wait_for(p_lock._get_lock(), std::chrono::microseconds(p_usec)) == THREADING_NAMESPACE::cv_status::no_timeout; + } + + template + _ALWAYS_INLINE_ bool wait_for_usec(const MutexLock> &p_lock, uint64_t p_usec) const { + return condition.wait_for(p_lock.mutex._get_lock(), std::chrono::microseconds(p_usec)) == THREADING_NAMESPACE::cv_status::no_timeout; + } + _ALWAYS_INLINE_ void notify_one() const { condition.notify_one(); } @@ -81,13 +94,19 @@ class ConditionVariable { }; #else // No threads. - class ConditionVariable { public: template void wait(const MutexLock &p_lock) const {} + template + void wait(const MutexLock> &p_lock) const {} void notify_one() const {} void notify_all() const {} + + template + bool wait_for_usec(const MutexLock &p_lock, uint64_t p_usec) const { return true; } + template + bool wait_for_usec(const MutexLock> &p_lock, uint64_t p_usec) const { return true; } }; #endif // THREADS_ENABLED diff --git a/drivers/gles3/rasterizer_gles3.cpp b/drivers/gles3/rasterizer_gles3.cpp index f87e8e93163..ff37f8ca318 100644 --- a/drivers/gles3/rasterizer_gles3.cpp +++ b/drivers/gles3/rasterizer_gles3.cpp @@ -489,23 +489,13 @@ void RasterizerGLES3::set_boot_image(const Ref &p_image, const Color &p_c texture_storage->texture_2d_initialize(texture, p_image); Rect2 imgrect(0, 0, p_image->get_width(), p_image->get_height()); + // Size2 win_size_f = Size2(win_size); // This is needed for the .floor() function below because Size2i does not have a floor() function (but Size2 does) Rect2 screenrect; - if (p_scale) { - if (win_size.width > win_size.height) { - //scale horizontally - screenrect.size.y = win_size.height; - screenrect.size.x = imgrect.size.x * win_size.height / imgrect.size.y; - screenrect.position.x = (win_size.width - screenrect.size.x) / 2; - } else { - //scale vertically - screenrect.size.x = win_size.width; - screenrect.size.y = imgrect.size.y * win_size.width / imgrect.size.x; - screenrect.position.y = (win_size.height - screenrect.size.y) / 2; - } + if (p_scale) { + screenrect = OS::get_singleton()->calculate_boot_screen_rect(win_size, imgrect.size); } else { - screenrect = imgrect; - screenrect.position += ((Size2(win_size.width, win_size.height) - screenrect.size) / 2.0).floor(); + screenrect = DisplayServer::calculate_boot_image_rect(win_size, imgrect); } #ifdef WINDOWS_ENABLED @@ -530,6 +520,43 @@ void RasterizerGLES3::set_boot_image(const Ref &p_image, const Color &p_c gl_end_frame(true); + // After the first commit, a tiling compositor (e.g. Hyprland via XWayland) + // may have sent a ConfigureNotify with the actual window size. + // Pump events and re-render if the size changed. + DisplayServer::get_singleton()->pump_resize_events(); + Size2i new_win_size = DisplayServer::get_singleton()->window_get_size(); + + if (new_win_size != win_size && new_win_size.width > 0 && new_win_size.height > 0) { + win_size = new_win_size; + glViewport(0, 0, win_size.width, win_size.height); + glClearColor(p_color.r, p_color.g, p_color.b, + OS::get_singleton()->is_layered_allowed() ? p_color.a : 1.0); + glClear(GL_COLOR_BUFFER_BIT); + + Rect2 screenrect2; + if (p_scale) { + screenrect2 = OS::get_singleton()->calculate_boot_screen_rect(win_size, imgrect.size); + } else { + screenrect2 = DisplayServer::calculate_boot_image_rect(win_size, imgrect); + } +#ifdef WINDOWS_ENABLED + if (!screen_flipped_y) +#endif + { + screenrect2.position.y = win_size.y - screenrect2.position.y; + screenrect2.size.y = -screenrect2.size.y; + } + screenrect2.position /= win_size; + screenrect2.size /= win_size; + + GLES3::Texture *t2 = texture_storage->get_texture(texture); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, t2->tex_id); + copy_effects->copy_to_rect(screenrect2); + glBindTexture(GL_TEXTURE_2D, 0); + gl_end_frame(true); + } + texture_storage->texture_free(texture); } diff --git a/editor/doc/editor_help.cpp b/editor/doc/editor_help.cpp index 2b4cedbc761..dbb475b0147 100644 --- a/editor/doc/editor_help.cpp +++ b/editor/doc/editor_help.cpp @@ -3061,6 +3061,8 @@ void EditorHelp::_load_doc_thread(void *p_udata) { } OS::get_singleton()->benchmark_end_measure("EditorHelp", vformat("Generate Documentation (Run %d)", doc_generation_count)); + + _worker_thread_done.set(); } void EditorHelp::_gen_doc_thread(void *p_udata) { @@ -3094,6 +3096,8 @@ void EditorHelp::_gen_doc_thread(void *p_udata) { } OS::get_singleton()->benchmark_end_measure("EditorHelp", vformat("Generate Documentation (Run %d)", doc_generation_count)); + + _worker_thread_done.set(); } void EditorHelp::_gen_extensions_docs() { @@ -3109,6 +3113,10 @@ static void _load_script_doc_cache(bool p_changes) { } void EditorHelp::load_script_doc_cache() { + if (_cleanup_in_progress.is_set()) { + return; + } + if (!ProjectSettings::get_singleton()->is_project_loaded()) { print_verbose("Skipping loading script doc cache since no project is open."); return; @@ -3132,6 +3140,7 @@ void EditorHelp::load_script_doc_cache() { return; } + _worker_thread_done.set_to(false); worker_thread.start(_load_script_doc_cache_thread, nullptr); } @@ -3152,13 +3161,18 @@ void EditorHelp::_process_postponed_docs() { void EditorHelp::_load_script_doc_cache_thread(void *p_udata) { ERR_FAIL_COND_MSG(!ProjectSettings::get_singleton()->is_project_loaded(), "Error: cannot load script doc cache without a project."); + _worker_thread_done.set(); + return; ERR_FAIL_COND_MSG(!ResourceLoader::exists(get_script_doc_cache_full_path()), "Error: cannot load script doc cache from inexistent file."); + _worker_thread_done.set(); + return; Ref script_doc_cache_res = ResourceLoader::load(get_script_doc_cache_full_path(), "", ResourceFormatLoader::CACHE_MODE_IGNORE); if (script_doc_cache_res.is_null()) { print_verbose("Script doc cache is corrupted. Regenerating it instead."); _delete_script_doc_cache(); callable_mp_static(EditorHelp::regenerate_script_doc_cache).call_deferred(); + _worker_thread_done.set(); return; } @@ -3175,6 +3189,8 @@ void EditorHelp::_load_script_doc_cache_thread(void *p_udata) { // Always delete the doc cache after successful load since most uses of editor will change a script, invalidating cache. _delete_script_doc_cache(); + + _worker_thread_done.set(); } /// Helper method to deal with "sources_changed" signal having a parameter. @@ -3183,6 +3199,10 @@ static void _regenerate_script_doc_cache(bool p_changes) { } void EditorHelp::regenerate_script_doc_cache() { + if (_cleanup_in_progress.is_set()) { + return; + } + if (EditorFileSystem::get_singleton()->is_scanning()) { // Wait until EditorFileSystem scanning is complete to use updated filesystem structure. EditorFileSystem::get_singleton()->connect(SNAME("sources_changed"), callable_mp_static(_regenerate_script_doc_cache), CONNECT_ONE_SHOT); @@ -3191,6 +3211,7 @@ void EditorHelp::regenerate_script_doc_cache() { _wait_for_thread(worker_thread); _wait_for_thread(loader_thread); + _loader_thread_done.set_to(false); loader_thread.start(_regen_script_doc_thread, EditorFileSystem::get_singleton()->get_filesystem()); } @@ -3200,6 +3221,8 @@ void EditorHelp::_finish_regen_script_doc_thread(void *p_udata) { _script_docs_loaded.set(); OS::get_singleton()->benchmark_end_measure("EditorHelp", "Generate Script Documentation"); + + _worker_thread_done.set(); } void EditorHelp::_regen_script_doc_thread(void *p_udata) { @@ -3215,6 +3238,9 @@ void EditorHelp::_regen_script_doc_thread(void *p_udata) { _reload_scripts_documentation(dir); + _loader_thread_done.set(); + _worker_thread_done.set_to(false); // worker_thread is about to start + // All ResourceLoader::load() calls are done, so we can no longer deadlock with main thread. // Switch to back to worker_thread from loader_thread to resynchronize access to DocData. worker_thread.start(_finish_regen_script_doc_thread, nullptr); @@ -3265,6 +3291,10 @@ void EditorHelp::save_script_doc_cache() { } void EditorHelp::generate_doc(bool p_use_cache, bool p_use_script_cache) { + if (_cleanup_in_progress.is_set()) { + return; + } + doc_generation_count++; OS::get_singleton()->benchmark_begin_measure("EditorHelp", vformat("Generate Documentation (Run %d)", doc_generation_count)); @@ -3280,10 +3310,12 @@ void EditorHelp::generate_doc(bool p_use_cache, bool p_use_script_cache) { } if (p_use_cache && FileAccess::exists(get_cache_full_path())) { + _worker_thread_done.set_to(false); worker_thread.start(_load_doc_thread, (void *)p_use_script_cache); } else { print_verbose("Regenerating editor help cache"); doc->generate(); + _worker_thread_done.set_to(false); worker_thread.start(_gen_doc_thread, (void *)p_use_script_cache); } } @@ -3376,7 +3408,28 @@ void EditorHelp::update_doc() { } void EditorHelp::cleanup_doc() { - _wait_for_thread(); + _cleanup_in_progress.set(); + + // loader_thread runs _regen_script_doc_thread which calls ResourceLoader::load(). + // Flush the message queue so load tasks can dispatch to the main thread. + if (loader_thread.is_started()) { + while (!_loader_thread_done.is_set()) { + MessageQueue::get_singleton()->flush(); + OS::get_singleton()->delay_usec(1000); + } + loader_thread.wait_to_finish(); + } + + // worker_thread runs _load_doc_thread, _gen_doc_thread, _load_script_doc_cache_thread, + // or _finish_regen_script_doc_thread. All may post deferred calls needing the main thread. + if (worker_thread.is_started()) { + while (!_worker_thread_done.is_set()) { + MessageQueue::get_singleton()->flush(); + OS::get_singleton()->delay_usec(1000); + } + worker_thread.wait_to_finish(); + } + memdelete(doc); doc = nullptr; } diff --git a/editor/doc/editor_help.h b/editor/doc/editor_help.h index 4db238431eb..41200946324 100644 --- a/editor/doc/editor_help.h +++ b/editor/doc/editor_help.h @@ -204,6 +204,10 @@ class EditorHelp : public VBoxContainer { inline static Thread loader_thread; ///< Only load scripts here to avoid deadlocking with main thread. inline static SafeFlag _script_docs_loaded = SafeFlag(false); + inline static SafeFlag _worker_thread_done = SafeFlag(false); + inline static SafeFlag _loader_thread_done = SafeFlag(false); + inline static SafeFlag _cleanup_in_progress = SafeFlag(false); + inline static LocalVector _docs_to_add; inline static LocalVector _docs_to_remove; inline static LocalVector _docs_to_remove_by_path; diff --git a/editor/file_system/editor_file_system.cpp b/editor/file_system/editor_file_system.cpp index 52536aeb352..02337870063 100644 --- a/editor/file_system/editor_file_system.cpp +++ b/editor/file_system/editor_file_system.cpp @@ -1730,8 +1730,9 @@ void EditorFileSystem::_notification(int p_what) { case NOTIFICATION_EXIT_TREE: { Thread &active_thread = thread.is_started() ? thread : thread_sources; if (use_threads && active_thread.is_started()) { + const uint64_t TIMEOUT_USEC = 1000; while (scanning) { - OS::get_singleton()->delay_usec(1000); + OS::get_singleton()->delay_usec(TIMEOUT_USEC); } active_thread.wait_to_finish(); WARN_PRINT("Scan thread aborted..."); diff --git a/editor/inspector/editor_resource_preview.cpp b/editor/inspector/editor_resource_preview.cpp index e4f16288032..ac4d56f5781 100644 --- a/editor/inspector/editor_resource_preview.cpp +++ b/editor/inspector/editor_resource_preview.cpp @@ -600,8 +600,14 @@ void EditorResourcePreview::stop() { preview_generators.write[i]->abort(); } + const uint64_t TIMEOUT_USEC = 3000000; // 3 seconds + uint64_t start = OS::get_singleton()->get_ticks_usec(); + while (!exited.is_set()) { - // Sync pending work. + if (OS::get_singleton()->get_ticks_usec() - start > TIMEOUT_USEC) { + WARN_PRINT("EditorResourcePreview: Timed out waiting for preview thread."); + break; + } OS::get_singleton()->delay_usec(10000); RenderingServer::get_singleton()->sync(); MessageQueue::get_singleton()->flush(); diff --git a/editor/scene/2d/tiles/tiles_editor_plugin.cpp b/editor/scene/2d/tiles/tiles_editor_plugin.cpp index 169af750c4f..df2104b8854 100644 --- a/editor/scene/2d/tiles/tiles_editor_plugin.cpp +++ b/editor/scene/2d/tiles/tiles_editor_plugin.cpp @@ -77,14 +77,19 @@ void TilesEditorUtils::_thread_func(void *ud) { } void TilesEditorUtils::_thread() { + // Worker thread that generates thumbnail previews for tile patterns. pattern_thread_exited.clear(); + + // Main loop: wait for queued pattern requests, process them until shutdown. while (!pattern_thread_exit.is_set()) { + // Wait for work to be queued. pattern_preview_sem.wait(); pattern_preview_mutex.lock(); if (pattern_preview_queue.is_empty()) { pattern_preview_mutex.unlock(); } else { + // Dequeue the next pattern request. QueueItem item = pattern_preview_queue.front()->get(); pattern_preview_queue.pop_front(); pattern_preview_mutex.unlock(); @@ -94,18 +99,20 @@ void TilesEditorUtils::_thread() { Vector2 thumbnail_size2 = Vector2(thumbnail_size, thumbnail_size); if (item.pattern.is_valid() && !item.pattern->is_empty()) { - // Generate the pattern preview + // Create a temporary viewport to render the pattern / Generate the pattern preview. SubViewport *viewport = memnew(SubViewport); viewport->set_size(thumbnail_size2); viewport->set_disable_input(true); viewport->set_transparent_background(true); viewport->set_update_mode(SubViewport::UPDATE_ONCE); + // Set up the tile map layer with the pattern to render. TileMapLayer *tile_map_layer = memnew(TileMapLayer); tile_map_layer->set_tile_set(item.tile_set); tile_map_layer->set_pattern(Vector2(), item.pattern); viewport->add_child(tile_map_layer); + // Calculate the bounding rect of the pattern for scaling. Rect2 encompassing_rect; encompassing_rect.set_position(tile_map_layer->map_to_local(tile_map_layer->get_tile_map_layer_data().begin()->key)); for (KeyValue kv : tile_map_layer->get_tile_map_layer_data()) { @@ -127,22 +134,41 @@ void TilesEditorUtils::_thread() { } } + // Scale and center the pattern to fit the thumbnail size. Vector2 scale = thumbnail_size2 / MAX(encompassing_rect.size.x, encompassing_rect.size.y); tile_map_layer->set_scale(scale); tile_map_layer->set_position(-(scale * encompassing_rect.get_center()) + thumbnail_size2 / 2); - // Add the viewport at the last moment to avoid rendering too early. + // Add the viewport to the scene tree (deferred to avoid rendering too early). callable_mp((Node *)EditorNode::get_singleton(), &Node::add_child).call_deferred(viewport, false, Node::INTERNAL_MODE_DISABLED); + // Register a callback to be notified when the frame is drawn. RS::get_singleton()->connect(SNAME("frame_pre_draw"), callable_mp(this, &TilesEditorUtils::_preview_frame_started), Object::CONNECT_ONE_SHOT); + // Wait for the rendering server to complete the frame. pattern_preview_done.wait(); + // The thread can be safely interrupted during shutdown by the destructor, + // which posts the inner semaphore to unblock the frame wait. + // After unblocking, the thread checks the exit flag and cleans up + // without accessing potentially invalid rendering state. + if (pattern_thread_exit.is_set()) { + // Disconnect frame_pre_draw if it hasn't fired yet. + if (RS::get_singleton()->is_connected(SNAME("frame_pre_draw"), callable_mp(this, &TilesEditorUtils::_preview_frame_started))) { + RS::get_singleton()->disconnect(SNAME("frame_pre_draw"), callable_mp(this, &TilesEditorUtils::_preview_frame_started)); + } + viewport->queue_free(); + break; + } + + // Capture the rendered image from the viewport. Ref image = viewport->get_texture()->get_image(); /// Find the index for the given pattern. @todo Optimize. + // Return the result via the callback. item.callback.call(item.pattern, ImageTexture::create_from_image(image)); + // Clean up the temporary viewport. viewport->queue_free(); } } @@ -326,10 +352,21 @@ TilesEditorUtils::~TilesEditorUtils() { if (pattern_preview_thread.is_started()) { pattern_thread_exit.set(); pattern_preview_sem.post(); + pattern_preview_done.post(); // Unblock thread if stuck waiting for a frame to be drawn. + + const uint64_t TIMEOUT_USEC = 3000000; // 3 seconds + uint64_t start = OS::get_singleton()->get_ticks_usec(); + while (!pattern_thread_exited.is_set()) { + if (OS::get_singleton()->get_ticks_usec() - start > TIMEOUT_USEC) { + WARN_PRINT("TilesEditorUtils: Timed out waiting for pattern preview thread to exit."); + break; + } OS::get_singleton()->delay_usec(10000); - RenderingServer::get_singleton()->sync(); //sync pending stuff, as thread may be blocked on visual server + RenderingServer::get_singleton()->sync(); + MessageQueue::get_singleton()->flush(); } + pattern_preview_thread.wait_to_finish(); } singleton = nullptr; diff --git a/editor/scene/2d/tiles/tiles_editor_plugin.h b/editor/scene/2d/tiles/tiles_editor_plugin.h index f30990ec185..ca5afc0ebf9 100644 --- a/editor/scene/2d/tiles/tiles_editor_plugin.h +++ b/editor/scene/2d/tiles/tiles_editor_plugin.h @@ -87,6 +87,12 @@ class TilesEditorUtils : public Object { void _preview_frame_started(); void _pattern_preview_done(); static void _thread_func(void *ud); + /// Worker thread that generates thumbnail previews for tile patterns. + /// Waits for queued pattern requests, renders each pattern in a temporary viewport, + /// and returns the result via a callback. The thread can be safely interrupted + /// during shutdown by the destructor, which posts the inner semaphore to unblock + /// the frame wait. After unblocking, the thread checks the exit flag and cleans + /// up without accessing potentially invalid rendering state. void _thread(); public: diff --git a/main/main.cpp b/main/main.cpp index 872daba2b45..0135fad6e4b 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -60,6 +60,7 @@ #include "core/os/os.h" #include "core/os/time.h" #include "core/register_core_types.h" +#include "core/string/print_string.h" #include "core/string/translation_server.h" #include "core/version.h" #include "drivers/register_driver_types.h" @@ -3555,7 +3556,10 @@ Error Main::setup2(bool p_show_boot_logo) { Color clear = GLOBAL_DEF_BASIC("rendering/environment/defaults/default_clear_color", get_boot_splash_bg_color()); RenderingServer::get_singleton()->set_default_clear_color(clear); + print_verbose(vformat("p_show_boot_logo value: %s", String(p_show_boot_logo ? "true" : "false"))); if (p_show_boot_logo) { + print_verbose("Setting up boot logo..."); + DisplayServer::get_singleton()->process_events(); setup_boot_logo(); } @@ -5125,6 +5129,11 @@ void Main::cleanup(bool p_force) { // Flush before uninitializing the scene, but delete the MessageQueue as late as possible. message_queue->flush(); + // Drain worker threads BEFORE deleting the SceneTree. + // Worker threads may hold references to scene objects; deleting the tree + // while they are still running causes use-after-free and deadlocks. + WorkerThreadPool::get_singleton()->exit_languages_threads(); + OS::get_singleton()->delete_main_loop(); OS::get_singleton()->_cmdline.clear(); @@ -5133,15 +5142,12 @@ void Main::cleanup(bool p_force) { OS::get_singleton()->_local_clipboard = ""; ResourceLoader::clear_translation_remaps(); - - WorkerThreadPool::get_singleton()->exit_languages_threads(); - ScriptServer::finish_languages(); // Sync pending commands that may have been queued from a different thread during ScriptServer finalization RenderingServer::get_singleton()->sync(); - //clear global shader variables before scene and other graphics stuff are deinitialized. + // Clear global shader variables before scene and other graphics stuff are deinitialized. rendering_server->global_shader_parameters_clear(); #ifndef XR_DISABLED diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp index a862e669a4c..034546ce77b 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.cpp +++ b/platform/linuxbsd/wayland/display_server_wayland.cpp @@ -752,9 +752,9 @@ void DisplayServerWayland::show_window(WindowID p_window_id) { if (!wd.visible) { DEBUG_LOG_WAYLAND(vformat("Showing window %d", p_window_id)); - // Showing this window will reset its mode with whatever the compositor - // reports. We'll save the mode beforehand so that we can reapply it later. - // TODO: Fix/Port/Move/Whatever to `WaylandThread` APIs. + /// Showing this window will reset its mode with whatever the compositor + /// reports. We'll save the mode beforehand so that we can reapply it later. + /// @todo Fix/Port/Move/Whatever to `WaylandThread` APIs. WindowMode setup_mode = wd.mode; // Let's determine the closest toplevel. For toplevels it will be themselves, @@ -788,6 +788,39 @@ void DisplayServerWayland::show_window(WindowID p_window_id) { if (wd.rect_changed_callback.is_valid()) { wd.rect_changed_callback.call(wd.rect); } + + // After window_create, the compositor may have sent a configure event with the + // actual tiling size (e.g. Hyprland). Sync wd.rect.size to the physical size + // NOW, before creating the EGL/Vulkan surface. + { + struct wl_surface *wl_surf = wayland_thread.window_get_wl_surface(p_window_id); + WaylandThread::WindowState *ws = wayland_thread.wl_surface_get_window_state(wl_surf); + if (ws) { + double win_scale = WaylandThread::window_state_get_scale_factor(ws); + Size2i physical_size = WaylandThread::scale_vector2i(ws->rect.size, win_scale); + if (physical_size != Size2i()) { + print_verbose(vformat("[display_server_wayland.cpp|show_window] Compositor physical size %s differs from requested %s, " + "syncing before EGL surface creation", + physical_size, wd.rect.size)); + wd.rect.size = physical_size; + } + + // IMPORTANT: The wayland_thread.window_create() method above calls + // wl_surface_commit() without a buffer. That, alone, should probably be done differently. + // The buffer scale is normally deferred to the first frame callback + // (wayland_thread.cpp::_frame_wl_callback_on_done), + // but the EGL surface and VkSurfaceKHR have already been created by then. + // So, wl_surface reports an undefined size while the + // wl_egl_window has the correct physical buffer size, and the + // Vulkan swap chain gets created at the wrong dimensions for the frame. + // Setting it here ensures the wl_surface size matches the wl_egl_window buffer for the first frame. + int buffer_scale = (win_scale >= 1.0) ? int(win_scale) : 1; + if (ws->buffer_scale != buffer_scale) { + wl_surface_set_buffer_scale(wl_surf, buffer_scale); + } + } + } + } else { DEBUG_LOG_WAYLAND("!!!!! Making popup !!!!!"); diff --git a/platform/linuxbsd/wayland/display_server_wayland.h b/platform/linuxbsd/wayland/display_server_wayland.h index 97c289cf6d3..399ae5b7ae9 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.h +++ b/platform/linuxbsd/wayland/display_server_wayland.h @@ -76,10 +76,10 @@ class DisplayServerWayland : public DisplayServer { WindowID parent_id = INVALID_WINDOW_ID; - // For popups. + /// For popups. WindowID root_id = INVALID_WINDOW_ID; - // For toplevels. + /// For toplevels. List popup_stack; Rect2i rect; @@ -94,7 +94,7 @@ class DisplayServerWayland : public DisplayServer { struct wl_egl_window *wl_egl_window = nullptr; #endif - // Flags whether we have allocated a buffer through the video drivers. + /// Flags whether we have allocated a buffer through the video drivers. bool visible = false; DisplayServer::VSyncMode vsync_mode = VSYNC_ENABLED; @@ -119,9 +119,9 @@ class DisplayServerWayland : public DisplayServer { }; enum class SuspendState { - NONE, // Unsuspended. - TIMEOUT, // Legacy fallback. - CAPABILITY, // New "suspended" wm_capability flag. + NONE, ///< Unsuspended. + TIMEOUT, ///< Legacy fallback. + CAPABILITY, ///< New "suspended" wm_capability flag. }; CursorShape cursor_shape = CURSOR_ARROW; @@ -141,10 +141,10 @@ class DisplayServerWayland : public DisplayServer { Context context; bool swap_cancel_ok = false; - // NOTE: These are the based on WINDOW_FLAG_POPUP, which does NOT imply what it - // seems. It's particularly confusing for our usecase, but just know that these - // are the "take all input thx" windows while the `popup_stack` variable keeps - // track of all the generic floating window concept. + /// @note These are the based on WINDOW_FLAG_POPUP, which does NOT imply what it + /// seems. It's particularly confusing for our usecase, but just know that these + /// are the "take all input thx" windows while the `popup_stack` variable keeps + /// track of all the generic floating window concept. List popup_menu_list; BitField last_mouse_monitor_mask = MouseButtonMask::NONE; diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp index cd615951189..c53c49d254e 100644 --- a/platform/linuxbsd/wayland/wayland_thread.cpp +++ b/platform/linuxbsd/wayland/wayland_thread.cpp @@ -31,6 +31,7 @@ /**************************************************************************/ #include "wayland_thread.h" +#include "core/string/print_string.h" #ifdef WAYLAND_ENABLED @@ -1257,6 +1258,8 @@ void WaylandThread::_xdg_toplevel_on_configure(void *data, struct xdg_toplevel * window_state_update_size(ws, width, height); } + ws->initial_configure_done = true; + DEBUG_LOG_WAYLAND_THREAD(vformat("XDG toplevel on configure width %d height %d.", width, height)); } @@ -1437,6 +1440,8 @@ void WaylandThread::libdecor_frame_on_configure(struct libdecor_frame *frame, st window_state_update_size(ws, width, height); + ws->initial_configure_done = true; + DEBUG_LOG_WAYLAND_THREAD(vformat("libdecor frame on configure rect %s", ws->rect)); } @@ -3093,6 +3098,10 @@ struct wl_display *WaylandThread::get_wl_display() const { return wl_display; } +void WaylandThread::roundtrip() { + wl_display_roundtrip(wl_display); +} + // NOTE: Stuff like libdecor can (and will) register foreign proxies which // aren't formatted as we like. This method is needed to detect whether a proxy // has our tag. Also, be careful! The proxy has to be manually tagged or it @@ -3284,7 +3293,7 @@ void WaylandThread::window_state_update_size(WindowState *p_ws, int p_width, int DEBUG_LOG_WAYLAND_THREAD(vformat("Resizing the window from %s to %s (buffer scale x%d).", p_ws->rect.size, scaled_size, p_ws->buffer_scale)); } - // FIXME: Actually resize the hint instead of centering it. + /// @todo FIXME: Actually resize the hint instead of centering it. p_ws->wayland_thread->pointer_set_hint(scaled_size / 2); Ref rect_msg; @@ -3589,7 +3598,15 @@ void WaylandThread::window_create(DisplayServer::WindowID p_window_id, int p_wid wl_surface_commit(ws.wl_surface); // Wait for the surface to be configured before continuing. - wl_display_roundtrip(wl_display); + // On some compositors (e.g., Hyprland under load), the configure event may + // not arrive within a single roundtrip. Loop until it does. + const int MAX_CONFIGURE_ROUNDTRIPS = 10; + for (int i = 0; i < MAX_CONFIGURE_ROUNDTRIPS && !ws.initial_configure_done; i++) { + wl_display_roundtrip(wl_display); + } + if (!ws.initial_configure_done) { + WARN_PRINT("WaylandThread: Surface was not configured after multiple roundtrips. Window may not appear."); + } window_state_update_size(&ws, ws.rect.size.width, ws.rect.size.height); } @@ -4863,7 +4880,8 @@ bool WaylandThread::wait_frame_suspend_ms(int p_timeout) { return true; } - remaining_ms -= OS::get_singleton()->get_ticks_msec() - begin_ms; + // remaining_ms -= OS::get_singleton()->get_ticks_msec() - begin_ms; + remaining_ms = p_timeout - (int)(OS::get_singleton()->get_ticks_msec() - begin_ms); } DEBUG_LOG_WAYLAND_THREAD("Frame timeout."); diff --git a/platform/linuxbsd/wayland/wayland_thread.h b/platform/linuxbsd/wayland/wayland_thread.h index 6c28e39188e..70bf48280cc 100644 --- a/platform/linuxbsd/wayland/wayland_thread.h +++ b/platform/linuxbsd/wayland/wayland_thread.h @@ -89,7 +89,7 @@ class WaylandThread { public: - // Messages used for exchanging information between Godot's and Wayland's thread. + /// Messages used for exchanging information between Godot's and Wayland's thread. class Message : public RefCounted { GDSOFTCLASS(Message, RefCounted); @@ -105,13 +105,13 @@ class WaylandThread { DisplayServer::WindowID id = DisplayServer::INVALID_WINDOW_ID; }; - // Message data for window rect changes. + /// Message data for window rect changes. class WindowRectMessage : public WindowMessage { GDSOFTCLASS(WindowRectMessage, WindowMessage); public: - // NOTE: This is in "scaled" terms. For example, if there's a 1920x1080 rect - // with a scale factor of 2, the actual value of `rect` will be 3840x2160. + /// @note This is in "scaled" terms. For example, if there's a 1920x1080 rect + /// with a scale factor of 2, the actual value of `rect` will be 3840x2160. Rect2i rect; }; @@ -175,7 +175,7 @@ class WaylandThread { struct xdg_wm_base *xdg_wm_base = nullptr; uint32_t xdg_wm_base_name = 0; - // NOTE: Deprecated. + /// @note Deprecated. struct zxdg_exporter_v1 *xdg_exporter_v1 = nullptr; uint32_t xdg_exporter_v1_name = 0; @@ -223,13 +223,13 @@ class WaylandThread { struct zwp_text_input_manager_v3 *wp_text_input_manager = nullptr; uint32_t wp_text_input_manager_name = 0; - // We're really not meant to use this one directly but we still need to know - // whether it's available. + /// We're really not meant to use this one directly but we still need to know + /// whether it's available. uint32_t wp_fifo_manager_name = 0; }; - // General Wayland-specific states. Shouldn't be accessed directly. - // TODO: Make private? + /// General Wayland-specific states. Shouldn't be accessed directly. + /// @todo Make private? struct WindowState { DisplayServer::WindowID id = DisplayServer::INVALID_WINDOW_ID; @@ -239,6 +239,8 @@ class WaylandThread { DisplayServer::WindowMode mode = DisplayServer::WINDOW_MODE_WINDOWED; bool suspended = false; + bool initial_configure_done = false; + // These are true by default as it isn't guaranteed that we'll find an // xdg-shell implementation with wm_capabilities available. If and once we // receive a wm_capabilities event these will get reset and updated with @@ -249,11 +251,11 @@ class WaylandThread { HashSet wl_outputs; - // NOTE: If for whatever reason this callback is destroyed _while_ the event - // thread is still running, it might be a good idea to set its user data to - // `nullptr`. From some initial testing of mine, it looks like it might still - // be called even after being destroyed, pointing to probably invalid window - // data by then and segfaulting hard. + /// @note If for whatever reason this callback is destroyed _while_ the event + /// thread is still running, it might be a good idea to set its user data to + /// `nullptr`. From some initial testing of mine, it looks like it might still + /// be called even after being destroyed, pointing to probably invalid window + /// data by then and segfaulting hard. struct wl_callback *frame_callback = nullptr; uint64_t last_frame_time = 0; @@ -264,7 +266,7 @@ class WaylandThread { struct wp_viewport *wp_viewport = nullptr; struct wp_fractional_scale_v1 *wp_fractional_scale = nullptr; - // NOTE: Deprecated. + /// @note Deprecated. struct zxdg_exported_v1 *xdg_exported_v1 = nullptr; struct zxdg_exported_v2 *xdg_exported_v2 = nullptr; @@ -273,23 +275,23 @@ class WaylandThread { String exported_handle; - // Currently applied buffer scale. + //. Currently applied buffer scale. int buffer_scale = 1; - // Buffer scale must be applied right before rendering but _after_ committing - // everything else or otherwise we might have an inconsistent state (e.g. - // double scale and odd resolution). This flag assists with that; when set, - // on the next frame, we'll commit whatever is set in `buffer_scale`. + /// Buffer scale must be applied right before rendering but _after_ committing + /// everything else or otherwise we might have an inconsistent state (e.g. + /// double scale and odd resolution). This flag assists with that; when set, + /// on the next frame, we'll commit whatever is set in `buffer_scale`. bool buffer_scale_changed = false; - // NOTE: The preferred buffer scale is currently only dynamically calculated. - // It can be accessed by calling `window_state_get_preferred_buffer_scale`. + /// @note The preferred buffer scale is currently only dynamically calculated. + /// It can be accessed by calling `window_state_get_preferred_buffer_scale`. - // Override used by the fractional scale add-on object. If less or equal to 0 - // (default) then the normal output-based scale is used instead. + /// Override used by the fractional scale add-on object. If less or equal to 0 + /// (default) then the normal output-based scale is used instead. double fractional_scale = 0; - // What the compositor is recommending us. + /// What the compositor is recommending us. double preferred_fractional_scale = 0; struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration = nullptr; @@ -297,9 +299,9 @@ class WaylandThread { struct zwp_idle_inhibitor_v1 *wp_idle_inhibitor = nullptr; #ifdef LIBDECOR_ENABLED - // If this is null the xdg_* variables must be set and vice-versa. This way we - // can handle this mess gracefully enough to hopefully being able of getting - // rid of this cleanly once we have our own CSDs. + /// If this is null the xdg_* variables must be set and vice-versa. This way we + /// can handle this mess gracefully enough to hopefully being able of getting + /// rid of this cleanly once we have our own CSDs. struct libdecor_frame *libdecor_frame = nullptr; struct libdecor_configuration *pending_libdecor_configuration = nullptr; #endif @@ -308,7 +310,7 @@ class WaylandThread { WaylandThread *wayland_thread; }; - // "High level" Godot-side screen data. + /// "High level" Redot-side screen data. struct ScreenData { // Geometry data. Point2i position; @@ -347,7 +349,7 @@ class WaylandThread { Point2 position; uint32_t motion_time = 0; - // Relative motion has its own optional event and so needs its own time. + /// Relative motion has its own optional event and so needs its own time. Vector2 relative_motion; uint32_t relative_motion_time = 0; @@ -359,7 +361,7 @@ class WaylandThread { DisplayServer::WindowID pointed_id = DisplayServer::INVALID_WINDOW_ID; DisplayServer::WindowID last_pointed_id = DisplayServer::INVALID_WINDOW_ID; - // This is needed to check for a new double click every time. + /// This is needed to check for a new double click every time. bool double_click_begun = false; uint32_t button_time = 0; @@ -367,10 +369,10 @@ class WaylandThread { uint32_t scroll_type = WL_POINTER_AXIS_SOURCE_WHEEL; - // The amount "scrolled" in pixels, in each direction. + /// The amount "scrolled" in pixels, in each direction. Vector2 scroll_vector; - // The amount of scroll "clicks" in each direction, in fractions of 120. + /// The amount of scroll "clicks" in each direction, in fractions of 120. Vector2i discrete_scroll_vector_120; uint32_t pinch_scale = 1; @@ -430,31 +432,31 @@ class WaylandThread { struct zwp_pointer_gesture_pinch_v1 *wp_pointer_gesture_pinch = nullptr; - // NOTE: According to the wp_pointer_gestures protocol specification, there - // can be only one active gesture at a time. + /// @note According to the wp_pointer_gestures protocol specification, there + /// can be only one active gesture at a time. Gesture active_gesture = Gesture::NONE; - // Used for delta calculations. - // NOTE: The wp_pointer_gestures protocol keeps track of the total scale of - // the pinch gesture, while godot instead wants its delta. + /// Used for delta calculations. + /// @note The wp_pointer_gestures protocol keeps track of the total scale of + /// the pinch gesture, while godot instead wants its delta. wl_fixed_t old_pinch_scale = 0; struct wl_surface *cursor_surface = nullptr; struct wl_callback *cursor_frame_callback = nullptr; uint32_t cursor_time_ms = 0; - // This variable is needed to buffer all pointer changes until a - // wl_pointer.frame event, as per Wayland's specification. Everything is - // first set in `data_buffer` and then `data` is set with its contents on - // an input frame event. All methods should generally read from - // `pointer_data` and write to `data_buffer`. + /// This variable is needed to buffer all pointer changes until a + /// wl_pointer.frame event, as per Wayland's specification. Everything is + /// first set in `data_buffer` and then `data` is set with its contents on + /// an input frame event. All methods should generally read from + /// `pointer_data` and write to `data_buffer`. PointerData pointer_data_buffer; PointerData pointer_data; - // Keyboard. + /// Keyboard. struct wl_keyboard *wl_keyboard = nullptr; - // For key events. + /// For key events. DisplayServer::WindowID focused_id = DisplayServer::INVALID_WINDOW_ID; struct xkb_context *xkb_context = nullptr; @@ -468,9 +470,9 @@ class WaylandThread { xkb_layout_index_t current_layout_index = 0; - // Clients with `wl_seat`s older than version 4 do not support - // `wl_keyboard::repeat_info`, so we'll provide a reasonable default of 25 - // keys per second, with a start delay of 600 milliseconds. + /// Clients with `wl_seat`s older than version 4 do not support + /// `wl_keyboard::repeat_info`, so we'll provide a reasonable default of 25 + /// keys per second, with a start delay of 600 milliseconds. int32_t repeat_key_delay_msec = 1000 / 25; int32_t repeat_start_delay_msec = 600; @@ -538,7 +540,7 @@ class WaylandThread { struct wl_display *wl_display = nullptr; }; - // FIXME: Is this the right thing to do? + /// @todo FIXME: Is this the right thing to do? inline static const char *proxy_tag = "godot"; Thread events_thread; @@ -551,32 +553,32 @@ class WaylandThread { String cursor_theme_name; int unscaled_cursor_size = 24; - // NOTE: Regarding screen scale handling, the cursor cache is currently - // "static", by which I mean that we try to change it as little as possible and - // thus will be as big as the largest screen. This is mainly due to the fact - // that doing it dynamically doesn't look like it's worth it to me currently, - // especially as usually screen scales don't change continuously. + /// @note Regarding screen scale handling, the cursor cache is currently + /// "static", by which I mean that we try to change it as little as possible and + /// thus will be as big as the largest screen. This is mainly due to the fact + /// that doing it dynamically doesn't look like it's worth it to me currently, + /// especially as usually screen scales don't change continuously. int cursor_scale = 1; - // Use cursor-shape-v1 protocol if the compositor supports it. + /// Use cursor-shape-v1 protocol if the compositor supports it. wp_cursor_shape_device_v1_shape standard_cursors[DisplayServer::CURSOR_MAX] = { - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT, //CURSOR_ARROW - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT, //CURSOR_IBEAM - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER, //CURSOR_POINTING_HAND - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR, //CURSOR_CROSS - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_WAIT, //CURSOR_WAIT - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_PROGRESS, //CURSOR_BUSY - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRAB, //CURSOR_DRAG - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRABBING, //CURSOR_CAN_DROP - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NO_DROP, //CURSOR_FORBIDDEN - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE, //CURSOR_VSIZE - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE, //CURSOR_HSIZE - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE, //CURSOR_BDIAGSIZE - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE, //CURSOR_FDIAGSIZE - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE, //CURSOR_MOVE - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ROW_RESIZE, //CURSOR_VSPLIT - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_COL_RESIZE, //CURSOR_HSPLIT - wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_HELP, //CURSOR_HELP + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT, ///< CURSOR_ARROW + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT, ///< CURSOR_IBEAM + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER, ///< CURSOR_POINTING_HAND + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR, ///< CURSOR_CROSS + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_WAIT, ///< CURSOR_WAIT + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_PROGRESS, ///< CURSOR_BUSY + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRAB, ///< CURSOR_DRAG + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRABBING, ///< CURSOR_CAN_DROP + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NO_DROP, ///< CURSOR_FORBIDDEN + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE, ///< CURSOR_VSIZE + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE, ///< CURSOR_HSIZE + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE, ///< CURSOR_BDIAGSIZE + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE, ///< CURSOR_FDIAGSIZE + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE, ///< CURSOR_MOVE + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ROW_RESIZE, ///< CURSOR_VSPLIT + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_COL_RESIZE, ///< CURSOR_HSPLIT + wp_cursor_shape_device_v1_shape::WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_HELP, ///< CURSOR_HELP }; // Fallback to reading $XCURSOR and system themes if the compositor does not. @@ -606,7 +608,7 @@ class WaylandThread { struct libdecor *libdecor_context = nullptr; #endif // LIBDECOR_ENABLED - // Main polling method. + /// Main polling method. static void _poll_events_thread(void *p_data); // Core Wayland event handlers. @@ -733,7 +735,7 @@ class WaylandThread { static void _xdg_toplevel_decoration_on_configure(void *data, struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration, uint32_t mode); - // NOTE: Deprecated. + /// @note Deprecated. static void _xdg_exported_v1_on_handle(void *data, zxdg_exported_v1 *exported, const char *handle); static void _xdg_exported_v2_on_handle(void *data, zxdg_exported_v2 *exported, const char *handle); @@ -931,6 +933,9 @@ class WaylandThread { // libdecor event handlers. static void libdecor_on_error(struct libdecor *context, enum libdecor_error error, const char *message); + /// @note This is pretty much a reimplementation of _xdg_surface_on_configure + /// and _xdg_toplevel_on_configure. Libdecor really likes wrapping everything, + /// forcing us to do stuff like this. static void libdecor_frame_on_configure(struct libdecor_frame *frame, struct libdecor_configuration *configuration, void *user_data); static void libdecor_frame_on_close(struct libdecor_frame *frame, void *user_data); @@ -1095,7 +1100,7 @@ class WaylandThread { void selection_set_text(const String &p_text); - // Optional primary support - requires wp_primary_selection_unstable_v1 + /// Optional primary support - requires wp_primary_selection_unstable_v1 bool primary_has_mime(const String &p_mime) const; Vector primary_get_mime(const String &p_mime) const; @@ -1106,6 +1111,7 @@ class WaylandThread { void set_frame(); bool get_reset_frame(); bool wait_frame_suspend_ms(int p_timeout); + bool is_fifo_available() const; uint64_t window_get_last_frame_time(DisplayServer::WindowID p_window_id) const; @@ -1114,6 +1120,7 @@ class WaylandThread { Error init(); void destroy(); + void roundtrip(); }; #endif // WAYLAND_ENABLED diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index 23858169174..49ba5a5774a 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -1941,6 +1941,28 @@ void DisplayServerX11::show_window(WindowID p_id) { } } + // Search the X11 event queue for ConfigureNotify events and process all that are currently queued early. + { + MutexLock mutex_lock(events_mutex); + + for (uint32_t event_index = 0; event_index < polled_events.size(); ++event_index) { + XEvent &config_event = polled_events[event_index]; + if (config_event.type == ConfigureNotify) { + _window_changed(&config_event); + get_config_event = true; + } + } + XEvent config_event; + while (XCheckTypedEvent(x11_display, ConfigureNotify, &config_event)) { + _window_changed(&config_event); + get_config_event = true; + } + } + + if (!get_config_event) { + print_verbose("X11: get_config_event never succeeded!"); + }; + // Estimate maximize/full screen window size, ConfigureNotify may arrive only after maximize animation is finished. if (!get_config_event && (wd.maximized || wd.fullscreen)) { int screen = window_get_current_screen(p_id); @@ -1971,7 +1993,6 @@ void DisplayServerX11::show_window(WindowID p_id) { if (sz == Size2i()) { return; } - wd.size = sz; #if defined(RD_ENABLED) if (rendering_context) { @@ -1987,6 +2008,7 @@ void DisplayServerX11::show_window(WindowID p_id) { } #endif } + print_verbose(vformat("show_window: final size %s", wd.size)); } } @@ -2692,6 +2714,29 @@ Size2i DisplayServerX11::window_get_size_with_decorations(WindowID p_window) con return Size2i(w, h); } +void DisplayServerX11::pump_resize_events() { + _THREAD_SAFE_METHOD_ + + // Under XWayland + tiling WMs (e.g. Hyprland), the compositor sends + // ConfigureNotify only after the first wl_surface.commit (glXSwapBuffers). + // Wait one frame-worth of time for the event to arrive, then drain it. + OS::get_singleton()->delay_usec(16000); // ~1 frame @ 60 fps + XSync(x11_display, False); + + { + MutexLock mutex_lock(events_mutex); + for (uint32_t i = 0; i < polled_events.size(); ++i) { + if (polled_events[i].type == ConfigureNotify) { + _window_changed(&polled_events[i]); + } + } + XEvent ev; + while (XCheckTypedEvent(x11_display, ConfigureNotify, &ev)) { + _window_changed(&ev); + } + } +} + // Just a helper to reduce code duplication in `window_is_maximize_allowed` // and `_set_wm_maximized`. bool DisplayServerX11::_window_maximize_check(WindowID p_window, const char *p_atom_name) const { diff --git a/platform/linuxbsd/x11/display_server_x11.h b/platform/linuxbsd/x11/display_server_x11.h index e607e01d9f9..9a92f5226da 100644 --- a/platform/linuxbsd/x11/display_server_x11.h +++ b/platform/linuxbsd/x11/display_server_x11.h @@ -516,6 +516,8 @@ class DisplayServerX11 : public DisplayServer { virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const override; virtual Size2i window_get_size_with_decorations(WindowID p_window = MAIN_WINDOW_ID) const override; + virtual void pump_resize_events() override; + virtual void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID) override; virtual WindowMode window_get_mode(WindowID p_window = MAIN_WINDOW_ID) const override; diff --git a/servers/display_server.cpp b/servers/display_server.cpp index 52098ea3fa2..553759b44a0 100644 --- a/servers/display_server.cpp +++ b/servers/display_server.cpp @@ -579,6 +579,15 @@ float DisplayServer::screen_get_scale(int p_screen) const { return 1.0f; } +/// Compute a floor-centered, scaled image rect for a window (used by both rasterizers). +Rect2 DisplayServer::calculate_boot_image_rect(const Size2 &p_window_size, const Rect2 &p_imgrect) { + float screen_scale = DisplayServer::get_singleton()->screen_get_scale(); + Rect2 screenrect = p_imgrect; + screenrect.size *= screen_scale; // convert image rect to physical pixels - necessary for Wayland - should return 1.0 elsewhere + screenrect.position += ((p_window_size - screenrect.size) / 2.0).floor(); + return screenrect; +} + bool DisplayServer::is_touchscreen_available() const { return Input::get_singleton() && Input::get_singleton()->is_emulating_touch_from_mouse(); } diff --git a/servers/display_server.h b/servers/display_server.h index cd51a99eb39..8153135787a 100644 --- a/servers/display_server.h +++ b/servers/display_server.h @@ -365,6 +365,7 @@ class DisplayServer : public Object { virtual Rect2i screen_get_usable_rect(int p_screen = SCREEN_OF_MAIN_WINDOW) const = 0; virtual int screen_get_dpi(int p_screen = SCREEN_OF_MAIN_WINDOW) const = 0; virtual float screen_get_scale(int p_screen = SCREEN_OF_MAIN_WINDOW) const; + static Rect2 calculate_boot_image_rect(const Size2 &p_window_size, const Rect2 &p_imgrect); virtual float screen_get_max_scale() const { float scale = 1.f; int screen_count = get_screen_count(); @@ -501,6 +502,11 @@ class DisplayServer : public Object { virtual Size2i window_get_size(WindowID p_window = MAIN_WINDOW_ID) const = 0; virtual Size2i window_get_size_with_decorations(WindowID p_window = MAIN_WINDOW_ID) const = 0; + /// After the first rendered frame is committed, tiling compositors may + /// send a resize event. This hook lets the display server pump those + /// events synchronously before the boot image is re-rendered. + virtual void pump_resize_events() {} + virtual void window_set_mode(WindowMode p_mode, WindowID p_window = MAIN_WINDOW_ID) = 0; virtual WindowMode window_get_mode(WindowID p_window = MAIN_WINDOW_ID) const = 0; @@ -859,7 +865,6 @@ class DisplayServer : public Object { virtual void tablet_set_current_driver(const String &p_driver) {} virtual void process_events() = 0; - virtual void force_process_and_drop_events(); virtual void release_rendering_thread(); diff --git a/servers/rendering/renderer_rd/renderer_compositor_rd.cpp b/servers/rendering/renderer_rd/renderer_compositor_rd.cpp index 775e7af51b9..aa8b4be2aad 100644 --- a/servers/rendering/renderer_rd/renderer_compositor_rd.cpp +++ b/servers/rendering/renderer_rd/renderer_compositor_rd.cpp @@ -224,14 +224,13 @@ void RendererCompositorRD::set_boot_image(const Ref &p_image, const Color } Size2 window_size = DisplayServer::get_singleton()->window_get_size(); - Rect2 imgrect(0, 0, p_image->get_width(), p_image->get_height()); Rect2 screenrect; + if (p_scale) { screenrect = OS::get_singleton()->calculate_boot_screen_rect(window_size, imgrect.size); } else { - screenrect = imgrect; - screenrect.position += ((window_size - screenrect.size) / 2.0).floor(); + screenrect = DisplayServer::calculate_boot_image_rect(window_size, imgrect); } screenrect.position /= window_size; @@ -274,6 +273,48 @@ void RendererCompositorRD::set_boot_image(const Ref &p_image, const Color RD::get_singleton()->swap_buffers(true); + // After the first commit, a tiling compositor (e.g. Hyprland via XWayland) + // may send a ConfigureNotify with the actual tiled window size. + // pump_resize_events() waits briefly, drains ConfigureNotify events, and + // calls _window_changed() which updates wd.size and recreates the Vulkan + // swapchain via rendering_context->window_set_size(). + DisplayServer::get_singleton()->pump_resize_events(); + Size2 new_window_size = DisplayServer::get_singleton()->window_get_size(); + + if (new_window_size != window_size && new_window_size.width > 0 && new_window_size.height > 0) { + // Swapchain was already recreated by _window_changed() inside pump_resize_events(). + // screen_prepare_for_drawing() will use the new swapchain. + err = RD::get_singleton()->screen_prepare_for_drawing(DisplayServer::MAIN_WINDOW_ID); + if (err == OK) { + window_size = new_window_size; + + Rect2 screenrect2; + if (p_scale) { + screenrect2 = OS::get_singleton()->calculate_boot_screen_rect(window_size, imgrect.size); + } else { + screenrect2 = DisplayServer::calculate_boot_image_rect(window_size, imgrect); + } + screenrect2.position /= window_size; + screenrect2.size /= window_size; + + RD::DrawListID draw_list2 = RD::get_singleton()->draw_list_begin_for_screen(DisplayServer::MAIN_WINDOW_ID, p_color); + RD::get_singleton()->draw_list_bind_render_pipeline(draw_list2, blit.pipelines[BLIT_MODE_NORMAL_ALPHA]); + RD::get_singleton()->draw_list_bind_index_array(draw_list2, blit.array); + RD::get_singleton()->draw_list_bind_uniform_set(draw_list2, uset, 0); + + // Reuse all push_constant fields from the first pass; only dst_rect changes. + blit.push_constant.dst_rect[0] = screenrect2.position.x; + blit.push_constant.dst_rect[1] = screenrect2.position.y; + blit.push_constant.dst_rect[2] = screenrect2.size.width; + blit.push_constant.dst_rect[3] = screenrect2.size.height; + + RD::get_singleton()->draw_list_set_push_constant(draw_list2, &blit.push_constant, sizeof(BlitPushConstant)); + RD::get_singleton()->draw_list_draw(draw_list2, true); + RD::get_singleton()->draw_list_end(); + RD::get_singleton()->swap_buffers(true); + } + } + texture_storage->texture_free(texture); RD::get_singleton()->free(sampler); }