diff options
37 files changed, 1939 insertions, 87 deletions
diff --git a/core/error/error_macros.h b/core/error/error_macros.h index c8182975d5..016c963e04 100644 --- a/core/error/error_macros.h +++ b/core/error/error_macros.h @@ -812,4 +812,14 @@ void _err_flush_stdout(); #define DEV_ASSERT(m_cond) #endif +#ifdef DEV_ENABLED +#define DEV_CHECK_ONCE(m_cond) \ + if (unlikely(!(m_cond))) { \ + ERR_PRINT_ONCE("DEV_CHECK_ONCE failed \"" _STR(m_cond) "\" is false."); \ + } else \ + ((void)0) +#else +#define DEV_CHECK_ONCE(m_cond) +#endif + #endif // ERROR_MACROS_H diff --git a/core/io/file_access.cpp b/core/io/file_access.cpp index 6026dbf896..55286277fa 100644 --- a/core/io/file_access.cpp +++ b/core/io/file_access.cpp @@ -38,7 +38,7 @@ #include "core/io/marshalls.h" #include "core/os/os.h" -FileAccess::CreateFunc FileAccess::create_func[ACCESS_MAX] = { nullptr, nullptr }; +FileAccess::CreateFunc FileAccess::create_func[ACCESS_MAX] = {}; FileAccess::FileCloseFailNotify FileAccess::close_fail_notify = nullptr; diff --git a/core/templates/paged_array.h b/core/templates/paged_array.h index 69a792958a..6b7e0cee16 100644 --- a/core/templates/paged_array.h +++ b/core/templates/paged_array.h @@ -229,6 +229,12 @@ public: count--; } + void remove_at_unordered(uint64_t p_index) { + ERR_FAIL_UNSIGNED_INDEX(p_index, count); + (*this)[p_index] = (*this)[count - 1]; + pop_back(); + } + void clear() { //destruct if needed if (!std::is_trivially_destructible<T>::value) { diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index 57a4aea93b..b824bc4fde 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -120,7 +120,7 @@ <description> Displays OS native dialog for selecting files or directories in the file system. Callbacks have the following arguments: [code]status: bool, selected_paths: PackedStringArray, selected_filter_index: int[/code]. - [b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG] feature, i.e. Linux (X11), Windows, and macOS. + [b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG] feature. Supported platforms include Linux (X11 and Wayland), Windows, and macOS. [b]Note:[/b] [param current_directory] might be ignored. [b]Note:[/b] On Linux, [param show_hidden] is ignored. [b]Note:[/b] On macOS, native file dialogs have no title. @@ -145,7 +145,7 @@ - [code]"values"[/code] - [PackedStringArray] of values. If empty, boolean option (check box) is used. - [code]"default"[/code] - default selected option index ([int]) or default boolean value ([bool]). Callbacks have the following arguments: [code]status: bool, selected_paths: PackedStringArray, selected_filter_index: int, selected_option: Dictionary[/code]. - [b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG] feature, i.e. Linux, Windows, and macOS. + [b]Note:[/b] This method is implemented if the display server has the [constant FEATURE_NATIVE_DIALOG] feature. Supported platforms include Linux (X11 and Wayland), Windows, and macOS. [b]Note:[/b] [param current_directory] might be ignored. [b]Note:[/b] On Linux (X11), [param show_hidden] is ignored. [b]Note:[/b] On macOS, native file dialogs have no title. diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml index 9df3a31a96..449f5639fd 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -262,13 +262,14 @@ The color of the viewport border in the 2D editor. This border represents the viewport's size at the base resolution defined in the Project Settings. Objects placed outside this border will not be visible unless a [Camera2D] node is used, or unless the window is resized and the stretch mode is set to [code]disabled[/code]. </member> <member name="editors/3d/default_fov" type="float" setter="" getter=""> - The default camera field of view to use in the 3D editor (in degrees). The camera field of view can be adjusted on a per-scene basis using the [b]View[/b] menu at the top of the 3D editor. If a scene had its camera field of view adjusted using the [b]View[/b] menu, this setting is ignored in the scene in question. This setting is also ignored while a Camera3D node is being previewed in the editor. + The default camera vertical field of view to use in the 3D editor (in degrees). The camera field of view can be adjusted on a per-scene basis using the [b]View[/b] menu at the top of the 3D editor. If a scene had its camera field of view adjusted using the [b]View[/b] menu, this setting is ignored in the scene in question. This setting is also ignored while a [Camera3D] node is being previewed in the editor. + [b]Note:[/b] The editor camera always uses the [b]Keep Height[/b] aspect mode. </member> <member name="editors/3d/default_z_far" type="float" setter="" getter=""> - The default camera far clip distance to use in the 3D editor (in degrees). Higher values make it possible to view objects placed further away from the camera, at the cost of lower precision in the depth buffer (which can result in visible Z-fighting in the distance). The camera far clip distance can be adjusted on a per-scene basis using the [b]View[/b] menu at the top of the 3D editor. If a scene had its camera far clip distance adjusted using the [b]View[/b] menu, this setting is ignored in the scene in question. This setting is also ignored while a Camera3D node is being previewed in the editor. + The default camera far clip distance to use in the 3D editor (in degrees). Higher values make it possible to view objects placed further away from the camera, at the cost of lower precision in the depth buffer (which can result in visible Z-fighting in the distance). The camera far clip distance can be adjusted on a per-scene basis using the [b]View[/b] menu at the top of the 3D editor. If a scene had its camera far clip distance adjusted using the [b]View[/b] menu, this setting is ignored in the scene in question. This setting is also ignored while a [Camera3D] node is being previewed in the editor. </member> <member name="editors/3d/default_z_near" type="float" setter="" getter=""> - The default camera near clip distance to use in the 3D editor (in degrees). Lower values make it possible to view objects placed closer to the camera, at the cost of lower precision in the depth buffer (which can result in visible Z-fighting in the distance). The camera near clip distance can be adjusted on a per-scene basis using the [b]View[/b] menu at the top of the 3D editor. If a scene had its camera near clip distance adjusted using the [b]View[/b] menu, this setting is ignored in the scene in question. This setting is also ignored while a Camera3D node is being previewed in the editor. + The default camera near clip distance to use in the 3D editor (in degrees). Lower values make it possible to view objects placed closer to the camera, at the cost of lower precision in the depth buffer (which can result in visible Z-fighting in the distance). The camera near clip distance can be adjusted on a per-scene basis using the [b]View[/b] menu at the top of the 3D editor. If a scene had its camera near clip distance adjusted using the [b]View[/b] menu, this setting is ignored in the scene in question. This setting is also ignored while a [Camera3D] node is being previewed in the editor. </member> <member name="editors/3d/freelook/freelook_activation_modifier" type="int" setter="" getter=""> The modifier key to use to enable freelook in the 3D editor (on top of pressing the right mouse button). diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index aea5ef42af..ae5eb435ba 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -2548,6 +2548,10 @@ <member name="rendering/lights_and_shadows/positional_shadow/soft_shadow_filter_quality.mobile" type="int" setter="" getter="" default="0"> Lower-end override for [member rendering/lights_and_shadows/positional_shadow/soft_shadow_filter_quality] on mobile devices, due to performance concerns or driver support. </member> + <member name="rendering/lights_and_shadows/tighter_shadow_caster_culling" type="bool" setter="" getter="" default="true"> + If [code]true[/code], items that cannot cast shadows into the view frustum will not be rendered into shadow maps. + This can increase performance. + </member> <member name="rendering/lights_and_shadows/use_physical_light_units" type="bool" setter="" getter="" default="false"> Enables the use of physically based units for light sources. Physically based units tend to be much larger than the arbitrary units used by Godot, but they can be used to match lighting within Godot to real-world lighting. Due to the large dynamic range of lighting conditions present in nature, Godot bakes exposure into the various lighting quantities before rendering. Most light sources bake exposure automatically at run time based on the active [CameraAttributes] resource, but [LightmapGI] and [VoxelGI] require a [CameraAttributes] resource to be set at bake time to reduce the dynamic range. At run time, Godot will automatically reconcile the baked exposure with the active exposure to ensure lighting remains consistent. </member> diff --git a/doc/classes/SkeletonProfileHumanoid.xml b/doc/classes/SkeletonProfileHumanoid.xml index fe9eded7a9..5539d1d980 100644 --- a/doc/classes/SkeletonProfileHumanoid.xml +++ b/doc/classes/SkeletonProfileHumanoid.xml @@ -5,6 +5,63 @@ </brief_description> <description> A [SkeletonProfile] as a preset that is optimized for the human form. This exists for standardization, so all parameters are read-only. + A humanoid skeleton profile contains 54 bones divided in 4 groups: [code]"Body"[/code], [code]"Face"[/code], [code]"LeftHand"[/code], and [code]"RightHand"[/code]. It is structured as follows: + [codeblock] + Root + └─ Hips + ├─ LeftUpperLeg + │ └─ LeftLowerLeg + │ └─ LeftFoot + │ └─ LeftToes + ├─ RightUpperLeg + │ └─ RightLowerLeg + │ └─ RightFoot + │ └─ RightToes + └─ Spine + └─ Chest + └─ UpperChest + ├─ Neck + │ └─ Head + │ ├─ Jaw + │ ├─ LeftEye + │ └─ RightEye + ├─ LeftShoulder + │ └─ LeftUpperArm + │ └─ LeftLowerArm + │ └─ LeftHand + │ ├─ LeftThumbMetacarpal + │ │ └─ LeftThumbProximal + │ ├─ LeftIndexProximal + │ │ └─ LeftIndexIntermediate + │ │ └─ LeftIndexDistal + │ ├─ LeftMiddleProximal + │ │ └─ LeftMiddleIntermediate + │ │ └─ LeftMiddleDistal + │ ├─ LeftRingProximal + │ │ └─ LeftRingIntermediate + │ │ └─ LeftRingDistal + │ └─ LeftLittleProximal + │ └─ LeftLittleIntermediate + │ └─ LeftLittleDistal + └─ RightShoulder + └─ RightUpperArm + └─ RightLowerArm + └─ RightHand + ├─ RightThumbMetacarpal + │ └─ RightThumbProximal + ├─ RightIndexProximal + │ └─ RightIndexIntermediate + │ └─ RightIndexDistal + ├─ RightMiddleProximal + │ └─ RightMiddleIntermediate + │ └─ RightMiddleDistal + ├─ RightRingProximal + │ └─ RightRingIntermediate + │ └─ RightRingDistal + └─ RightLittleProximal + └─ RightLittleIntermediate + └─ RightLittleDistal + [/codeblock] </description> <tutorials> <link title="Retargeting 3D Skeletons">$DOCS_URL/tutorials/assets_pipeline/retargeting_3d_skeletons.html</link> diff --git a/doc/classes/Timer.xml b/doc/classes/Timer.xml index 03a651ad9a..00e5c6e144 100644 --- a/doc/classes/Timer.xml +++ b/doc/classes/Timer.xml @@ -15,15 +15,15 @@ <method name="is_stopped" qualifiers="const"> <return type="bool" /> <description> - Returns [code]true[/code] if the timer is stopped. + Returns [code]true[/code] if the timer is stopped or has not started. </description> </method> <method name="start"> <return type="void" /> <param index="0" name="time_sec" type="float" default="-1" /> <description> - Starts the timer. Sets [member wait_time] to [param time_sec] if [code]time_sec > 0[/code]. This also resets the remaining time to [member wait_time]. - [b]Note:[/b] This method will not resume a paused timer. See [member paused]. + Starts the timer, if it was not started already. Fails if the timer is not inside the tree. If [param time_sec] is greater than [code]0[/code], this value is used for the [member wait_time]. + [b]Note:[/b] This method does not resume a paused timer. See [member paused]. </description> </method> <method name="stop"> @@ -35,40 +35,40 @@ </methods> <members> <member name="autostart" type="bool" setter="set_autostart" getter="has_autostart" default="false"> - If [code]true[/code], the timer will automatically start when entering the scene tree. - [b]Note:[/b] This property is automatically set to [code]false[/code] after the timer enters the scene tree and starts. + If [code]true[/code], the timer will start immediately when it enters the scene tree. + [b]Note:[/b] After the timer enters the tree, this property is automatically set to [code]false[/code]. </member> <member name="one_shot" type="bool" setter="set_one_shot" getter="is_one_shot" default="false"> - If [code]true[/code], the timer will stop when reaching 0. If [code]false[/code], it will restart. + If [code]true[/code], the timer will stop after reaching the end. Otherwise, as by default, the timer will automatically restart. </member> <member name="paused" type="bool" setter="set_paused" getter="is_paused"> - If [code]true[/code], the timer is paused and will not process until it is unpaused again, even if [method start] is called. + If [code]true[/code], the timer is paused. A paused timer does not process until this property is set back to [code]false[/code], even when [method start] is called. </member> <member name="process_callback" type="int" setter="set_timer_process_callback" getter="get_timer_process_callback" enum="Timer.TimerProcessCallback" default="1"> - Processing callback. See [enum TimerProcessCallback]. + Specifies when the timer is updated during the main loop (see [enum TimerProcessCallback]). </member> <member name="time_left" type="float" setter="" getter="get_time_left"> - The timer's remaining time in seconds. Returns 0 if the timer is inactive. - [b]Note:[/b] This value is read-only and cannot be set. It is based on [member wait_time], which can be set using [method start]. + The timer's remaining time in seconds. This is always [code]0[/code] if the timer is stopped. + [b]Note:[/b] This property is read-only and cannot be modified. It is based on [member wait_time]. </member> <member name="wait_time" type="float" setter="set_wait_time" getter="get_wait_time" default="1.0"> - The wait time in seconds. - [b]Note:[/b] Timers can only emit once per rendered frame at most (or once per physics frame if [member process_callback] is [constant TIMER_PROCESS_PHYSICS]). This means very low wait times (lower than 0.05 seconds) will behave in significantly different ways depending on the rendered framerate. For very low wait times, it is recommended to use a process loop in a script instead of using a Timer node. Timers are affected by [member Engine.time_scale], a higher scale means quicker timeouts, and vice versa. + The time required for the timer to end, in seconds. This property can also be set every time [method start] is called. + [b]Note:[/b] Timers can only process once per physics or process frame (depending on the [member process_callback]). An unstable framerate may cause the timer to end inconsistently, which is especially noticeable if the wait time is lower than roughly [code]0.05[/code] seconds. For very short timers, it is recommended to write your own code instead of using a [Timer] node. Timers are also affected by [member Engine.time_scale]. </member> </members> <signals> <signal name="timeout"> <description> - Emitted when the timer reaches 0. + Emitted when the timer reaches the end. </description> </signal> </signals> <constants> <constant name="TIMER_PROCESS_PHYSICS" value="0" enum="TimerProcessCallback"> - Update the timer during physics frames (see [constant Node.NOTIFICATION_INTERNAL_PHYSICS_PROCESS]). + Update the timer every physics process frame (see [constant Node.NOTIFICATION_INTERNAL_PHYSICS_PROCESS]). </constant> <constant name="TIMER_PROCESS_IDLE" value="1" enum="TimerProcessCallback"> - Update the timer during process frames (see [constant Node.NOTIFICATION_INTERNAL_PROCESS]). + Update the timer every process (rendered) frame (see [constant Node.NOTIFICATION_INTERNAL_PROCESS]). </constant> </constants> </class> diff --git a/editor/editor_dock_manager.cpp b/editor/editor_dock_manager.cpp index 0a13377857..dc4c809860 100644 --- a/editor/editor_dock_manager.cpp +++ b/editor/editor_dock_manager.cpp @@ -510,9 +510,7 @@ void EditorDockManager::save_docks_to_config(Ref<ConfigFile> p_layout, const Str Array bottom_docks_dump; for (Control *bdock : bottom_docks) { - Control *dock = bdock; - String name = dock->get_name(); - bottom_docks_dump.push_back(name); + bottom_docks_dump.push_back(bdock->get_name()); } p_layout->set_value(p_section, "dock_bottom", bottom_docks_dump); diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index 3691172c18..6711be3d06 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -5158,27 +5158,11 @@ void EditorNode::_scene_tab_closed(int p_tab) { scene_tabs->update_scene_tabs(); } -class EditorBottomDockButton : public Button { - GDCLASS(EditorBottomDockButton, Button) - - static void _bind_methods() { - ADD_SIGNAL(MethodInfo("dropping")); - } - -public: - virtual bool can_drop_data(const Point2 &p_point, const Variant &p_data) const override { - if (!is_pressed()) { - const_cast<EditorBottomDockButton *>(this)->emit_signal("dropping"); - } - return false; - } -}; - Button *EditorNode::add_bottom_panel_item(String p_text, Control *p_item, bool p_at_front) { - Button *tb = memnew(EditorBottomDockButton); + Button *tb = memnew(Button); tb->set_flat(true); tb->connect("toggled", callable_mp(this, &EditorNode::_bottom_panel_switch_by_control).bind(p_item)); - tb->connect("dropping", callable_mp(this, &EditorNode::_bottom_panel_switch_by_control).bind(true, p_item)); + tb->set_drag_forwarding(Callable(), callable_mp(this, &EditorNode::_bottom_panel_drag_hover).bind(tb, p_item), Callable()); tb->set_text(p_text); tb->set_toggle_mode(true); tb->set_focus_mode(Control::FOCUS_NONE); @@ -6015,6 +5999,13 @@ void EditorNode::_bottom_panel_raise_toggled(bool p_pressed) { top_split->set_visible(!p_pressed); } +bool EditorNode::_bottom_panel_drag_hover(const Vector2 &, const Variant &, Button *p_button, Control *p_control) { + if (!p_button->is_pressed()) { + _bottom_panel_switch_by_control(true, p_control); + } + return false; +} + void EditorNode::_update_renderer_color() { String rendering_method = renderer->get_selected_metadata(); diff --git a/editor/editor_node.h b/editor/editor_node.h index 7d6123d04b..e7011e921e 100644 --- a/editor/editor_node.h +++ b/editor/editor_node.h @@ -657,6 +657,7 @@ private: void _bottom_panel_switch_by_control(bool p_enable, Control *p_control); void _bottom_panel_switch(bool p_enable, int p_idx); void _bottom_panel_raise_toggled(bool); + bool _bottom_panel_drag_hover(const Vector2 &, const Variant &, Button *p_button, Control *p_control); void _begin_first_scan(); diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp index facc3c3bb5..02ae15b80d 100644 --- a/editor/filesystem_dock.cpp +++ b/editor/filesystem_dock.cpp @@ -495,12 +495,7 @@ void FileSystemDock::_update_display_mode(bool p_force) { void FileSystemDock::_notification(int p_what) { switch (p_what) { - case NOTIFICATION_ENTER_TREE: { - if (initialized) { - return; - } - initialized = true; - + case NOTIFICATION_READY: { EditorFeatureProfileManager::get_singleton()->connect("current_feature_profile_changed", callable_mp(this, &FileSystemDock::_feature_profile_changed)); EditorFileSystem::get_singleton()->connect("filesystem_changed", callable_mp(this, &FileSystemDock::_fs_changed)); EditorResourcePreview::get_singleton()->connect("preview_invalidated", callable_mp(this, &FileSystemDock::_preview_invalidated)); diff --git a/editor/filesystem_dock.h b/editor/filesystem_dock.h index d161a3bd15..c9d2bc535e 100644 --- a/editor/filesystem_dock.h +++ b/editor/filesystem_dock.h @@ -230,8 +230,6 @@ private: String current_path; String select_after_scan; - bool initialized = false; - bool updating_tree = false; int tree_update_id; FileSystemTree *tree = nullptr; diff --git a/editor/plugins/node_3d_editor_plugin.cpp b/editor/plugins/node_3d_editor_plugin.cpp index e4d24832bf..df2203009f 100644 --- a/editor/plugins/node_3d_editor_plugin.cpp +++ b/editor/plugins/node_3d_editor_plugin.cpp @@ -1749,7 +1749,8 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { _edit.mode = TRANSFORM_NONE; _edit.original = spatial_editor->get_gizmo_transform(); // To prevent to break when flipping with scale. - bool can_select_gizmos = spatial_editor->get_single_selected_node(); + bool node_selected = spatial_editor->get_single_selected_node(); + bool can_select_gizmos = node_selected; { int idx = view_menu->get_popup()->get_item_index(VIEW_GIZMOS); @@ -1839,17 +1840,17 @@ void Node3DEditorViewport::_sinput(const Ref<InputEvent> &p_event) { clicked = ObjectID(); - if (can_select_gizmos && ((spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT && b->is_command_or_control_pressed()) || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE)) { + if (node_selected && ((spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SELECT && b->is_command_or_control_pressed()) || spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_ROTATE)) { begin_transform(TRANSFORM_ROTATE, false); break; } - if (can_select_gizmos && spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_MOVE) { + if (node_selected && spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_MOVE) { begin_transform(TRANSFORM_TRANSLATE, false); break; } - if (can_select_gizmos && spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SCALE) { + if (node_selected && spatial_editor->get_tool_mode() == Node3DEditor::TOOL_MODE_SCALE) { begin_transform(TRANSFORM_SCALE, false); break; } @@ -8576,7 +8577,8 @@ Node3DEditor::Node3DEditor() { settings_fov->set_step(0.1); settings_fov->set_value(EDITOR_GET("editors/3d/default_fov")); settings_fov->set_select_all_on_focus(true); - settings_vbc->add_margin_child(TTR("Perspective FOV (deg.):"), settings_fov); + settings_fov->set_tooltip_text(TTR("FOV is defined as a vertical value, as the editor camera always uses the Keep Height aspect mode.")); + settings_vbc->add_margin_child(TTR("Perspective VFOV (deg.):"), settings_fov); settings_znear = memnew(SpinBox); settings_znear->set_max(MAX_Z); diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 2acca5b856..f2dccf6d68 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -3816,7 +3816,6 @@ Error GLTFDocument::_serialize_materials(Ref<GLTFState> p_state) { } img->decompress(); img->convert(Image::FORMAT_RGBA8); - img->convert_ra_rgba8_to_rg(); for (int32_t y = 0; y < img->get_height(); y++) { for (int32_t x = 0; x < img->get_width(); x++) { Color c = img->get_pixel(x, y); diff --git a/modules/openxr/doc_classes/OpenXRExtensionWrapperExtension.xml b/modules/openxr/doc_classes/OpenXRExtensionWrapperExtension.xml index e09b58e5b4..fa93704a0a 100644 --- a/modules/openxr/doc_classes/OpenXRExtensionWrapperExtension.xml +++ b/modules/openxr/doc_classes/OpenXRExtensionWrapperExtension.xml @@ -123,6 +123,14 @@ Called when the OpenXR session state is changed to visible. This means OpenXR is now ready to receive frames. </description> </method> + <method name="_set_hand_joint_locations_and_get_next_pointer" qualifiers="virtual"> + <return type="int" /> + <param index="0" name="hand_index" type="int" /> + <param index="1" name="next_pointer" type="void*" /> + <description> + Adds additional data structures when each hand tracker is created. + </description> + </method> <method name="_set_instance_create_info_and_get_next_pointer" qualifiers="virtual"> <return type="int" /> <param index="0" name="next_pointer" type="void*" /> diff --git a/modules/openxr/extensions/openxr_extension_wrapper.h b/modules/openxr/extensions/openxr_extension_wrapper.h index 31f8d23268..b9c9247bee 100644 --- a/modules/openxr/extensions/openxr_extension_wrapper.h +++ b/modules/openxr/extensions/openxr_extension_wrapper.h @@ -60,6 +60,7 @@ public: virtual void *set_instance_create_info_and_get_next_pointer(void *p_next_pointer) { return p_next_pointer; } // Add additional data structures when we create our OpenXR instance. virtual void *set_session_create_and_get_next_pointer(void *p_next_pointer) { return p_next_pointer; } // Add additional data structures when we create our OpenXR session. virtual void *set_swapchain_create_info_and_get_next_pointer(void *p_next_pointer) { return p_next_pointer; } // Add additional data structures when creating OpenXR swap chains. + virtual void *set_hand_joint_locations_and_get_next_pointer(int p_hand_index, void *p_next_pointer) { return p_next_pointer; } // `on_register_metadata` allows extensions to register additional controller metadata. // This function is called even when OpenXRApi is not constructured as the metadata diff --git a/modules/openxr/extensions/openxr_extension_wrapper_extension.cpp b/modules/openxr/extensions/openxr_extension_wrapper_extension.cpp index 4829f713d2..23238cabb9 100644 --- a/modules/openxr/extensions/openxr_extension_wrapper_extension.cpp +++ b/modules/openxr/extensions/openxr_extension_wrapper_extension.cpp @@ -38,6 +38,7 @@ void OpenXRExtensionWrapperExtension::_bind_methods() { GDVIRTUAL_BIND(_set_instance_create_info_and_get_next_pointer, "next_pointer"); GDVIRTUAL_BIND(_set_session_create_and_get_next_pointer, "next_pointer"); GDVIRTUAL_BIND(_set_swapchain_create_info_and_get_next_pointer, "next_pointer"); + GDVIRTUAL_BIND(_set_hand_joint_locations_and_get_next_pointer, "hand_index", "next_pointer"); GDVIRTUAL_BIND(_on_register_metadata); GDVIRTUAL_BIND(_on_before_instance_created); GDVIRTUAL_BIND(_on_instance_created, "instance"); @@ -117,6 +118,16 @@ void *OpenXRExtensionWrapperExtension::set_swapchain_create_info_and_get_next_po return nullptr; } +void *OpenXRExtensionWrapperExtension::set_hand_joint_locations_and_get_next_pointer(int p_hand_index, void *p_next_pointer) { + uint64_t pointer; + + if (GDVIRTUAL_CALL(_set_hand_joint_locations_and_get_next_pointer, p_hand_index, GDExtensionPtr<void>(p_next_pointer), pointer)) { + return reinterpret_cast<void *>(pointer); + } + + return nullptr; +} + void OpenXRExtensionWrapperExtension::on_register_metadata() { GDVIRTUAL_CALL(_on_register_metadata); } diff --git a/modules/openxr/extensions/openxr_extension_wrapper_extension.h b/modules/openxr/extensions/openxr_extension_wrapper_extension.h index 5c5e64f927..6acf229e16 100644 --- a/modules/openxr/extensions/openxr_extension_wrapper_extension.h +++ b/modules/openxr/extensions/openxr_extension_wrapper_extension.h @@ -58,12 +58,14 @@ public: virtual void *set_instance_create_info_and_get_next_pointer(void *p_next_pointer) override; virtual void *set_session_create_and_get_next_pointer(void *p_next_pointer) override; virtual void *set_swapchain_create_info_and_get_next_pointer(void *p_next_pointer) override; + virtual void *set_hand_joint_locations_and_get_next_pointer(int p_hand_index, void *p_next_pointer) override; //TODO workaround as GDExtensionPtr<void> return type results in build error in godot-cpp GDVIRTUAL1R(uint64_t, _set_system_properties_and_get_next_pointer, GDExtensionPtr<void>); GDVIRTUAL1R(uint64_t, _set_instance_create_info_and_get_next_pointer, GDExtensionPtr<void>); GDVIRTUAL1R(uint64_t, _set_session_create_and_get_next_pointer, GDExtensionPtr<void>); GDVIRTUAL1R(uint64_t, _set_swapchain_create_info_and_get_next_pointer, GDExtensionPtr<void>); + GDVIRTUAL2R(uint64_t, _set_hand_joint_locations_and_get_next_pointer, int, GDExtensionPtr<void>); virtual void on_register_metadata() override; virtual void on_before_instance_created() override; diff --git a/modules/openxr/extensions/openxr_hand_tracking_extension.cpp b/modules/openxr/extensions/openxr_hand_tracking_extension.cpp index 04fc4bf890..884fb41a3c 100644 --- a/modules/openxr/extensions/openxr_hand_tracking_extension.cpp +++ b/modules/openxr/extensions/openxr_hand_tracking_extension.cpp @@ -179,6 +179,14 @@ void OpenXRHandTrackingExtension::on_process() { next_pointer = &hand_trackers[i].data_source; } + // Needed for vendor hand tracking extensions implemented from GDExtension. + for (OpenXRExtensionWrapper *wrapper : OpenXRAPI::get_singleton()->get_registered_extension_wrappers()) { + void *np = wrapper->set_hand_joint_locations_and_get_next_pointer(i, next_pointer); + if (np != nullptr) { + next_pointer = np; + } + } + hand_trackers[i].locations.type = XR_TYPE_HAND_JOINT_LOCATIONS_EXT; hand_trackers[i].locations.next = next_pointer; hand_trackers[i].locations.isActive = false; diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp index eafabe03e7..7b777a9845 100644 --- a/modules/openxr/openxr_api.cpp +++ b/modules/openxr/openxr_api.cpp @@ -1497,6 +1497,10 @@ void OpenXRAPI::unregister_extension_wrapper(OpenXRExtensionWrapper *p_extension registered_extension_wrappers.erase(p_extension_wrapper); } +const Vector<OpenXRExtensionWrapper *> &OpenXRAPI::get_registered_extension_wrappers() { + return registered_extension_wrappers; +} + void OpenXRAPI::register_extension_metadata() { for (OpenXRExtensionWrapper *extension_wrapper : registered_extension_wrappers) { extension_wrapper->on_register_metadata(); diff --git a/modules/openxr/openxr_api.h b/modules/openxr/openxr_api.h index 6e55020aef..8cc6be3a9d 100644 --- a/modules/openxr/openxr_api.h +++ b/modules/openxr/openxr_api.h @@ -329,6 +329,7 @@ public: void set_xr_interface(OpenXRInterface *p_xr_interface); static void register_extension_wrapper(OpenXRExtensionWrapper *p_extension_wrapper); static void unregister_extension_wrapper(OpenXRExtensionWrapper *p_extension_wrapper); + static const Vector<OpenXRExtensionWrapper *> &get_registered_extension_wrappers(); static void register_extension_metadata(); static void cleanup_extension_wrappers(); diff --git a/platform/linuxbsd/detect.py b/platform/linuxbsd/detect.py index e6a47f556d..7946ef6228 100644 --- a/platform/linuxbsd/detect.py +++ b/platform/linuxbsd/detect.py @@ -208,8 +208,8 @@ def configure(env: "Environment"): if env["wayland"]: if os.system("wayland-scanner -v") != 0: - print("wayland-scanner not found. Aborting.") - sys.exit(255) + print("wayland-scanner not found. Disabling wayland support.") + env["wayland"] = False if env["touch"]: env.Append(CPPDEFINES=["TOUCH_ENABLED"]) diff --git a/platform/linuxbsd/wayland/SCsub b/platform/linuxbsd/wayland/SCsub index dbb3c02690..d2b000ff66 100644 --- a/platform/linuxbsd/wayland/SCsub +++ b/platform/linuxbsd/wayland/SCsub @@ -151,11 +151,22 @@ env.WAYLAND_API_CODE( source="#thirdparty/wayland-protocols/unstable/tablet/tablet-unstable-v2.xml", ) +env.WAYLAND_API_HEADER( + target="protocol/xdg_foreign.gen.h", + source="#thirdparty/wayland-protocols/unstable/xdg-foreign/xdg-foreign-unstable-v1.xml", +) + +env.WAYLAND_API_CODE( + target="protocol/xdg_foreign.gen.c", + source="#thirdparty/wayland-protocols/unstable/xdg-foreign/xdg-foreign-unstable-v1.xml", +) + source_files = [ "protocol/wayland.gen.c", "protocol/viewporter.gen.c", "protocol/fractional_scale.gen.c", "protocol/xdg_shell.gen.c", + "protocol/xdg_foreign.gen.c", "protocol/xdg_decoration.gen.c", "protocol/xdg_activation.gen.c", "protocol/relative_pointer.gen.c", diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp index 556c260360..02b715056a 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.cpp +++ b/platform/linuxbsd/wayland/display_server_wayland.cpp @@ -196,6 +196,9 @@ bool DisplayServerWayland::has_feature(Feature p_feature) const { case FEATURE_SWAP_BUFFERS: case FEATURE_KEEP_SCREEN_ON: case FEATURE_CLIPBOARD_PRIMARY: +#ifdef DBUS_ENABLED + case FEATURE_NATIVE_DIALOG: +#endif case FEATURE_HIDPI: { return true; } break; @@ -269,6 +272,22 @@ bool DisplayServerWayland::is_dark_mode() const { } } +Error DisplayServerWayland::file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) { + WindowID window_id = MAIN_WINDOW_ID; + // TODO: Use window IDs for multiwindow support. + + WaylandThread::WindowState *ws = wayland_thread.wl_surface_get_window_state(wayland_thread.window_get_wl_surface(window_id)); + return portal_desktop->file_dialog_show(window_id, (ws ? ws->exported_handle : String()), p_title, p_current_directory, String(), p_filename, p_mode, p_filters, TypedArray<Dictionary>(), p_callback, false); +} + +Error DisplayServerWayland::file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) { + WindowID window_id = MAIN_WINDOW_ID; + // TODO: Use window IDs for multiwindow support. + + WaylandThread::WindowState *ws = wayland_thread.wl_surface_get_window_state(wayland_thread.window_get_wl_surface(window_id)); + return portal_desktop->file_dialog_show(window_id, (ws ? ws->exported_handle : String()), p_title, p_current_directory, p_root, p_filename, p_mode, p_filters, p_options, p_callback, true); +} + #endif void DisplayServerWayland::mouse_set_mode(MouseMode p_mode) { @@ -1223,10 +1242,11 @@ DisplayServerWayland::DisplayServerWayland(const String &p_rendering_driver, Win if (context_rd) { if (context_rd->initialize() != OK) { + ERR_PRINT(vformat("Could not initialize %s", context_rd->get_api_name())); memdelete(context_rd); context_rd = nullptr; r_error = ERR_CANT_CREATE; - ERR_FAIL_MSG(vformat("Could not initialize %s", context_rd->get_api_name())); + return; } } #endif diff --git a/platform/linuxbsd/wayland/display_server_wayland.h b/platform/linuxbsd/wayland/display_server_wayland.h index 56e24fe3f3..3e7f3c4cb4 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.h +++ b/platform/linuxbsd/wayland/display_server_wayland.h @@ -171,6 +171,9 @@ public: #ifdef DBUS_ENABLED virtual bool is_dark_mode_supported() const override; virtual bool is_dark_mode() const override; + + virtual Error file_dialog_show(const String &p_title, const String &p_current_directory, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const Callable &p_callback) override; + virtual Error file_dialog_with_options_show(const String &p_title, const String &p_current_directory, const String &p_root, const String &p_filename, bool p_show_hidden, FileDialogMode p_mode, const Vector<String> &p_filters, const TypedArray<Dictionary> &p_options, const Callable &p_callback) override; #endif virtual void mouse_set_mode(MouseMode p_mode) override; diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp index 9410e4653d..7e96f2dd75 100644 --- a/platform/linuxbsd/wayland/wayland_thread.cpp +++ b/platform/linuxbsd/wayland/wayland_thread.cpp @@ -370,6 +370,12 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re return; } + if (strcmp(interface, zxdg_exporter_v1_interface.name) == 0) { + registry->wl_exporter = (struct zxdg_exporter_v1 *)wl_registry_bind(wl_registry, name, &zxdg_exporter_v1_interface, 1); + registry->wl_exporter_name = name; + return; + } + if (strcmp(interface, wl_compositor_interface.name) == 0) { registry->wl_compositor = (struct wl_compositor *)wl_registry_bind(wl_registry, name, &wl_compositor_interface, 4); registry->wl_compositor_name = name; @@ -570,6 +576,17 @@ void WaylandThread::_wl_registry_on_global_remove(void *data, struct wl_registry return; } + if (name == registry->wl_exporter_name) { + if (registry->wl_exporter) { + zxdg_exporter_v1_destroy(registry->wl_exporter); + registry->wl_exporter = nullptr; + } + + registry->wl_exporter_name = 0; + + return; + } + if (name == registry->wl_compositor_name) { if (registry->wl_compositor) { wl_compositor_destroy(registry->wl_compositor); @@ -1107,6 +1124,13 @@ void WaylandThread::_xdg_toplevel_on_wm_capabilities(void *data, struct xdg_topl } } +void WaylandThread::_xdg_exported_on_exported(void *data, zxdg_exported_v1 *exported, const char *handle) { + WindowState *ws = (WindowState *)data; + ERR_FAIL_NULL(ws); + + ws->exported_handle = vformat("wayland:%s", String::utf8(handle)); +} + void WaylandThread::_xdg_toplevel_decoration_on_configure(void *data, struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration, uint32_t mode) { if (mode == ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE) { #ifdef LIBDECOR_ENABLED @@ -2975,6 +2999,11 @@ void WaylandThread::window_create(DisplayServer::WindowID p_window_id, int p_wid // "loop". wl_surface_commit(ws.wl_surface); + if (registry.wl_exporter) { + ws.xdg_exported = zxdg_exporter_v1_export(registry.wl_exporter, ws.wl_surface); + zxdg_exported_v1_add_listener(ws.xdg_exported, &xdg_exported_listener, &ws); + } + // Wait for the surface to be configured before continuing. wl_display_roundtrip(wl_display); } @@ -3980,6 +4009,10 @@ void WaylandThread::destroy() { xdg_wm_base_destroy(registry.xdg_wm_base); } + if (registry.wl_exporter) { + zxdg_exporter_v1_destroy(registry.wl_exporter); + } + if (registry.wl_shm) { wl_shm_destroy(registry.wl_shm); } diff --git a/platform/linuxbsd/wayland/wayland_thread.h b/platform/linuxbsd/wayland/wayland_thread.h index 8591db4306..43c562aade 100644 --- a/platform/linuxbsd/wayland/wayland_thread.h +++ b/platform/linuxbsd/wayland/wayland_thread.h @@ -63,6 +63,7 @@ #include "wayland/protocol/wayland.gen.h" #include "wayland/protocol/xdg_activation.gen.h" #include "wayland/protocol/xdg_decoration.gen.h" +#include "wayland/protocol/xdg_foreign.gen.h" #include "wayland/protocol/xdg_shell.gen.h" #ifdef LIBDECOR_ENABLED @@ -132,6 +133,9 @@ public: struct xdg_wm_base *xdg_wm_base = nullptr; uint32_t xdg_wm_base_name = 0; + struct zxdg_exporter_v1 *wl_exporter = nullptr; + uint32_t wl_exporter_name = 0; + // wayland-protocols globals. struct wp_viewporter *wp_viewporter = nullptr; @@ -197,6 +201,9 @@ public: struct wp_viewport *wp_viewport = nullptr; struct wp_fractional_scale_v1 *wp_fractional_scale = nullptr; + struct zxdg_exported_v1 *xdg_exported = nullptr; + + String exported_handle; // Currently applied buffer scale. int buffer_scale = 1; @@ -599,6 +606,8 @@ private: static void _xdg_toplevel_decoration_on_configure(void *data, struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration, uint32_t mode); + static void _xdg_exported_on_exported(void *data, zxdg_exported_v1 *exported, const char *handle); + static void _xdg_activation_token_on_done(void *data, struct xdg_activation_token_v1 *xdg_activation_token, const char *token); // Core Wayland event listeners. @@ -753,6 +762,10 @@ private: .frame = _wp_tablet_tool_on_frame, }; + static constexpr struct zxdg_exported_v1_listener xdg_exported_listener = { + .handle = _xdg_exported_on_exported + }; + static constexpr struct zxdg_toplevel_decoration_v1_listener xdg_toplevel_decoration_listener = { .configure = _xdg_toplevel_decoration_on_configure, }; diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 657caa7939..fa73740e04 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -247,8 +247,13 @@ public: // IUnknown methods HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) { static const QITAB qit[] = { +#ifdef __MINGW32__ + { &__uuidof(IFileDialogEvents), static_cast<decltype(qit[0].dwOffset)>(OFFSETOFCLASS(IFileDialogEvents, FileDialogEventHandler)) }, + { &__uuidof(IFileDialogControlEvents), static_cast<decltype(qit[0].dwOffset)>(OFFSETOFCLASS(IFileDialogControlEvents, FileDialogEventHandler)) }, +#else QITABENT(FileDialogEventHandler, IFileDialogEvents), QITABENT(FileDialogEventHandler, IFileDialogControlEvents), +#endif { 0, 0 }, }; return QISearch(this, qit, riid, ppv); diff --git a/scene/resources/animation.cpp b/scene/resources/animation.cpp index e503c79bed..6532a15114 100644 --- a/scene/resources/animation.cpp +++ b/scene/resources/animation.cpp @@ -6065,7 +6065,7 @@ Variant Animation::cubic_interpolate_in_time_variant(const Variant &pre_a, const return Variant(); } break; case Variant::FLOAT: { - return Math::cubic_interpolate_in_time(a.operator double(), b.operator double(), pre_a.operator double(), post_b.operator double(), (double)c, (double)p_pre_a_t, (double)p_b_t, (double)p_post_b_t); + return Math::cubic_interpolate_in_time(a.operator double(), b.operator double(), pre_a.operator double(), post_b.operator double(), (double)c, (double)p_b_t, (double)p_pre_a_t, (double)p_post_b_t); } break; case Variant::VECTOR2: { return (a.operator Vector2()).cubic_interpolate_in_time(b.operator Vector2(), pre_a.operator Vector2(), post_b.operator Vector2(), c, p_b_t, p_pre_a_t, p_post_b_t); @@ -6100,10 +6100,10 @@ Variant Animation::cubic_interpolate_in_time_variant(const Variant &pre_a, const const Color cb = b.operator Color(); const Color cpb = post_b.operator Color(); return Color( - Math::cubic_interpolate_in_time((double)ca.r, (double)cb.r, (double)cpa.r, (double)cpb.r, (double)c, (double)p_pre_a_t, (double)p_b_t, (double)p_post_b_t), - Math::cubic_interpolate_in_time((double)ca.g, (double)cb.g, (double)cpa.g, (double)cpb.g, (double)c, (double)p_pre_a_t, (double)p_b_t, (double)p_post_b_t), - Math::cubic_interpolate_in_time((double)ca.b, (double)cb.b, (double)cpa.b, (double)cpb.b, (double)c, (double)p_pre_a_t, (double)p_b_t, (double)p_post_b_t), - Math::cubic_interpolate_in_time((double)ca.a, (double)cb.a, (double)cpa.a, (double)cpb.a, (double)c, (double)p_pre_a_t, (double)p_b_t, (double)p_post_b_t)); + Math::cubic_interpolate_in_time((double)ca.r, (double)cb.r, (double)cpa.r, (double)cpb.r, (double)c, (double)p_b_t, (double)p_pre_a_t, (double)p_post_b_t), + Math::cubic_interpolate_in_time((double)ca.g, (double)cb.g, (double)cpa.g, (double)cpb.g, (double)c, (double)p_b_t, (double)p_pre_a_t, (double)p_post_b_t), + Math::cubic_interpolate_in_time((double)ca.b, (double)cb.b, (double)cpa.b, (double)cpb.b, (double)c, (double)p_b_t, (double)p_pre_a_t, (double)p_post_b_t), + Math::cubic_interpolate_in_time((double)ca.a, (double)cb.a, (double)cpa.a, (double)cpb.a, (double)c, (double)p_b_t, (double)p_pre_a_t, (double)p_post_b_t)); } break; case Variant::AABB: { const ::AABB apa = pre_a.operator ::AABB(); @@ -6120,9 +6120,9 @@ Variant Animation::cubic_interpolate_in_time_variant(const Variant &pre_a, const const Basis bb = b.operator Basis(); const Basis bpb = post_b.operator Basis(); return Basis( - ba.rows[0].cubic_interpolate_in_time(bb.rows[0], bpa.rows[0], bpb.rows[0], c, p_pre_a_t, p_b_t, p_post_b_t), - ba.rows[1].cubic_interpolate_in_time(bb.rows[1], bpa.rows[1], bpb.rows[1], c, p_pre_a_t, p_b_t, p_post_b_t), - ba.rows[2].cubic_interpolate_in_time(bb.rows[2], bpa.rows[2], bpb.rows[2], c, p_pre_a_t, p_b_t, p_post_b_t)); + ba.rows[0].cubic_interpolate_in_time(bb.rows[0], bpa.rows[0], bpb.rows[0], c, p_b_t, p_pre_a_t, p_post_b_t), + ba.rows[1].cubic_interpolate_in_time(bb.rows[1], bpa.rows[1], bpb.rows[1], c, p_b_t, p_pre_a_t, p_post_b_t), + ba.rows[2].cubic_interpolate_in_time(bb.rows[2], bpa.rows[2], bpb.rows[2], c, p_b_t, p_pre_a_t, p_post_b_t)); } break; case Variant::QUATERNION: { return (a.operator Quaternion()).spherical_cubic_interpolate_in_time(b.operator Quaternion(), pre_a.operator Quaternion(), post_b.operator Quaternion(), c, p_b_t, p_pre_a_t, p_post_b_t); @@ -6134,9 +6134,9 @@ Variant Animation::cubic_interpolate_in_time_variant(const Variant &pre_a, const const Transform2D tpb = post_b.operator Transform2D(); // TODO: May cause unintended skew, we needs spherical_cubic_interpolate_in_time() for angle and Transform2D::cubic_interpolate_with(). return Transform2D( - ta[0].cubic_interpolate_in_time(tb[0], tpa[0], tpb[0], c, p_pre_a_t, p_b_t, p_post_b_t), - ta[1].cubic_interpolate_in_time(tb[1], tpa[1], tpb[1], c, p_pre_a_t, p_b_t, p_post_b_t), - ta[2].cubic_interpolate_in_time(tb[2], tpa[2], tpb[2], c, p_pre_a_t, p_b_t, p_post_b_t)); + ta[0].cubic_interpolate_in_time(tb[0], tpa[0], tpb[0], c, p_b_t, p_pre_a_t, p_post_b_t), + ta[1].cubic_interpolate_in_time(tb[1], tpa[1], tpb[1], c, p_b_t, p_pre_a_t, p_post_b_t), + ta[2].cubic_interpolate_in_time(tb[2], tpa[2], tpb[2], c, p_b_t, p_pre_a_t, p_post_b_t)); } break; case Variant::TRANSFORM3D: { const Transform3D tpa = pre_a.operator Transform3D(); @@ -6145,10 +6145,10 @@ Variant Animation::cubic_interpolate_in_time_variant(const Variant &pre_a, const const Transform3D tpb = post_b.operator Transform3D(); // TODO: May cause unintended skew, we needs Transform3D::cubic_interpolate_with(). return Transform3D( - ta.basis.rows[0].cubic_interpolate_in_time(tb.basis.rows[0], tpa.basis.rows[0], tpb.basis.rows[0], c, p_pre_a_t, p_b_t, p_post_b_t), - ta.basis.rows[1].cubic_interpolate_in_time(tb.basis.rows[1], tpa.basis.rows[1], tpb.basis.rows[1], c, p_pre_a_t, p_b_t, p_post_b_t), - ta.basis.rows[2].cubic_interpolate_in_time(tb.basis.rows[2], tpa.basis.rows[2], tpb.basis.rows[2], c, p_pre_a_t, p_b_t, p_post_b_t), - ta.origin.cubic_interpolate_in_time(tb.origin, tpa.origin, tpb.origin, c, p_pre_a_t, p_b_t, p_post_b_t)); + ta.basis.rows[0].cubic_interpolate_in_time(tb.basis.rows[0], tpa.basis.rows[0], tpb.basis.rows[0], c, p_b_t, p_pre_a_t, p_post_b_t), + ta.basis.rows[1].cubic_interpolate_in_time(tb.basis.rows[1], tpa.basis.rows[1], tpb.basis.rows[1], c, p_b_t, p_pre_a_t, p_post_b_t), + ta.basis.rows[2].cubic_interpolate_in_time(tb.basis.rows[2], tpa.basis.rows[2], tpb.basis.rows[2], c, p_b_t, p_pre_a_t, p_post_b_t), + ta.origin.cubic_interpolate_in_time(tb.origin, tpa.origin, tpb.origin, c, p_b_t, p_pre_a_t, p_post_b_t)); } break; case Variant::BOOL: case Variant::INT: diff --git a/servers/rendering/renderer_scene_cull.cpp b/servers/rendering/renderer_scene_cull.cpp index 2f7e4fef06..b8f14bb611 100644 --- a/servers/rendering/renderer_scene_cull.cpp +++ b/servers/rendering/renderer_scene_cull.cpp @@ -33,6 +33,7 @@ #include "core/config/project_settings.h" #include "core/object/worker_thread_pool.h" #include "core/os/os.h" +#include "rendering_light_culler.h" #include "rendering_server_default.h" #include <new> @@ -158,7 +159,7 @@ void RendererSceneCull::_instance_pair(Instance *p_A, Instance *p_B) { light->geometries.insert(A); if (geom->can_cast_shadows) { - light->shadow_dirty = true; + light->make_shadow_dirty(); } if (A->scenario && A->array_index >= 0) { @@ -265,7 +266,7 @@ void RendererSceneCull::_instance_unpair(Instance *p_A, Instance *p_B) { light->geometries.erase(A); if (geom->can_cast_shadows) { - light->shadow_dirty = true; + light->make_shadow_dirty(); } if (A->scenario && A->array_index >= 0) { @@ -871,7 +872,7 @@ void RendererSceneCull::instance_set_layer_mask(RID p_instance, uint32_t p_mask) if (geom->can_cast_shadows) { for (HashSet<RendererSceneCull::Instance *>::Iterator I = geom->lights.begin(); I != geom->lights.end(); ++I) { InstanceLightData *light = static_cast<InstanceLightData *>((*I)->base_data); - light->shadow_dirty = true; + light->make_shadow_dirty(); } } } @@ -1565,7 +1566,7 @@ void RendererSceneCull::_update_instance(Instance *p_instance) { RSG::light_storage->light_instance_set_transform(light->instance, p_instance->transform); RSG::light_storage->light_instance_set_aabb(light->instance, p_instance->transform.xform(p_instance->aabb)); - light->shadow_dirty = true; + light->make_shadow_dirty(); RS::LightBakeMode bake_mode = RSG::light_storage->light_get_bake_mode(p_instance->base); if (RSG::light_storage->light_get_type(p_instance->base) != RS::LIGHT_DIRECTIONAL && bake_mode != light->bake_mode) { @@ -1650,7 +1651,7 @@ void RendererSceneCull::_update_instance(Instance *p_instance) { if (geom->can_cast_shadows) { for (const Instance *E : geom->lights) { InstanceLightData *light = static_cast<InstanceLightData *>(E->base_data); - light->shadow_dirty = true; + light->make_shadow_dirty(); } } @@ -2075,6 +2076,9 @@ void RendererSceneCull::_update_instance_lightmap_captures(Instance *p_instance) } void RendererSceneCull::_light_instance_setup_directional_shadow(int p_shadow_index, Instance *p_instance, const Transform3D p_cam_transform, const Projection &p_cam_projection, bool p_cam_orthogonal, bool p_cam_vaspect) { + // For later tight culling, the light culler needs to know the details of the directional light. + light_culler->prepare_directional_light(p_instance, p_shadow_index); + InstanceLightData *light = static_cast<InstanceLightData *>(p_instance->base_data); Transform3D light_transform = p_instance->transform; @@ -2345,6 +2349,10 @@ bool RendererSceneCull::_light_instance_update_shadow(Instance *p_instance, cons RendererSceneRender::RenderShadowData &shadow_data = render_shadow_data[max_shadows_used++]; + if (!light->is_shadow_update_full()) { + light_culler->cull_regular_light(instance_shadow_cull_result); + } + for (int j = 0; j < (int)instance_shadow_cull_result.size(); j++) { Instance *instance = instance_shadow_cull_result[j]; if (!instance->visible || !((1 << instance->base_type) & RS::INSTANCE_GEOMETRY_MASK) || !static_cast<InstanceGeometryData *>(instance->base_data)->can_cast_shadows || !(p_visible_layers & instance->layer_mask)) { @@ -2423,6 +2431,10 @@ bool RendererSceneCull::_light_instance_update_shadow(Instance *p_instance, cons RendererSceneRender::RenderShadowData &shadow_data = render_shadow_data[max_shadows_used++]; + if (!light->is_shadow_update_full()) { + light_culler->cull_regular_light(instance_shadow_cull_result); + } + for (int j = 0; j < (int)instance_shadow_cull_result.size(); j++) { Instance *instance = instance_shadow_cull_result[j]; if (!instance->visible || !((1 << instance->base_type) & RS::INSTANCE_GEOMETRY_MASK) || !static_cast<InstanceGeometryData *>(instance->base_data)->can_cast_shadows || !(p_visible_layers & instance->layer_mask)) { @@ -2486,6 +2498,10 @@ bool RendererSceneCull::_light_instance_update_shadow(Instance *p_instance, cons RendererSceneRender::RenderShadowData &shadow_data = render_shadow_data[max_shadows_used++]; + if (!light->is_shadow_update_full()) { + light_culler->cull_regular_light(instance_shadow_cull_result); + } + for (int j = 0; j < (int)instance_shadow_cull_result.size(); j++) { Instance *instance = instance_shadow_cull_result[j]; if (!instance->visible || !((1 << instance->base_type) & RS::INSTANCE_GEOMETRY_MASK) || !static_cast<InstanceGeometryData *>(instance->base_data)->can_cast_shadows || !(p_visible_layers & instance->layer_mask)) { @@ -2940,6 +2956,9 @@ void RendererSceneCull::_scene_cull(CullData &cull_data, InstanceCullResult &cul } for (uint32_t j = 0; j < cull_data.cull->shadow_count; j++) { + if (!light_culler->cull_directional_light(cull_data.scenario->instance_aabbs[i], j)) { + continue; + } for (uint32_t k = 0; k < cull_data.cull->shadows[j].cascade_count; k++) { if (IN_FRUSTUM(cull_data.cull->shadows[j].cascades[k].frustum) && VIS_CHECK) { uint32_t base_type = idata.flags & InstanceData::FLAG_BASE_TYPE_MASK; @@ -2992,6 +3011,9 @@ void RendererSceneCull::_scene_cull(CullData &cull_data, InstanceCullResult &cul void RendererSceneCull::_render_scene(const RendererSceneRender::CameraData *p_camera_data, const Ref<RenderSceneBuffers> &p_render_buffers, RID p_environment, RID p_force_camera_attributes, uint32_t p_visible_layers, RID p_scenario, RID p_viewport, RID p_shadow_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, bool p_using_shadows, RenderingMethod::RenderInfo *r_render_info) { Instance *render_reflection_probe = instance_owner.get_or_null(p_reflection_probe); //if null, not rendering to it + // Prepare the light - camera volume culling system. + light_culler->prepare_camera(p_camera_data->main_transform, p_camera_data->main_projection); + Scenario *scenario = scenario_owner.get_or_null(p_scenario); ERR_FAIL_COND(p_render_buffers.is_null()); @@ -3126,6 +3148,7 @@ void RendererSceneCull::_render_scene(const RendererSceneRender::CameraData *p_c #ifdef DEBUG_CULL_TIME uint64_t time_from = OS::get_singleton()->get_ticks_usec(); #endif + if (cull_to > thread_cull_threshold) { //multiple threads for (InstanceCullResult &thread : scene_cull_result_threads) { @@ -3263,9 +3286,31 @@ void RendererSceneCull::_render_scene(const RendererSceneRender::CameraData *p_c } } - if (light->shadow_dirty) { - light->last_version++; - light->shadow_dirty = false; + // We can detect whether multiple cameras are hitting this light, whether or not the shadow is dirty, + // so that we can turn off tighter caster culling. + light->detect_light_intersects_multiple_cameras(Engine::get_singleton()->get_frames_drawn()); + + if (light->is_shadow_dirty()) { + // Dirty shadows have no need to be drawn if + // the light volume doesn't intersect the camera frustum. + + // Returns false if the entire light can be culled. + bool allow_redraw = light_culler->prepare_regular_light(*ins); + + // Directional lights aren't handled here, _light_instance_update_shadow is called from elsewhere. + // Checking for this in case this changes, as this is assumed. + DEV_CHECK_ONCE(RSG::light_storage->light_get_type(ins->base) != RS::LIGHT_DIRECTIONAL); + + // Tighter caster culling to the camera frustum should work correctly with multiple viewports + cameras. + // The first camera will cull tightly, but if the light is present on more than 1 camera, the second will + // do a full render, and mark the light as non-dirty. + // There is however a cost to tighter shadow culling in this situation (2 shadow updates in 1 frame), + // so we should detect this and switch off tighter caster culling automatically. + // This is done in the logic for `decrement_shadow_dirty()`. + if (allow_redraw) { + light->last_version++; + light->decrement_shadow_dirty(); + } } bool redraw = RSG::light_storage->shadow_atlas_update_light(p_shadow_atlas, light->instance, coverage, light->last_version); @@ -3273,10 +3318,14 @@ void RendererSceneCull::_render_scene(const RendererSceneRender::CameraData *p_c if (redraw && max_shadows_used < MAX_UPDATE_SHADOWS) { //must redraw! RENDER_TIMESTAMP("> Render Light3D " + itos(i)); - light->shadow_dirty = _light_instance_update_shadow(ins, p_camera_data->main_transform, p_camera_data->main_projection, p_camera_data->is_orthogonal, p_camera_data->vaspect, p_shadow_atlas, scenario, p_screen_mesh_lod_threshold, p_visible_layers); + if (_light_instance_update_shadow(ins, p_camera_data->main_transform, p_camera_data->main_projection, p_camera_data->is_orthogonal, p_camera_data->vaspect, p_shadow_atlas, scenario, p_screen_mesh_lod_threshold, p_visible_layers)) { + light->make_shadow_dirty(); + } RENDER_TIMESTAMP("< Render Light3D " + itos(i)); } else { - light->shadow_dirty = redraw; + if (redraw) { + light->make_shadow_dirty(); + } } } } @@ -3953,7 +4002,7 @@ void RendererSceneCull::_update_dirty_instance(Instance *p_instance) { //ability to cast shadows change, let lights now for (const Instance *E : geom->lights) { InstanceLightData *light = static_cast<InstanceLightData *>(E->base_data); - light->shadow_dirty = true; + light->make_shadow_dirty(); } geom->can_cast_shadows = can_cast_shadows; @@ -4165,6 +4214,12 @@ RendererSceneCull::RendererSceneCull() { thread_cull_threshold = MAX(thread_cull_threshold, (uint32_t)WorkerThreadPool::get_singleton()->get_thread_count()); //make sure there is at least one thread per CPU dummy_occlusion_culling = memnew(RendererSceneOcclusionCull); + + light_culler = memnew(RenderingLightCuller); + + bool tighter_caster_culling = GLOBAL_DEF("rendering/lights_and_shadows/tighter_shadow_caster_culling", true); + light_culler->set_caster_culling_active(tighter_caster_culling); + light_culler->set_light_culling_active(tighter_caster_culling); } RendererSceneCull::~RendererSceneCull() { @@ -4187,4 +4242,9 @@ RendererSceneCull::~RendererSceneCull() { if (dummy_occlusion_culling) { memdelete(dummy_occlusion_culling); } + + if (light_culler) { + memdelete(light_culler); + light_culler = nullptr; + } } diff --git a/servers/rendering/renderer_scene_cull.h b/servers/rendering/renderer_scene_cull.h index e3e20b8502..a09823b008 100644 --- a/servers/rendering/renderer_scene_cull.h +++ b/servers/rendering/renderer_scene_cull.h @@ -46,6 +46,8 @@ #include "servers/rendering/storage/utilities.h" #include "servers/xr/xr_interface.h" +class RenderingLightCuller; + class RendererSceneCull : public RenderingMethod { public: RendererSceneRender *scene_render = nullptr; @@ -679,7 +681,6 @@ public: uint64_t last_version; List<Instance *>::Element *D; // directional light in scenario - bool shadow_dirty; bool uses_projector = false; bool uses_softshadow = false; @@ -690,12 +691,59 @@ public: RS::LightBakeMode bake_mode; uint32_t max_sdfgi_cascade = 2; + private: + // Instead of a single dirty flag, we maintain a count + // so that we can detect lights that are being made dirty + // each frame, and switch on tighter caster culling. + int32_t shadow_dirty_count; + + uint32_t light_update_frame_id; + bool light_intersects_multiple_cameras; + uint32_t light_intersects_multiple_cameras_timeout_frame_id; + + public: + bool is_shadow_dirty() const { return shadow_dirty_count != 0; } + void make_shadow_dirty() { shadow_dirty_count = light_intersects_multiple_cameras ? 1 : 2; } + void detect_light_intersects_multiple_cameras(uint32_t p_frame_id) { + // We need to detect the case where shadow updates are occurring + // more than once per frame. In this case, we need to turn off + // tighter caster culling, so situation reverts to one full shadow update + // per frame (light_intersects_multiple_cameras is set). + if (p_frame_id == light_update_frame_id) { + light_intersects_multiple_cameras = true; + light_intersects_multiple_cameras_timeout_frame_id = p_frame_id + 60; + } else { + // When shadow_volume_intersects_multiple_cameras is set, we + // want to detect the situation this is no longer the case, via a timeout. + // The system can go back to tighter caster culling in this situation. + // Having a long-ish timeout prevents rapid cycling. + if (light_intersects_multiple_cameras && (p_frame_id >= light_intersects_multiple_cameras_timeout_frame_id)) { + light_intersects_multiple_cameras = false; + light_intersects_multiple_cameras_timeout_frame_id = UINT32_MAX; + } + } + light_update_frame_id = p_frame_id; + } + + void decrement_shadow_dirty() { + shadow_dirty_count--; + DEV_ASSERT(shadow_dirty_count >= 0); + } + + // Shadow updates can either full (everything in the shadow volume) + // or closely culled to the camera frustum. + bool is_shadow_update_full() const { return shadow_dirty_count == 0; } + InstanceLightData() { bake_mode = RS::LIGHT_BAKE_DISABLED; - shadow_dirty = true; D = nullptr; last_version = 0; baked_light = nullptr; + + shadow_dirty_count = 1; + light_update_frame_id = UINT32_MAX; + light_intersects_multiple_cameras_timeout_frame_id = UINT32_MAX; + light_intersects_multiple_cameras = false; } }; @@ -955,6 +1003,7 @@ public: uint32_t geometry_instance_pair_mask = 0; // used in traditional forward, unnecessary on clustered LocalVector<Vector2> camera_jitter_array; + RenderingLightCuller *light_culler = nullptr; virtual RID instance_allocate(); virtual void instance_initialize(RID p_rid); diff --git a/servers/rendering/rendering_light_culler.cpp b/servers/rendering/rendering_light_culler.cpp new file mode 100644 index 0000000000..0d704c85de --- /dev/null +++ b/servers/rendering/rendering_light_culler.cpp @@ -0,0 +1,1125 @@ +/**************************************************************************/ +/* rendering_light_culler.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "rendering_light_culler.h" + +#include "core/math/plane.h" +#include "core/math/projection.h" +#include "rendering_server_globals.h" +#include "scene/3d/camera_3d.h" + +#ifdef RENDERING_LIGHT_CULLER_DEBUG_STRINGS +const char *RenderingLightCuller::Data::string_planes[] = { + "NEAR", + "FAR", + "LEFT", + "TOP", + "RIGHT", + "BOTTOM", +}; +const char *RenderingLightCuller::Data::string_points[] = { + "FAR_LEFT_TOP", + "FAR_LEFT_BOTTOM", + "FAR_RIGHT_TOP", + "FAR_RIGHT_BOTTOM", + "NEAR_LEFT_TOP", + "NEAR_LEFT_BOTTOM", + "NEAR_RIGHT_TOP", + "NEAR_RIGHT_BOTTOM", +}; + +String RenderingLightCuller::Data::plane_bitfield_to_string(unsigned int BF) { + String sz; + + for (int n = 0; n < 6; n++) { + unsigned int bit = 1 << n; + if (BF & bit) { + sz += String(string_planes[n]) + ", "; + } + } + + return sz; +} +#endif + +void RenderingLightCuller::prepare_directional_light(const RendererSceneCull::Instance *p_instance, int32_t p_directional_light_id) { + //data.directional_light = p_instance; + // Something is probably going wrong, we shouldn't have this many directional lights... + ERR_FAIL_COND(p_directional_light_id > 512); + DEV_ASSERT(p_directional_light_id >= 0); + + // First make sure we have enough directional lights to hold this one. + if (p_directional_light_id >= (int32_t)data.directional_cull_planes.size()) { + data.directional_cull_planes.resize(p_directional_light_id + 1); + } + + _prepare_light(*p_instance, p_directional_light_id); +} + +bool RenderingLightCuller::_prepare_light(const RendererSceneCull::Instance &p_instance, int32_t p_directional_light_id) { + if (!data.is_active()) { + return true; + } + + LightSource lsource; + switch (RSG::light_storage->light_get_type(p_instance.base)) { + case RS::LIGHT_SPOT: + lsource.type = LightSource::ST_SPOTLIGHT; + lsource.angle = RSG::light_storage->light_get_param(p_instance.base, RS::LIGHT_PARAM_SPOT_ANGLE); + lsource.range = RSG::light_storage->light_get_param(p_instance.base, RS::LIGHT_PARAM_RANGE); + break; + case RS::LIGHT_OMNI: + lsource.type = LightSource::ST_OMNI; + lsource.range = RSG::light_storage->light_get_param(p_instance.base, RS::LIGHT_PARAM_RANGE); + break; + case RS::LIGHT_DIRECTIONAL: + lsource.type = LightSource::ST_DIRECTIONAL; + // Could deal with a max directional shadow range here? NYI + // LIGHT_PARAM_SHADOW_MAX_DISTANCE + break; + } + + lsource.pos = p_instance.transform.origin; + lsource.dir = -p_instance.transform.basis.get_column(2); + lsource.dir.normalize(); + + bool visible; + if (p_directional_light_id == -1) { + visible = _add_light_camera_planes(data.regular_cull_planes, lsource); + } else { + visible = _add_light_camera_planes(data.directional_cull_planes[p_directional_light_id], lsource); + } + + if (data.light_culling_active) { + return visible; + } + return true; +} + +bool RenderingLightCuller::cull_directional_light(const RendererSceneCull::InstanceBounds &p_bound, int32_t p_directional_light_id) { + if (!data.is_active() || !is_caster_culling_active()) { + return true; + } + + ERR_FAIL_INDEX_V(p_directional_light_id, (int32_t)data.directional_cull_planes.size(), true); + + LightCullPlanes &cull_planes = data.directional_cull_planes[p_directional_light_id]; + + Vector3 mins = Vector3(p_bound.bounds[0], p_bound.bounds[1], p_bound.bounds[2]); + Vector3 maxs = Vector3(p_bound.bounds[3], p_bound.bounds[4], p_bound.bounds[5]); + AABB bb(mins, maxs - mins); + + real_t r_min, r_max; + for (int p = 0; p < cull_planes.num_cull_planes; p++) { + bb.project_range_in_plane(cull_planes.cull_planes[p], r_min, r_max); + if (r_min > 0.0f) { +#ifdef LIGHT_CULLER_DEBUG_DIRECTIONAL_LIGHT + cull_planes.rejected_count++; +#endif + + return false; + } + } + + return true; +} + +void RenderingLightCuller::cull_regular_light(PagedArray<RendererSceneCull::Instance *> &r_instance_shadow_cull_result) { + if (!data.is_active() || !is_caster_culling_active()) { + return; + } + + // If the light is out of range, no need to check anything, just return 0 casters. + // Ideally an out of range light should not even be drawn AT ALL (no shadow map, no PCF etc). + if (data.out_of_range) { + return; + } + + // Shorter local alias. + PagedArray<RendererSceneCull::Instance *> &list = r_instance_shadow_cull_result; + +#ifdef LIGHT_CULLER_DEBUG_LOGGING + uint32_t count_before = r_instance_shadow_cull_result.size(); +#endif + + // Go through all the casters in the list (the list will hopefully shrink as we go). + for (int n = 0; n < (int)list.size(); n++) { + // World space aabb. + const AABB &bb = list[n]->transformed_aabb; + +#ifdef LIGHT_CULLER_DEBUG_LOGGING + if (is_logging()) { + print_line("bb : " + String(bb)); + } +#endif + + real_t r_min, r_max; + bool show = true; + + for (int p = 0; p < data.regular_cull_planes.num_cull_planes; p++) { + // As we only need r_min, could this be optimized? + bb.project_range_in_plane(data.regular_cull_planes.cull_planes[p], r_min, r_max); + +#ifdef LIGHT_CULLER_DEBUG_LOGGING + if (is_logging()) { + print_line("\tplane " + itos(p) + " : " + String(data.regular_cull_planes.cull_planes[p]) + " r_min " + String(Variant(r_min)) + " r_max " + String(Variant(r_max))); + } +#endif + + if (r_min > 0.0f) { + show = false; + break; + } + } + + // Remove. + if (!show) { + list.remove_at_unordered(n); + + // Repeat this element next iteration of the loop as it has been removed and replaced by the last. + n--; + +#ifdef LIGHT_CULLER_DEBUG_REGULAR_LIGHT + data.regular_rejected_count++; +#endif + } + } + +#ifdef LIGHT_CULLER_DEBUG_LOGGING + uint32_t removed = r_instance_shadow_cull_result.size() - count_before; + if (removed) { + if (((data.debug_count) % 60) == 0) { + print_line("[" + itos(data.debug_count) + "] linear cull before " + itos(count_before) + " after " + itos(r_instance_shadow_cull_result.size())); + } + } +#endif +} + +void RenderingLightCuller::LightCullPlanes::add_cull_plane(const Plane &p) { + ERR_FAIL_COND(num_cull_planes >= MAX_CULL_PLANES); + cull_planes[num_cull_planes++] = p; +} + +// Directional lights are different to points, as the origin is infinitely in the distance, so the plane third +// points are derived differently. +bool RenderingLightCuller::add_light_camera_planes_directional(LightCullPlanes &r_cull_planes, const LightSource &p_light_source) { + uint32_t lookup = 0; + r_cull_planes.num_cull_planes = 0; + + // Directional light, we will use dot against the light direction to determine back facing planes. + for (int n = 0; n < 6; n++) { + float dot = data.frustum_planes[n].normal.dot(p_light_source.dir); + if (dot > 0.0f) { + lookup |= 1 << n; + + // Add backfacing camera frustum planes. + r_cull_planes.add_cull_plane(data.frustum_planes[n]); + } + } + + ERR_FAIL_COND_V(lookup >= LUT_SIZE, true); + + // Deal with special case... if the light is INSIDE the view frustum (i.e. all planes face away) + // then we will add the camera frustum planes to clip the light volume .. there is no need to + // render shadow casters outside the frustum as shadows can never re-enter the frustum. + + // Should never happen with directional light?? This may be able to be removed. + if (lookup == 63) { + r_cull_planes.num_cull_planes = 0; + for (int n = 0; n < data.frustum_planes.size(); n++) { + r_cull_planes.add_cull_plane(data.frustum_planes[n]); + } + + return true; + } + +// Each edge forms a plane. +#ifdef RENDERING_LIGHT_CULLER_CALCULATE_LUT + const LocalVector<uint8_t> &entry = _calculated_LUT[lookup]; + + // each edge forms a plane + int n_edges = entry.size() - 1; +#else + uint8_t *entry = &data.LUT_entries[lookup][0]; + int n_edges = data.LUT_entry_sizes[lookup] - 1; +#endif + + for (int e = 0; e < n_edges; e++) { + int i0 = entry[e]; + int i1 = entry[e + 1]; + const Vector3 &pt0 = data.frustum_points[i0]; + const Vector3 &pt1 = data.frustum_points[i1]; + + // Create a third point from the light direction. + Vector3 pt2 = pt0 - p_light_source.dir; + + // Create plane from 3 points. + Plane p(pt0, pt1, pt2); + r_cull_planes.add_cull_plane(p); + } + + // Last to 0 edge. + if (n_edges) { + int i0 = entry[n_edges]; // Last. + int i1 = entry[0]; // First. + + const Vector3 &pt0 = data.frustum_points[i0]; + const Vector3 &pt1 = data.frustum_points[i1]; + + // Create a third point from the light direction. + Vector3 pt2 = pt0 - p_light_source.dir; + + // Create plane from 3 points. + Plane p(pt0, pt1, pt2); + r_cull_planes.add_cull_plane(p); + } + +#ifdef LIGHT_CULLER_DEBUG_LOGGING + if (is_logging()) { + print_line("lcam.pos is " + String(p_light_source.pos)); + } +#endif + + return true; +} + +bool RenderingLightCuller::_add_light_camera_planes(LightCullPlanes &r_cull_planes, const LightSource &p_light_source) { + if (!data.is_active()) { + return true; + } + + // We should have called prepare_camera before this. + ERR_FAIL_COND_V(data.frustum_planes.size() != 6, true); + + switch (p_light_source.type) { + case LightSource::ST_SPOTLIGHT: + case LightSource::ST_OMNI: + break; + case LightSource::ST_DIRECTIONAL: + return add_light_camera_planes_directional(r_cull_planes, p_light_source); + break; + default: + return false; // not yet supported + break; + } + + // Start with 0 cull planes. + r_cull_planes.num_cull_planes = 0; + data.out_of_range = false; + uint32_t lookup = 0; + + // Find which of the camera planes are facing away from the light. + // We can also test for the situation where the light max range means it cannot + // affect the camera frustum. This is absolutely worth doing because it is relatively + // cheap, and if the entire light can be culled this can vastly improve performance + // (much more than just culling casters). + + // POINT LIGHT (spotlight, omni) + // Instead of using dot product to compare light direction to plane, we can simply + // find out which side of the plane the camera is on. By definition this marks the point at which the plane + // becomes invisible. + + // OMNIS + if (p_light_source.type == LightSource::ST_OMNI) { + for (int n = 0; n < 6; n++) { + float dist = data.frustum_planes[n].distance_to(p_light_source.pos); + if (dist < 0.0f) { + lookup |= 1 << n; + + // Add backfacing camera frustum planes. + r_cull_planes.add_cull_plane(data.frustum_planes[n]); + } else { + // Is the light out of range? + // This is one of the tests. If the point source is more than range distance from a frustum plane, it can't + // be seen. + if (dist >= p_light_source.range) { + // If the light is out of range, no need to do anything else, everything will be culled. + data.out_of_range = true; + return false; + } + } + } + } else { + // SPOTLIGHTs, more complex to cull. + Vector3 pos_end = p_light_source.pos + (p_light_source.dir * p_light_source.range); + + // This is the radius of the cone at distance 1. + float radius_at_dist_one = Math::tan(Math::deg_to_rad(p_light_source.angle)); + + // The worst case radius of the cone at the end point can be calculated + // (the radius will scale linearly with length along the cone). + float end_cone_radius = radius_at_dist_one * p_light_source.range; + + for (int n = 0; n < 6; n++) { + float dist = data.frustum_planes[n].distance_to(p_light_source.pos); + if (dist < 0.0f) { + // Either the plane is backfacing or we are inside the frustum. + lookup |= 1 << n; + + // Add backfacing camera frustum planes. + r_cull_planes.add_cull_plane(data.frustum_planes[n]); + } else { + // The light is in front of the plane. + + // Is the light out of range? + if (dist >= p_light_source.range) { + data.out_of_range = true; + return false; + } + + // For a spotlight, we can use an extra test + // at this point the cone start is in front of the plane... + // If the cone end point is further than the maximum possible distance to the plane + // we can guarantee that the cone does not cross the plane, and hence the cone + // is outside the frustum. + float dist_end = data.frustum_planes[n].distance_to(pos_end); + + if (dist_end >= end_cone_radius) { + data.out_of_range = true; + return false; + } + } + } + } + + // The lookup should be within the LUT, logic should prevent this. + ERR_FAIL_COND_V(lookup >= LUT_SIZE, true); + + // Deal with special case... if the light is INSIDE the view frustum (i.e. all planes face away) + // then we will add the camera frustum planes to clip the light volume .. there is no need to + // render shadow casters outside the frustum as shadows can never re-enter the frustum. + if (lookup == 63) { + r_cull_planes.num_cull_planes = 0; + for (int n = 0; n < data.frustum_planes.size(); n++) { + r_cull_planes.add_cull_plane(data.frustum_planes[n]); + } + + return true; + } + + // Each edge forms a plane. + uint8_t *entry = &data.LUT_entries[lookup][0]; + int n_edges = data.LUT_entry_sizes[lookup] - 1; + + for (int e = 0; e < n_edges; e++) { + int i0 = entry[e]; + int i1 = entry[e + 1]; + const Vector3 &pt0 = data.frustum_points[i0]; + const Vector3 &pt1 = data.frustum_points[i1]; + + // Create plane from 3 points. + Plane p(pt0, pt1, p_light_source.pos); + r_cull_planes.add_cull_plane(p); + } + + // Last to 0 edge. + if (n_edges) { + int i0 = entry[n_edges]; // Last. + int i1 = entry[0]; // First. + + const Vector3 &pt0 = data.frustum_points[i0]; + const Vector3 &pt1 = data.frustum_points[i1]; + + // Create plane from 3 points. + Plane p(pt0, pt1, p_light_source.pos); + r_cull_planes.add_cull_plane(p); + } + +#ifdef LIGHT_CULLER_DEBUG_LOGGING + if (is_logging()) { + print_line("lsource.pos is " + String(p_light_source.pos)); + } +#endif + + return true; +} + +bool RenderingLightCuller::prepare_camera(const Transform3D &p_cam_transform, const Projection &p_cam_matrix) { + data.debug_count++; + if (data.debug_count >= 120) { + data.debug_count = 0; + } + + // For debug flash off and on. +#ifdef LIGHT_CULLER_DEBUG_FLASH + if (!Engine::get_singleton()->is_editor_hint()) { + int dc = Engine::get_singleton()->get_process_frames() / LIGHT_CULLER_DEBUG_FLASH_FREQUENCY; + bool bnew_active; + bnew_active = (dc % 2) == 0; + + if (bnew_active != data.light_culling_active) { + data.light_culling_active = bnew_active; + print_line("switching light culler " + String(Variant(data.light_culling_active))); + } + } +#endif + + if (!data.is_active()) { + return false; + } + + // Get the camera frustum planes in world space. + data.frustum_planes = p_cam_matrix.get_projection_planes(p_cam_transform); + DEV_CHECK_ONCE(data.frustum_planes.size() == 6); + + data.regular_cull_planes.num_cull_planes = 0; + +#ifdef LIGHT_CULLER_DEBUG_DIRECTIONAL_LIGHT + if (is_logging()) { + for (uint32_t n = 0; n < data.directional_cull_planes.size(); n++) { + print_line("LightCuller directional light " + itos(n) + " rejected " + itos(data.directional_cull_planes[n].rejected_count) + " instances."); + } + } +#endif +#ifdef LIGHT_CULLER_DEBUG_REGULAR_LIGHT + if (data.regular_rejected_count) { + print_line("LightCuller regular lights rejected " + itos(data.regular_rejected_count) + " instances."); + } + data.regular_rejected_count = 0; +#endif + + data.directional_cull_planes.resize(0); + +#ifdef LIGHT_CULLER_DEBUG_LOGGING + if (is_logging()) { + for (int p = 0; p < 6; p++) { + print_line("plane " + itos(p) + " : " + String(data.frustum_planes[p])); + } + } +#endif + + // We want to calculate the frustum corners in a specific order. + const Projection::Planes intersections[8][3] = { + { Projection::PLANE_FAR, Projection::PLANE_LEFT, Projection::PLANE_TOP }, + { Projection::PLANE_FAR, Projection::PLANE_LEFT, Projection::PLANE_BOTTOM }, + { Projection::PLANE_FAR, Projection::PLANE_RIGHT, Projection::PLANE_TOP }, + { Projection::PLANE_FAR, Projection::PLANE_RIGHT, Projection::PLANE_BOTTOM }, + { Projection::PLANE_NEAR, Projection::PLANE_LEFT, Projection::PLANE_TOP }, + { Projection::PLANE_NEAR, Projection::PLANE_LEFT, Projection::PLANE_BOTTOM }, + { Projection::PLANE_NEAR, Projection::PLANE_RIGHT, Projection::PLANE_TOP }, + { Projection::PLANE_NEAR, Projection::PLANE_RIGHT, Projection::PLANE_BOTTOM }, + }; + + for (int i = 0; i < 8; i++) { + // 3 plane intersection, gives us a point. + bool res = data.frustum_planes[intersections[i][0]].intersect_3(data.frustum_planes[intersections[i][1]], data.frustum_planes[intersections[i][2]], &data.frustum_points[i]); + + // What happens with a zero frustum? NYI - deal with this. + ERR_FAIL_COND_V(!res, false); + +#ifdef LIGHT_CULLER_DEBUG_LOGGING + if (is_logging()) { + print_line("point " + itos(i) + " -> " + String(data.frustum_points[i])); + } +#endif + } + + return true; +} + +RenderingLightCuller::RenderingLightCuller() { + // Used to determine which frame to give debug output. + data.debug_count = -1; + + // Uncomment below to switch off light culler in the editor. + // data.caster_culling_active = Engine::get_singleton()->is_editor_hint() == false; + +#ifdef RENDERING_LIGHT_CULLER_CALCULATE_LUT + create_LUT(); +#endif +} + +/* clang-format off */ +uint8_t RenderingLightCuller::Data::LUT_entry_sizes[LUT_SIZE] = {0, 4, 4, 0, 4, 6, 6, 8, 4, 6, 6, 8, 6, 6, 6, 6, 4, 6, 6, 8, 0, 8, 8, 0, 6, 6, 6, 6, 8, 6, 6, 4, 4, 6, 6, 8, 6, 6, 6, 6, 0, 8, 8, 0, 8, 6, 6, 4, 6, 6, 6, 6, 8, 6, 6, 4, 8, 6, 6, 4, 0, 4, 4, 0, }; + +// The lookup table used to determine which edges form the silhouette of the camera frustum, +// depending on the viewing angle (defined by which camera planes are backward facing). +uint8_t RenderingLightCuller::Data::LUT_entries[LUT_SIZE][8] = { +{0, 0, 0, 0, 0, 0, 0, 0, }, +{7, 6, 4, 5, 0, 0, 0, 0, }, +{1, 0, 2, 3, 0, 0, 0, 0, }, +{0, 0, 0, 0, 0, 0, 0, 0, }, +{1, 5, 4, 0, 0, 0, 0, 0, }, +{1, 5, 7, 6, 4, 0, 0, 0, }, +{4, 0, 2, 3, 1, 5, 0, 0, }, +{5, 7, 6, 4, 0, 2, 3, 1, }, +{0, 4, 6, 2, 0, 0, 0, 0, }, +{0, 4, 5, 7, 6, 2, 0, 0, }, +{6, 2, 3, 1, 0, 4, 0, 0, }, +{2, 3, 1, 0, 4, 5, 7, 6, }, +{0, 1, 5, 4, 6, 2, 0, 0, }, +{0, 1, 5, 7, 6, 2, 0, 0, }, +{6, 2, 3, 1, 5, 4, 0, 0, }, +{2, 3, 1, 5, 7, 6, 0, 0, }, +{2, 6, 7, 3, 0, 0, 0, 0, }, +{2, 6, 4, 5, 7, 3, 0, 0, }, +{7, 3, 1, 0, 2, 6, 0, 0, }, +{3, 1, 0, 2, 6, 4, 5, 7, }, +{0, 0, 0, 0, 0, 0, 0, 0, }, +{2, 6, 4, 0, 1, 5, 7, 3, }, +{7, 3, 1, 5, 4, 0, 2, 6, }, +{0, 0, 0, 0, 0, 0, 0, 0, }, +{2, 0, 4, 6, 7, 3, 0, 0, }, +{2, 0, 4, 5, 7, 3, 0, 0, }, +{7, 3, 1, 0, 4, 6, 0, 0, }, +{3, 1, 0, 4, 5, 7, 0, 0, }, +{2, 0, 1, 5, 4, 6, 7, 3, }, +{2, 0, 1, 5, 7, 3, 0, 0, }, +{7, 3, 1, 5, 4, 6, 0, 0, }, +{3, 1, 5, 7, 0, 0, 0, 0, }, +{3, 7, 5, 1, 0, 0, 0, 0, }, +{3, 7, 6, 4, 5, 1, 0, 0, }, +{5, 1, 0, 2, 3, 7, 0, 0, }, +{7, 6, 4, 5, 1, 0, 2, 3, }, +{3, 7, 5, 4, 0, 1, 0, 0, }, +{3, 7, 6, 4, 0, 1, 0, 0, }, +{5, 4, 0, 2, 3, 7, 0, 0, }, +{7, 6, 4, 0, 2, 3, 0, 0, }, +{0, 0, 0, 0, 0, 0, 0, 0, }, +{3, 7, 6, 2, 0, 4, 5, 1, }, +{5, 1, 0, 4, 6, 2, 3, 7, }, +{0, 0, 0, 0, 0, 0, 0, 0, }, +{3, 7, 5, 4, 6, 2, 0, 1, }, +{3, 7, 6, 2, 0, 1, 0, 0, }, +{5, 4, 6, 2, 3, 7, 0, 0, }, +{7, 6, 2, 3, 0, 0, 0, 0, }, +{3, 2, 6, 7, 5, 1, 0, 0, }, +{3, 2, 6, 4, 5, 1, 0, 0, }, +{5, 1, 0, 2, 6, 7, 0, 0, }, +{1, 0, 2, 6, 4, 5, 0, 0, }, +{3, 2, 6, 7, 5, 4, 0, 1, }, +{3, 2, 6, 4, 0, 1, 0, 0, }, +{5, 4, 0, 2, 6, 7, 0, 0, }, +{6, 4, 0, 2, 0, 0, 0, 0, }, +{3, 2, 0, 4, 6, 7, 5, 1, }, +{3, 2, 0, 4, 5, 1, 0, 0, }, +{5, 1, 0, 4, 6, 7, 0, 0, }, +{1, 0, 4, 5, 0, 0, 0, 0, }, +{0, 0, 0, 0, 0, 0, 0, 0, }, +{3, 2, 0, 1, 0, 0, 0, 0, }, +{5, 4, 6, 7, 0, 0, 0, 0, }, +{0, 0, 0, 0, 0, 0, 0, 0, }, +}; + +/* clang-format on */ + +#ifdef RENDERING_LIGHT_CULLER_CALCULATE_LUT + +// See e.g. http://lspiroengine.com/?p=153 for reference. +// Principles are the same, but differences to the article: +// * Order of planes / points is different in Godot. +// * We use a lookup table at runtime. +void RenderingLightCuller::create_LUT() { + // Each pair of planes that are opposite can have an edge. + for (int plane_0 = 0; plane_0 < PLANE_TOTAL; plane_0++) { + // For each neighbour of the plane. + PlaneOrder neighs[4]; + get_neighbouring_planes((PlaneOrder)plane_0, neighs); + + for (int n = 0; n < 4; n++) { + int plane_1 = neighs[n]; + + // If these are opposite we need to add the 2 points they share. + PointOrder pts[2]; + get_corners_of_planes((PlaneOrder)plane_0, (PlaneOrder)plane_1, pts); + + add_LUT(plane_0, plane_1, pts); + } + } + + for (uint32_t n = 0; n < LUT_SIZE; n++) { + compact_LUT_entry(n); + } + + debug_print_LUT(); + debug_print_LUT_as_table(); +} + +// we can pre-create the entire LUT and store it hard coded as a static inside the executable! +// it is only small in size, 64 entries with max 8 bytes per entry +void RenderingLightCuller::debug_print_LUT_as_table() { + print_line("\nLIGHT VOLUME TABLE BEGIN\n"); + + print_line("Copy this to LUT_entry_sizes:\n"); + String sz = "{"; + for (int n = 0; n < LUT_SIZE; n++) { + const LocalVector<uint8_t> &entry = _calculated_LUT[n]; + + sz += itos(entry.size()) + ", "; + } + sz += "}"; + print_line(sz); + print_line("\nCopy this to LUT_entries:\n"); + + for (int n = 0; n < LUT_SIZE; n++) { + const LocalVector<uint8_t> &entry = _calculated_LUT[n]; + + String sz = "{"; + + // First is the number of points in the entry. + int s = entry.size(); + + for (int p = 0; p < 8; p++) { + if (p < s) + sz += itos(entry[p]); + else + sz += "0"; // just a spacer + + sz += ", "; + } + + sz += "},"; + print_line(sz); + } + + print_line("\nLIGHT VOLUME TABLE END\n"); +} + +void RenderingLightCuller::debug_print_LUT() { + for (int n = 0; n < LUT_SIZE; n++) { + String sz; + sz = "LUT" + itos(n) + ":\t"; + + sz += Data::plane_bitfield_to_string(n); + print_line(sz); + + const LocalVector<uint8_t> &entry = _calculated_LUT[n]; + + sz = "\t" + string_LUT_entry(entry); + + print_line(sz); + } +} + +String RenderingLightCuller::string_LUT_entry(const LocalVector<uint8_t> &p_entry) { + String string; + + for (uint32_t n = 0; n < p_entry.size(); n++) { + uint8_t val = p_entry[n]; + DEV_ASSERT(val < 8); + const char *sz_point = Data::string_points[val]; + string += sz_point; + string += ", "; + } + + return string; +} + +String RenderingLightCuller::debug_string_LUT_entry(const LocalVector<uint8_t> &p_entry, bool p_pair) { + String string; + + for (uint32_t i = 0; i < p_entry.size(); i++) { + int pt_order = p_entry[i]; + if (p_pair && ((i % 2) == 0)) { + string += itos(pt_order) + "-"; + } else { + string += itos(pt_order) + ", "; + } + } + + return string; +} + +void RenderingLightCuller::add_LUT(int p_plane_0, int p_plane_1, PointOrder p_pts[2]) { + // Note that some entries to the LUT will be "impossible" situations, + // because it contains all combinations of plane flips. + uint32_t bit0 = 1 << p_plane_0; + uint32_t bit1 = 1 << p_plane_1; + + // All entries of the LUT that have plane 0 set and plane 1 not set. + for (uint32_t n = 0; n < 64; n++) { + // If bit0 not set... + if (!(n & bit0)) + continue; + + // If bit1 set... + if (n & bit1) + continue; + + // Meets criteria. + add_LUT_entry(n, p_pts); + } +} + +void RenderingLightCuller::add_LUT_entry(uint32_t p_entry_id, PointOrder p_pts[2]) { + DEV_ASSERT(p_entry_id < LUT_SIZE); + LocalVector<uint8_t> &entry = _calculated_LUT[p_entry_id]; + + entry.push_back(p_pts[0]); + entry.push_back(p_pts[1]); +} + +void RenderingLightCuller::compact_LUT_entry(uint32_t p_entry_id) { + DEV_ASSERT(p_entry_id < LUT_SIZE); + LocalVector<uint8_t> &entry = _calculated_LUT[p_entry_id]; + + int num_pairs = entry.size() / 2; + + if (num_pairs == 0) + return; + + LocalVector<uint8_t> temp; + + String string; + string = "Compact LUT" + itos(p_entry_id) + ":\t"; + string += debug_string_LUT_entry(entry, true); + print_line(string); + + // Add first pair. + temp.push_back(entry[0]); + temp.push_back(entry[1]); + unsigned int BFpairs = 1; + + string = debug_string_LUT_entry(temp) + " -> "; + print_line(string); + + // Attempt to add a pair each time. + for (int done = 1; done < num_pairs; done++) { + string = "done " + itos(done) + ": "; + // Find a free pair. + for (int p = 1; p < num_pairs; p++) { + unsigned int bit = 1 << p; + // Is it done already? + if (BFpairs & bit) + continue; + + // There must be at least 1 free pair. + // Attempt to add. + int a = entry[p * 2]; + int b = entry[(p * 2) + 1]; + + string += "[" + itos(a) + "-" + itos(b) + "], "; + + int found_a = temp.find(a); + int found_b = temp.find(b); + + // Special case, if they are both already in the list, no need to add + // as this is a link from the tail to the head of the list. + if ((found_a != -1) && (found_b != -1)) { + string += "foundAB link " + itos(found_a) + ", " + itos(found_b) + " "; + BFpairs |= bit; + goto found; + } + + // Find a. + if (found_a != -1) { + string += "foundA " + itos(found_a) + " "; + temp.insert(found_a + 1, b); + BFpairs |= bit; + goto found; + } + + // Find b. + if (found_b != -1) { + string += "foundB " + itos(found_b) + " "; + temp.insert(found_b, a); + BFpairs |= bit; + goto found; + } + + } // Check each pair for adding. + + // If we got here before finding a link, the whole set of planes is INVALID + // e.g. far and near plane only, does not create continuous sillouhette of edges. + print_line("\tINVALID"); + entry.clear(); + return; + + found:; + print_line(string); + string = "\ttemp now : " + debug_string_LUT_entry(temp); + print_line(string); + } + + // temp should now be the sorted entry .. delete the old one and replace by temp. + entry.clear(); + entry = temp; +} + +void RenderingLightCuller::get_neighbouring_planes(PlaneOrder p_plane, PlaneOrder r_neigh_planes[4]) const { + // Table of neighbouring planes to each. + static const PlaneOrder neigh_table[PLANE_TOTAL][4] = { + { // LSM_FP_NEAR + PLANE_LEFT, + PLANE_RIGHT, + PLANE_TOP, + PLANE_BOTTOM }, + { // LSM_FP_FAR + PLANE_LEFT, + PLANE_RIGHT, + PLANE_TOP, + PLANE_BOTTOM }, + { // LSM_FP_LEFT + PLANE_TOP, + PLANE_BOTTOM, + PLANE_NEAR, + PLANE_FAR }, + { // LSM_FP_TOP + PLANE_LEFT, + PLANE_RIGHT, + PLANE_NEAR, + PLANE_FAR }, + { // LSM_FP_RIGHT + PLANE_TOP, + PLANE_BOTTOM, + PLANE_NEAR, + PLANE_FAR }, + { // LSM_FP_BOTTOM + PLANE_LEFT, + PLANE_RIGHT, + PLANE_NEAR, + PLANE_FAR }, + }; + + for (int n = 0; n < 4; n++) { + r_neigh_planes[n] = neigh_table[p_plane][n]; + } +} + +// Given two planes, returns the two points shared by those planes. The points are always +// returned in counter-clockwise order, assuming the first input plane is facing towards +// the viewer. + +// param p_plane_a The plane facing towards the viewer. +// param p_plane_b A plane neighboring p_plane_a. +// param r_points An array of exactly two elements to be filled with the indices of the points +// on return. + +void RenderingLightCuller::get_corners_of_planes(PlaneOrder p_plane_a, PlaneOrder p_plane_b, PointOrder r_points[2]) const { + static const PointOrder fp_table[PLANE_TOTAL][PLANE_TOTAL][2] = { + { + // LSM_FP_NEAR + { + // LSM_FP_NEAR + PT_NEAR_LEFT_TOP, PT_NEAR_RIGHT_TOP, // Invalid combination. + }, + { + // LSM_FP_FAR + PT_FAR_RIGHT_TOP, PT_FAR_LEFT_TOP, // Invalid combination. + }, + { + // LSM_FP_LEFT + PT_NEAR_LEFT_TOP, + PT_NEAR_LEFT_BOTTOM, + }, + { + // LSM_FP_TOP + PT_NEAR_RIGHT_TOP, + PT_NEAR_LEFT_TOP, + }, + { + // LSM_FP_RIGHT + PT_NEAR_RIGHT_BOTTOM, + PT_NEAR_RIGHT_TOP, + }, + { + // LSM_FP_BOTTOM + PT_NEAR_LEFT_BOTTOM, + PT_NEAR_RIGHT_BOTTOM, + }, + }, + + { + // LSM_FP_FAR + { + // LSM_FP_NEAR + PT_FAR_LEFT_TOP, PT_FAR_RIGHT_TOP, // Invalid combination. + }, + { + // LSM_FP_FAR + PT_FAR_RIGHT_TOP, PT_FAR_LEFT_TOP, // Invalid combination. + }, + { + // LSM_FP_LEFT + PT_FAR_LEFT_BOTTOM, + PT_FAR_LEFT_TOP, + }, + { + // LSM_FP_TOP + PT_FAR_LEFT_TOP, + PT_FAR_RIGHT_TOP, + }, + { + // LSM_FP_RIGHT + PT_FAR_RIGHT_TOP, + PT_FAR_RIGHT_BOTTOM, + }, + { + // LSM_FP_BOTTOM + PT_FAR_RIGHT_BOTTOM, + PT_FAR_LEFT_BOTTOM, + }, + }, + + { + // LSM_FP_LEFT + { + // LSM_FP_NEAR + PT_NEAR_LEFT_BOTTOM, + PT_NEAR_LEFT_TOP, + }, + { + // LSM_FP_FAR + PT_FAR_LEFT_TOP, + PT_FAR_LEFT_BOTTOM, + }, + { + // LSM_FP_LEFT + PT_FAR_LEFT_BOTTOM, PT_FAR_LEFT_BOTTOM, // Invalid combination. + }, + { + // LSM_FP_TOP + PT_NEAR_LEFT_TOP, + PT_FAR_LEFT_TOP, + }, + { + // LSM_FP_RIGHT + PT_FAR_LEFT_BOTTOM, PT_FAR_LEFT_BOTTOM, // Invalid combination. + }, + { + // LSM_FP_BOTTOM + PT_FAR_LEFT_BOTTOM, + PT_NEAR_LEFT_BOTTOM, + }, + }, + + { + // LSM_FP_TOP + { + // LSM_FP_NEAR + PT_NEAR_LEFT_TOP, + PT_NEAR_RIGHT_TOP, + }, + { + // LSM_FP_FAR + PT_FAR_RIGHT_TOP, + PT_FAR_LEFT_TOP, + }, + { + // LSM_FP_LEFT + PT_FAR_LEFT_TOP, + PT_NEAR_LEFT_TOP, + }, + { + // LSM_FP_TOP + PT_NEAR_LEFT_TOP, PT_FAR_LEFT_TOP, // Invalid combination. + }, + { + // LSM_FP_RIGHT + PT_NEAR_RIGHT_TOP, + PT_FAR_RIGHT_TOP, + }, + { + // LSM_FP_BOTTOM + PT_FAR_LEFT_BOTTOM, PT_NEAR_LEFT_BOTTOM, // Invalid combination. + }, + }, + + { + // LSM_FP_RIGHT + { + // LSM_FP_NEAR + PT_NEAR_RIGHT_TOP, + PT_NEAR_RIGHT_BOTTOM, + }, + { + // LSM_FP_FAR + PT_FAR_RIGHT_BOTTOM, + PT_FAR_RIGHT_TOP, + }, + { + // LSM_FP_LEFT + PT_FAR_RIGHT_BOTTOM, PT_FAR_RIGHT_BOTTOM, // Invalid combination. + }, + { + // LSM_FP_TOP + PT_FAR_RIGHT_TOP, + PT_NEAR_RIGHT_TOP, + }, + { + // LSM_FP_RIGHT + PT_FAR_RIGHT_BOTTOM, PT_FAR_RIGHT_BOTTOM, // Invalid combination. + }, + { + // LSM_FP_BOTTOM + PT_NEAR_RIGHT_BOTTOM, + PT_FAR_RIGHT_BOTTOM, + }, + }, + + // == + + // P_NEAR, + // P_FAR, + // P_LEFT, + // P_TOP, + // P_RIGHT, + // P_BOTTOM, + + { + // LSM_FP_BOTTOM + { + // LSM_FP_NEAR + PT_NEAR_RIGHT_BOTTOM, + PT_NEAR_LEFT_BOTTOM, + }, + { + // LSM_FP_FAR + PT_FAR_LEFT_BOTTOM, + PT_FAR_RIGHT_BOTTOM, + }, + { + // LSM_FP_LEFT + PT_NEAR_LEFT_BOTTOM, + PT_FAR_LEFT_BOTTOM, + }, + { + // LSM_FP_TOP + PT_NEAR_LEFT_BOTTOM, PT_FAR_LEFT_BOTTOM, // Invalid combination. + }, + { + // LSM_FP_RIGHT + PT_FAR_RIGHT_BOTTOM, + PT_NEAR_RIGHT_BOTTOM, + }, + { + // LSM_FP_BOTTOM + PT_FAR_LEFT_BOTTOM, PT_NEAR_LEFT_BOTTOM, // Invalid combination. + }, + }, + + // == + + }; + r_points[0] = fp_table[p_plane_a][p_plane_b][0]; + r_points[1] = fp_table[p_plane_a][p_plane_b][1]; +} + +#endif diff --git a/servers/rendering/rendering_light_culler.h b/servers/rendering/rendering_light_culler.h new file mode 100644 index 0000000000..602543850a --- /dev/null +++ b/servers/rendering/rendering_light_culler.h @@ -0,0 +1,248 @@ +/**************************************************************************/ +/* rendering_light_culler.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef RENDERING_LIGHT_CULLER_H +#define RENDERING_LIGHT_CULLER_H + +#include "core/math/plane.h" +#include "core/math/vector3.h" +#include "renderer_scene_cull.h" + +struct Projection; +struct Transform3D; + +// For testing performance improvements from the LightCuller: +// Uncomment LIGHT_CULLER_DEBUG_FLASH and it will turn the culler +// on and off every LIGHT_CULLER_DEBUG_FLASH_FREQUENCY camera prepares. +// Uncomment LIGHT_CULLER_DEBUG_LOGGING to get periodic print of the number of casters culled before / after. +// Uncomment LIGHT_CULLER_DEBUG_DIRECTIONAL_LIGHT to get periodic print of the number of casters culled for the directional light.. + +// #define LIGHT_CULLER_DEBUG_LOGGING +// #define LIGHT_CULLER_DEBUG_DIRECTIONAL_LIGHT +// #define LIGHT_CULLER_DEBUG_REGULAR_LIGHT +// #define LIGHT_CULLER_DEBUG_FLASH +#define LIGHT_CULLER_DEBUG_FLASH_FREQUENCY 1024 +//////////////////////////////////////////////////////////////////////////////////////////////// + +// The code to generate the lookup table is included but commented out. +// This may be useful for debugging / regenerating the LUT in the future, +// especially if the order of planes changes. +// When this define is set, the generated lookup table will be printed to debug output. +// The generated lookup table can be copy pasted +// straight to LUT_entry_sizes and LUT_entries. +// See the referenced article for explanation. +// #define RENDERING_LIGHT_CULLER_CALCULATE_LUT + +//////////////////////////////////////////////////////////////////////////////////////////////// +// This define will be set automatically depending on earlier defines, you can leave this as is. +#if defined(LIGHT_CULLER_DEBUG_LOGGING) || defined(RENDERING_LIGHT_CULLER_CALCULATE_LUT) +#define RENDERING_LIGHT_CULLER_DEBUG_STRINGS +#endif + +// Culls shadow casters that can't cast shadows into the camera frustum. +class RenderingLightCuller { +public: + RenderingLightCuller(); + +private: + class LightSource { + public: + enum SourceType { + ST_UNKNOWN, + ST_DIRECTIONAL, + ST_SPOTLIGHT, + ST_OMNI, + }; + + LightSource() { + type = ST_UNKNOWN; + angle = 0.0f; + range = FLT_MAX; + } + + // All in world space, culling done in world space. + Vector3 pos; + Vector3 dir; + SourceType type; + + float angle; // For spotlight. + float range; + }; + + // Same order as godot. + enum PlaneOrder { + PLANE_NEAR, + PLANE_FAR, + PLANE_LEFT, + PLANE_TOP, + PLANE_RIGHT, + PLANE_BOTTOM, + PLANE_TOTAL, + }; + + // Same order as godot. + enum PointOrder { + PT_FAR_LEFT_TOP, + PT_FAR_LEFT_BOTTOM, + PT_FAR_RIGHT_TOP, + PT_FAR_RIGHT_BOTTOM, + PT_NEAR_LEFT_TOP, + PT_NEAR_LEFT_BOTTOM, + PT_NEAR_RIGHT_TOP, + PT_NEAR_RIGHT_BOTTOM, + }; + + // 6 bits, 6 planes. + enum { + NUM_CAM_PLANES = 6, + NUM_CAM_POINTS = 8, + MAX_CULL_PLANES = 17, + LUT_SIZE = 64, + }; + +public: + // Before each pass with a different camera, you must call this so the culler can pre-create + // the camera frustum planes and corner points in world space which are used for the culling. + bool prepare_camera(const Transform3D &p_cam_transform, const Projection &p_cam_matrix); + + // REGULAR LIGHTS (SPOT, OMNI). + // These are prepared then used for culling one by one, single threaded. + // prepare_regular_light() returns false if the entire light is culled (i.e. there is no intersection between the light and the view frustum). + bool prepare_regular_light(const RendererSceneCull::Instance &p_instance) { return _prepare_light(p_instance, -1); } + + // Cull according to the regular light planes that were setup in the previous call to prepare_regular_light. + void cull_regular_light(PagedArray<RendererSceneCull::Instance *> &r_instance_shadow_cull_result); + + // Directional lights are prepared in advance, and can be culled multithreaded chopping and changing between + // different directional_light_id. + void prepare_directional_light(const RendererSceneCull::Instance *p_instance, int32_t p_directional_light_id); + + // Return false if the instance is to be culled. + bool cull_directional_light(const RendererSceneCull::InstanceBounds &p_bound, int32_t p_directional_light_id); + + // Can turn on and off from the engine if desired. + void set_caster_culling_active(bool p_active) { data.caster_culling_active = p_active; } + void set_light_culling_active(bool p_active) { data.light_culling_active = p_active; } + +private: + struct LightCullPlanes { + void add_cull_plane(const Plane &p); + Plane cull_planes[MAX_CULL_PLANES]; + int num_cull_planes = 0; +#ifdef LIGHT_CULLER_DEBUG_DIRECTIONAL_LIGHT + uint32_t rejected_count = 0; +#endif + }; + + bool _prepare_light(const RendererSceneCull::Instance &p_instance, int32_t p_directional_light_id = -1); + + // Internal version uses LightSource. + bool _add_light_camera_planes(LightCullPlanes &r_cull_planes, const LightSource &p_light_source); + + // Directional light gives parallel culling planes (as opposed to point lights). + bool add_light_camera_planes_directional(LightCullPlanes &r_cull_planes, const LightSource &p_light_source); + + // Is the light culler active? maybe not in the editor... + bool is_caster_culling_active() const { return data.caster_culling_active; } + bool is_light_culling_active() const { return data.light_culling_active; } + + // Do we want to log some debug output? + bool is_logging() const { return data.debug_count == 0; } + + struct Data { + // Camera frustum planes (world space) - order ePlane. + Vector<Plane> frustum_planes; + + // Camera frustum corners (world space) - order ePoint. + Vector3 frustum_points[NUM_CAM_POINTS]; + + // Master can have multiple directional lights. + // These need to store their own cull planes individually, as master + // chops and changes between culling different lights + // instead of doing one by one, and we don't want to prepare + // lights multiple times per frame. + LocalVector<LightCullPlanes> directional_cull_planes; + + // Single threaded cull planes for regular lights + // (OMNI, SPOT). These lights reuse the same set of cull plane data. + LightCullPlanes regular_cull_planes; + +#ifdef LIGHT_CULLER_DEBUG_REGULAR_LIGHT + uint32_t regular_rejected_count = 0; +#endif + // The whole regular light can be out of range of the view frustum, in which case all casters should be culled. + bool out_of_range = false; + +#ifdef RENDERING_LIGHT_CULLER_DEBUG_STRINGS + static String plane_bitfield_to_string(unsigned int BF); + // Names of the plane and point enums, useful for debugging. + static const char *string_planes[]; + static const char *string_points[]; +#endif + + // Precalculated look up table. + static uint8_t LUT_entry_sizes[LUT_SIZE]; + static uint8_t LUT_entries[LUT_SIZE][8]; + + bool caster_culling_active = true; + bool light_culling_active = true; + + // Light culling is a basic on / off switch. + // Caster culling only works if light culling is also on. + bool is_active() const { return light_culling_active; } + + // Ideally a frame counter, but for ease of implementation + // this is just incremented on each prepare_camera. + // used to turn on and off debugging features. + int debug_count = -1; + } data; + + // This functionality is not required in general use (and is compiled out), + // as the lookup table can normally be hard coded + // (provided order of planes etc does not change). + // It is provided for debugging / future maintenance. +#ifdef RENDERING_LIGHT_CULLER_CALCULATE_LUT + void get_neighbouring_planes(PlaneOrder p_plane, PlaneOrder r_neigh_planes[4]) const; + void get_corners_of_planes(PlaneOrder p_plane_a, PlaneOrder p_plane_b, PointOrder r_points[2]) const; + void create_LUT(); + void compact_LUT_entry(uint32_t p_entry_id); + void debug_print_LUT(); + void debug_print_LUT_as_table(); + void add_LUT(int p_plane_0, int p_plane_1, PointOrder p_pts[2]); + void add_LUT_entry(uint32_t p_entry_id, PointOrder p_pts[2]); + String debug_string_LUT_entry(const LocalVector<uint8_t> &p_entry, bool p_pair = false); + String string_LUT_entry(const LocalVector<uint8_t> &p_entry); + + // Contains a list of points for each combination of plane facing directions. + LocalVector<uint8_t> _calculated_LUT[LUT_SIZE]; +#endif +}; + +#endif // RENDERING_LIGHT_CULLER_H diff --git a/thirdparty/README.md b/thirdparty/README.md index 5507fa9600..1988744340 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -971,6 +971,8 @@ Files extracted from upstream source: - `unstable/tablet/tablet-unstable-v2.xml` - `unstable/xdg-decoration/README` - `unstable/xdg-decoration/xdg-decoration-unstable-v1.xml` +- `unstable/xdg-foreign/README` +- `unstable/xdg-foreign/xdg-foreign-unstable-v1.xml` - `COPYING` diff --git a/thirdparty/wayland-protocols/unstable/xdg-foreign/README b/thirdparty/wayland-protocols/unstable/xdg-foreign/README new file mode 100644 index 0000000000..f5bcb838f3 --- /dev/null +++ b/thirdparty/wayland-protocols/unstable/xdg-foreign/README @@ -0,0 +1,4 @@ +xdg foreign protocol + +Maintainers: +Jonas Ådahl <jadahl@gmail.com> diff --git a/thirdparty/wayland-protocols/unstable/xdg-foreign/xdg-foreign-unstable-v1.xml b/thirdparty/wayland-protocols/unstable/xdg-foreign/xdg-foreign-unstable-v1.xml new file mode 100644 index 0000000000..913963aa52 --- /dev/null +++ b/thirdparty/wayland-protocols/unstable/xdg-foreign/xdg-foreign-unstable-v1.xml @@ -0,0 +1,182 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="xdg_foreign_unstable_v1"> + + <copyright> + Copyright © 2015-2016 Red Hat Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + </copyright> + + <description summary="Protocol for exporting xdg surface handles"> + This protocol specifies a way for making it possible to reference a surface + of a different client. With such a reference, a client can, by using the + interfaces provided by this protocol, manipulate the relationship between + its own surfaces and the surface of some other client. For example, stack + some of its own surface above the other clients surface. + + In order for a client A to get a reference of a surface of client B, client + B must first export its surface using xdg_exporter.export. Upon doing this, + client B will receive a handle (a unique string) that it may share with + client A in some way (for example D-Bus). After client A has received the + handle from client B, it may use xdg_importer.import to create a reference + to the surface client B just exported. See the corresponding requests for + details. + + A possible use case for this is out-of-process dialogs. For example when a + sandboxed client without file system access needs the user to select a file + on the file system, given sandbox environment support, it can export its + surface, passing the exported surface handle to an unsandboxed process that + can show a file browser dialog and stack it above the sandboxed client's + surface. + + Warning! The protocol described in this file is experimental and backward + incompatible changes may be made. Backward compatible changes may be added + together with the corresponding interface version bump. Backward + incompatible changes are done by bumping the version number in the protocol + and interface names and resetting the interface version. Once the protocol + is to be declared stable, the 'z' prefix and the version number in the + protocol and interface names are removed and the interface version number is + reset. + </description> + + <interface name="zxdg_exporter_v1" version="1"> + <description summary="interface for exporting surfaces"> + A global interface used for exporting surfaces that can later be imported + using xdg_importer. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the xdg_exporter object"> + Notify the compositor that the xdg_exporter object will no longer be + used. + </description> + </request> + + <request name="export"> + <description summary="export a surface"> + The export request exports the passed surface so that it can later be + imported via xdg_importer. When called, a new xdg_exported object will + be created and xdg_exported.handle will be sent immediately. See the + corresponding interface and event for details. + + A surface may be exported multiple times, and each exported handle may + be used to create an xdg_imported multiple times. Only xdg_surface + surfaces may be exported. + </description> + <arg name="id" type="new_id" interface="zxdg_exported_v1" + summary="the new xdg_exported object"/> + <arg name="surface" type="object" interface="wl_surface" + summary="the surface to export"/> + </request> + </interface> + + <interface name="zxdg_importer_v1" version="1"> + <description summary="interface for importing surfaces"> + A global interface used for importing surfaces exported by xdg_exporter. + With this interface, a client can create a reference to a surface of + another client. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the xdg_importer object"> + Notify the compositor that the xdg_importer object will no longer be + used. + </description> + </request> + + <request name="import"> + <description summary="import a surface"> + The import request imports a surface from any client given a handle + retrieved by exporting said surface using xdg_exporter.export. When + called, a new xdg_imported object will be created. This new object + represents the imported surface, and the importing client can + manipulate its relationship using it. See xdg_imported for details. + </description> + <arg name="id" type="new_id" interface="zxdg_imported_v1" + summary="the new xdg_imported object"/> + <arg name="handle" type="string" + summary="the exported surface handle"/> + </request> + </interface> + + <interface name="zxdg_exported_v1" version="1"> + <description summary="an exported surface handle"> + An xdg_exported object represents an exported reference to a surface. The + exported surface may be referenced as long as the xdg_exported object not + destroyed. Destroying the xdg_exported invalidates any relationship the + importer may have established using xdg_imported. + </description> + + <request name="destroy" type="destructor"> + <description summary="unexport the exported surface"> + Revoke the previously exported surface. This invalidates any + relationship the importer may have set up using the xdg_imported created + given the handle sent via xdg_exported.handle. + </description> + </request> + + <event name="handle"> + <description summary="the exported surface handle"> + The handle event contains the unique handle of this exported surface + reference. It may be shared with any client, which then can use it to + import the surface by calling xdg_importer.import. A handle may be + used to import the surface multiple times. + </description> + <arg name="handle" type="string" summary="the exported surface handle"/> + </event> + </interface> + + <interface name="zxdg_imported_v1" version="1"> + <description summary="an imported surface handle"> + An xdg_imported object represents an imported reference to surface exported + by some client. A client can use this interface to manipulate + relationships between its own surfaces and the imported surface. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the xdg_imported object"> + Notify the compositor that it will no longer use the xdg_imported + object. Any relationship that may have been set up will at this point + be invalidated. + </description> + </request> + + <request name="set_parent_of"> + <description summary="set as the parent of some surface"> + Set the imported surface as the parent of some surface of the client. + The passed surface must be a toplevel xdg_surface. Calling this function + sets up a surface to surface relation with the same stacking and positioning + semantics as xdg_surface.set_parent. + </description> + <arg name="surface" type="object" interface="wl_surface" + summary="the child surface"/> + </request> + + <event name="destroyed"> + <description summary="the imported surface handle has been destroyed"> + The imported surface handle has been destroyed and any relationship set + up has been invalidated. This may happen for various reasons, for + example if the exported surface or the exported surface handle has been + destroyed, if the handle used for importing was invalid. + </description> + </event> + </interface> + +</protocol> |