summaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/fbx/editor/editor_scene_importer_fbx2gltf.cpp5
-rw-r--r--modules/fbx/editor/editor_scene_importer_ufbx.cpp2
-rw-r--r--modules/fbx/fbx_document.cpp29
-rw-r--r--modules/gdscript/doc_classes/@GDScript.xml2
-rw-r--r--modules/gdscript/editor/gdscript_highlighter.cpp73
-rw-r--r--modules/gdscript/editor/gdscript_highlighter.h5
-rw-r--r--modules/gdscript/gdscript_analyzer.cpp61
-rw-r--r--modules/gdscript/gdscript_compiler.cpp8
-rw-r--r--modules/gdscript/gdscript_compiler.h6
-rw-r--r--modules/gdscript/gdscript_editor.cpp44
-rw-r--r--modules/gdscript/gdscript_parser.cpp56
-rw-r--r--modules/gdscript/gdscript_parser.h1
-rw-r--r--modules/gdscript/gdscript_tokenizer.cpp3
-rw-r--r--modules/gdscript/gdscript_tokenizer_buffer.cpp6
-rw-r--r--modules/gdscript/gdscript_warning.cpp8
-rw-r--r--modules/gdscript/gdscript_warning.h4
-rw-r--r--modules/gdscript/tests/gdscript_test_runner.cpp23
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_array.gd3
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_array.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_object.gd3
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_object.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/cast_object_to_int.gd3
-rw-r--r--modules/gdscript/tests/scripts/analyzer/errors/cast_object_to_int.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/cast_enum_to_int.gd9
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/cast_enum_to_int.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/enum_typecheck_inner_class.gd3
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/infer_type_on_string_format.gd6
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/infer_type_on_string_format.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/unassigned_builtin_typed.gd7
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/unassigned_builtin_typed.out2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.gd2
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/enum_without_default_value.gd9
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/enum_without_default_value.out7
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/unsafe_cast.gd24
-rw-r--r--modules/gdscript/tests/scripts/analyzer/warnings/unsafe_cast.out33
-rw-r--r--modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_autocomplete.cfg9
-rw-r--r--modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_autocomplete.gd4
-rw-r--r--modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_values_autocompletion.cfg5
-rw-r--r--modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_values_autocompletion.gd4
-rw-r--r--modules/gdscript/tests/scripts/parser/features/constants.out32
-rw-r--r--modules/gdscript/tests/scripts/parser/features/continuation_lines_comments.bin.gd12
-rw-r--r--modules/gdscript/tests/scripts/parser/features/continuation_lines_comments.bin.out2
-rw-r--r--modules/gdscript/tests/scripts/parser/features/match_array.gd33
-rw-r--r--modules/gdscript/tests/scripts/parser/features/match_array.out6
-rw-r--r--modules/gdscript/tests/scripts/parser/features/match_dictionary.gd21
-rw-r--r--modules/gdscript/tests/scripts/parser/features/match_dictionary.out3
-rw-r--r--modules/gdscript/tests/scripts/parser/features/static_typing.out20
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.gd11
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.out15
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/unused_constant.gd4
-rw-r--r--modules/gdscript/tests/scripts/parser/warnings/unused_constant.out5
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/cast_freed_object.gd4
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/cast_freed_object.out6
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/cast_int_to_array.gd4
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/cast_int_to_array.out6
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/cast_int_to_object.gd4
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/cast_int_to_object.out6
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/cast_object_to_int.gd4
-rw-r--r--modules/gdscript/tests/scripts/runtime/errors/cast_object_to_int.out6
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/await_signal_with_parameters.gd25
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/await_signal_with_parameters.out4
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/member_info.gd1
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/reset_unassigned_variables_in_loops.gd2
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/type_casting.gd24
-rw-r--r--modules/gdscript/tests/scripts/runtime/features/type_casting.out10
-rw-r--r--modules/gltf/doc_classes/GLTFPhysicsBody.xml2
-rw-r--r--modules/gltf/doc_classes/GLTFPhysicsShape.xml16
-rw-r--r--modules/gltf/editor/editor_scene_exporter_gltf_settings.cpp9
-rw-r--r--modules/gltf/editor/editor_scene_exporter_gltf_settings.h1
-rw-r--r--modules/gltf/editor/editor_scene_importer_blend.cpp5
-rw-r--r--modules/gltf/editor/editor_scene_importer_gltf.cpp5
-rw-r--r--modules/gltf/extensions/physics/gltf_document_extension_physics.cpp1
-rw-r--r--modules/gltf/extensions/physics/gltf_physics_shape.cpp65
-rw-r--r--modules/gltf/extensions/physics/gltf_physics_shape.h5
-rw-r--r--modules/gltf/gltf_document.cpp19
-rw-r--r--modules/gridmap/doc_classes/GridMap.xml4
-rw-r--r--modules/gridmap/grid_map.cpp19
-rw-r--r--modules/gridmap/grid_map.h1
-rw-r--r--modules/jsonrpc/SCsub7
-rw-r--r--modules/jsonrpc/tests/test_jsonrpc.cpp86
-rw-r--r--modules/jsonrpc/tests/test_jsonrpc.h139
-rw-r--r--modules/ktx/SCsub1
-rw-r--r--modules/lightmapper_rd/lightmapper_rd.cpp2
-rw-r--r--modules/mbedtls/SCsub38
-rw-r--r--modules/mbedtls/crypto_mbedtls.cpp46
-rw-r--r--modules/mbedtls/crypto_mbedtls.h2
-rw-r--r--modules/mbedtls/tls_context_mbedtls.h1
-rw-r--r--modules/mono/csharp_script.cpp194
-rw-r--r--modules/mono/csharp_script.h2
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs9
-rw-r--r--modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs4
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs5
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/DebugView.cs73
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs10
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs5
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs41
-rw-r--r--modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj1
-rw-r--r--modules/mono/glue/runtime_interop.cpp4
-rw-r--r--modules/mono/mono_gd/gd_mono.h2
-rw-r--r--modules/multiplayer/editor/replication_editor.cpp4
-rw-r--r--modules/multiplayer/multiplayer_spawner.cpp1
-rw-r--r--modules/multiplayer/multiplayer_synchronizer.cpp1
-rw-r--r--modules/navigation/2d/godot_navigation_server_2d.cpp32
-rw-r--r--modules/navigation/2d/godot_navigation_server_2d.h5
-rw-r--r--modules/navigation/2d/nav_mesh_generator_2d.cpp210
-rw-r--r--modules/navigation/2d/nav_mesh_generator_2d.h15
-rw-r--r--modules/navigation/3d/godot_navigation_server_3d.cpp151
-rw-r--r--modules/navigation/3d/godot_navigation_server_3d.h10
-rw-r--r--modules/navigation/3d/nav_mesh_generator_3d.cpp66
-rw-r--r--modules/navigation/3d/nav_mesh_generator_3d.h15
-rw-r--r--modules/navigation/3d/navigation_mesh_generator.cpp2
-rw-r--r--modules/navigation/editor/navigation_mesh_editor_plugin.cpp3
-rw-r--r--modules/openxr/config.py4
-rw-r--r--modules/openxr/doc_classes/OpenXRCompositionLayer.xml43
-rw-r--r--modules/openxr/doc_classes/OpenXRCompositionLayerCylinder.xml25
-rw-r--r--modules/openxr/doc_classes/OpenXRCompositionLayerEquirect.xml28
-rw-r--r--modules/openxr/doc_classes/OpenXRCompositionLayerQuad.xml16
-rw-r--r--modules/openxr/doc_classes/OpenXRExtensionWrapperExtension.xml31
-rw-r--r--modules/openxr/doc_classes/OpenXRInterface.xml7
-rw-r--r--modules/openxr/editor/openxr_action_set_editor.cpp4
-rw-r--r--modules/openxr/editor/openxr_select_interaction_profile_dialog.cpp2
-rw-r--r--modules/openxr/extensions/openxr_composition_layer_extension.cpp322
-rw-r--r--modules/openxr/extensions/openxr_composition_layer_extension.h116
-rw-r--r--modules/openxr/extensions/openxr_extension_wrapper.h5
-rw-r--r--modules/openxr/extensions/openxr_extension_wrapper_extension.cpp34
-rw-r--r--modules/openxr/extensions/openxr_extension_wrapper_extension.h12
-rw-r--r--modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.cpp18
-rw-r--r--modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.h1
-rw-r--r--modules/openxr/extensions/platform/openxr_vulkan_extension.cpp4
-rw-r--r--modules/openxr/openxr_api.cpp536
-rw-r--r--modules/openxr/openxr_api.h54
-rw-r--r--modules/openxr/openxr_interface.cpp5
-rw-r--r--modules/openxr/openxr_interface.h1
-rw-r--r--modules/openxr/register_types.cpp15
-rw-r--r--modules/openxr/scene/openxr_composition_layer.cpp351
-rw-r--r--modules/openxr/scene/openxr_composition_layer.h98
-rw-r--r--modules/openxr/scene/openxr_composition_layer_cylinder.cpp235
-rw-r--r--modules/openxr/scene/openxr_composition_layer_cylinder.h75
-rw-r--r--modules/openxr/scene/openxr_composition_layer_equirect.cpp260
-rw-r--r--modules/openxr/scene/openxr_composition_layer_equirect.h79
-rw-r--r--modules/openxr/scene/openxr_composition_layer_quad.cpp131
-rw-r--r--modules/openxr/scene/openxr_composition_layer_quad.h63
-rw-r--r--modules/openxr/util.h20
-rw-r--r--modules/raycast/SCsub5
-rw-r--r--modules/raycast/godot_update_embree.py222
-rw-r--r--modules/raycast/lightmap_raycaster_embree.cpp11
-rw-r--r--modules/raycast/lightmap_raycaster_embree.h2
-rw-r--r--modules/raycast/raycast_occlusion_cull.cpp75
-rw-r--r--modules/raycast/raycast_occlusion_cull.h4
-rw-r--r--modules/raycast/static_raycaster_embree.cpp9
-rw-r--r--modules/raycast/static_raycaster_embree.h2
-rw-r--r--modules/regex/tests/test_regex.h42
-rw-r--r--modules/svg/image_loader_svg.cpp12
-rw-r--r--modules/text_server_adv/SCsub1
-rw-r--r--modules/text_server_adv/gdextension_build/SConstruct1
-rw-r--r--modules/text_server_adv/text_server_adv.cpp144
-rw-r--r--modules/text_server_adv/text_server_adv.h5
-rw-r--r--modules/text_server_fb/text_server_fb.cpp134
-rw-r--r--modules/text_server_fb/text_server_fb.h5
-rw-r--r--modules/upnp/doc_classes/UPNPDevice.xml4
-rw-r--r--modules/upnp/upnp.cpp29
-rw-r--r--modules/zip/zip_reader.cpp9
162 files changed, 4368 insertions, 1031 deletions
diff --git a/modules/fbx/editor/editor_scene_importer_fbx2gltf.cpp b/modules/fbx/editor/editor_scene_importer_fbx2gltf.cpp
index b615b5aed8..6629e05197 100644
--- a/modules/fbx/editor/editor_scene_importer_fbx2gltf.cpp
+++ b/modules/fbx/editor/editor_scene_importer_fbx2gltf.cpp
@@ -118,10 +118,9 @@ Node *EditorSceneFormatImporterFBX2GLTF::import_scene(const String &p_path, uint
#ifndef DISABLE_DEPRECATED
bool trimming = p_options.has("animation/trimming") ? (bool)p_options["animation/trimming"] : false;
- bool remove_immutable = p_options.has("animation/remove_immutable_tracks") ? (bool)p_options["animation/remove_immutable_tracks"] : true;
- return gltf->generate_scene(state, (float)p_options["animation/fps"], trimming, remove_immutable);
+ return gltf->generate_scene(state, (float)p_options["animation/fps"], trimming, false);
#else
- return gltf->generate_scene(state, (float)p_options["animation/fps"], (bool)p_options["animation/trimming"], (bool)p_options["animation/remove_immutable_tracks"]);
+ return gltf->generate_scene(state, (float)p_options["animation/fps"], (bool)p_options["animation/trimming"], false);
#endif
}
diff --git a/modules/fbx/editor/editor_scene_importer_ufbx.cpp b/modules/fbx/editor/editor_scene_importer_ufbx.cpp
index 241fdba0c5..3cc919fae2 100644
--- a/modules/fbx/editor/editor_scene_importer_ufbx.cpp
+++ b/modules/fbx/editor/editor_scene_importer_ufbx.cpp
@@ -87,7 +87,7 @@ Node *EditorSceneFormatImporterUFBX::import_scene(const String &p_path, uint32_t
}
return nullptr;
}
- return fbx->generate_scene(state, (float)p_options["animation/fps"], (bool)p_options["animation/trimming"], (bool)p_options["animation/remove_immutable_tracks"]);
+ return fbx->generate_scene(state, (float)p_options["animation/fps"], (bool)p_options["animation/trimming"], false);
}
Variant EditorSceneFormatImporterUFBX::get_option_visibility(const String &p_path, bool p_for_animation,
diff --git a/modules/fbx/fbx_document.cpp b/modules/fbx/fbx_document.cpp
index 1dbe4c535a..44a929d285 100644
--- a/modules/fbx/fbx_document.cpp
+++ b/modules/fbx/fbx_document.cpp
@@ -990,8 +990,8 @@ Error FBXDocument::_parse_images(Ref<FBXState> p_state, const String &p_base_pat
for (int texture_i = 0; texture_i < static_cast<int>(fbx_scene->texture_files.count); texture_i++) {
const ufbx_texture_file &fbx_texture_file = fbx_scene->texture_files[texture_i];
String path = _as_string(fbx_texture_file.filename);
- path = ProjectSettings::get_singleton()->localize_path(path);
- if (path.is_absolute_path() && !path.is_resource_file()) {
+ // Use only filename for absolute paths to avoid portability issues.
+ if (path.is_absolute_path()) {
path = path.get_file();
}
if (!p_base_path.is_empty()) {
@@ -1080,7 +1080,7 @@ Error FBXDocument::_parse_materials(Ref<FBXState> p_state) {
if (fbx_material->pbr.base_color.has_value) {
Color albedo = _material_color(fbx_material->pbr.base_color, fbx_material->pbr.base_factor);
- material->set_albedo(albedo);
+ material->set_albedo(albedo.linear_to_srgb());
}
if (fbx_material->features.double_sided.enabled) {
@@ -1178,7 +1178,11 @@ Error FBXDocument::_parse_materials(Ref<FBXState> p_state) {
}
// Combined textures and factors are very unreliable in FBX
- material->set_albedo(Color(1, 1, 1));
+ Color albedo_factor = Color(1, 1, 1);
+ if (fbx_material->pbr.base_factor.has_value) {
+ albedo_factor *= (float)fbx_material->pbr.base_factor.value_real;
+ }
+ material->set_albedo(albedo_factor.linear_to_srgb());
// TODO: Does not support rotation, could be inverted?
material->set_uv1_offset(_as_vec3(base_texture->uv_transform.translation));
@@ -1232,11 +1236,11 @@ Error FBXDocument::_parse_materials(Ref<FBXState> p_state) {
if (fbx_material->pbr.emission_color.has_value) {
material->set_feature(BaseMaterial3D::FEATURE_EMISSION, true);
- material->set_emission(_material_color(fbx_material->pbr.emission_color));
+ material->set_emission(_material_color(fbx_material->pbr.emission_color).linear_to_srgb());
material->set_emission_energy_multiplier(float(fbx_material->pbr.emission_factor.value_real));
}
- const ufbx_texture *emission_texture = _get_file_texture(fbx_material->pbr.ambient_occlusion.texture);
+ const ufbx_texture *emission_texture = _get_file_texture(fbx_material->pbr.emission_color.texture);
if (emission_texture) {
material->set_texture(BaseMaterial3D::TEXTURE_EMISSION, _get_texture(p_state, GLTFTextureIndex(emission_texture->file_index), TEXTURE_TYPE_GENERIC));
material->set_feature(BaseMaterial3D::FEATURE_EMISSION, true);
@@ -1266,7 +1270,7 @@ Error FBXDocument::_parse_cameras(Ref<FBXState> p_state) {
camera->set_fov(Math::deg_to_rad(real_t(fbx_camera->field_of_view_deg.y)));
} else {
camera->set_perspective(false);
- camera->set_size_mag(real_t(fbx_camera->orthographic_size.y));
+ camera->set_size_mag(real_t(fbx_camera->orthographic_size.y * 0.5f));
}
if (fbx_camera->near_plane != 0.0f) {
camera->set_depth_near(fbx_camera->near_plane);
@@ -1625,6 +1629,9 @@ void FBXDocument::_generate_skeleton_bone_node(Ref<FBXState> p_state, const GLTF
active_skeleton = skeleton;
current_node = active_skeleton;
+ if (active_skeleton) {
+ p_scene_parent = active_skeleton;
+ }
if (requires_extra_node) {
current_node = nullptr;
@@ -2015,8 +2022,8 @@ Node *FBXDocument::generate_scene(Ref<GLTFState> p_state, float p_bake_fps, bool
GLTFNodeIndex fbx_root = state->root_nodes.write[0];
Node *fbx_root_node = state->get_scene_node(fbx_root);
Node *root = fbx_root_node;
- if (fbx_root_node && fbx_root_node->get_parent()) {
- root = fbx_root_node->get_parent();
+ if (root && root->get_owner() && root->get_owner() != root) {
+ root = root->get_owner();
}
ERR_FAIL_NULL_V(root, nullptr);
_process_mesh_instances(state, root);
@@ -2235,6 +2242,10 @@ Error FBXDocument::_parse_lights(Ref<FBXState> p_state) {
}
String FBXDocument::_get_texture_path(const String &p_base_dir, const String &p_source_file_path) const {
+ // Check if the original path exists first.
+ if (FileAccess::exists(p_source_file_path)) {
+ return p_source_file_path.strip_edges();
+ }
const String tex_file_name = p_source_file_path.get_file();
const Vector<String> subdirs = {
"", "textures/", "Textures/", "images/",
diff --git a/modules/gdscript/doc_classes/@GDScript.xml b/modules/gdscript/doc_classes/@GDScript.xml
index 4869573972..07d917ea04 100644
--- a/modules/gdscript/doc_classes/@GDScript.xml
+++ b/modules/gdscript/doc_classes/@GDScript.xml
@@ -168,7 +168,7 @@
# Load a scene called "main" located in the root of the project directory and cache it in a variable.
var main = load("res://main.tscn") # main will contain a PackedScene resource.
[/codeblock]
- [b]Important:[/b] The path must be absolute. A relative path will always return [code]null[/code].
+ [b]Important:[/b] Relative paths are [i]not[/i] relative to the script calling this method, instead it is prefixed with [code]"res://"[/code]. Loading from relative paths might not work as expected.
This function is a simplified version of [method ResourceLoader.load], which can be used for more advanced scenarios.
[b]Note:[/b] Files have to be imported into the engine first to load them using this function. If you want to load [Image]s at run-time, you may use [method Image.load]. If you want to import audio files, you can use the snippet described in [member AudioStreamMP3.data].
[b]Note:[/b] If [member ProjectSettings.editor/export/convert_text_resources_to_binary] is [code]true[/code], [method @GDScript.load] will not be able to read converted files in an exported project. If you rely on run-time loading of files present within the PCK, set [member ProjectSettings.editor/export/convert_text_resources_to_binary] to [code]false[/code].
diff --git a/modules/gdscript/editor/gdscript_highlighter.cpp b/modules/gdscript/editor/gdscript_highlighter.cpp
index 439555bacb..f83b784f85 100644
--- a/modules/gdscript/editor/gdscript_highlighter.cpp
+++ b/modules/gdscript/editor/gdscript_highlighter.cpp
@@ -53,7 +53,6 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
bool in_keyword = false;
bool in_word = false;
bool in_number = false;
- bool in_raw_string = false;
bool in_node_path = false;
bool in_node_ref = false;
bool in_annotation = false;
@@ -65,8 +64,10 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
bool in_function_name = false; // Any call.
bool in_function_declaration = false; // Only declaration.
- bool in_var_const_declaration = false;
bool in_signal_declaration = false;
+ bool is_after_func_signal_declaration = false;
+ bool in_var_const_declaration = false;
+ bool is_after_var_const_declaration = false;
bool expect_type = false;
int in_declaration_params = 0; // The number of opened `(` after func/signal name.
@@ -125,6 +126,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
if (from != line_length) {
// Check if we are in entering a region.
if (in_region == -1) {
+ const bool r_prefix = from > 0 && str[from - 1] == 'r';
for (int c = 0; c < color_regions.size(); c++) {
// Check there is enough room.
int chars_left = line_length - from;
@@ -134,6 +136,10 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
continue;
}
+ if (color_regions[c].is_string && color_regions[c].r_prefix != r_prefix) {
+ continue;
+ }
+
// Search the line.
bool match = true;
const char32_t *start_key = color_regions[c].start_key.get_data();
@@ -152,7 +158,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
// Check if it's the whole line.
if (end_key_length == 0 || color_regions[c].line_only || from + end_key_length > line_length) {
// Don't skip comments, for highlighting markers.
- if (color_regions[in_region].type == ColorRegion::TYPE_COMMENT) {
+ if (color_regions[in_region].is_comment) {
break;
}
if (from + end_key_length > line_length) {
@@ -174,7 +180,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
}
// Don't skip comments, for highlighting markers.
- if (j == line_length && color_regions[in_region].type != ColorRegion::TYPE_COMMENT) {
+ if (j == line_length && !color_regions[in_region].is_comment) {
continue;
}
}
@@ -196,7 +202,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
highlighter_info["color"] = region_color;
color_map[j] = highlighter_info;
- if (color_regions[in_region].type == ColorRegion::TYPE_COMMENT) {
+ if (color_regions[in_region].is_comment) {
int marker_start_pos = from;
int marker_len = 0;
while (from <= line_length) {
@@ -240,7 +246,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
}
if (str[from] == '\\') {
- if (!in_raw_string) {
+ if (!color_regions[in_region].r_prefix) {
Dictionary escape_char_highlighter_info;
escape_char_highlighter_info["color"] = symbol_color;
color_map[from] = escape_char_highlighter_info;
@@ -248,7 +254,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
from++;
- if (!in_raw_string) {
+ if (!color_regions[in_region].r_prefix) {
int esc_len = 0;
if (str[from] == 'u') {
esc_len = 4;
@@ -410,6 +416,8 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
col = class_names[word];
} else if (reserved_keywords.has(word)) {
col = reserved_keywords[word];
+ // Don't highlight `list` as a type in `for elem: Type in list`.
+ expect_type = false;
} else if (member_keywords.has(word)) {
col = member_keywords[word];
}
@@ -480,6 +488,13 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
}
if (is_a_symbol) {
+ if (in_function_declaration || in_signal_declaration) {
+ is_after_func_signal_declaration = true;
+ }
+ if (in_var_const_declaration) {
+ is_after_var_const_declaration = true;
+ }
+
if (in_declaration_params > 0) {
switch (str[j]) {
case '(':
@@ -495,7 +510,7 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
in_declaration_param_dicts -= 1;
break;
}
- } else if ((in_function_declaration || in_signal_declaration || prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::FUNC)) && str[j] == '(') {
+ } else if ((is_after_func_signal_declaration || prev_text == GDScriptTokenizer::get_token_name(GDScriptTokenizer::Token::FUNC)) && str[j] == '(') {
in_declaration_params = 1;
in_declaration_param_dicts = 0;
}
@@ -526,28 +541,25 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
expect_type = true;
in_type_params = 0;
}
- if ((in_var_const_declaration || (in_declaration_params == 1 && in_declaration_param_dicts == 0)) && str[j] == ':') {
+ if ((is_after_var_const_declaration || (in_declaration_params == 1 && in_declaration_param_dicts == 0)) && str[j] == ':') {
expect_type = true;
in_type_params = 0;
}
}
+ in_function_name = false;
+ in_function_declaration = false;
+ in_signal_declaration = false;
+ in_var_const_declaration = false;
+ in_lambda = false;
+ in_member_variable = false;
+
if (!is_whitespace(str[j])) {
- in_function_declaration = false;
- in_var_const_declaration = false;
- in_signal_declaration = false;
- in_function_name = false;
- in_lambda = false;
- in_member_variable = false;
+ is_after_func_signal_declaration = false;
+ is_after_var_const_declaration = false;
}
}
- if (!in_raw_string && in_region == -1 && str[j] == 'r' && j < line_length - 1 && (str[j + 1] == '"' || str[j + 1] == '\'')) {
- in_raw_string = true;
- } else if (in_raw_string && in_region == -1) {
- in_raw_string = false;
- }
-
// Keep symbol color for binary '&&'. In the case of '&&&' use StringName color for the last ampersand.
if (!in_string_name && in_region == -1 && str[j] == '&' && !is_binary_op) {
if (j >= 2 && str[j - 1] == '&' && str[j - 2] != '&' && prev_is_binary_op) {
@@ -579,7 +591,9 @@ Dictionary GDScriptSyntaxHighlighter::_get_line_syntax_highlighting_impl(int p_l
in_annotation = false;
}
- if (in_raw_string) {
+ const bool in_raw_string_prefix = in_region == -1 && str[j] == 'r' && j + 1 < line_length && (str[j + 1] == '"' || str[j + 1] == '\'');
+
+ if (in_raw_string_prefix) {
color = string_color;
} else if (in_node_ref) {
next_type = NODE_REF;
@@ -781,6 +795,10 @@ void GDScriptSyntaxHighlighter::_update_cache() {
add_color_region(ColorRegion::TYPE_STRING, "'", "'", string_color);
add_color_region(ColorRegion::TYPE_MULTILINE_STRING, "\"\"\"", "\"\"\"", string_color);
add_color_region(ColorRegion::TYPE_MULTILINE_STRING, "'''", "'''", string_color);
+ add_color_region(ColorRegion::TYPE_STRING, "\"", "\"", string_color, false, true);
+ add_color_region(ColorRegion::TYPE_STRING, "'", "'", string_color, false, true);
+ add_color_region(ColorRegion::TYPE_MULTILINE_STRING, "\"\"\"", "\"\"\"", string_color, false, true);
+ add_color_region(ColorRegion::TYPE_MULTILINE_STRING, "'''", "'''", string_color, false, true);
const Ref<Script> scr = _get_edited_resource();
if (scr.is_valid()) {
@@ -913,7 +931,7 @@ void GDScriptSyntaxHighlighter::_update_cache() {
}
}
-void GDScriptSyntaxHighlighter::add_color_region(ColorRegion::Type p_type, const String &p_start_key, const String &p_end_key, const Color &p_color, bool p_line_only) {
+void GDScriptSyntaxHighlighter::add_color_region(ColorRegion::Type p_type, const String &p_start_key, const String &p_end_key, const Color &p_color, bool p_line_only, bool p_r_prefix) {
ERR_FAIL_COND_MSG(p_start_key.is_empty(), "Color region start key cannot be empty.");
ERR_FAIL_COND_MSG(!is_symbol(p_start_key[0]), "Color region start key must start with a symbol.");
@@ -922,9 +940,9 @@ void GDScriptSyntaxHighlighter::add_color_region(ColorRegion::Type p_type, const
}
int at = 0;
- for (int i = 0; i < color_regions.size(); i++) {
- ERR_FAIL_COND_MSG(color_regions[i].start_key == p_start_key, "Color region with start key '" + p_start_key + "' already exists.");
- if (p_start_key.length() < color_regions[i].start_key.length()) {
+ for (const ColorRegion &region : color_regions) {
+ ERR_FAIL_COND_MSG(region.start_key == p_start_key && region.r_prefix == p_r_prefix, "Color region with start key '" + p_start_key + "' already exists.");
+ if (p_start_key.length() < region.start_key.length()) {
at++;
} else {
break;
@@ -937,6 +955,9 @@ void GDScriptSyntaxHighlighter::add_color_region(ColorRegion::Type p_type, const
color_region.start_key = p_start_key;
color_region.end_key = p_end_key;
color_region.line_only = p_line_only;
+ color_region.r_prefix = p_r_prefix;
+ color_region.is_string = p_type == ColorRegion::TYPE_STRING || p_type == ColorRegion::TYPE_MULTILINE_STRING;
+ color_region.is_comment = p_type == ColorRegion::TYPE_COMMENT || p_type == ColorRegion::TYPE_CODE_REGION;
color_regions.insert(at, color_region);
clear_highlighting_cache();
}
diff --git a/modules/gdscript/editor/gdscript_highlighter.h b/modules/gdscript/editor/gdscript_highlighter.h
index eb7bb7d801..655557b3d9 100644
--- a/modules/gdscript/editor/gdscript_highlighter.h
+++ b/modules/gdscript/editor/gdscript_highlighter.h
@@ -52,6 +52,9 @@ private:
String start_key;
String end_key;
bool line_only = false;
+ bool r_prefix = false;
+ bool is_string = false; // `TYPE_STRING` or `TYPE_MULTILINE_STRING`.
+ bool is_comment = false; // `TYPE_COMMENT` or `TYPE_CODE_REGION`.
};
Vector<ColorRegion> color_regions;
HashMap<int, int> color_region_cache;
@@ -103,7 +106,7 @@ private:
Color comment_marker_colors[COMMENT_MARKER_MAX];
HashMap<String, CommentMarkerLevel> comment_markers;
- void add_color_region(ColorRegion::Type p_type, const String &p_start_key, const String &p_end_key, const Color &p_color, bool p_line_only = false);
+ void add_color_region(ColorRegion::Type p_type, const String &p_start_key, const String &p_end_key, const Color &p_color, bool p_line_only = false, bool p_r_prefix = false);
public:
virtual void _update_cache() override;
diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp
index 6af6460b31..ec75663e97 100644
--- a/modules/gdscript/gdscript_analyzer.cpp
+++ b/modules/gdscript/gdscript_analyzer.cpp
@@ -1957,6 +1957,18 @@ void GDScriptAnalyzer::resolve_assignable(GDScriptParser::AssignableNode *p_assi
} else {
parser->push_warning(p_assignable, GDScriptWarning::UNTYPED_DECLARATION, declaration_type, p_assignable->identifier->name);
}
+ } else if (specified_type.kind == GDScriptParser::DataType::ENUM && p_assignable->initializer == nullptr) {
+ // Warn about enum variables without default value. Unless the enum defines the "0" value, then it's fine.
+ bool has_zero_value = false;
+ for (const KeyValue<StringName, int64_t> &kv : specified_type.enum_values) {
+ if (kv.value == 0) {
+ has_zero_value = true;
+ break;
+ }
+ }
+ if (!has_zero_value) {
+ parser->push_warning(p_assignable, GDScriptWarning::ENUM_VARIABLE_WITHOUT_DEFAULT, p_assignable->identifier->name);
+ }
}
#endif
@@ -1973,8 +1985,6 @@ void GDScriptAnalyzer::resolve_variable(GDScriptParser::VariableNode *p_variable
if (p_is_local) {
if (p_variable->usages == 0 && !String(p_variable->identifier->name).begins_with("_")) {
parser->push_warning(p_variable, GDScriptWarning::UNUSED_VARIABLE, p_variable->identifier->name);
- } else if (p_variable->assignments == 0) {
- parser->push_warning(p_variable, GDScriptWarning::UNASSIGNED_VARIABLE, p_variable->identifier->name);
}
}
is_shadowing(p_variable->identifier, kind, p_is_local);
@@ -1987,7 +1997,7 @@ void GDScriptAnalyzer::resolve_constant(GDScriptParser::ConstantNode *p_constant
#ifdef DEBUG_ENABLED
if (p_is_local) {
- if (p_constant->usages == 0) {
+ if (p_constant->usages == 0 && !String(p_constant->identifier->name).begins_with("_")) {
parser->push_warning(p_constant, GDScriptWarning::UNUSED_LOCAL_CONSTANT, p_constant->identifier->name);
}
}
@@ -2615,9 +2625,21 @@ void GDScriptAnalyzer::update_array_literal_element_type(GDScriptParser::ArrayNo
}
void GDScriptAnalyzer::reduce_assignment(GDScriptParser::AssignmentNode *p_assignment) {
- reduce_expression(p_assignment->assignee);
reduce_expression(p_assignment->assigned_value);
+#ifdef DEBUG_ENABLED
+ // Increment assignment count for local variables.
+ // Before we reduce the assignee because we don't want to warn about not being assigned when performing the assignment.
+ if (p_assignment->assignee->type == GDScriptParser::Node::IDENTIFIER) {
+ GDScriptParser::IdentifierNode *id = static_cast<GDScriptParser::IdentifierNode *>(p_assignment->assignee);
+ if (id->source == GDScriptParser::IdentifierNode::LOCAL_VARIABLE && id->variable_source) {
+ id->variable_source->assignments++;
+ }
+ }
+#endif
+
+ reduce_expression(p_assignment->assignee);
+
if (p_assignment->assigned_value == nullptr || p_assignment->assignee == nullptr) {
return;
}
@@ -2754,6 +2776,14 @@ void GDScriptAnalyzer::reduce_assignment(GDScriptParser::AssignmentNode *p_assig
if (assignee_type.is_hard_type() && assignee_type.builtin_type == Variant::INT && assigned_value_type.builtin_type == Variant::FLOAT) {
parser->push_warning(p_assignment->assigned_value, GDScriptWarning::NARROWING_CONVERSION);
}
+ // Check for assignment with operation before assignment.
+ if (p_assignment->operation != GDScriptParser::AssignmentNode::OP_NONE && p_assignment->assignee->type == GDScriptParser::Node::IDENTIFIER) {
+ GDScriptParser::IdentifierNode *id = static_cast<GDScriptParser::IdentifierNode *>(p_assignment->assignee);
+ // Use == 1 here because this assignment was already counted in the beginning of the function.
+ if (id->source == GDScriptParser::IdentifierNode::LOCAL_VARIABLE && id->variable_source && id->variable_source->assignments == 1) {
+ parser->push_warning(p_assignment, GDScriptWarning::UNASSIGNED_VARIABLE_OP_ASSIGN, id->name);
+ }
+ }
#endif
}
@@ -2846,6 +2876,11 @@ void GDScriptAnalyzer::reduce_binary_op(GDScriptParser::BinaryOpNode *p_binary_o
result.type_source = GDScriptParser::DataType::ANNOTATED_EXPLICIT;
result.kind = GDScriptParser::DataType::BUILTIN;
result.builtin_type = Variant::BOOL;
+ } else if (p_binary_op->variant_op == Variant::OP_MODULE && left_type.builtin_type == Variant::STRING) {
+ // The modulo operator (%) on string acts as formatting and will always return a string.
+ result.type_source = left_type.type_source;
+ result.kind = GDScriptParser::DataType::BUILTIN;
+ result.builtin_type = Variant::STRING;
} else if (left_type.is_variant() || right_type.is_variant()) {
// Cannot infer type because one operand can be anything.
result.kind = GDScriptParser::DataType::VARIANT;
@@ -3293,6 +3328,7 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
if (get_function_signature(p_call, is_constructor, base_type, p_call->function_name, return_type, par_types, default_arg_count, method_flags)) {
// If the method is implemented in the class hierarchy, the virtual flag will not be set for that MethodInfo and the search stops there.
// Virtual check only possible for super() calls because class hierarchy is known. Node/Objects may have scripts attached we don't know of at compile-time.
+ p_call->is_static = method_flags.has_flag(METHOD_FLAG_STATIC);
if (p_call->is_super && method_flags.has_flag(METHOD_FLAG_VIRTUAL)) {
push_error(vformat(R"*(Cannot call the parent class' virtual function "%s()" because it hasn't been defined.)*", p_call->function_name), p_call);
}
@@ -3311,7 +3347,7 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
base_type.is_meta_type = false;
}
- if (is_self && static_context && !method_flags.has_flag(METHOD_FLAG_STATIC)) {
+ if (is_self && static_context && !p_call->is_static) {
// Get the parent function above any lambda.
GDScriptParser::FunctionNode *parent_function = parser->current_function;
while (parent_function && parent_function->source_lambda) {
@@ -3323,10 +3359,10 @@ void GDScriptAnalyzer::reduce_call(GDScriptParser::CallNode *p_call, bool p_is_a
} else {
push_error(vformat(R"*(Cannot call non-static function "%s()" from a static variable initializer.)*", p_call->function_name), p_call);
}
- } else if (!is_self && base_type.is_meta_type && !method_flags.has_flag(METHOD_FLAG_STATIC)) {
+ } else if (!is_self && base_type.is_meta_type && !p_call->is_static) {
base_type.is_meta_type = false; // For `to_string()`.
push_error(vformat(R"*(Cannot call non-static function "%s()" on the class "%s" directly. Make an instance instead.)*", p_call->function_name, base_type.to_string()), p_call);
- } else if (is_self && !method_flags.has_flag(METHOD_FLAG_STATIC)) {
+ } else if (is_self && !p_call->is_static) {
mark_lambda_use_self();
}
@@ -3438,15 +3474,15 @@ void GDScriptAnalyzer::reduce_cast(GDScriptParser::CastNode *p_cast) {
if (op_type.is_variant() || !op_type.is_hard_type()) {
mark_node_unsafe(p_cast);
#ifdef DEBUG_ENABLED
- if (op_type.is_variant() && !op_type.is_hard_type()) {
- parser->push_warning(p_cast, GDScriptWarning::UNSAFE_CAST, cast_type.to_string());
- }
+ parser->push_warning(p_cast, GDScriptWarning::UNSAFE_CAST, cast_type.to_string());
#endif
} else {
bool valid = false;
if (op_type.builtin_type == Variant::INT && cast_type.kind == GDScriptParser::DataType::ENUM) {
mark_node_unsafe(p_cast);
valid = true;
+ } else if (op_type.kind == GDScriptParser::DataType::ENUM && cast_type.builtin_type == Variant::INT) {
+ valid = true;
} else if (op_type.kind == GDScriptParser::DataType::BUILTIN && cast_type.kind == GDScriptParser::DataType::BUILTIN) {
valid = Variant::can_convert(op_type.builtin_type, cast_type.builtin_type);
} else if (op_type.kind != GDScriptParser::DataType::BUILTIN && cast_type.kind != GDScriptParser::DataType::BUILTIN) {
@@ -3925,6 +3961,11 @@ void GDScriptAnalyzer::reduce_identifier(GDScriptParser::IdentifierNode *p_ident
case GDScriptParser::IdentifierNode::LOCAL_VARIABLE:
p_identifier->set_datatype(p_identifier->variable_source->get_datatype());
found_source = true;
+#ifdef DEBUG_ENABLED
+ if (p_identifier->variable_source && p_identifier->variable_source->assignments == 0 && !(p_identifier->get_datatype().is_hard_type() && p_identifier->get_datatype().kind == GDScriptParser::DataType::BUILTIN)) {
+ parser->push_warning(p_identifier, GDScriptWarning::UNASSIGNED_VARIABLE, p_identifier->name);
+ }
+#endif
break;
case GDScriptParser::IdentifierNode::LOCAL_ITERATOR:
p_identifier->set_datatype(p_identifier->bind_source->get_datatype());
diff --git a/modules/gdscript/gdscript_compiler.cpp b/modules/gdscript/gdscript_compiler.cpp
index 13ed66710c..c526d9c0a4 100644
--- a/modules/gdscript/gdscript_compiler.cpp
+++ b/modules/gdscript/gdscript_compiler.cpp
@@ -250,7 +250,7 @@ static bool _can_use_validate_call(const MethodBind *p_method, const Vector<GDSc
return true;
}
-GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::ExpressionNode *p_expression, bool p_root, bool p_initializer, const GDScriptCodeGenerator::Address &p_index_addr) {
+GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::ExpressionNode *p_expression, bool p_root, bool p_initializer) {
if (p_expression->is_constant && !(p_expression->get_datatype().is_meta_type && p_expression->get_datatype().kind == GDScriptParser::DataType::CLASS)) {
return codegen.add_constant(p_expression->reduced_value);
}
@@ -648,7 +648,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
// Not exact arguments, but still can use method bind call.
gen->write_call_method_bind(result, self, method, arguments);
}
- } else if (codegen.is_static || (codegen.function_node && codegen.function_node->is_static) || call->function_name == "new") {
+ } else if (call->is_static || codegen.is_static || (codegen.function_node && codegen.function_node->is_static) || call->function_name == "new") {
GDScriptCodeGenerator::Address self;
self.mode = GDScriptCodeGenerator::Address::CLASS;
if (is_awaited) {
@@ -781,9 +781,7 @@ GDScriptCodeGenerator::Address GDScriptCompiler::_parse_expression(CodeGen &code
bool named = subscript->is_attribute;
StringName name;
GDScriptCodeGenerator::Address index;
- if (p_index_addr.mode != GDScriptCodeGenerator::Address::NIL) {
- index = p_index_addr;
- } else if (subscript->is_attribute) {
+ if (subscript->is_attribute) {
if (subscript->base->type == GDScriptParser::Node::SELF && codegen.script) {
GDScriptParser::IdentifierNode *identifier = subscript->attribute;
HashMap<StringName, GDScript::MemberInfo>::Iterator MI = codegen.script->member_indices.find(identifier->name);
diff --git a/modules/gdscript/gdscript_compiler.h b/modules/gdscript/gdscript_compiler.h
index 0adbe1ed8e..637d61ca3b 100644
--- a/modules/gdscript/gdscript_compiler.h
+++ b/modules/gdscript/gdscript_compiler.h
@@ -149,13 +149,9 @@ class GDScriptCompiler {
void _set_error(const String &p_error, const GDScriptParser::Node *p_node);
- Error _create_binary_operator(CodeGen &codegen, const GDScriptParser::BinaryOpNode *on, Variant::Operator op, bool p_initializer = false, const GDScriptCodeGenerator::Address &p_index_addr = GDScriptCodeGenerator::Address());
- Error _create_binary_operator(CodeGen &codegen, const GDScriptParser::ExpressionNode *p_left_operand, const GDScriptParser::ExpressionNode *p_right_operand, Variant::Operator op, bool p_initializer = false, const GDScriptCodeGenerator::Address &p_index_addr = GDScriptCodeGenerator::Address());
-
GDScriptDataType _gdtype_from_datatype(const GDScriptParser::DataType &p_datatype, GDScript *p_owner, bool p_handle_metatype = true);
- GDScriptCodeGenerator::Address _parse_assign_right_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::AssignmentNode *p_assignmentint, const GDScriptCodeGenerator::Address &p_index_addr = GDScriptCodeGenerator::Address());
- GDScriptCodeGenerator::Address _parse_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::ExpressionNode *p_expression, bool p_root = false, bool p_initializer = false, const GDScriptCodeGenerator::Address &p_index_addr = GDScriptCodeGenerator::Address());
+ GDScriptCodeGenerator::Address _parse_expression(CodeGen &codegen, Error &r_error, const GDScriptParser::ExpressionNode *p_expression, bool p_root = false, bool p_initializer = false);
GDScriptCodeGenerator::Address _parse_match_pattern(CodeGen &codegen, Error &r_error, const GDScriptParser::PatternNode *p_pattern, const GDScriptCodeGenerator::Address &p_value_addr, const GDScriptCodeGenerator::Address &p_type_addr, const GDScriptCodeGenerator::Address &p_previous_test, bool p_is_first, bool p_is_nested);
List<GDScriptCodeGenerator::Address> _add_locals_in_block(CodeGen &codegen, const GDScriptParser::SuiteNode *p_block);
void _clear_addresses(CodeGen &codegen, const List<GDScriptCodeGenerator::Address> &p_addresses);
diff --git a/modules/gdscript/gdscript_editor.cpp b/modules/gdscript/gdscript_editor.cpp
index 854c944e14..eaef8a961c 100644
--- a/modules/gdscript/gdscript_editor.cpp
+++ b/modules/gdscript/gdscript_editor.cpp
@@ -651,6 +651,21 @@ static int _get_enum_constant_location(const StringName &p_class, const StringNa
return depth | ScriptLanguage::LOCATION_PARENT_MASK;
}
+static int _get_enum_location(const StringName &p_class, const StringName &p_enum) {
+ if (!ClassDB::has_enum(p_class, p_enum)) {
+ return ScriptLanguage::LOCATION_OTHER;
+ }
+
+ int depth = 0;
+ StringName class_test = p_class;
+ while (class_test && !ClassDB::has_enum(class_test, p_enum, true)) {
+ class_test = ClassDB::get_parent_class(class_test);
+ depth++;
+ }
+
+ return depth | ScriptLanguage::LOCATION_PARENT_MASK;
+}
+
// END LOCATION METHODS
static String _trim_parent_class(const String &p_class, const String &p_base_class) {
@@ -1202,13 +1217,15 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base
return;
}
+ List<StringName> enums;
+ ClassDB::get_enum_list(type, &enums);
+ for (const StringName &E : enums) {
+ int location = p_recursion_depth + _get_enum_location(type, E);
+ ScriptLanguage::CodeCompletionOption option(E, ScriptLanguage::CODE_COMPLETION_KIND_ENUM, location);
+ r_result.insert(option.display, option);
+ }
+
if (p_types_only) {
- List<StringName> enums;
- ClassDB::get_enum_list(type, &enums);
- for (const StringName &E : enums) {
- ScriptLanguage::CodeCompletionOption option(E, ScriptLanguage::CODE_COMPLETION_KIND_ENUM);
- r_result.insert(option.display, option);
- }
return;
}
@@ -1268,7 +1285,20 @@ static void _find_identifiers_in_base(const GDScriptCompletionIdentifier &p_base
}
return;
} break;
- case GDScriptParser::DataType::ENUM:
+ case GDScriptParser::DataType::ENUM: {
+ String type_str = base_type.native_type;
+ StringName type = type_str.get_slicec('.', 0);
+ StringName type_enum = base_type.enum_type;
+
+ List<StringName> enum_values;
+ ClassDB::get_enum_constants(type, type_enum, &enum_values);
+ for (const StringName &E : enum_values) {
+ int location = p_recursion_depth + _get_enum_constant_location(type, E);
+ ScriptLanguage::CodeCompletionOption option(E, ScriptLanguage::CODE_COMPLETION_KIND_CONSTANT, location);
+ r_result.insert(option.display, option);
+ }
+ }
+ [[fallthrough]];
case GDScriptParser::DataType::BUILTIN: {
if (p_types_only) {
return;
diff --git a/modules/gdscript/gdscript_parser.cpp b/modules/gdscript/gdscript_parser.cpp
index d706f4e9a3..bb5d6770c6 100644
--- a/modules/gdscript/gdscript_parser.cpp
+++ b/modules/gdscript/gdscript_parser.cpp
@@ -2270,28 +2270,31 @@ GDScriptParser::PatternNode *GDScriptParser::parse_match_pattern(PatternNode *p_
break;
case GDScriptTokenizer::Token::BRACKET_OPEN: {
// Array.
+ push_multiline(true);
advance();
pattern->pattern_type = PatternNode::PT_ARRAY;
-
- if (!check(GDScriptTokenizer::Token::BRACKET_CLOSE)) {
- do {
- PatternNode *sub_pattern = parse_match_pattern(p_root_pattern != nullptr ? p_root_pattern : pattern);
- if (sub_pattern == nullptr) {
- continue;
- }
- if (pattern->rest_used) {
- push_error(R"(The ".." pattern must be the last element in the pattern array.)");
- } else if (sub_pattern->pattern_type == PatternNode::PT_REST) {
- pattern->rest_used = true;
- }
- pattern->array.push_back(sub_pattern);
- } while (match(GDScriptTokenizer::Token::COMMA));
- }
+ do {
+ if (is_at_end() || check(GDScriptTokenizer::Token::BRACKET_CLOSE)) {
+ break;
+ }
+ PatternNode *sub_pattern = parse_match_pattern(p_root_pattern != nullptr ? p_root_pattern : pattern);
+ if (sub_pattern == nullptr) {
+ continue;
+ }
+ if (pattern->rest_used) {
+ push_error(R"(The ".." pattern must be the last element in the pattern array.)");
+ } else if (sub_pattern->pattern_type == PatternNode::PT_REST) {
+ pattern->rest_used = true;
+ }
+ pattern->array.push_back(sub_pattern);
+ } while (match(GDScriptTokenizer::Token::COMMA));
consume(GDScriptTokenizer::Token::BRACKET_CLOSE, R"(Expected "]" to close the array pattern.)");
+ pop_multiline();
break;
}
case GDScriptTokenizer::Token::BRACE_OPEN: {
// Dictionary.
+ push_multiline(true);
advance();
pattern->pattern_type = PatternNode::PT_DICTIONARY;
do {
@@ -2334,6 +2337,7 @@ GDScriptParser::PatternNode *GDScriptParser::parse_match_pattern(PatternNode *p_
}
} while (match(GDScriptTokenizer::Token::COMMA));
consume(GDScriptTokenizer::Token::BRACE_CLOSE, R"(Expected "}" to close the dictionary pattern.)");
+ pop_multiline();
break;
}
default: {
@@ -2769,10 +2773,6 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_assignment(ExpressionNode
return parse_expression(false); // Return the following expression.
}
-#ifdef DEBUG_ENABLED
- VariableNode *source_variable = nullptr;
-#endif
-
switch (p_previous_operand->type) {
case Node::IDENTIFIER: {
#ifdef DEBUG_ENABLED
@@ -2781,8 +2781,6 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_assignment(ExpressionNode
IdentifierNode *id = static_cast<IdentifierNode *>(p_previous_operand);
switch (id->source) {
case IdentifierNode::LOCAL_VARIABLE:
-
- source_variable = id->variable_source;
id->variable_source->usages--;
break;
case IdentifierNode::LOCAL_CONSTANT:
@@ -2813,16 +2811,10 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_assignment(ExpressionNode
update_extents(assignment);
make_completion_context(COMPLETION_ASSIGN, assignment);
-#ifdef DEBUG_ENABLED
- bool has_operator = true;
-#endif
switch (previous.type) {
case GDScriptTokenizer::Token::EQUAL:
assignment->operation = AssignmentNode::OP_NONE;
assignment->variant_op = Variant::OP_MAX;
-#ifdef DEBUG_ENABLED
- has_operator = false;
-#endif
break;
case GDScriptTokenizer::Token::PLUS_EQUAL:
assignment->operation = AssignmentNode::OP_ADDITION;
@@ -2878,16 +2870,6 @@ GDScriptParser::ExpressionNode *GDScriptParser::parse_assignment(ExpressionNode
}
complete_extents(assignment);
-#ifdef DEBUG_ENABLED
- if (source_variable != nullptr) {
- if (has_operator && source_variable->assignments == 0) {
- push_warning(assignment, GDScriptWarning::UNASSIGNED_VARIABLE_OP_ASSIGN, source_variable->identifier->name);
- }
-
- source_variable->assignments += 1;
- }
-#endif
-
return assignment;
}
diff --git a/modules/gdscript/gdscript_parser.h b/modules/gdscript/gdscript_parser.h
index ea67f1eaff..d047fa8e46 100644
--- a/modules/gdscript/gdscript_parser.h
+++ b/modules/gdscript/gdscript_parser.h
@@ -498,6 +498,7 @@ public:
Vector<ExpressionNode *> arguments;
StringName function_name;
bool is_super = false;
+ bool is_static = false;
CallNode() {
type = CALL;
diff --git a/modules/gdscript/gdscript_tokenizer.cpp b/modules/gdscript/gdscript_tokenizer.cpp
index 2940af585d..5b1639e250 100644
--- a/modules/gdscript/gdscript_tokenizer.cpp
+++ b/modules/gdscript/gdscript_tokenizer.cpp
@@ -1455,10 +1455,11 @@ GDScriptTokenizer::Token GDScriptTokenizerText::scan() {
if (_peek() != '\n') {
return make_error("Expected new line after \"\\\".");
}
- continuation_lines.push_back(line);
_advance();
newline(false);
line_continuation = true;
+ _skip_whitespace(); // Skip whitespace/comment lines after `\`. See GH-89403.
+ continuation_lines.push_back(line);
return scan(); // Recurse to get next token.
}
diff --git a/modules/gdscript/gdscript_tokenizer_buffer.cpp b/modules/gdscript/gdscript_tokenizer_buffer.cpp
index db523ea941..e53bc5bc41 100644
--- a/modules/gdscript/gdscript_tokenizer_buffer.cpp
+++ b/modules/gdscript/gdscript_tokenizer_buffer.cpp
@@ -285,9 +285,9 @@ Vector<uint8_t> GDScriptTokenizerBuffer::parse_code_string(const String &p_code,
// Remove continuation lines from map.
for (int line : tokenizer.get_continuation_lines()) {
- if (rev_token_lines.has(line + 1)) {
- token_lines.erase(rev_token_lines[line + 1]);
- token_columns.erase(rev_token_lines[line + 1]);
+ if (rev_token_lines.has(line)) {
+ token_lines.erase(rev_token_lines[line]);
+ token_columns.erase(rev_token_lines[line]);
}
}
diff --git a/modules/gdscript/gdscript_warning.cpp b/modules/gdscript/gdscript_warning.cpp
index e7d9787eab..48a0abe617 100644
--- a/modules/gdscript/gdscript_warning.cpp
+++ b/modules/gdscript/gdscript_warning.cpp
@@ -40,7 +40,7 @@ String GDScriptWarning::get_message() const {
switch (code) {
case UNASSIGNED_VARIABLE:
CHECK_SYMBOLS(1);
- return vformat(R"(The variable "%s" was used but never assigned a value.)", symbols[0]);
+ return vformat(R"(The variable "%s" was used before being assigned a value.)", symbols[0]);
case UNASSIGNED_VARIABLE_OP_ASSIGN:
CHECK_SYMBOLS(1);
return vformat(R"(Using assignment with operation but the variable "%s" was not previously assigned a value.)", symbols[0]);
@@ -96,7 +96,7 @@ String GDScriptWarning::get_message() const {
return vformat(R"*(The method "%s()" is not present on the inferred type "%s" (but may be present on a subtype).)*", symbols[0], symbols[1]);
case UNSAFE_CAST:
CHECK_SYMBOLS(1);
- return vformat(R"(The value is cast to "%s" but has an unknown type.)", symbols[0]);
+ return vformat(R"(Casting "Variant" to "%s" is unsafe.)", symbols[0]);
case UNSAFE_CALL_ARGUMENT:
CHECK_SYMBOLS(5);
return vformat(R"*(The argument %s of the %s "%s()" requires the subtype "%s" but the supertype "%s" was provided.)*", symbols[0], symbols[1], symbols[2], symbols[3], symbols[4]);
@@ -126,6 +126,9 @@ String GDScriptWarning::get_message() const {
case INT_AS_ENUM_WITHOUT_MATCH:
CHECK_SYMBOLS(3);
return vformat(R"(Cannot %s %s as Enum "%s": no enum member has matching value.)", symbols[0], symbols[1], symbols[2]);
+ case ENUM_VARIABLE_WITHOUT_DEFAULT:
+ CHECK_SYMBOLS(1);
+ return vformat(R"(The variable "%s" has an enum type and does not set an explicit default value. The default will be set to "0".)", symbols[0]);
case EMPTY_FILE:
return "Empty script file.";
case DEPRECATED_KEYWORD:
@@ -221,6 +224,7 @@ String GDScriptWarning::get_name_from_code(Code p_code) {
"NARROWING_CONVERSION",
"INT_AS_ENUM_WITHOUT_CAST",
"INT_AS_ENUM_WITHOUT_MATCH",
+ "ENUM_VARIABLE_WITHOUT_DEFAULT",
"EMPTY_FILE",
"DEPRECATED_KEYWORD",
"RENAMED_IN_GODOT_4_HINT",
diff --git a/modules/gdscript/gdscript_warning.h b/modules/gdscript/gdscript_warning.h
index 69cc8c179f..3ad9488138 100644
--- a/modules/gdscript/gdscript_warning.h
+++ b/modules/gdscript/gdscript_warning.h
@@ -65,7 +65,7 @@ public:
INFERRED_DECLARATION, // Variable/constant/parameter has an implicitly inferred static type.
UNSAFE_PROPERTY_ACCESS, // Property not found in the detected type (but can be in subtypes).
UNSAFE_METHOD_ACCESS, // Function not found in the detected type (but can be in subtypes).
- UNSAFE_CAST, // Cast used in an unknown type.
+ UNSAFE_CAST, // Casting a `Variant` value to non-`Variant`.
UNSAFE_CALL_ARGUMENT, // Function call argument is of a supertype of the required type.
UNSAFE_VOID_RETURN, // Function returns void but returned a call to a function that can't be type checked.
RETURN_VALUE_DISCARDED, // Function call returns something but the value isn't used.
@@ -78,6 +78,7 @@ public:
NARROWING_CONVERSION, // Float value into an integer slot, precision is lost.
INT_AS_ENUM_WITHOUT_CAST, // An integer value was used as an enum value without casting.
INT_AS_ENUM_WITHOUT_MATCH, // An integer value was used as an enum value without matching enum member.
+ ENUM_VARIABLE_WITHOUT_DEFAULT, // A variable with an enum type does not have a default value. The default will be set to `0` instead of the first enum value.
EMPTY_FILE, // A script file is empty.
DEPRECATED_KEYWORD, // The keyword is deprecated and should be replaced.
RENAMED_IN_GODOT_4_HINT, // A variable or function that could not be found has been renamed in Godot 4.
@@ -129,6 +130,7 @@ public:
WARN, // NARROWING_CONVERSION
WARN, // INT_AS_ENUM_WITHOUT_CAST
WARN, // INT_AS_ENUM_WITHOUT_MATCH
+ WARN, // ENUM_VARIABLE_WITHOUT_DEFAULT
WARN, // EMPTY_FILE
WARN, // DEPRECATED_KEYWORD
WARN, // RENAMED_IN_GODOT_4_HINT
diff --git a/modules/gdscript/tests/gdscript_test_runner.cpp b/modules/gdscript/tests/gdscript_test_runner.cpp
index a0329eb8d2..e3d16eaf42 100644
--- a/modules/gdscript/tests/gdscript_test_runner.cpp
+++ b/modules/gdscript/tests/gdscript_test_runner.cpp
@@ -300,14 +300,23 @@ bool GDScriptTestRunner::make_tests_for_dir(const String &p_dir) {
#endif
String out_file = next.get_basename() + ".out";
- if (!is_generating && !dir->file_exists(out_file)) {
- ERR_FAIL_V_MSG(false, "Could not find output file for " + next);
- }
- GDScriptTest test(current_dir.path_join(next), current_dir.path_join(out_file), source_dir);
- if (binary_tokens) {
- test.set_tokenizer_mode(GDScriptTest::TOKENIZER_BUFFER);
+ ERR_FAIL_COND_V_MSG(!is_generating && !dir->file_exists(out_file), false, "Could not find output file for " + next);
+
+ if (next.ends_with(".bin.gd")) {
+ // Test text mode first.
+ GDScriptTest text_test(current_dir.path_join(next), current_dir.path_join(out_file), source_dir);
+ tests.push_back(text_test);
+ // Test binary mode even without `--use-binary-tokens`.
+ GDScriptTest bin_test(current_dir.path_join(next), current_dir.path_join(out_file), source_dir);
+ bin_test.set_tokenizer_mode(GDScriptTest::TOKENIZER_BUFFER);
+ tests.push_back(bin_test);
+ } else {
+ GDScriptTest test(current_dir.path_join(next), current_dir.path_join(out_file), source_dir);
+ if (binary_tokens) {
+ test.set_tokenizer_mode(GDScriptTest::TOKENIZER_BUFFER);
+ }
+ tests.push_back(test);
}
- tests.push_back(test);
}
}
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_array.gd b/modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_array.gd
new file mode 100644
index 0000000000..b53e814eea
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_array.gd
@@ -0,0 +1,3 @@
+func test():
+ var integer := 1
+ print(integer as Array)
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_array.out b/modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_array.out
new file mode 100644
index 0000000000..e3e82c2b7e
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_array.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Invalid cast. Cannot convert from "int" to "Array".
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_object.gd b/modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_object.gd
new file mode 100644
index 0000000000..323e367f8e
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_object.gd
@@ -0,0 +1,3 @@
+func test():
+ var integer := 1
+ print(integer as Node)
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_object.out b/modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_object.out
new file mode 100644
index 0000000000..7de40418bf
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/cast_int_to_object.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Invalid cast. Cannot convert from "int" to "Node".
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/cast_object_to_int.gd b/modules/gdscript/tests/scripts/analyzer/errors/cast_object_to_int.gd
new file mode 100644
index 0000000000..f6cd5e217e
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/cast_object_to_int.gd
@@ -0,0 +1,3 @@
+func test():
+ var object := RefCounted.new()
+ print(object as int)
diff --git a/modules/gdscript/tests/scripts/analyzer/errors/cast_object_to_int.out b/modules/gdscript/tests/scripts/analyzer/errors/cast_object_to_int.out
new file mode 100644
index 0000000000..8af0847577
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/errors/cast_object_to_int.out
@@ -0,0 +1,2 @@
+GDTEST_ANALYZER_ERROR
+Invalid cast. Cannot convert from "RefCounted" to "int".
diff --git a/modules/gdscript/tests/scripts/analyzer/features/cast_enum_to_int.gd b/modules/gdscript/tests/scripts/analyzer/features/cast_enum_to_int.gd
new file mode 100644
index 0000000000..77ef9e2073
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/cast_enum_to_int.gd
@@ -0,0 +1,9 @@
+# GH-85882
+
+enum Foo { A, B, C }
+
+func test():
+ var a := Foo.A
+ var b := a as int + 1
+ print(b)
+
diff --git a/modules/gdscript/tests/scripts/analyzer/features/cast_enum_to_int.out b/modules/gdscript/tests/scripts/analyzer/features/cast_enum_to_int.out
new file mode 100644
index 0000000000..a7f1357bb2
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/cast_enum_to_int.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+1
diff --git a/modules/gdscript/tests/scripts/analyzer/features/enum_typecheck_inner_class.gd b/modules/gdscript/tests/scripts/analyzer/features/enum_typecheck_inner_class.gd
index 1c4b19d8e0..0444051831 100644
--- a/modules/gdscript/tests/scripts/analyzer/features/enum_typecheck_inner_class.gd
+++ b/modules/gdscript/tests/scripts/analyzer/features/enum_typecheck_inner_class.gd
@@ -11,6 +11,7 @@ class InnerClass:
var e2: InnerClass.MyEnum
var e3: EnumTypecheckOuterClass.InnerClass.MyEnum
+ @warning_ignore("unassigned_variable")
print("Self ", e1, e2, e3)
e1 = MyEnum.V1
e2 = MyEnum.V1
@@ -48,6 +49,7 @@ func test_outer_from_outer():
var e1: MyEnum
var e2: EnumTypecheckOuterClass.MyEnum
+ @warning_ignore("unassigned_variable")
print("Self ", e1, e2)
e1 = MyEnum.V1
e2 = MyEnum.V1
@@ -66,6 +68,7 @@ func test_inner_from_outer():
var e1: InnerClass.MyEnum
var e2: EnumTypecheckOuterClass.InnerClass.MyEnum
+ @warning_ignore("unassigned_variable")
print("Inner ", e1, e2)
e1 = InnerClass.MyEnum.V1
e2 = InnerClass.MyEnum.V1
diff --git a/modules/gdscript/tests/scripts/analyzer/features/infer_type_on_string_format.gd b/modules/gdscript/tests/scripts/analyzer/features/infer_type_on_string_format.gd
new file mode 100644
index 0000000000..c83a3a8a14
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/infer_type_on_string_format.gd
@@ -0,0 +1,6 @@
+# GH-88082
+
+func test():
+ var x = 1
+ var message := "value: %s" % x
+ print(message)
diff --git a/modules/gdscript/tests/scripts/analyzer/features/infer_type_on_string_format.out b/modules/gdscript/tests/scripts/analyzer/features/infer_type_on_string_format.out
new file mode 100644
index 0000000000..cf6464a4c3
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/infer_type_on_string_format.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+value: 1
diff --git a/modules/gdscript/tests/scripts/analyzer/features/unassigned_builtin_typed.gd b/modules/gdscript/tests/scripts/analyzer/features/unassigned_builtin_typed.gd
new file mode 100644
index 0000000000..8099b366f3
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/unassigned_builtin_typed.gd
@@ -0,0 +1,7 @@
+# GH-88117, GH-85796
+
+func test():
+ var array: Array
+ # Should not emit unassigned warning because the Array type has a default value.
+ array.assign([1, 2, 3])
+ print(array)
diff --git a/modules/gdscript/tests/scripts/analyzer/features/unassigned_builtin_typed.out b/modules/gdscript/tests/scripts/analyzer/features/unassigned_builtin_typed.out
new file mode 100644
index 0000000000..6d85a6cc07
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/features/unassigned_builtin_typed.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+[1, 2, 3]
diff --git a/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.gd b/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.gd
index 8a1ab6f406..333950d64e 100644
--- a/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.gd
+++ b/modules/gdscript/tests/scripts/analyzer/features/warning_ignore_warnings.gd
@@ -31,8 +31,8 @@ func int_func() -> int:
func test_warnings(unused_private_class_variable):
var t = 1
- @warning_ignore("unassigned_variable")
var unassigned_variable
+ @warning_ignore("unassigned_variable")
print(unassigned_variable)
var _unassigned_variable_op_assign
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/enum_without_default_value.gd b/modules/gdscript/tests/scripts/analyzer/warnings/enum_without_default_value.gd
new file mode 100644
index 0000000000..13e3edf93f
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/enum_without_default_value.gd
@@ -0,0 +1,9 @@
+enum HasZero { A = 0, B = 1 }
+enum HasNoZero { A = 1, B = 2 }
+var has_zero: HasZero # No warning, because the default `0` is valid.
+var has_no_zero: HasNoZero # Warning, because there is no `0` in the enum.
+
+
+func test():
+ print(has_zero)
+ print(has_no_zero)
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/enum_without_default_value.out b/modules/gdscript/tests/scripts/analyzer/warnings/enum_without_default_value.out
new file mode 100644
index 0000000000..ae40e0bc8c
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/enum_without_default_value.out
@@ -0,0 +1,7 @@
+GDTEST_OK
+>> WARNING
+>> Line: 4
+>> ENUM_VARIABLE_WITHOUT_DEFAULT
+>> The variable "has_no_zero" has an enum type and does not set an explicit default value. The default will be set to "0".
+0
+0
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/unsafe_cast.gd b/modules/gdscript/tests/scripts/analyzer/warnings/unsafe_cast.gd
new file mode 100644
index 0000000000..1a6d10f8f7
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/unsafe_cast.gd
@@ -0,0 +1,24 @@
+# We don't want to execute it because of errors, just analyze.
+func no_exec_test():
+ var weak_int = 1
+ print(weak_int as Variant) # No warning.
+ print(weak_int as int)
+ print(weak_int as Node)
+
+ var weak_node = Node.new()
+ print(weak_node as Variant) # No warning.
+ print(weak_node as int)
+ print(weak_node as Node)
+
+ var weak_variant = null
+ print(weak_variant as Variant) # No warning.
+ print(weak_variant as int)
+ print(weak_variant as Node)
+
+ var hard_variant: Variant = null
+ print(hard_variant as Variant) # No warning.
+ print(hard_variant as int)
+ print(hard_variant as Node)
+
+func test():
+ pass
diff --git a/modules/gdscript/tests/scripts/analyzer/warnings/unsafe_cast.out b/modules/gdscript/tests/scripts/analyzer/warnings/unsafe_cast.out
new file mode 100644
index 0000000000..c1e683d942
--- /dev/null
+++ b/modules/gdscript/tests/scripts/analyzer/warnings/unsafe_cast.out
@@ -0,0 +1,33 @@
+GDTEST_OK
+>> WARNING
+>> Line: 5
+>> UNSAFE_CAST
+>> Casting "Variant" to "int" is unsafe.
+>> WARNING
+>> Line: 6
+>> UNSAFE_CAST
+>> Casting "Variant" to "Node" is unsafe.
+>> WARNING
+>> Line: 10
+>> UNSAFE_CAST
+>> Casting "Variant" to "int" is unsafe.
+>> WARNING
+>> Line: 11
+>> UNSAFE_CAST
+>> Casting "Variant" to "Node" is unsafe.
+>> WARNING
+>> Line: 15
+>> UNSAFE_CAST
+>> Casting "Variant" to "int" is unsafe.
+>> WARNING
+>> Line: 16
+>> UNSAFE_CAST
+>> Casting "Variant" to "Node" is unsafe.
+>> WARNING
+>> Line: 20
+>> UNSAFE_CAST
+>> Casting "Variant" to "int" is unsafe.
+>> WARNING
+>> Line: 21
+>> UNSAFE_CAST
+>> Casting "Variant" to "Node" is unsafe.
diff --git a/modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_autocomplete.cfg b/modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_autocomplete.cfg
new file mode 100644
index 0000000000..7c7b465f26
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_autocomplete.cfg
@@ -0,0 +1,9 @@
+[output]
+include=[
+ {"display": "DrawMode",
+ "location": 256},
+ {"display": "Anchor",
+ "location": 257},
+ {"display": "TextureRepeat",
+ "location": 258},
+]
diff --git a/modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_autocomplete.gd b/modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_autocomplete.gd
new file mode 100644
index 0000000000..83f4b17a86
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_autocomplete.gd
@@ -0,0 +1,4 @@
+extends Control
+
+func _ready():
+ var t = BaseButton.➡
diff --git a/modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_values_autocompletion.cfg b/modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_values_autocompletion.cfg
new file mode 100644
index 0000000000..7ccfa550e2
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_values_autocompletion.cfg
@@ -0,0 +1,5 @@
+[output]
+include=[
+ {"display": "HEURISTIC_MAX"},
+ {"display": "HEURISTIC_OCTILE"},
+]
diff --git a/modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_values_autocompletion.gd b/modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_values_autocompletion.gd
new file mode 100644
index 0000000000..99e38be6b9
--- /dev/null
+++ b/modules/gdscript/tests/scripts/completion/builtin_enum/builtin_enum_values_autocompletion.gd
@@ -0,0 +1,4 @@
+extends Control
+
+func _ready():
+ AStarGrid2D.Heuristic.➡
diff --git a/modules/gdscript/tests/scripts/parser/features/constants.out b/modules/gdscript/tests/scripts/parser/features/constants.out
index 7ec33470d3..d73c5eb7cd 100644
--- a/modules/gdscript/tests/scripts/parser/features/constants.out
+++ b/modules/gdscript/tests/scripts/parser/features/constants.out
@@ -1,33 +1 @@
GDTEST_OK
->> WARNING
->> Line: 2
->> UNUSED_LOCAL_CONSTANT
->> The local constant "_TEST" is declared but never used in the block. If this is intended, prefix it with an underscore: "__TEST".
->> WARNING
->> Line: 3
->> UNUSED_LOCAL_CONSTANT
->> The local constant "_STRING" is declared but never used in the block. If this is intended, prefix it with an underscore: "__STRING".
->> WARNING
->> Line: 4
->> UNUSED_LOCAL_CONSTANT
->> The local constant "_VECTOR" is declared but never used in the block. If this is intended, prefix it with an underscore: "__VECTOR".
->> WARNING
->> Line: 5
->> UNUSED_LOCAL_CONSTANT
->> The local constant "_ARRAY" is declared but never used in the block. If this is intended, prefix it with an underscore: "__ARRAY".
->> WARNING
->> Line: 6
->> UNUSED_LOCAL_CONSTANT
->> The local constant "_DICTIONARY" is declared but never used in the block. If this is intended, prefix it with an underscore: "__DICTIONARY".
->> WARNING
->> Line: 9
->> UNUSED_LOCAL_CONSTANT
->> The local constant "_HELLO" is declared but never used in the block. If this is intended, prefix it with an underscore: "__HELLO".
->> WARNING
->> Line: 10
->> UNUSED_LOCAL_CONSTANT
->> The local constant "_INFINITY" is declared but never used in the block. If this is intended, prefix it with an underscore: "__INFINITY".
->> WARNING
->> Line: 11
->> UNUSED_LOCAL_CONSTANT
->> The local constant "_NOT_A_NUMBER" is declared but never used in the block. If this is intended, prefix it with an underscore: "__NOT_A_NUMBER".
diff --git a/modules/gdscript/tests/scripts/parser/features/continuation_lines_comments.bin.gd b/modules/gdscript/tests/scripts/parser/features/continuation_lines_comments.bin.gd
new file mode 100644
index 0000000000..cb0bc94d2e
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/continuation_lines_comments.bin.gd
@@ -0,0 +1,12 @@
+# GH-89403
+
+func test():
+ var x := 1
+ if x == 0 \
+ # Comment.
+ # Comment.
+ and (x < 1 or x > 2) \
+ # Comment.
+ and x != 3:
+ pass
+ print("Ok")
diff --git a/modules/gdscript/tests/scripts/parser/features/continuation_lines_comments.bin.out b/modules/gdscript/tests/scripts/parser/features/continuation_lines_comments.bin.out
new file mode 100644
index 0000000000..0e9f482af4
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/continuation_lines_comments.bin.out
@@ -0,0 +1,2 @@
+GDTEST_OK
+Ok
diff --git a/modules/gdscript/tests/scripts/parser/features/match_array.gd b/modules/gdscript/tests/scripts/parser/features/match_array.gd
new file mode 100644
index 0000000000..9103092cb4
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/match_array.gd
@@ -0,0 +1,33 @@
+func foo(x):
+ match x:
+ ["value1"]:
+ print('["value1"]')
+ ["value1", "value2"]:
+ print('["value1", "value2"]')
+
+func bar(x):
+ match x:
+ [
+ "value1"
+ ]:
+ print('multiline ["value1"]')
+ [
+ "value1",
+ "value2",
+ ]:
+ print('multiline ["value1", "value2",]')
+ [
+ "value1",
+ [
+ "value2",
+ ..,
+ ],
+ ]:
+ print('multiline ["value1", ["value2", ..,],]')
+
+func test():
+ foo(["value1"])
+ foo(["value1", "value2"])
+ bar(["value1"])
+ bar(["value1", "value2"])
+ bar(["value1", ["value2", "value3"]])
diff --git a/modules/gdscript/tests/scripts/parser/features/match_array.out b/modules/gdscript/tests/scripts/parser/features/match_array.out
new file mode 100644
index 0000000000..d0111f07b1
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/features/match_array.out
@@ -0,0 +1,6 @@
+GDTEST_OK
+["value1"]
+["value1", "value2"]
+multiline ["value1"]
+multiline ["value1", "value2",]
+multiline ["value1", ["value2", ..,],]
diff --git a/modules/gdscript/tests/scripts/parser/features/match_dictionary.gd b/modules/gdscript/tests/scripts/parser/features/match_dictionary.gd
index 75857fb8ff..7685622e5a 100644
--- a/modules/gdscript/tests/scripts/parser/features/match_dictionary.gd
+++ b/modules/gdscript/tests/scripts/parser/features/match_dictionary.gd
@@ -26,6 +26,24 @@ func bar(x):
_:
print("wildcard")
+func baz(x):
+ match x:
+ {
+ "key1": "value1"
+ }:
+ print('multiline {"key1": "value1"}')
+ {
+ "key2": "value2",
+ }:
+ print('multiline {"key2": "value2",}')
+ {
+ "key3": {
+ "key1",
+ ..,
+ },
+ }:
+ print('multiline {"key3": {"key1", ..,},}')
+
func test():
foo({"key1": "value1", "key2": "value2"})
foo({"key1": "value1", "key2": ""})
@@ -41,3 +59,6 @@ func test():
bar({1: "1"})
bar({2: "2"})
bar({3: "3"})
+ baz({"key1": "value1"})
+ baz({"key2": "value2"})
+ baz({"key3": {"key1": "value1", "key2": "value2"}})
diff --git a/modules/gdscript/tests/scripts/parser/features/match_dictionary.out b/modules/gdscript/tests/scripts/parser/features/match_dictionary.out
index 4dee886927..f9adcbd331 100644
--- a/modules/gdscript/tests/scripts/parser/features/match_dictionary.out
+++ b/modules/gdscript/tests/scripts/parser/features/match_dictionary.out
@@ -13,3 +13,6 @@ wildcard
1
2
wildcard
+multiline {"key1": "value1"}
+multiline {"key2": "value2",}
+multiline {"key3": {"key1", ..,},}
diff --git a/modules/gdscript/tests/scripts/parser/features/static_typing.out b/modules/gdscript/tests/scripts/parser/features/static_typing.out
index 40a8f97416..d73c5eb7cd 100644
--- a/modules/gdscript/tests/scripts/parser/features/static_typing.out
+++ b/modules/gdscript/tests/scripts/parser/features/static_typing.out
@@ -1,21 +1 @@
GDTEST_OK
->> WARNING
->> Line: 11
->> UNUSED_LOCAL_CONSTANT
->> The local constant "_INTEGER" is declared but never used in the block. If this is intended, prefix it with an underscore: "__INTEGER".
->> WARNING
->> Line: 12
->> UNUSED_LOCAL_CONSTANT
->> The local constant "_INTEGER_REDUNDANT_TYPED" is declared but never used in the block. If this is intended, prefix it with an underscore: "__INTEGER_REDUNDANT_TYPED".
->> WARNING
->> Line: 13
->> UNUSED_LOCAL_CONSTANT
->> The local constant "_INTEGER_REDUNDANT_TYPED2" is declared but never used in the block. If this is intended, prefix it with an underscore: "__INTEGER_REDUNDANT_TYPED2".
->> WARNING
->> Line: 14
->> UNUSED_LOCAL_CONSTANT
->> The local constant "_INTEGER_REDUNDANT_INFERRED" is declared but never used in the block. If this is intended, prefix it with an underscore: "__INTEGER_REDUNDANT_INFERRED".
->> WARNING
->> Line: 15
->> UNUSED_LOCAL_CONSTANT
->> The local constant "_INTEGER_REDUNDANT_INFERRED2" is declared but never used in the block. If this is intended, prefix it with an underscore: "__INTEGER_REDUNDANT_INFERRED2".
diff --git a/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.gd b/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.gd
index afb5059eea..b38cffb754 100644
--- a/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.gd
+++ b/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.gd
@@ -1,2 +1,11 @@
func test():
- var __
+ var unassigned
+ print(unassigned)
+ unassigned = "something" # Assigned only after use.
+
+ var a
+ print(a) # Unassigned, warn.
+ if a: # Still unassigned, warn.
+ a = 1
+ print(a) # Assigned (dead code), don't warn.
+ print(a) # "Maybe" assigned, don't warn.
diff --git a/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.out b/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.out
index 10f89be132..36db304ef4 100644
--- a/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.out
+++ b/modules/gdscript/tests/scripts/parser/warnings/unassigned_variable.out
@@ -1,5 +1,16 @@
GDTEST_OK
>> WARNING
->> Line: 2
+>> Line: 3
>> UNASSIGNED_VARIABLE
->> The variable "__" was used but never assigned a value.
+>> The variable "unassigned" was used before being assigned a value.
+>> WARNING
+>> Line: 7
+>> UNASSIGNED_VARIABLE
+>> The variable "a" was used before being assigned a value.
+>> WARNING
+>> Line: 8
+>> UNASSIGNED_VARIABLE
+>> The variable "a" was used before being assigned a value.
+<null>
+<null>
+<null>
diff --git a/modules/gdscript/tests/scripts/parser/warnings/unused_constant.gd b/modules/gdscript/tests/scripts/parser/warnings/unused_constant.gd
new file mode 100644
index 0000000000..3d355197e1
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/warnings/unused_constant.gd
@@ -0,0 +1,4 @@
+func test():
+ const UNUSED = "not used"
+
+ const _UNUSED = "not used, but no warning since the constant name starts with an underscore"
diff --git a/modules/gdscript/tests/scripts/parser/warnings/unused_constant.out b/modules/gdscript/tests/scripts/parser/warnings/unused_constant.out
new file mode 100644
index 0000000000..99ced48433
--- /dev/null
+++ b/modules/gdscript/tests/scripts/parser/warnings/unused_constant.out
@@ -0,0 +1,5 @@
+GDTEST_OK
+>> WARNING
+>> Line: 2
+>> UNUSED_LOCAL_CONSTANT
+>> The local constant "UNUSED" is declared but never used in the block. If this is intended, prefix it with an underscore: "_UNUSED".
diff --git a/modules/gdscript/tests/scripts/runtime/errors/cast_freed_object.gd b/modules/gdscript/tests/scripts/runtime/errors/cast_freed_object.gd
new file mode 100644
index 0000000000..6b766f4d3d
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/cast_freed_object.gd
@@ -0,0 +1,4 @@
+func test():
+ var node := Node.new()
+ node.free()
+ print(node as Node2D)
diff --git a/modules/gdscript/tests/scripts/runtime/errors/cast_freed_object.out b/modules/gdscript/tests/scripts/runtime/errors/cast_freed_object.out
new file mode 100644
index 0000000000..90d81dd9a1
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/cast_freed_object.out
@@ -0,0 +1,6 @@
+GDTEST_RUNTIME_ERROR
+>> SCRIPT ERROR
+>> on function: test()
+>> runtime/errors/cast_freed_object.gd
+>> 4
+>> Trying to cast a freed object.
diff --git a/modules/gdscript/tests/scripts/runtime/errors/cast_int_to_array.gd b/modules/gdscript/tests/scripts/runtime/errors/cast_int_to_array.gd
new file mode 100644
index 0000000000..00817c588f
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/cast_int_to_array.gd
@@ -0,0 +1,4 @@
+func test():
+ var integer: Variant = 1
+ @warning_ignore("unsafe_cast")
+ print(integer as Array)
diff --git a/modules/gdscript/tests/scripts/runtime/errors/cast_int_to_array.out b/modules/gdscript/tests/scripts/runtime/errors/cast_int_to_array.out
new file mode 100644
index 0000000000..545d7a4906
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/cast_int_to_array.out
@@ -0,0 +1,6 @@
+GDTEST_RUNTIME_ERROR
+>> SCRIPT ERROR
+>> on function: test()
+>> runtime/errors/cast_int_to_array.gd
+>> 4
+>> Invalid cast: could not convert value to 'Array'.
diff --git a/modules/gdscript/tests/scripts/runtime/errors/cast_int_to_object.gd b/modules/gdscript/tests/scripts/runtime/errors/cast_int_to_object.gd
new file mode 100644
index 0000000000..44673a4513
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/cast_int_to_object.gd
@@ -0,0 +1,4 @@
+func test():
+ var integer: Variant = 1
+ @warning_ignore("unsafe_cast")
+ print(integer as Node)
diff --git a/modules/gdscript/tests/scripts/runtime/errors/cast_int_to_object.out b/modules/gdscript/tests/scripts/runtime/errors/cast_int_to_object.out
new file mode 100644
index 0000000000..7c39b46396
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/cast_int_to_object.out
@@ -0,0 +1,6 @@
+GDTEST_RUNTIME_ERROR
+>> SCRIPT ERROR
+>> on function: test()
+>> runtime/errors/cast_int_to_object.gd
+>> 4
+>> Invalid cast: can't convert a non-object value to an object type.
diff --git a/modules/gdscript/tests/scripts/runtime/errors/cast_object_to_int.gd b/modules/gdscript/tests/scripts/runtime/errors/cast_object_to_int.gd
new file mode 100644
index 0000000000..830d0c0c4a
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/cast_object_to_int.gd
@@ -0,0 +1,4 @@
+func test():
+ var object: Variant = RefCounted.new()
+ @warning_ignore("unsafe_cast")
+ print(object as int)
diff --git a/modules/gdscript/tests/scripts/runtime/errors/cast_object_to_int.out b/modules/gdscript/tests/scripts/runtime/errors/cast_object_to_int.out
new file mode 100644
index 0000000000..f922199fb3
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/errors/cast_object_to_int.out
@@ -0,0 +1,6 @@
+GDTEST_RUNTIME_ERROR
+>> SCRIPT ERROR
+>> on function: test()
+>> runtime/errors/cast_object_to_int.gd
+>> 4
+>> Invalid cast: could not convert value to 'int'.
diff --git a/modules/gdscript/tests/scripts/runtime/features/await_signal_with_parameters.gd b/modules/gdscript/tests/scripts/runtime/features/await_signal_with_parameters.gd
new file mode 100644
index 0000000000..ff0001676d
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/await_signal_with_parameters.gd
@@ -0,0 +1,25 @@
+signal no_parameters()
+signal one_parameter(number)
+signal two_parameters(number1, number2)
+
+func await_no_parameters():
+ var result = await no_parameters
+ print(result)
+
+func await_one_parameter():
+ var result = await one_parameter
+ print(result)
+
+func await_two_parameters():
+ var result = await two_parameters
+ print(result)
+
+func test():
+ await_no_parameters()
+ no_parameters.emit()
+
+ await_one_parameter()
+ one_parameter.emit(1)
+
+ await_two_parameters()
+ two_parameters.emit(1, 2)
diff --git a/modules/gdscript/tests/scripts/runtime/features/await_signal_with_parameters.out b/modules/gdscript/tests/scripts/runtime/features/await_signal_with_parameters.out
new file mode 100644
index 0000000000..9e203c601c
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/await_signal_with_parameters.out
@@ -0,0 +1,4 @@
+GDTEST_OK
+<null>
+1
+[1, 2]
diff --git a/modules/gdscript/tests/scripts/runtime/features/member_info.gd b/modules/gdscript/tests/scripts/runtime/features/member_info.gd
index d7485f49e6..42b29eee43 100644
--- a/modules/gdscript/tests/scripts/runtime/features/member_info.gd
+++ b/modules/gdscript/tests/scripts/runtime/features/member_info.gd
@@ -23,6 +23,7 @@ var test_var_hard_int: int
var test_var_hard_variant_type: Variant.Type
@export var test_var_hard_variant_type_exported: Variant.Type
var test_var_hard_node_process_mode: Node.ProcessMode
+@warning_ignore("enum_variable_without_default")
var test_var_hard_my_enum: MyEnum
var test_var_hard_array: Array
var test_var_hard_array_int: Array[int]
diff --git a/modules/gdscript/tests/scripts/runtime/features/reset_unassigned_variables_in_loops.gd b/modules/gdscript/tests/scripts/runtime/features/reset_unassigned_variables_in_loops.gd
index c45f8dce48..2bd5362f2a 100644
--- a/modules/gdscript/tests/scripts/runtime/features/reset_unassigned_variables_in_loops.gd
+++ b/modules/gdscript/tests/scripts/runtime/features/reset_unassigned_variables_in_loops.gd
@@ -7,6 +7,7 @@ func test():
var b
if true:
var c
+ @warning_ignore("unassigned_variable")
prints("Begin:", i, a, b, c)
a = 1
b = 1
@@ -20,6 +21,7 @@ func test():
var b
if true:
var c
+ @warning_ignore("unassigned_variable")
prints("Begin:", j, a, b, c)
a = 1
b = 1
diff --git a/modules/gdscript/tests/scripts/runtime/features/type_casting.gd b/modules/gdscript/tests/scripts/runtime/features/type_casting.gd
new file mode 100644
index 0000000000..c63ea16c32
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/type_casting.gd
@@ -0,0 +1,24 @@
+func print_value(value: Variant) -> void:
+ if value is Object:
+ @warning_ignore("unsafe_method_access")
+ print("<%s>" % value.get_class())
+ else:
+ print(var_to_str(value))
+
+func test():
+ var int_value := 1
+ print_value(int_value as Variant)
+ print_value(int_value as int)
+ print_value(int_value as float)
+
+ var node_value := Node.new()
+ print_value(node_value as Variant)
+ print_value(node_value as Object)
+ print_value(node_value as Node)
+ print_value(node_value as Node2D)
+ node_value.free()
+
+ var null_value = null
+ print_value(null_value as Variant)
+ @warning_ignore("unsafe_cast")
+ print_value(null_value as Node)
diff --git a/modules/gdscript/tests/scripts/runtime/features/type_casting.out b/modules/gdscript/tests/scripts/runtime/features/type_casting.out
new file mode 100644
index 0000000000..7da5a4c0a4
--- /dev/null
+++ b/modules/gdscript/tests/scripts/runtime/features/type_casting.out
@@ -0,0 +1,10 @@
+GDTEST_OK
+1
+1
+1.0
+<Node>
+<Node>
+<Node>
+null
+null
+null
diff --git a/modules/gltf/doc_classes/GLTFPhysicsBody.xml b/modules/gltf/doc_classes/GLTFPhysicsBody.xml
index 5cfc22f6b2..cd701e2f2f 100644
--- a/modules/gltf/doc_classes/GLTFPhysicsBody.xml
+++ b/modules/gltf/doc_classes/GLTFPhysicsBody.xml
@@ -22,7 +22,7 @@
<return type="GLTFPhysicsBody" />
<param index="0" name="body_node" type="CollisionObject3D" />
<description>
- Create a new GLTFPhysicsBody instance from the given Godot [CollisionObject3D] node.
+ Creates a new GLTFPhysicsBody instance from the given Godot [CollisionObject3D] node.
</description>
</method>
<method name="to_dictionary" qualifiers="const">
diff --git a/modules/gltf/doc_classes/GLTFPhysicsShape.xml b/modules/gltf/doc_classes/GLTFPhysicsShape.xml
index c397c660d9..a4aaf9415c 100644
--- a/modules/gltf/doc_classes/GLTFPhysicsShape.xml
+++ b/modules/gltf/doc_classes/GLTFPhysicsShape.xml
@@ -23,7 +23,14 @@
<return type="GLTFPhysicsShape" />
<param index="0" name="shape_node" type="CollisionShape3D" />
<description>
- Create a new GLTFPhysicsShape instance from the given Godot [CollisionShape3D] node.
+ Creates a new GLTFPhysicsShape instance from the given Godot [CollisionShape3D] node.
+ </description>
+ </method>
+ <method name="from_resource" qualifiers="static">
+ <return type="GLTFPhysicsShape" />
+ <param index="0" name="shape_resource" type="Shape3D" />
+ <description>
+ Creates a new GLTFPhysicsShape instance from the given Godot [Shape3D] resource.
</description>
</method>
<method name="to_dictionary" qualifiers="const">
@@ -39,6 +46,13 @@
Converts this GLTFPhysicsShape instance into a Godot [CollisionShape3D] node.
</description>
</method>
+ <method name="to_resource">
+ <return type="Shape3D" />
+ <param index="0" name="cache_shapes" type="bool" default="false" />
+ <description>
+ Converts this GLTFPhysicsShape instance into a Godot [Shape3D] resource.
+ </description>
+ </method>
</methods>
<members>
<member name="height" type="float" setter="set_height" getter="get_height" default="2.0">
diff --git a/modules/gltf/editor/editor_scene_exporter_gltf_settings.cpp b/modules/gltf/editor/editor_scene_exporter_gltf_settings.cpp
index b0283b0c55..d11300343a 100644
--- a/modules/gltf/editor/editor_scene_exporter_gltf_settings.cpp
+++ b/modules/gltf/editor/editor_scene_exporter_gltf_settings.cpp
@@ -84,6 +84,11 @@ void EditorSceneExporterGLTFSettings::_get_property_list(List<PropertyInfo> *p_l
}
}
+void EditorSceneExporterGLTFSettings::_on_extension_property_list_changed() {
+ generate_property_list(_document);
+ emit_signal("property_list_changed");
+}
+
bool EditorSceneExporterGLTFSettings::_set_extension_setting(const String &p_name_str, const Variant &p_value) {
PackedStringArray split = String(p_name_str).split("/", true, 1);
if (!_config_name_to_extension_map.has(split[0])) {
@@ -130,6 +135,10 @@ void EditorSceneExporterGLTFSettings::generate_property_list(Ref<GLTFDocument> p
String image_format_hint_string = "None,PNG,JPEG";
// Add properties from all document extensions.
for (Ref<GLTFDocumentExtension> &extension : GLTFDocument::get_all_gltf_document_extensions()) {
+ const Callable on_prop_changed = callable_mp(this, &EditorSceneExporterGLTFSettings::_on_extension_property_list_changed);
+ if (!extension->is_connected("property_list_changed", on_prop_changed)) {
+ extension->connect("property_list_changed", on_prop_changed);
+ }
const String config_prefix = get_friendly_config_prefix(extension);
_config_name_to_extension_map[config_prefix] = extension;
// If the extension allows saving in different image formats, add to the enum.
diff --git a/modules/gltf/editor/editor_scene_exporter_gltf_settings.h b/modules/gltf/editor/editor_scene_exporter_gltf_settings.h
index 6032932367..e1ce674274 100644
--- a/modules/gltf/editor/editor_scene_exporter_gltf_settings.h
+++ b/modules/gltf/editor/editor_scene_exporter_gltf_settings.h
@@ -48,6 +48,7 @@ protected:
bool _set(const StringName &p_name, const Variant &p_value);
bool _get(const StringName &p_name, Variant &r_ret) const;
void _get_property_list(List<PropertyInfo> *p_list) const;
+ void _on_extension_property_list_changed();
bool _set_extension_setting(const String &p_name_str, const Variant &p_value);
bool _get_extension_setting(const String &p_name_str, Variant &r_ret) const;
diff --git a/modules/gltf/editor/editor_scene_importer_blend.cpp b/modules/gltf/editor/editor_scene_importer_blend.cpp
index 1b71caef5b..abc89e4e97 100644
--- a/modules/gltf/editor/editor_scene_importer_blend.cpp
+++ b/modules/gltf/editor/editor_scene_importer_blend.cpp
@@ -301,10 +301,9 @@ Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_
#ifndef DISABLE_DEPRECATED
bool trimming = p_options.has("animation/trimming") ? (bool)p_options["animation/trimming"] : false;
- bool remove_immutable = p_options.has("animation/remove_immutable_tracks") ? (bool)p_options["animation/remove_immutable_tracks"] : true;
- return gltf->generate_scene(state, (float)p_options["animation/fps"], trimming, remove_immutable);
+ return gltf->generate_scene(state, (float)p_options["animation/fps"], trimming, false);
#else
- return gltf->generate_scene(state, (float)p_options["animation/fps"], (bool)p_options["animation/trimming"], (bool)p_options["animation/remove_immutable_tracks"]);
+ return gltf->generate_scene(state, (float)p_options["animation/fps"], (bool)p_options["animation/trimming"], false);
#endif
}
diff --git a/modules/gltf/editor/editor_scene_importer_gltf.cpp b/modules/gltf/editor/editor_scene_importer_gltf.cpp
index 78beaac3ed..e4ed79d15e 100644
--- a/modules/gltf/editor/editor_scene_importer_gltf.cpp
+++ b/modules/gltf/editor/editor_scene_importer_gltf.cpp
@@ -72,10 +72,9 @@ Node *EditorSceneFormatImporterGLTF::import_scene(const String &p_path, uint32_t
#ifndef DISABLE_DEPRECATED
bool trimming = p_options.has("animation/trimming") ? (bool)p_options["animation/trimming"] : false;
- bool remove_immutable = p_options.has("animation/remove_immutable_tracks") ? (bool)p_options["animation/remove_immutable_tracks"] : true;
- return gltf->generate_scene(state, (float)p_options["animation/fps"], trimming, remove_immutable);
+ return gltf->generate_scene(state, (float)p_options["animation/fps"], trimming, false);
#else
- return gltf->generate_scene(state, (float)p_options["animation/fps"], (bool)p_options["animation/trimming"], (bool)p_options["animation/remove_immutable_tracks"]);
+ return gltf->generate_scene(state, (float)p_options["animation/fps"], (bool)p_options["animation/trimming"], false);
#endif
}
diff --git a/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp b/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp
index c6e34cc023..352b439332 100644
--- a/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp
+++ b/modules/gltf/extensions/physics/gltf_document_extension_physics.cpp
@@ -355,6 +355,7 @@ void GLTFDocumentExtensionPhysics::convert_scene_node(Ref<GLTFState> p_state, Re
if (cast_to<CollisionShape3D>(p_scene_node)) {
CollisionShape3D *godot_shape = Object::cast_to<CollisionShape3D>(p_scene_node);
Ref<GLTFPhysicsShape> gltf_shape = GLTFPhysicsShape::from_node(godot_shape);
+ ERR_FAIL_COND_MSG(gltf_shape.is_null(), "GLTF Physics: Could not convert CollisionShape3D to GLTFPhysicsShape. Does it have a valid Shape3D?");
{
Ref<ImporterMesh> importer_mesh = gltf_shape->get_importer_mesh();
if (importer_mesh.is_valid()) {
diff --git a/modules/gltf/extensions/physics/gltf_physics_shape.cpp b/modules/gltf/extensions/physics/gltf_physics_shape.cpp
index 6c9ed82a69..6897bdbd3a 100644
--- a/modules/gltf/extensions/physics/gltf_physics_shape.cpp
+++ b/modules/gltf/extensions/physics/gltf_physics_shape.cpp
@@ -46,6 +46,9 @@ void GLTFPhysicsShape::_bind_methods() {
ClassDB::bind_static_method("GLTFPhysicsShape", D_METHOD("from_node", "shape_node"), &GLTFPhysicsShape::from_node);
ClassDB::bind_method(D_METHOD("to_node", "cache_shapes"), &GLTFPhysicsShape::to_node, DEFVAL(false));
+ ClassDB::bind_static_method("GLTFPhysicsShape", D_METHOD("from_resource", "shape_resource"), &GLTFPhysicsShape::from_resource);
+ ClassDB::bind_method(D_METHOD("to_resource", "cache_shapes"), &GLTFPhysicsShape::to_resource, DEFVAL(false));
+
ClassDB::bind_static_method("GLTFPhysicsShape", D_METHOD("from_dictionary", "dictionary"), &GLTFPhysicsShape::from_dictionary);
ClassDB::bind_method(D_METHOD("to_dictionary"), &GLTFPhysicsShape::to_dictionary);
@@ -159,44 +162,57 @@ Ref<ImporterMesh> _convert_hull_points_to_mesh(const Vector<Vector3> &p_hull_poi
Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_node(const CollisionShape3D *p_godot_shape_node) {
Ref<GLTFPhysicsShape> gltf_shape;
- gltf_shape.instantiate();
ERR_FAIL_NULL_V_MSG(p_godot_shape_node, gltf_shape, "Tried to create a GLTFPhysicsShape from a CollisionShape3D node, but the given node was null.");
+ Ref<Shape3D> shape_resource = p_godot_shape_node->get_shape();
+ ERR_FAIL_COND_V_MSG(shape_resource.is_null(), gltf_shape, "Tried to create a GLTFPhysicsShape from a CollisionShape3D node, but the given node had a null shape.");
+ gltf_shape = from_resource(shape_resource);
+ // Check if the shape is part of a trigger.
Node *parent = p_godot_shape_node->get_parent();
if (cast_to<const Area3D>(parent)) {
gltf_shape->set_is_trigger(true);
}
- // All the code for working with the shape is below this comment.
- Ref<Shape3D> shape_resource = p_godot_shape_node->get_shape();
- ERR_FAIL_COND_V_MSG(shape_resource.is_null(), gltf_shape, "Tried to create a GLTFPhysicsShape from a CollisionShape3D node, but the given node had a null shape.");
- gltf_shape->_shape_cache = shape_resource;
- if (cast_to<BoxShape3D>(shape_resource.ptr())) {
+ return gltf_shape;
+}
+
+CollisionShape3D *GLTFPhysicsShape::to_node(bool p_cache_shapes) {
+ CollisionShape3D *godot_shape_node = memnew(CollisionShape3D);
+ to_resource(p_cache_shapes); // Sets `_shape_cache`.
+ godot_shape_node->set_shape(_shape_cache);
+ return godot_shape_node;
+}
+
+Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_resource(const Ref<Shape3D> &p_shape_resource) {
+ Ref<GLTFPhysicsShape> gltf_shape;
+ gltf_shape.instantiate();
+ ERR_FAIL_COND_V_MSG(p_shape_resource.is_null(), gltf_shape, "Tried to create a GLTFPhysicsShape from a Shape3D resource, but the given resource was null.");
+ if (cast_to<BoxShape3D>(p_shape_resource.ptr())) {
gltf_shape->shape_type = "box";
- Ref<BoxShape3D> box = shape_resource;
+ Ref<BoxShape3D> box = p_shape_resource;
gltf_shape->set_size(box->get_size());
- } else if (cast_to<const CapsuleShape3D>(shape_resource.ptr())) {
+ } else if (cast_to<const CapsuleShape3D>(p_shape_resource.ptr())) {
gltf_shape->shape_type = "capsule";
- Ref<CapsuleShape3D> capsule = shape_resource;
+ Ref<CapsuleShape3D> capsule = p_shape_resource;
gltf_shape->set_radius(capsule->get_radius());
gltf_shape->set_height(capsule->get_height());
- } else if (cast_to<const CylinderShape3D>(shape_resource.ptr())) {
+ } else if (cast_to<const CylinderShape3D>(p_shape_resource.ptr())) {
gltf_shape->shape_type = "cylinder";
- Ref<CylinderShape3D> cylinder = shape_resource;
+ Ref<CylinderShape3D> cylinder = p_shape_resource;
gltf_shape->set_radius(cylinder->get_radius());
gltf_shape->set_height(cylinder->get_height());
- } else if (cast_to<const SphereShape3D>(shape_resource.ptr())) {
+ } else if (cast_to<const SphereShape3D>(p_shape_resource.ptr())) {
gltf_shape->shape_type = "sphere";
- Ref<SphereShape3D> sphere = shape_resource;
+ Ref<SphereShape3D> sphere = p_shape_resource;
gltf_shape->set_radius(sphere->get_radius());
- } else if (cast_to<const ConvexPolygonShape3D>(shape_resource.ptr())) {
+ } else if (cast_to<const ConvexPolygonShape3D>(p_shape_resource.ptr())) {
gltf_shape->shape_type = "convex";
- Ref<ConvexPolygonShape3D> convex = shape_resource;
+ Ref<ConvexPolygonShape3D> convex = p_shape_resource;
Vector<Vector3> hull_points = convex->get_points();
Ref<ImporterMesh> importer_mesh = _convert_hull_points_to_mesh(hull_points);
ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), gltf_shape, "GLTFPhysicsShape: Failed to convert convex hull points to a mesh.");
gltf_shape->set_importer_mesh(importer_mesh);
- } else if (cast_to<const ConcavePolygonShape3D>(shape_resource.ptr())) {
+ } else if (cast_to<const ConcavePolygonShape3D>(p_shape_resource.ptr())) {
gltf_shape->shape_type = "trimesh";
- Ref<ConcavePolygonShape3D> concave = shape_resource;
+ Ref<ConcavePolygonShape3D> concave = p_shape_resource;
Ref<ImporterMesh> importer_mesh;
importer_mesh.instantiate();
Array surface_array;
@@ -205,14 +221,14 @@ Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_node(const CollisionShape3D *p_godo
importer_mesh->add_surface(Mesh::PRIMITIVE_TRIANGLES, surface_array);
gltf_shape->set_importer_mesh(importer_mesh);
} else {
- ERR_PRINT("Tried to create a GLTFPhysicsShape from a CollisionShape3D node, but the given node's shape '" + String(Variant(shape_resource)) +
+ ERR_PRINT("Tried to create a GLTFPhysicsShape from a Shape3D, but the given shape '" + String(Variant(p_shape_resource)) +
"' had an unsupported shape type. Only BoxShape3D, CapsuleShape3D, CylinderShape3D, SphereShape3D, ConcavePolygonShape3D, and ConvexPolygonShape3D are supported.");
}
+ gltf_shape->_shape_cache = p_shape_resource;
return gltf_shape;
}
-CollisionShape3D *GLTFPhysicsShape::to_node(bool p_cache_shapes) {
- CollisionShape3D *godot_shape_node = memnew(CollisionShape3D);
+Ref<Shape3D> GLTFPhysicsShape::to_resource(bool p_cache_shapes) {
if (!p_cache_shapes || _shape_cache == nullptr) {
if (shape_type == "box") {
Ref<BoxShape3D> box;
@@ -237,19 +253,18 @@ CollisionShape3D *GLTFPhysicsShape::to_node(bool p_cache_shapes) {
sphere->set_radius(radius);
_shape_cache = sphere;
} else if (shape_type == "convex") {
- ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), godot_shape_node, "GLTFPhysicsShape: Error converting convex hull shape to a node: The mesh resource is null.");
+ ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), _shape_cache, "GLTFPhysicsShape: Error converting convex hull shape to a shape resource: The mesh resource is null.");
Ref<ConvexPolygonShape3D> convex = importer_mesh->get_mesh()->create_convex_shape();
_shape_cache = convex;
} else if (shape_type == "trimesh") {
- ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), godot_shape_node, "GLTFPhysicsShape: Error converting concave mesh shape to a node: The mesh resource is null.");
+ ERR_FAIL_COND_V_MSG(importer_mesh.is_null(), _shape_cache, "GLTFPhysicsShape: Error converting concave mesh shape to a shape resource: The mesh resource is null.");
Ref<ConcavePolygonShape3D> concave = importer_mesh->create_trimesh_shape();
_shape_cache = concave;
} else {
- ERR_PRINT("GLTFPhysicsShape: Error converting to a node: Shape type '" + shape_type + "' is unknown.");
+ ERR_PRINT("GLTFPhysicsShape: Error converting to a shape resource: Shape type '" + shape_type + "' is unknown.");
}
}
- godot_shape_node->set_shape(_shape_cache);
- return godot_shape_node;
+ return _shape_cache;
}
Ref<GLTFPhysicsShape> GLTFPhysicsShape::from_dictionary(const Dictionary p_dictionary) {
diff --git a/modules/gltf/extensions/physics/gltf_physics_shape.h b/modules/gltf/extensions/physics/gltf_physics_shape.h
index ec0a8931f1..a6974473ee 100644
--- a/modules/gltf/extensions/physics/gltf_physics_shape.h
+++ b/modules/gltf/extensions/physics/gltf_physics_shape.h
@@ -55,7 +55,7 @@ private:
bool is_trigger = false;
GLTFMeshIndex mesh_index = -1;
Ref<ImporterMesh> importer_mesh = nullptr;
- // Internal only, for caching Godot shape resources. Used in `to_node`.
+ // Internal only, for caching Godot shape resources. Used in `to_resource` and `to_node`.
Ref<Shape3D> _shape_cache = nullptr;
public:
@@ -83,6 +83,9 @@ public:
static Ref<GLTFPhysicsShape> from_node(const CollisionShape3D *p_shape_node);
CollisionShape3D *to_node(bool p_cache_shapes = false);
+ static Ref<GLTFPhysicsShape> from_resource(const Ref<Shape3D> &p_shape_resource);
+ Ref<Shape3D> to_resource(bool p_cache_shapes = false);
+
static Ref<GLTFPhysicsShape> from_dictionary(const Dictionary p_dictionary);
Dictionary to_dictionary() const;
};
diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp
index 6e7ca370dd..8f0f0d219e 100644
--- a/modules/gltf/gltf_document.cpp
+++ b/modules/gltf/gltf_document.cpp
@@ -3059,7 +3059,17 @@ Error GLTFDocument::_parse_meshes(Ref<GLTFState> p_state) {
array[Mesh::ARRAY_TANGENT] = tangents;
}
- if (p_state->force_disable_compression || !a.has("POSITION") || !a.has("NORMAL") || p.has("targets") || (a.has("JOINTS_0") || a.has("JOINTS_1"))) {
+ // Disable compression if all z equals 0 (the mesh is 2D).
+ const Vector<Vector3> &vertices = array[Mesh::ARRAY_VERTEX];
+ bool is_mesh_2d = true;
+ for (int k = 0; k < vertices.size(); k++) {
+ if (!Math::is_zero_approx(vertices[k].z)) {
+ is_mesh_2d = false;
+ break;
+ }
+ }
+
+ if (p_state->force_disable_compression || is_mesh_2d || !a.has("POSITION") || !a.has("NORMAL") || p.has("targets") || (a.has("JOINTS_0") || a.has("JOINTS_1"))) {
flags &= ~RS::ARRAY_FLAG_COMPRESS_ATTRIBUTES;
}
@@ -5675,6 +5685,9 @@ void GLTFDocument::_generate_skeleton_bone_node(Ref<GLTFState> p_state, const GL
active_skeleton = skeleton;
current_node = active_skeleton;
+ if (active_skeleton) {
+ p_scene_parent = active_skeleton;
+ }
if (requires_extra_node) {
current_node = nullptr;
@@ -7094,6 +7107,9 @@ Node *GLTFDocument::_generate_scene_node_tree(Ref<GLTFState> p_state) {
if (p_state->extensions_used.has("GODOT_single_root")) {
_generate_scene_node(p_state, 0, nullptr, nullptr);
single_root = p_state->scene_nodes[0];
+ if (single_root && single_root->get_owner() && single_root->get_owner() != single_root) {
+ single_root = single_root->get_owner();
+ }
} else {
single_root = memnew(Node3D);
for (int32_t root_i = 0; root_i < p_state->root_nodes.size(); root_i++) {
@@ -7284,6 +7300,7 @@ Node *GLTFDocument::generate_scene(Ref<GLTFState> p_state, float p_bake_fps, boo
}
Error GLTFDocument::append_from_scene(Node *p_node, Ref<GLTFState> p_state, uint32_t p_flags) {
+ ERR_FAIL_NULL_V(p_node, FAILED);
Ref<GLTFState> state = p_state;
ERR_FAIL_COND_V(state.is_null(), FAILED);
state->use_named_skin_binds = p_flags & GLTF_IMPORT_USE_NAMED_SKIN_BINDS;
diff --git a/modules/gridmap/doc_classes/GridMap.xml b/modules/gridmap/doc_classes/GridMap.xml
index 7aeecef980..ef7276f493 100644
--- a/modules/gridmap/doc_classes/GridMap.xml
+++ b/modules/gridmap/doc_classes/GridMap.xml
@@ -12,8 +12,8 @@
</description>
<tutorials>
<link title="Using gridmaps">$DOCS_URL/tutorials/3d/using_gridmaps.html</link>
- <link title="3D Platformer Demo">https://godotengine.org/asset-library/asset/125</link>
- <link title="3D Kinematic Character Demo">https://godotengine.org/asset-library/asset/126</link>
+ <link title="3D Platformer Demo">https://godotengine.org/asset-library/asset/2748</link>
+ <link title="3D Kinematic Character Demo">https://godotengine.org/asset-library/asset/2739</link>
</tutorials>
<methods>
<method name="clear">
diff --git a/modules/gridmap/grid_map.cpp b/modules/gridmap/grid_map.cpp
index fb449a67f8..25519c6e3c 100644
--- a/modules/gridmap/grid_map.cpp
+++ b/modules/gridmap/grid_map.cpp
@@ -195,7 +195,7 @@ real_t GridMap::get_collision_priority() const {
void GridMap::set_physics_material(Ref<PhysicsMaterial> p_material) {
physics_material = p_material;
- _recreate_octant_data();
+ _update_physics_bodies_characteristics();
}
Ref<PhysicsMaterial> GridMap::get_physics_material() const {
@@ -370,8 +370,8 @@ void GridMap::set_cell_item(const Vector3i &p_position, int p_item, int p_rot) {
PhysicsServer3D::get_singleton()->body_set_collision_mask(g->static_body, collision_mask);
PhysicsServer3D::get_singleton()->body_set_collision_priority(g->static_body, collision_priority);
if (physics_material.is_valid()) {
- PhysicsServer3D::get_singleton()->body_set_param(g->static_body, PhysicsServer3D::BODY_PARAM_FRICTION, physics_material->get_friction());
- PhysicsServer3D::get_singleton()->body_set_param(g->static_body, PhysicsServer3D::BODY_PARAM_BOUNCE, physics_material->get_bounce());
+ PhysicsServer3D::get_singleton()->body_set_param(g->static_body, PhysicsServer3D::BODY_PARAM_FRICTION, physics_material->computed_friction());
+ PhysicsServer3D::get_singleton()->body_set_param(g->static_body, PhysicsServer3D::BODY_PARAM_BOUNCE, physics_material->computed_bounce());
}
SceneTree *st = SceneTree::get_singleton();
@@ -748,6 +748,19 @@ void GridMap::_update_physics_bodies_collision_properties() {
}
}
+void GridMap::_update_physics_bodies_characteristics() {
+ real_t friction = 1.0;
+ real_t bounce = 0.0;
+ if (physics_material.is_valid()) {
+ friction = physics_material->computed_friction();
+ bounce = physics_material->computed_bounce();
+ }
+ for (const KeyValue<OctantKey, Octant *> &E : octant_map) {
+ PhysicsServer3D::get_singleton()->body_set_param(E.value->static_body, PhysicsServer3D::BODY_PARAM_FRICTION, friction);
+ PhysicsServer3D::get_singleton()->body_set_param(E.value->static_body, PhysicsServer3D::BODY_PARAM_BOUNCE, bounce);
+ }
+}
+
void GridMap::_octant_enter_world(const OctantKey &p_key) {
ERR_FAIL_COND(!octant_map.has(p_key));
Octant &g = *octant_map[p_key];
diff --git a/modules/gridmap/grid_map.h b/modules/gridmap/grid_map.h
index 7398a540de..1f006f661a 100644
--- a/modules/gridmap/grid_map.h
+++ b/modules/gridmap/grid_map.h
@@ -187,6 +187,7 @@ class GridMap : public Node3D {
}
void _update_physics_bodies_collision_properties();
+ void _update_physics_bodies_characteristics();
void _octant_enter_world(const OctantKey &p_key);
void _octant_exit_world(const OctantKey &p_key);
bool _octant_update(const OctantKey &p_key);
diff --git a/modules/jsonrpc/SCsub b/modules/jsonrpc/SCsub
index fe5312670a..8ee4f8bfea 100644
--- a/modules/jsonrpc/SCsub
+++ b/modules/jsonrpc/SCsub
@@ -5,3 +5,10 @@ Import("env_modules")
env_jsonrpc = env_modules.Clone()
env_jsonrpc.add_source_files(env.modules_sources, "*.cpp")
+
+if env["tests"]:
+ env_jsonrpc.Append(CPPDEFINES=["TESTS_ENABLED"])
+ env_jsonrpc.add_source_files(env.modules_sources, "./tests/*.cpp")
+
+ if env["disable_exceptions"]:
+ env_jsonrpc.Append(CPPDEFINES=["DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS"])
diff --git a/modules/jsonrpc/tests/test_jsonrpc.cpp b/modules/jsonrpc/tests/test_jsonrpc.cpp
new file mode 100644
index 0000000000..8500ff6da9
--- /dev/null
+++ b/modules/jsonrpc/tests/test_jsonrpc.cpp
@@ -0,0 +1,86 @@
+/**************************************************************************/
+/* test_jsonrpc.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 "test_jsonrpc.h"
+
+#include "core/io/json.h"
+
+namespace TestJSONRPC {
+
+void check_error_code(const Dictionary &p_dict, const JSONRPC::ErrorCode &p_code) {
+ CHECK(p_dict["jsonrpc"] == "2.0");
+ REQUIRE(p_dict.has("error"));
+ const Dictionary &err_body = p_dict["error"];
+ const int &code = err_body["code"];
+ CHECK(code == p_code);
+}
+
+void check_invalid(const Dictionary &p_dict) {
+ check_error_code(p_dict, JSONRPC::INVALID_REQUEST);
+}
+
+void check_invalid_string(const String &p_str) {
+ JSON json;
+ REQUIRE(json.parse(p_str) == OK);
+ const Dictionary &dict = json.get_data();
+ check_invalid(dict);
+}
+
+String TestClassJSONRPC::something(const String &p_in) {
+ return p_in + ", please";
+}
+
+void TestClassJSONRPC::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("something", "in"), &TestClassJSONRPC::something);
+}
+
+void test_process_action(const Variant &p_in, const Variant &p_expected, bool p_process_array_elements) {
+ TestClassJSONRPC json_rpc = TestClassJSONRPC();
+ const Variant &observed = json_rpc.process_action(p_in, p_process_array_elements);
+ CHECK(observed == p_expected);
+}
+
+void test_process_string(const String &p_in, const String &p_expected) {
+ TestClassJSONRPC json_rpc = TestClassJSONRPC();
+ const String &out_str = json_rpc.process_string(p_in);
+ CHECK(out_str == p_expected);
+}
+
+void check_error_no_method(const Dictionary &p_dict) {
+ check_error_code(p_dict, JSONRPC::METHOD_NOT_FOUND);
+}
+
+void test_process_action_bad_method(const Dictionary &p_in) {
+ TestClassJSONRPC json_rpc = TestClassJSONRPC();
+ const Dictionary &out_dict = json_rpc.process_action(p_in);
+ check_error_no_method(out_dict);
+}
+
+} // namespace TestJSONRPC
diff --git a/modules/jsonrpc/tests/test_jsonrpc.h b/modules/jsonrpc/tests/test_jsonrpc.h
new file mode 100644
index 0000000000..5770581019
--- /dev/null
+++ b/modules/jsonrpc/tests/test_jsonrpc.h
@@ -0,0 +1,139 @@
+/**************************************************************************/
+/* test_jsonrpc.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 TEST_JSONRPC_H
+#define TEST_JSONRPC_H
+
+#include "tests/test_macros.h"
+#include "tests/test_utils.h"
+
+#include "../jsonrpc.h"
+
+namespace TestJSONRPC {
+
+void check_invalid(const Dictionary &p_dict);
+
+TEST_CASE("[JSONRPC] process_action invalid") {
+ JSONRPC json_rpc = JSONRPC();
+
+ check_invalid(json_rpc.process_action("String is invalid"));
+ check_invalid(json_rpc.process_action(1234));
+ check_invalid(json_rpc.process_action(false));
+ check_invalid(json_rpc.process_action(3.14159));
+}
+
+void check_invalid_string(const String &p_str);
+
+TEST_CASE("[JSONRPC] process_string invalid") {
+ JSONRPC json_rpc = JSONRPC();
+
+ check_invalid_string(json_rpc.process_string("\"String is invalid\""));
+ check_invalid_string(json_rpc.process_string("1234"));
+ check_invalid_string(json_rpc.process_string("false"));
+ check_invalid_string(json_rpc.process_string("3.14159"));
+}
+
+class TestClassJSONRPC : public JSONRPC {
+ GDCLASS(TestClassJSONRPC, JSONRPC)
+
+public:
+ String something(const String &p_in);
+
+protected:
+ static void _bind_methods();
+};
+
+void test_process_action(const Variant &p_in, const Variant &p_expected, bool p_process_array_elements = false);
+
+TEST_CASE("[JSONRPC] process_action Dictionary") {
+ ClassDB::register_class<TestClassJSONRPC>();
+
+ Dictionary in_dict = Dictionary();
+ in_dict["method"] = "something";
+ in_dict["id"] = "ID";
+ in_dict["params"] = "yes";
+
+ Dictionary expected_dict = Dictionary();
+ expected_dict["jsonrpc"] = "2.0";
+ expected_dict["id"] = "ID";
+ expected_dict["result"] = "yes, please";
+
+ test_process_action(in_dict, expected_dict);
+}
+
+TEST_CASE("[JSONRPC] process_action Array") {
+ Array in;
+ Dictionary in_1;
+ in_1["method"] = "something";
+ in_1["id"] = 1;
+ in_1["params"] = "more";
+ in.push_back(in_1);
+ Dictionary in_2;
+ in_2["method"] = "something";
+ in_2["id"] = 2;
+ in_2["params"] = "yes";
+ in.push_back(in_2);
+
+ Array expected;
+ Dictionary expected_1;
+ expected_1["jsonrpc"] = "2.0";
+ expected_1["id"] = 1;
+ expected_1["result"] = "more, please";
+ expected.push_back(expected_1);
+ Dictionary expected_2;
+ expected_2["jsonrpc"] = "2.0";
+ expected_2["id"] = 2;
+ expected_2["result"] = "yes, please";
+ expected.push_back(expected_2);
+
+ test_process_action(in, expected, true);
+}
+
+void test_process_string(const String &p_in, const String &p_expected);
+
+TEST_CASE("[JSONRPC] process_string Dictionary") {
+ const String in = "{\"method\":\"something\",\"id\":\"ID\",\"params\":\"yes\"}";
+ const String expected = "{\"id\":\"ID\",\"jsonrpc\":\"2.0\",\"result\":\"yes, please\"}";
+
+ test_process_string(in, expected);
+}
+
+void test_process_action_bad_method(const Dictionary &p_in);
+
+TEST_CASE("[JSONRPC] process_action bad method") {
+ Dictionary in_dict;
+ in_dict["method"] = "nothing";
+
+ test_process_action_bad_method(in_dict);
+}
+
+} // namespace TestJSONRPC
+
+#endif // TEST_JSONRPC_H
diff --git a/modules/ktx/SCsub b/modules/ktx/SCsub
index acdb829979..c4cb732498 100644
--- a/modules/ktx/SCsub
+++ b/modules/ktx/SCsub
@@ -22,6 +22,7 @@ thirdparty_sources = [
"lib/texture1.c",
"lib/texture2.c",
"lib/vkformat_check.c",
+ "lib/vkformat_typesize.c",
"lib/dfdutils/createdfd.c",
"lib/dfdutils/colourspaces.c",
"lib/dfdutils/interpretdfd.c",
diff --git a/modules/lightmapper_rd/lightmapper_rd.cpp b/modules/lightmapper_rd/lightmapper_rd.cpp
index b6f5d6ce57..835fb3e59d 100644
--- a/modules/lightmapper_rd/lightmapper_rd.cpp
+++ b/modules/lightmapper_rd/lightmapper_rd.cpp
@@ -1908,7 +1908,7 @@ LightmapperRD::BakeError LightmapperRD::bake(BakeQuality p_quality, bool p_use_d
seams_push_constant.slice = uint32_t(i * subslices + k);
seams_push_constant.debug = debug;
- RD::DrawListID draw_list = rd->draw_list_begin(framebuffers[i], RD::INITIAL_ACTION_LOAD, RD::FINAL_ACTION_STORE, RD::INITIAL_ACTION_CLEAR, RD::FINAL_ACTION_DISCARD, clear_colors);
+ RD::DrawListID draw_list = rd->draw_list_begin(framebuffers[i * subslices + k], RD::INITIAL_ACTION_LOAD, RD::FINAL_ACTION_STORE, RD::INITIAL_ACTION_CLEAR, RD::FINAL_ACTION_DISCARD, clear_colors);
rd->draw_list_bind_uniform_set(draw_list, raster_base_uniform, 0);
rd->draw_list_bind_uniform_set(draw_list, blendseams_raster_uniform, 1);
diff --git a/modules/mbedtls/SCsub b/modules/mbedtls/SCsub
index 4b8f65d8ff..90ce98c751 100644
--- a/modules/mbedtls/SCsub
+++ b/modules/mbedtls/SCsub
@@ -12,24 +12,24 @@ thirdparty_obj = []
if env["builtin_mbedtls"]:
thirdparty_sources = [
"aes.c",
+ "aesce.c",
"aesni.c",
- "arc4.c",
"aria.c",
"asn1parse.c",
"asn1write.c",
"base64.c",
"bignum.c",
- "blowfish.c",
+ "bignum_core.c",
+ "bignum_mod_raw.c",
"camellia.c",
"ccm.c",
- "certs.c",
"chacha20.c",
"chachapoly.c",
"cipher.c",
"cipher_wrap.c",
"cmac.c",
- "ctr_drbg.c",
"constant_time.c",
+ "ctr_drbg.c",
"debug.c",
"des.c",
"dhm.c",
@@ -42,13 +42,10 @@ if env["builtin_mbedtls"]:
"entropy_poll.c",
"error.c",
"gcm.c",
- "havege.c",
"hkdf.c",
"hmac_drbg.c",
- "md2.c",
- "md4.c",
- "md5.c",
"md.c",
+ "md5.c",
"memory_buffer_alloc.c",
"mps_reader.c",
"mps_trace.c",
@@ -58,30 +55,37 @@ if env["builtin_mbedtls"]:
"padlock.c",
"pem.c",
"pk.c",
- "pkcs11.c",
+ "pk_ecc.c",
+ "pk_wrap.c",
"pkcs12.c",
"pkcs5.c",
+ "pkcs7.c",
"pkparse.c",
- "pk_wrap.c",
"pkwrite.c",
"platform.c",
"platform_util.c",
"poly1305.c",
"ripemd160.c",
"rsa.c",
- "rsa_internal.c",
+ "rsa_alt_helpers.c",
"sha1.c",
+ "sha3.c",
"sha256.c",
"sha512.c",
"ssl_cache.c",
"ssl_ciphersuites.c",
- "ssl_cli.c",
+ "ssl_client.c",
"ssl_cookie.c",
+ "ssl_debug_helpers_generated.c",
"ssl_msg.c",
- "ssl_srv.c",
"ssl_ticket.c",
"ssl_tls.c",
+ "ssl_tls12_client.c",
+ "ssl_tls12_server.c",
+ "ssl_tls13_client.c",
+ "ssl_tls13_generic.c",
"ssl_tls13_keys.c",
+ "ssl_tls13_server.c",
"threading.c",
"timing.c",
"version.c",
@@ -91,18 +95,18 @@ if env["builtin_mbedtls"]:
"x509_crl.c",
"x509_crt.c",
"x509_csr.c",
+ "x509write.c",
"x509write_crt.c",
"x509write_csr.c",
- "xtea.c",
]
thirdparty_dir = "#thirdparty/mbedtls/library/"
thirdparty_sources = [thirdparty_dir + file for file in thirdparty_sources]
env_mbed_tls.Prepend(CPPPATH=["#thirdparty/mbedtls/include/"])
- env_mbed_tls.Append(
- CPPDEFINES=[("MBEDTLS_CONFIG_FILE", '\\"thirdparty/mbedtls/include/godot_module_mbedtls_config.h\\"')]
- )
+ config_path = "thirdparty/mbedtls/include/godot_module_mbedtls_config.h"
+ config_path = f"<{config_path}>" if env_mbed_tls["ninja"] and env_mbed_tls.msvc else f'\\"{config_path}\\"'
+ env_mbed_tls.Append(CPPDEFINES=[("MBEDTLS_CONFIG_FILE", config_path)])
env_thirdparty = env_mbed_tls.Clone()
env_thirdparty.disable_warnings()
diff --git a/modules/mbedtls/crypto_mbedtls.cpp b/modules/mbedtls/crypto_mbedtls.cpp
index 859278d65e..e910627b32 100644
--- a/modules/mbedtls/crypto_mbedtls.cpp
+++ b/modules/mbedtls/crypto_mbedtls.cpp
@@ -69,7 +69,7 @@ Error CryptoKeyMbedTLS::load(const String &p_path, bool p_public_only) {
if (p_public_only) {
ret = mbedtls_pk_parse_public_key(&pkey, out.ptr(), out.size());
} else {
- ret = mbedtls_pk_parse_key(&pkey, out.ptr(), out.size(), nullptr, 0);
+ ret = _parse_key(out.ptr(), out.size());
}
// We MUST zeroize the memory for safety!
mbedtls_platform_zeroize(out.ptrw(), out.size());
@@ -108,7 +108,7 @@ Error CryptoKeyMbedTLS::load_from_string(const String &p_string_key, bool p_publ
if (p_public_only) {
ret = mbedtls_pk_parse_public_key(&pkey, (unsigned char *)p_string_key.utf8().get_data(), p_string_key.utf8().size());
} else {
- ret = mbedtls_pk_parse_key(&pkey, (unsigned char *)p_string_key.utf8().get_data(), p_string_key.utf8().size(), nullptr, 0);
+ ret = _parse_key((unsigned char *)p_string_key.utf8().get_data(), p_string_key.utf8().size());
}
ERR_FAIL_COND_V_MSG(ret, FAILED, "Error parsing key '" + itos(ret) + "'.");
@@ -134,6 +134,25 @@ String CryptoKeyMbedTLS::save_to_string(bool p_public_only) {
return s;
}
+int CryptoKeyMbedTLS::_parse_key(const uint8_t *p_buf, int p_size) {
+#if MBEDTLS_VERSION_MAJOR >= 3
+ mbedtls_entropy_context rng_entropy;
+ mbedtls_ctr_drbg_context rng_drbg;
+
+ mbedtls_ctr_drbg_init(&rng_drbg);
+ mbedtls_entropy_init(&rng_entropy);
+ int ret = mbedtls_ctr_drbg_seed(&rng_drbg, mbedtls_entropy_func, &rng_entropy, nullptr, 0);
+ ERR_FAIL_COND_V_MSG(ret != 0, ret, vformat("mbedtls_ctr_drbg_seed returned -0x%x\n", (unsigned int)-ret));
+
+ ret = mbedtls_pk_parse_key(&pkey, p_buf, p_size, nullptr, 0, mbedtls_ctr_drbg_random, &rng_drbg);
+ mbedtls_ctr_drbg_free(&rng_drbg);
+ mbedtls_entropy_free(&rng_entropy);
+ return ret;
+#else
+ return mbedtls_pk_parse_key(&pkey, p_buf, p_size, nullptr, 0);
+#endif
+}
+
X509Certificate *X509CertificateMbedTLS::create() {
return memnew(X509CertificateMbedTLS);
}
@@ -393,12 +412,17 @@ Ref<X509Certificate> CryptoMbedTLS::generate_self_signed_certificate(Ref<CryptoK
mbedtls_x509write_crt_set_version(&crt, MBEDTLS_X509_CRT_VERSION_3);
mbedtls_x509write_crt_set_md_alg(&crt, MBEDTLS_MD_SHA256);
+ uint8_t rand_serial[20];
+ mbedtls_ctr_drbg_random(&ctr_drbg, rand_serial, sizeof(rand_serial));
+
+#if MBEDTLS_VERSION_MAJOR >= 3
+ mbedtls_x509write_crt_set_serial_raw(&crt, rand_serial, sizeof(rand_serial));
+#else
mbedtls_mpi serial;
mbedtls_mpi_init(&serial);
- uint8_t rand_serial[20];
- mbedtls_ctr_drbg_random(&ctr_drbg, rand_serial, 20);
- ERR_FAIL_COND_V(mbedtls_mpi_read_binary(&serial, rand_serial, 20), nullptr);
+ ERR_FAIL_COND_V(mbedtls_mpi_read_binary(&serial, rand_serial, sizeof(rand_serial)), nullptr);
mbedtls_x509write_crt_set_serial(&crt, &serial);
+#endif
mbedtls_x509write_crt_set_validity(&crt, p_not_before.utf8().get_data(), p_not_after.utf8().get_data());
mbedtls_x509write_crt_set_basic_constraints(&crt, 1, -1);
@@ -407,7 +431,9 @@ Ref<X509Certificate> CryptoMbedTLS::generate_self_signed_certificate(Ref<CryptoK
unsigned char buf[4096];
memset(buf, 0, 4096);
int ret = mbedtls_x509write_crt_pem(&crt, buf, 4096, mbedtls_ctr_drbg_random, &ctr_drbg);
+#if MBEDTLS_VERSION_MAJOR < 3
mbedtls_mpi_free(&serial);
+#endif
mbedtls_x509write_crt_free(&crt);
ERR_FAIL_COND_V_MSG(ret != 0, nullptr, "Failed to generate certificate: " + itos(ret));
buf[4095] = '\0'; // Make sure strlen can't fail.
@@ -461,9 +487,17 @@ Vector<uint8_t> CryptoMbedTLS::sign(HashingContext::HashType p_hash_type, const
ERR_FAIL_COND_V_MSG(!key.is_valid(), Vector<uint8_t>(), "Invalid key provided.");
ERR_FAIL_COND_V_MSG(key->is_public_only(), Vector<uint8_t>(), "Invalid key provided. Cannot sign with public_only keys.");
size_t sig_size = 0;
+#if MBEDTLS_VERSION_MAJOR >= 3
+ unsigned char buf[MBEDTLS_PK_SIGNATURE_MAX_SIZE];
+#else
unsigned char buf[MBEDTLS_MPI_MAX_SIZE];
+#endif
Vector<uint8_t> out;
- int ret = mbedtls_pk_sign(&(key->pkey), type, p_hash.ptr(), size, buf, &sig_size, mbedtls_ctr_drbg_random, &ctr_drbg);
+ int ret = mbedtls_pk_sign(&(key->pkey), type, p_hash.ptr(), size, buf,
+#if MBEDTLS_VERSION_MAJOR >= 3
+ sizeof(buf),
+#endif
+ &sig_size, mbedtls_ctr_drbg_random, &ctr_drbg);
ERR_FAIL_COND_V_MSG(ret, out, "Error while signing: " + itos(ret));
out.resize(sig_size);
memcpy(out.ptrw(), buf, sig_size);
diff --git a/modules/mbedtls/crypto_mbedtls.h b/modules/mbedtls/crypto_mbedtls.h
index 60a413ed7c..52918cedf0 100644
--- a/modules/mbedtls/crypto_mbedtls.h
+++ b/modules/mbedtls/crypto_mbedtls.h
@@ -46,6 +46,8 @@ private:
int locks = 0;
bool public_only = true;
+ int _parse_key(const uint8_t *p_buf, int p_size);
+
public:
static CryptoKey *create();
static void make_default() { CryptoKey::_create = create; }
diff --git a/modules/mbedtls/tls_context_mbedtls.h b/modules/mbedtls/tls_context_mbedtls.h
index 29323688fb..b8a9e80b9f 100644
--- a/modules/mbedtls/tls_context_mbedtls.h
+++ b/modules/mbedtls/tls_context_mbedtls.h
@@ -36,7 +36,6 @@
#include "core/io/file_access.h"
#include "core/object/ref_counted.h"
-#include <mbedtls/config.h>
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/debug.h>
#include <mbedtls/entropy.h>
diff --git a/modules/mono/csharp_script.cpp b/modules/mono/csharp_script.cpp
index 858d1d3e4e..dcc18ebdd7 100644
--- a/modules/mono/csharp_script.cpp
+++ b/modules/mono/csharp_script.cpp
@@ -410,115 +410,6 @@ ScriptLanguage::ScriptNameCasing CSharpLanguage::preferred_file_name_casing() co
}
#ifdef TOOLS_ENABLED
-struct VariantCsName {
- Variant::Type variant_type;
- const String cs_type;
-};
-
-static String variant_type_to_managed_name(const String &p_var_type_name) {
- if (p_var_type_name.is_empty()) {
- return "Variant";
- }
-
- if (ClassDB::class_exists(p_var_type_name)) {
- return pascal_to_pascal_case(p_var_type_name);
- }
-
- if (p_var_type_name == Variant::get_type_name(Variant::OBJECT)) {
- return "GodotObject";
- }
-
- if (p_var_type_name == Variant::get_type_name(Variant::INT)) {
- return "long";
- }
-
- if (p_var_type_name == Variant::get_type_name(Variant::FLOAT)) {
- return "double";
- }
-
- if (p_var_type_name == Variant::get_type_name(Variant::STRING)) {
- return "string"; // I prefer this one >:[
- }
-
- if (p_var_type_name == Variant::get_type_name(Variant::DICTIONARY)) {
- return "Collections.Dictionary";
- }
-
- if (p_var_type_name.begins_with(Variant::get_type_name(Variant::ARRAY) + "[")) {
- String element_type = p_var_type_name.trim_prefix(Variant::get_type_name(Variant::ARRAY) + "[").trim_suffix("]");
- return "Collections.Array<" + variant_type_to_managed_name(element_type) + ">";
- }
-
- if (p_var_type_name == Variant::get_type_name(Variant::ARRAY)) {
- return "Collections.Array";
- }
-
- if (p_var_type_name == Variant::get_type_name(Variant::PACKED_BYTE_ARRAY)) {
- return "byte[]";
- }
- if (p_var_type_name == Variant::get_type_name(Variant::PACKED_INT32_ARRAY)) {
- return "int[]";
- }
- if (p_var_type_name == Variant::get_type_name(Variant::PACKED_INT64_ARRAY)) {
- return "long[]";
- }
- if (p_var_type_name == Variant::get_type_name(Variant::PACKED_FLOAT32_ARRAY)) {
- return "float[]";
- }
- if (p_var_type_name == Variant::get_type_name(Variant::PACKED_FLOAT64_ARRAY)) {
- return "double[]";
- }
- if (p_var_type_name == Variant::get_type_name(Variant::PACKED_STRING_ARRAY)) {
- return "string[]";
- }
- if (p_var_type_name == Variant::get_type_name(Variant::PACKED_VECTOR2_ARRAY)) {
- return "Vector2[]";
- }
- if (p_var_type_name == Variant::get_type_name(Variant::PACKED_VECTOR3_ARRAY)) {
- return "Vector3[]";
- }
- if (p_var_type_name == Variant::get_type_name(Variant::PACKED_COLOR_ARRAY)) {
- return "Color[]";
- }
-
- if (p_var_type_name == Variant::get_type_name(Variant::SIGNAL)) {
- return "Signal";
- }
-
- const VariantCsName var_types[] = {
- { Variant::BOOL, "bool" },
- { Variant::INT, "long" },
- { Variant::VECTOR2, "Vector2" },
- { Variant::VECTOR2I, "Vector2I" },
- { Variant::RECT2, "Rect2" },
- { Variant::RECT2I, "Rect2I" },
- { Variant::VECTOR3, "Vector3" },
- { Variant::VECTOR3I, "Vector3I" },
- { Variant::TRANSFORM2D, "Transform2D" },
- { Variant::VECTOR4, "Vector4" },
- { Variant::VECTOR4I, "Vector4I" },
- { Variant::PLANE, "Plane" },
- { Variant::QUATERNION, "Quaternion" },
- { Variant::AABB, "Aabb" },
- { Variant::BASIS, "Basis" },
- { Variant::TRANSFORM3D, "Transform3D" },
- { Variant::PROJECTION, "Projection" },
- { Variant::COLOR, "Color" },
- { Variant::STRING_NAME, "StringName" },
- { Variant::NODE_PATH, "NodePath" },
- { Variant::RID, "Rid" },
- { Variant::CALLABLE, "Callable" },
- };
-
- for (unsigned int i = 0; i < sizeof(var_types) / sizeof(VariantCsName); i++) {
- if (p_var_type_name == Variant::get_type_name(var_types[i].variant_type)) {
- return var_types[i].cs_type;
- }
- }
-
- return "Variant";
-}
-
String CSharpLanguage::make_function(const String &, const String &p_name, const PackedStringArray &p_args) const {
// The make_function() API does not work for C# scripts.
// It will always append the generated function at the very end of the script. In C#, it will break compilation by
@@ -1050,6 +941,31 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) {
to_reload_state.push_back(scr);
}
+ // Deserialize managed callables.
+ // This is done before reloading script's internal state, so potential callables invoked in properties work.
+ {
+ MutexLock lock(ManagedCallable::instances_mutex);
+
+ for (const KeyValue<ManagedCallable *, Array> &elem : ManagedCallable::instances_pending_reload) {
+ ManagedCallable *managed_callable = elem.key;
+ const Array &serialized_data = elem.value;
+
+ GCHandleIntPtr delegate = { nullptr };
+
+ bool success = GDMonoCache::managed_callbacks.DelegateUtils_TryDeserializeDelegateWithGCHandle(
+ &serialized_data, &delegate);
+
+ if (success) {
+ ERR_CONTINUE(delegate.value == nullptr);
+ managed_callable->delegate_handle = delegate;
+ } else if (OS::get_singleton()->is_stdout_verbose()) {
+ OS::get_singleton()->print("Failed to deserialize delegate\n");
+ }
+ }
+
+ ManagedCallable::instances_pending_reload.clear();
+ }
+
for (Ref<CSharpScript> &scr : to_reload_state) {
for (const ObjectID &obj_id : scr->pending_reload_instances) {
Object *obj = ObjectDB::get_instance(obj_id);
@@ -1072,7 +988,7 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) {
properties[G.first] = G.second;
}
- // Restore serialized state and call OnAfterDeserialization
+ // Restore serialized state and call OnAfterDeserialize.
GDMonoCache::managed_callbacks.CSharpInstanceBridge_DeserializeState(
csi->get_gchandle_intptr(), &properties, &state_backup.event_signals);
}
@@ -1082,30 +998,6 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) {
scr->pending_reload_state.clear();
}
- // Deserialize managed callables
- {
- MutexLock lock(ManagedCallable::instances_mutex);
-
- for (const KeyValue<ManagedCallable *, Array> &elem : ManagedCallable::instances_pending_reload) {
- ManagedCallable *managed_callable = elem.key;
- const Array &serialized_data = elem.value;
-
- GCHandleIntPtr delegate = { nullptr };
-
- bool success = GDMonoCache::managed_callbacks.DelegateUtils_TryDeserializeDelegateWithGCHandle(
- &serialized_data, &delegate);
-
- if (success) {
- ERR_CONTINUE(delegate.value == nullptr);
- managed_callable->delegate_handle = delegate;
- } else if (OS::get_singleton()->is_stdout_verbose()) {
- OS::get_singleton()->print("Failed to deserialize delegate\n");
- }
- }
-
- ManagedCallable::instances_pending_reload.clear();
- }
-
#ifdef TOOLS_ENABLED
// FIXME: Hack to refresh editor in order to display new properties and signals. See if there is a better alternative.
if (Engine::get_singleton()->is_editor_hint()) {
@@ -1408,7 +1300,11 @@ GDExtensionBool CSharpLanguage::_instance_binding_reference_callback(void *p_tok
}
void *CSharpLanguage::get_instance_binding(Object *p_object) {
- void *binding = p_object->get_instance_binding(get_singleton(), &_instance_binding_callbacks);
+ return p_object->get_instance_binding(get_singleton(), &_instance_binding_callbacks);
+}
+
+void *CSharpLanguage::get_instance_binding_with_setup(Object *p_object) {
+ void *binding = get_instance_binding(p_object);
// Initially this was in `_instance_binding_create_callback`. However, after the new instance
// binding re-write it was resulting in a deadlock in `_instance_binding_reference`, as
@@ -1433,11 +1329,7 @@ void *CSharpLanguage::get_existing_instance_binding(Object *p_object) {
#ifdef DEBUG_ENABLED
CRASH_COND(p_object->has_instance_binding(p_object));
#endif
- return p_object->get_instance_binding(get_singleton(), &_instance_binding_callbacks);
-}
-
-void CSharpLanguage::set_instance_binding(Object *p_object, void *p_binding) {
- p_object->set_instance_binding(get_singleton(), p_binding, &_instance_binding_callbacks);
+ return get_instance_binding(p_object);
}
bool CSharpLanguage::has_instance_binding(Object *p_object) {
@@ -1464,13 +1356,6 @@ void CSharpLanguage::tie_native_managed_to_unmanaged(GCHandleIntPtr p_gchandle_i
// Another reason for doing this is that this instance could outlive CSharpLanguage, which would
// be problematic when using a script. See: https://github.com/godotengine/godot/issues/25621
- CSharpScriptBinding script_binding;
-
- script_binding.inited = true;
- script_binding.type_name = *p_native_name;
- script_binding.gchandle = gchandle;
- script_binding.owner = p_unmanaged;
-
if (p_ref_counted) {
// Unsafe refcount increment. The managed instance also counts as a reference.
// This way if the unmanaged world has no references to our owner
@@ -1486,14 +1371,13 @@ void CSharpLanguage::tie_native_managed_to_unmanaged(GCHandleIntPtr p_gchandle_i
// The object was just created, no script instance binding should have been attached
CRASH_COND(CSharpLanguage::has_instance_binding(p_unmanaged));
- void *data;
- {
- MutexLock lock(CSharpLanguage::get_singleton()->get_language_bind_mutex());
- data = (void *)CSharpLanguage::get_singleton()->insert_script_binding(p_unmanaged, script_binding);
- }
+ void *binding = CSharpLanguage::get_singleton()->get_instance_binding(p_unmanaged);
- // Should be thread safe because the object was just created and nothing else should be referencing it
- CSharpLanguage::set_instance_binding(p_unmanaged, data);
+ CSharpScriptBinding &script_binding = ((RBMap<Object *, CSharpScriptBinding>::Element *)binding)->value();
+ script_binding.inited = true;
+ script_binding.type_name = *p_native_name;
+ script_binding.gchandle = gchandle;
+ script_binding.owner = p_unmanaged;
}
void CSharpLanguage::tie_user_managed_to_unmanaged(GCHandleIntPtr p_gchandle_intptr, Object *p_unmanaged, Ref<CSharpScript> *p_script, bool p_ref_counted) {
@@ -2092,7 +1976,7 @@ CSharpInstance::~CSharpInstance() {
bool die = _unreference_owner_unsafe();
CRASH_COND(die); // `owner_keep_alive` holds a reference, so it can't die
- void *data = CSharpLanguage::get_instance_binding(owner);
+ void *data = CSharpLanguage::get_instance_binding_with_setup(owner);
CRASH_COND(data == nullptr);
CSharpScriptBinding &script_binding = ((RBMap<Object *, CSharpScriptBinding>::Element *)data)->get();
CRASH_COND(!script_binding.inited);
diff --git a/modules/mono/csharp_script.h b/modules/mono/csharp_script.h
index 06d526f494..e3f39c50f4 100644
--- a/modules/mono/csharp_script.h
+++ b/modules/mono/csharp_script.h
@@ -442,7 +442,7 @@ class CSharpLanguage : public ScriptLanguage {
public:
static void *get_instance_binding(Object *p_object);
static void *get_existing_instance_binding(Object *p_object);
- static void set_instance_binding(Object *p_object, void *p_binding);
+ static void *get_instance_binding_with_setup(Object *p_object);
static bool has_instance_binding(Object *p_object);
const Mutex &get_language_bind_mutex() {
diff --git a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs
index 46020fda89..d3720dcb72 100644
--- a/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/Export/ExportPlugin.cs
@@ -90,6 +90,13 @@ namespace GodotTools.Export
$"Resource of type {Internal.CSharpLanguageType} has an invalid file extension: {path}",
nameof(path));
+ if (!ProjectContainsDotNet())
+ {
+ _maybeLastExportError = $"This project contains C# files but no solution file was found at the following path: {GodotSharpDirs.ProjectSlnPath}\n" +
+ "A solution file is required for projects with C# files. Please ensure that the solution file exists in the specified location and try again.";
+ throw new InvalidOperationException($"{path} is a C# file but no solution file exists.");
+ }
+
// TODO: What if the source file is not part of the game's C# project?
bool includeScriptsContent = (bool)GetOption("dotnet/include_scripts_content");
@@ -202,7 +209,7 @@ namespace GodotTools.Export
List<string> outputPaths = new();
- bool embedBuildResults = (bool)GetOption("dotnet/embed_build_outputs") || platform == OS.Platforms.Android;
+ bool embedBuildResults = ((bool)GetOption("dotnet/embed_build_outputs") || platform == OS.Platforms.Android) && platform != OS.Platforms.MacOS;
foreach (PublishConfig config in targets)
{
diff --git a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
index bf6cab11c7..d3899c809a 100644
--- a/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
+++ b/modules/mono/editor/GodotTools/GodotTools/GodotSharpEditor.cs
@@ -167,8 +167,8 @@ namespace GodotTools
public void ShowConfirmCreateSlnDialog()
{
- _confirmCreateSlnDialog.Title = "C# solution already exists. This will override the existing C# project file, any manual changes will be lost.".TTR();
- _confirmCreateSlnDialog.DialogText = "Create C# solution".TTR();
+ _confirmCreateSlnDialog.Title = "Create C# solution".TTR();
+ _confirmCreateSlnDialog.DialogText = "C# solution already exists. This will override the existing C# project file, any manual changes will be lost.".TTR();
EditorInterface.Singleton.PopupDialogCentered(_confirmCreateSlnDialog);
}
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs
index 9b5aec7031..fb1d32c0cb 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Array.cs
@@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Godot.NativeInterop;
+using System.Diagnostics;
#nullable enable
@@ -16,6 +17,8 @@ namespace Godot.Collections
/// interfacing with the engine. Otherwise prefer .NET collections
/// such as <see cref="System.Array"/> or <see cref="List{T}"/>.
/// </summary>
+ [DebuggerTypeProxy(typeof(ArrayDebugView<Variant>))]
+ [DebuggerDisplay("Count = {Count}")]
#pragma warning disable CA1710 // Identifiers should have correct suffix
public sealed class Array :
#pragma warning restore CA1710
@@ -1040,6 +1043,8 @@ namespace Godot.Collections
/// such as arrays or <see cref="List{T}"/>.
/// </summary>
/// <typeparam name="T">The type of the array.</typeparam>
+ [DebuggerTypeProxy(typeof(ArrayDebugView<>))]
+ [DebuggerDisplay("Count = {Count}")]
[SuppressMessage("ReSharper", "RedundantExtendsListEntry")]
[SuppressMessage("Naming", "CA1710", MessageId = "Identifiers should have correct suffix")]
public sealed class Array<[MustBeVariant] T> :
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/DebugView.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/DebugView.cs
new file mode 100644
index 0000000000..6d6eb61f8f
--- /dev/null
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/DebugView.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Godot.Collections
+{
+ internal sealed class ArrayDebugView<T>
+ {
+ private readonly IList<T> _array;
+
+ public ArrayDebugView(IList<T> array)
+ {
+ ArgumentNullException.ThrowIfNull(array);
+
+ _array = array;
+ }
+
+ [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+ public T[] Items
+ {
+ get
+ {
+ var items = new T[_array.Count];
+ _array.CopyTo(items, 0);
+ return items;
+ }
+ }
+ }
+
+ internal sealed class DictionaryDebugView<TKey, TValue>
+ {
+ private readonly IDictionary<TKey, TValue> _dictionary;
+
+ public DictionaryDebugView(IDictionary<TKey, TValue> dictionary)
+ {
+ ArgumentNullException.ThrowIfNull(dictionary);
+
+ _dictionary = dictionary;
+ }
+
+ [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+ public DictionaryKeyItemDebugView<TKey, TValue>[] Items
+ {
+ get
+ {
+ var items = new KeyValuePair<TKey, TValue>[_dictionary.Count];
+ var views = new DictionaryKeyItemDebugView<TKey, TValue>[_dictionary.Count];
+ _dictionary.CopyTo(items, 0);
+ for (int i = 0; i < items.Length; i++)
+ {
+ views[i] = new DictionaryKeyItemDebugView<TKey, TValue>(items[i]);
+ }
+ return views;
+ }
+ }
+ }
+
+ [DebuggerDisplay("{Value}", Name = "[{Key}]")]
+ internal readonly struct DictionaryKeyItemDebugView<TKey, TValue>
+ {
+ public DictionaryKeyItemDebugView(KeyValuePair<TKey, TValue> keyValue)
+ {
+ Key = keyValue.Key;
+ Value = keyValue.Value;
+ }
+
+ [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)]
+ public TKey Key { get; }
+
+ [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)]
+ public TValue Value { get; }
+ }
+}
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs
index ef3c9c79d4..7a88fea5f1 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/DelegateUtils.cs
@@ -207,7 +207,7 @@ namespace Godot
foreach (FieldInfo field in fields)
{
- Type fieldType = field.GetType();
+ Type fieldType = field.FieldType;
Variant.Type variantType = GD.TypeToVariantType(fieldType);
@@ -216,7 +216,7 @@ namespace Godot
static byte[] VarToBytes(in godot_variant var)
{
- NativeFuncs.godotsharp_var_to_bytes(var, false.ToGodotBool(), out var varBytes);
+ NativeFuncs.godotsharp_var_to_bytes(var, godot_bool.True, out var varBytes);
using (varBytes)
return Marshaling.ConvertNativePackedByteArrayToSystemArray(varBytes);
}
@@ -483,7 +483,7 @@ namespace Godot
if (fieldInfo != null)
{
- var variantValue = GD.BytesToVar(valueBuffer);
+ var variantValue = GD.BytesToVarWithObjects(valueBuffer);
object? managedValue = RuntimeTypeConversionHelper.ConvertToObjectOfType(
(godot_variant)variantValue.NativeVar, fieldInfo.FieldType);
fieldInfo.SetValue(recreatedTarget, managedValue);
@@ -799,7 +799,7 @@ namespace Godot
return func(variant);
if (typeof(GodotObject).IsAssignableFrom(type))
- return Convert.ChangeType(VariantUtils.ConvertTo<GodotObject>(variant), type, CultureInfo.InvariantCulture);
+ return VariantUtils.ConvertTo<GodotObject>(variant);
if (typeof(GodotObject[]).IsAssignableFrom(type))
{
@@ -818,7 +818,7 @@ namespace Godot
}
using var godotArray = NativeFuncs.godotsharp_variant_as_array(variant);
- return Convert.ChangeType(ConvertToSystemArrayOfGodotObject(godotArray, type), type, CultureInfo.InvariantCulture);
+ return ConvertToSystemArrayOfGodotObject(godotArray, type);
}
if (type.IsEnum)
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs
index d08fad90db..864815866a 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/Dictionary.cs
@@ -4,6 +4,7 @@ using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Godot.NativeInterop;
+using System.Diagnostics;
#nullable enable
@@ -14,6 +15,8 @@ namespace Godot.Collections
/// typed elements allocated in the engine in C++. Useful when
/// interfacing with the engine.
/// </summary>
+ [DebuggerTypeProxy(typeof(DictionaryDebugView<Variant, Variant>))]
+ [DebuggerDisplay("Count = {Count}")]
public sealed class Dictionary :
IDictionary<Variant, Variant>,
IReadOnlyDictionary<Variant, Variant>,
@@ -480,6 +483,8 @@ namespace Godot.Collections
/// </summary>
/// <typeparam name="TKey">The type of the dictionary's keys.</typeparam>
/// <typeparam name="TValue">The type of the dictionary's values.</typeparam>
+ [DebuggerTypeProxy(typeof(DictionaryDebugView<,>))]
+ [DebuggerDisplay("Count = {Count}")]
public class Dictionary<[MustBeVariant] TKey, [MustBeVariant] TValue> :
IDictionary<TKey, TValue>,
IReadOnlyDictionary<TKey, TValue>,
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs
index 9cd5498fa8..37f319b697 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs
@@ -1208,39 +1208,39 @@ namespace Godot
/// Do a simple expression match, where '*' matches zero or more
/// arbitrary characters and '?' matches any single character except '.'.
/// </summary>
- /// <param name="instance">The string to check.</param>
- /// <param name="expr">Expression to check.</param>
+ /// <param name="str">The string to check.</param>
+ /// <param name="pattern">Expression to check.</param>
/// <param name="caseSensitive">
/// If <see langword="true"/>, the check will be case sensitive.
/// </param>
/// <returns>If the expression has any matches.</returns>
- private static bool ExprMatch(this string instance, string expr, bool caseSensitive)
+ private static bool WildcardMatch(ReadOnlySpan<char> str, ReadOnlySpan<char> pattern, bool caseSensitive)
{
// case '\0':
- if (expr.Length == 0)
- return instance.Length == 0;
+ if (pattern.IsEmpty)
+ return str.IsEmpty;
- switch (expr[0])
+ switch (pattern[0])
{
case '*':
- return ExprMatch(instance, expr.Substring(1), caseSensitive) || (instance.Length > 0 &&
- ExprMatch(instance.Substring(1), expr, caseSensitive));
+ return WildcardMatch(str, pattern.Slice(1), caseSensitive)
+ || (!str.IsEmpty && WildcardMatch(str.Slice(1), pattern, caseSensitive));
case '?':
- return instance.Length > 0 && instance[0] != '.' &&
- ExprMatch(instance.Substring(1), expr.Substring(1), caseSensitive);
+ return !str.IsEmpty && str[0] != '.' &&
+ WildcardMatch(str.Slice(1), pattern.Slice(1), caseSensitive);
default:
- if (instance.Length == 0)
+ if (str.IsEmpty)
return false;
- if (caseSensitive)
- return instance[0] == expr[0];
- return (char.ToUpperInvariant(instance[0]) == char.ToUpperInvariant(expr[0])) &&
- ExprMatch(instance.Substring(1), expr.Substring(1), caseSensitive);
+ bool charMatches = caseSensitive ?
+ str[0] == pattern[0] :
+ char.ToUpperInvariant(str[0]) == char.ToUpperInvariant(pattern[0]);
+ return charMatches &&
+ WildcardMatch(str.Slice(1), pattern.Slice(1), caseSensitive);
}
}
/// <summary>
- /// Do a simple case sensitive expression match, using ? and * wildcards
- /// (see <see cref="ExprMatch(string, string, bool)"/>).
+ /// Do a simple case sensitive expression match, using ? and * wildcards.
/// </summary>
/// <seealso cref="MatchN(string, string)"/>
/// <param name="instance">The string to check.</param>
@@ -1254,12 +1254,11 @@ namespace Godot
if (instance.Length == 0 || expr.Length == 0)
return false;
- return instance.ExprMatch(expr, caseSensitive);
+ return WildcardMatch(instance, expr, caseSensitive);
}
/// <summary>
- /// Do a simple case insensitive expression match, using ? and * wildcards
- /// (see <see cref="ExprMatch(string, string, bool)"/>).
+ /// Do a simple case insensitive expression match, using ? and * wildcards.
/// </summary>
/// <seealso cref="Match(string, string, bool)"/>
/// <param name="instance">The string to check.</param>
@@ -1270,7 +1269,7 @@ namespace Godot
if (instance.Length == 0 || expr.Length == 0)
return false;
- return instance.ExprMatch(expr, caseSensitive: false);
+ return WildcardMatch(instance, expr, caseSensitive: false);
}
/// <summary>
diff --git a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj
index d54942e654..d4c11da963 100644
--- a/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj
+++ b/modules/mono/glue/GodotSharp/GodotSharp/GodotSharp.csproj
@@ -77,6 +77,7 @@
<Compile Include="Core\Color.cs" />
<Compile Include="Core\Colors.cs" />
<Compile Include="Core\DebuggingUtils.cs" />
+ <Compile Include="Core\DebugView.cs" />
<Compile Include="Core\DelegateUtils.cs" />
<Compile Include="Core\Dictionary.cs" />
<Compile Include="Core\Dispatcher.cs" />
diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp
index 0089e9c2a2..4bb324c0ee 100644
--- a/modules/mono/glue/runtime_interop.cpp
+++ b/modules/mono/glue/runtime_interop.cpp
@@ -239,7 +239,7 @@ GCHandleIntPtr godotsharp_internal_unmanaged_get_instance_binding_managed(Object
CRASH_COND(!p_unmanaged);
#endif
- void *data = CSharpLanguage::get_instance_binding(p_unmanaged);
+ void *data = CSharpLanguage::get_instance_binding_with_setup(p_unmanaged);
ERR_FAIL_NULL_V(data, { nullptr });
CSharpScriptBinding &script_binding = ((RBMap<Object *, CSharpScriptBinding>::Element *)data)->value();
ERR_FAIL_COND_V(!script_binding.inited, { nullptr });
@@ -252,7 +252,7 @@ GCHandleIntPtr godotsharp_internal_unmanaged_instance_binding_create_managed(Obj
CRASH_COND(!p_unmanaged);
#endif
- void *data = CSharpLanguage::get_instance_binding(p_unmanaged);
+ void *data = CSharpLanguage::get_instance_binding_with_setup(p_unmanaged);
ERR_FAIL_NULL_V(data, { nullptr });
CSharpScriptBinding &script_binding = ((RBMap<Object *, CSharpScriptBinding>::Element *)data)->value();
ERR_FAIL_COND_V(!script_binding.inited, { nullptr });
diff --git a/modules/mono/mono_gd/gd_mono.h b/modules/mono/mono_gd/gd_mono.h
index 1b46a619ca..0cb087db57 100644
--- a/modules/mono/mono_gd/gd_mono.h
+++ b/modules/mono/mono_gd/gd_mono.h
@@ -77,7 +77,9 @@ class GDMono {
void _try_load_project_assembly();
#endif
+#ifdef DEBUG_METHODS_ENABLED
uint64_t api_core_hash = 0;
+#endif
#ifdef TOOLS_ENABLED
uint64_t api_editor_hash = 0;
#endif
diff --git a/modules/multiplayer/editor/replication_editor.cpp b/modules/multiplayer/editor/replication_editor.cpp
index 58803124cf..8453a41473 100644
--- a/modules/multiplayer/editor/replication_editor.cpp
+++ b/modules/multiplayer/editor/replication_editor.cpp
@@ -131,7 +131,7 @@ void ReplicationEditor::_pick_new_property() {
EditorNode::get_singleton()->show_warning(TTR("Not possible to add a new property to synchronize without a root."));
return;
}
- pick_node->popup_scenetree_dialog();
+ pick_node->popup_scenetree_dialog(nullptr, current);
pick_node->get_filter_line_edit()->clear();
pick_node->get_filter_line_edit()->grab_focus();
}
@@ -600,7 +600,7 @@ void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn,
item->set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER);
item->set_cell_mode(2, TreeItem::CELL_MODE_RANGE);
item->set_range_config(2, 0, 2, 1);
- item->set_text(2, "Never,Always,On Change");
+ item->set_text(2, TTR("Never", "Replication Mode") + "," + TTR("Always", "Replication Mode") + "," + TTR("On Change", "Replication Mode"));
item->set_range(2, (int)p_mode);
item->set_editable(2, true);
}
diff --git a/modules/multiplayer/multiplayer_spawner.cpp b/modules/multiplayer/multiplayer_spawner.cpp
index 264a2e9c8e..6c0669b2f3 100644
--- a/modules/multiplayer/multiplayer_spawner.cpp
+++ b/modules/multiplayer/multiplayer_spawner.cpp
@@ -251,6 +251,7 @@ NodePath MultiplayerSpawner::get_spawn_path() const {
void MultiplayerSpawner::set_spawn_path(const NodePath &p_path) {
spawn_path = p_path;
_update_spawn_node();
+ update_configuration_warnings();
}
void MultiplayerSpawner::_track(Node *p_node, const Variant &p_argument, int p_scene_id) {
diff --git a/modules/multiplayer/multiplayer_synchronizer.cpp b/modules/multiplayer/multiplayer_synchronizer.cpp
index 02e3a11964..c2ce500af8 100644
--- a/modules/multiplayer/multiplayer_synchronizer.cpp
+++ b/modules/multiplayer/multiplayer_synchronizer.cpp
@@ -354,6 +354,7 @@ void MultiplayerSynchronizer::set_root_path(const NodePath &p_path) {
_stop();
root_path = p_path;
_start();
+ update_configuration_warnings();
}
NodePath MultiplayerSynchronizer::get_root_path() const {
diff --git a/modules/navigation/2d/godot_navigation_server_2d.cpp b/modules/navigation/2d/godot_navigation_server_2d.cpp
index 28bcd16310..bf69adc14c 100644
--- a/modules/navigation/2d/godot_navigation_server_2d.cpp
+++ b/modules/navigation/2d/godot_navigation_server_2d.cpp
@@ -229,6 +229,10 @@ bool GodotNavigationServer2D::is_baking_navigation_polygon(Ref<NavigationPolygon
#endif
}
+Vector<Vector2> GodotNavigationServer2D::simplify_path(const Vector<Vector2> &p_path, real_t p_epsilon) {
+ return vector_v3_to_v2(NavigationServer3D::get_singleton()->simplify_path(vector_v2_to_v3(p_path), p_epsilon));
+}
+
GodotNavigationServer2D::GodotNavigationServer2D() {}
GodotNavigationServer2D::~GodotNavigationServer2D() {}
@@ -385,7 +389,16 @@ bool FORWARD_1_C(agent_is_map_changed, RID, p_agent, rid_to_rid);
void FORWARD_2(agent_set_paused, RID, p_agent, bool, p_paused, rid_to_rid, bool_to_bool);
bool FORWARD_1_C(agent_get_paused, RID, p_agent, rid_to_rid);
-void FORWARD_1(free, RID, p_object, rid_to_rid);
+void GodotNavigationServer2D::free(RID p_object) {
+#ifdef CLIPPER2_ENABLED
+ if (navmesh_generator_2d && navmesh_generator_2d->owns(p_object)) {
+ navmesh_generator_2d->free(p_object);
+ return;
+ }
+#endif // CLIPPER2_ENABLED
+ NavigationServer3D::get_singleton()->free(p_object);
+}
+
void FORWARD_2(agent_set_avoidance_callback, RID, p_agent, Callable, p_callback, rid_to_rid, callable_to_callable);
bool GodotNavigationServer2D::agent_has_avoidance_callback(RID p_agent) const {
return NavigationServer3D::get_singleton()->agent_has_avoidance_callback(p_agent);
@@ -449,3 +462,20 @@ void GodotNavigationServer2D::query_path(const Ref<NavigationPathQueryParameters
p_query_result->set_path_rids(_query_result.path_rids);
p_query_result->set_path_owner_ids(_query_result.path_owner_ids);
}
+
+RID GodotNavigationServer2D::source_geometry_parser_create() {
+#ifdef CLIPPER2_ENABLED
+ if (navmesh_generator_2d) {
+ return navmesh_generator_2d->source_geometry_parser_create();
+ }
+#endif // CLIPPER2_ENABLED
+ return RID();
+}
+
+void GodotNavigationServer2D::source_geometry_parser_set_callback(RID p_parser, const Callable &p_callback) {
+#ifdef CLIPPER2_ENABLED
+ if (navmesh_generator_2d) {
+ navmesh_generator_2d->source_geometry_parser_set_callback(p_parser, p_callback);
+ }
+#endif // CLIPPER2_ENABLED
+}
diff --git a/modules/navigation/2d/godot_navigation_server_2d.h b/modules/navigation/2d/godot_navigation_server_2d.h
index a148887a65..ea77fa5e6e 100644
--- a/modules/navigation/2d/godot_navigation_server_2d.h
+++ b/modules/navigation/2d/godot_navigation_server_2d.h
@@ -252,6 +252,11 @@ public:
virtual void bake_from_source_geometry_data(const Ref<NavigationPolygon> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData2D> &p_source_geometry_data, const Callable &p_callback = Callable()) override;
virtual void bake_from_source_geometry_data_async(const Ref<NavigationPolygon> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData2D> &p_source_geometry_data, const Callable &p_callback = Callable()) override;
virtual bool is_baking_navigation_polygon(Ref<NavigationPolygon> p_navigation_polygon) const override;
+
+ virtual RID source_geometry_parser_create() override;
+ virtual void source_geometry_parser_set_callback(RID p_parser, const Callable &p_callback) override;
+
+ virtual Vector<Vector2> simplify_path(const Vector<Vector2> &p_path, real_t p_epsilon) override;
};
#endif // GODOT_NAVIGATION_SERVER_2D_H
diff --git a/modules/navigation/2d/nav_mesh_generator_2d.cpp b/modules/navigation/2d/nav_mesh_generator_2d.cpp
index 069cc0f4a8..13399b858e 100644
--- a/modules/navigation/2d/nav_mesh_generator_2d.cpp
+++ b/modules/navigation/2d/nav_mesh_generator_2d.cpp
@@ -43,9 +43,9 @@
#include "scene/resources/2d/circle_shape_2d.h"
#include "scene/resources/2d/concave_polygon_shape_2d.h"
#include "scene/resources/2d/convex_polygon_shape_2d.h"
+#include "scene/resources/2d/navigation_mesh_source_geometry_data_2d.h"
+#include "scene/resources/2d/navigation_polygon.h"
#include "scene/resources/2d/rectangle_shape_2d.h"
-#include "scene/resources/navigation_mesh_source_geometry_data_2d.h"
-#include "scene/resources/navigation_polygon.h"
#include "thirdparty/clipper2/include/clipper2/clipper.h"
#include "thirdparty/misc/polypartition.h"
@@ -53,11 +53,14 @@
NavMeshGenerator2D *NavMeshGenerator2D::singleton = nullptr;
Mutex NavMeshGenerator2D::baking_navmesh_mutex;
Mutex NavMeshGenerator2D::generator_task_mutex;
+RWLock NavMeshGenerator2D::generator_rid_rwlock;
bool NavMeshGenerator2D::use_threads = true;
bool NavMeshGenerator2D::baking_use_multiple_threads = true;
bool NavMeshGenerator2D::baking_use_high_priority_threads = true;
HashSet<Ref<NavigationPolygon>> NavMeshGenerator2D::baking_navmeshes;
HashMap<WorkerThreadPool::TaskID, NavMeshGenerator2D::NavMeshGeneratorTask2D *> NavMeshGenerator2D::generator_tasks;
+RID_Owner<NavMeshGenerator2D::NavMeshGeometryParser2D> NavMeshGenerator2D::generator_parser_owner;
+LocalVector<NavMeshGenerator2D::NavMeshGeometryParser2D *> NavMeshGenerator2D::generator_parsers;
NavMeshGenerator2D *NavMeshGenerator2D::get_singleton() {
return singleton;
@@ -72,7 +75,7 @@ NavMeshGenerator2D::NavMeshGenerator2D() {
// Using threads might cause problems on certain exports or with the Editor on certain devices.
// This is the main switch to turn threaded navmesh baking off should the need arise.
- use_threads = baking_use_multiple_threads && !Engine::get_singleton()->is_editor_hint();
+ use_threads = baking_use_multiple_threads;
}
NavMeshGenerator2D::~NavMeshGenerator2D() {
@@ -126,6 +129,13 @@ void NavMeshGenerator2D::cleanup() {
}
generator_tasks.clear();
+ generator_rid_rwlock.write_lock();
+ for (NavMeshGeometryParser2D *parser : generator_parsers) {
+ generator_parser_owner.free(parser->self);
+ }
+ generator_parsers.clear();
+ generator_rid_rwlock.write_unlock();
+
generator_task_mutex.unlock();
baking_navmesh_mutex.unlock();
}
@@ -236,6 +246,15 @@ void NavMeshGenerator2D::generator_parse_geometry_node(Ref<NavigationPolygon> p_
generator_parse_tilemap_node(p_navigation_mesh, p_source_geometry_data, p_node);
generator_parse_navigationobstacle_node(p_navigation_mesh, p_source_geometry_data, p_node);
+ generator_rid_rwlock.read_lock();
+ for (const NavMeshGeometryParser2D *parser : generator_parsers) {
+ if (!parser->callback.is_valid()) {
+ continue;
+ }
+ parser->callback.call(p_navigation_mesh, p_source_geometry_data, p_node);
+ }
+ generator_rid_rwlock.read_unlock();
+
if (p_recurse_children) {
for (int i = 0; i < p_node->get_child_count(); i++) {
generator_parse_geometry_node(p_navigation_mesh, p_source_geometry_data, p_node->get_child(i), p_recurse_children);
@@ -575,8 +594,6 @@ void NavMeshGenerator2D::generator_parse_tilemap_node(const Ref<NavigationPolygo
return;
}
- int tilemap_layer = 0; // only main tile map layer is supported
-
Ref<TileSet> tile_set = tilemap->get_tileset();
if (!tile_set.is_valid()) {
return;
@@ -589,77 +606,115 @@ void NavMeshGenerator2D::generator_parse_tilemap_node(const Ref<NavigationPolygo
return;
}
- const Transform2D tilemap_xform = p_source_geometry_data->root_node_transform * tilemap->get_global_transform();
- TypedArray<Vector2i> used_cells = tilemap->get_used_cells(tilemap_layer);
+ HashSet<Vector2i> cells_with_navigation_polygon;
+ HashSet<Vector2i> cells_with_collision_polygon;
- for (int used_cell_index = 0; used_cell_index < used_cells.size(); used_cell_index++) {
- const Vector2i &cell = used_cells[used_cell_index];
+ const Transform2D tilemap_xform = p_source_geometry_data->root_node_transform * tilemap->get_global_transform();
- const TileData *tile_data = tilemap->get_cell_tile_data(tilemap_layer, cell, false);
- if (tile_data == nullptr) {
- continue;
- }
+#ifdef DEBUG_ENABLED
+ int error_print_counter = 0;
+ int error_print_max = 10;
+#endif // DEBUG_ENABLED
- // Transform flags.
- const int alternative_id = tilemap->get_cell_alternative_tile(tilemap_layer, cell, false);
- bool flip_h = (alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_H);
- bool flip_v = (alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_V);
- bool transpose = (alternative_id & TileSetAtlasSource::TRANSFORM_TRANSPOSE);
+ for (int tilemap_layer = 0; tilemap_layer < tilemap->get_layers_count(); tilemap_layer++) {
+ TypedArray<Vector2i> used_cells = tilemap->get_used_cells(tilemap_layer);
- Transform2D tile_transform;
- tile_transform.set_origin(tilemap->map_to_local(cell));
+ for (int used_cell_index = 0; used_cell_index < used_cells.size(); used_cell_index++) {
+ const Vector2i &cell = used_cells[used_cell_index];
- const Transform2D tile_transform_offset = tilemap_xform * tile_transform;
+ const TileData *tile_data = tilemap->get_cell_tile_data(tilemap_layer, cell, false);
+ if (tile_data == nullptr) {
+ continue;
+ }
- if (navigation_layers_count > 0) {
- Ref<NavigationPolygon> navigation_polygon = tile_data->get_navigation_polygon(tilemap_layer, flip_h, flip_v, transpose);
- if (navigation_polygon.is_valid()) {
- for (int outline_index = 0; outline_index < navigation_polygon->get_outline_count(); outline_index++) {
- const Vector<Vector2> &navigation_polygon_outline = navigation_polygon->get_outline(outline_index);
- if (navigation_polygon_outline.size() == 0) {
- continue;
+ // Transform flags.
+ const int alternative_id = tilemap->get_cell_alternative_tile(tilemap_layer, cell, false);
+ bool flip_h = (alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_H);
+ bool flip_v = (alternative_id & TileSetAtlasSource::TRANSFORM_FLIP_V);
+ bool transpose = (alternative_id & TileSetAtlasSource::TRANSFORM_TRANSPOSE);
+
+ Transform2D tile_transform;
+ tile_transform.set_origin(tilemap->map_to_local(cell));
+
+ const Transform2D tile_transform_offset = tilemap_xform * tile_transform;
+
+ if (navigation_layers_count > 0) {
+ Ref<NavigationPolygon> navigation_polygon = tile_data->get_navigation_polygon(tilemap_layer, flip_h, flip_v, transpose);
+ if (navigation_polygon.is_valid()) {
+ if (cells_with_navigation_polygon.has(cell)) {
+#ifdef DEBUG_ENABLED
+ error_print_counter++;
+ if (error_print_counter <= error_print_max) {
+ WARN_PRINT(vformat("TileMap navigation mesh baking error. The TileMap cell key Vector2i(%s, %s) has navigation mesh from 2 or more different TileMap layers assigned. This can cause unexpected navigation mesh baking results. The duplicated cell data was ignored.", cell.x, cell.y));
+ }
+#endif // DEBUG_ENABLED
+ } else {
+ cells_with_navigation_polygon.insert(cell);
+
+ for (int outline_index = 0; outline_index < navigation_polygon->get_outline_count(); outline_index++) {
+ const Vector<Vector2> &navigation_polygon_outline = navigation_polygon->get_outline(outline_index);
+ if (navigation_polygon_outline.size() == 0) {
+ continue;
+ }
+
+ Vector<Vector2> traversable_outline;
+ traversable_outline.resize(navigation_polygon_outline.size());
+
+ const Vector2 *navigation_polygon_outline_ptr = navigation_polygon_outline.ptr();
+ Vector2 *traversable_outline_ptrw = traversable_outline.ptrw();
+
+ for (int traversable_outline_index = 0; traversable_outline_index < traversable_outline.size(); traversable_outline_index++) {
+ traversable_outline_ptrw[traversable_outline_index] = tile_transform_offset.xform(navigation_polygon_outline_ptr[traversable_outline_index]);
+ }
+
+ p_source_geometry_data->_add_traversable_outline(traversable_outline);
+ }
}
+ }
+ }
- Vector<Vector2> traversable_outline;
- traversable_outline.resize(navigation_polygon_outline.size());
-
- const Vector2 *navigation_polygon_outline_ptr = navigation_polygon_outline.ptr();
- Vector2 *traversable_outline_ptrw = traversable_outline.ptrw();
-
- for (int traversable_outline_index = 0; traversable_outline_index < traversable_outline.size(); traversable_outline_index++) {
- traversable_outline_ptrw[traversable_outline_index] = tile_transform_offset.xform(navigation_polygon_outline_ptr[traversable_outline_index]);
+ if (physics_layers_count > 0 && (parsed_geometry_type == NavigationPolygon::PARSED_GEOMETRY_STATIC_COLLIDERS || parsed_geometry_type == NavigationPolygon::PARSED_GEOMETRY_BOTH) && (tile_set->get_physics_layer_collision_layer(tilemap_layer) & parsed_collision_mask)) {
+ if (cells_with_collision_polygon.has(cell)) {
+#ifdef DEBUG_ENABLED
+ error_print_counter++;
+ if (error_print_counter <= error_print_max) {
+ WARN_PRINT(vformat("TileMap navigation mesh baking error. The cell key Vector2i(%s, %s) has collision polygons from 2 or more different TileMap layers assigned that all match the parsed collision mask. This can cause unexpected navigation mesh baking results. The duplicated cell data was ignored.", cell.x, cell.y));
}
+#endif // DEBUG_ENABLED
+ } else {
+ cells_with_collision_polygon.insert(cell);
- p_source_geometry_data->_add_traversable_outline(traversable_outline);
- }
- }
- }
+ for (int collision_polygon_index = 0; collision_polygon_index < tile_data->get_collision_polygons_count(tilemap_layer); collision_polygon_index++) {
+ PackedVector2Array collision_polygon_points = tile_data->get_collision_polygon_points(tilemap_layer, collision_polygon_index);
+ if (collision_polygon_points.size() == 0) {
+ continue;
+ }
- if (physics_layers_count > 0 && (parsed_geometry_type == NavigationPolygon::PARSED_GEOMETRY_STATIC_COLLIDERS || parsed_geometry_type == NavigationPolygon::PARSED_GEOMETRY_BOTH) && (tile_set->get_physics_layer_collision_layer(tilemap_layer) & parsed_collision_mask)) {
- for (int collision_polygon_index = 0; collision_polygon_index < tile_data->get_collision_polygons_count(tilemap_layer); collision_polygon_index++) {
- PackedVector2Array collision_polygon_points = tile_data->get_collision_polygon_points(tilemap_layer, collision_polygon_index);
- if (collision_polygon_points.size() == 0) {
- continue;
- }
+ if (flip_h || flip_v || transpose) {
+ collision_polygon_points = TileData::get_transformed_vertices(collision_polygon_points, flip_h, flip_v, transpose);
+ }
- if (flip_h || flip_v || transpose) {
- collision_polygon_points = TileData::get_transformed_vertices(collision_polygon_points, flip_h, flip_v, transpose);
- }
+ Vector<Vector2> obstruction_outline;
+ obstruction_outline.resize(collision_polygon_points.size());
- Vector<Vector2> obstruction_outline;
- obstruction_outline.resize(collision_polygon_points.size());
+ const Vector2 *collision_polygon_points_ptr = collision_polygon_points.ptr();
+ Vector2 *obstruction_outline_ptrw = obstruction_outline.ptrw();
- const Vector2 *collision_polygon_points_ptr = collision_polygon_points.ptr();
- Vector2 *obstruction_outline_ptrw = obstruction_outline.ptrw();
+ for (int obstruction_outline_index = 0; obstruction_outline_index < obstruction_outline.size(); obstruction_outline_index++) {
+ obstruction_outline_ptrw[obstruction_outline_index] = tile_transform_offset.xform(collision_polygon_points_ptr[obstruction_outline_index]);
+ }
- for (int obstruction_outline_index = 0; obstruction_outline_index < obstruction_outline.size(); obstruction_outline_index++) {
- obstruction_outline_ptrw[obstruction_outline_index] = tile_transform_offset.xform(collision_polygon_points_ptr[obstruction_outline_index]);
+ p_source_geometry_data->_add_obstruction_outline(obstruction_outline);
+ }
}
-
- p_source_geometry_data->_add_obstruction_outline(obstruction_outline);
}
}
}
+#ifdef DEBUG_ENABLED
+ if (error_print_counter > error_print_max) {
+ ERR_PRINT(vformat("TileMap navigation mesh baking error. A total of %s cells with navigation or collision polygons from 2 or more different TileMap layers overlap. This can cause unexpected navigation mesh baking results. The duplicated cell data was ignored.", error_print_counter));
+ }
+#endif // DEBUG_ENABLED
}
void NavMeshGenerator2D::generator_parse_navigationobstacle_node(const Ref<NavigationPolygon> &p_navigation_mesh, Ref<NavigationMeshSourceGeometryData2D> p_source_geometry_data, Node *p_node) {
@@ -777,6 +832,47 @@ bool NavMeshGenerator2D::generator_emit_callback(const Callable &p_callback) {
return ce.error == Callable::CallError::CALL_OK;
}
+RID NavMeshGenerator2D::source_geometry_parser_create() {
+ RWLockWrite write_lock(generator_rid_rwlock);
+
+ RID rid = generator_parser_owner.make_rid();
+
+ NavMeshGeometryParser2D *parser = generator_parser_owner.get_or_null(rid);
+ parser->self = rid;
+
+ generator_parsers.push_back(parser);
+
+ return rid;
+}
+
+void NavMeshGenerator2D::source_geometry_parser_set_callback(RID p_parser, const Callable &p_callback) {
+ RWLockWrite write_lock(generator_rid_rwlock);
+
+ NavMeshGeometryParser2D *parser = generator_parser_owner.get_or_null(p_parser);
+ ERR_FAIL_NULL(parser);
+
+ parser->callback = p_callback;
+}
+
+bool NavMeshGenerator2D::owns(RID p_object) {
+ RWLockRead read_lock(generator_rid_rwlock);
+ return generator_parser_owner.owns(p_object);
+}
+
+void NavMeshGenerator2D::free(RID p_object) {
+ RWLockWrite write_lock(generator_rid_rwlock);
+
+ if (generator_parser_owner.owns(p_object)) {
+ NavMeshGeometryParser2D *parser = generator_parser_owner.get_or_null(p_object);
+
+ generator_parsers.erase(parser);
+
+ generator_parser_owner.free(p_object);
+ } else {
+ ERR_PRINT("Attempted to free a NavMeshGenerator2D RID that did not exist (or was already freed).");
+ }
+}
+
void NavMeshGenerator2D::generator_bake_from_source_geometry_data(Ref<NavigationPolygon> p_navigation_mesh, Ref<NavigationMeshSourceGeometryData2D> p_source_geometry_data) {
if (p_navigation_mesh.is_null() || p_source_geometry_data.is_null()) {
return;
diff --git a/modules/navigation/2d/nav_mesh_generator_2d.h b/modules/navigation/2d/nav_mesh_generator_2d.h
index 2567a170ef..235a84d548 100644
--- a/modules/navigation/2d/nav_mesh_generator_2d.h
+++ b/modules/navigation/2d/nav_mesh_generator_2d.h
@@ -35,6 +35,7 @@
#include "core/object/class_db.h"
#include "core/object/worker_thread_pool.h"
+#include "core/templates/rid_owner.h"
class Node;
class NavigationPolygon;
@@ -46,6 +47,14 @@ class NavMeshGenerator2D : public Object {
static Mutex baking_navmesh_mutex;
static Mutex generator_task_mutex;
+ static RWLock generator_rid_rwlock;
+ struct NavMeshGeometryParser2D {
+ RID self;
+ Callable callback;
+ };
+ static RID_Owner<NavMeshGeometryParser2D> generator_parser_owner;
+ static LocalVector<NavMeshGeometryParser2D *> generator_parsers;
+
static bool use_threads;
static bool baking_use_multiple_threads;
static bool baking_use_high_priority_threads;
@@ -97,6 +106,12 @@ public:
static void bake_from_source_geometry_data_async(Ref<NavigationPolygon> p_navigation_mesh, Ref<NavigationMeshSourceGeometryData2D> p_source_geometry_data, const Callable &p_callback = Callable());
static bool is_baking(Ref<NavigationPolygon> p_navigation_polygon);
+ static RID source_geometry_parser_create();
+ static void source_geometry_parser_set_callback(RID p_parser, const Callable &p_callback);
+
+ static bool owns(RID p_object);
+ static void free(RID p_object);
+
NavMeshGenerator2D();
~NavMeshGenerator2D();
};
diff --git a/modules/navigation/3d/godot_navigation_server_3d.cpp b/modules/navigation/3d/godot_navigation_server_3d.cpp
index 15de33f58f..61a128e004 100644
--- a/modules/navigation/3d/godot_navigation_server_3d.cpp
+++ b/modules/navigation/3d/godot_navigation_server_3d.cpp
@@ -1202,6 +1202,11 @@ COMMAND_1(free, RID, p_object) {
} else if (obstacle_owner.owns(p_object)) {
internal_free_obstacle(p_object);
+#ifndef _3D_DISABLED
+ } else if (navmesh_generator_3d && navmesh_generator_3d->owns(p_object)) {
+ navmesh_generator_3d->free(p_object);
+#endif // _3D_DISABLED
+
} else {
ERR_PRINT("Attempted to free a NavigationServer RID that did not exist (or was already freed).");
}
@@ -1381,11 +1386,157 @@ PathQueryResult GodotNavigationServer3D::_query_path(const PathQueryParameters &
// add path postprocessing
+ if (r_query_result.path.size() > 2 && p_parameters.simplify_path) {
+ const LocalVector<uint32_t> &simplified_path_indices = get_simplified_path_indices(r_query_result.path, p_parameters.simplify_epsilon);
+
+ uint32_t indices_count = simplified_path_indices.size();
+
+ {
+ Vector3 *w = r_query_result.path.ptrw();
+ const Vector3 *r = r_query_result.path.ptr();
+ for (uint32_t i = 0; i < indices_count; i++) {
+ w[i] = r[simplified_path_indices[i]];
+ }
+ r_query_result.path.resize(indices_count);
+ }
+
+ if (p_parameters.metadata_flags.has_flag(PathMetadataFlags::PATH_INCLUDE_TYPES)) {
+ int32_t *w = r_query_result.path_types.ptrw();
+ const int32_t *r = r_query_result.path_types.ptr();
+ for (uint32_t i = 0; i < indices_count; i++) {
+ w[i] = r[simplified_path_indices[i]];
+ }
+ r_query_result.path_types.resize(indices_count);
+ }
+
+ if (p_parameters.metadata_flags.has_flag(PathMetadataFlags::PATH_INCLUDE_RIDS)) {
+ TypedArray<RID> simplified_path_rids;
+ simplified_path_rids.resize(indices_count);
+ for (uint32_t i = 0; i < indices_count; i++) {
+ simplified_path_rids[i] = r_query_result.path_rids[i];
+ }
+ r_query_result.path_rids = simplified_path_rids;
+ }
+
+ if (p_parameters.metadata_flags.has_flag(PathMetadataFlags::PATH_INCLUDE_OWNERS)) {
+ int64_t *w = r_query_result.path_owner_ids.ptrw();
+ const int64_t *r = r_query_result.path_owner_ids.ptr();
+ for (uint32_t i = 0; i < indices_count; i++) {
+ w[i] = r[simplified_path_indices[i]];
+ }
+ r_query_result.path_owner_ids.resize(indices_count);
+ }
+ }
+
// add path stats
return r_query_result;
}
+RID GodotNavigationServer3D::source_geometry_parser_create() {
+#ifndef _3D_DISABLED
+ if (navmesh_generator_3d) {
+ return navmesh_generator_3d->source_geometry_parser_create();
+ }
+#endif // _3D_DISABLED
+ return RID();
+}
+
+void GodotNavigationServer3D::source_geometry_parser_set_callback(RID p_parser, const Callable &p_callback) {
+#ifndef _3D_DISABLED
+ if (navmesh_generator_3d) {
+ navmesh_generator_3d->source_geometry_parser_set_callback(p_parser, p_callback);
+ }
+#endif // _3D_DISABLED
+}
+
+Vector<Vector3> GodotNavigationServer3D::simplify_path(const Vector<Vector3> &p_path, real_t p_epsilon) {
+ if (p_path.size() <= 2) {
+ return p_path;
+ }
+
+ p_epsilon = MAX(0.0, p_epsilon);
+
+ LocalVector<uint32_t> simplified_path_indices = get_simplified_path_indices(p_path, p_epsilon);
+
+ uint32_t indices_count = simplified_path_indices.size();
+
+ Vector<Vector3> simplified_path;
+ simplified_path.resize(indices_count);
+
+ Vector3 *w = simplified_path.ptrw();
+ const Vector3 *r = p_path.ptr();
+ for (uint32_t i = 0; i < indices_count; i++) {
+ w[i] = r[simplified_path_indices[i]];
+ }
+
+ return simplified_path;
+}
+
+LocalVector<uint32_t> GodotNavigationServer3D::get_simplified_path_indices(const Vector<Vector3> &p_path, real_t p_epsilon) {
+ p_epsilon = MAX(0.0, p_epsilon);
+ real_t squared_epsilon = p_epsilon * p_epsilon;
+
+ LocalVector<bool> valid_points;
+ valid_points.resize(p_path.size());
+ for (uint32_t i = 0; i < valid_points.size(); i++) {
+ valid_points[i] = false;
+ }
+
+ simplify_path_segment(0, p_path.size() - 1, p_path, squared_epsilon, valid_points);
+
+ int valid_point_index = 0;
+
+ for (bool valid : valid_points) {
+ if (valid) {
+ valid_point_index += 1;
+ }
+ }
+
+ LocalVector<uint32_t> simplified_path_indices;
+ simplified_path_indices.resize(valid_point_index);
+ valid_point_index = 0;
+
+ for (uint32_t i = 0; i < valid_points.size(); i++) {
+ if (valid_points[i]) {
+ simplified_path_indices[valid_point_index] = i;
+ valid_point_index += 1;
+ }
+ }
+
+ return simplified_path_indices;
+}
+
+void GodotNavigationServer3D::simplify_path_segment(int p_start_inx, int p_end_inx, const Vector<Vector3> &p_points, real_t p_epsilon, LocalVector<bool> &r_valid_points) {
+ r_valid_points[p_start_inx] = true;
+ r_valid_points[p_end_inx] = true;
+
+ const Vector3 &start_point = p_points[p_start_inx];
+ const Vector3 &end_point = p_points[p_end_inx];
+
+ Vector3 path_segment[2] = { start_point, end_point };
+
+ real_t point_max_distance = 0.0;
+ int point_max_index = 0;
+
+ for (int i = p_start_inx; i < p_end_inx; i++) {
+ const Vector3 &checked_point = p_points[i];
+
+ const Vector3 closest_point = Geometry3D::get_closest_point_to_segment(checked_point, path_segment);
+ real_t distance_squared = closest_point.distance_squared_to(checked_point);
+
+ if (distance_squared > point_max_distance) {
+ point_max_index = i;
+ point_max_distance = distance_squared;
+ }
+ }
+
+ if (point_max_distance > p_epsilon) {
+ simplify_path_segment(p_start_inx, point_max_index, p_points, p_epsilon, r_valid_points);
+ simplify_path_segment(point_max_index, p_end_inx, p_points, p_epsilon, r_valid_points);
+ }
+}
+
int GodotNavigationServer3D::get_process_info(ProcessInfo p_info) const {
switch (p_info) {
case INFO_ACTIVE_MAPS: {
diff --git a/modules/navigation/3d/godot_navigation_server_3d.h b/modules/navigation/3d/godot_navigation_server_3d.h
index f7d991d47a..5ba7ed1088 100644
--- a/modules/navigation/3d/godot_navigation_server_3d.h
+++ b/modules/navigation/3d/godot_navigation_server_3d.h
@@ -264,6 +264,16 @@ public:
virtual void bake_from_source_geometry_data_async(const Ref<NavigationMesh> &p_navigation_mesh, const Ref<NavigationMeshSourceGeometryData3D> &p_source_geometry_data, const Callable &p_callback = Callable()) override;
virtual bool is_baking_navigation_mesh(Ref<NavigationMesh> p_navigation_mesh) const override;
+ virtual RID source_geometry_parser_create() override;
+ virtual void source_geometry_parser_set_callback(RID p_parser, const Callable &p_callback) override;
+
+ virtual Vector<Vector3> simplify_path(const Vector<Vector3> &p_path, real_t p_epsilon) override;
+
+private:
+ static void simplify_path_segment(int p_start_inx, int p_end_inx, const Vector<Vector3> &p_points, real_t p_epsilon, LocalVector<bool> &r_valid_points);
+ static LocalVector<uint32_t> get_simplified_path_indices(const Vector<Vector3> &p_path, real_t p_epsilon);
+
+public:
COMMAND_1(free, RID, p_object);
virtual void set_active(bool p_active) override;
diff --git a/modules/navigation/3d/nav_mesh_generator_3d.cpp b/modules/navigation/3d/nav_mesh_generator_3d.cpp
index 8b43eba080..cc3bbdbf01 100644
--- a/modules/navigation/3d/nav_mesh_generator_3d.cpp
+++ b/modules/navigation/3d/nav_mesh_generator_3d.cpp
@@ -45,12 +45,12 @@
#include "scene/resources/3d/convex_polygon_shape_3d.h"
#include "scene/resources/3d/cylinder_shape_3d.h"
#include "scene/resources/3d/height_map_shape_3d.h"
+#include "scene/resources/3d/navigation_mesh_source_geometry_data_3d.h"
#include "scene/resources/3d/primitive_meshes.h"
#include "scene/resources/3d/shape_3d.h"
#include "scene/resources/3d/sphere_shape_3d.h"
#include "scene/resources/3d/world_boundary_shape_3d.h"
#include "scene/resources/navigation_mesh.h"
-#include "scene/resources/navigation_mesh_source_geometry_data_3d.h"
#include "modules/modules_enabled.gen.h" // For csg, gridmap.
@@ -66,11 +66,14 @@
NavMeshGenerator3D *NavMeshGenerator3D::singleton = nullptr;
Mutex NavMeshGenerator3D::baking_navmesh_mutex;
Mutex NavMeshGenerator3D::generator_task_mutex;
+RWLock NavMeshGenerator3D::generator_rid_rwlock;
bool NavMeshGenerator3D::use_threads = true;
bool NavMeshGenerator3D::baking_use_multiple_threads = true;
bool NavMeshGenerator3D::baking_use_high_priority_threads = true;
HashSet<Ref<NavigationMesh>> NavMeshGenerator3D::baking_navmeshes;
HashMap<WorkerThreadPool::TaskID, NavMeshGenerator3D::NavMeshGeneratorTask3D *> NavMeshGenerator3D::generator_tasks;
+RID_Owner<NavMeshGenerator3D::NavMeshGeometryParser3D> NavMeshGenerator3D::generator_parser_owner;
+LocalVector<NavMeshGenerator3D::NavMeshGeometryParser3D *> NavMeshGenerator3D::generator_parsers;
NavMeshGenerator3D *NavMeshGenerator3D::get_singleton() {
return singleton;
@@ -85,7 +88,7 @@ NavMeshGenerator3D::NavMeshGenerator3D() {
// Using threads might cause problems on certain exports or with the Editor on certain devices.
// This is the main switch to turn threaded navmesh baking off should the need arise.
- use_threads = baking_use_multiple_threads && !Engine::get_singleton()->is_editor_hint();
+ use_threads = baking_use_multiple_threads;
}
NavMeshGenerator3D::~NavMeshGenerator3D() {
@@ -139,6 +142,13 @@ void NavMeshGenerator3D::cleanup() {
}
generator_tasks.clear();
+ generator_rid_rwlock.write_lock();
+ for (NavMeshGeometryParser3D *parser : generator_parsers) {
+ generator_parser_owner.free(parser->self);
+ }
+ generator_parsers.clear();
+ generator_rid_rwlock.write_unlock();
+
generator_task_mutex.unlock();
baking_navmesh_mutex.unlock();
}
@@ -254,6 +264,15 @@ void NavMeshGenerator3D::generator_parse_geometry_node(const Ref<NavigationMesh>
#endif
generator_parse_navigationobstacle_node(p_navigation_mesh, p_source_geometry_data, p_node);
+ generator_rid_rwlock.read_lock();
+ for (const NavMeshGeometryParser3D *parser : generator_parsers) {
+ if (!parser->callback.is_valid()) {
+ continue;
+ }
+ parser->callback.call(p_navigation_mesh, p_source_geometry_data, p_node);
+ }
+ generator_rid_rwlock.read_unlock();
+
if (p_recurse_children) {
for (int i = 0; i < p_node->get_child_count(); i++) {
generator_parse_geometry_node(p_navigation_mesh, p_source_geometry_data, p_node->get_child(i), p_recurse_children);
@@ -700,7 +719,7 @@ void NavMeshGenerator3D::generator_bake_from_source_geometry_data(Ref<Navigation
cfg.detailSampleDist = MAX(p_navigation_mesh->get_cell_size() * p_navigation_mesh->get_detail_sample_distance(), 0.1f);
cfg.detailSampleMaxError = p_navigation_mesh->get_cell_height() * p_navigation_mesh->get_detail_sample_max_error();
- if (p_navigation_mesh->get_border_size() > 0.0 && !Math::is_equal_approx(p_navigation_mesh->get_cell_size(), p_navigation_mesh->get_border_size())) {
+ if (p_navigation_mesh->get_border_size() > 0.0 && Math::fmod(p_navigation_mesh->get_border_size(), p_navigation_mesh->get_cell_size()) != 0.0) {
WARN_PRINT("Property border_size is ceiled to cell_size voxel units and loses precision.");
}
if (!Math::is_equal_approx((float)cfg.walkableHeight * cfg.ch, p_navigation_mesh->get_agent_height())) {
@@ -920,4 +939,45 @@ bool NavMeshGenerator3D::generator_emit_callback(const Callable &p_callback) {
return ce.error == Callable::CallError::CALL_OK;
}
+RID NavMeshGenerator3D::source_geometry_parser_create() {
+ RWLockWrite write_lock(generator_rid_rwlock);
+
+ RID rid = generator_parser_owner.make_rid();
+
+ NavMeshGeometryParser3D *parser = generator_parser_owner.get_or_null(rid);
+ parser->self = rid;
+
+ generator_parsers.push_back(parser);
+
+ return rid;
+}
+
+void NavMeshGenerator3D::source_geometry_parser_set_callback(RID p_parser, const Callable &p_callback) {
+ RWLockWrite write_lock(generator_rid_rwlock);
+
+ NavMeshGeometryParser3D *parser = generator_parser_owner.get_or_null(p_parser);
+ ERR_FAIL_NULL(parser);
+
+ parser->callback = p_callback;
+}
+
+bool NavMeshGenerator3D::owns(RID p_object) {
+ RWLockRead read_lock(generator_rid_rwlock);
+ return generator_parser_owner.owns(p_object);
+}
+
+void NavMeshGenerator3D::free(RID p_object) {
+ RWLockWrite write_lock(generator_rid_rwlock);
+
+ if (generator_parser_owner.owns(p_object)) {
+ NavMeshGeometryParser3D *parser = generator_parser_owner.get_or_null(p_object);
+
+ generator_parsers.erase(parser);
+
+ generator_parser_owner.free(p_object);
+ } else {
+ ERR_PRINT("Attempted to free a NavMeshGenerator3D RID that did not exist (or was already freed).");
+ }
+}
+
#endif // _3D_DISABLED
diff --git a/modules/navigation/3d/nav_mesh_generator_3d.h b/modules/navigation/3d/nav_mesh_generator_3d.h
index 9c9b3bdefe..b46a1736e0 100644
--- a/modules/navigation/3d/nav_mesh_generator_3d.h
+++ b/modules/navigation/3d/nav_mesh_generator_3d.h
@@ -35,6 +35,7 @@
#include "core/object/class_db.h"
#include "core/object/worker_thread_pool.h"
+#include "core/templates/rid_owner.h"
#include "modules/modules_enabled.gen.h" // For csg, gridmap.
class Node;
@@ -47,6 +48,14 @@ class NavMeshGenerator3D : public Object {
static Mutex baking_navmesh_mutex;
static Mutex generator_task_mutex;
+ static RWLock generator_rid_rwlock;
+ struct NavMeshGeometryParser3D {
+ RID self;
+ Callable callback;
+ };
+ static RID_Owner<NavMeshGeometryParser3D> generator_parser_owner;
+ static LocalVector<NavMeshGeometryParser3D *> generator_parsers;
+
static bool use_threads;
static bool baking_use_multiple_threads;
static bool baking_use_high_priority_threads;
@@ -102,6 +111,12 @@ public:
static void bake_from_source_geometry_data_async(Ref<NavigationMesh> p_navigation_mesh, Ref<NavigationMeshSourceGeometryData3D> p_source_geometry_data, const Callable &p_callback = Callable());
static bool is_baking(Ref<NavigationMesh> p_navigation_mesh);
+ static RID source_geometry_parser_create();
+ static void source_geometry_parser_set_callback(RID p_parser, const Callable &p_callback);
+
+ static bool owns(RID p_object);
+ static void free(RID p_object);
+
NavMeshGenerator3D();
~NavMeshGenerator3D();
};
diff --git a/modules/navigation/3d/navigation_mesh_generator.cpp b/modules/navigation/3d/navigation_mesh_generator.cpp
index 8393896db1..54df42e266 100644
--- a/modules/navigation/3d/navigation_mesh_generator.cpp
+++ b/modules/navigation/3d/navigation_mesh_generator.cpp
@@ -32,7 +32,7 @@
#include "navigation_mesh_generator.h"
-#include "scene/resources/navigation_mesh_source_geometry_data_3d.h"
+#include "scene/resources/3d/navigation_mesh_source_geometry_data_3d.h"
#include "servers/navigation_server_3d.h"
NavigationMeshGenerator *NavigationMeshGenerator::singleton = nullptr;
diff --git a/modules/navigation/editor/navigation_mesh_editor_plugin.cpp b/modules/navigation/editor/navigation_mesh_editor_plugin.cpp
index 18d66c7b69..d7bf1cdd38 100644
--- a/modules/navigation/editor/navigation_mesh_editor_plugin.cpp
+++ b/modules/navigation/editor/navigation_mesh_editor_plugin.cpp
@@ -42,7 +42,6 @@
#include "scene/gui/button.h"
#include "scene/gui/dialogs.h"
#include "scene/gui/label.h"
-#include "scene/resources/navigation_mesh_source_geometry_data_3d.h"
void NavigationMeshEditor::_node_removed(Node *p_node) {
if (p_node == node) {
@@ -99,7 +98,7 @@ void NavigationMeshEditor::_bake_pressed() {
}
}
- node->bake_navigation_mesh(false);
+ node->bake_navigation_mesh(true);
node->update_gizmos();
}
diff --git a/modules/openxr/config.py b/modules/openxr/config.py
index 92aaf06fce..6c6b9fdb8b 100644
--- a/modules/openxr/config.py
+++ b/modules/openxr/config.py
@@ -22,6 +22,10 @@ def get_doc_classes():
"OpenXRInteractionProfileMetadata",
"OpenXRIPBinding",
"OpenXRHand",
+ "OpenXRCompositionLayer",
+ "OpenXRCompositionLayerQuad",
+ "OpenXRCompositionLayerCylinder",
+ "OpenXRCompositionLayerEquirect",
]
diff --git a/modules/openxr/doc_classes/OpenXRCompositionLayer.xml b/modules/openxr/doc_classes/OpenXRCompositionLayer.xml
new file mode 100644
index 0000000000..b9c69075e1
--- /dev/null
+++ b/modules/openxr/doc_classes/OpenXRCompositionLayer.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRCompositionLayer" inherits="Node3D" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+ <brief_description>
+ The parent class of all OpenXR composition layer nodes.
+ </brief_description>
+ <description>
+ Composition layers allow 2D viewports to be displayed inside of the headset by the XR compositor through special projections that retain their quality. This allows for rendering clear text while keeping the layer at a native resolution.
+ [b]Note:[/b] If the OpenXR runtime doesn't support the given composition layer type, a fallback mesh can be generated with a [ViewportTexture], in order to emulate the composition layer.
+ </description>
+ <tutorials>
+ </tutorials>
+ <methods>
+ <method name="intersects_ray" qualifiers="const">
+ <return type="Vector2" />
+ <param index="0" name="origin" type="Vector3" />
+ <param index="1" name="direction" type="Vector3" />
+ <description>
+ Returns UV coordinates where the given ray intersects with the composition layer. [param origin] and [param direction] must be in global space.
+ Returns [code]Vector2(-1.0, -1.0)[/code] if the ray doesn't intersect.
+ </description>
+ </method>
+ <method name="is_natively_supported" qualifiers="const">
+ <return type="bool" />
+ <description>
+ Returns true if the OpenXR runtime natively supports this composition layer type.
+ [b]Note:[/b] This will only return an accurate result after the OpenXR session has started.
+ </description>
+ </method>
+ </methods>
+ <members>
+ <member name="alpha_blend" type="bool" setter="set_alpha_blend" getter="get_alpha_blend" default="false">
+ Enables the blending the layer using its alpha channel.
+ Can be combined with [member Viewport.transparent_bg] to give the layer a transparent background.
+ </member>
+ <member name="layer_viewport" type="SubViewport" setter="set_layer_viewport" getter="get_layer_viewport">
+ The [SubViewport] to render on the composition layer.
+ </member>
+ <member name="sort_order" type="int" setter="set_sort_order" getter="get_sort_order" default="1">
+ The sort order for this composition layer. Higher numbers will be shown in front of lower numbers.
+ [b]Note:[/b] This will have no effect if a fallback mesh is being used.
+ </member>
+ </members>
+</class>
diff --git a/modules/openxr/doc_classes/OpenXRCompositionLayerCylinder.xml b/modules/openxr/doc_classes/OpenXRCompositionLayerCylinder.xml
new file mode 100644
index 0000000000..dd8a11e2b9
--- /dev/null
+++ b/modules/openxr/doc_classes/OpenXRCompositionLayerCylinder.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRCompositionLayerCylinder" inherits="OpenXRCompositionLayer" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+ <brief_description>
+ An OpenXR composition layer that is rendered as an internal slice of a cylinder.
+ </brief_description>
+ <description>
+ An OpenXR composition layer that allows rendering a [SubViewport] on an internal slice of a cylinder.
+ </description>
+ <tutorials>
+ </tutorials>
+ <members>
+ <member name="aspect_ratio" type="float" setter="set_aspect_ratio" getter="get_aspect_ratio" default="1.0">
+ The aspect ratio of the slice. Used to set the height relative to the width.
+ </member>
+ <member name="central_angle" type="float" setter="set_central_angle" getter="get_central_angle" default="1.5708">
+ The central angle of the cylinder. Used to set the width.
+ </member>
+ <member name="fallback_segments" type="int" setter="set_fallback_segments" getter="get_fallback_segments" default="10">
+ The number of segments to use in the fallback mesh.
+ </member>
+ <member name="radius" type="float" setter="set_radius" getter="get_radius" default="1.0">
+ The radius of the cylinder.
+ </member>
+ </members>
+</class>
diff --git a/modules/openxr/doc_classes/OpenXRCompositionLayerEquirect.xml b/modules/openxr/doc_classes/OpenXRCompositionLayerEquirect.xml
new file mode 100644
index 0000000000..716ea72854
--- /dev/null
+++ b/modules/openxr/doc_classes/OpenXRCompositionLayerEquirect.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRCompositionLayerEquirect" inherits="OpenXRCompositionLayer" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+ <brief_description>
+ An OpenXR composition layer that is rendered as an internal slice of a sphere.
+ </brief_description>
+ <description>
+ An OpenXR composition layer that allows rendering a [SubViewport] on an internal slice of a sphere.
+ </description>
+ <tutorials>
+ </tutorials>
+ <members>
+ <member name="central_horizontal_angle" type="float" setter="set_central_horizontal_angle" getter="get_central_horizontal_angle" default="1.5708">
+ The central horizontal angle of the sphere. Used to set the width.
+ </member>
+ <member name="fallback_segments" type="int" setter="set_fallback_segments" getter="get_fallback_segments" default="10">
+ The number of segments to use in the fallback mesh.
+ </member>
+ <member name="lower_vertical_angle" type="float" setter="set_lower_vertical_angle" getter="get_lower_vertical_angle" default="0.785398">
+ The lower vertical angle of the sphere. Used (together with [member upper_vertical_angle]) to set the height.
+ </member>
+ <member name="radius" type="float" setter="set_radius" getter="get_radius" default="1.0">
+ The radius of the sphere.
+ </member>
+ <member name="upper_vertical_angle" type="float" setter="set_upper_vertical_angle" getter="get_upper_vertical_angle" default="0.785398">
+ The upper vertical angle of the sphere. Used (together with [member lower_vertical_angle]) to set the height.
+ </member>
+ </members>
+</class>
diff --git a/modules/openxr/doc_classes/OpenXRCompositionLayerQuad.xml b/modules/openxr/doc_classes/OpenXRCompositionLayerQuad.xml
new file mode 100644
index 0000000000..6632f90ed2
--- /dev/null
+++ b/modules/openxr/doc_classes/OpenXRCompositionLayerQuad.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="OpenXRCompositionLayerQuad" inherits="OpenXRCompositionLayer" experimental="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
+ <brief_description>
+ An OpenXR composition layer that is rendered as a quad.
+ </brief_description>
+ <description>
+ An OpenXR composition layer that allows rendering a [SubViewport] on a quad.
+ </description>
+ <tutorials>
+ </tutorials>
+ <members>
+ <member name="quad_size" type="Vector2" setter="set_quad_size" getter="get_quad_size" default="Vector2(1, 1)">
+ The dimensions of the quad.
+ </member>
+ </members>
+</class>
diff --git a/modules/openxr/doc_classes/OpenXRExtensionWrapperExtension.xml b/modules/openxr/doc_classes/OpenXRExtensionWrapperExtension.xml
index 9d6b197ee1..79aa547c52 100644
--- a/modules/openxr/doc_classes/OpenXRExtensionWrapperExtension.xml
+++ b/modules/openxr/doc_classes/OpenXRExtensionWrapperExtension.xml
@@ -46,6 +46,18 @@
Returns a [PackedStringArray] of positional tracker names that are used within the extension wrapper.
</description>
</method>
+ <method name="_get_viewport_composition_layer_extension_properties" qualifiers="virtual">
+ <return type="Dictionary[]" />
+ <description>
+ Gets an array of [Dictionary]s that represent properties, just like [method Object._get_property_list], that will be added to [OpenXRCompositionLayer] nodes.
+ </description>
+ </method>
+ <method name="_get_viewport_composition_layer_extension_property_defaults" qualifiers="virtual">
+ <return type="Dictionary" />
+ <description>
+ Gets a [Dictionary] containing the default values for the properties returned by [method _get_viewport_composition_layer_extension_properties].
+ </description>
+ </method>
<method name="_on_before_instance_created" qualifiers="virtual">
<return type="void" />
<description>
@@ -152,6 +164,14 @@
Called when the OpenXR session state is changed to visible. This means OpenXR is now ready to receive frames.
</description>
</method>
+ <method name="_on_viewport_composition_layer_destroyed" qualifiers="virtual">
+ <return type="void" />
+ <param index="0" name="layer" type="const void*" />
+ <description>
+ Called when a composition layer created via [OpenXRCompositionLayer] is destroyed.
+ [param layer] is a pointer to an [code]XrCompositionLayerBaseHeader[/code] struct.
+ </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" />
@@ -188,6 +208,17 @@
Adds additional data structures when interogating OpenXR system abilities.
</description>
</method>
+ <method name="_set_viewport_composition_layer_and_get_next_pointer" qualifiers="virtual">
+ <return type="int" />
+ <param index="0" name="layer" type="const void*" />
+ <param index="1" name="property_values" type="Dictionary" />
+ <param index="2" name="next_pointer" type="void*" />
+ <description>
+ Adds additional data structures to composition layers created by [OpenXRCompositionLayer].
+ [param property_values] contains the values of the properties returned by [method _get_viewport_composition_layer_extension_properties].
+ [param layer] is a pointer to an [code]XrCompositionLayerBaseHeader[/code] struct.
+ </description>
+ </method>
<method name="get_openxr_api">
<return type="OpenXRAPIExtension" />
<description>
diff --git a/modules/openxr/doc_classes/OpenXRInterface.xml b/modules/openxr/doc_classes/OpenXRInterface.xml
index 5d38788dcd..1136ac1b69 100644
--- a/modules/openxr/doc_classes/OpenXRInterface.xml
+++ b/modules/openxr/doc_classes/OpenXRInterface.xml
@@ -152,6 +152,13 @@
Informs the user queued a recenter of the player position.
</description>
</signal>
+ <signal name="refresh_rate_changed">
+ <param index="0" name="refresh_rate" type="float" />
+ <description>
+ Informs the user the HMD refresh rate has changed.
+ [b]Node:[/b] Only emitted if XR runtime supports the refresh rate extension.
+ </description>
+ </signal>
<signal name="session_begun">
<description>
Informs our OpenXR session has been started.
diff --git a/modules/openxr/editor/openxr_action_set_editor.cpp b/modules/openxr/editor/openxr_action_set_editor.cpp
index a9fc6c4db6..8b4a0e989c 100644
--- a/modules/openxr/editor/openxr_action_set_editor.cpp
+++ b/modules/openxr/editor/openxr_action_set_editor.cpp
@@ -261,13 +261,13 @@ OpenXRActionSetEditor::OpenXRActionSetEditor(Ref<OpenXRActionMap> p_action_map,
action_set_hb->add_child(action_set_priority);
add_action = memnew(Button);
- add_action->set_tooltip_text("Add Action.");
+ add_action->set_tooltip_text(TTR("Add action."));
add_action->connect("pressed", callable_mp(this, &OpenXRActionSetEditor::_on_add_action));
add_action->set_flat(true);
action_set_hb->add_child(add_action);
rem_action_set = memnew(Button);
- rem_action_set->set_tooltip_text("Remove Action Set.");
+ rem_action_set->set_tooltip_text(TTR("Remove action set."));
rem_action_set->connect("pressed", callable_mp(this, &OpenXRActionSetEditor::_on_remove_action_set));
rem_action_set->set_flat(true);
action_set_hb->add_child(rem_action_set);
diff --git a/modules/openxr/editor/openxr_select_interaction_profile_dialog.cpp b/modules/openxr/editor/openxr_select_interaction_profile_dialog.cpp
index 51642d8503..8fd66fac04 100644
--- a/modules/openxr/editor/openxr_select_interaction_profile_dialog.cpp
+++ b/modules/openxr/editor/openxr_select_interaction_profile_dialog.cpp
@@ -111,7 +111,7 @@ void OpenXRSelectInteractionProfileDialog::ok_pressed() {
}
OpenXRSelectInteractionProfileDialog::OpenXRSelectInteractionProfileDialog() {
- set_title("Select an interaction profile");
+ set_title(TTR("Select an interaction profile"));
scroll = memnew(ScrollContainer);
scroll->set_custom_minimum_size(Size2(600.0, 400.0));
diff --git a/modules/openxr/extensions/openxr_composition_layer_extension.cpp b/modules/openxr/extensions/openxr_composition_layer_extension.cpp
new file mode 100644
index 0000000000..1fba8e5f8b
--- /dev/null
+++ b/modules/openxr/extensions/openxr_composition_layer_extension.cpp
@@ -0,0 +1,322 @@
+/**************************************************************************/
+/* openxr_composition_layer_extension.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 "openxr_composition_layer_extension.h"
+
+#include "servers/rendering/rendering_server_globals.h"
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRCompositionLayerExtension
+
+OpenXRCompositionLayerExtension *OpenXRCompositionLayerExtension::singleton = nullptr;
+
+OpenXRCompositionLayerExtension *OpenXRCompositionLayerExtension::get_singleton() {
+ return singleton;
+}
+
+OpenXRCompositionLayerExtension::OpenXRCompositionLayerExtension() {
+ singleton = this;
+}
+
+OpenXRCompositionLayerExtension::~OpenXRCompositionLayerExtension() {
+ singleton = nullptr;
+}
+
+HashMap<String, bool *> OpenXRCompositionLayerExtension::get_requested_extensions() {
+ HashMap<String, bool *> request_extensions;
+
+ request_extensions[XR_KHR_COMPOSITION_LAYER_CYLINDER_EXTENSION_NAME] = &cylinder_ext_available;
+ request_extensions[XR_KHR_COMPOSITION_LAYER_EQUIRECT2_EXTENSION_NAME] = &equirect_ext_available;
+
+ return request_extensions;
+}
+
+void OpenXRCompositionLayerExtension::on_session_created(const XrSession p_instance) {
+ OpenXRAPI::get_singleton()->register_composition_layer_provider(this);
+}
+
+void OpenXRCompositionLayerExtension::on_session_destroyed() {
+ OpenXRAPI::get_singleton()->unregister_composition_layer_provider(this);
+}
+
+void OpenXRCompositionLayerExtension::on_pre_render() {
+ for (OpenXRViewportCompositionLayerProvider *composition_layer : composition_layers) {
+ composition_layer->on_pre_render();
+ }
+}
+
+int OpenXRCompositionLayerExtension::get_composition_layer_count() {
+ return composition_layers.size();
+}
+
+XrCompositionLayerBaseHeader *OpenXRCompositionLayerExtension::get_composition_layer(int p_index) {
+ ERR_FAIL_INDEX_V(p_index, composition_layers.size(), nullptr);
+ return composition_layers[p_index]->get_composition_layer();
+}
+
+int OpenXRCompositionLayerExtension::get_composition_layer_order(int p_index) {
+ ERR_FAIL_INDEX_V(p_index, composition_layers.size(), 1);
+ return composition_layers[p_index]->get_sort_order();
+}
+
+void OpenXRCompositionLayerExtension::register_viewport_composition_layer_provider(OpenXRViewportCompositionLayerProvider *p_composition_layer) {
+ composition_layers.push_back(p_composition_layer);
+}
+
+void OpenXRCompositionLayerExtension::unregister_viewport_composition_layer_provider(OpenXRViewportCompositionLayerProvider *p_composition_layer) {
+ composition_layers.erase(p_composition_layer);
+}
+
+bool OpenXRCompositionLayerExtension::is_available(XrStructureType p_which) {
+ switch (p_which) {
+ case XR_TYPE_COMPOSITION_LAYER_QUAD: {
+ // Doesn't require an extension.
+ return true;
+ } break;
+ case XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR: {
+ return cylinder_ext_available;
+ } break;
+ case XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR: {
+ return equirect_ext_available;
+ } break;
+ default: {
+ ERR_PRINT(vformat("Unsupported composition layer type: %s", p_which));
+ return false;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////
+// OpenXRViewportCompositionLayerProvider
+
+OpenXRViewportCompositionLayerProvider::OpenXRViewportCompositionLayerProvider(XrCompositionLayerBaseHeader *p_composition_layer) {
+ composition_layer = p_composition_layer;
+ openxr_api = OpenXRAPI::get_singleton();
+ composition_layer_extension = OpenXRCompositionLayerExtension::get_singleton();
+}
+
+OpenXRViewportCompositionLayerProvider::~OpenXRViewportCompositionLayerProvider() {
+ for (OpenXRExtensionWrapper *extension : OpenXRAPI::get_registered_extension_wrappers()) {
+ extension->on_viewport_composition_layer_destroyed(composition_layer);
+ }
+
+ // This will reset the viewport and free the swapchain too.
+ set_viewport(RID(), Size2i());
+}
+
+void OpenXRViewportCompositionLayerProvider::set_alpha_blend(bool p_alpha_blend) {
+ if (alpha_blend != p_alpha_blend) {
+ alpha_blend = p_alpha_blend;
+ if (alpha_blend) {
+ composition_layer->layerFlags |= XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
+ } else {
+ composition_layer->layerFlags &= ~XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
+ }
+ }
+}
+
+void OpenXRViewportCompositionLayerProvider::set_viewport(RID p_viewport, Size2i p_size) {
+ RenderingServer *rs = RenderingServer::get_singleton();
+ ERR_FAIL_NULL(rs);
+
+ if (viewport != p_viewport) {
+ if (viewport.is_valid()) {
+ RID rt = rs->viewport_get_render_target(viewport);
+ RSG::texture_storage->render_target_set_override(rt, RID(), RID(), RID());
+ }
+
+ viewport = p_viewport;
+
+ if (viewport.is_valid()) {
+ viewport_size = p_size;
+ } else {
+ free_swapchain();
+ viewport_size = Size2i();
+ }
+ }
+}
+
+void OpenXRViewportCompositionLayerProvider::set_extension_property_values(const Dictionary &p_extension_property_values) {
+ extension_property_values = p_extension_property_values;
+ extension_property_values_changed = true;
+}
+
+void OpenXRViewportCompositionLayerProvider::on_pre_render() {
+ RenderingServer *rs = RenderingServer::get_singleton();
+ ERR_FAIL_NULL(rs);
+
+ if (viewport.is_valid() && openxr_api && openxr_api->is_running()) {
+ RS::ViewportUpdateMode update_mode = rs->viewport_get_update_mode(viewport);
+ if (update_mode == RS::VIEWPORT_UPDATE_ONCE || update_mode == RS::VIEWPORT_UPDATE_ALWAYS) {
+ // Update our XR swapchain
+ if (update_and_acquire_swapchain(update_mode == RS::VIEWPORT_UPDATE_ONCE)) {
+ // Render to our XR swapchain image.
+ RID rt = rs->viewport_get_render_target(viewport);
+ RSG::texture_storage->render_target_set_override(rt, get_current_swapchain_texture(), RID(), RID());
+ }
+ }
+ }
+}
+
+XrCompositionLayerBaseHeader *OpenXRViewportCompositionLayerProvider::get_composition_layer() {
+ if (openxr_api == nullptr || composition_layer_extension == nullptr) {
+ // OpenXR not initialised or we're in the editor?
+ return nullptr;
+ }
+
+ if (!composition_layer_extension->is_available(composition_layer->type)) {
+ // Selected type is not supported, ignore our layer.
+ return nullptr;
+ }
+
+ if (swapchain_info.get_swapchain() == XR_NULL_HANDLE) {
+ // Don't have a swapchain to display? Ignore our layer.
+ return nullptr;
+ }
+
+ if (swapchain_info.is_image_acquired()) {
+ swapchain_info.release();
+ }
+
+ // Update the layer struct for the swapchain.
+ switch (composition_layer->type) {
+ case XR_TYPE_COMPOSITION_LAYER_QUAD: {
+ XrCompositionLayerQuad *quad_layer = (XrCompositionLayerQuad *)composition_layer;
+ quad_layer->subImage.swapchain = swapchain_info.get_swapchain();
+ quad_layer->subImage.imageArrayIndex = 0;
+ quad_layer->subImage.imageRect.offset.x = 0;
+ quad_layer->subImage.imageRect.offset.y = 0;
+ quad_layer->subImage.imageRect.extent.width = swapchain_size.width;
+ quad_layer->subImage.imageRect.extent.height = swapchain_size.height;
+ } break;
+
+ case XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR: {
+ XrCompositionLayerCylinderKHR *cylinder_layer = (XrCompositionLayerCylinderKHR *)composition_layer;
+ cylinder_layer->subImage.swapchain = swapchain_info.get_swapchain();
+ cylinder_layer->subImage.imageArrayIndex = 0;
+ cylinder_layer->subImage.imageRect.offset.x = 0;
+ cylinder_layer->subImage.imageRect.offset.y = 0;
+ cylinder_layer->subImage.imageRect.extent.width = swapchain_size.width;
+ cylinder_layer->subImage.imageRect.extent.height = swapchain_size.height;
+ } break;
+
+ case XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR: {
+ XrCompositionLayerEquirect2KHR *equirect_layer = (XrCompositionLayerEquirect2KHR *)composition_layer;
+ equirect_layer->subImage.swapchain = swapchain_info.get_swapchain();
+ equirect_layer->subImage.imageArrayIndex = 0;
+ equirect_layer->subImage.imageRect.offset.x = 0;
+ equirect_layer->subImage.imageRect.offset.y = 0;
+ equirect_layer->subImage.imageRect.extent.width = swapchain_size.width;
+ equirect_layer->subImage.imageRect.extent.height = swapchain_size.height;
+ } break;
+
+ default: {
+ return nullptr;
+ } break;
+ }
+
+ if (extension_property_values_changed) {
+ extension_property_values_changed = false;
+
+ void *next_pointer = nullptr;
+ for (OpenXRExtensionWrapper *extension : OpenXRAPI::get_registered_extension_wrappers()) {
+ void *np = extension->set_viewport_composition_layer_and_get_next_pointer(composition_layer, extension_property_values, next_pointer);
+ if (np) {
+ next_pointer = np;
+ }
+ }
+ composition_layer->next = next_pointer;
+ }
+
+ return composition_layer;
+}
+
+bool OpenXRViewportCompositionLayerProvider::update_and_acquire_swapchain(bool p_static_image) {
+ if (openxr_api == nullptr || composition_layer_extension == nullptr) {
+ // OpenXR not initialised or we're in the editor?
+ return false;
+ }
+ if (!composition_layer_extension->is_available(composition_layer->type)) {
+ // Selected type is not supported?
+ return false;
+ }
+
+ // See if our current swapchain is outdated.
+ if (swapchain_info.get_swapchain() != XR_NULL_HANDLE) {
+ // If this swap chain, or the previous one, were static, then we can't reuse it.
+ if (swapchain_size == viewport_size && !p_static_image && !static_image) {
+ // We're all good! Just acquire it.
+ // We can ignore should_render here, return will be false.
+ XrBool32 should_render = true;
+ return swapchain_info.acquire(should_render);
+ }
+
+ swapchain_info.queue_free();
+ }
+
+ // Create our new swap chain
+ int64_t swapchain_format = openxr_api->get_color_swapchain_format();
+ const uint32_t sample_count = 3;
+ const uint32_t array_size = 1;
+ XrSwapchainCreateFlags create_flags = 0;
+ if (p_static_image) {
+ create_flags |= XR_SWAPCHAIN_CREATE_STATIC_IMAGE_BIT;
+ }
+ if (!swapchain_info.create(create_flags, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, swapchain_format, viewport_size.width, viewport_size.height, sample_count, array_size)) {
+ swapchain_size = Size2i();
+ return false;
+ }
+
+ // Acquire our image so we can start rendering into it,
+ // we can ignore should_render here, ret will be false.
+ XrBool32 should_render = true;
+ bool ret = swapchain_info.acquire(should_render);
+
+ swapchain_size = viewport_size;
+ static_image = p_static_image;
+ return ret;
+}
+
+void OpenXRViewportCompositionLayerProvider::free_swapchain() {
+ if (swapchain_info.get_swapchain() != XR_NULL_HANDLE) {
+ swapchain_info.queue_free();
+ }
+
+ swapchain_size = Size2i();
+ static_image = false;
+}
+
+RID OpenXRViewportCompositionLayerProvider::get_current_swapchain_texture() {
+ if (openxr_api == nullptr) {
+ return RID();
+ }
+
+ return swapchain_info.get_image();
+}
diff --git a/modules/openxr/extensions/openxr_composition_layer_extension.h b/modules/openxr/extensions/openxr_composition_layer_extension.h
new file mode 100644
index 0000000000..4fefc416e6
--- /dev/null
+++ b/modules/openxr/extensions/openxr_composition_layer_extension.h
@@ -0,0 +1,116 @@
+/**************************************************************************/
+/* openxr_composition_layer_extension.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 OPENXR_COMPOSITION_LAYER_EXTENSION_H
+#define OPENXR_COMPOSITION_LAYER_EXTENSION_H
+
+#include "openxr_composition_layer_provider.h"
+#include "openxr_extension_wrapper.h"
+
+#include "../openxr_api.h"
+
+class OpenXRViewportCompositionLayerProvider;
+
+// This extension provides access to composition layers for displaying 2D content through the XR compositor.
+
+// OpenXRCompositionLayerExtension enables the extensions related to this functionality
+class OpenXRCompositionLayerExtension : public OpenXRExtensionWrapper, public OpenXRCompositionLayerProvider {
+public:
+ static OpenXRCompositionLayerExtension *get_singleton();
+
+ OpenXRCompositionLayerExtension();
+ virtual ~OpenXRCompositionLayerExtension() override;
+
+ virtual HashMap<String, bool *> get_requested_extensions() override;
+ virtual void on_session_created(const XrSession p_instance) override;
+ virtual void on_session_destroyed() override;
+ virtual void on_pre_render() override;
+
+ virtual int get_composition_layer_count() override;
+ virtual XrCompositionLayerBaseHeader *get_composition_layer(int p_index) override;
+ virtual int get_composition_layer_order(int p_index) override;
+
+ void register_viewport_composition_layer_provider(OpenXRViewportCompositionLayerProvider *p_composition_layer);
+ void unregister_viewport_composition_layer_provider(OpenXRViewportCompositionLayerProvider *p_composition_layer);
+
+ bool is_available(XrStructureType p_which);
+
+private:
+ static OpenXRCompositionLayerExtension *singleton;
+
+ Vector<OpenXRViewportCompositionLayerProvider *> composition_layers;
+
+ bool cylinder_ext_available = false;
+ bool equirect_ext_available = false;
+};
+
+class OpenXRViewportCompositionLayerProvider {
+ XrCompositionLayerBaseHeader *composition_layer = nullptr;
+ int sort_order = 1;
+ bool alpha_blend = false;
+ Dictionary extension_property_values;
+ bool extension_property_values_changed = true;
+
+ RID viewport;
+ Size2i viewport_size;
+
+ OpenXRAPI::OpenXRSwapChainInfo swapchain_info;
+ Size2i swapchain_size;
+ bool static_image = false;
+
+ OpenXRAPI *openxr_api = nullptr;
+ OpenXRCompositionLayerExtension *composition_layer_extension = nullptr;
+
+ bool update_and_acquire_swapchain(bool p_static_image);
+ void free_swapchain();
+ RID get_current_swapchain_texture();
+
+public:
+ XrStructureType get_openxr_type() { return composition_layer->type; }
+
+ void set_sort_order(int p_sort_order) { sort_order = p_sort_order; }
+ int get_sort_order() const { return sort_order; }
+
+ void set_alpha_blend(bool p_alpha_blend);
+ bool get_alpha_blend() const { return alpha_blend; }
+
+ void set_viewport(RID p_viewport, Size2i p_size);
+ RID get_viewport() const { return viewport; }
+
+ void set_extension_property_values(const Dictionary &p_property_values);
+
+ void on_pre_render();
+ XrCompositionLayerBaseHeader *get_composition_layer();
+
+ OpenXRViewportCompositionLayerProvider(XrCompositionLayerBaseHeader *p_composition_layer);
+ ~OpenXRViewportCompositionLayerProvider();
+};
+
+#endif // OPENXR_COMPOSITION_LAYER_EXTENSION_H
diff --git a/modules/openxr/extensions/openxr_extension_wrapper.h b/modules/openxr/extensions/openxr_extension_wrapper.h
index ad326472ab..ce03df0b30 100644
--- a/modules/openxr/extensions/openxr_extension_wrapper.h
+++ b/modules/openxr/extensions/openxr_extension_wrapper.h
@@ -96,6 +96,11 @@ public:
virtual void on_state_loss_pending() {} // `on_state_loss_pending` is called when the OpenXR session state is changed to loss pending.
virtual void on_state_exiting() {} // `on_state_exiting` is called when the OpenXR session state is changed to exiting.
+ virtual void *set_viewport_composition_layer_and_get_next_pointer(const XrCompositionLayerBaseHeader *p_layer, Dictionary p_property_values, void *p_next_pointer) { return p_next_pointer; } // Add additional data structures to composition layers created via OpenXRCompositionLayer.
+ virtual void on_viewport_composition_layer_destroyed(const XrCompositionLayerBaseHeader *p_layer) {} // `on_viewport_composition_layer_destroyed` is called when a composition layer created via OpenXRCompositionLayer is destroyed.
+ virtual void get_viewport_composition_layer_extension_properties(List<PropertyInfo> *p_property_list) {} // Get additional property definitions for OpenXRCompositionLayer.
+ virtual Dictionary get_viewport_composition_layer_extension_property_defaults() { return Dictionary(); } // Get the default values for the additional property definitions for OpenXRCompositionLayer.
+
// `on_event_polled` is called when there is an OpenXR event to process.
// Should return true if the event was handled, false otherwise.
virtual bool on_event_polled(const XrEventDataBuffer &event) {
diff --git a/modules/openxr/extensions/openxr_extension_wrapper_extension.cpp b/modules/openxr/extensions/openxr_extension_wrapper_extension.cpp
index 60a934e3a8..3b31e1b1f6 100644
--- a/modules/openxr/extensions/openxr_extension_wrapper_extension.cpp
+++ b/modules/openxr/extensions/openxr_extension_wrapper_extension.cpp
@@ -60,6 +60,10 @@ void OpenXRExtensionWrapperExtension::_bind_methods() {
GDVIRTUAL_BIND(_on_state_loss_pending);
GDVIRTUAL_BIND(_on_state_exiting);
GDVIRTUAL_BIND(_on_event_polled, "event");
+ GDVIRTUAL_BIND(_set_viewport_composition_layer_and_get_next_pointer, "layer", "property_values", "next_pointer");
+ GDVIRTUAL_BIND(_get_viewport_composition_layer_extension_properties);
+ GDVIRTUAL_BIND(_get_viewport_composition_layer_extension_property_defaults);
+ GDVIRTUAL_BIND(_on_viewport_composition_layer_destroyed, "layer");
ClassDB::bind_method(D_METHOD("get_openxr_api"), &OpenXRExtensionWrapperExtension::get_openxr_api);
ClassDB::bind_method(D_METHOD("register_extension_wrapper"), &OpenXRExtensionWrapperExtension::register_extension_wrapper);
@@ -240,6 +244,36 @@ bool OpenXRExtensionWrapperExtension::on_event_polled(const XrEventDataBuffer &p
return false;
}
+void *OpenXRExtensionWrapperExtension::set_viewport_composition_layer_and_get_next_pointer(const XrCompositionLayerBaseHeader *p_layer, Dictionary p_property_values, void *p_next_pointer) {
+ uint64_t pointer = 0;
+
+ if (GDVIRTUAL_CALL(_set_viewport_composition_layer_and_get_next_pointer, GDExtensionConstPtr<void>(p_layer), p_property_values, GDExtensionPtr<void>(p_next_pointer), pointer)) {
+ return reinterpret_cast<void *>(pointer);
+ }
+
+ return p_next_pointer;
+}
+
+void OpenXRExtensionWrapperExtension::on_viewport_composition_layer_destroyed(const XrCompositionLayerBaseHeader *p_layer) {
+ GDVIRTUAL_CALL(_on_viewport_composition_layer_destroyed, GDExtensionConstPtr<void>(p_layer));
+}
+
+void OpenXRExtensionWrapperExtension::get_viewport_composition_layer_extension_properties(List<PropertyInfo> *p_property_list) {
+ TypedArray<Dictionary> properties;
+
+ if (GDVIRTUAL_CALL(_get_viewport_composition_layer_extension_properties, properties)) {
+ for (int i = 0; i < properties.size(); i++) {
+ p_property_list->push_back(PropertyInfo::from_dict(properties[i]));
+ }
+ }
+}
+
+Dictionary OpenXRExtensionWrapperExtension::get_viewport_composition_layer_extension_property_defaults() {
+ Dictionary property_defaults;
+ GDVIRTUAL_CALL(_get_viewport_composition_layer_extension_property_defaults, property_defaults);
+ return property_defaults;
+}
+
Ref<OpenXRAPIExtension> OpenXRExtensionWrapperExtension::get_openxr_api() {
return openxr_api;
}
diff --git a/modules/openxr/extensions/openxr_extension_wrapper_extension.h b/modules/openxr/extensions/openxr_extension_wrapper_extension.h
index d3b78bf617..71d2a57ff8 100644
--- a/modules/openxr/extensions/openxr_extension_wrapper_extension.h
+++ b/modules/openxr/extensions/openxr_extension_wrapper_extension.h
@@ -38,6 +38,7 @@
#include "core/os/os.h"
#include "core/os/thread_safe.h"
#include "core/variant/native_ptr.h"
+#include "core/variant/typed_array.h"
class OpenXRExtensionWrapperExtension : public Object, public OpenXRExtensionWrapper, public OpenXRCompositionLayerProvider {
GDCLASS(OpenXRExtensionWrapperExtension, Object);
@@ -59,6 +60,7 @@ public:
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;
+
virtual int get_composition_layer_count() override;
virtual XrCompositionLayerBaseHeader *get_composition_layer(int p_index) override;
virtual int get_composition_layer_order(int p_index) override;
@@ -117,6 +119,16 @@ public:
GDVIRTUAL1R(bool, _on_event_polled, GDExtensionConstPtr<void>);
+ virtual void *set_viewport_composition_layer_and_get_next_pointer(const XrCompositionLayerBaseHeader *p_layer, Dictionary p_property_values, void *p_next_pointer) override;
+ virtual void on_viewport_composition_layer_destroyed(const XrCompositionLayerBaseHeader *p_layer) override;
+ virtual void get_viewport_composition_layer_extension_properties(List<PropertyInfo> *p_property_list) override;
+ virtual Dictionary get_viewport_composition_layer_extension_property_defaults() override;
+
+ GDVIRTUAL3R(uint64_t, _set_viewport_composition_layer_and_get_next_pointer, GDExtensionConstPtr<void>, Dictionary, GDExtensionPtr<void>);
+ GDVIRTUAL1(_on_viewport_composition_layer_destroyed, GDExtensionConstPtr<void>);
+ GDVIRTUAL0R(TypedArray<Dictionary>, _get_viewport_composition_layer_extension_properties);
+ GDVIRTUAL0R(Dictionary, _get_viewport_composition_layer_extension_property_defaults);
+
Ref<OpenXRAPIExtension> get_openxr_api();
void register_extension_wrapper();
diff --git a/modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.cpp b/modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.cpp
index 0ef7070531..402389144a 100644
--- a/modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.cpp
+++ b/modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.cpp
@@ -29,6 +29,7 @@
/**************************************************************************/
#include "openxr_fb_display_refresh_rate_extension.h"
+#include "../openxr_interface.h"
OpenXRDisplayRefreshRateExtension *OpenXRDisplayRefreshRateExtension::singleton = nullptr;
@@ -64,6 +65,23 @@ void OpenXRDisplayRefreshRateExtension::on_instance_destroyed() {
display_refresh_rate_ext = false;
}
+bool OpenXRDisplayRefreshRateExtension::on_event_polled(const XrEventDataBuffer &event) {
+ switch (event.type) {
+ case XR_TYPE_EVENT_DATA_DISPLAY_REFRESH_RATE_CHANGED_FB: {
+ const XrEventDataDisplayRefreshRateChangedFB *event_fb = (XrEventDataDisplayRefreshRateChangedFB *)&event;
+
+ OpenXRInterface *xr_interface = OpenXRAPI::get_singleton()->get_xr_interface();
+ if (xr_interface) {
+ xr_interface->on_refresh_rate_changes(event_fb->toDisplayRefreshRate);
+ }
+
+ return true;
+ } break;
+ default:
+ return false;
+ }
+}
+
float OpenXRDisplayRefreshRateExtension::get_refresh_rate() const {
float refresh_rate = 0.0;
diff --git a/modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.h b/modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.h
index d8015fe600..1048d245a4 100644
--- a/modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.h
+++ b/modules/openxr/extensions/openxr_fb_display_refresh_rate_extension.h
@@ -51,6 +51,7 @@ public:
virtual void on_instance_created(const XrInstance p_instance) override;
virtual void on_instance_destroyed() override;
+ virtual bool on_event_polled(const XrEventDataBuffer &event) override;
float get_refresh_rate() const;
void set_refresh_rate(float p_refresh_rate);
diff --git a/modules/openxr/extensions/platform/openxr_vulkan_extension.cpp b/modules/openxr/extensions/platform/openxr_vulkan_extension.cpp
index f5e7fc192c..da613f8435 100644
--- a/modules/openxr/extensions/platform/openxr_vulkan_extension.cpp
+++ b/modules/openxr/extensions/platform/openxr_vulkan_extension.cpp
@@ -229,6 +229,10 @@ void OpenXRVulkanExtension::get_usable_swapchain_formats(Vector<int64_t> &p_usab
}
void OpenXRVulkanExtension::get_usable_depth_formats(Vector<int64_t> &p_usable_swap_chains) {
+ // Note, it is very likely we do NOT support any of depth formats where we can combine our stencil support (e.g. _S8_UINT).
+ // Right now this isn't a problem but once stencil support becomes an issue, we need to check for this in the rendering engine
+ // and create a separate buffer for the stencil.
+
p_usable_swap_chains.push_back(VK_FORMAT_D24_UNORM_S8_UINT);
p_usable_swap_chains.push_back(VK_FORMAT_D32_SFLOAT_S8_UINT);
p_usable_swap_chains.push_back(VK_FORMAT_D32_SFLOAT);
diff --git a/modules/openxr/openxr_api.cpp b/modules/openxr/openxr_api.cpp
index 8dd017c213..1fe402341b 100644
--- a/modules/openxr/openxr_api.cpp
+++ b/modules/openxr/openxr_api.cpp
@@ -63,6 +63,198 @@
#define OPENXR_LOADER_NAME "libopenxr_loader.so"
#endif
+////////////////////////////////////
+// OpenXRAPI::OpenXRSwapChainInfo
+
+Vector<OpenXRAPI::OpenXRSwapChainInfo> OpenXRAPI::OpenXRSwapChainInfo::free_queue;
+
+bool OpenXRAPI::OpenXRSwapChainInfo::create(XrSwapchainCreateFlags p_create_flags, XrSwapchainUsageFlags p_usage_flags, int64_t p_swapchain_format, uint32_t p_width, uint32_t p_height, uint32_t p_sample_count, uint32_t p_array_size) {
+ OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+ ERR_FAIL_NULL_V(openxr_api, false);
+
+ XrSession xr_session = openxr_api->get_session();
+ ERR_FAIL_COND_V(xr_session == XR_NULL_HANDLE, false);
+
+ OpenXRGraphicsExtensionWrapper *xr_graphics_extension = openxr_api->get_graphics_extension();
+ ERR_FAIL_NULL_V(xr_graphics_extension, false);
+
+ // We already have a swapchain?
+ ERR_FAIL_COND_V(swapchain != XR_NULL_HANDLE, false);
+
+ XrResult result;
+
+ void *next_pointer = nullptr;
+ for (OpenXRExtensionWrapper *wrapper : openxr_api->get_registered_extension_wrappers()) {
+ void *np = wrapper->set_swapchain_create_info_and_get_next_pointer(next_pointer);
+ if (np != nullptr) {
+ next_pointer = np;
+ }
+ }
+
+ XrSwapchainCreateInfo swapchain_create_info = {
+ XR_TYPE_SWAPCHAIN_CREATE_INFO, // type
+ next_pointer, // next
+ p_create_flags, // createFlags
+ p_usage_flags, // usageFlags
+ p_swapchain_format, // format
+ p_sample_count, // sampleCount
+ p_width, // width
+ p_height, // height
+ 1, // faceCount
+ p_array_size, // arraySize
+ 1 // mipCount
+ };
+
+ XrSwapchain new_swapchain;
+ result = openxr_api->xrCreateSwapchain(xr_session, &swapchain_create_info, &new_swapchain);
+ if (XR_FAILED(result)) {
+ print_line("OpenXR: Failed to get swapchain [", openxr_api->get_error_string(result), "]");
+ return false;
+ }
+
+ if (!xr_graphics_extension->get_swapchain_image_data(new_swapchain, p_swapchain_format, p_width, p_height, p_sample_count, p_array_size, &swapchain_graphics_data)) {
+ openxr_api->xrDestroySwapchain(new_swapchain);
+ return false;
+ }
+
+ swapchain = new_swapchain;
+
+ return true;
+}
+
+void OpenXRAPI::OpenXRSwapChainInfo::queue_free() {
+ if (image_acquired) {
+ release();
+ }
+
+ if (swapchain != XR_NULL_HANDLE) {
+ free_queue.push_back(*this);
+
+ swapchain_graphics_data = nullptr;
+ swapchain = XR_NULL_HANDLE;
+ }
+}
+
+void OpenXRAPI::OpenXRSwapChainInfo::free_queued() {
+ for (OpenXRAPI::OpenXRSwapChainInfo &swapchain_info : free_queue) {
+ swapchain_info.free();
+ }
+ free_queue.clear();
+}
+
+void OpenXRAPI::OpenXRSwapChainInfo::free() {
+ OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+ ERR_FAIL_NULL(openxr_api);
+
+ if (image_acquired) {
+ release();
+ }
+
+ if (openxr_api->get_graphics_extension() && swapchain_graphics_data != nullptr) {
+ openxr_api->get_graphics_extension()->cleanup_swapchain_graphics_data(&swapchain_graphics_data);
+ }
+
+ if (swapchain != XR_NULL_HANDLE) {
+ openxr_api->xrDestroySwapchain(swapchain);
+ swapchain = XR_NULL_HANDLE;
+ }
+}
+
+bool OpenXRAPI::OpenXRSwapChainInfo::acquire(XrBool32 &p_should_render) {
+ ERR_FAIL_COND_V(image_acquired, true); // This was not released when it should be, error out and reuse...
+
+ OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+ ERR_FAIL_NULL_V(openxr_api, false);
+
+ XrResult result;
+
+ if (!skip_acquire_swapchain) {
+ XrSwapchainImageAcquireInfo swapchain_image_acquire_info = {
+ XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO, // type
+ nullptr // next
+ };
+
+ result = openxr_api->xrAcquireSwapchainImage(swapchain, &swapchain_image_acquire_info, &image_index);
+ if (!XR_UNQUALIFIED_SUCCESS(result)) {
+ // Make sure end_frame knows we need to submit an empty frame
+ p_should_render = false;
+
+ if (XR_FAILED(result)) {
+ // Unexpected failure, log this!
+ print_line("OpenXR: failed to acquire swapchain image [", openxr_api->get_error_string(result), "]");
+ return false;
+ } else {
+ // In this scenario we silently fail, the XR runtime is simply not ready yet to acquire the swapchain.
+ return false;
+ }
+ }
+ }
+
+ XrSwapchainImageWaitInfo swapchain_image_wait_info = {
+ XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO, // type
+ nullptr, // next
+ 17000000 // timeout in nanoseconds
+ };
+
+ result = openxr_api->xrWaitSwapchainImage(swapchain, &swapchain_image_wait_info);
+ if (!XR_UNQUALIFIED_SUCCESS(result)) {
+ // Make sure end_frame knows we need to submit an empty frame
+ p_should_render = false;
+
+ if (XR_FAILED(result)) {
+ // Unexpected failure, log this!
+ print_line("OpenXR: failed to wait for swapchain image [", openxr_api->get_error_string(result), "]");
+ return false;
+ } else {
+ // Make sure to skip trying to acquire the swapchain image in the next frame
+ skip_acquire_swapchain = true;
+ return false;
+ }
+ } else {
+ skip_acquire_swapchain = false;
+ }
+
+ image_acquired = true;
+ return true;
+}
+
+bool OpenXRAPI::OpenXRSwapChainInfo::release() {
+ if (!image_acquired) {
+ // Already released or never acquired.
+ return true;
+ }
+
+ image_acquired = false; // Regardless if we succeed or not, consider this released.
+
+ OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+ ERR_FAIL_NULL_V(openxr_api, false);
+
+ XrSwapchainImageReleaseInfo swapchain_image_release_info = {
+ XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO, // type
+ nullptr // next
+ };
+ XrResult result = openxr_api->xrReleaseSwapchainImage(swapchain, &swapchain_image_release_info);
+ if (XR_FAILED(result)) {
+ print_line("OpenXR: failed to release swapchain image! [", openxr_api->get_error_string(result), "]");
+ return false;
+ }
+
+ return true;
+}
+
+RID OpenXRAPI::OpenXRSwapChainInfo::get_image() {
+ OpenXRAPI *openxr_api = OpenXRAPI::get_singleton();
+
+ if (image_acquired && openxr_api && openxr_api->get_graphics_extension()) {
+ return OpenXRAPI::get_singleton()->get_graphics_extension()->get_texture(swapchain_graphics_data, image_index);
+ } else {
+ return RID();
+ }
+}
+
+////////////////////////////////////
+// OpenXRAPI
+
OpenXRAPI *OpenXRAPI::singleton = nullptr;
Vector<OpenXRExtensionWrapper *> OpenXRAPI::registered_extension_wrappers;
@@ -568,6 +760,21 @@ bool OpenXRAPI::load_supported_view_configuration_views(XrViewConfigurationType
print_verbose(String(" - recommended render sample count: ") + itos(view_configuration_views[i].recommendedSwapchainSampleCount));
}
+ // Allocate buffers we'll be populating with view information.
+ views = (XrView *)memalloc(sizeof(XrView) * view_count);
+ ERR_FAIL_NULL_V_MSG(views, false, "OpenXR Couldn't allocate memory for views");
+ memset(views, 0, sizeof(XrView) * view_count);
+
+ projection_views = (XrCompositionLayerProjectionView *)memalloc(sizeof(XrCompositionLayerProjectionView) * view_count);
+ ERR_FAIL_NULL_V_MSG(projection_views, false, "OpenXR Couldn't allocate memory for projection views");
+ memset(projection_views, 0, sizeof(XrCompositionLayerProjectionView) * view_count);
+
+ if (submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available()) {
+ depth_views = (XrCompositionLayerDepthInfoKHR *)memalloc(sizeof(XrCompositionLayerDepthInfoKHR) * view_count);
+ ERR_FAIL_NULL_V_MSG(depth_views, false, "OpenXR Couldn't allocate memory for depth views");
+ memset(depth_views, 0, sizeof(XrCompositionLayerDepthInfoKHR) * view_count);
+ }
+
return true;
}
@@ -878,94 +1085,92 @@ bool OpenXRAPI::is_swapchain_format_supported(int64_t p_swapchain_format) {
return false;
}
-bool OpenXRAPI::create_swapchains() {
+bool OpenXRAPI::obtain_swapchain_formats() {
ERR_FAIL_NULL_V(graphics_extension, false);
ERR_FAIL_COND_V(session == XR_NULL_HANDLE, false);
- /*
- TODO: We need to improve on this, for now we're taking our old approach of creating our main swapchains and substituting
- those for the ones Godot normally creates.
- This however means we can only use swapchains for our main XR view.
-
- It would have been nicer if we could override the swapchain creation in Godot with ours but we have a timing issue here.
- We can't create XR swapchains until after our XR session is fully instantiated, yet Godot creates its swapchain much earlier.
-
- Also Godot only creates a swapchain for the main output.
- OpenXR will require us to create swapchains as the render target for additional viewports if we want to use the layer system
- to optimize text rendering and background rendering as OpenXR may choose to reuse the results for reprojection while we're
- already rendering the next frame.
-
- Finally an area we need to expand upon is that Foveated rendering is only enabled for the swap chain we create,
- as we render 3D content into internal buffers that are copied into the swapchain, we do now have (basic) VRS support
- */
-
- Size2 recommended_size = get_recommended_target_size();
- uint32_t sample_count = 1;
-
- // We start with our color swapchain...
{
// Build a vector with swapchain formats we want to use, from best fit to worst
Vector<int64_t> usable_swapchain_formats;
- int64_t swapchain_format_to_use = 0;
+ color_swapchain_format = 0;
graphics_extension->get_usable_swapchain_formats(usable_swapchain_formats);
// now find out which one is supported
- for (int i = 0; i < usable_swapchain_formats.size() && swapchain_format_to_use == 0; i++) {
+ for (int i = 0; i < usable_swapchain_formats.size() && color_swapchain_format == 0; i++) {
if (is_swapchain_format_supported(usable_swapchain_formats[i])) {
- swapchain_format_to_use = usable_swapchain_formats[i];
+ color_swapchain_format = usable_swapchain_formats[i];
}
}
- if (swapchain_format_to_use == 0) {
- swapchain_format_to_use = usable_swapchain_formats[0]; // just use the first one and hope for the best...
- print_line("Couldn't find usable color swap chain format, using", get_swapchain_format_name(swapchain_format_to_use), "instead.");
+ if (color_swapchain_format == 0) {
+ color_swapchain_format = usable_swapchain_formats[0]; // just use the first one and hope for the best...
+ print_line("Couldn't find usable color swap chain format, using", get_swapchain_format_name(color_swapchain_format), "instead.");
} else {
- print_verbose(String("Using color swap chain format:") + get_swapchain_format_name(swapchain_format_to_use));
- }
-
- if (!create_swapchain(XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, swapchain_format_to_use, recommended_size.width, recommended_size.height, sample_count, view_count, swapchains[OPENXR_SWAPCHAIN_COLOR].swapchain, &swapchains[OPENXR_SWAPCHAIN_COLOR].swapchain_graphics_data)) {
- return false;
+ print_verbose(String("Using color swap chain format:") + get_swapchain_format_name(color_swapchain_format));
}
}
- views = (XrView *)memalloc(sizeof(XrView) * view_count);
- ERR_FAIL_NULL_V_MSG(views, false, "OpenXR Couldn't allocate memory for views");
-
- projection_views = (XrCompositionLayerProjectionView *)memalloc(sizeof(XrCompositionLayerProjectionView) * view_count);
- ERR_FAIL_NULL_V_MSG(projection_views, false, "OpenXR Couldn't allocate memory for projection views");
-
- // We create our depth swapchain if:
- // - we've enabled submitting depth buffer
- // - we support our depth layer extension
- // - we have our spacewarp extension (not yet implemented)
- if (submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available()) {
+ {
// Build a vector with swapchain formats we want to use, from best fit to worst
Vector<int64_t> usable_swapchain_formats;
- int64_t swapchain_format_to_use = 0;
+ depth_swapchain_format = 0;
graphics_extension->get_usable_depth_formats(usable_swapchain_formats);
// now find out which one is supported
- for (int i = 0; i < usable_swapchain_formats.size() && swapchain_format_to_use == 0; i++) {
+ for (int i = 0; i < usable_swapchain_formats.size() && depth_swapchain_format == 0; i++) {
if (is_swapchain_format_supported(usable_swapchain_formats[i])) {
- swapchain_format_to_use = usable_swapchain_formats[i];
+ depth_swapchain_format = usable_swapchain_formats[i];
}
}
- if (swapchain_format_to_use == 0) {
- print_line("Couldn't find usable depth swap chain format, depth buffer will not be submitted.");
+ if (depth_swapchain_format == 0) {
+ WARN_PRINT_ONCE("Couldn't find usable depth swap chain format, depth buffer will not be submitted if requested.");
} else {
- print_verbose(String("Using depth swap chain format:") + get_swapchain_format_name(swapchain_format_to_use));
+ print_verbose(String("Using depth swap chain format:") + get_swapchain_format_name(depth_swapchain_format));
+ }
+ }
+
+ return true;
+}
- // Note, if VK_FORMAT_D32_SFLOAT is used here but we're using the forward+ renderer, we should probably output a warning.
+bool OpenXRAPI::create_main_swapchains(Size2i p_size) {
+ ERR_FAIL_NULL_V(graphics_extension, false);
+ ERR_FAIL_COND_V(session == XR_NULL_HANDLE, false);
- if (!create_swapchain(XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, swapchain_format_to_use, recommended_size.width, recommended_size.height, sample_count, view_count, swapchains[OPENXR_SWAPCHAIN_DEPTH].swapchain, &swapchains[OPENXR_SWAPCHAIN_DEPTH].swapchain_graphics_data)) {
- return false;
- }
+ /*
+ TODO: We need to improve on this, for now we're taking our old approach of creating our main swapchains and substituting
+ those for the ones Godot normally creates.
+ This however means we can only use swapchains for our main XR view.
+
+ It would have been nicer if we could override the swapchain creation in Godot with ours but we have a timing issue here.
+ We can't create XR swapchains until after our XR session is fully instantiated, yet Godot creates its swapchain much earlier.
+
+ We only creates a swapchain for the main output here.
+ Additional swapchains may be created through our composition layer extension.
+
+ Finally an area we need to expand upon is that Foveated rendering is only enabled for the swap chain we create,
+ as we render 3D content into internal buffers that are copied into the swapchain, we do now have (basic) VRS support
+ */
+
+ main_swapchain_size = p_size;
+ uint32_t sample_count = 1;
+
+ // We start with our color swapchain...
+ if (color_swapchain_format != 0) {
+ if (!main_swapchains[OPENXR_SWAPCHAIN_COLOR].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT, color_swapchain_format, main_swapchain_size.width, main_swapchain_size.height, sample_count, view_count)) {
+ return false;
+ }
+ }
- depth_views = (XrCompositionLayerDepthInfoKHR *)memalloc(sizeof(XrCompositionLayerDepthInfoKHR) * view_count);
- ERR_FAIL_NULL_V_MSG(depth_views, false, "OpenXR Couldn't allocate memory for depth views");
+ // We create our depth swapchain if:
+ // - we've enabled submitting depth buffer
+ // - we support our depth layer extension
+ // - we have our spacewarp extension (not yet implemented)
+ if (depth_swapchain_format != 0 && submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available()) {
+ if (!main_swapchains[OPENXR_SWAPCHAIN_DEPTH].create(0, XR_SWAPCHAIN_USAGE_SAMPLED_BIT | XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, depth_swapchain_format, main_swapchain_size.width, main_swapchain_size.height, sample_count, view_count)) {
+ return false;
}
}
@@ -981,28 +1186,30 @@ bool OpenXRAPI::create_swapchains() {
projection_views[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW;
projection_views[i].next = nullptr;
- projection_views[i].subImage.swapchain = swapchains[OPENXR_SWAPCHAIN_COLOR].swapchain;
+ projection_views[i].subImage.swapchain = main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain();
projection_views[i].subImage.imageArrayIndex = i;
projection_views[i].subImage.imageRect.offset.x = 0;
projection_views[i].subImage.imageRect.offset.y = 0;
- projection_views[i].subImage.imageRect.extent.width = recommended_size.width;
- projection_views[i].subImage.imageRect.extent.height = recommended_size.height;
+ projection_views[i].subImage.imageRect.extent.width = main_swapchain_size.width;
+ projection_views[i].subImage.imageRect.extent.height = main_swapchain_size.height;
if (submit_depth_buffer && OpenXRCompositionLayerDepthExtension::get_singleton()->is_available() && depth_views) {
projection_views[i].next = &depth_views[i];
depth_views[i].type = XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR;
depth_views[i].next = nullptr;
- depth_views[i].subImage.swapchain = swapchains[OPENXR_SWAPCHAIN_DEPTH].swapchain;
+ depth_views[i].subImage.swapchain = main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_swapchain();
depth_views[i].subImage.imageArrayIndex = i;
depth_views[i].subImage.imageRect.offset.x = 0;
depth_views[i].subImage.imageRect.offset.y = 0;
- depth_views[i].subImage.imageRect.extent.width = recommended_size.width;
- depth_views[i].subImage.imageRect.extent.height = recommended_size.height;
+ depth_views[i].subImage.imageRect.extent.width = main_swapchain_size.width;
+ depth_views[i].subImage.imageRect.extent.height = main_swapchain_size.height;
+ // OpenXR spec says that: minDepth < maxDepth.
depth_views[i].minDepth = 0.0;
depth_views[i].maxDepth = 1.0;
- depth_views[i].nearZ = 0.01; // Near and far Z will be set to the correct values in fill_projection_matrix
- depth_views[i].farZ = 100.0;
+ // But we can reverse near and far for reverse-Z.
+ depth_views[i].nearZ = 100.0; // Near and far Z will be set to the correct values in fill_projection_matrix
+ depth_views[i].farZ = 0.01;
}
};
@@ -1014,10 +1221,6 @@ void OpenXRAPI::destroy_session() {
xrEndSession(session);
}
- if (graphics_extension) {
- graphics_extension->cleanup_swapchain_graphics_data(&swapchains[OPENXR_SWAPCHAIN_COLOR].swapchain_graphics_data);
- }
-
if (views != nullptr) {
memfree(views);
views = nullptr;
@@ -1033,12 +1236,8 @@ void OpenXRAPI::destroy_session() {
depth_views = nullptr;
}
- for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) {
- if (swapchains[i].swapchain != XR_NULL_HANDLE) {
- xrDestroySwapchain(swapchains[i].swapchain);
- swapchains[i].swapchain = XR_NULL_HANDLE;
- }
- }
+ free_main_swapchains();
+ OpenXRSwapChainInfo::free_queued();
if (supported_swapchain_formats != nullptr) {
memfree(supported_swapchain_formats);
@@ -1071,51 +1270,6 @@ void OpenXRAPI::destroy_session() {
}
}
-bool OpenXRAPI::create_swapchain(XrSwapchainUsageFlags p_usage_flags, int64_t p_swapchain_format, uint32_t p_width, uint32_t p_height, uint32_t p_sample_count, uint32_t p_array_size, XrSwapchain &r_swapchain, void **r_swapchain_graphics_data) {
- ERR_FAIL_COND_V(session == XR_NULL_HANDLE, false);
- ERR_FAIL_NULL_V(graphics_extension, false);
-
- XrResult result;
-
- void *next_pointer = nullptr;
- for (OpenXRExtensionWrapper *wrapper : registered_extension_wrappers) {
- void *np = wrapper->set_swapchain_create_info_and_get_next_pointer(next_pointer);
- if (np != nullptr) {
- next_pointer = np;
- }
- }
-
- XrSwapchainCreateInfo swapchain_create_info = {
- XR_TYPE_SWAPCHAIN_CREATE_INFO, // type
- next_pointer, // next
- 0, // createFlags
- p_usage_flags, // usageFlags
- p_swapchain_format, // format
- p_sample_count, // sampleCount
- p_width, // width
- p_height, // height
- 1, // faceCount
- p_array_size, // arraySize
- 1 // mipCount
- };
-
- XrSwapchain new_swapchain;
- result = xrCreateSwapchain(session, &swapchain_create_info, &new_swapchain);
- if (XR_FAILED(result)) {
- print_line("OpenXR: Failed to get swapchain [", get_error_string(result), "]");
- return false;
- }
-
- if (!graphics_extension->get_swapchain_image_data(new_swapchain, p_swapchain_format, p_width, p_height, p_sample_count, p_array_size, r_swapchain_graphics_data)) {
- xrDestroySwapchain(new_swapchain);
- return false;
- }
-
- r_swapchain = new_swapchain;
-
- return true;
-}
-
bool OpenXRAPI::on_state_idle() {
print_verbose("On state idle");
@@ -1142,17 +1296,6 @@ bool OpenXRAPI::on_state_ready() {
return false;
}
- // This is when we create our swapchain, this can be a "long" time after Godot finishes, we can deal with this for now
- // but once we want to provide Viewports for additional layers where OpenXR requires us to create further swapchains,
- // we'll be creating those viewport WAY before we reach this point.
- // We may need to implement a wait in our init in main.cpp polling our events until the session is ready.
- // That will be very very ugly
- // The other possibility is to create a separate OpenXRViewport type specifically for this goal as part of our OpenXR module
-
- if (!create_swapchains()) {
- return false;
- }
-
// we're running
running = true;
@@ -1164,8 +1307,6 @@ bool OpenXRAPI::on_state_ready() {
xr_interface->on_state_ready();
}
- // TODO Tell android
-
return true;
}
@@ -1499,6 +1640,11 @@ bool OpenXRAPI::initialize_session() {
return false;
}
+ if (!obtain_swapchain_formats()) {
+ destroy_session();
+ return false;
+ }
+
return true;
}
@@ -1658,8 +1804,9 @@ bool OpenXRAPI::get_view_projection(uint32_t p_view, double p_z_near, double p_z
// if we're using depth views, make sure we update our near and far there...
if (depth_views != nullptr) {
for (uint32_t i = 0; i < view_count; i++) {
- depth_views[i].nearZ = p_z_near;
- depth_views[i].farZ = p_z_far;
+ // As we are using reverse-Z these need to be flipped.
+ depth_views[i].nearZ = p_z_far;
+ depth_views[i].farZ = p_z_near;
}
}
@@ -1805,72 +1952,10 @@ bool OpenXRAPI::process() {
return true;
}
-bool OpenXRAPI::acquire_image(OpenXRSwapChainInfo &p_swapchain) {
- ERR_FAIL_COND_V(p_swapchain.image_acquired, true); // This was not released when it should be, error out and reuse...
-
- XrResult result;
-
- if (!p_swapchain.skip_acquire_swapchain) {
- XrSwapchainImageAcquireInfo swapchain_image_acquire_info = {
- XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO, // type
- nullptr // next
- };
-
- result = xrAcquireSwapchainImage(p_swapchain.swapchain, &swapchain_image_acquire_info, &p_swapchain.image_index);
- if (!XR_UNQUALIFIED_SUCCESS(result)) {
- // Make sure end_frame knows we need to submit an empty frame
- frame_state.shouldRender = false;
-
- if (XR_FAILED(result)) {
- // Unexpected failure, log this!
- print_line("OpenXR: failed to acquire swapchain image [", get_error_string(result), "]");
- return false;
- } else {
- // In this scenario we silently fail, the XR runtime is simply not ready yet to acquire the swapchain.
- return false;
- }
- }
- }
-
- XrSwapchainImageWaitInfo swapchain_image_wait_info = {
- XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO, // type
- nullptr, // next
- 17000000 // timeout in nanoseconds
- };
-
- result = xrWaitSwapchainImage(p_swapchain.swapchain, &swapchain_image_wait_info);
- if (!XR_UNQUALIFIED_SUCCESS(result)) {
- // Make sure end_frame knows we need to submit an empty frame
- frame_state.shouldRender = false;
-
- if (XR_FAILED(result)) {
- // Unexpected failure, log this!
- print_line("OpenXR: failed to wait for swapchain image [", get_error_string(result), "]");
- return false;
- } else {
- // Make sure to skip trying to acquire the swapchain image in the next frame
- p_swapchain.skip_acquire_swapchain = true;
- return false;
- }
- } else {
- p_swapchain.skip_acquire_swapchain = false;
- }
-
- return true;
-}
-
-bool OpenXRAPI::release_image(OpenXRSwapChainInfo &p_swapchain) {
- XrSwapchainImageReleaseInfo swapchain_image_release_info = {
- XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO, // type
- nullptr // next
- };
- XrResult result = xrReleaseSwapchainImage(p_swapchain.swapchain, &swapchain_image_release_info);
- if (XR_FAILED(result)) {
- print_line("OpenXR: failed to release swapchain image! [", get_error_string(result), "]");
- return false;
+void OpenXRAPI::free_main_swapchains() {
+ for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) {
+ main_swapchains[i].queue_free();
}
-
- return true;
}
void OpenXRAPI::pre_render() {
@@ -1880,6 +1965,18 @@ void OpenXRAPI::pre_render() {
return;
}
+ // Process any swapchains that were queued to be freed
+ OpenXRSwapChainInfo::free_queued();
+
+ Size2i swapchain_size = get_recommended_target_size();
+ if (swapchain_size != main_swapchain_size) {
+ // Out with the old.
+ free_main_swapchains();
+
+ // In with the new.
+ create_main_swapchains(swapchain_size);
+ }
+
// Waitframe does 2 important things in our process:
// 1) It provides us with predictive timing, telling us when OpenXR expects to display the frame we're about to commit
// 2) It will use the previous timing to pause our thread so that rendering starts as close to displaying as possible
@@ -1972,9 +2069,15 @@ void OpenXRAPI::pre_render() {
print_line("OpenXR: failed to being frame [", get_error_string(result), "]");
return;
}
+
+ // Reset this, we haven't found a viewport for output yet
+ has_xr_viewport = false;
}
bool OpenXRAPI::pre_draw_viewport(RID p_render_target) {
+ // We found an XR viewport!
+ has_xr_viewport = true;
+
if (!can_render()) {
return false;
}
@@ -1983,11 +2086,10 @@ bool OpenXRAPI::pre_draw_viewport(RID p_render_target) {
// Acquire our images
for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) {
- if (!swapchains[i].image_acquired && swapchains[i].swapchain != XR_NULL_HANDLE) {
- if (!acquire_image(swapchains[i])) {
+ if (!main_swapchains[i].is_image_acquired() && main_swapchains[i].get_swapchain() != XR_NULL_HANDLE) {
+ if (!main_swapchains[i].acquire(frame_state.shouldRender)) {
return false;
}
- swapchains[i].image_acquired = true;
}
}
@@ -1999,21 +2101,17 @@ bool OpenXRAPI::pre_draw_viewport(RID p_render_target) {
}
XrSwapchain OpenXRAPI::get_color_swapchain() {
- return swapchains[OPENXR_SWAPCHAIN_COLOR].swapchain;
+ return main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_swapchain();
}
RID OpenXRAPI::get_color_texture() {
- if (swapchains[OPENXR_SWAPCHAIN_COLOR].image_acquired) {
- return graphics_extension->get_texture(swapchains[OPENXR_SWAPCHAIN_COLOR].swapchain_graphics_data, swapchains[OPENXR_SWAPCHAIN_COLOR].image_index);
- } else {
- return RID();
- }
+ return main_swapchains[OPENXR_SWAPCHAIN_COLOR].get_image();
}
RID OpenXRAPI::get_depth_texture() {
// Note, image will not be acquired if we didn't have a suitable swap chain format.
- if (submit_depth_buffer && swapchains[OPENXR_SWAPCHAIN_DEPTH].image_acquired) {
- return graphics_extension->get_texture(swapchains[OPENXR_SWAPCHAIN_DEPTH].swapchain_graphics_data, swapchains[OPENXR_SWAPCHAIN_DEPTH].image_index);
+ if (submit_depth_buffer) {
+ return main_swapchains[OPENXR_SWAPCHAIN_DEPTH].get_image();
} else {
return RID();
}
@@ -2038,15 +2136,19 @@ void OpenXRAPI::end_frame() {
return;
}
- if (frame_state.shouldRender && view_pose_valid && !swapchains[OPENXR_SWAPCHAIN_COLOR].image_acquired) {
- print_line("OpenXR: No viewport was marked with use_xr, there is no rendered output!");
+ if (frame_state.shouldRender && view_pose_valid) {
+ if (!has_xr_viewport) {
+ print_line("OpenXR: No viewport was marked with use_xr, there is no rendered output!");
+ } else if (!main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) {
+ print_line("OpenXR: No swapchain could be acquired to render to!");
+ }
}
// must have:
// - shouldRender set to true
// - a valid view pose for projection_views[eye].pose to submit layer
// - an image to render
- if (!frame_state.shouldRender || !view_pose_valid || !swapchains[OPENXR_SWAPCHAIN_COLOR].image_acquired) {
+ if (!frame_state.shouldRender || !view_pose_valid || !main_swapchains[OPENXR_SWAPCHAIN_COLOR].is_image_acquired()) {
// submit 0 layers when we shouldn't render
XrFrameEndInfo frame_end_info = {
XR_TYPE_FRAME_END_INFO, // type
@@ -2068,10 +2170,8 @@ void OpenXRAPI::end_frame() {
// release our swapchain image if we acquired it
for (int i = 0; i < OPENXR_SWAPCHAIN_MAX; i++) {
- if (swapchains[i].image_acquired) {
- swapchains[i].image_acquired = false; // whether we succeed or not, consider this released.
-
- release_image(swapchains[i]);
+ if (main_swapchains[i].is_image_acquired()) {
+ main_swapchains[i].release();
}
}
@@ -2315,7 +2415,7 @@ OpenXRAPI::OpenXRAPI() {
submit_depth_buffer = GLOBAL_GET("xr/openxr/submit_depth_buffer");
}
- // reset a few things that can't be done in our class definition
+ // Reset a few things that can't be done in our class definition.
frame_state.predictedDisplayTime = 0;
frame_state.predictedDisplayPeriod = 0;
}
diff --git a/modules/openxr/openxr_api.h b/modules/openxr/openxr_api.h
index 7ec622364b..e835366200 100644
--- a/modules/openxr/openxr_api.h
+++ b/modules/openxr/openxr_api.h
@@ -57,6 +57,31 @@
class OpenXRInterface;
class OpenXRAPI {
+public:
+ class OpenXRSwapChainInfo {
+ private:
+ XrSwapchain swapchain = XR_NULL_HANDLE;
+ void *swapchain_graphics_data = nullptr;
+ uint32_t image_index = 0;
+ bool image_acquired = false;
+ bool skip_acquire_swapchain = false;
+
+ static Vector<OpenXRSwapChainInfo> free_queue;
+
+ public:
+ _FORCE_INLINE_ XrSwapchain get_swapchain() const { return swapchain; }
+ _FORCE_INLINE_ bool is_image_acquired() const { return image_acquired; }
+
+ bool create(XrSwapchainCreateFlags p_create_flags, XrSwapchainUsageFlags p_usage_flags, int64_t p_swapchain_format, uint32_t p_width, uint32_t p_height, uint32_t p_sample_count, uint32_t p_array_size);
+ void queue_free();
+ static void free_queued();
+ void free();
+
+ bool acquire(XrBool32 &p_should_render);
+ bool release();
+ RID get_image();
+ };
+
private:
// our singleton
static OpenXRAPI *singleton;
@@ -137,20 +162,16 @@ private:
OPENXR_SWAPCHAIN_MAX
};
- struct OpenXRSwapChainInfo {
- XrSwapchain swapchain = XR_NULL_HANDLE;
- void *swapchain_graphics_data = nullptr;
- uint32_t image_index = 0;
- bool image_acquired = false;
- bool skip_acquire_swapchain = false;
- };
-
- OpenXRSwapChainInfo swapchains[OPENXR_SWAPCHAIN_MAX];
+ int64_t color_swapchain_format = 0;
+ int64_t depth_swapchain_format = 0;
+ Size2i main_swapchain_size = { 0, 0 };
+ OpenXRSwapChainInfo main_swapchains[OPENXR_SWAPCHAIN_MAX];
XrSpace play_space = XR_NULL_HANDLE;
XrSpace view_space = XR_NULL_HANDLE;
bool view_pose_valid = false;
XRPose::TrackingConfidence head_pose_confidence = XRPose::XR_TRACKING_CONFIDENCE_NONE;
+ bool has_xr_viewport = false;
bool emulating_local_floor = false;
bool should_reset_emulated_floor_height = false;
@@ -238,14 +259,11 @@ private:
bool setup_view_space();
bool load_supported_swapchain_formats();
bool is_swapchain_format_supported(int64_t p_swapchain_format);
- bool create_swapchains();
+ bool obtain_swapchain_formats();
+ bool create_main_swapchains(Size2i p_size);
+ void free_main_swapchains();
void destroy_session();
- // swapchains
- bool create_swapchain(XrSwapchainUsageFlags p_usage_flags, int64_t p_swapchain_format, uint32_t p_width, uint32_t p_height, uint32_t p_sample_count, uint32_t p_array_size, XrSwapchain &r_swapchain, void **r_swapchain_graphics_data);
- bool acquire_image(OpenXRSwapChainInfo &p_swapchain);
- bool release_image(OpenXRSwapChainInfo &p_swapchain);
-
// action map
struct Tracker { // Trackers represent tracked physical objects such as controllers, pucks, etc.
String name; // Name for this tracker (i.e. "/user/hand/left")
@@ -314,6 +332,7 @@ public:
XrInstance get_instance() const { return instance; };
XrSystemId get_system_id() const { return system_id; };
XrSession get_session() const { return session; };
+ OpenXRGraphicsExtensionWrapper *get_graphics_extension() const { return graphics_extension; };
String get_runtime_name() const { return runtime_name; };
String get_runtime_version() const { return runtime_version; };
@@ -338,6 +357,7 @@ public:
String get_error_string(XrResult result) const;
String get_swapchain_format_name(int64_t p_swapchain_format) const;
+ OpenXRInterface *get_xr_interface() const { return xr_interface; }
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);
@@ -405,6 +425,10 @@ public:
// Play space.
Size2 get_play_space_bounds() const;
+ // swapchains
+ int64_t get_color_swapchain_format() const { return color_swapchain_format; }
+ int64_t get_depth_swapchain_format() const { return depth_swapchain_format; }
+
// action map
String get_default_action_map_resource_name();
diff --git a/modules/openxr/openxr_interface.cpp b/modules/openxr/openxr_interface.cpp
index 956e5ed3f3..7eb9a6ebe1 100644
--- a/modules/openxr/openxr_interface.cpp
+++ b/modules/openxr/openxr_interface.cpp
@@ -43,6 +43,7 @@ void OpenXRInterface::_bind_methods() {
ADD_SIGNAL(MethodInfo("session_focussed"));
ADD_SIGNAL(MethodInfo("session_visible"));
ADD_SIGNAL(MethodInfo("pose_recentered"));
+ ADD_SIGNAL(MethodInfo("refresh_rate_changed", PropertyInfo(Variant::FLOAT, "refresh_rate")));
// Display refresh rate
ClassDB::bind_method(D_METHOD("get_display_refresh_rate"), &OpenXRInterface::get_display_refresh_rate);
@@ -1258,6 +1259,10 @@ void OpenXRInterface::on_pose_recentered() {
emit_signal(SNAME("pose_recentered"));
}
+void OpenXRInterface::on_refresh_rate_changes(float p_new_rate) {
+ emit_signal(SNAME("refresh_rate_changed"), p_new_rate);
+}
+
/** Hand tracking. */
void OpenXRInterface::set_motion_range(const Hand p_hand, const HandMotionRange p_motion_range) {
ERR_FAIL_INDEX(p_hand, HAND_MAX);
diff --git a/modules/openxr/openxr_interface.h b/modules/openxr/openxr_interface.h
index aee9751d6b..737f22d642 100644
--- a/modules/openxr/openxr_interface.h
+++ b/modules/openxr/openxr_interface.h
@@ -174,6 +174,7 @@ public:
void on_state_focused();
void on_state_stopping();
void on_pose_recentered();
+ void on_refresh_rate_changes(float p_new_rate);
void tracker_profile_changed(RID p_tracker, RID p_interaction_profile);
/** Hand tracking. */
diff --git a/modules/openxr/register_types.cpp b/modules/openxr/register_types.cpp
index 3d34b27407..eb0527f07c 100644
--- a/modules/openxr/register_types.cpp
+++ b/modules/openxr/register_types.cpp
@@ -39,9 +39,14 @@
#include "extensions/openxr_extension_wrapper_extension.h"
+#include "scene/openxr_composition_layer.h"
+#include "scene/openxr_composition_layer_cylinder.h"
+#include "scene/openxr_composition_layer_equirect.h"
+#include "scene/openxr_composition_layer_quad.h"
#include "scene/openxr_hand.h"
#include "extensions/openxr_composition_layer_depth_extension.h"
+#include "extensions/openxr_composition_layer_extension.h"
#include "extensions/openxr_eye_gaze_interaction.h"
#include "extensions/openxr_fb_display_refresh_rate_extension.h"
#include "extensions/openxr_hand_tracking_extension.h"
@@ -110,6 +115,7 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) {
OpenXRAPI::register_extension_wrapper(memnew(OpenXRLocalFloorExtension));
OpenXRAPI::register_extension_wrapper(memnew(OpenXRPicoControllerExtension));
OpenXRAPI::register_extension_wrapper(memnew(OpenXRCompositionLayerDepthExtension));
+ OpenXRAPI::register_extension_wrapper(memnew(OpenXRCompositionLayerExtension));
OpenXRAPI::register_extension_wrapper(memnew(OpenXRHTCControllerExtension));
OpenXRAPI::register_extension_wrapper(memnew(OpenXRHTCViveTrackerExtension));
OpenXRAPI::register_extension_wrapper(memnew(OpenXRHuaweiControllerExtension));
@@ -135,7 +141,9 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) {
const char *init_error_message =
"OpenXR was requested but failed to start.\n"
"Please check if your HMD is connected.\n"
- "When using Windows MR please note that WMR only has DirectX support, make sure SteamVR is your default OpenXR runtime.\n"
+#ifdef WINDOWS_ENABLED
+ "When using Windows Mixed Reality, note that WMR only has DirectX support. Make sure SteamVR is your default OpenXR runtime.\n"
+#endif
"Godot will start in normal mode.\n";
WARN_PRINT(init_error_message);
@@ -162,6 +170,11 @@ void initialize_openxr_module(ModuleInitializationLevel p_level) {
GDREGISTER_CLASS(OpenXRIPBinding);
GDREGISTER_CLASS(OpenXRInteractionProfile);
+ GDREGISTER_ABSTRACT_CLASS(OpenXRCompositionLayer);
+ GDREGISTER_CLASS(OpenXRCompositionLayerEquirect);
+ GDREGISTER_CLASS(OpenXRCompositionLayerCylinder);
+ GDREGISTER_CLASS(OpenXRCompositionLayerQuad);
+
GDREGISTER_CLASS(OpenXRHand);
XRServer *xr_server = XRServer::get_singleton();
diff --git a/modules/openxr/scene/openxr_composition_layer.cpp b/modules/openxr/scene/openxr_composition_layer.cpp
new file mode 100644
index 0000000000..ce883b79b3
--- /dev/null
+++ b/modules/openxr/scene/openxr_composition_layer.cpp
@@ -0,0 +1,351 @@
+/**************************************************************************/
+/* openxr_composition_layer.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 "openxr_composition_layer.h"
+
+#include "../extensions/openxr_composition_layer_extension.h"
+#include "../openxr_api.h"
+#include "../openxr_interface.h"
+
+#include "scene/3d/mesh_instance_3d.h"
+#include "scene/3d/xr_nodes.h"
+#include "scene/main/viewport.h"
+
+HashSet<SubViewport *> OpenXRCompositionLayer::viewports_in_use;
+
+OpenXRCompositionLayer::OpenXRCompositionLayer() {
+ openxr_api = OpenXRAPI::get_singleton();
+ composition_layer_extension = OpenXRCompositionLayerExtension::get_singleton();
+
+ Ref<OpenXRInterface> openxr_interface = XRServer::get_singleton()->find_interface("OpenXR");
+ if (openxr_interface.is_valid()) {
+ openxr_interface->connect("session_begun", callable_mp(this, &OpenXRCompositionLayer::_on_openxr_session_begun));
+ openxr_interface->connect("session_stopping", callable_mp(this, &OpenXRCompositionLayer::_on_openxr_session_stopping));
+ }
+
+ set_process_internal(true);
+ set_notify_local_transform(true);
+
+ if (Engine::get_singleton()->is_editor_hint()) {
+ // In the editor, create the fallback right away.
+ _create_fallback_node();
+ }
+}
+
+OpenXRCompositionLayer::~OpenXRCompositionLayer() {
+ Ref<OpenXRInterface> openxr_interface = XRServer::get_singleton()->find_interface("OpenXR");
+ if (openxr_interface.is_valid()) {
+ openxr_interface->disconnect("session_begun", callable_mp(this, &OpenXRCompositionLayer::_on_openxr_session_begun));
+ openxr_interface->disconnect("session_stopping", callable_mp(this, &OpenXRCompositionLayer::_on_openxr_session_stopping));
+ }
+
+ if (layer_viewport) {
+ viewports_in_use.erase(layer_viewport);
+ }
+
+ if (openxr_layer_provider != nullptr) {
+ memdelete(openxr_layer_provider);
+ openxr_layer_provider = nullptr;
+ }
+}
+
+void OpenXRCompositionLayer::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_layer_viewport", "viewport"), &OpenXRCompositionLayer::set_layer_viewport);
+ ClassDB::bind_method(D_METHOD("get_layer_viewport"), &OpenXRCompositionLayer::get_layer_viewport);
+
+ ClassDB::bind_method(D_METHOD("set_sort_order", "order"), &OpenXRCompositionLayer::set_sort_order);
+ ClassDB::bind_method(D_METHOD("get_sort_order"), &OpenXRCompositionLayer::get_sort_order);
+
+ ClassDB::bind_method(D_METHOD("set_alpha_blend", "enabled"), &OpenXRCompositionLayer::set_alpha_blend);
+ ClassDB::bind_method(D_METHOD("get_alpha_blend"), &OpenXRCompositionLayer::get_alpha_blend);
+
+ ClassDB::bind_method(D_METHOD("is_natively_supported"), &OpenXRCompositionLayer::is_natively_supported);
+
+ ClassDB::bind_method(D_METHOD("intersects_ray", "origin", "direction"), &OpenXRCompositionLayer::intersects_ray);
+
+ ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "layer_viewport", PROPERTY_HINT_NODE_TYPE, "SubViewport"), "set_layer_viewport", "get_layer_viewport");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "sort_order", PROPERTY_HINT_NONE, ""), "set_sort_order", "get_sort_order");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "alpha_blend", PROPERTY_HINT_NONE, ""), "set_alpha_blend", "get_alpha_blend");
+}
+
+void OpenXRCompositionLayer::_create_fallback_node() {
+ ERR_FAIL_COND(fallback);
+ fallback = memnew(MeshInstance3D);
+ fallback->set_cast_shadows_setting(GeometryInstance3D::SHADOW_CASTING_SETTING_OFF);
+ add_child(fallback, false, INTERNAL_MODE_FRONT);
+ should_update_fallback_mesh = true;
+}
+
+void OpenXRCompositionLayer::_on_openxr_session_begun() {
+ if (!is_natively_supported()) {
+ if (!fallback) {
+ _create_fallback_node();
+ }
+ } else if (layer_viewport && is_visible() && is_inside_tree()) {
+ openxr_layer_provider->set_viewport(layer_viewport->get_viewport_rid(), layer_viewport->get_size());
+ }
+}
+
+void OpenXRCompositionLayer::_on_openxr_session_stopping() {
+ if (fallback && !Engine::get_singleton()->is_editor_hint()) {
+ fallback->queue_free();
+ remove_child(fallback);
+ fallback = nullptr;
+ } else {
+ openxr_layer_provider->set_viewport(RID(), Size2i());
+ }
+}
+
+void OpenXRCompositionLayer::update_fallback_mesh() {
+ should_update_fallback_mesh = true;
+}
+
+void OpenXRCompositionLayer::set_layer_viewport(SubViewport *p_viewport) {
+ if (layer_viewport == p_viewport) {
+ return;
+ }
+
+ ERR_FAIL_COND_EDMSG(viewports_in_use.has(p_viewport), RTR("Cannot use the same SubViewport with multiple OpenXR composition layers. Clear it from its current layer first."));
+
+ if (layer_viewport) {
+ viewports_in_use.erase(layer_viewport);
+ }
+
+ layer_viewport = p_viewport;
+
+ if (layer_viewport) {
+ viewports_in_use.insert(layer_viewport);
+
+ SubViewport::UpdateMode update_mode = layer_viewport->get_update_mode();
+ if (update_mode == SubViewport::UPDATE_WHEN_VISIBLE || update_mode == SubViewport::UPDATE_WHEN_PARENT_VISIBLE) {
+ WARN_PRINT_ONCE("OpenXR composition layers cannot use SubViewports with UPDATE_WHEN_VISIBLE or UPDATE_WHEN_PARENT_VISIBLE. Switching to UPDATE_ALWAYS.");
+ layer_viewport->set_update_mode(SubViewport::UPDATE_ALWAYS);
+ }
+ }
+
+ if (fallback) {
+ _reset_fallback_material();
+ } else if (openxr_api && openxr_api->is_running() && is_visible() && is_inside_tree()) {
+ if (layer_viewport) {
+ openxr_layer_provider->set_viewport(layer_viewport->get_viewport_rid(), layer_viewport->get_size());
+ } else {
+ openxr_layer_provider->set_viewport(RID(), Size2i());
+ }
+ }
+}
+
+SubViewport *OpenXRCompositionLayer::get_layer_viewport() const {
+ return layer_viewport;
+}
+
+void OpenXRCompositionLayer::set_sort_order(int p_order) {
+ if (openxr_layer_provider) {
+ openxr_layer_provider->set_sort_order(p_order);
+ }
+}
+
+int OpenXRCompositionLayer::get_sort_order() const {
+ if (openxr_layer_provider) {
+ return openxr_layer_provider->get_sort_order();
+ }
+ return 1;
+}
+
+void OpenXRCompositionLayer::set_alpha_blend(bool p_alpha_blend) {
+ if (openxr_layer_provider) {
+ openxr_layer_provider->set_alpha_blend(p_alpha_blend);
+ if (fallback) {
+ _reset_fallback_material();
+ }
+ }
+}
+
+bool OpenXRCompositionLayer::get_alpha_blend() const {
+ if (openxr_layer_provider) {
+ return openxr_layer_provider->get_alpha_blend();
+ }
+ return false;
+}
+
+bool OpenXRCompositionLayer::is_natively_supported() const {
+ if (composition_layer_extension) {
+ return composition_layer_extension->is_available(openxr_layer_provider->get_openxr_type());
+ }
+ return false;
+}
+
+Vector2 OpenXRCompositionLayer::intersects_ray(const Vector3 &p_origin, const Vector3 &p_direction) const {
+ return Vector2(-1.0, -1.0);
+}
+
+void OpenXRCompositionLayer::_reset_fallback_material() {
+ ERR_FAIL_NULL(fallback);
+
+ if (fallback->get_mesh().is_null()) {
+ return;
+ }
+
+ if (layer_viewport) {
+ Ref<StandardMaterial3D> material = fallback->get_surface_override_material(0);
+ if (material.is_null()) {
+ material.instantiate();
+ material->set_shading_mode(StandardMaterial3D::SHADING_MODE_UNSHADED);
+ material->set_flag(StandardMaterial3D::FLAG_DISABLE_DEPTH_TEST, true);
+ material->set_local_to_scene(true);
+ fallback->set_surface_override_material(0, material);
+ }
+ material->set_transparency(get_alpha_blend() ? StandardMaterial3D::TRANSPARENCY_ALPHA : StandardMaterial3D::TRANSPARENCY_DISABLED);
+
+ Ref<ViewportTexture> texture = material->get_texture(StandardMaterial3D::TEXTURE_ALBEDO);
+ if (texture.is_null()) {
+ texture.instantiate();
+ // ViewportTexture can't be configured without a local scene, so use this hack to set it.
+ HashMap<Ref<Resource>, Ref<Resource>> remap_cache;
+ texture->configure_for_local_scene(this, remap_cache);
+ }
+
+ Node *loc_scene = texture->get_local_scene();
+ NodePath viewport_path = loc_scene->get_path_to(layer_viewport);
+ texture->set_viewport_path_in_scene(viewport_path);
+ material->set_texture(StandardMaterial3D::TEXTURE_ALBEDO, texture);
+ } else {
+ fallback->set_surface_override_material(0, Ref<Material>());
+ }
+}
+
+void OpenXRCompositionLayer::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_POSTINITIALIZE: {
+ if (openxr_layer_provider) {
+ for (OpenXRExtensionWrapper *extension : OpenXRAPI::get_registered_extension_wrappers()) {
+ extension_property_values.merge(extension->get_viewport_composition_layer_extension_property_defaults());
+ }
+ openxr_layer_provider->set_extension_property_values(extension_property_values);
+ }
+ } break;
+ case NOTIFICATION_INTERNAL_PROCESS: {
+ if (fallback) {
+ if (should_update_fallback_mesh) {
+ fallback->set_mesh(_create_fallback_mesh());
+ _reset_fallback_material();
+ should_update_fallback_mesh = false;
+ }
+ }
+ } break;
+ case NOTIFICATION_VISIBILITY_CHANGED: {
+ if (!fallback && openxr_api && openxr_api->is_running() && is_inside_tree()) {
+ if (layer_viewport && is_visible()) {
+ openxr_layer_provider->set_viewport(layer_viewport->get_viewport_rid(), layer_viewport->get_size());
+ } else {
+ openxr_layer_provider->set_viewport(RID(), Size2i());
+ }
+ }
+ update_configuration_warnings();
+ } break;
+ case NOTIFICATION_LOCAL_TRANSFORM_CHANGED: {
+ update_configuration_warnings();
+ } break;
+ case NOTIFICATION_ENTER_TREE: {
+ if (composition_layer_extension) {
+ composition_layer_extension->register_viewport_composition_layer_provider(openxr_layer_provider);
+ }
+
+ if (!fallback && layer_viewport && openxr_api && openxr_api->is_running() && is_visible()) {
+ openxr_layer_provider->set_viewport(layer_viewport->get_viewport_rid(), layer_viewport->get_size());
+ }
+ } break;
+ case NOTIFICATION_EXIT_TREE: {
+ if (composition_layer_extension) {
+ composition_layer_extension->unregister_viewport_composition_layer_provider(openxr_layer_provider);
+ }
+
+ // When a node is removed in the editor, we need to clear the layer viewport, because otherwise
+ // there will be issues with the tracking in viewports_in_use, since nodes deleted in the editor
+ // aren't really deleted in order to support undo.
+ if (Engine::get_singleton()->is_editor_hint() && layer_viewport) {
+ set_layer_viewport(nullptr);
+ } else if (!fallback) {
+ // This will clean up existing resources.
+ openxr_layer_provider->set_viewport(RID(), Size2i());
+ }
+ } break;
+ }
+}
+
+void OpenXRCompositionLayer::_get_property_list(List<PropertyInfo> *p_property_list) const {
+ List<PropertyInfo> extension_properties;
+ for (OpenXRExtensionWrapper *extension : OpenXRAPI::get_registered_extension_wrappers()) {
+ extension->get_viewport_composition_layer_extension_properties(&extension_properties);
+ }
+
+ for (const PropertyInfo &pinfo : extension_properties) {
+ StringName prop_name = pinfo.name;
+ if (!String(prop_name).contains("/")) {
+ WARN_PRINT_ONCE(vformat("Discarding OpenXRCompositionLayer property name '%s' from extension because it doesn't contain a '/'."));
+ continue;
+ }
+ p_property_list->push_back(pinfo);
+ }
+}
+
+bool OpenXRCompositionLayer::_get(const StringName &p_property, Variant &r_value) const {
+ if (extension_property_values.has(p_property)) {
+ r_value = extension_property_values[p_property];
+ }
+
+ return true;
+}
+
+bool OpenXRCompositionLayer::_set(const StringName &p_property, const Variant &p_value) {
+ extension_property_values[p_property] = p_value;
+
+ if (openxr_layer_provider) {
+ openxr_layer_provider->set_extension_property_values(extension_property_values);
+ }
+
+ return true;
+}
+
+PackedStringArray OpenXRCompositionLayer::get_configuration_warnings() const {
+ PackedStringArray warnings = Node3D::get_configuration_warnings();
+
+ if (is_visible() && is_inside_tree()) {
+ XROrigin3D *origin = Object::cast_to<XROrigin3D>(get_parent());
+ if (origin == nullptr) {
+ warnings.push_back(RTR("OpenXR composition layers must have an XROrigin3D node as their parent."));
+ }
+ }
+
+ if (!get_transform().basis.is_orthonormal()) {
+ warnings.push_back(RTR("OpenXR composition layers must have orthonormalized transforms (ie. no scale or shearing)."));
+ }
+
+ return warnings;
+}
diff --git a/modules/openxr/scene/openxr_composition_layer.h b/modules/openxr/scene/openxr_composition_layer.h
new file mode 100644
index 0000000000..9f064379d3
--- /dev/null
+++ b/modules/openxr/scene/openxr_composition_layer.h
@@ -0,0 +1,98 @@
+/**************************************************************************/
+/* openxr_composition_layer.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 OPENXR_COMPOSITION_LAYER_H
+#define OPENXR_COMPOSITION_LAYER_H
+
+#include <openxr/openxr.h>
+
+#include "scene/3d/node_3d.h"
+
+class MeshInstance3D;
+class Mesh;
+class OpenXRAPI;
+class OpenXRCompositionLayerExtension;
+class OpenXRViewportCompositionLayerProvider;
+class SubViewport;
+
+class OpenXRCompositionLayer : public Node3D {
+ GDCLASS(OpenXRCompositionLayer, Node3D);
+
+ SubViewport *layer_viewport = nullptr;
+ MeshInstance3D *fallback = nullptr;
+ bool should_update_fallback_mesh = false;
+
+ Dictionary extension_property_values;
+
+ void _create_fallback_node();
+ void _reset_fallback_material();
+
+protected:
+ OpenXRAPI *openxr_api = nullptr;
+ OpenXRCompositionLayerExtension *composition_layer_extension = nullptr;
+ OpenXRViewportCompositionLayerProvider *openxr_layer_provider = nullptr;
+
+ static void _bind_methods();
+
+ void _notification(int p_what);
+ void _get_property_list(List<PropertyInfo> *p_property_list) const;
+ bool _get(const StringName &p_property, Variant &r_value) const;
+ bool _set(const StringName &p_property, const Variant &p_value);
+
+ virtual void _on_openxr_session_begun();
+ virtual void _on_openxr_session_stopping();
+
+ virtual Ref<Mesh> _create_fallback_mesh() = 0;
+
+ void update_fallback_mesh();
+
+ static HashSet<SubViewport *> viewports_in_use;
+
+public:
+ void set_layer_viewport(SubViewport *p_viewport);
+ SubViewport *get_layer_viewport() const;
+
+ void set_sort_order(int p_order);
+ int get_sort_order() const;
+
+ void set_alpha_blend(bool p_alpha_blend);
+ bool get_alpha_blend() const;
+
+ bool is_natively_supported() const;
+
+ virtual PackedStringArray get_configuration_warnings() const override;
+
+ virtual Vector2 intersects_ray(const Vector3 &p_origin, const Vector3 &p_direction) const;
+
+ OpenXRCompositionLayer();
+ ~OpenXRCompositionLayer();
+};
+
+#endif // OPENXR_COMPOSITION_LAYER_H
diff --git a/modules/openxr/scene/openxr_composition_layer_cylinder.cpp b/modules/openxr/scene/openxr_composition_layer_cylinder.cpp
new file mode 100644
index 0000000000..ae5a8da5f3
--- /dev/null
+++ b/modules/openxr/scene/openxr_composition_layer_cylinder.cpp
@@ -0,0 +1,235 @@
+/**************************************************************************/
+/* openxr_composition_layer_cylinder.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 "openxr_composition_layer_cylinder.h"
+
+#include "../extensions/openxr_composition_layer_extension.h"
+#include "../openxr_api.h"
+#include "../openxr_interface.h"
+
+#include "scene/3d/mesh_instance_3d.h"
+#include "scene/main/viewport.h"
+#include "scene/resources/mesh.h"
+
+OpenXRCompositionLayerCylinder::OpenXRCompositionLayerCylinder() {
+ composition_layer = {
+ XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR, // type
+ nullptr, // next
+ 0, // layerFlags
+ XR_NULL_HANDLE, // space
+ XR_EYE_VISIBILITY_BOTH, // eyeVisibility
+ {}, // subImage
+ { { 0, 0, 0, 0 }, { 0, 0, 0 } }, // pose
+ radius, // radius
+ central_angle, // centralAngle
+ aspect_ratio, // aspectRatio
+ };
+ openxr_layer_provider = memnew(OpenXRViewportCompositionLayerProvider((XrCompositionLayerBaseHeader *)&composition_layer));
+}
+
+OpenXRCompositionLayerCylinder::~OpenXRCompositionLayerCylinder() {
+}
+
+void OpenXRCompositionLayerCylinder::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_radius", "radius"), &OpenXRCompositionLayerCylinder::set_radius);
+ ClassDB::bind_method(D_METHOD("get_radius"), &OpenXRCompositionLayerCylinder::get_radius);
+
+ ClassDB::bind_method(D_METHOD("set_aspect_ratio", "aspect_ratio"), &OpenXRCompositionLayerCylinder::set_aspect_ratio);
+ ClassDB::bind_method(D_METHOD("get_aspect_ratio"), &OpenXRCompositionLayerCylinder::get_aspect_ratio);
+
+ ClassDB::bind_method(D_METHOD("set_central_angle", "angle"), &OpenXRCompositionLayerCylinder::set_central_angle);
+ ClassDB::bind_method(D_METHOD("get_central_angle"), &OpenXRCompositionLayerCylinder::get_central_angle);
+
+ ClassDB::bind_method(D_METHOD("set_fallback_segments", "segments"), &OpenXRCompositionLayerCylinder::set_fallback_segments);
+ ClassDB::bind_method(D_METHOD("get_fallback_segments"), &OpenXRCompositionLayerCylinder::get_fallback_segments);
+
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "radius", PROPERTY_HINT_NONE, ""), "set_radius", "get_radius");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "aspect_ratio", PROPERTY_HINT_RANGE, "0,100"), "set_aspect_ratio", "get_aspect_ratio");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "central_angle", PROPERTY_HINT_RANGE, "0,360,0.1,or_less,or_greater,radians_as_degrees"), "set_central_angle", "get_central_angle");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "fallback_segments", PROPERTY_HINT_NONE, ""), "set_fallback_segments", "get_fallback_segments");
+}
+
+void OpenXRCompositionLayerCylinder::_on_openxr_session_begun() {
+ OpenXRCompositionLayer::_on_openxr_session_begun();
+ if (openxr_api) {
+ composition_layer.space = openxr_api->get_play_space();
+ }
+}
+
+Ref<Mesh> OpenXRCompositionLayerCylinder::_create_fallback_mesh() {
+ Ref<ArrayMesh> mesh;
+ mesh.instantiate();
+
+ float arc_length = radius * central_angle;
+ float half_height = ((1.0 / aspect_ratio) * arc_length) / 2.0;
+
+ Array arrays;
+ arrays.resize(ArrayMesh::ARRAY_MAX);
+
+ Vector<Vector3> vertices;
+ Vector<Vector3> normals;
+ Vector<Vector2> uvs;
+ Vector<int> indices;
+
+ float delta_angle = central_angle / fallback_segments;
+ float start_angle = (-Math_PI / 2.0) - (central_angle / 2.0);
+
+ for (uint32_t i = 0; i < fallback_segments + 1; i++) {
+ float current_angle = start_angle + (delta_angle * i);
+ float x = radius * Math::cos(current_angle);
+ float z = radius * Math::sin(current_angle);
+ Vector3 normal(Math::cos(current_angle), 0, Math::sin(current_angle));
+
+ vertices.push_back(Vector3(x, -half_height, z));
+ normals.push_back(normal);
+ uvs.push_back(Vector2((float)i / fallback_segments, 1));
+
+ vertices.push_back(Vector3(x, half_height, z));
+ normals.push_back(normal);
+ uvs.push_back(Vector2((float)i / fallback_segments, 0));
+ }
+
+ for (uint32_t i = 0; i < fallback_segments; i++) {
+ uint32_t index = i * 2;
+ indices.push_back(index);
+ indices.push_back(index + 1);
+ indices.push_back(index + 3);
+ indices.push_back(index);
+ indices.push_back(index + 3);
+ indices.push_back(index + 2);
+ }
+
+ arrays[ArrayMesh::ARRAY_VERTEX] = vertices;
+ arrays[ArrayMesh::ARRAY_NORMAL] = normals;
+ arrays[ArrayMesh::ARRAY_TEX_UV] = uvs;
+ arrays[ArrayMesh::ARRAY_INDEX] = indices;
+
+ mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arrays);
+ return mesh;
+}
+
+void OpenXRCompositionLayerCylinder::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_LOCAL_TRANSFORM_CHANGED: {
+ Transform3D transform = get_transform();
+ Quaternion quat(transform.basis.orthonormalized());
+ composition_layer.pose.orientation = { (float)quat.x, (float)quat.y, (float)quat.z, (float)quat.w };
+ composition_layer.pose.position = { (float)transform.origin.x, (float)transform.origin.y, (float)transform.origin.z };
+ } break;
+ }
+}
+
+void OpenXRCompositionLayerCylinder::set_radius(float p_radius) {
+ ERR_FAIL_COND(p_radius <= 0);
+ radius = p_radius;
+ composition_layer.radius = radius;
+ update_fallback_mesh();
+}
+
+float OpenXRCompositionLayerCylinder::get_radius() const {
+ return radius;
+}
+
+void OpenXRCompositionLayerCylinder::set_aspect_ratio(float p_aspect_ratio) {
+ ERR_FAIL_COND(p_aspect_ratio <= 0);
+ aspect_ratio = p_aspect_ratio;
+ composition_layer.aspectRatio = aspect_ratio;
+ update_fallback_mesh();
+}
+
+float OpenXRCompositionLayerCylinder::get_aspect_ratio() const {
+ return aspect_ratio;
+}
+
+void OpenXRCompositionLayerCylinder::set_central_angle(float p_central_angle) {
+ ERR_FAIL_COND(p_central_angle <= 0);
+ central_angle = p_central_angle;
+ composition_layer.centralAngle = central_angle;
+ update_fallback_mesh();
+}
+
+float OpenXRCompositionLayerCylinder::get_central_angle() const {
+ return central_angle;
+}
+
+void OpenXRCompositionLayerCylinder::set_fallback_segments(uint32_t p_fallback_segments) {
+ ERR_FAIL_COND(p_fallback_segments == 0);
+ fallback_segments = p_fallback_segments;
+ update_fallback_mesh();
+}
+
+uint32_t OpenXRCompositionLayerCylinder::get_fallback_segments() const {
+ return fallback_segments;
+}
+
+Vector2 OpenXRCompositionLayerCylinder::intersects_ray(const Vector3 &p_origin, const Vector3 &p_direction) const {
+ Transform3D cylinder_transform = get_global_transform();
+ Vector3 cylinder_axis = cylinder_transform.basis.get_column(1);
+
+ Vector3 offset = p_origin - cylinder_transform.origin;
+ float a = p_direction.dot(p_direction - cylinder_axis * p_direction.dot(cylinder_axis));
+ float b = 2.0 * (p_direction.dot(offset - cylinder_axis * offset.dot(cylinder_axis)));
+ float c = offset.dot(offset - cylinder_axis * offset.dot(cylinder_axis)) - (radius * radius);
+
+ float discriminant = b * b - 4.0 * a * c;
+ if (discriminant < 0.0) {
+ return Vector2(-1.0, -1.0);
+ }
+
+ float t0 = (-b - Math::sqrt(discriminant)) / (2.0 * a);
+ float t1 = (-b + Math::sqrt(discriminant)) / (2.0 * a);
+ float t = MAX(t0, t1);
+
+ if (t < 0.0) {
+ return Vector2(-1.0, -1.0);
+ }
+ Vector3 intersection = p_origin + p_direction * t;
+
+ Basis correction = cylinder_transform.basis.inverse();
+ correction.rotate(Vector3(0.0, 1.0, 0.0), -Math_PI / 2.0);
+ Vector3 relative_point = correction.xform(intersection - cylinder_transform.origin);
+
+ Vector2 projected_point = Vector2(relative_point.x, relative_point.z);
+ float intersection_angle = Math::atan2(projected_point.y, projected_point.x);
+ if (Math::abs(intersection_angle) > central_angle / 2.0) {
+ return Vector2(-1.0, -1.0);
+ }
+
+ float arc_length = radius * central_angle;
+ float height = aspect_ratio * arc_length;
+ if (Math::abs(relative_point.y) > height / 2.0) {
+ return Vector2(-1.0, -1.0);
+ }
+
+ float u = 0.5 + (intersection_angle / central_angle);
+ float v = 1.0 - (0.5 + (relative_point.y / height));
+
+ return Vector2(u, v);
+}
diff --git a/modules/openxr/scene/openxr_composition_layer_cylinder.h b/modules/openxr/scene/openxr_composition_layer_cylinder.h
new file mode 100644
index 0000000000..aed0fabd78
--- /dev/null
+++ b/modules/openxr/scene/openxr_composition_layer_cylinder.h
@@ -0,0 +1,75 @@
+/**************************************************************************/
+/* openxr_composition_layer_cylinder.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 OPENXR_COMPOSITION_LAYER_CYLINDER_H
+#define OPENXR_COMPOSITION_LAYER_CYLINDER_H
+
+#include <openxr/openxr.h>
+
+#include "openxr_composition_layer.h"
+
+class OpenXRCompositionLayerCylinder : public OpenXRCompositionLayer {
+ GDCLASS(OpenXRCompositionLayerCylinder, OpenXRCompositionLayer);
+
+ XrCompositionLayerCylinderKHR composition_layer;
+
+ float radius = 1.0;
+ float aspect_ratio = 1.0;
+ float central_angle = Math_PI / 2.0;
+ uint32_t fallback_segments = 10;
+
+protected:
+ static void _bind_methods();
+
+ void _notification(int p_what);
+
+ virtual void _on_openxr_session_begun() override;
+ virtual Ref<Mesh> _create_fallback_mesh() override;
+
+public:
+ void set_radius(float p_radius);
+ float get_radius() const;
+
+ void set_aspect_ratio(float p_aspect_ratio);
+ float get_aspect_ratio() const;
+
+ void set_central_angle(float p_angle);
+ float get_central_angle() const;
+
+ void set_fallback_segments(uint32_t p_fallback_segments);
+ uint32_t get_fallback_segments() const;
+
+ virtual Vector2 intersects_ray(const Vector3 &p_origin, const Vector3 &p_direction) const override;
+
+ OpenXRCompositionLayerCylinder();
+ ~OpenXRCompositionLayerCylinder();
+};
+
+#endif // OPENXR_COMPOSITION_LAYER_CYLINDER_H
diff --git a/modules/openxr/scene/openxr_composition_layer_equirect.cpp b/modules/openxr/scene/openxr_composition_layer_equirect.cpp
new file mode 100644
index 0000000000..d67e71443c
--- /dev/null
+++ b/modules/openxr/scene/openxr_composition_layer_equirect.cpp
@@ -0,0 +1,260 @@
+/**************************************************************************/
+/* openxr_composition_layer_equirect.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 "openxr_composition_layer_equirect.h"
+
+#include "../extensions/openxr_composition_layer_extension.h"
+#include "../openxr_api.h"
+#include "../openxr_interface.h"
+
+#include "scene/3d/mesh_instance_3d.h"
+#include "scene/main/viewport.h"
+#include "scene/resources/mesh.h"
+
+OpenXRCompositionLayerEquirect::OpenXRCompositionLayerEquirect() {
+ composition_layer = {
+ XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR, // type
+ nullptr, // next
+ 0, // layerFlags
+ XR_NULL_HANDLE, // space
+ XR_EYE_VISIBILITY_BOTH, // eyeVisibility
+ {}, // subImage
+ { { 0, 0, 0, 0 }, { 0, 0, 0 } }, // pose
+ radius, // radius
+ central_horizontal_angle, // centralHorizontalAngle
+ upper_vertical_angle, // upperVerticalAngle
+ -lower_vertical_angle, // lowerVerticalAngle
+ };
+ openxr_layer_provider = memnew(OpenXRViewportCompositionLayerProvider((XrCompositionLayerBaseHeader *)&composition_layer));
+}
+
+OpenXRCompositionLayerEquirect::~OpenXRCompositionLayerEquirect() {
+}
+
+void OpenXRCompositionLayerEquirect::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_radius", "radius"), &OpenXRCompositionLayerEquirect::set_radius);
+ ClassDB::bind_method(D_METHOD("get_radius"), &OpenXRCompositionLayerEquirect::get_radius);
+
+ ClassDB::bind_method(D_METHOD("set_central_horizontal_angle", "angle"), &OpenXRCompositionLayerEquirect::set_central_horizontal_angle);
+ ClassDB::bind_method(D_METHOD("get_central_horizontal_angle"), &OpenXRCompositionLayerEquirect::get_central_horizontal_angle);
+
+ ClassDB::bind_method(D_METHOD("set_upper_vertical_angle", "angle"), &OpenXRCompositionLayerEquirect::set_upper_vertical_angle);
+ ClassDB::bind_method(D_METHOD("get_upper_vertical_angle"), &OpenXRCompositionLayerEquirect::get_upper_vertical_angle);
+
+ ClassDB::bind_method(D_METHOD("set_lower_vertical_angle", "angle"), &OpenXRCompositionLayerEquirect::set_lower_vertical_angle);
+ ClassDB::bind_method(D_METHOD("get_lower_vertical_angle"), &OpenXRCompositionLayerEquirect::get_lower_vertical_angle);
+
+ ClassDB::bind_method(D_METHOD("set_fallback_segments", "segments"), &OpenXRCompositionLayerEquirect::set_fallback_segments);
+ ClassDB::bind_method(D_METHOD("get_fallback_segments"), &OpenXRCompositionLayerEquirect::get_fallback_segments);
+
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "radius", PROPERTY_HINT_NONE, ""), "set_radius", "get_radius");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "central_horizontal_angle", PROPERTY_HINT_RANGE, "0,360,0.1,or_less,or_greater,radians_as_degrees"), "set_central_horizontal_angle", "get_central_horizontal_angle");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "upper_vertical_angle", PROPERTY_HINT_RANGE, "0,90,0.1,or_less,or_greater,radians_as_degrees"), "set_upper_vertical_angle", "get_upper_vertical_angle");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lower_vertical_angle", PROPERTY_HINT_RANGE, "0,90,0.1,or_less,or_greater,radians_as_degrees"), "set_lower_vertical_angle", "get_lower_vertical_angle");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "fallback_segments", PROPERTY_HINT_NONE, ""), "set_fallback_segments", "get_fallback_segments");
+}
+
+void OpenXRCompositionLayerEquirect::_on_openxr_session_begun() {
+ OpenXRCompositionLayer::_on_openxr_session_begun();
+ if (openxr_api) {
+ composition_layer.space = openxr_api->get_play_space();
+ }
+}
+
+Ref<Mesh> OpenXRCompositionLayerEquirect::_create_fallback_mesh() {
+ Ref<ArrayMesh> mesh;
+ mesh.instantiate();
+
+ Array arrays;
+ arrays.resize(ArrayMesh::ARRAY_MAX);
+
+ Vector<Vector3> vertices;
+ Vector<Vector3> normals;
+ Vector<Vector2> uvs;
+ Vector<int> indices;
+
+ float step_horizontal = central_horizontal_angle / fallback_segments;
+ float step_vertical = (upper_vertical_angle + lower_vertical_angle) / fallback_segments;
+
+ float start_horizontal_angle = Math_PI - (central_horizontal_angle / 2.0);
+
+ for (uint32_t i = 0; i < fallback_segments + 1; i++) {
+ for (uint32_t j = 0; j < fallback_segments + 1; j++) {
+ float horizontal_angle = start_horizontal_angle + (step_horizontal * i);
+ float vertical_angle = -lower_vertical_angle + (step_vertical * j);
+
+ Vector3 vertex(
+ radius * Math::cos(vertical_angle) * Math::sin(horizontal_angle),
+ radius * Math::sin(vertical_angle),
+ radius * Math::cos(vertical_angle) * Math::cos(horizontal_angle));
+
+ vertices.push_back(vertex);
+ normals.push_back(vertex.normalized());
+ uvs.push_back(Vector2(1.0 - ((float)i / fallback_segments), 1.0 - (float(j) / fallback_segments)));
+ }
+ }
+
+ for (uint32_t i = 0; i < fallback_segments; i++) {
+ for (uint32_t j = 0; j < fallback_segments; j++) {
+ uint32_t index = i * (fallback_segments + 1) + j;
+ indices.push_back(index);
+ indices.push_back(index + fallback_segments + 1);
+ indices.push_back(index + fallback_segments + 2);
+
+ indices.push_back(index);
+ indices.push_back(index + fallback_segments + 2);
+ indices.push_back(index + 1);
+ }
+ }
+
+ arrays[ArrayMesh::ARRAY_VERTEX] = vertices;
+ arrays[ArrayMesh::ARRAY_NORMAL] = normals;
+ arrays[ArrayMesh::ARRAY_TEX_UV] = uvs;
+ arrays[ArrayMesh::ARRAY_INDEX] = indices;
+
+ mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arrays);
+ return mesh;
+}
+
+void OpenXRCompositionLayerEquirect::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_LOCAL_TRANSFORM_CHANGED: {
+ Transform3D transform = get_transform();
+ Quaternion quat(transform.basis.orthonormalized());
+ composition_layer.pose.orientation = { (float)quat.x, (float)quat.y, (float)quat.z, (float)quat.w };
+ composition_layer.pose.position = { (float)transform.origin.x, (float)transform.origin.y, (float)transform.origin.z };
+ } break;
+ }
+}
+
+void OpenXRCompositionLayerEquirect::set_radius(float p_radius) {
+ ERR_FAIL_COND(p_radius <= 0);
+ radius = p_radius;
+ composition_layer.radius = radius;
+ update_fallback_mesh();
+}
+
+float OpenXRCompositionLayerEquirect::get_radius() const {
+ return radius;
+}
+
+void OpenXRCompositionLayerEquirect::set_central_horizontal_angle(float p_angle) {
+ ERR_FAIL_COND(p_angle <= 0);
+ central_horizontal_angle = p_angle;
+ composition_layer.centralHorizontalAngle = central_horizontal_angle;
+ update_fallback_mesh();
+}
+
+float OpenXRCompositionLayerEquirect::get_central_horizontal_angle() const {
+ return central_horizontal_angle;
+}
+
+void OpenXRCompositionLayerEquirect::set_upper_vertical_angle(float p_angle) {
+ ERR_FAIL_COND(p_angle <= 0 || p_angle > (Math_PI / 2.0));
+ upper_vertical_angle = p_angle;
+ composition_layer.upperVerticalAngle = p_angle;
+ update_fallback_mesh();
+}
+
+float OpenXRCompositionLayerEquirect::get_upper_vertical_angle() const {
+ return upper_vertical_angle;
+}
+
+void OpenXRCompositionLayerEquirect::set_lower_vertical_angle(float p_angle) {
+ ERR_FAIL_COND(p_angle <= 0 || p_angle > (Math_PI / 2.0));
+ lower_vertical_angle = p_angle;
+ composition_layer.lowerVerticalAngle = -p_angle;
+ update_fallback_mesh();
+}
+
+float OpenXRCompositionLayerEquirect::get_lower_vertical_angle() const {
+ return lower_vertical_angle;
+}
+
+void OpenXRCompositionLayerEquirect::set_fallback_segments(uint32_t p_fallback_segments) {
+ ERR_FAIL_COND(p_fallback_segments == 0);
+ fallback_segments = p_fallback_segments;
+ update_fallback_mesh();
+}
+
+uint32_t OpenXRCompositionLayerEquirect::get_fallback_segments() const {
+ return fallback_segments;
+}
+
+Vector2 OpenXRCompositionLayerEquirect::intersects_ray(const Vector3 &p_origin, const Vector3 &p_direction) const {
+ Transform3D equirect_transform = get_global_transform();
+
+ Vector3 offset = p_origin - equirect_transform.origin;
+ float a = p_direction.dot(p_direction);
+ float b = 2.0 * offset.dot(p_direction);
+ float c = offset.dot(offset) - (radius * radius);
+
+ float discriminant = b * b - 4.0 * a * c;
+ if (discriminant < 0.0) {
+ return Vector2(-1.0, -1.0);
+ }
+
+ float t0 = (-b - Math::sqrt(discriminant)) / (2.0 * a);
+ float t1 = (-b + Math::sqrt(discriminant)) / (2.0 * a);
+ float t = MAX(t0, t1);
+
+ if (t < 0.0) {
+ return Vector2(-1.0, -1.0);
+ }
+ Vector3 intersection = p_origin + p_direction * t;
+
+ Basis correction = equirect_transform.basis.inverse();
+ correction.rotate(Vector3(0.0, 1.0, 0.0), -Math_PI / 2.0);
+ Vector3 relative_point = correction.xform(intersection - equirect_transform.origin);
+
+ float horizontal_intersection_angle = Math::atan2(relative_point.z, relative_point.x);
+ if (Math::abs(horizontal_intersection_angle) > central_horizontal_angle / 2.0) {
+ return Vector2(-1.0, -1.0);
+ }
+
+ float vertical_intersection_angle = Math::acos(relative_point.y / radius) - (Math_PI / 2.0);
+ if (vertical_intersection_angle < 0) {
+ if (Math::abs(vertical_intersection_angle) > upper_vertical_angle) {
+ return Vector2(-1.0, -1.0);
+ }
+ } else if (vertical_intersection_angle > lower_vertical_angle) {
+ return Vector2(-1.0, -1.0);
+ }
+
+ // Re-center the intersection angle if the vertical angle is uneven between upper and lower.
+ if (upper_vertical_angle != lower_vertical_angle) {
+ vertical_intersection_angle -= (-upper_vertical_angle + lower_vertical_angle) / 2.0;
+ }
+
+ float u = 0.5 + (horizontal_intersection_angle / central_horizontal_angle);
+ float v = 0.5 + (vertical_intersection_angle / (upper_vertical_angle + lower_vertical_angle));
+
+ return Vector2(u, v);
+}
diff --git a/modules/openxr/scene/openxr_composition_layer_equirect.h b/modules/openxr/scene/openxr_composition_layer_equirect.h
new file mode 100644
index 0000000000..7a002a48dc
--- /dev/null
+++ b/modules/openxr/scene/openxr_composition_layer_equirect.h
@@ -0,0 +1,79 @@
+/**************************************************************************/
+/* openxr_composition_layer_equirect.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 OPENXR_COMPOSITION_LAYER_EQUIRECT_H
+#define OPENXR_COMPOSITION_LAYER_EQUIRECT_H
+
+#include <openxr/openxr.h>
+
+#include "openxr_composition_layer.h"
+
+class OpenXRCompositionLayerEquirect : public OpenXRCompositionLayer {
+ GDCLASS(OpenXRCompositionLayerEquirect, OpenXRCompositionLayer);
+
+ XrCompositionLayerEquirect2KHR composition_layer;
+
+ float radius = 1.0;
+ float central_horizontal_angle = Math_PI / 2.0;
+ float upper_vertical_angle = Math_PI / 4.0;
+ float lower_vertical_angle = Math_PI / 4.0;
+ uint32_t fallback_segments = 10;
+
+protected:
+ static void _bind_methods();
+
+ void _notification(int p_what);
+
+ virtual void _on_openxr_session_begun() override;
+ virtual Ref<Mesh> _create_fallback_mesh() override;
+
+public:
+ void set_radius(float p_radius);
+ float get_radius() const;
+
+ void set_central_horizontal_angle(float p_angle);
+ float get_central_horizontal_angle() const;
+
+ void set_upper_vertical_angle(float p_angle);
+ float get_upper_vertical_angle() const;
+
+ void set_lower_vertical_angle(float p_angle);
+ float get_lower_vertical_angle() const;
+
+ void set_fallback_segments(uint32_t p_fallback_segments);
+ uint32_t get_fallback_segments() const;
+
+ virtual Vector2 intersects_ray(const Vector3 &p_origin, const Vector3 &p_direction) const override;
+
+ OpenXRCompositionLayerEquirect();
+ ~OpenXRCompositionLayerEquirect();
+};
+
+#endif // OPENXR_COMPOSITION_LAYER_EQUIRECT_H
diff --git a/modules/openxr/scene/openxr_composition_layer_quad.cpp b/modules/openxr/scene/openxr_composition_layer_quad.cpp
new file mode 100644
index 0000000000..17d57851e4
--- /dev/null
+++ b/modules/openxr/scene/openxr_composition_layer_quad.cpp
@@ -0,0 +1,131 @@
+/**************************************************************************/
+/* openxr_composition_layer_quad.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 "openxr_composition_layer_quad.h"
+
+#include "../extensions/openxr_composition_layer_extension.h"
+#include "../openxr_api.h"
+#include "../openxr_interface.h"
+
+#include "scene/3d/mesh_instance_3d.h"
+#include "scene/main/viewport.h"
+#include "scene/resources/3d/primitive_meshes.h"
+
+OpenXRCompositionLayerQuad::OpenXRCompositionLayerQuad() {
+ composition_layer = {
+ XR_TYPE_COMPOSITION_LAYER_QUAD, // type
+ nullptr, // next
+ 0, // layerFlags
+ XR_NULL_HANDLE, // space
+ XR_EYE_VISIBILITY_BOTH, // eyeVisibility
+ {}, // subImage
+ { { 0, 0, 0, 0 }, { 0, 0, 0 } }, // pose
+ { (float)quad_size.x, (float)quad_size.y }, // size
+ };
+ openxr_layer_provider = memnew(OpenXRViewportCompositionLayerProvider((XrCompositionLayerBaseHeader *)&composition_layer));
+}
+
+OpenXRCompositionLayerQuad::~OpenXRCompositionLayerQuad() {
+}
+
+void OpenXRCompositionLayerQuad::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_quad_size", "size"), &OpenXRCompositionLayerQuad::set_quad_size);
+ ClassDB::bind_method(D_METHOD("get_quad_size"), &OpenXRCompositionLayerQuad::get_quad_size);
+
+ ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "quad_size", PROPERTY_HINT_NONE, ""), "set_quad_size", "get_quad_size");
+}
+
+void OpenXRCompositionLayerQuad::_on_openxr_session_begun() {
+ OpenXRCompositionLayer::_on_openxr_session_begun();
+ if (openxr_api) {
+ composition_layer.space = openxr_api->get_play_space();
+ }
+}
+
+Ref<Mesh> OpenXRCompositionLayerQuad::_create_fallback_mesh() {
+ Ref<QuadMesh> mesh;
+ mesh.instantiate();
+ mesh->set_size(quad_size);
+ return mesh;
+}
+
+void OpenXRCompositionLayerQuad::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_LOCAL_TRANSFORM_CHANGED: {
+ Transform3D transform = get_transform();
+ Quaternion quat(transform.basis.orthonormalized());
+ composition_layer.pose.orientation = { (float)quat.x, (float)quat.y, (float)quat.z, (float)quat.w };
+ composition_layer.pose.position = { (float)transform.origin.x, (float)transform.origin.y, (float)transform.origin.z };
+ } break;
+ }
+}
+
+void OpenXRCompositionLayerQuad::set_quad_size(const Size2 &p_size) {
+ quad_size = p_size;
+ composition_layer.size = { (float)quad_size.x, (float)quad_size.y };
+ update_fallback_mesh();
+}
+
+Size2 OpenXRCompositionLayerQuad::get_quad_size() const {
+ return quad_size;
+}
+
+Vector2 OpenXRCompositionLayerQuad::intersects_ray(const Vector3 &p_origin, const Vector3 &p_direction) const {
+ Transform3D quad_transform = get_global_transform();
+ Vector3 quad_normal = quad_transform.basis.get_column(2);
+
+ float denom = quad_normal.dot(p_direction);
+ if (Math::abs(denom) > 0.0001) {
+ Vector3 vector = quad_transform.origin - p_origin;
+ float t = vector.dot(quad_normal) / denom;
+ if (t < 0.0) {
+ return Vector2(-1.0, -1.0);
+ }
+ Vector3 intersection = p_origin + p_direction * t;
+
+ Vector3 relative_point = intersection - quad_transform.origin;
+ Vector2 projected_point = Vector2(
+ relative_point.dot(quad_transform.basis.get_column(0)),
+ relative_point.dot(quad_transform.basis.get_column(1)));
+ if (Math::abs(projected_point.x) > quad_size.x / 2.0) {
+ return Vector2(-1.0, -1.0);
+ }
+ if (Math::abs(projected_point.y) > quad_size.y / 2.0) {
+ return Vector2(-1.0, -1.0);
+ }
+
+ float u = 0.5 + (projected_point.x / quad_size.x);
+ float v = 1.0 - (0.5 + (projected_point.y / quad_size.y));
+
+ return Vector2(u, v);
+ }
+
+ return Vector2(-1.0, -1.0);
+}
diff --git a/modules/openxr/scene/openxr_composition_layer_quad.h b/modules/openxr/scene/openxr_composition_layer_quad.h
new file mode 100644
index 0000000000..d88b596984
--- /dev/null
+++ b/modules/openxr/scene/openxr_composition_layer_quad.h
@@ -0,0 +1,63 @@
+/**************************************************************************/
+/* openxr_composition_layer_quad.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 OPENXR_COMPOSITION_LAYER_QUAD_H
+#define OPENXR_COMPOSITION_LAYER_QUAD_H
+
+#include <openxr/openxr.h>
+
+#include "openxr_composition_layer.h"
+
+class OpenXRCompositionLayerQuad : public OpenXRCompositionLayer {
+ GDCLASS(OpenXRCompositionLayerQuad, OpenXRCompositionLayer);
+
+ XrCompositionLayerQuad composition_layer;
+
+ Size2 quad_size = Size2(1.0, 1.0);
+
+protected:
+ static void _bind_methods();
+
+ void _notification(int p_what);
+
+ virtual void _on_openxr_session_begun() override;
+ virtual Ref<Mesh> _create_fallback_mesh() override;
+
+public:
+ void set_quad_size(const Size2 &p_size);
+ Size2 get_quad_size() const;
+
+ virtual Vector2 intersects_ray(const Vector3 &p_origin, const Vector3 &p_direction) const override;
+
+ OpenXRCompositionLayerQuad();
+ ~OpenXRCompositionLayerQuad();
+};
+
+#endif // OPENXR_COMPOSITION_LAYER_QUAD_H
diff --git a/modules/openxr/util.h b/modules/openxr/util.h
index 7488b5cf8e..4c611d6f4a 100644
--- a/modules/openxr/util.h
+++ b/modules/openxr/util.h
@@ -34,21 +34,23 @@
#define UNPACK(...) __VA_ARGS__
#define INIT_XR_FUNC_V(openxr_api, name) \
- do { \
+ if constexpr (true) { \
XrResult get_instance_proc_addr_result; \
get_instance_proc_addr_result = openxr_api->get_instance_proc_addr(#name, (PFN_xrVoidFunction *)&name##_ptr); \
ERR_FAIL_COND_V(XR_FAILED(get_instance_proc_addr_result), false); \
- } while (0)
+ } else \
+ ((void)0)
#define EXT_INIT_XR_FUNC_V(name) INIT_XR_FUNC_V(OpenXRAPI::get_singleton(), name)
#define OPENXR_API_INIT_XR_FUNC_V(name) INIT_XR_FUNC_V(this, name)
#define INIT_XR_FUNC(openxr_api, name) \
- do { \
+ if constexpr (true) { \
XrResult get_instance_proc_addr_result; \
get_instance_proc_addr_result = openxr_api->get_instance_proc_addr(#name, (PFN_xrVoidFunction *)&name##_ptr); \
ERR_FAIL_COND(XR_FAILED(get_instance_proc_addr_result)); \
- } while (0)
+ } else \
+ ((void)0)
#define EXT_INIT_XR_FUNC(name) INIT_XR_FUNC(OpenXRAPI::get_singleton(), name)
#define OPENXR_API_INIT_XR_FUNC(name) INIT_XR_FUNC(this, name)
@@ -59,16 +61,18 @@
#define EXT_TRY_INIT_XR_FUNC(name) TRY_INIT_XR_FUNC(OpenXRAPI::get_singleton(), name)
#define OPENXR_TRY_API_INIT_XR_FUNC(name) TRY_INIT_XR_FUNC(this, name)
#define GDEXTENSION_INIT_XR_FUNC(name) \
- do { \
+ if constexpr (true) { \
name##_ptr = reinterpret_cast<PFN_##name>(get_openxr_api()->get_instance_proc_addr(#name)); \
ERR_FAIL_NULL(name##_ptr); \
- } while (0)
+ } else \
+ ((void)0)
#define GDEXTENSION_INIT_XR_FUNC_V(name) \
- do { \
+ if constexpr (true) { \
name##_ptr = reinterpret_cast<PFN_##name>(get_openxr_api()->get_instance_proc_addr(#name)); \
ERR_FAIL_NULL_V(name##_ptr, false); \
- } while (0)
+ } else \
+ ((void)0)
#define EXT_PROTO_XRRESULT_FUNC1(func_name, arg1_type, arg1) \
PFN_##func_name func_name##_ptr = nullptr; \
diff --git a/modules/raycast/SCsub b/modules/raycast/SCsub
index 209ebab388..f3a8e30763 100644
--- a/modules/raycast/SCsub
+++ b/modules/raycast/SCsub
@@ -15,10 +15,10 @@ if env["builtin_embree"]:
embree_src = [
"common/sys/sysinfo.cpp",
"common/sys/alloc.cpp",
+ "common/sys/estring.cpp",
"common/sys/filename.cpp",
"common/sys/library.cpp",
"common/sys/thread.cpp",
- "common/sys/string.cpp",
"common/sys/regression.cpp",
"common/sys/mutex.cpp",
"common/sys/condition.cpp",
@@ -36,6 +36,7 @@ if env["builtin_embree"]:
"kernels/common/rtcore.cpp",
"kernels/common/rtcore_builder.cpp",
"kernels/common/scene.cpp",
+ "kernels/common/scene_verify.cpp",
"kernels/common/alloc.cpp",
"kernels/common/geometry.cpp",
"kernels/common/scene_triangle_mesh.cpp",
@@ -56,8 +57,6 @@ if env["builtin_embree"]:
"kernels/bvh/bvh_builder_twolevel.cpp",
"kernels/bvh/bvh_intersector1_bvh4.cpp",
"kernels/bvh/bvh_intersector_hybrid4_bvh4.cpp",
- "kernels/bvh/bvh_intersector_stream_bvh4.cpp",
- "kernels/bvh/bvh_intersector_stream_filters.cpp",
]
thirdparty_sources = [thirdparty_dir + file for file in embree_src]
diff --git a/modules/raycast/godot_update_embree.py b/modules/raycast/godot_update_embree.py
index f7af937c8b..c179060365 100644
--- a/modules/raycast/godot_update_embree.py
+++ b/modules/raycast/godot_update_embree.py
@@ -1,6 +1,13 @@
-import glob, os, shutil, subprocess, re
+import glob
+import os
+import re
+import shutil
+import stat
+import subprocess
+from types import TracebackType
+from typing import Any, Callable, Tuple, Type
-git_tag = "v3.13.5"
+git_tag = "v4.3.1"
include_dirs = [
"common/tasking",
@@ -15,7 +22,7 @@ include_dirs = [
"common/simd",
"common/simd/arm",
"common/simd/wasm",
- "include/embree3",
+ "include/embree4",
"kernels/subdiv",
"kernels/geometry",
]
@@ -23,10 +30,10 @@ include_dirs = [
cpp_files = [
"common/sys/sysinfo.cpp",
"common/sys/alloc.cpp",
+ "common/sys/estring.cpp",
"common/sys/filename.cpp",
"common/sys/library.cpp",
"common/sys/thread.cpp",
- "common/sys/string.cpp",
"common/sys/regression.cpp",
"common/sys/mutex.cpp",
"common/sys/condition.cpp",
@@ -44,6 +51,7 @@ cpp_files = [
"kernels/common/rtcore.cpp",
"kernels/common/rtcore_builder.cpp",
"kernels/common/scene.cpp",
+ "kernels/common/scene_verify.cpp",
"kernels/common/alloc.cpp",
"kernels/common/geometry.cpp",
"kernels/common/scene_triangle_mesh.cpp",
@@ -65,26 +73,58 @@ cpp_files = [
"kernels/bvh/bvh_intersector1.cpp",
"kernels/bvh/bvh_intersector1_bvh4.cpp",
"kernels/bvh/bvh_intersector_hybrid4_bvh4.cpp",
- "kernels/bvh/bvh_intersector_stream_bvh4.cpp",
- "kernels/bvh/bvh_intersector_stream_filters.cpp",
"kernels/bvh/bvh_intersector_hybrid.cpp",
- "kernels/bvh/bvh_intersector_stream.cpp",
]
-os.chdir("../../thirdparty")
+config_files = [
+ "kernels/config.h.in",
+ "kernels/rtcore_config.h.in",
+]
+
+license_file = "LICENSE.txt"
+
+os.chdir(f"{os.path.dirname(__file__)}/../../thirdparty")
dir_name = "embree"
if os.path.exists(dir_name):
shutil.rmtree(dir_name)
+# In case something went wrong and embree-tmp stayed on the system.
+if os.path.exists("embree-tmp"):
+ shutil.rmtree("embree-tmp")
+
subprocess.run(["git", "clone", "https://github.com/embree/embree.git", "embree-tmp"])
os.chdir("embree-tmp")
subprocess.run(["git", "checkout", git_tag])
commit_hash = str(subprocess.check_output(["git", "rev-parse", "HEAD"], universal_newlines=True)).strip()
+
+def on_rm_error(
+ function: Callable[..., Any], path: str, excinfo: Tuple[Type[Exception], Exception, TracebackType]
+) -> None:
+ """
+ Error handler for `shutil.rmtree()`.
+
+ If the error is due to read-only files,
+ it will change the file permissions and retry.
+ """
+ os.chmod(path, stat.S_IWRITE)
+ os.unlink(path)
+
+
+# 3.12 Python and beyond should replace `onerror` with `onexc`.
+# We remove the .git directory because it contains
+# a lot of read-only files that are problematic on Windows.
+shutil.rmtree(".git", onerror=on_rm_error)
+
all_files = set(cpp_files)
+for config_file in config_files:
+ all_files.add(config_file)
+
+all_files.add(license_file)
+
dest_dir = os.path.join("..", dir_name)
for include_dir in include_dirs:
headers = glob.iglob(os.path.join(include_dir, "*.h"))
@@ -105,87 +145,8 @@ with open(os.path.join(dest_dir, "kernels/hash.h"), "w", encoding="utf-8", newli
"""
)
-with open(os.path.join(dest_dir, "kernels/config.h"), "w", encoding="utf-8", newline="\n") as config_file:
- config_file.write(
- """// Copyright 2009-2021 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-/* #undef EMBREE_RAY_MASK */
-/* #undef EMBREE_STAT_COUNTERS */
-/* #undef EMBREE_BACKFACE_CULLING */
-/* #undef EMBREE_BACKFACE_CULLING_CURVES */
-#define EMBREE_FILTER_FUNCTION
-/* #undef EMBREE_IGNORE_INVALID_RAYS */
-#define EMBREE_GEOMETRY_TRIANGLE
-/* #undef EMBREE_GEOMETRY_QUAD */
-/* #undef EMBREE_GEOMETRY_CURVE */
-/* #undef EMBREE_GEOMETRY_SUBDIVISION */
-/* #undef EMBREE_GEOMETRY_USER */
-/* #undef EMBREE_GEOMETRY_INSTANCE */
-/* #undef EMBREE_GEOMETRY_GRID */
-/* #undef EMBREE_GEOMETRY_POINT */
-#define EMBREE_RAY_PACKETS
-/* #undef EMBREE_COMPACT_POLYS */
-
-#define EMBREE_CURVE_SELF_INTERSECTION_AVOIDANCE_FACTOR 2.0
-#define EMBREE_DISC_POINT_SELF_INTERSECTION_AVOIDANCE
-
-#if defined(EMBREE_GEOMETRY_TRIANGLE)
- #define IF_ENABLED_TRIS(x) x
-#else
- #define IF_ENABLED_TRIS(x)
-#endif
-
-#if defined(EMBREE_GEOMETRY_QUAD)
- #define IF_ENABLED_QUADS(x) x
-#else
- #define IF_ENABLED_QUADS(x)
-#endif
-
-#if defined(EMBREE_GEOMETRY_CURVE) || defined(EMBREE_GEOMETRY_POINT)
- #define IF_ENABLED_CURVES_OR_POINTS(x) x
-#else
- #define IF_ENABLED_CURVES_OR_POINTS(x)
-#endif
-
-#if defined(EMBREE_GEOMETRY_CURVE)
- #define IF_ENABLED_CURVES(x) x
-#else
- #define IF_ENABLED_CURVES(x)
-#endif
-
-#if defined(EMBREE_GEOMETRY_POINT)
- #define IF_ENABLED_POINTS(x) x
-#else
- #define IF_ENABLED_POINTS(x)
-#endif
-
-#if defined(EMBREE_GEOMETRY_SUBDIVISION)
- #define IF_ENABLED_SUBDIV(x) x
-#else
- #define IF_ENABLED_SUBDIV(x)
-#endif
-
-#if defined(EMBREE_GEOMETRY_USER)
- #define IF_ENABLED_USER(x) x
-#else
- #define IF_ENABLED_USER(x)
-#endif
-
-#if defined(EMBREE_GEOMETRY_INSTANCE)
- #define IF_ENABLED_INSTANCE(x) x
-#else
- #define IF_ENABLED_INSTANCE(x)
-#endif
-
-#if defined(EMBREE_GEOMETRY_GRID)
- #define IF_ENABLED_GRIDS(x) x
-#else
- #define IF_ENABLED_GRIDS(x)
-#endif
-"""
- )
-
+for config_file in config_files:
+ os.rename(os.path.join(dest_dir, config_file), os.path.join(dest_dir, config_file[:-3]))
with open("CMakeLists.txt", "r", encoding="utf-8") as cmake_file:
cmake_content = cmake_file.read()
@@ -193,70 +154,25 @@ with open("CMakeLists.txt", "r", encoding="utf-8") as cmake_file:
minor_version = int(re.compile(r"EMBREE_VERSION_MINOR\s(\d+)").findall(cmake_content)[0])
patch_version = int(re.compile(r"EMBREE_VERSION_PATCH\s(\d+)").findall(cmake_content)[0])
-with open(
- os.path.join(dest_dir, "include/embree3/rtcore_config.h"), "w", encoding="utf-8", newline="\n"
-) as config_file:
- config_file.write(
- f"""// Copyright 2009-2021 Intel Corporation
-// SPDX-License-Identifier: Apache-2.0
-
-#pragma once
+shutil.move(os.path.join(dest_dir, "kernels/rtcore_config.h"), os.path.join(dest_dir, ("include/embree4/")))
-#define RTC_VERSION_MAJOR {major_version}
-#define RTC_VERSION_MINOR {minor_version}
-#define RTC_VERSION_PATCH {patch_version}
-#define RTC_VERSION {major_version}{minor_version:02d}{patch_version:02d}
-#define RTC_VERSION_STRING "{major_version}.{minor_version}.{patch_version}"
-
-#define RTC_MAX_INSTANCE_LEVEL_COUNT 1
-
-#define EMBREE_MIN_WIDTH 0
-#define RTC_MIN_WIDTH EMBREE_MIN_WIDTH
-
-#if !defined(EMBREE_STATIC_LIB)
-# define EMBREE_STATIC_LIB
-#endif
-/* #undef EMBREE_API_NAMESPACE*/
-
-#if defined(EMBREE_API_NAMESPACE)
-# define RTC_NAMESPACE
-# define RTC_NAMESPACE_BEGIN namespace {{
-# define RTC_NAMESPACE_END }}
-# define RTC_NAMESPACE_USE using namespace;
-# define RTC_API_EXTERN_C
-# undef EMBREE_API_NAMESPACE
-#else
-# define RTC_NAMESPACE_BEGIN
-# define RTC_NAMESPACE_END
-# define RTC_NAMESPACE_USE
-# if defined(__cplusplus)
-# define RTC_API_EXTERN_C extern "C"
-# else
-# define RTC_API_EXTERN_C
-# endif
-#endif
-
-#if defined(ISPC)
-# define RTC_API_IMPORT extern "C" unmasked
-# define RTC_API_EXPORT extern "C" unmasked
-#elif defined(EMBREE_STATIC_LIB)
-# define RTC_API_IMPORT RTC_API_EXTERN_C
-# define RTC_API_EXPORT RTC_API_EXTERN_C
-#elif defined(_WIN32)
-# define RTC_API_IMPORT RTC_API_EXTERN_C __declspec(dllimport)
-# define RTC_API_EXPORT RTC_API_EXTERN_C __declspec(dllexport)
-#else
-# define RTC_API_IMPORT RTC_API_EXTERN_C
-# define RTC_API_EXPORT RTC_API_EXTERN_C __attribute__ ((visibility ("default")))
-#endif
-
-#if defined(RTC_EXPORT_API)
-# define RTC_API RTC_API_EXPORT
-#else
-# define RTC_API RTC_API_IMPORT
-#endif
-"""
- )
+with open(
+ os.path.join(dest_dir, "include/embree4/rtcore_config.h"), "r+", encoding="utf-8", newline="\n"
+) as rtcore_config:
+ lines = rtcore_config.readlines()
+ rtcore_config.seek(0)
+ for i, line in enumerate(lines):
+ if line.startswith("#define RTC_VERSION_MAJOR"):
+ lines[i : i + 5] = [
+ f"#define RTC_VERSION_MAJOR {major_version}\n",
+ f"#define RTC_VERSION_MINOR {minor_version}\n",
+ f"#define RTC_VERSION_PATCH {patch_version}\n",
+ f"#define RTC_VERSION {major_version}{minor_version:02d}{patch_version:02d}\n",
+ f'#define RTC_VERSION_STRING "{major_version}.{minor_version}.{patch_version}"\n',
+ ]
+ break
+ rtcore_config.writelines(lines)
+ rtcore_config.truncate()
os.chdir("..")
shutil.rmtree("embree-tmp")
@@ -264,4 +180,4 @@ shutil.rmtree("embree-tmp")
subprocess.run(["git", "restore", "embree/patches"])
for patch in os.listdir("embree/patches"):
- subprocess.run(["git", "apply", "embree/patches/" + patch])
+ subprocess.run(["git", "apply", f"embree/patches/{patch}"])
diff --git a/modules/raycast/lightmap_raycaster_embree.cpp b/modules/raycast/lightmap_raycaster_embree.cpp
index 2a66c36d53..84d9e19a3f 100644
--- a/modules/raycast/lightmap_raycaster_embree.cpp
+++ b/modules/raycast/lightmap_raycaster_embree.cpp
@@ -69,11 +69,12 @@ void LightmapRaycasterEmbree::filter_function(const struct RTCFilterFunctionNArg
}
bool LightmapRaycasterEmbree::intersect(Ray &r_ray) {
- RTCIntersectContext context;
-
- rtcInitIntersectContext(&context);
-
- rtcIntersect1(embree_scene, &context, (RTCRayHit *)&r_ray);
+ RTCRayQueryContext context;
+ rtcInitRayQueryContext(&context);
+ RTCIntersectArguments args;
+ rtcInitIntersectArguments(&args);
+ args.context = &context;
+ rtcIntersect1(embree_scene, (RTCRayHit *)&r_ray, &args);
return r_ray.geomID != RTC_INVALID_GEOMETRY_ID;
}
diff --git a/modules/raycast/lightmap_raycaster_embree.h b/modules/raycast/lightmap_raycaster_embree.h
index d1999e329e..2b4530a368 100644
--- a/modules/raycast/lightmap_raycaster_embree.h
+++ b/modules/raycast/lightmap_raycaster_embree.h
@@ -37,7 +37,7 @@
#include "core/object/object.h"
#include "scene/3d/lightmapper.h"
-#include <embree3/rtcore.h>
+#include <embree4/rtcore.h>
class LightmapRaycasterEmbree : public LightmapRaycaster {
GDCLASS(LightmapRaycasterEmbree, LightmapRaycaster);
diff --git a/modules/raycast/raycast_occlusion_cull.cpp b/modules/raycast/raycast_occlusion_cull.cpp
index 5005000eae..94d8b267d1 100644
--- a/modules/raycast/raycast_occlusion_cull.cpp
+++ b/modules/raycast/raycast_occlusion_cull.cpp
@@ -488,11 +488,13 @@ void RaycastOcclusionCull::Scenario::update() {
}
void RaycastOcclusionCull::Scenario::_raycast(uint32_t p_idx, const RaycastThreadData *p_raycast_data) const {
- RTCIntersectContext ctx;
- rtcInitIntersectContext(&ctx);
- ctx.flags = RTC_INTERSECT_CONTEXT_FLAG_COHERENT;
-
- rtcIntersect16((const int *)&p_raycast_data->masks[p_idx * TILE_RAYS], ebr_scene[current_scene_idx], &ctx, &p_raycast_data->rays[p_idx]);
+ RTCRayQueryContext context;
+ rtcInitRayQueryContext(&context);
+ RTCIntersectArguments args;
+ rtcInitIntersectArguments(&args);
+ args.flags = RTC_RAY_QUERY_FLAG_COHERENT;
+ args.context = &context;
+ rtcIntersect16((const int *)&p_raycast_data->masks[p_idx * TILE_RAYS], ebr_scene[current_scene_idx], &p_raycast_data->rays[p_idx], &args);
}
void RaycastOcclusionCull::Scenario::raycast(CameraRayTile *r_rays, const uint32_t *p_valid_masks, uint32_t p_tile_count) const {
@@ -536,6 +538,64 @@ void RaycastOcclusionCull::buffer_set_size(RID p_buffer, const Vector2i &p_size)
buffers[p_buffer].resize(p_size);
}
+Projection RaycastOcclusionCull::_jitter_projection(const Projection &p_cam_projection, const Size2i &p_viewport_size) {
+ if (!_jitter_enabled) {
+ return p_cam_projection;
+ }
+
+ // Prevent divide by zero when using NULL viewport.
+ if ((p_viewport_size.x <= 0) || (p_viewport_size.y <= 0)) {
+ return p_cam_projection;
+ }
+
+ Projection p = p_cam_projection;
+
+ int32_t frame = Engine::get_singleton()->get_frames_drawn();
+ frame %= 9;
+
+ Vector2 jitter;
+
+ switch (frame) {
+ default:
+ break;
+ case 1: {
+ jitter = Vector2(-1, -1);
+ } break;
+ case 2: {
+ jitter = Vector2(1, -1);
+ } break;
+ case 3: {
+ jitter = Vector2(-1, 1);
+ } break;
+ case 4: {
+ jitter = Vector2(1, 1);
+ } break;
+ case 5: {
+ jitter = Vector2(-0.5f, -0.5f);
+ } break;
+ case 6: {
+ jitter = Vector2(0.5f, -0.5f);
+ } break;
+ case 7: {
+ jitter = Vector2(-0.5f, 0.5f);
+ } break;
+ case 8: {
+ jitter = Vector2(0.5f, 0.5f);
+ } break;
+ }
+
+ // The multiplier here determines the divergence from center,
+ // and is to some extent a balancing act.
+ // Higher divergence gives fewer false hidden, but more false shown.
+ // False hidden is obvious to viewer, false shown is not.
+ // False shown can lower percentage that are occluded, and therefore performance.
+ jitter *= Vector2(1 / (float)p_viewport_size.x, 1 / (float)p_viewport_size.y) * 0.05f;
+
+ p.add_jitter_offset(jitter);
+
+ return p;
+}
+
void RaycastOcclusionCull::buffer_update(RID p_buffer, const Transform3D &p_cam_transform, const Projection &p_cam_projection, bool p_cam_orthogonal) {
if (!buffers.has(p_buffer)) {
return;
@@ -550,7 +610,9 @@ void RaycastOcclusionCull::buffer_update(RID p_buffer, const Transform3D &p_cam_
Scenario &scenario = scenarios[buffer.scenario_rid];
scenario.update();
- buffer.update_camera_rays(p_cam_transform, p_cam_projection, p_cam_orthogonal);
+ Projection jittered_proj = _jitter_projection(p_cam_projection, buffer.get_occlusion_buffer_size());
+
+ buffer.update_camera_rays(p_cam_transform, jittered_proj, p_cam_orthogonal);
scenario.raycast(buffer.camera_rays, buffer.camera_ray_masks.ptr(), buffer.camera_rays_tile_count);
buffer.sort_rays(-p_cam_transform.basis.get_column(2), p_cam_orthogonal);
@@ -596,6 +658,7 @@ void RaycastOcclusionCull::_init_embree() {
RaycastOcclusionCull::RaycastOcclusionCull() {
raycast_singleton = this;
int default_quality = GLOBAL_GET("rendering/occlusion_culling/bvh_build_quality");
+ _jitter_enabled = GLOBAL_GET("rendering/occlusion_culling/jitter_projection");
build_quality = RS::ViewportOcclusionCullingBuildQuality(default_quality);
}
diff --git a/modules/raycast/raycast_occlusion_cull.h b/modules/raycast/raycast_occlusion_cull.h
index ab5eb4eaf0..335a685672 100644
--- a/modules/raycast/raycast_occlusion_cull.h
+++ b/modules/raycast/raycast_occlusion_cull.h
@@ -40,7 +40,7 @@
#include "scene/resources/mesh.h"
#include "servers/rendering/renderer_scene_occlusion_cull.h"
-#include <embree3/rtcore.h>
+#include <embree4/rtcore.h>
class RaycastOcclusionCull : public RendererSceneOcclusionCull {
typedef RTCRayHit16 CameraRayTile;
@@ -163,8 +163,10 @@ private:
HashMap<RID, Scenario> scenarios;
HashMap<RID, RaycastHZBuffer> buffers;
RS::ViewportOcclusionCullingBuildQuality build_quality;
+ bool _jitter_enabled = false;
void _init_embree();
+ Projection _jitter_projection(const Projection &p_cam_projection, const Size2i &p_viewport_size);
public:
virtual bool is_occluder(RID p_rid) override;
diff --git a/modules/raycast/static_raycaster_embree.cpp b/modules/raycast/static_raycaster_embree.cpp
index f9076d30dd..a6ad340397 100644
--- a/modules/raycast/static_raycaster_embree.cpp
+++ b/modules/raycast/static_raycaster_embree.cpp
@@ -53,9 +53,12 @@ void StaticRaycasterEmbree::free() {
}
bool StaticRaycasterEmbree::intersect(Ray &r_ray) {
- RTCIntersectContext context;
- rtcInitIntersectContext(&context);
- rtcIntersect1(embree_scene, &context, (RTCRayHit *)&r_ray);
+ RTCRayQueryContext context;
+ rtcInitRayQueryContext(&context);
+ RTCIntersectArguments args;
+ rtcInitIntersectArguments(&args);
+ args.context = &context;
+ rtcIntersect1(embree_scene, (RTCRayHit *)&r_ray, &args);
return r_ray.geomID != RTC_INVALID_GEOMETRY_ID;
}
diff --git a/modules/raycast/static_raycaster_embree.h b/modules/raycast/static_raycaster_embree.h
index 24e1c7b92f..3ffab32bad 100644
--- a/modules/raycast/static_raycaster_embree.h
+++ b/modules/raycast/static_raycaster_embree.h
@@ -35,7 +35,7 @@
#include "core/math/static_raycaster.h"
-#include <embree3/rtcore.h>
+#include <embree4/rtcore.h>
class StaticRaycasterEmbree : public StaticRaycaster {
GDCLASS(StaticRaycasterEmbree, StaticRaycaster);
diff --git a/modules/regex/tests/test_regex.h b/modules/regex/tests/test_regex.h
index 1c305f70f7..af58e2487b 100644
--- a/modules/regex/tests/test_regex.h
+++ b/modules/regex/tests/test_regex.h
@@ -184,6 +184,48 @@ TEST_CASE("[RegEx] Empty pattern") {
CHECK(re.is_valid());
}
+TEST_CASE("[RegEx] Complex Grouping") {
+ const String test = "https://docs.godotengine.org/en/latest/contributing/";
+
+ // Ignored protocol in grouping.
+ RegEx re("^(?:https?://)([a-zA-Z]{2,4})\\.([a-zA-Z][a-zA-Z0-9_\\-]{2,64})\\.([a-zA-Z]{2,4})");
+ REQUIRE(re.is_valid());
+ Ref<RegExMatch> expr = re.search(test);
+
+ CHECK(expr->get_group_count() == 3);
+
+ CHECK(expr->get_string(0) == "https://docs.godotengine.org");
+
+ CHECK(expr->get_string(1) == "docs");
+ CHECK(expr->get_string(2) == "godotengine");
+ CHECK(expr->get_string(3) == "org");
+}
+
+TEST_CASE("[RegEx] Number Expression") {
+ const String test = "(2.5e-3 + 35 + 46) / 2.8e0 = 28.9294642857";
+
+ // Not an exact regex for number but a good test.
+ RegEx re("([+-]?\\d+)(\\.\\d+([eE][+-]?\\d+)?)?");
+ REQUIRE(re.is_valid());
+ Array number_match = re.search_all(test);
+
+ CHECK(number_match.size() == 5);
+
+ Ref<RegExMatch> number = number_match[0];
+ CHECK(number->get_string(0) == "2.5e-3");
+ CHECK(number->get_string(1) == "2");
+ number = number_match[1];
+ CHECK(number->get_string(0) == "35");
+ number = number_match[2];
+ CHECK(number->get_string(0) == "46");
+ number = number_match[3];
+ CHECK(number->get_string(0) == "2.8e0");
+ number = number_match[4];
+ CHECK(number->get_string(0) == "28.9294642857");
+ CHECK(number->get_string(1) == "28");
+ CHECK(number->get_string(2) == ".9294642857");
+}
+
TEST_CASE("[RegEx] Invalid end position") {
const String s = "Godot";
diff --git a/modules/svg/image_loader_svg.cpp b/modules/svg/image_loader_svg.cpp
index affe163aeb..d903137195 100644
--- a/modules/svg/image_loader_svg.cpp
+++ b/modules/svg/image_loader_svg.cpp
@@ -173,9 +173,17 @@ void ImageLoaderSVG::get_recognized_extensions(List<String> *p_extensions) const
}
Error ImageLoaderSVG::load_image(Ref<Image> p_image, Ref<FileAccess> p_fileaccess, BitField<ImageFormatLoader::LoaderFlags> p_flags, float p_scale) {
- String svg = p_fileaccess->get_as_utf8_string();
+ const uint64_t len = p_fileaccess->get_length() - p_fileaccess->get_position();
+ Vector<uint8_t> buffer;
+ buffer.resize(len);
+ p_fileaccess->get_buffer(buffer.ptrw(), buffer.size());
+
+ String svg;
+ Error err = svg.parse_utf8((const char *)buffer.ptr(), buffer.size());
+ if (err != OK) {
+ return err;
+ }
- Error err;
if (p_flags & FLAG_CONVERT_COLORS) {
err = create_image_from_string(p_image, svg, p_scale, false, forced_color_map);
} else {
diff --git a/modules/text_server_adv/SCsub b/modules/text_server_adv/SCsub
index b04ad77bc9..15695476de 100644
--- a/modules/text_server_adv/SCsub
+++ b/modules/text_server_adv/SCsub
@@ -117,6 +117,7 @@ if env["builtin_harfbuzz"]:
"src/hb-subset-cff1.cc",
"src/hb-subset-cff2.cc",
"src/hb-subset-input.cc",
+ "src/hb-subset-instancer-iup.cc",
"src/hb-subset-instancer-solver.cc",
"src/hb-subset-plan.cc",
"src/hb-subset-repacker.cc",
diff --git a/modules/text_server_adv/gdextension_build/SConstruct b/modules/text_server_adv/gdextension_build/SConstruct
index 1f27f5ade9..1d9d36fbbf 100644
--- a/modules/text_server_adv/gdextension_build/SConstruct
+++ b/modules/text_server_adv/gdextension_build/SConstruct
@@ -357,6 +357,7 @@ thirdparty_harfbuzz_sources = [
"src/hb-subset-cff1.cc",
"src/hb-subset-cff2.cc",
"src/hb-subset-input.cc",
+ "src/hb-subset-instancer-iup.cc",
"src/hb-subset-instancer-solver.cc",
"src/hb-subset-plan.cc",
"src/hb-subset-repacker.cc",
diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp
index f5174d7d46..1ed335fe99 100644
--- a/modules/text_server_adv/text_server_adv.cpp
+++ b/modules/text_server_adv/text_server_adv.cpp
@@ -958,7 +958,7 @@ void TextServerAdvanced::_generateMTSDF_threaded(void *p_td, uint32_t p_y) {
}
}
-_FORCE_INLINE_ TextServerAdvanced::FontGlyph TextServerAdvanced::rasterize_msdf(FontAdvanced *p_font_data, FontForSizeAdvanced *p_data, int p_pixel_range, int p_rect_margin, FT_Outline *outline, const Vector2 &advance) const {
+_FORCE_INLINE_ TextServerAdvanced::FontGlyph TextServerAdvanced::rasterize_msdf(FontAdvanced *p_font_data, FontForSizeAdvanced *p_data, int p_pixel_range, int p_rect_margin, FT_Outline *p_outline, const Vector2 &p_advance) const {
msdfgen::Shape shape;
shape.contours.clear();
@@ -974,13 +974,13 @@ _FORCE_INLINE_ TextServerAdvanced::FontGlyph TextServerAdvanced::rasterize_msdf(
ft_functions.shift = 0;
ft_functions.delta = 0;
- int error = FT_Outline_Decompose(outline, &ft_functions, &context);
+ int error = FT_Outline_Decompose(p_outline, &ft_functions, &context);
ERR_FAIL_COND_V_MSG(error, FontGlyph(), "FreeType: Outline decomposition error: '" + String(FT_Error_String(error)) + "'.");
if (!shape.contours.empty() && shape.contours.back().edges.empty()) {
shape.contours.pop_back();
}
- if (FT_Outline_Get_Orientation(outline) == 1) {
+ if (FT_Outline_Get_Orientation(p_outline) == 1) {
for (int i = 0; i < (int)shape.contours.size(); ++i) {
shape.contours[i].reverse();
}
@@ -993,12 +993,19 @@ _FORCE_INLINE_ TextServerAdvanced::FontGlyph TextServerAdvanced::rasterize_msdf(
FontGlyph chr;
chr.found = true;
- chr.advance = advance;
+ chr.advance = p_advance;
if (shape.validate() && shape.contours.size() > 0) {
int w = (bounds.r - bounds.l);
int h = (bounds.t - bounds.b);
+ if (w == 0 || h == 0) {
+ chr.texture_idx = -1;
+ chr.uv_rect = Rect2();
+ chr.rect = Rect2();
+ return chr;
+ }
+
int mw = w + p_rect_margin * 4;
int mh = h + p_rect_margin * 4;
@@ -1056,12 +1063,24 @@ _FORCE_INLINE_ TextServerAdvanced::FontGlyph TextServerAdvanced::rasterize_msdf(
#endif
#ifdef MODULE_FREETYPE_ENABLED
-_FORCE_INLINE_ TextServerAdvanced::FontGlyph TextServerAdvanced::rasterize_bitmap(FontForSizeAdvanced *p_data, int p_rect_margin, FT_Bitmap bitmap, int yofs, int xofs, const Vector2 &advance, bool p_bgra) const {
- int w = bitmap.width;
- int h = bitmap.rows;
+_FORCE_INLINE_ TextServerAdvanced::FontGlyph TextServerAdvanced::rasterize_bitmap(FontForSizeAdvanced *p_data, int p_rect_margin, FT_Bitmap p_bitmap, int p_yofs, int p_xofs, const Vector2 &p_advance, bool p_bgra) const {
+ FontGlyph chr;
+ chr.advance = p_advance * p_data->scale / p_data->oversampling;
+ chr.found = true;
+
+ int w = p_bitmap.width;
+ int h = p_bitmap.rows;
+
+ if (w == 0 || h == 0) {
+ chr.texture_idx = -1;
+ chr.uv_rect = Rect2();
+ chr.rect = Rect2();
+ return chr;
+ }
+
int color_size = 2;
- switch (bitmap.pixel_mode) {
+ switch (p_bitmap.pixel_mode) {
case FT_PIXEL_MODE_MONO:
case FT_PIXEL_MODE_GRAY: {
color_size = 2;
@@ -1100,54 +1119,54 @@ _FORCE_INLINE_ TextServerAdvanced::FontGlyph TextServerAdvanced::rasterize_bitma
for (int j = 0; j < w; j++) {
int ofs = ((i + tex_pos.y + p_rect_margin * 2) * tex.texture_w + j + tex_pos.x + p_rect_margin * 2) * color_size;
ERR_FAIL_COND_V(ofs >= tex.image->data_size(), FontGlyph());
- switch (bitmap.pixel_mode) {
+ switch (p_bitmap.pixel_mode) {
case FT_PIXEL_MODE_MONO: {
- int byte = i * bitmap.pitch + (j >> 3);
+ int byte = i * p_bitmap.pitch + (j >> 3);
int bit = 1 << (7 - (j % 8));
wr[ofs + 0] = 255; // grayscale as 1
- wr[ofs + 1] = (bitmap.buffer[byte] & bit) ? 255 : 0;
+ wr[ofs + 1] = (p_bitmap.buffer[byte] & bit) ? 255 : 0;
} break;
case FT_PIXEL_MODE_GRAY:
wr[ofs + 0] = 255; // grayscale as 1
- wr[ofs + 1] = bitmap.buffer[i * bitmap.pitch + j];
+ wr[ofs + 1] = p_bitmap.buffer[i * p_bitmap.pitch + j];
break;
case FT_PIXEL_MODE_BGRA: {
- int ofs_color = i * bitmap.pitch + (j << 2);
- wr[ofs + 2] = bitmap.buffer[ofs_color + 0];
- wr[ofs + 1] = bitmap.buffer[ofs_color + 1];
- wr[ofs + 0] = bitmap.buffer[ofs_color + 2];
- wr[ofs + 3] = bitmap.buffer[ofs_color + 3];
+ int ofs_color = i * p_bitmap.pitch + (j << 2);
+ wr[ofs + 2] = p_bitmap.buffer[ofs_color + 0];
+ wr[ofs + 1] = p_bitmap.buffer[ofs_color + 1];
+ wr[ofs + 0] = p_bitmap.buffer[ofs_color + 2];
+ wr[ofs + 3] = p_bitmap.buffer[ofs_color + 3];
} break;
case FT_PIXEL_MODE_LCD: {
- int ofs_color = i * bitmap.pitch + (j * 3);
+ int ofs_color = i * p_bitmap.pitch + (j * 3);
if (p_bgra) {
- wr[ofs + 0] = bitmap.buffer[ofs_color + 2];
- wr[ofs + 1] = bitmap.buffer[ofs_color + 1];
- wr[ofs + 2] = bitmap.buffer[ofs_color + 0];
+ wr[ofs + 0] = p_bitmap.buffer[ofs_color + 2];
+ wr[ofs + 1] = p_bitmap.buffer[ofs_color + 1];
+ wr[ofs + 2] = p_bitmap.buffer[ofs_color + 0];
wr[ofs + 3] = 255;
} else {
- wr[ofs + 0] = bitmap.buffer[ofs_color + 0];
- wr[ofs + 1] = bitmap.buffer[ofs_color + 1];
- wr[ofs + 2] = bitmap.buffer[ofs_color + 2];
+ wr[ofs + 0] = p_bitmap.buffer[ofs_color + 0];
+ wr[ofs + 1] = p_bitmap.buffer[ofs_color + 1];
+ wr[ofs + 2] = p_bitmap.buffer[ofs_color + 2];
wr[ofs + 3] = 255;
}
} break;
case FT_PIXEL_MODE_LCD_V: {
- int ofs_color = i * bitmap.pitch * 3 + j;
+ int ofs_color = i * p_bitmap.pitch * 3 + j;
if (p_bgra) {
- wr[ofs + 0] = bitmap.buffer[ofs_color + bitmap.pitch * 2];
- wr[ofs + 1] = bitmap.buffer[ofs_color + bitmap.pitch];
- wr[ofs + 2] = bitmap.buffer[ofs_color + 0];
+ wr[ofs + 0] = p_bitmap.buffer[ofs_color + p_bitmap.pitch * 2];
+ wr[ofs + 1] = p_bitmap.buffer[ofs_color + p_bitmap.pitch];
+ wr[ofs + 2] = p_bitmap.buffer[ofs_color + 0];
wr[ofs + 3] = 255;
} else {
- wr[ofs + 0] = bitmap.buffer[ofs_color + 0];
- wr[ofs + 1] = bitmap.buffer[ofs_color + bitmap.pitch];
- wr[ofs + 2] = bitmap.buffer[ofs_color + bitmap.pitch * 2];
+ wr[ofs + 0] = p_bitmap.buffer[ofs_color + 0];
+ wr[ofs + 1] = p_bitmap.buffer[ofs_color + p_bitmap.pitch];
+ wr[ofs + 2] = p_bitmap.buffer[ofs_color + p_bitmap.pitch * 2];
wr[ofs + 3] = 255;
}
} break;
default:
- ERR_FAIL_V_MSG(FontGlyph(), "Font uses unsupported pixel format: " + String::num_int64(bitmap.pixel_mode) + ".");
+ ERR_FAIL_V_MSG(FontGlyph(), "Font uses unsupported pixel format: " + String::num_int64(p_bitmap.pixel_mode) + ".");
break;
}
}
@@ -1156,13 +1175,10 @@ _FORCE_INLINE_ TextServerAdvanced::FontGlyph TextServerAdvanced::rasterize_bitma
tex.dirty = true;
- FontGlyph chr;
- chr.advance = advance * p_data->scale / p_data->oversampling;
chr.texture_idx = tex_pos.index;
- chr.found = true;
chr.uv_rect = Rect2(tex_pos.x + p_rect_margin, tex_pos.y + p_rect_margin, w + p_rect_margin * 2, h + p_rect_margin * 2);
- chr.rect.position = Vector2(xofs - p_rect_margin, -yofs - p_rect_margin) * p_data->scale / p_data->oversampling;
+ chr.rect.position = Vector2(p_xofs - p_rect_margin, -p_yofs - p_rect_margin) * p_data->scale / p_data->oversampling;
chr.rect.size = chr.uv_rect.size * p_data->scale / p_data->oversampling;
return chr;
}
@@ -1432,8 +1448,25 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_f
#endif
if (!p_font_data->face_init) {
- // Get style flags and name.
- if (fd->face->family_name != nullptr) {
+ // When a font does not provide a `family_name`, FreeType tries to synthesize one based on other names.
+ // FreeType automatically converts non-ASCII characters to "?" in the synthesized name.
+ // To avoid that behavior, use the format-specific name directly if available.
+ hb_face_t *hb_face = hb_font_get_face(fd->hb_handle);
+ unsigned int num_entries = 0;
+ const hb_ot_name_entry_t *names = hb_ot_name_list_names(hb_face, &num_entries);
+ const hb_language_t english = hb_language_from_string("en", -1);
+ for (unsigned int i = 0; i < num_entries; i++) {
+ if (names[i].name_id != HB_OT_NAME_ID_FONT_FAMILY) {
+ continue;
+ }
+ if (!p_font_data->font_name.is_empty() && names[i].language != english) {
+ continue;
+ }
+ unsigned int text_size = hb_ot_name_get_utf32(hb_face, names[i].name_id, names[i].language, nullptr, nullptr) + 1;
+ p_font_data->font_name.resize(text_size);
+ hb_ot_name_get_utf32(hb_face, names[i].name_id, names[i].language, &text_size, (uint32_t *)p_font_data->font_name.ptrw());
+ }
+ if (p_font_data->font_name.is_empty() && fd->face->family_name != nullptr) {
p_font_data->font_name = String::utf8((const char *)fd->face->family_name);
}
if (fd->face->style_name != nullptr) {
@@ -1452,7 +1485,6 @@ _FORCE_INLINE_ bool TextServerAdvanced::_ensure_cache_for_size(FontAdvanced *p_f
p_font_data->style_flags.set_flag(FONT_FIXED_WIDTH);
}
- hb_face_t *hb_face = hb_font_get_face(fd->hb_handle);
// Get supported scripts from OpenType font data.
p_font_data->supported_scripts.clear();
unsigned int count = hb_ot_layout_table_get_script_tags(hb_face, HB_OT_TAG_GSUB, 0, nullptr, nullptr);
@@ -3603,9 +3635,6 @@ void TextServerAdvanced::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca
const FontGlyph &gl = fd->cache[size]->glyph_map[index];
if (gl.found) {
- if (gl.uv_rect.size.x <= 2 || gl.uv_rect.size.y <= 2) {
- return; // Nothing to draw.
- }
ERR_FAIL_COND(gl.texture_idx < -1 || gl.texture_idx >= fd->cache[size]->textures.size());
if (gl.texture_idx != -1) {
@@ -3714,9 +3743,6 @@ void TextServerAdvanced::_font_draw_glyph_outline(const RID &p_font_rid, const R
const FontGlyph &gl = fd->cache[size]->glyph_map[index];
if (gl.found) {
- if (gl.uv_rect.size.x <= 2 || gl.uv_rect.size.y <= 2) {
- return; // Nothing to draw.
- }
ERR_FAIL_COND(gl.texture_idx < -1 || gl.texture_idx >= fd->cache[size]->textures.size());
if (gl.texture_idx != -1) {
@@ -6976,6 +7002,34 @@ String TextServerAdvanced::_string_to_lower(const String &p_string, const String
return String::utf16(lower.ptr(), len);
}
+String TextServerAdvanced::_string_to_title(const String &p_string, const String &p_language) const {
+#ifndef ICU_STATIC_DATA
+ if (!icu_data_loaded) {
+ return p_string.capitalize();
+ }
+#endif
+
+ if (p_string.is_empty()) {
+ return p_string;
+ }
+ const String lang = (p_language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale() : p_language;
+
+ // Convert to UTF-16.
+ Char16String utf16 = p_string.utf16();
+
+ Vector<char16_t> upper;
+ UErrorCode err = U_ZERO_ERROR;
+ int32_t len = u_strToTitle(nullptr, 0, utf16.get_data(), -1, nullptr, lang.ascii().get_data(), &err);
+ ERR_FAIL_COND_V_MSG(err != U_BUFFER_OVERFLOW_ERROR, p_string, u_errorName(err));
+ upper.resize(len);
+ err = U_ZERO_ERROR;
+ u_strToTitle(upper.ptrw(), len, utf16.get_data(), -1, nullptr, lang.ascii().get_data(), &err);
+ ERR_FAIL_COND_V_MSG(U_FAILURE(err), p_string, u_errorName(err));
+
+ // Convert back to UTF-32.
+ return String::utf16(upper.ptr(), len);
+}
+
PackedInt32Array TextServerAdvanced::_string_get_word_breaks(const String &p_string, const String &p_language, int64_t p_chars_per_line) const {
const String lang = (p_language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale() : p_language;
// Convert to UTF-16.
diff --git a/modules/text_server_adv/text_server_adv.h b/modules/text_server_adv/text_server_adv.h
index 154fe670eb..1cd73a6999 100644
--- a/modules/text_server_adv/text_server_adv.h
+++ b/modules/text_server_adv/text_server_adv.h
@@ -354,10 +354,10 @@ class TextServerAdvanced : public TextServerExtension {
_FORCE_INLINE_ FontTexturePosition find_texture_pos_for_glyph(FontForSizeAdvanced *p_data, int p_color_size, Image::Format p_image_format, int p_width, int p_height, bool p_msdf) const;
#ifdef MODULE_MSDFGEN_ENABLED
- _FORCE_INLINE_ FontGlyph rasterize_msdf(FontAdvanced *p_font_data, FontForSizeAdvanced *p_data, int p_pixel_range, int p_rect_margin, FT_Outline *outline, const Vector2 &advance) const;
+ _FORCE_INLINE_ FontGlyph rasterize_msdf(FontAdvanced *p_font_data, FontForSizeAdvanced *p_data, int p_pixel_range, int p_rect_margin, FT_Outline *p_outline, const Vector2 &p_advance) const;
#endif
#ifdef MODULE_FREETYPE_ENABLED
- _FORCE_INLINE_ FontGlyph rasterize_bitmap(FontForSizeAdvanced *p_data, int p_rect_margin, FT_Bitmap bitmap, int yofs, int xofs, const Vector2 &advance, bool p_bgra) const;
+ _FORCE_INLINE_ FontGlyph rasterize_bitmap(FontForSizeAdvanced *p_data, int p_rect_margin, FT_Bitmap p_bitmap, int p_yofs, int p_xofs, const Vector2 &p_advance, bool p_bgra) const;
#endif
_FORCE_INLINE_ bool _ensure_glyph(FontAdvanced *p_font_data, const Vector2i &p_size, int32_t p_glyph) const;
_FORCE_INLINE_ bool _ensure_cache_for_size(FontAdvanced *p_font_data, const Vector2i &p_size) const;
@@ -991,6 +991,7 @@ public:
MODBIND2RC(String, string_to_upper, const String &, const String &);
MODBIND2RC(String, string_to_lower, const String &, const String &);
+ MODBIND2RC(String, string_to_title, const String &, const String &);
MODBIND0(cleanup);
diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp
index 302bd677fe..c62f308818 100644
--- a/modules/text_server_fb/text_server_fb.cpp
+++ b/modules/text_server_fb/text_server_fb.cpp
@@ -71,8 +71,10 @@ using namespace godot;
#endif
#endif
-#ifdef MODULE_SVG_ENABLED
#ifdef MODULE_FREETYPE_ENABLED
+#include FT_SFNT_NAMES_H
+#include FT_TRUETYPE_IDS_H
+#ifdef MODULE_SVG_ENABLED
#include "thorvg_svg_in_ot.h"
#endif
#endif
@@ -392,7 +394,7 @@ void TextServerFallback::_generateMTSDF_threaded(void *p_td, uint32_t p_y) {
}
}
-_FORCE_INLINE_ TextServerFallback::FontGlyph TextServerFallback::rasterize_msdf(FontFallback *p_font_data, FontForSizeFallback *p_data, int p_pixel_range, int p_rect_margin, FT_Outline *outline, const Vector2 &advance) const {
+_FORCE_INLINE_ TextServerFallback::FontGlyph TextServerFallback::rasterize_msdf(FontFallback *p_font_data, FontForSizeFallback *p_data, int p_pixel_range, int p_rect_margin, FT_Outline *p_outline, const Vector2 &p_advance) const {
msdfgen::Shape shape;
shape.contours.clear();
@@ -408,13 +410,13 @@ _FORCE_INLINE_ TextServerFallback::FontGlyph TextServerFallback::rasterize_msdf(
ft_functions.shift = 0;
ft_functions.delta = 0;
- int error = FT_Outline_Decompose(outline, &ft_functions, &context);
+ int error = FT_Outline_Decompose(p_outline, &ft_functions, &context);
ERR_FAIL_COND_V_MSG(error, FontGlyph(), "FreeType: Outline decomposition error: '" + String(FT_Error_String(error)) + "'.");
if (!shape.contours.empty() && shape.contours.back().edges.empty()) {
shape.contours.pop_back();
}
- if (FT_Outline_Get_Orientation(outline) == 1) {
+ if (FT_Outline_Get_Orientation(p_outline) == 1) {
for (int i = 0; i < (int)shape.contours.size(); ++i) {
shape.contours[i].reverse();
}
@@ -427,12 +429,18 @@ _FORCE_INLINE_ TextServerFallback::FontGlyph TextServerFallback::rasterize_msdf(
FontGlyph chr;
chr.found = true;
- chr.advance = advance;
+ chr.advance = p_advance;
if (shape.validate() && shape.contours.size() > 0) {
int w = (bounds.r - bounds.l);
int h = (bounds.t - bounds.b);
+ if (w == 0 || h == 0) {
+ chr.texture_idx = -1;
+ chr.uv_rect = Rect2();
+ chr.rect = Rect2();
+ return chr;
+ }
int mw = w + p_rect_margin * 4;
int mh = h + p_rect_margin * 4;
@@ -489,12 +497,24 @@ _FORCE_INLINE_ TextServerFallback::FontGlyph TextServerFallback::rasterize_msdf(
#endif
#ifdef MODULE_FREETYPE_ENABLED
-_FORCE_INLINE_ TextServerFallback::FontGlyph TextServerFallback::rasterize_bitmap(FontForSizeFallback *p_data, int p_rect_margin, FT_Bitmap bitmap, int yofs, int xofs, const Vector2 &advance, bool p_bgra) const {
- int w = bitmap.width;
- int h = bitmap.rows;
+_FORCE_INLINE_ TextServerFallback::FontGlyph TextServerFallback::rasterize_bitmap(FontForSizeFallback *p_data, int p_rect_margin, FT_Bitmap p_bitmap, int p_yofs, int p_xofs, const Vector2 &p_advance, bool p_bgra) const {
+ FontGlyph chr;
+ chr.advance = p_advance * p_data->scale / p_data->oversampling;
+ chr.found = true;
+
+ int w = p_bitmap.width;
+ int h = p_bitmap.rows;
+
+ if (w == 0 || h == 0) {
+ chr.texture_idx = -1;
+ chr.uv_rect = Rect2();
+ chr.rect = Rect2();
+ return chr;
+ }
+
int color_size = 2;
- switch (bitmap.pixel_mode) {
+ switch (p_bitmap.pixel_mode) {
case FT_PIXEL_MODE_MONO:
case FT_PIXEL_MODE_GRAY: {
color_size = 2;
@@ -533,54 +553,54 @@ _FORCE_INLINE_ TextServerFallback::FontGlyph TextServerFallback::rasterize_bitma
for (int j = 0; j < w; j++) {
int ofs = ((i + tex_pos.y + p_rect_margin * 2) * tex.texture_w + j + tex_pos.x + p_rect_margin * 2) * color_size;
ERR_FAIL_COND_V(ofs >= tex.image->data_size(), FontGlyph());
- switch (bitmap.pixel_mode) {
+ switch (p_bitmap.pixel_mode) {
case FT_PIXEL_MODE_MONO: {
- int byte = i * bitmap.pitch + (j >> 3);
+ int byte = i * p_bitmap.pitch + (j >> 3);
int bit = 1 << (7 - (j % 8));
wr[ofs + 0] = 255; // grayscale as 1
- wr[ofs + 1] = (bitmap.buffer[byte] & bit) ? 255 : 0;
+ wr[ofs + 1] = (p_bitmap.buffer[byte] & bit) ? 255 : 0;
} break;
case FT_PIXEL_MODE_GRAY:
wr[ofs + 0] = 255; // grayscale as 1
- wr[ofs + 1] = bitmap.buffer[i * bitmap.pitch + j];
+ wr[ofs + 1] = p_bitmap.buffer[i * p_bitmap.pitch + j];
break;
case FT_PIXEL_MODE_BGRA: {
- int ofs_color = i * bitmap.pitch + (j << 2);
- wr[ofs + 2] = bitmap.buffer[ofs_color + 0];
- wr[ofs + 1] = bitmap.buffer[ofs_color + 1];
- wr[ofs + 0] = bitmap.buffer[ofs_color + 2];
- wr[ofs + 3] = bitmap.buffer[ofs_color + 3];
+ int ofs_color = i * p_bitmap.pitch + (j << 2);
+ wr[ofs + 2] = p_bitmap.buffer[ofs_color + 0];
+ wr[ofs + 1] = p_bitmap.buffer[ofs_color + 1];
+ wr[ofs + 0] = p_bitmap.buffer[ofs_color + 2];
+ wr[ofs + 3] = p_bitmap.buffer[ofs_color + 3];
} break;
case FT_PIXEL_MODE_LCD: {
- int ofs_color = i * bitmap.pitch + (j * 3);
+ int ofs_color = i * p_bitmap.pitch + (j * 3);
if (p_bgra) {
- wr[ofs + 0] = bitmap.buffer[ofs_color + 2];
- wr[ofs + 1] = bitmap.buffer[ofs_color + 1];
- wr[ofs + 2] = bitmap.buffer[ofs_color + 0];
+ wr[ofs + 0] = p_bitmap.buffer[ofs_color + 2];
+ wr[ofs + 1] = p_bitmap.buffer[ofs_color + 1];
+ wr[ofs + 2] = p_bitmap.buffer[ofs_color + 0];
wr[ofs + 3] = 255;
} else {
- wr[ofs + 0] = bitmap.buffer[ofs_color + 0];
- wr[ofs + 1] = bitmap.buffer[ofs_color + 1];
- wr[ofs + 2] = bitmap.buffer[ofs_color + 2];
+ wr[ofs + 0] = p_bitmap.buffer[ofs_color + 0];
+ wr[ofs + 1] = p_bitmap.buffer[ofs_color + 1];
+ wr[ofs + 2] = p_bitmap.buffer[ofs_color + 2];
wr[ofs + 3] = 255;
}
} break;
case FT_PIXEL_MODE_LCD_V: {
- int ofs_color = i * bitmap.pitch * 3 + j;
+ int ofs_color = i * p_bitmap.pitch * 3 + j;
if (p_bgra) {
- wr[ofs + 0] = bitmap.buffer[ofs_color + bitmap.pitch * 2];
- wr[ofs + 1] = bitmap.buffer[ofs_color + bitmap.pitch];
- wr[ofs + 2] = bitmap.buffer[ofs_color + 0];
+ wr[ofs + 0] = p_bitmap.buffer[ofs_color + p_bitmap.pitch * 2];
+ wr[ofs + 1] = p_bitmap.buffer[ofs_color + p_bitmap.pitch];
+ wr[ofs + 2] = p_bitmap.buffer[ofs_color + 0];
wr[ofs + 3] = 255;
} else {
- wr[ofs + 0] = bitmap.buffer[ofs_color + 0];
- wr[ofs + 1] = bitmap.buffer[ofs_color + bitmap.pitch];
- wr[ofs + 2] = bitmap.buffer[ofs_color + bitmap.pitch * 2];
+ wr[ofs + 0] = p_bitmap.buffer[ofs_color + 0];
+ wr[ofs + 1] = p_bitmap.buffer[ofs_color + p_bitmap.pitch];
+ wr[ofs + 2] = p_bitmap.buffer[ofs_color + p_bitmap.pitch * 2];
wr[ofs + 3] = 255;
}
} break;
default:
- ERR_FAIL_V_MSG(FontGlyph(), "Font uses unsupported pixel format: " + String::num_int64(bitmap.pixel_mode) + ".");
+ ERR_FAIL_V_MSG(FontGlyph(), "Font uses unsupported pixel format: " + String::num_int64(p_bitmap.pixel_mode) + ".");
break;
}
}
@@ -589,13 +609,10 @@ _FORCE_INLINE_ TextServerFallback::FontGlyph TextServerFallback::rasterize_bitma
tex.dirty = true;
- FontGlyph chr;
- chr.advance = advance * p_data->scale / p_data->oversampling;
chr.texture_idx = tex_pos.index;
- chr.found = true;
chr.uv_rect = Rect2(tex_pos.x + p_rect_margin, tex_pos.y + p_rect_margin, w + p_rect_margin * 2, h + p_rect_margin * 2);
- chr.rect.position = Vector2(xofs - p_rect_margin, -yofs - p_rect_margin) * p_data->scale / p_data->oversampling;
+ chr.rect.position = Vector2(p_xofs - p_rect_margin, -p_yofs - p_rect_margin) * p_data->scale / p_data->oversampling;
chr.rect.size = chr.uv_rect.size * p_data->scale / p_data->oversampling;
return chr;
}
@@ -857,8 +874,37 @@ _FORCE_INLINE_ bool TextServerFallback::_ensure_cache_for_size(FontFallback *p_f
fd->underline_thickness = (FT_MulFix(fd->face->underline_thickness, fd->face->size->metrics.y_scale) / 64.0) / fd->oversampling * fd->scale;
if (!p_font_data->face_init) {
- // Get style flags and name.
- if (fd->face->family_name != nullptr) {
+ // When a font does not provide a `family_name`, FreeType tries to synthesize one based on other names.
+ // FreeType automatically converts non-ASCII characters to "?" in the synthesized name.
+ // To avoid that behavior, use the format-specific name directly if available.
+ if (FT_IS_SFNT(fd->face)) {
+ int name_count = FT_Get_Sfnt_Name_Count(fd->face);
+ for (int i = 0; i < name_count; i++) {
+ FT_SfntName sfnt_name;
+ if (FT_Get_Sfnt_Name(fd->face, i, &sfnt_name) != 0) {
+ continue;
+ }
+ if (sfnt_name.name_id != TT_NAME_ID_FONT_FAMILY && sfnt_name.name_id != TT_NAME_ID_TYPOGRAPHIC_FAMILY) {
+ continue;
+ }
+ if (!p_font_data->font_name.is_empty() && sfnt_name.language_id != TT_MS_LANGID_ENGLISH_UNITED_STATES) {
+ continue;
+ }
+
+ switch (sfnt_name.platform_id) {
+ case TT_PLATFORM_APPLE_UNICODE: {
+ p_font_data->font_name.parse_utf16((const char16_t *)sfnt_name.string, sfnt_name.string_len / 2, false);
+ } break;
+
+ case TT_PLATFORM_MICROSOFT: {
+ if (sfnt_name.encoding_id == TT_MS_ID_UNICODE_CS || sfnt_name.encoding_id == TT_MS_ID_UCS_4) {
+ p_font_data->font_name.parse_utf16((const char16_t *)sfnt_name.string, sfnt_name.string_len / 2, false);
+ }
+ } break;
+ }
+ }
+ }
+ if (p_font_data->font_name.is_empty() && fd->face->family_name != nullptr) {
p_font_data->font_name = String::utf8((const char *)fd->face->family_name);
}
if (fd->face->style_name != nullptr) {
@@ -2536,9 +2582,6 @@ void TextServerFallback::_font_draw_glyph(const RID &p_font_rid, const RID &p_ca
const FontGlyph &gl = fd->cache[size]->glyph_map[index];
if (gl.found) {
- if (gl.uv_rect.size.x <= 2 || gl.uv_rect.size.y <= 2) {
- return; // Nothing to draw.
- }
ERR_FAIL_COND(gl.texture_idx < -1 || gl.texture_idx >= fd->cache[size]->textures.size());
if (gl.texture_idx != -1) {
@@ -2647,9 +2690,6 @@ void TextServerFallback::_font_draw_glyph_outline(const RID &p_font_rid, const R
const FontGlyph &gl = fd->cache[size]->glyph_map[index];
if (gl.found) {
- if (gl.uv_rect.size.x <= 2 || gl.uv_rect.size.y <= 2) {
- return; // Nothing to draw.
- }
ERR_FAIL_COND(gl.texture_idx < -1 || gl.texture_idx >= fd->cache[size]->textures.size());
if (gl.texture_idx != -1) {
@@ -4439,6 +4479,10 @@ String TextServerFallback::_string_to_lower(const String &p_string, const String
return p_string.to_lower();
}
+String TextServerFallback::_string_to_title(const String &p_string, const String &p_language) const {
+ return p_string.capitalize();
+}
+
PackedInt32Array TextServerFallback::_string_get_word_breaks(const String &p_string, const String &p_language, int64_t p_chars_per_line) const {
PackedInt32Array ret;
diff --git a/modules/text_server_fb/text_server_fb.h b/modules/text_server_fb/text_server_fb.h
index 0db1f7318f..31370c7da7 100644
--- a/modules/text_server_fb/text_server_fb.h
+++ b/modules/text_server_fb/text_server_fb.h
@@ -303,10 +303,10 @@ class TextServerFallback : public TextServerExtension {
_FORCE_INLINE_ FontTexturePosition find_texture_pos_for_glyph(FontForSizeFallback *p_data, int p_color_size, Image::Format p_image_format, int p_width, int p_height, bool p_msdf) const;
#ifdef MODULE_MSDFGEN_ENABLED
- _FORCE_INLINE_ FontGlyph rasterize_msdf(FontFallback *p_font_data, FontForSizeFallback *p_data, int p_pixel_range, int p_rect_margin, FT_Outline *outline, const Vector2 &advance) const;
+ _FORCE_INLINE_ FontGlyph rasterize_msdf(FontFallback *p_font_data, FontForSizeFallback *p_data, int p_pixel_range, int p_rect_margin, FT_Outline *p_outline, const Vector2 &p_advance) const;
#endif
#ifdef MODULE_FREETYPE_ENABLED
- _FORCE_INLINE_ FontGlyph rasterize_bitmap(FontForSizeFallback *p_data, int p_rect_margin, FT_Bitmap bitmap, int yofs, int xofs, const Vector2 &advance, bool p_bgra) const;
+ _FORCE_INLINE_ FontGlyph rasterize_bitmap(FontForSizeFallback *p_data, int p_rect_margin, FT_Bitmap p_bitmap, int p_yofs, int p_xofs, const Vector2 &p_advance, bool p_bgra) const;
#endif
_FORCE_INLINE_ bool _ensure_glyph(FontFallback *p_font_data, const Vector2i &p_size, int32_t p_glyph) const;
_FORCE_INLINE_ bool _ensure_cache_for_size(FontFallback *p_font_data, const Vector2i &p_size) const;
@@ -848,6 +848,7 @@ public:
MODBIND2RC(String, string_to_upper, const String &, const String &);
MODBIND2RC(String, string_to_lower, const String &, const String &);
+ MODBIND2RC(String, string_to_title, const String &, const String &);
MODBIND0(cleanup);
diff --git a/modules/upnp/doc_classes/UPNPDevice.xml b/modules/upnp/doc_classes/UPNPDevice.xml
index a70ae1b9cc..82ac179611 100644
--- a/modules/upnp/doc_classes/UPNPDevice.xml
+++ b/modules/upnp/doc_classes/UPNPDevice.xml
@@ -71,7 +71,7 @@
<constant name="IGD_STATUS_HTTP_EMPTY" value="2" enum="IGDStatus">
Empty HTTP response.
</constant>
- <constant name="IGD_STATUS_NO_URLS" value="3" enum="IGDStatus">
+ <constant name="IGD_STATUS_NO_URLS" value="3" enum="IGDStatus" deprecated="This value is no longer used.">
Returned response contained no URLs.
</constant>
<constant name="IGD_STATUS_NO_IGD" value="4" enum="IGDStatus">
@@ -86,7 +86,7 @@
<constant name="IGD_STATUS_INVALID_CONTROL" value="7" enum="IGDStatus">
Invalid control.
</constant>
- <constant name="IGD_STATUS_MALLOC_ERROR" value="8" enum="IGDStatus">
+ <constant name="IGD_STATUS_MALLOC_ERROR" value="8" enum="IGDStatus" deprecated="This value is no longer used.">
Memory allocation error.
</constant>
<constant name="IGD_STATUS_UNKNOWN_ERROR" value="9" enum="IGDStatus">
diff --git a/modules/upnp/upnp.cpp b/modules/upnp/upnp.cpp
index 2812f37eb2..95453c1ecd 100644
--- a/modules/upnp/upnp.cpp
+++ b/modules/upnp/upnp.cpp
@@ -121,33 +121,20 @@ void UPNP::parse_igd(Ref<UPNPDevice> dev, UPNPDev *devlist) {
return;
}
- struct UPNPUrls *urls = (UPNPUrls *)malloc(sizeof(struct UPNPUrls));
-
- if (!urls) {
- dev->set_igd_status(UPNPDevice::IGD_STATUS_MALLOC_ERROR);
- return;
- }
-
+ struct UPNPUrls urls = {};
struct IGDdatas data;
- memset(urls, 0, sizeof(struct UPNPUrls));
-
parserootdesc(xml, size, &data);
free(xml);
xml = nullptr;
- GetUPNPUrls(urls, &data, dev->get_description_url().utf8().get_data(), 0);
-
- if (!urls) {
- dev->set_igd_status(UPNPDevice::IGD_STATUS_NO_URLS);
- return;
- }
+ GetUPNPUrls(&urls, &data, dev->get_description_url().utf8().get_data(), 0);
char addr[16];
- int i = UPNP_GetValidIGD(devlist, urls, &data, (char *)&addr, 16);
+ int i = UPNP_GetValidIGD(devlist, &urls, &data, (char *)&addr, 16);
if (i != 1) {
- FreeUPNPUrls(urls);
+ FreeUPNPUrls(&urls);
switch (i) {
case 0:
@@ -165,18 +152,18 @@ void UPNP::parse_igd(Ref<UPNPDevice> dev, UPNPDev *devlist) {
}
}
- if (urls->controlURL[0] == '\0') {
- FreeUPNPUrls(urls);
+ if (urls.controlURL[0] == '\0') {
+ FreeUPNPUrls(&urls);
dev->set_igd_status(UPNPDevice::IGD_STATUS_INVALID_CONTROL);
return;
}
- dev->set_igd_control_url(urls->controlURL);
+ dev->set_igd_control_url(urls.controlURL);
dev->set_igd_service_type(data.first.servicetype);
dev->set_igd_our_addr(addr);
dev->set_igd_status(UPNPDevice::IGD_STATUS_OK);
- FreeUPNPUrls(urls);
+ FreeUPNPUrls(&urls);
}
int UPNP::upnp_result(int in) {
diff --git a/modules/zip/zip_reader.cpp b/modules/zip/zip_reader.cpp
index f4a92dce5b..123d1e5d46 100644
--- a/modules/zip/zip_reader.cpp
+++ b/modules/zip/zip_reader.cpp
@@ -58,7 +58,14 @@ Error ZIPReader::close() {
PackedStringArray ZIPReader::get_files() {
ERR_FAIL_COND_V_MSG(fa.is_null(), PackedStringArray(), "ZIPReader must be opened before use.");
- int err = unzGoToFirstFile(uzf);
+ unz_global_info gi;
+ int err = unzGetGlobalInfo(uzf, &gi);
+ ERR_FAIL_COND_V(err != UNZ_OK, PackedStringArray());
+ if (gi.number_entry == 0) {
+ return PackedStringArray();
+ }
+
+ err = unzGoToFirstFile(uzf);
ERR_FAIL_COND_V(err != UNZ_OK, PackedStringArray());
List<String> s;