浏览代码

Fixing handling of max bodies and max contact constraints (#1506)

* Added the following constants on PhysicsSystem: cMaxBodiesLimit, cMaxBodyPairsLimit and cMaxContactConstraintsLimit. These constants are the max allowable values for PhysicsSystem::Init. Exceeding these will trigger an assert and the system will clamp the values.
* Moved the 'broad phase bit' to the highest bit in BodyID to avoid running out of NodeIDs in BroadPhaseQuadTree when calling PhysicsSystem::OptimizeBroadPhase on a tree with a very high body count.
* TempAllocatorImpl uses 64 bit integers internally to allow for a higher max contact constraint count.
Updated MaxBodies performance test to also test the max number of constraints.
Jorrit Rouwe 7 月之前
父节点
当前提交
e021bd2de6

+ 4 - 4
.github/workflows/determinism_check.yml

@@ -1,10 +1,10 @@
 name: Determinism Check
 
 env:
-  CONVEX_VS_MESH_HASH: '0x554adfba1815d6af'
-  RAGDOLL_HASH: '0xdf768b4736057c87'
-  PYRAMID_HASH: '0x2e2dda8c1f4eb906'
-  CHARACTER_VIRTUAL_HASH: '0x4ec98831ce0590ff'
+  CONVEX_VS_MESH_HASH: '0xbcf1289e948679f9'
+  RAGDOLL_HASH: '0xb6979cd9fc00610b'
+  PYRAMID_HASH: '0x62b2a3fa25d06200'
+  CHARACTER_VIRTUAL_HASH: '0x19c55223035a8f1a'
   EMSCRIPTEN_VERSION: 4.0.2
   NODE_VERSION: 23.x
   UBUNTU_CLANG_VERSION: clang++-18

+ 3 - 0
Docs/ReleaseNotes.md

@@ -26,6 +26,7 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * Added overridable `CollisionCollector::OnBodyEnd` that is called after all hits for a body have been processed when collecting hits through `NarrowPhaseQuery`.
 * Added `ClosestHitPerBodyCollisionCollector` which will report the closest / deepest hit per body that the collision query collides with.
 * Added `PhysicsSystem::SetSimCollideBodyVsBody`. This allows overriding the collision detection between two bodies. It can be used to only store the 1st hit for sensor collisions. This makes sensors cheaper if you only need to know if there is an overlap or not. An example can be found in `SimCollideBodyVsBodyTest`.
+* Added the following constants on PhysicsSystem: cMaxBodiesLimit, cMaxBodyPairsLimit and cMaxContactConstraintsLimit. These constants are the max allowable values for PhysicsSystem::Init. Exceeding these will trigger an assert and the system will clamp the values.
 
 ### Bug fixes
 
@@ -45,6 +46,8 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * Fixed a bug in ManifoldBetweenTwoFaces that led to incorrect `ContactManifold::mRelativeContactPointsOn2` when the contact normal and the face normal were not roughly parallel. Also it led to possibly jitter in the simulation in that case.
 * Fixed InternalEdgeRemovingCollector not working when colliding with a very dense triangle grid because it ran out of internal space. Now falling back to memory allocations when this happens to avoid ghost collisions.
 * Fixed running out of stack space when simulating a really high number of active rigid bodies.
+* Moved the 'broad phase bit' to the highest bit in BodyID to avoid running out of NodeIDs in BroadPhaseQuadTree when calling PhysicsSystem::OptimizeBroadPhase on a tree with a very high body count.
+* TempAllocatorImpl uses 64 bit integers internally to allow for a higher max contact constraint count.
 
 ## v5.2.0
 

+ 2 - 1
Jolt/Core/Core.h

@@ -430,7 +430,8 @@
 	JPH_MSVC_SUPPRESS_WARNING(4514)																\
 	JPH_MSVC_SUPPRESS_WARNING(5262)																\
 	JPH_MSVC_SUPPRESS_WARNING(5264)																\
-	JPH_MSVC_SUPPRESS_WARNING(4738)
+	JPH_MSVC_SUPPRESS_WARNING(4738)																\
+	JPH_MSVC_SUPPRESS_WARNING(5045)
 
 #define JPH_SUPPRESS_WARNINGS_STD_END															\
 	JPH_SUPPRESS_WARNING_POP

+ 6 - 6
Jolt/Core/TempAllocator.h

@@ -34,7 +34,7 @@ public:
 	JPH_OVERRIDE_NEW_DELETE
 
 	/// Constructs the allocator with a maximum allocatable size of inSize
-	explicit						TempAllocatorImpl(uint inSize) :
+	explicit						TempAllocatorImpl(size_t inSize) :
 		mBase(static_cast<uint8 *>(AlignedAllocate(inSize, JPH_RVECTOR_ALIGNMENT))),
 		mSize(inSize)
 	{
@@ -56,7 +56,7 @@ public:
 		}
 		else
 		{
-			uint new_top = mTop + AlignUp(inSize, JPH_RVECTOR_ALIGNMENT);
+			size_t new_top = mTop + AlignUp(inSize, JPH_RVECTOR_ALIGNMENT);
 			if (new_top > mSize)
 			{
 				Trace("TempAllocator: Out of memory trying to allocate %u bytes", inSize);
@@ -93,13 +93,13 @@ public:
 	}
 
 	/// Get the total size of the fixed buffer
-	uint							GetSize() const
+	size_t							GetSize() const
 	{
 		return mSize;
 	}
 
 	/// Get current usage in bytes of the buffer
-	uint							GetUsage() const
+	size_t							GetUsage() const
 	{
 		return mTop;
 	}
@@ -118,8 +118,8 @@ public:
 
 private:
 	uint8 *							mBase;							///< Base address of the memory block
-	uint							mSize;							///< Size of the memory block
-	uint							mTop = 0;						///< End of currently allocated area
+	size_t							mSize;							///< Size of the memory block
+	size_t							mTop = 0;						///< End of currently allocated area
 };
 
 /// Implementation of the TempAllocator that just falls back to malloc/free

+ 1 - 1
Jolt/Jolt.natvis

@@ -68,7 +68,7 @@
 		<DisplayString>min=({mMin}), max=({mMax})</DisplayString>
 	</Type>
 	<Type Name="JPH::BodyID">
-		<DisplayString>{mID}</DisplayString>
+		<DisplayString>idx={mID &amp; 0x007fffff}, seq={(mID >> 23) &amp; 0xff}, in_bp={mID >> 24,d}</DisplayString>
 	</Type>
 	<Type Name="JPH::Body">
 		<DisplayString>{mDebugName}: p=({mPosition.mF32[0],g}, {mPosition.mF32[1],g}, {mPosition.mF32[2],g}), r=({mRotation.mValue.mF32[0],g}, {mRotation.mValue.mF32[1],g}, {mRotation.mValue.mF32[2],g}, {mRotation.mValue.mF32[3],g}), v=({mLinearVelocity.mF32[0],g}, {mLinearVelocity.mF32[1],g}, {mLinearVelocity.mF32[2],g}), w=({mAngularVelocity.mF32[0],g}, {mAngularVelocity.mF32[1],g}, {mAngularVelocity.mF32[2],g})</DisplayString>

+ 4 - 3
Jolt/Physics/Body/BodyID.h

@@ -15,9 +15,10 @@ public:
 	JPH_OVERRIDE_NEW_DELETE
 
 	static constexpr uint32	cInvalidBodyID = 0xffffffff;	///< The value for an invalid body ID
-	static constexpr uint32	cBroadPhaseBit = 0x00800000;	///< This bit is used by the broadphase
+	static constexpr uint32	cBroadPhaseBit = 0x80000000;	///< This bit is used by the broadphase
 	static constexpr uint32	cMaxBodyIndex = 0x7fffff;		///< Maximum value for body index (also the maximum amount of bodies supported - 1)
 	static constexpr uint8	cMaxSequenceNumber = 0xff;		///< Maximum value for the sequence number
+	static constexpr uint	cSequenceNumberShift = 23;		///< Number of bits to shift to get the sequence number
 
 	/// Construct invalid body ID
 							BodyID() :
@@ -34,7 +35,7 @@ public:
 
 	/// Construct from index and sequence number
 	explicit				BodyID(uint32 inID, uint8 inSequenceNumber) :
-		mID((uint32(inSequenceNumber) << 24) | inID)
+		mID((uint32(inSequenceNumber) << cSequenceNumberShift) | inID)
 	{
 		JPH_ASSERT(inID <= cMaxBodyIndex); // Should not overlap with broadphase bit or sequence number
 	}
@@ -51,7 +52,7 @@ public:
 	/// Functions querying the broadphase can (after acquiring a body lock) detect that the body has been removed (we assume that this won't happen more than 128 times in a row).
 	inline uint8			GetSequenceNumber() const
 	{
-		return uint8(mID >> 24);
+		return uint8(mID >> cSequenceNumberShift);
 	}
 
 	/// Returns the index and sequence number combined in an uint32

+ 1 - 1
Jolt/Physics/Collision/BroadPhase/QuadTree.h

@@ -38,7 +38,7 @@ private:
 		/// Construct a node ID
 		static inline NodeID	sInvalid()							{ return NodeID(cInvalidNodeIndex); }
 		static inline NodeID	sFromBodyID(BodyID inID)			{ NodeID node_id(inID.GetIndexAndSequenceNumber()); JPH_ASSERT(node_id.IsBody()); return node_id; }
-		static inline NodeID	sFromNodeIndex(uint32 inIdx)		{ NodeID node_id(inIdx | cIsNode); JPH_ASSERT(node_id.IsNode()); return node_id; }
+		static inline NodeID	sFromNodeIndex(uint32 inIdx)		{ JPH_ASSERT((inIdx & cIsNode) == 0); return NodeID(inIdx | cIsNode); }
 
 		/// Check what type of ID it is
 		inline bool				IsValid() const						{ return mID != cInvalidNodeIndex; }

+ 15 - 6
Jolt/Physics/Constraints/ContactConstraintManager.cpp

@@ -249,9 +249,14 @@ void ContactConstraintManager::CachedBodyPair::RestoreState(StateRecorder &inStr
 
 void ContactConstraintManager::ManifoldCache::Init(uint inMaxBodyPairs, uint inMaxContactConstraints, uint inCachedManifoldsSize)
 {
-	mAllocator.Init(inMaxBodyPairs * sizeof(BodyPairMap::KeyValue) + inCachedManifoldsSize);
+	uint max_body_pairs = min(inMaxBodyPairs, cMaxBodyPairsLimit);
+	JPH_ASSERT(max_body_pairs == inMaxBodyPairs, "Cannot support this many body pairs!");
+	JPH_ASSERT(inMaxContactConstraints <= cMaxContactConstraintsLimit); // Should have been enforced by caller
+
+	mAllocator.Init(uint(min(uint64(max_body_pairs) * sizeof(BodyPairMap::KeyValue) + inCachedManifoldsSize, uint64(~uint(0)))));
+
 	mCachedManifolds.Init(GetNextPowerOf2(inMaxContactConstraints));
-	mCachedBodyPairs.Init(GetNextPowerOf2(inMaxBodyPairs));
+	mCachedBodyPairs.Init(GetNextPowerOf2(max_body_pairs));
 }
 
 void ContactConstraintManager::ManifoldCache::Clear()
@@ -676,14 +681,18 @@ ContactConstraintManager::~ContactConstraintManager()
 
 void ContactConstraintManager::Init(uint inMaxBodyPairs, uint inMaxContactConstraints)
 {
-	mMaxConstraints = inMaxContactConstraints;
+	// Limit the number of constraints so that the allocation size fits in an unsigned integer
+	mMaxConstraints = min(inMaxContactConstraints, cMaxContactConstraintsLimit);
+	JPH_ASSERT(mMaxConstraints == inMaxContactConstraints, "Cannot support this many contact constraints!");
 
 	// Calculate worst case cache usage
-	uint cached_manifolds_size = inMaxContactConstraints * (sizeof(CachedManifold) + (MaxContactPoints - 1) * sizeof(CachedContactPoint));
+	constexpr uint cMaxManifoldSizePerConstraint = sizeof(CachedManifold) + (MaxContactPoints - 1) * sizeof(CachedContactPoint);
+	static_assert(cMaxManifoldSizePerConstraint < sizeof(ContactConstraint)); // If not true, then the next line can overflow
+	uint cached_manifolds_size = mMaxConstraints * cMaxManifoldSizePerConstraint;
 
 	// Init the caches
-	mCache[0].Init(inMaxBodyPairs, inMaxContactConstraints, cached_manifolds_size);
-	mCache[1].Init(inMaxBodyPairs, inMaxContactConstraints, cached_manifolds_size);
+	mCache[0].Init(inMaxBodyPairs, mMaxConstraints, cached_manifolds_size);
+	mCache[1].Init(inMaxBodyPairs, mMaxConstraints, cached_manifolds_size);
 }
 
 void ContactConstraintManager::PrepareConstraintBuffer(PhysicsUpdateContext *inContext)

+ 8 - 0
Jolt/Physics/Constraints/ContactConstraintManager.h

@@ -472,6 +472,14 @@ private:
 		WorldContactPoints		mContactPoints;
 	};
 
+public:
+	/// The maximum value that can be passed to Init for inMaxContactConstraints. Note you should really use a lower value, using this value will cost a lot of memory!
+	static constexpr uint		cMaxContactConstraintsLimit = ~uint(0) / sizeof(ContactConstraint);
+
+	/// The maximum value that can be passed to Init for inMaxBodyPairs. Note you should really use a lower value, using this value will cost a lot of memory!
+	static constexpr uint		cMaxBodyPairsLimit = ~uint(0) / sizeof(BodyPairMap::KeyValue);
+
+private:
 	/// Internal helper function to calculate the friction and non-penetration constraint properties. Templated to the motion type to reduce the amount of branches and calculations.
 	template <EMotionType Type1, EMotionType Type2>
 	JPH_INLINE void				TemplatedCalculateFrictionAndNonPenetrationConstraintProperties(ContactConstraint &ioConstraint, const ContactSettings &inSettings, float inDeltaTime, Vec3Arg inGravityDeltaTime, RMat44Arg inTransformBody1, RMat44Arg inTransformBody2, const Body &inBody1, const Body &inBody2);

+ 5 - 3
Jolt/Physics/PhysicsSystem.cpp

@@ -77,13 +77,15 @@ PhysicsSystem::~PhysicsSystem()
 
 void PhysicsSystem::Init(uint inMaxBodies, uint inNumBodyMutexes, uint inMaxBodyPairs, uint inMaxContactConstraints, const BroadPhaseLayerInterface &inBroadPhaseLayerInterface, const ObjectVsBroadPhaseLayerFilter &inObjectVsBroadPhaseLayerFilter, const ObjectLayerPairFilter &inObjectLayerPairFilter)
 {
-	JPH_ASSERT(inMaxBodies <= BodyID::cMaxBodyIndex + 1, "Cannot support this many bodies");
+	// Clamp max bodies
+	uint max_bodies = min(inMaxBodies, cMaxBodiesLimit);
+	JPH_ASSERT(max_bodies == inMaxBodies, "Cannot support this many bodies!");
 
 	mObjectVsBroadPhaseLayerFilter = &inObjectVsBroadPhaseLayerFilter;
 	mObjectLayerPairFilter = &inObjectLayerPairFilter;
 
 	// Initialize body manager
-	mBodyManager.Init(inMaxBodies, inNumBodyMutexes, inBroadPhaseLayerInterface);
+	mBodyManager.Init(max_bodies, inNumBodyMutexes, inBroadPhaseLayerInterface);
 
 	// Create broadphase
 	mBroadPhase = new BROAD_PHASE();
@@ -93,7 +95,7 @@ void PhysicsSystem::Init(uint inMaxBodies, uint inNumBodyMutexes, uint inMaxBody
 	mContactManager.Init(inMaxBodyPairs, inMaxContactConstraints);
 
 	// Init islands builder
-	mIslandBuilder.Init(inMaxBodies);
+	mIslandBuilder.Init(max_bodies);
 
 	// Initialize body interface
 	mBodyInterfaceLocking.Init(mBodyLockInterfaceLocking, mBodyManager, *mBroadPhase);

+ 9 - 0
Jolt/Physics/PhysicsSystem.h

@@ -35,6 +35,15 @@ public:
 								PhysicsSystem()												: mContactManager(mPhysicsSettings) JPH_IF_ENABLE_ASSERTS(, mConstraintManager(&mBodyManager)) { }
 								~PhysicsSystem();
 
+	/// The maximum value that can be passed to Init for inMaxBodies.
+	static constexpr uint		cMaxBodiesLimit = BodyID::cMaxBodyIndex + 1;
+
+	/// The maximum value that can be passed to Init for inMaxBodyPairs. Note you should really use a lower value, using this value will cost a lot of memory!
+	static constexpr uint		cMaxBodyPairsLimit = ContactConstraintManager::cMaxBodyPairsLimit;
+
+	/// The maximum value that can be passed to Init for inMaxContactConstraints. Note you should really use a lower value, using this value will cost a lot of memory!
+	static constexpr uint		cMaxContactConstraintsLimit = ContactConstraintManager::cMaxContactConstraintsLimit;
+
 	/// Initialize the system.
 	/// @param inMaxBodies Maximum number of bodies to support.
 	/// @param inNumBodyMutexes Number of body mutexes to use. Should be a power of 2 in the range [1, 64], use 0 to auto detect.

+ 27 - 8
PerformanceTest/MaxBodiesScene.h

@@ -21,14 +21,24 @@ public:
 		return "MaxBodies";
 	}
 
-	virtual uint			GetTempAllocatorSizeMB() const override
+	virtual size_t			GetTempAllocatorSizeMB() const override
 	{
-		return 256;
+		return 8192;
 	}
 
 	virtual uint			GetMaxBodies() const override
 	{
-		return BodyID::cMaxBodyIndex + 1;
+		return PhysicsSystem::cMaxBodiesLimit;
+	}
+
+	virtual uint			GetMaxBodyPairs() const override
+	{
+		return PhysicsSystem::cMaxBodyPairsLimit;
+	}
+
+	virtual uint			GetMaxContactConstraints() const override
+	{
+		return PhysicsSystem::cMaxContactConstraintsLimit;
 	}
 
 	virtual void			StartTest(PhysicsSystem &inPhysicsSystem, EMotionQuality inMotionQuality) override
@@ -42,17 +52,26 @@ public:
 		inPhysicsSystem.SetPhysicsSettings(settings);
 
 		// Create the bodies
-		uint num_bodies = GetMaxBodies();
+		uint num_bodies = inPhysicsSystem.GetMaxBodies();
+		uint num_constraints = 0;
 		BodyIDVector body_ids;
 		body_ids.reserve(num_bodies);
 		uint num_per_axis = uint(pow(float(num_bodies), 1.0f / 3.0f)) + 1;
-		BodyCreationSettings bcs(new BoxShape(Vec3::sReplicate(0.5f)), RVec3::sZero(), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
-		for (uint x = 0; x < num_per_axis && body_ids.size() < num_bodies; ++x)
+		Vec3 half_extent = Vec3::sReplicate(0.5f);
+		BodyCreationSettings bcs(new BoxShape(half_extent), RVec3::sZero(), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
+		bcs.mOverrideMassProperties = EOverrideMassProperties::MassAndInertiaProvided;
+		bcs.mMassPropertiesOverride.SetMassAndInertiaOfSolidBox(2.0f * half_extent, 1000.0f);
+		for (uint z = 0; z < num_per_axis && body_ids.size() < num_bodies; ++z)
 			for (uint y = 0; y < num_per_axis && body_ids.size() < num_bodies; ++y)
-				for (uint z = 0; z < num_per_axis && body_ids.size() < num_bodies; ++z)
+				for (uint x = 0; x < num_per_axis && body_ids.size() < num_bodies; ++x)
 				{
-					bcs.mPosition = RVec3(Real(x), Real(y), Real(z));
+					// When we reach the limit of contact constraints, start placing the boxes further apart so they don't collide
+					bcs.mPosition = RVec3(num_constraints < PhysicsSystem::cMaxContactConstraintsLimit? Real(x) : 2.0_r * x, 2.0_r * y, 2.0_r * z);
 					body_ids.push_back(bi.CreateBody(bcs)->GetID());
+
+					// From the 2nd box onwards in a row, we will get a contact constraint
+					if (x > 0)
+						++num_constraints;
 				}
 
 		// Add the bodies to the simulation

+ 4 - 4
PerformanceTest/PerformanceTest.cpp

@@ -218,9 +218,6 @@ int main(int argc, char** argv)
 	// Register all Jolt physics types
 	RegisterTypes();
 
-	// Create temp allocator
-	TempAllocatorImpl temp_allocator(scene->GetTempAllocatorSizeMB() * 1024 * 1024);
-
 	// Show used instruction sets
 	Trace(GetConfigurationString());
 
@@ -231,6 +228,9 @@ int main(int argc, char** argv)
 	// Output scene we're running
 	Trace("Running scene: %s", scene->GetName());
 
+	// Create temp allocator
+	TempAllocatorImpl temp_allocator(scene->GetTempAllocatorSizeMB() * 1024 * 1024);
+
 	// Find the asset path
 	bool found = false;
 	filesystem::path asset_path(argv[0]);
@@ -309,7 +309,7 @@ int main(int argc, char** argv)
 
 				// Create physics system
 				PhysicsSystem physics_system;
-				physics_system.Init(scene->GetMaxBodies(), 0, 65536, 20480, broad_phase_layer_interface, object_vs_broadphase_layer_filter, object_vs_object_layer_filter);
+				physics_system.Init(scene->GetMaxBodies(), 0, scene->GetMaxBodyPairs(), scene->GetMaxContactConstraints(), broad_phase_layer_interface, object_vs_broadphase_layer_filter, object_vs_object_layer_filter);
 
 				// Start test scene
 				scene->StartTest(physics_system, motion_quality);

+ 7 - 1
PerformanceTest/PerformanceTestScene.h

@@ -15,11 +15,17 @@ public:
 	virtual const char *	GetName() const = 0;
 
 	// Get the number of MB that the temp allocator should preallocate
-	virtual uint			GetTempAllocatorSizeMB() const						{ return 32; }
+	virtual size_t			GetTempAllocatorSizeMB() const						{ return 32; }
 
 	// Get the max number of bodies to support in the physics system
 	virtual uint			GetMaxBodies() const								{ return 10240; }
 
+	// Get the max number of body pairs to support in the physics system
+	virtual uint			GetMaxBodyPairs() const								{ return 65536; }
+
+	// Get the max number of contact constraints to support in the physics system
+	virtual uint			GetMaxContactConstraints() const					{ return 20480; }
+
 	// Load assets for the scene
 	virtual bool			Load([[maybe_unused]] const String &inAssetPath)	{ return true; }
 

+ 7 - 5
TestFramework/Renderer/VK/RendererVK.cpp

@@ -18,6 +18,7 @@
 #include <Jolt/Core/QuickSort.h>
 #include <Jolt/Core/RTTI.h>
 
+JPH_SUPPRESS_WARNINGS_STD_BEGIN
 #ifdef JPH_PLATFORM_WINDOWS
 	#include <vulkan/vulkan_win32.h>
 	#include <Window/ApplicationWindowWin.h>
@@ -28,6 +29,7 @@
 	#include <vulkan/vulkan_metal.h>
 	#include <Window/ApplicationWindowMacOS.h>
 #endif
+JPH_SUPPRESS_WARNINGS_STD_END
 
 #ifdef JPH_DEBUG
 
@@ -774,10 +776,10 @@ void RendererVK::DestroySwapChain()
 		vkDestroyImageView(mDevice, view, nullptr);
 	mSwapChainImageViews.clear();
 
-	if (mSwapChain != nullptr)
+	if (mSwapChain != VK_NULL_HANDLE)
 	{
 		vkDestroySwapchainKHR(mDevice, mSwapChain, nullptr);
-		mSwapChain = nullptr;
+		mSwapChain = VK_NULL_HANDLE;
 	}
 }
 
@@ -795,7 +797,7 @@ void RendererVK::BeginFrame(const CameraState &inCamera, float inWorldScale)
 	Renderer::BeginFrame(inCamera, inWorldScale);
 
 	// If we have no swap chain, bail out
-	if (mSwapChain == nullptr)
+	if (mSwapChain == VK_NULL_HANDLE)
 		return;
 
 	// Update frame index
@@ -810,7 +812,7 @@ void RendererVK::BeginFrame(const CameraState &inCamera, float inWorldScale)
 		vkDeviceWaitIdle(mDevice);
 		DestroySwapChain();
 		CreateSwapChain(mPhysicalDevice);
-		if (mSwapChain == nullptr)
+		if (mSwapChain == VK_NULL_HANDLE)
 			return;
 		result = vkAcquireNextImageKHR(mDevice, mSwapChain, UINT64_MAX, mImageAvailableSemaphores[mFrameIndex], VK_NULL_HANDLE, &mImageIndex);
 		mSubOptimalSwapChain = false;
@@ -906,7 +908,7 @@ void RendererVK::EndFrame()
 	JPH_PROFILE_FUNCTION();
 
 	// If we have no swap chain, bail out
-	if (mSwapChain == nullptr)
+	if (mSwapChain == VK_NULL_HANDLE)
 	{
 		Renderer::EndFrame();
 		return;