Browse Source

Merge pull request #57489 from akien-mga/3.4-cherrypicks

Rémi Verschelde 3 years ago
parent
commit
caaa648e86

+ 2 - 2
doc/classes/AudioEffectSpectrumAnalyzer.xml

@@ -8,8 +8,8 @@
 		See also [AudioStreamGenerator] for procedurally generating sounds.
 	</description>
 	<tutorials>
-		<link title="https://godotengine.org/asset-library/asset/528">Audio Spectrum Demo</link>
-		<link title="https://godotengine.org/article/godot-32-will-get-new-audio-features">Godot 3.2 will get new audio features</link>
+		<link title="Audio Spectrum Demo">https://godotengine.org/asset-library/asset/528</link>
+		<link title="Godot 3.2 will get new audio features">https://godotengine.org/article/godot-32-will-get-new-audio-features</link>
 	</tutorials>
 	<methods>
 	</methods>

+ 1 - 1
doc/classes/AudioStreamGenerator.xml

@@ -10,7 +10,7 @@
 	</description>
 	<tutorials>
 		<link title="Audio Generator Demo">https://godotengine.org/asset-library/asset/526</link>
-		<link title="https://godotengine.org/article/godot-32-will-get-new-audio-features">Godot 3.2 will get new audio features</link>
+		<link title="Godot 3.2 will get new audio features">https://godotengine.org/article/godot-32-will-get-new-audio-features</link>
 	</tutorials>
 	<methods>
 	</methods>

+ 1 - 1
doc/classes/AudioStreamGeneratorPlayback.xml

@@ -8,7 +8,7 @@
 	</description>
 	<tutorials>
 		<link title="Audio Generator Demo">https://godotengine.org/asset-library/asset/526</link>
-		<link title="https://godotengine.org/article/godot-32-will-get-new-audio-features">Godot 3.2 will get new audio features</link>
+		<link title="Godot 3.2 will get new audio features">https://godotengine.org/article/godot-32-will-get-new-audio-features</link>
 	</tutorials>
 	<methods>
 		<method name="can_push_buffer" qualifiers="const">

+ 1 - 0
doc/classes/ReflectionProbe.xml

@@ -47,6 +47,7 @@
 		</member>
 		<member name="max_distance" type="float" setter="set_max_distance" getter="get_max_distance" default="0.0">
 			The maximum distance away from the [ReflectionProbe] an object can be before it is culled. Decrease this to improve performance, especially when using the [constant UPDATE_ALWAYS] [member update_mode].
+			[b]Note:[/b] The maximum reflection distance is always at least equal to the [member extents]. This means that decreasing [member max_distance] will not always cull objects from reflections, especially if the reflection probe's [member extents] are already large.
 		</member>
 		<member name="origin_offset" type="Vector3" setter="set_origin_offset" getter="get_origin_offset" default="Vector3( 0, 0, 0 )">
 			Sets the origin offset to be used when this [ReflectionProbe] is in [member box_projection] mode. This can be set to a non-zero value to ensure a reflection fits a rectangle-shaped room, while reducing the amount of objects that "get in the way" of the reflection.

+ 1 - 1
editor/plugins/skeleton_editor_plugin.cpp

@@ -77,7 +77,7 @@ void SkeletonEditor::create_physical_skeleton() {
 			if (!bones_infos[parent].physical_bone) {
 				bones_infos.write[parent].physical_bone = create_physical_bone(parent, bone_id, bones_infos);
 
-				ur->create_action(TTR("Create physical bones"));
+				ur->create_action(TTR("Create physical bones"), UndoRedo::MERGE_ALL);
 				ur->add_do_method(skeleton, "add_child", bones_infos[parent].physical_bone);
 				ur->add_do_reference(bones_infos[parent].physical_bone);
 				ur->add_undo_method(skeleton, "remove_child", bones_infos[parent].physical_bone);

+ 44 - 7
modules/mono/mono_gd/support/android_support.cpp

@@ -164,11 +164,12 @@ const char *godot_so_name = "libgodot_android.so";
 void *mono_dl_handle = NULL;
 void *godot_dl_handle = NULL;
 
-void *try_dlopen(const String &p_so_path, int p_flags) {
+void *_try_dlopen_file_path(const String &p_so_path, int p_flags) {
 	if (!FileAccess::exists(p_so_path)) {
-		if (OS::get_singleton()->is_stdout_verbose())
+		if (OS::get_singleton()->is_stdout_verbose()) {
 			OS::get_singleton()->print("Cannot find shared library: '%s'\n", p_so_path.utf8().get_data());
-		return NULL;
+		}
+		return nullptr;
 	}
 
 	int lflags = gd_mono_convert_dl_flags(p_flags);
@@ -176,13 +177,48 @@ void *try_dlopen(const String &p_so_path, int p_flags) {
 	void *handle = dlopen(p_so_path.utf8().get_data(), lflags);
 
 	if (!handle) {
-		if (OS::get_singleton()->is_stdout_verbose())
+		if (OS::get_singleton()->is_stdout_verbose()) {
 			OS::get_singleton()->print("Failed to open shared library: '%s'. Error: '%s'\n", p_so_path.utf8().get_data(), dlerror());
-		return NULL;
+		}
+		return nullptr;
 	}
 
-	if (OS::get_singleton()->is_stdout_verbose())
+	if (OS::get_singleton()->is_stdout_verbose()) {
 		OS::get_singleton()->print("Successfully loaded shared library: '%s'\n", p_so_path.utf8().get_data());
+	}
+
+	return handle;
+}
+
+void *try_dlopen(const String &p_so_path, int p_flags) {
+	void *handle = _try_dlopen_file_path(p_so_path, p_flags);
+
+	if (handle) {
+		return handle;
+	}
+
+	// Try only with the file name, without specifying the location.
+	// This is needed when installing from Android App Bundles, as the native
+	// libraries are not extracted. They are loaded directly from the APK.
+	// See: https://stackoverflow.com/a/56551499
+	// If we pass only the file name to dlopen without the location, it should
+	// search the native libraries in all locations, including inside the apk.
+
+	String so_name = p_so_path.get_file();
+
+	int lflags = gd_mono_convert_dl_flags(p_flags);
+
+	handle = dlopen(so_name.utf8().get_data(), lflags);
+	if (!handle) {
+		if (OS::get_singleton()->is_stdout_verbose()) {
+			OS::get_singleton()->print("Failed to open shared library: '%s'. Error: '%s'\n", so_name.utf8().get_data(), dlerror());
+		}
+		return nullptr;
+	}
+
+	if (OS::get_singleton()->is_stdout_verbose()) {
+		OS::get_singleton()->print("Successfully loaded shared library: '%s'\n", so_name.utf8().get_data());
+	}
 
 	return handle;
 }
@@ -196,6 +232,7 @@ void *gd_mono_android_dlopen(const char *p_name, int p_flags, char **r_err, void
 			String so_path = path::join(app_native_lib_dir, mono_so_name);
 
 			mono_dl_handle = try_dlopen(so_path, p_flags);
+			ERR_FAIL_COND_V_MSG(!mono_dl_handle, nullptr, "Failed to load Mono native library from path");
 		}
 
 		return mono_dl_handle;
@@ -371,7 +408,7 @@ void initialize() {
 	String so_path = path::join(app_native_lib_dir, godot_so_name);
 
 	godot_dl_handle = try_dlopen(so_path, gd_mono_convert_dl_flags(MONO_DL_LAZY));
-	ERR_FAIL_COND(!godot_dl_handle);
+	ERR_FAIL_COND_MSG(!godot_dl_handle, "Failed to load Godot native library");
 }
 
 void cleanup() {

+ 2 - 2
modules/stb_vorbis/audio_stream_ogg_vorbis.cpp

@@ -185,8 +185,8 @@ void AudioStreamOGGVorbis::set_data(const PoolVector<uint8_t> &p_data) {
 			w.release();
 			alloc_try *= 2;
 		} else {
-			ERR_FAIL_COND(alloc_try == MAX_TEST_MEM);
-			ERR_FAIL_COND(ogg_stream == nullptr);
+			ERR_FAIL_COND_MSG(alloc_try == MAX_TEST_MEM, "Failed allocating memory for OGG Vorbis stream.");
+			ERR_FAIL_COND_MSG(!ogg_stream, "OGG Vorbis decoding failed. Check that your data is a valid OGG Vorbis audio stream.");
 
 			stb_vorbis_info info = stb_vorbis_get_info(ogg_stream);
 

+ 1 - 1
modules/stb_vorbis/resource_importer_ogg_vorbis.cpp

@@ -91,7 +91,7 @@ Error ResourceImporterOGGVorbis::import(const String &p_source_file, const Strin
 	ogg_stream.instance();
 
 	ogg_stream->set_data(data);
-	ERR_FAIL_COND_V(!ogg_stream->get_data().size(), ERR_FILE_CORRUPT);
+	ERR_FAIL_COND_V_MSG(!ogg_stream->get_data().size(), ERR_FILE_CORRUPT, "Couldn't import file as AudioStreamOGGVorbis: " + p_source_file);
 	ogg_stream->set_loop(loop);
 	ogg_stream->set_loop_offset(loop_offset);
 

+ 2 - 2
platform/android/export/export_plugin.cpp

@@ -987,14 +987,14 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p
 
 					if (tname == "meta-data" && attrname == "name" && value == "xr_mode_metadata_name") {
 						// Update the meta-data 'android:name' attribute based on the selected XR mode.
-						if (xr_mode_index == XR_MODE_OVR || xr_mode_index == XR_MODE_OPENXR) {
+						if (xr_mode_index == XR_MODE_OVR) {
 							string_table.write[attr_value] = "com.samsung.android.vr.application.mode";
 						}
 					}
 
 					if (tname == "meta-data" && attrname == "value" && value == "xr_mode_metadata_value") {
 						// Update the meta-data 'android:value' attribute based on the selected XR mode.
-						if (xr_mode_index == XR_MODE_OVR || xr_mode_index == XR_MODE_OPENXR) {
+						if (xr_mode_index == XR_MODE_OVR) {
 							string_table.write[attr_value] = "vr_only";
 						}
 					}

+ 5 - 1
platform/android/export/gradle_export_util.cpp

@@ -287,7 +287,9 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_
 			bool_to_string(p_has_storage_permission));
 
 	if (uses_xr) {
-		manifest_application_text += "        <meta-data tools:node=\"replace\" android:name=\"com.samsung.android.vr.application.mode\" android:value=\"vr_only\" />\n";
+		if (xr_mode_index == XR_MODE_OVR) {
+			manifest_application_text += "        <meta-data tools:node=\"replace\" android:name=\"com.samsung.android.vr.application.mode\" android:value=\"vr_only\" />\n";
+		}
 
 		bool hand_tracking_enabled = (int)(p_preset->get("xr_features/hand_tracking")) > XR_HAND_TRACKING_NONE;
 		if (hand_tracking_enabled) {
@@ -297,6 +299,8 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_
 					"        <meta-data tools:node=\"replace\" android:name=\"com.oculus.handtracking.frequency\" android:value=\"%s\" />\n",
 					hand_tracking_frequency);
 		}
+	} else {
+		manifest_application_text += "        <meta-data tools:node=\"remove\" android:name=\"com.oculus.supportedDevices\" />\n";
 	}
 	manifest_application_text += _get_activity_tag(p_preset);
 	manifest_application_text += "    </application>\n";

+ 6 - 0
platform/android/java/app/AndroidManifest.xml

@@ -50,6 +50,12 @@
             android:name="xr_hand_tracking_metadata_name"
             android:value="xr_hand_tracking_metadata_value"/>
 
+        <!-- Supported Meta devices -->
+        <!-- This is removed by the exporter if the xr mode is not VR. -->
+        <meta-data
+            android:name="com.oculus.supportedDevices"
+            android:value="all" />
+
         <activity
             android:name=".GodotApp"
             android:label="@string/godot_project_name_string"

+ 21 - 20
platform/javascript/js/engine/config.js

@@ -225,33 +225,34 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-
 	 */
 	Config.prototype.update = function (opts) {
 		const config = opts || {};
-		function parse(key, def) {
+		const me = this;
+		function parse(key) {
 			if (typeof (config[key]) === 'undefined') {
-				return def;
+				return me[key];
 			}
 			return config[key];
 		}
 		// Module config
-		this.unloadAfterInit = parse('unloadAfterInit', this.unloadAfterInit);
-		this.onPrintError = parse('onPrintError', this.onPrintError);
-		this.onPrint = parse('onPrint', this.onPrint);
-		this.onProgress = parse('onProgress', this.onProgress);
+		this.unloadAfterInit = parse('unloadAfterInit');
+		this.onPrintError = parse('onPrintError');
+		this.onPrint = parse('onPrint');
+		this.onProgress = parse('onProgress');
 
 		// Godot config
-		this.canvas = parse('canvas', this.canvas);
-		this.executable = parse('executable', this.executable);
-		this.mainPack = parse('mainPack', this.mainPack);
-		this.locale = parse('locale', this.locale);
-		this.canvasResizePolicy = parse('canvasResizePolicy', this.canvasResizePolicy);
-		this.persistentPaths = parse('persistentPaths', this.persistentPaths);
-		this.persistentDrops = parse('persistentDrops', this.persistentDrops);
-		this.experimentalVK = parse('experimentalVK', this.experimentalVK);
-		this.focusCanvas = parse('focusCanvas', this.focusCanvas);
-		this.gdnativeLibs = parse('gdnativeLibs', this.gdnativeLibs);
-		this.fileSizes = parse('fileSizes', this.fileSizes);
-		this.args = parse('args', this.args);
-		this.onExecute = parse('onExecute', this.onExecute);
-		this.onExit = parse('onExit', this.onExit);
+		this.canvas = parse('canvas');
+		this.executable = parse('executable');
+		this.mainPack = parse('mainPack');
+		this.locale = parse('locale');
+		this.canvasResizePolicy = parse('canvasResizePolicy');
+		this.persistentPaths = parse('persistentPaths');
+		this.persistentDrops = parse('persistentDrops');
+		this.experimentalVK = parse('experimentalVK');
+		this.focusCanvas = parse('focusCanvas');
+		this.gdnativeLibs = parse('gdnativeLibs');
+		this.fileSizes = parse('fileSizes');
+		this.args = parse('args');
+		this.onExecute = parse('onExecute');
+		this.onExit = parse('onExit');
 	};
 
 	/**

+ 1 - 1
platform/javascript/js/libs/library_godot_input.js

@@ -87,7 +87,7 @@ const GodotInputGamepads = {
 		},
 
 		init: function (onchange) {
-			GodotEventListeners.samples = [];
+			GodotInputGamepads.samples = [];
 			function add(pad) {
 				const guid = GodotInputGamepads.get_guid(pad);
 				const c_id = GodotRuntime.allocString(pad.id);

+ 5 - 0
platform/windows/godot_windows.cpp

@@ -140,6 +140,11 @@ __declspec(dllexport) int widechar_main(int argc, wchar_t **argv) {
 
 	setlocale(LC_CTYPE, "");
 
+#ifndef TOOLS_ENABLED
+	// Workaround to prevent LTCG (MSVC LTO) from removing "pck" section
+	char *dummy_guard = dummy;
+#endif
+
 	char **argv_utf8 = new char *[argc];
 
 	for (int i = 0; i < argc; ++i) {

+ 2 - 2
scene/2d/collision_object_2d.cpp

@@ -170,7 +170,7 @@ uint32_t CollisionObject2D::create_shape_owner(Object *p_owner) {
 		id = shapes.back()->key() + 1;
 	}
 
-	sd.owner = p_owner;
+	sd.owner_id = p_owner ? p_owner->get_instance_id() : 0;
 
 	shapes[id] = sd;
 
@@ -283,7 +283,7 @@ Transform2D CollisionObject2D::shape_owner_get_transform(uint32_t p_owner) const
 Object *CollisionObject2D::shape_owner_get_owner(uint32_t p_owner) const {
 	ERR_FAIL_COND_V(!shapes.has(p_owner), nullptr);
 
-	return shapes[p_owner].owner;
+	return ObjectDB::get_instance(shapes[p_owner].owner_id);
 }
 
 void CollisionObject2D::shape_owner_add_shape(uint32_t p_owner, const Ref<Shape2D> &p_shape) {

+ 2 - 2
scene/2d/collision_object_2d.h

@@ -45,7 +45,7 @@ class CollisionObject2D : public Node2D {
 	bool pickable;
 
 	struct ShapeData {
-		Object *owner;
+		ObjectID owner_id;
 		Transform2D xform;
 		struct Shape {
 			Ref<Shape2D> shape;
@@ -61,7 +61,7 @@ class CollisionObject2D : public Node2D {
 			disabled = false;
 			one_way_collision = false;
 			one_way_collision_margin = 0;
-			owner = nullptr;
+			owner_id = 0;
 		}
 	};
 

+ 2 - 2
scene/3d/collision_object.cpp

@@ -363,7 +363,7 @@ uint32_t CollisionObject::create_shape_owner(Object *p_owner) {
 		id = shapes.back()->key() + 1;
 	}
 
-	sd.owner = p_owner;
+	sd.owner_id = p_owner ? p_owner->get_instance_id() : 0;
 
 	shapes[id] = sd;
 
@@ -442,7 +442,7 @@ Transform CollisionObject::shape_owner_get_transform(uint32_t p_owner) const {
 Object *CollisionObject::shape_owner_get_owner(uint32_t p_owner) const {
 	ERR_FAIL_COND_V(!shapes.has(p_owner), nullptr);
 
-	return shapes[p_owner].owner;
+	return ObjectDB::get_instance(shapes[p_owner].owner_id);
 }
 
 void CollisionObject::shape_owner_add_shape(uint32_t p_owner, const Ref<Shape> &p_shape) {

+ 2 - 2
scene/3d/collision_object.h

@@ -45,7 +45,7 @@ class CollisionObject : public Spatial {
 	RID rid;
 
 	struct ShapeData {
-		Object *owner;
+		ObjectID owner_id;
 		Transform xform;
 		struct ShapeBase {
 			RID debug_shape;
@@ -58,7 +58,7 @@ class CollisionObject : public Spatial {
 
 		ShapeData() {
 			disabled = false;
-			owner = nullptr;
+			owner_id = 0;
 		}
 	};
 

+ 10 - 2
scene/gui/texture_button.cpp

@@ -168,6 +168,12 @@ void TextureButton::_notification(int p_what) {
 
 			Point2 ofs;
 			Size2 size;
+			bool draw_focus = (has_focus() && focused.is_valid());
+
+			// If no other texture is valid, try using focused texture.
+			if (!texdraw.is_valid() && draw_focus) {
+				texdraw = focused;
+			}
 
 			if (texdraw.is_valid()) {
 				size = texdraw->get_size();
@@ -224,7 +230,9 @@ void TextureButton::_notification(int p_what) {
 				size.width *= hflip ? -1.0f : 1.0f;
 				size.height *= vflip ? -1.0f : 1.0f;
 
-				if (_tile) {
+				if (texdraw == focused) {
+					// Do nothing, we only needed to calculate the rectangle.
+				} else if (_tile) {
 					draw_texture_rect(texdraw, Rect2(ofs, size), _tile);
 				} else {
 					draw_texture_rect_region(texdraw, Rect2(ofs, size), _texture_region);
@@ -233,7 +241,7 @@ void TextureButton::_notification(int p_what) {
 				_position_rect = Rect2();
 			}
 
-			if (has_focus() && focused.is_valid()) {
+			if (draw_focus) {
 				draw_texture_rect(focused, Rect2(ofs, size), false);
 			};
 		} break;

+ 36 - 31
servers/visual/portals/portal_gameplay_monitor.cpp

@@ -109,7 +109,7 @@ void PortalGameplayMonitor::unload(PortalRenderer &p_portal_renderer) {
 	for (int n = 0; n < _active_room_ids_prev->size(); n++) {
 		int room_id = (*_active_room_ids_prev)[n];
 		VSRoom &room = p_portal_renderer.get_room(room_id);
-		room.last_gameplay_tick_hit = 0;
+		room.last_room_tick_hit = 0;
 
 		VisualServerCallbacks::Message msg;
 		msg.object_id = room._godot_instance_ID;
@@ -121,7 +121,7 @@ void PortalGameplayMonitor::unload(PortalRenderer &p_portal_renderer) {
 	for (int n = 0; n < _active_roomgroup_ids_prev->size(); n++) {
 		int roomgroup_id = (*_active_roomgroup_ids_prev)[n];
 		VSRoomGroup &roomgroup = p_portal_renderer.get_roomgroup(roomgroup_id);
-		roomgroup.last_gameplay_tick_hit = 0;
+		roomgroup.last_room_tick_hit = 0;
 
 		VisualServerCallbacks::Message msg;
 		msg.object_id = roomgroup._godot_instance_ID;
@@ -133,7 +133,7 @@ void PortalGameplayMonitor::unload(PortalRenderer &p_portal_renderer) {
 	for (int n = 0; n < _active_sghost_ids_prev->size(); n++) {
 		int id = (*_active_sghost_ids_prev)[n];
 		VSStaticGhost &ghost = p_portal_renderer.get_static_ghost(id);
-		ghost.last_gameplay_tick_hit = 0;
+		ghost.last_room_tick_hit = 0;
 
 		VisualServerCallbacks::Message msg;
 		msg.object_id = ghost.object_id;
@@ -185,6 +185,9 @@ void PortalGameplayMonitor::update_gameplay(PortalRenderer &p_portal_renderer, c
 	// if there is no change in the source room IDs, then we can optimize out a lot of the checks
 	// (anything not to do with roamers)
 	bool source_rooms_changed = _source_rooms_changed(p_source_room_ids, p_num_source_rooms);
+	if (source_rooms_changed) {
+		_room_tick++;
+	}
 
 	// lock output
 	VisualServerCallbacks *callbacks = VSG::scene->get_callbacks();
@@ -249,7 +252,7 @@ void PortalGameplayMonitor::update_gameplay(PortalRenderer &p_portal_renderer, c
 			const VSRoom &room = p_portal_renderer.get_room(room_id);
 
 			// gone out of view
-			if (room.last_gameplay_tick_hit != _gameplay_tick) {
+			if (room.last_room_tick_hit != _room_tick) {
 				VisualServerCallbacks::Message msg;
 				msg.object_id = room._godot_instance_ID;
 				msg.type = _exit_callback_type;
@@ -264,7 +267,7 @@ void PortalGameplayMonitor::update_gameplay(PortalRenderer &p_portal_renderer, c
 			const VSRoomGroup &roomgroup = p_portal_renderer.get_roomgroup(roomgroup_id);
 
 			// gone out of view
-			if (roomgroup.last_gameplay_tick_hit != _gameplay_tick) {
+			if (roomgroup.last_room_tick_hit != _room_tick) {
 				VisualServerCallbacks::Message msg;
 				msg.object_id = roomgroup._godot_instance_ID;
 				msg.type = _exit_callback_type;
@@ -279,7 +282,7 @@ void PortalGameplayMonitor::update_gameplay(PortalRenderer &p_portal_renderer, c
 			VSStaticGhost &ghost = p_portal_renderer.get_static_ghost(id);
 
 			// gone out of view
-			if (ghost.last_gameplay_tick_hit != _gameplay_tick) {
+			if (ghost.last_room_tick_hit != _room_tick) {
 				VisualServerCallbacks::Message msg;
 				msg.object_id = ghost.object_id;
 				msg.type = VisualServerCallbacks::CALLBACK_NOTIFICATION_EXIT_GAMEPLAY;
@@ -293,7 +296,7 @@ void PortalGameplayMonitor::update_gameplay(PortalRenderer &p_portal_renderer, c
 	callbacks->unlock();
 
 	// swap the current and previous lists
-	_swap();
+	_swap(source_rooms_changed);
 }
 
 void PortalGameplayMonitor::_update_gameplay_room(PortalRenderer &p_portal_renderer, int p_room_id, bool p_source_rooms_changed) {
@@ -367,14 +370,14 @@ void PortalGameplayMonitor::_update_gameplay_room(PortalRenderer &p_portal_rende
 	// later tests only relevant if a room has just come into play
 	bool room_came_into_play = false;
 
-	if (room.last_gameplay_tick_hit != _gameplay_tick) {
+	if (room.last_room_tick_hit != _room_tick) {
 		room_came_into_play = true;
 
 		// add the room to the active list
 		_active_room_ids_curr->push_back(p_room_id);
 
 		// if wasn't present in the tick before, add the notification to enter
-		if (room.last_gameplay_tick_hit != (_gameplay_tick - 1)) {
+		if (room.last_room_tick_hit != (_room_tick - 1)) {
 			VisualServerCallbacks::Message msg;
 			msg.object_id = room._godot_instance_ID;
 			msg.type = _enter_callback_type;
@@ -383,7 +386,7 @@ void PortalGameplayMonitor::_update_gameplay_room(PortalRenderer &p_portal_rende
 		}
 
 		// mark as done
-		room.last_gameplay_tick_hit = _gameplay_tick;
+		room.last_room_tick_hit = _room_tick;
 	}
 
 	// no need to do later tests
@@ -398,12 +401,12 @@ void PortalGameplayMonitor::_update_gameplay_room(PortalRenderer &p_portal_rende
 
 		VSRoomGroup &roomgroup = p_portal_renderer.get_roomgroup(roomgroup_id);
 
-		if (roomgroup.last_gameplay_tick_hit != _gameplay_tick) {
+		if (roomgroup.last_room_tick_hit != _room_tick) {
 			// add the room to the active list
 			_active_roomgroup_ids_curr->push_back(roomgroup_id);
 
 			// if wasn't present in the tick before, add the notification to enter
-			if (roomgroup.last_gameplay_tick_hit != (_gameplay_tick - 1)) {
+			if (roomgroup.last_room_tick_hit != (_room_tick - 1)) {
 				VisualServerCallbacks::Message msg;
 				msg.object_id = roomgroup._godot_instance_ID;
 				msg.type = _enter_callback_type;
@@ -412,7 +415,7 @@ void PortalGameplayMonitor::_update_gameplay_room(PortalRenderer &p_portal_rende
 			}
 
 			// mark as done
-			roomgroup.last_gameplay_tick_hit = _gameplay_tick;
+			roomgroup.last_room_tick_hit = _room_tick;
 		}
 	} // for through roomgroups
 
@@ -425,14 +428,14 @@ void PortalGameplayMonitor::_update_gameplay_room(PortalRenderer &p_portal_rende
 		VSStaticGhost &ghost = p_portal_renderer.get_static_ghost(id);
 
 		// done already?
-		if (ghost.last_gameplay_tick_hit == _gameplay_tick)
+		if (ghost.last_room_tick_hit == _room_tick)
 			continue;
 
 		// add to the active list
 		_active_sghost_ids_curr->push_back(id);
 
 		// if wasn't present in the tick before, add the notification to enter
-		if (ghost.last_gameplay_tick_hit != (_gameplay_tick - 1)) {
+		if (ghost.last_room_tick_hit != (_room_tick - 1)) {
 			VisualServerCallbacks::Message msg;
 			msg.object_id = ghost.object_id;
 			msg.type = VisualServerCallbacks::CALLBACK_NOTIFICATION_ENTER_GAMEPLAY;
@@ -441,11 +444,11 @@ void PortalGameplayMonitor::_update_gameplay_room(PortalRenderer &p_portal_rende
 		}
 
 		// mark as done
-		ghost.last_gameplay_tick_hit = _gameplay_tick;
+		ghost.last_room_tick_hit = _room_tick;
 	}
 }
 
-void PortalGameplayMonitor::_swap() {
+void PortalGameplayMonitor::_swap(bool p_source_rooms_changed) {
 	LocalVector<uint32_t, int32_t> *temp = _active_moving_pool_ids_curr;
 	_active_moving_pool_ids_curr = _active_moving_pool_ids_prev;
 	_active_moving_pool_ids_prev = temp;
@@ -456,18 +459,20 @@ void PortalGameplayMonitor::_swap() {
 	_active_rghost_pool_ids_prev = temp;
 	_active_rghost_pool_ids_curr->clear();
 
-	temp = _active_room_ids_curr;
-	_active_room_ids_curr = _active_room_ids_prev;
-	_active_room_ids_prev = temp;
-	_active_room_ids_curr->clear();
-
-	temp = _active_roomgroup_ids_curr;
-	_active_roomgroup_ids_curr = _active_roomgroup_ids_prev;
-	_active_roomgroup_ids_prev = temp;
-	_active_roomgroup_ids_curr->clear();
-
-	temp = _active_sghost_ids_curr;
-	_active_sghost_ids_curr = _active_sghost_ids_prev;
-	_active_sghost_ids_prev = temp;
-	_active_sghost_ids_curr->clear();
+	if (p_source_rooms_changed) {
+		temp = _active_room_ids_curr;
+		_active_room_ids_curr = _active_room_ids_prev;
+		_active_room_ids_prev = temp;
+		_active_room_ids_curr->clear();
+
+		temp = _active_roomgroup_ids_curr;
+		_active_roomgroup_ids_curr = _active_roomgroup_ids_prev;
+		_active_roomgroup_ids_prev = temp;
+		_active_roomgroup_ids_curr->clear();
+
+		temp = _active_sghost_ids_curr;
+		_active_sghost_ids_curr = _active_sghost_ids_prev;
+		_active_sghost_ids_prev = temp;
+		_active_sghost_ids_curr->clear();
+	}
 }

+ 8 - 1
servers/visual/portals/portal_gameplay_monitor.h

@@ -52,10 +52,17 @@ public:
 private:
 	void _update_gameplay_room(PortalRenderer &p_portal_renderer, int p_room_id, bool p_source_rooms_changed);
 	bool _source_rooms_changed(const int *p_source_room_ids, int p_num_source_rooms);
-	void _swap();
+	void _swap(bool p_source_rooms_changed);
 
+	// gameplay ticks happen every physics tick
 	uint32_t _gameplay_tick = 1;
 
+	// Room ticks only happen when the rooms the cameras are within change.
+	// This is an optimization. This tick needs to be maintained separately from _gameplay_tick
+	// because testing against the previous tick is used to determine whether to send enter or exit
+	// gameplay notifications, and this must be synchronized differently for rooms, roomgroups and static ghosts.
+	uint32_t _room_tick = 1;
+
 	// we need two version, current and previous
 	LocalVector<uint32_t, int32_t> _active_moving_pool_ids[2];
 	LocalVector<uint32_t, int32_t> *_active_moving_pool_ids_curr;

+ 1 - 1
servers/visual/portals/portal_renderer.h

@@ -68,7 +68,7 @@ struct VSStaticGhost {
 	ObjectID object_id;
 
 	uint32_t last_tick_hit = 0;
-	uint32_t last_gameplay_tick_hit = 0;
+	uint32_t last_room_tick_hit = 0;
 };
 
 class PortalRenderer {

+ 3 - 3
servers/visual/portals/portal_types.h

@@ -227,7 +227,7 @@ struct VSRoomGroup {
 	}
 
 	// used for calculating gameplay notifications
-	uint32_t last_gameplay_tick_hit = 0;
+	uint32_t last_room_tick_hit = 0;
 
 	ObjectID _godot_instance_ID = 0;
 
@@ -257,7 +257,7 @@ struct VSRoom {
 		_secondary_pvs_size = 0;
 		_priority = 0;
 		_contains_internal_rooms = false;
-		last_gameplay_tick_hit = 0;
+		last_room_tick_hit = 0;
 	}
 
 	void cleanup_after_conversion() {
@@ -354,7 +354,7 @@ struct VSRoom {
 	uint16_t _secondary_pvs_size = 0;
 
 	// used for calculating gameplay notifications
-	uint32_t last_gameplay_tick_hit = 0;
+	uint32_t last_room_tick_hit = 0;
 
 	// convex hull of the room, either determined by geometry or manual bound
 	LocalVector<Plane, int32_t> _planes;