2
0
Эх сурвалжийг харах

Samples and JoltViewer can run on macOS (#1434)

Moved window management code out of Renderer so that the window can be created using objective C
Note that the Vulkan SDK is required to run the samples on macOS: https://vulkan.lunarg.com/sdk/home#mac
Jorrit Rouwe 7 сар өмнө
parent
commit
a9d7cd16f0
43 өөрчлөгдсөн 1140 нэмэгдсэн , 393 устгасан
  1. 7 1
      .github/workflows/build.yml
  2. 2 5
      Assets/Shaders/TrianglePixelShader.frag
  3. 3 5
      Build/CMakeLists.txt
  4. 1 0
      Build/README.md
  5. 34 0
      Build/iOS/JoltViewerInfo.plist
  6. 34 0
      Build/iOS/SamplesInfo.plist
  7. 13 0
      Build/macos_install_vulkan_sdk.sh
  8. 1 1
      Docs/ReleaseNotes.md
  9. 7 1
      JoltViewer/JoltViewer.cmake
  10. 3 3
      README.md
  11. 7 1
      Samples/Samples.cmake
  12. 143 116
      TestFramework/Application/Application.cpp
  13. 6 0
      TestFramework/Application/Application.h
  14. 2 2
      TestFramework/Input/Keyboard.h
  15. 9 9
      TestFramework/Input/Linux/KeyboardLinux.cpp
  16. 4 2
      TestFramework/Input/Linux/KeyboardLinux.h
  17. 5 4
      TestFramework/Input/Linux/MouseLinux.cpp
  18. 1 1
      TestFramework/Input/Linux/MouseLinux.h
  19. 35 0
      TestFramework/Input/MacOS/KeyboardMacOS.h
  20. 134 0
      TestFramework/Input/MacOS/KeyboardMacOS.mm
  21. 54 0
      TestFramework/Input/MacOS/MouseMacOS.h
  22. 103 0
      TestFramework/Input/MacOS/MouseMacOS.mm
  23. 2 2
      TestFramework/Input/Mouse.h
  24. 3 2
      TestFramework/Input/Win/KeyboardWin.cpp
  25. 1 1
      TestFramework/Input/Win/KeyboardWin.h
  26. 6 6
      TestFramework/Input/Win/MouseWin.cpp
  27. 4 2
      TestFramework/Input/Win/MouseWin.h
  28. 12 13
      TestFramework/Renderer/DX12/RendererDX12.cpp
  29. 1 1
      TestFramework/Renderer/DX12/RendererDX12.h
  30. 9 163
      TestFramework/Renderer/Renderer.cpp
  31. 9 33
      TestFramework/Renderer/Renderer.h
  32. 27 11
      TestFramework/Renderer/VK/RendererVK.cpp
  33. 1 1
      TestFramework/Renderer/VK/RendererVK.h
  34. 29 0
      TestFramework/TestFramework.cmake
  35. 4 3
      TestFramework/UI/UIAnimationSlide.cpp
  36. 6 4
      TestFramework/UI/UIManager.cpp
  37. 38 0
      TestFramework/Window/ApplicationWindow.h
  38. 67 0
      TestFramework/Window/ApplicationWindowLinux.cpp
  39. 32 0
      TestFramework/Window/ApplicationWindowLinux.h
  40. 41 0
      TestFramework/Window/ApplicationWindowMacOS.h
  41. 109 0
      TestFramework/Window/ApplicationWindowMacOS.mm
  42. 107 0
      TestFramework/Window/ApplicationWindowWin.cpp
  43. 24 0
      TestFramework/Window/ApplicationWindowWin.h

+ 7 - 1
.github/workflows/build.yml

@@ -352,6 +352,8 @@ jobs:
   macos:
     runs-on: macos-latest
     name: macOS
+    env:
+        VULKAN_SDK_INSTALL: ${{github.workspace}}/vulkan_sdk
     strategy:
         fail-fast: false
         matrix:
@@ -360,8 +362,12 @@ jobs:
     steps:
     - name: Checkout Code
       uses: actions/checkout@v4
+    - name: Install Vulkan
+      run: ${{github.workspace}}/Build/macos_install_vulkan_sdk.sh ${VULKAN_SDK_INSTALL}
     - name: Configure CMake
-      run: cmake -B ${{github.workspace}}/Build/MacOS_${{matrix.build_type}} -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -DCMAKE_CXX_COMPILER=clang++ Build
+      run: |
+        source ${VULKAN_SDK_INSTALL}/setup-env.sh
+        cmake -B ${{github.workspace}}/Build/MacOS_${{matrix.build_type}} -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -DCMAKE_CXX_COMPILER=clang++ Build
     - name: Build
       run: cmake --build ${{github.workspace}}/Build/MacOS_${{matrix.build_type}} -j $(nproc)
     - name: Test

+ 2 - 5
Assets/Shaders/TrianglePixelShader.frag

@@ -14,7 +14,7 @@ layout(location = 4) in vec4 iColor;
 
 layout(location = 0) out vec4 oColor;
 
-layout(set = 1, binding = 0) uniform sampler2DShadow LightDepthSampler;
+layout(set = 1, binding = 0) uniform sampler2D LightDepthSampler;
 
 void main()
 {
@@ -85,10 +85,7 @@ void main()
 		// Sample shadow factor
 		shadow_factor = 0.0;
 		for (uint i = 0; i < num_samples; ++i)
-		{
-			vec3 location_and_reference = vec3(tex_coord + offsets[i], light_depth);
-			shadow_factor += texture(LightDepthSampler, location_and_reference);
-		}
+			shadow_factor += texture(LightDepthSampler, tex_coord + offsets[i]).x <= light_depth? 1.0 : 0.0;
 		shadow_factor /= num_samples;
 	}
 

+ 3 - 5
Build/CMakeLists.txt

@@ -406,7 +406,7 @@ if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
 		endif()
 	endif()
 
-	if ((WIN32 OR LINUX) AND NOT ("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "ARM")) # ARM 32-bit is missing dinput8.lib
+	if ((WIN32 OR LINUX OR ("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin")) AND NOT ("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "ARM")) # ARM 32-bit is missing dinput8.lib
 		# Windows only targets
 		if (TARGET_SAMPLES OR TARGET_VIEWER)
 			include(${PHYSICS_REPO_ROOT}/TestFramework/TestFramework.cmake)
@@ -428,9 +428,7 @@ if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
 	endif()
 
 	# Copy the assets folder
-	if (TARGET_PERFORMANCE_TEST)
-		add_custom_command(TARGET PerformanceTest PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${PHYSICS_REPO_ROOT}/Assets/ $<TARGET_FILE_DIR:PerformanceTest>/Assets/)
-	elseif (TEST_FRAMEWORK_AVAILABLE)
-		add_custom_command(TARGET TestFramework PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${PHYSICS_REPO_ROOT}/Assets/ $<TARGET_FILE_DIR:TestFramework>/Assets/)
+	if (TARGET_PERFORMANCE_TEST OR TEST_FRAMEWORK_AVAILABLE)
+		add_custom_command(TARGET Jolt PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${PHYSICS_REPO_ROOT}/Assets/ $<TARGET_FILE_DIR:Jolt>/Assets/)
 	endif()
 endif()

+ 1 - 0
Build/README.md

@@ -152,6 +152,7 @@ To implement your custom memory allocator override Allocate, Free, Reallocate, A
 	<ul>
 		<li>Install XCode</li>
 		<li>Download CMake 3.23+ (https://cmake.org/download/)</li>
+		<li>If you want to build the Samples or JoltViewer, install the <a href="https://vulkan.lunarg.com/sdk/home#mac">Vulkan SDK</a></li>
 		<li>Run: ./cmake_xcode_macos.sh</li>
 		<li>This will open XCode with a newly generated project</li>
 		<li>Build and run the project</li>

+ 34 - 0
Build/iOS/JoltViewerInfo.plist

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>English</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleGetInfoString</key>
+	<string></string>
+	<key>CFBundleIconFile</key>
+	<string></string>
+	<key>CFBundleIdentifier</key>
+	<string>com.joltphysics.joltviewer</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleLongVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleName</key>
+	<string>JoltViewer</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>1.0</string>
+	<key>CSResourcesFileMapped</key>
+	<true/>
+	<key>NSHumanReadableCopyright</key>
+	<string></string>
+</dict>
+</plist>

+ 34 - 0
Build/iOS/SamplesInfo.plist

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>English</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleGetInfoString</key>
+	<string></string>
+	<key>CFBundleIconFile</key>
+	<string></string>
+	<key>CFBundleIdentifier</key>
+	<string>com.joltphysics.samples</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleLongVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleName</key>
+	<string>Samples</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>1.0</string>
+	<key>CSResourcesFileMapped</key>
+	<true/>
+	<key>NSHumanReadableCopyright</key>
+	<string></string>
+</dict>
+</plist>

+ 13 - 0
Build/macos_install_vulkan_sdk.sh

@@ -0,0 +1,13 @@
+set -e
+if [ -z $1 ] 
+then
+	echo "Need to specify installation diretory"
+	exit
+fi
+
+VULKAN_TEMP=/tmp/vulkan_sdk_install
+mkdir ${VULKAN_TEMP}
+curl -L -o ${VULKAN_TEMP}/vulkan_sdk.dmg https://sdk.lunarg.com/sdk/download/latest/mac/vulkan_sdk.dmg?Human=true
+unzip ${VULKAN_TEMP}/vulkan_sdk.dmg -d ${VULKAN_TEMP}
+${VULKAN_TEMP}/InstallVulkan.app/Contents/MacOS/InstallVulkan --root $1 --accept-licenses --default-answer --confirm-command install
+rm -rf ${VULKAN_TEMP}

+ 1 - 1
Docs/ReleaseNotes.md

@@ -16,7 +16,7 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
 * Added binary serialization to `SkeletalAnimation`.
 * Added support for RISC-V, LoongArch and PowerPC (Little Endian) CPUs.
 * Added the ability to add a sub shape at a specified index in a MutableCompoundShape rather than at the end.
-* The Samples and JoltViewer can run on Linux using Vulkan now. Make sure to install the Vulkan SDK before compiling (see: Build/ubuntu24_install_vulkan_sdk.sh).
+* The Samples and JoltViewer can run on Linux/macOS using Vulkan now. Make sure to install the Vulkan SDK before compiling (see: Build/ubuntu24_install_vulkan_sdk.sh or [download](https://vulkan.lunarg.com/sdk/home) the SDK).
 
 ### Bug fixes
 

+ 7 - 1
JoltViewer/JoltViewer.cmake

@@ -12,7 +12,13 @@ set(JOLT_VIEWER_SRC_FILES
 source_group(TREE ${JOLT_VIEWER_ROOT} FILES ${JOLT_VIEWER_SRC_FILES})
 
 # Create JoltViewer executable
-add_executable(JoltViewer ${JOLT_VIEWER_SRC_FILES})
+if ("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin")
+	add_executable(JoltViewer MACOSX_BUNDLE ${JOLT_VIEWER_SRC_FILES})
+	set_property(TARGET JoltViewer PROPERTY MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/iOS/JoltViewerInfo.plist")
+	set_property(TARGET JoltViewer PROPERTY XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.joltphysics.joltviewer")
+else()
+	add_executable(JoltViewer ${JOLT_VIEWER_SRC_FILES})
+endif()
 target_include_directories(JoltViewer PUBLIC ${JOLT_VIEWER_ROOT})
 target_link_libraries(JoltViewer LINK_PUBLIC TestFramework)
 

+ 3 - 3
README.md

@@ -135,10 +135,10 @@ If you're interested in how Jolt scales with multiple CPUs and compares to other
 * Docs - Contains documentation for the library.
 * HelloWorld - A simple application demonstrating how to use the Jolt Physics library.
 * Jolt - All source code for the library is in this folder.
-* JoltViewer - It is possible to record the output of the physics engine using the DebugRendererRecorder class (a .jor file), this folder contains the source code to an application that can visualize a recording. This is useful for e.g. visualizing the output of the PerformanceTest from different platforms. Currently available on Windows and Linux only.
+* JoltViewer - It is possible to record the output of the physics engine using the DebugRendererRecorder class (a .jor file), this folder contains the source code to an application that can visualize a recording. This is useful for e.g. visualizing the output of the PerformanceTest from different platforms. Currently available on Windows, macOS and Linux.
 * PerformanceTest - Contains a simple application that runs a [performance test](Docs/PerformanceTest.md) and collects timing information.
-* Samples - This contains the sample application, see the [Samples](Docs/Samples.md) section. Currently available on Windows and Linux only.
-* TestFramework - A rendering framework to visualize the results of the physics engine. Used by Samples and JoltViewer. Currently available on Windows and Linux only.
+* Samples - This contains the sample application, see the [Samples](Docs/Samples.md) section. Currently available on Windows, macOS and Linux.
+* TestFramework - A rendering framework to visualize the results of the physics engine. Used by Samples and JoltViewer. Currently available on Windows, macOS and Linux.
 * UnitTests - A set of unit tests to validate the behavior of the physics engine.
 * WebIncludes - A number of JavaScript resources used by the internal profiling framework of the physics engine.
 

+ 7 - 1
Samples/Samples.cmake

@@ -312,7 +312,13 @@ endif()
 source_group(TREE ${SAMPLES_ROOT} FILES ${SAMPLES_SRC_FILES})
 
 # Create Samples executable
-add_executable(Samples ${SAMPLES_SRC_FILES})
+if ("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin")
+	add_executable(Samples MACOSX_BUNDLE ${SAMPLES_SRC_FILES})
+	set_property(TARGET Samples PROPERTY MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/iOS/SamplesInfo.plist")
+	set_property(TARGET Samples PROPERTY XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.joltphysics.samples")
+else()
+	add_executable(Samples ${SAMPLES_SRC_FILES})
+endif()
 target_include_directories(Samples PUBLIC ${SAMPLES_ROOT})
 target_link_libraries(Samples LINK_PUBLIC TestFramework)
 

+ 143 - 116
TestFramework/Application/Application.cpp

@@ -22,9 +22,15 @@
 	#include <crtdbg.h>
 	#include <Input/Win/KeyboardWin.h>
 	#include <Input/Win/MouseWin.h>
+	#include <Window/ApplicationWindowWin.h>
 #elif defined(JPH_PLATFORM_LINUX)
 	#include <Input/Linux/KeyboardLinux.h>
 	#include <Input/Linux/MouseLinux.h>
+	#include <Window/ApplicationWindowLinux.h>
+#elif defined(JPH_PLATFORM_MACOS)
+	#include <Input/MacOS/KeyboardMacOS.h>
+	#include <Input/MacOS/MouseMacOS.h>
+	#include <Window/ApplicationWindowMacOS.h>
 #endif
 
 JPH_GCC_SUPPRESS_WARNING("-Wswitch")
@@ -65,6 +71,18 @@ Application::Application([[maybe_unused]] const String &inCommandLine) :
 		// Disable allocation checking
 		DisableCustomMemoryHook dcmh;
 
+		// Create window
+	#ifdef JPH_PLATFORM_WINDOWS
+		mWindow = new ApplicationWindowWin;
+	#elif defined(JPH_PLATFORM_LINUX)
+		mWindow = new ApplicationWindowLinux;
+	#elif defined(JPH_PLATFORM_MACOS)
+		mWindow = new ApplicationWindowMacOS;
+	#else
+		#error No window defined
+	#endif
+		mWindow->Initialize();
+
 		// Create renderer
 	#ifdef JPH_ENABLE_VULKAN
 		mRenderer = new RendererVK;
@@ -73,7 +91,7 @@ Application::Application([[maybe_unused]] const String &inCommandLine) :
 	#else
 		#error No renderer defined
 	#endif
-		mRenderer->Initialize();
+		mRenderer->Initialize(mWindow);
 
 		// Create font
 		Font *font = new Font(mRenderer);
@@ -88,20 +106,24 @@ Application::Application([[maybe_unused]] const String &inCommandLine) :
 		mKeyboard = new KeyboardWin;
 	#elif defined(JPH_PLATFORM_LINUX)
 		mKeyboard = new KeyboardLinux;
+	#elif defined(JPH_PLATFORM_MACOS)
+		mKeyboard = new KeyboardMacOS;
 	#else
 		#error No keyboard defined
 	#endif
-		mKeyboard->Initialize(mRenderer);
+		mKeyboard->Initialize(mWindow);
 
 		// Init mouse
 	#ifdef JPH_PLATFORM_WINDOWS
 		mMouse = new MouseWin;
 	#elif defined(JPH_PLATFORM_LINUX)
 		mMouse = new MouseLinux;
+	#elif defined(JPH_PLATFORM_MACOS)
+		mMouse = new MouseMacOS;
 	#else
 		#error No mouse defined
 	#endif
-		mMouse->Initialize(mRenderer);
+		mMouse->Initialize(mWindow);
 
 		// Init UI
 		mUI = new UIManager(mRenderer);
@@ -129,6 +151,7 @@ Application::~Application()
 		delete mDebugRenderer;
 		mFont = nullptr;
 		delete mRenderer;
+		delete mWindow;
 	}
 
 	// Unregisters all types with the factory and cleans up the default material
@@ -166,143 +189,147 @@ void Application::Run()
 	// Set initial camera position
 	ResetCamera();
 
-	// Main message loop
-	while (mRenderer->WindowUpdate())
-	{
-		// Get new input
-		mKeyboard->Poll();
-		mMouse->Poll();
+	// Enter the main loop
+	mWindow->MainLoop([this]() { return RenderFrame(); });
+}
 
-		// Handle keyboard input
-		for (EKey key = mKeyboard->GetFirstKey(); key != EKey::Invalid; key = mKeyboard->GetNextKey())
-			switch (key)
-			{
-			case EKey::P:
-				mIsPaused = !mIsPaused;
-				break;
-
-			case EKey::O:
-				mSingleStep = true;
-				break;
-
-			case EKey::T:
-				// Dump timing info to file
-				JPH_PROFILE_DUMP();
-				break;
-
-			case EKey::Escape:
-				mDebugUI->ToggleVisibility();
-				break;
-			}
+bool Application::RenderFrame()
+{
+	// Get new input
+	mKeyboard->Poll();
+	mMouse->Poll();
 
-		// Calculate delta time
-		chrono::high_resolution_clock::time_point time = chrono::high_resolution_clock::now();
-		chrono::microseconds delta = chrono::duration_cast<chrono::microseconds>(time - mLastUpdateTime);
-		mLastUpdateTime = time;
-		float clock_delta_time = 1.0e-6f * delta.count();
-		float world_delta_time = 0.0f;
-		if (mRequestedDeltaTime <= 0.0f)
+	// Handle keyboard input
+	for (EKey key = mKeyboard->GetFirstKey(); key != EKey::Invalid; key = mKeyboard->GetNextKey())
+		switch (key)
 		{
-			// If no fixed frequency update is requested, update with variable time step
-			world_delta_time = !mIsPaused || mSingleStep? clock_delta_time : 0.0f;
-			mResidualDeltaTime = 0.0f;
+		case EKey::P:
+			mIsPaused = !mIsPaused;
+			break;
+
+		case EKey::O:
+			mSingleStep = true;
+			break;
+
+		case EKey::T:
+			// Dump timing info to file
+			JPH_PROFILE_DUMP();
+			break;
+
+		case EKey::Escape:
+			mDebugUI->ToggleVisibility();
+			break;
 		}
-		else
+
+	// Calculate delta time
+	chrono::high_resolution_clock::time_point time = chrono::high_resolution_clock::now();
+	chrono::microseconds delta = chrono::duration_cast<chrono::microseconds>(time - mLastUpdateTime);
+	mLastUpdateTime = time;
+	float clock_delta_time = 1.0e-6f * delta.count();
+	float world_delta_time = 0.0f;
+	if (mRequestedDeltaTime <= 0.0f)
+	{
+		// If no fixed frequency update is requested, update with variable time step
+		world_delta_time = !mIsPaused || mSingleStep? clock_delta_time : 0.0f;
+		mResidualDeltaTime = 0.0f;
+	}
+	else
+	{
+		// Else use fixed time steps
+		if (mSingleStep)
 		{
-			// Else use fixed time steps
-			if (mSingleStep)
+			// Single step
+			world_delta_time = mRequestedDeltaTime;
+		}
+		else if (!mIsPaused)
+		{
+			// Calculate how much time has passed since the last render
+			world_delta_time = clock_delta_time + mResidualDeltaTime;
+			if (world_delta_time < mRequestedDeltaTime)
 			{
-				// Single step
-				world_delta_time = mRequestedDeltaTime;
+				// Too soon, set the residual time and don't update
+				mResidualDeltaTime = world_delta_time;
+				world_delta_time = 0.0f;
 			}
-			else if (!mIsPaused)
+			else
 			{
-				// Calculate how much time has passed since the last render
-				world_delta_time = clock_delta_time + mResidualDeltaTime;
-				if (world_delta_time < mRequestedDeltaTime)
-				{
-					// Too soon, set the residual time and don't update
-					mResidualDeltaTime = world_delta_time;
-					world_delta_time = 0.0f;
-				}
-				else
-				{
-					// Update and clamp the residual time to a full update to avoid spiral of death
-					mResidualDeltaTime = min(mRequestedDeltaTime, world_delta_time - mRequestedDeltaTime);
-					world_delta_time = mRequestedDeltaTime;
-				}
+				// Update and clamp the residual time to a full update to avoid spiral of death
+				mResidualDeltaTime = min(mRequestedDeltaTime, world_delta_time - mRequestedDeltaTime);
+				world_delta_time = mRequestedDeltaTime;
 			}
 		}
-		mSingleStep = false;
+	}
+	mSingleStep = false;
 
-		// Clear debug lines if we're going to step
-		if (world_delta_time > 0.0f)
-			ClearDebugRenderer();
+	// Clear debug lines if we're going to step
+	if (world_delta_time > 0.0f)
+		ClearDebugRenderer();
 
-		{
-			JPH_PROFILE("UpdateFrame");
-			if (!UpdateFrame(world_delta_time))
-				break;
-		}
-
-		// Draw coordinate axis
-		if (mDebugRendererCleared)
-			mDebugRenderer->DrawCoordinateSystem(RMat44::sIdentity());
+	{
+		JPH_PROFILE("UpdateFrame");
+		if (!UpdateFrame(world_delta_time))
+			return false;
+	}
 
-		// For next frame: mark that we haven't cleared debug stuff
-		mDebugRendererCleared = false;
+	// Draw coordinate axis
+	if (mDebugRendererCleared)
+		mDebugRenderer->DrawCoordinateSystem(RMat44::sIdentity());
 
-		// Update the camera position
-		if (!mUI->IsVisible())
-			UpdateCamera(clock_delta_time);
+	// For next frame: mark that we haven't cleared debug stuff
+	mDebugRendererCleared = false;
 
-		// Start rendering
-		mRenderer->BeginFrame(mWorldCamera, GetWorldScale());
+	// Update the camera position
+	if (!mUI->IsVisible())
+		UpdateCamera(clock_delta_time);
 
-		// Draw from light
-		static_cast<DebugRendererImp *>(mDebugRenderer)->DrawShadowPass();
+	// Start rendering
+	mRenderer->BeginFrame(mWorldCamera, GetWorldScale());
 
-		// Start drawing normally
-		mRenderer->EndShadowPass();
+	// Draw from light
+	static_cast<DebugRendererImp *>(mDebugRenderer)->DrawShadowPass();
 
-		// Draw debug information
-		static_cast<DebugRendererImp *>(mDebugRenderer)->Draw();
+	// Start drawing normally
+	mRenderer->EndShadowPass();
 
-		// Draw the frame rate counter
-		DrawFPS(clock_delta_time);
+	// Draw debug information
+	static_cast<DebugRendererImp *>(mDebugRenderer)->Draw();
 
-		if (mUI->IsVisible())
-		{
-			// Send mouse input to UI
-			bool left_pressed = mMouse->IsLeftPressed();
-			if (left_pressed && !mLeftMousePressed)
-				mUI->MouseDown(mMouse->GetX(), mMouse->GetY());
-			else if (!left_pressed && mLeftMousePressed)
-				mUI->MouseUp(mMouse->GetX(), mMouse->GetY());
-			mLeftMousePressed = left_pressed;
-			mUI->MouseMove(mMouse->GetX(), mMouse->GetY());
+	// Draw the frame rate counter
+	DrawFPS(clock_delta_time);
 
-			{
-				// Disable allocation checking
-				DisableCustomMemoryHook dcmh;
+	if (mUI->IsVisible())
+	{
+		// Send mouse input to UI
+		bool left_pressed = mMouse->IsLeftPressed();
+		if (left_pressed && !mLeftMousePressed)
+			mUI->MouseDown(mMouse->GetX(), mMouse->GetY());
+		else if (!left_pressed && mLeftMousePressed)
+			mUI->MouseUp(mMouse->GetX(), mMouse->GetY());
+		mLeftMousePressed = left_pressed;
+		mUI->MouseMove(mMouse->GetX(), mMouse->GetY());
 
-				// Update and draw the menu
-				mUI->Update(clock_delta_time);
-				mUI->Draw();
-			}
-		}
-		else
 		{
-			// Menu not visible, cancel any mouse operations
-			mUI->MouseCancel();
+			// Disable allocation checking
+			DisableCustomMemoryHook dcmh;
+
+			// Update and draw the menu
+			mUI->Update(clock_delta_time);
+			mUI->Draw();
 		}
+	}
+	else
+	{
+		// Menu not visible, cancel any mouse operations
+		mUI->MouseCancel();
+	}
 
-		// Show the frame
-		mRenderer->EndFrame();
+	// Show the frame
+	mRenderer->EndFrame();
 
-		// Notify of next frame
-		JPH_PROFILE_NEXTFRAME();
-	}
+	// Notify of next frame
+	JPH_PROFILE_NEXTFRAME();
+
+	return true;
 }
 
 void Application::GetCameraLocalHeadingAndPitch(float &outHeading, float &outPitch)
@@ -395,7 +422,7 @@ void Application::DrawFPS(float inDeltaTime)
 	int text_h = int(text_size.y * mFont->GetCharHeight());
 
 	// Draw FPS counter
-	int x = (mRenderer->GetWindowWidth() - text_w) / 2 - 20;
+	int x = (mWindow->GetWindowWidth() - text_w) / 2 - 20;
 	int y = 10;
 	mUI->DrawQuad(x - 5, y - 3, text_w + 10, text_h + 6, UITexturedQuad(), Color(0, 0, 0, 128));
 	mUI->DrawText(x, y, fps, mFont);
@@ -409,7 +436,7 @@ void Application::DrawFPS(float inDeltaTime)
 	{
 		string_view paused_str = "P: Unpause, ESC: Menu";
 		Float2 pause_size = mFont->MeasureText(paused_str);
-		mUI->DrawText(mRenderer->GetWindowWidth() - 5 - int(pause_size.x * mFont->GetCharHeight()), 5, paused_str, mFont);
+		mUI->DrawText(mWindow->GetWindowWidth() - 5 - int(pause_size.x * mFont->GetCharHeight()), 5, paused_str, mFont);
 	}
 
 	// Restore state

+ 6 - 0
TestFramework/Application/Application.h

@@ -30,6 +30,9 @@ protected:
 	/// Debug renderer module
 	DebugRenderer *				mDebugRenderer;
 
+	/// Main window
+	ApplicationWindow *			mWindow;
+
 	/// Render module
 	Renderer *					mRenderer;
 
@@ -90,6 +93,9 @@ protected:
 	void						ClearDebugRenderer();
 
 private:
+	/// Render a frame
+	bool						RenderFrame();
+
 	/// Extract heading and pitch from the local space (relative to the camera pivot) camera forward
 	void						GetCameraLocalHeadingAndPitch(float &outHeading, float &outPitch);
 

+ 2 - 2
TestFramework/Input/Keyboard.h

@@ -4,7 +4,7 @@
 
 #pragma once
 
-class Renderer;
+class ApplicationWindow;
 
 enum class EKey
 {
@@ -73,7 +73,7 @@ public:
 	virtual							~Keyboard() = default;
 
 	/// Initialization / shutdown
-	virtual bool					Initialize(Renderer *inRenderer) = 0;
+	virtual bool					Initialize(ApplicationWindow *inWindow) = 0;
 	virtual void					Shutdown() = 0;
 
 	/// Update the keyboard state

+ 9 - 9
TestFramework/Input/Linux/KeyboardLinux.cpp

@@ -5,27 +5,27 @@
 #include <TestFramework.h>
 
 #include <Input/Linux/KeyboardLinux.h>
-#include <Renderer/Renderer.h>
+#include <Window/ApplicationWindowLinux.h>
 
 KeyboardLinux::~KeyboardLinux()
 {
 	Shutdown();
 }
 
-bool KeyboardLinux::Initialize(Renderer *inRenderer)
+bool KeyboardLinux::Initialize(ApplicationWindow *inWindow)
 {
-	mRenderer = inRenderer;
-	inRenderer->SetEventListener([this](const XEvent &inEvent) { HandleEvent(inEvent); });
+	mWindow = static_cast<ApplicationWindowLinux *>(inWindow);
+	mWindow->SetEventListener([this](const XEvent &inEvent) { HandleEvent(inEvent); });
 
 	return true;
 }
 
 void KeyboardLinux::Shutdown()
 {
-	if (mRenderer != nullptr)
+	if (mWindow != nullptr)
 	{
-		mRenderer->SetEventListener({});
-		mRenderer = nullptr;
+		mWindow->SetEventListener({});
+		mWindow = nullptr;
 	}
 }
 
@@ -34,7 +34,7 @@ void KeyboardLinux::Poll()
 	// Reset the keys pressed
 	memset(mKeysPressed, 0, sizeof(mKeysPressed));
 
-	Display *display = mRenderer->GetDisplay();
+	Display *display = mWindow->GetDisplay();
 
 	// Get pressed keys
 	char keymap[32];
@@ -86,7 +86,7 @@ void KeyboardLinux::HandleEvent(const XEvent &inEvent)
 	if (inEvent.type == KeyPress && mPendingKeyBuffer.size() < mPendingKeyBuffer.capacity())
 	{
 		// Convert to key
-		KeySym keysym = XkbKeycodeToKeysym(mRenderer->GetDisplay(), inEvent.xkey.keycode, 0, 0);
+		KeySym keysym = XkbKeycodeToKeysym(mWindow->GetDisplay(), inEvent.xkey.keycode, 0, 0);
 		EKey key = ToKey(keysym);
 		if (key != EKey::Unknown)
 			mPendingKeyBuffer.push_back(key);

+ 4 - 2
TestFramework/Input/Linux/KeyboardLinux.h

@@ -7,6 +7,8 @@
 #include <Input/Keyboard.h>
 #include <Jolt/Core/StaticArray.h>
 
+class ApplicationWindowLinux;
+
 /// Keyboard interface class which keeps track on the status of all keys and keeps track of the list of keys pressed.
 class KeyboardLinux : public Keyboard
 {
@@ -15,7 +17,7 @@ public:
 	virtual							~KeyboardLinux() override;
 
 	/// Initialization / shutdown
-	virtual bool					Initialize(Renderer *inRenderer) override;
+	virtual bool					Initialize(ApplicationWindow *inWindow) override;
 	virtual void					Shutdown() override;
 
 	/// Update the keyboard state
@@ -32,7 +34,7 @@ private:
 	void							HandleEvent(const XEvent &inEvent);
 	EKey							ToKey(int inKey) const;
 
-	Renderer *						mRenderer = nullptr;
+	ApplicationWindowLinux *		mWindow = nullptr;
 	bool							mKeysPressed[(int)EKey::NumKeys] = { };
 	StaticArray<EKey, 128>			mPendingKeyBuffer;
 	StaticArray<EKey, 128>			mKeyBuffer;

+ 5 - 4
TestFramework/Input/Linux/MouseLinux.cpp

@@ -5,7 +5,7 @@
 #include <TestFramework.h>
 
 #include <Input/Linux/MouseLinux.h>
-#include <Renderer/Renderer.h>
+#include <Window/ApplicationWindowLinux.h>
 
 MouseLinux::MouseLinux()
 {
@@ -17,10 +17,11 @@ MouseLinux::~MouseLinux()
 	Shutdown();
 }
 
-bool MouseLinux::Initialize(Renderer *inRenderer)
+bool MouseLinux::Initialize(ApplicationWindow *inWindow)
 {
-	mDisplay = inRenderer->GetDisplay();
-	mWindow = inRenderer->GetWindow();
+	ApplicationWindowLinux *window = static_cast<ApplicationWindowLinux *>(inWindow);
+	mDisplay = window->GetDisplay();
+	mWindow = window->GetWindow();
 
 	// Poll once and reset the deltas
 	Poll();

+ 1 - 1
TestFramework/Input/Linux/MouseLinux.h

@@ -15,7 +15,7 @@ public:
 	virtual							~MouseLinux() override;
 
 	/// Initialization / shutdown
-	virtual bool					Initialize(Renderer *inWindow) override;
+	virtual bool					Initialize(ApplicationWindow *inWindow) override;
 	virtual void					Shutdown() override;
 
 	/// Update the mouse state

+ 35 - 0
TestFramework/Input/MacOS/KeyboardMacOS.h

@@ -0,0 +1,35 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Input/Keyboard.h>
+
+/// Keyboard interface class which keeps track on the status of all keys and keeps track of the list of keys pressed.
+class KeyboardMacOS : public Keyboard
+{
+public:
+	/// Initialization / shutdown
+	virtual bool					Initialize(ApplicationWindow *inWindow) override;
+	virtual void					Shutdown() override							{ }
+
+	/// Update the keyboard state
+	virtual void					Poll() override;
+
+	/// Checks if a key is pressed or not
+	virtual bool					IsKeyPressed(EKey inKey) const override		{ return mKeyPressed[(int)inKey]; }
+
+	/// Buffered keyboard input, returns EKey::Invalid for none
+	virtual EKey					GetFirstKey() override;
+	virtual EKey					GetNextKey() override;
+	
+	/// Handle a key press event
+	void							OnKeyPressed(EKey inKey, bool inPressed);
+
+private:
+	bool							mKeyPressed[(int)EKey::NumKeys] = { };
+	StaticArray<EKey, 128>			mPendingKeyBuffer;
+	StaticArray<EKey, 128>			mKeyBuffer;	
+	uint							mCurrentKey = 0;
+};

+ 134 - 0
TestFramework/Input/MacOS/KeyboardMacOS.mm

@@ -0,0 +1,134 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Input/MacOS/KeyboardMacOS.h>
+#import <GameController/GameController.h>
+
+static EKey sToKey(GCKeyCode inValue)
+{
+	if (inValue == GCKeyCodeKeyA) return EKey::A;
+	if (inValue == GCKeyCodeKeyB) return EKey::B;
+	if (inValue == GCKeyCodeKeyC) return EKey::C;
+	if (inValue == GCKeyCodeKeyD) return EKey::D;
+	if (inValue == GCKeyCodeKeyE) return EKey::E;
+	if (inValue == GCKeyCodeKeyF) return EKey::F;
+	if (inValue == GCKeyCodeKeyG) return EKey::G;
+	if (inValue == GCKeyCodeKeyH) return EKey::H;
+	if (inValue == GCKeyCodeKeyI) return EKey::I;
+	if (inValue == GCKeyCodeKeyJ) return EKey::J;
+	if (inValue == GCKeyCodeKeyK) return EKey::K;
+	if (inValue == GCKeyCodeKeyL) return EKey::L;
+	if (inValue == GCKeyCodeKeyM) return EKey::M;
+	if (inValue == GCKeyCodeKeyN) return EKey::N;
+	if (inValue == GCKeyCodeKeyO) return EKey::O;
+	if (inValue == GCKeyCodeKeyP) return EKey::P;
+	if (inValue == GCKeyCodeKeyQ) return EKey::Q;
+	if (inValue == GCKeyCodeKeyR) return EKey::R;
+	if (inValue == GCKeyCodeKeyS) return EKey::S;
+	if (inValue == GCKeyCodeKeyT) return EKey::T;
+	if (inValue == GCKeyCodeKeyU) return EKey::U;
+	if (inValue == GCKeyCodeKeyV) return EKey::V;
+	if (inValue == GCKeyCodeKeyW) return EKey::W;
+	if (inValue == GCKeyCodeKeyX) return EKey::X;
+	if (inValue == GCKeyCodeKeyY) return EKey::Y;
+	if (inValue == GCKeyCodeKeyZ) return EKey::Z;
+	if (inValue == GCKeyCodeZero) return EKey::Num0;
+	if (inValue == GCKeyCodeOne) return EKey::Num1;
+	if (inValue == GCKeyCodeTwo) return EKey::Num2;
+	if (inValue == GCKeyCodeThree) return EKey::Num3;
+	if (inValue == GCKeyCodeFour) return EKey::Num4;
+	if (inValue == GCKeyCodeFive) return EKey::Num5;
+	if (inValue == GCKeyCodeSix) return EKey::Num6;
+	if (inValue == GCKeyCodeSeven) return EKey::Num7;
+	if (inValue == GCKeyCodeEight) return EKey::Num8;
+	if (inValue == GCKeyCodeNine) return EKey::Num9;
+	if (inValue == GCKeyCodeSpacebar) return EKey::Space;
+	if (inValue == GCKeyCodeComma) return EKey::Comma;
+	if (inValue == GCKeyCodePeriod) return EKey::Period;
+	if (inValue == GCKeyCodeEscape) return EKey::Escape;
+	if (inValue == GCKeyCodeLeftShift) return EKey::LShift;
+	if (inValue == GCKeyCodeRightShift) return EKey::RShift;
+	if (inValue == GCKeyCodeLeftControl) return EKey::LControl;
+	if (inValue == GCKeyCodeRightControl) return EKey::RControl;
+	if (inValue == GCKeyCodeLeftAlt) return EKey::LAlt;
+	if (inValue == GCKeyCodeRightAlt) return EKey::RAlt;
+	if (inValue == GCKeyCodeLeftArrow) return EKey::Left;
+	if (inValue == GCKeyCodeRightArrow) return EKey::Right;
+	if (inValue == GCKeyCodeUpArrow) return EKey::Up;
+	if (inValue == GCKeyCodeDownArrow) return EKey::Down;
+	if (inValue == GCKeyCodeReturnOrEnter) return EKey::Return;
+	return EKey::Unknown;
+}
+
+// This class receives keyboard connect callbacks
+@interface KeyboardDelegate : NSObject
+@end
+
+@implementation KeyboardDelegate
+{
+	KeyboardMacOS *mKeyboard;
+}
+
+- (KeyboardDelegate *)init:(KeyboardMacOS *)Keyboard
+{
+	mKeyboard = Keyboard;
+	return self;
+}
+
+- (void)keyboardDidConnect:(NSNotification *)notification
+{
+	GCKeyboard *keyboard = (GCKeyboard *)notification.object;
+	if (!keyboard)
+		return;
+
+	__block KeyboardDelegate *weakSelf = self;
+	keyboard.keyboardInput.keyChangedHandler = ^(GCKeyboardInput *keyboard, GCControllerButtonInput *key, GCKeyCode keyCode, BOOL pressed) {
+		KeyboardDelegate *strongSelf = weakSelf;
+		if (strongSelf == nil)
+			return;
+
+		EKey ekey = sToKey(keyCode);
+		if (ekey != EKey::Invalid)
+			strongSelf->mKeyboard->OnKeyPressed(ekey, pressed);
+	};
+}
+	
+@end
+
+bool KeyboardMacOS::Initialize(ApplicationWindow *inWindow)
+{
+	KeyboardDelegate *delegate = [[KeyboardDelegate alloc] init: this];
+	[NSNotificationCenter.defaultCenter addObserver: delegate selector: @selector(keyboardDidConnect:) name: GCKeyboardDidConnectNotification object: nil];
+	return true;
+}
+
+void KeyboardMacOS::Poll()
+{
+	// Make the pending buffer the active buffer
+	mKeyBuffer = mPendingKeyBuffer;
+	mPendingKeyBuffer.clear();
+}
+
+EKey KeyboardMacOS::GetFirstKey()
+{
+	mCurrentKey = 0;
+	return GetNextKey();
+}
+
+EKey KeyboardMacOS::GetNextKey()
+{
+	if (mCurrentKey < mKeyBuffer.size())
+		return mKeyBuffer[mCurrentKey++];
+	return EKey::Invalid;
+}
+
+void KeyboardMacOS::OnKeyPressed(EKey inKey, bool inPressed)
+{
+	if (inPressed && mPendingKeyBuffer.size() < mPendingKeyBuffer.capacity())
+		mPendingKeyBuffer.push_back(inKey);
+	
+	mKeyPressed[(int)inKey] = inPressed;
+}

+ 54 - 0
TestFramework/Input/MacOS/MouseMacOS.h

@@ -0,0 +1,54 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Input/Mouse.h>
+
+class ApplicationWindowMacOS;
+
+/// Mouse interface class, keeps track of the mouse button state and of the absolute and relative movements of the mouse.
+class MouseMacOS : public Mouse
+{
+public:
+	/// Initialization / shutdown
+	virtual bool					Initialize(ApplicationWindow *inWindow) override;
+	virtual void					Shutdown() override;
+
+	/// Update the mouse state
+	virtual void					Poll() override;
+
+	virtual int						GetX() const override				{ return mX; }
+	virtual int						GetY() const override				{ return mY; }
+	virtual int						GetDX() const override				{ return mDeltaX; }
+	virtual int						GetDY() const override				{ return mDeltaY; }
+
+	virtual bool					IsLeftPressed() const override		{ return mLeftPressed; }
+	virtual bool					IsRightPressed() const override		{ return mRightPressed; }
+	virtual bool					IsMiddlePressed() const override	{ return mMiddlePressed; }
+
+	virtual void					HideCursor() override				{ }
+	virtual void					ShowCursor() override				{ }
+
+	/// Internal callbacks
+	void							OnMouseMoved(int inX, int inY)		{ mX = inX; mY = inY; }
+	void							OnMouseDelta(int inDX, int inDY)	{ mDeltaXAcc += inDX; mDeltaYAcc += inDY; }
+	void							SetLeftPressed(bool inPressed)		{ mLeftPressed = inPressed; }
+	void							SetRightPressed(bool inPressed)		{ mRightPressed = inPressed; }
+	void							SetMiddlePressed(bool inPressed)	{ mMiddlePressed = inPressed; }
+
+private:
+	ApplicationWindowMacOS *		mWindow = nullptr;
+	
+	int								mX = 0;
+	int								mY = 0;
+	int								mDeltaX = 0;
+	int								mDeltaY = 0;
+	int								mDeltaXAcc = 0;
+	int								mDeltaYAcc = 0;
+	
+	bool							mLeftPressed = false;
+	bool							mRightPressed = false;
+	bool							mMiddlePressed = false;
+};

+ 103 - 0
TestFramework/Input/MacOS/MouseMacOS.mm

@@ -0,0 +1,103 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Input/MacOS/MouseMacOS.h>
+#include <Window/ApplicationWindowMacOS.h>
+
+#import <Cocoa/Cocoa.h>
+#import <GameController/GameController.h>
+
+// This class receives mouse connect callbacks
+@interface MouseDelegate : NSObject
+@end
+
+@implementation MouseDelegate
+{
+	MouseMacOS *mMouse;
+}
+
+- (MouseDelegate *)init:(MouseMacOS *)mouse
+{
+	mMouse = mouse;
+	return self;
+}
+
+- (void)mouseDidConnect:(NSNotification *)notification
+{
+	GCMouse *mouse = (GCMouse *)notification.object;
+	if (mouse == nil)
+		return;
+
+	GCMouseInput *mouseInput = mouse.mouseInput;
+	if (mouseInput == nil)
+		return;
+
+	__block MouseDelegate *weakSelf = self;
+	mouseInput.mouseMovedHandler = ^(GCMouseInput *mouse, float deltaX, float deltaY) {
+		MouseDelegate *strongSelf = weakSelf;
+		if (strongSelf == nil)
+			return;
+
+		strongSelf->mMouse->OnMouseDelta(deltaX, -deltaY);
+	};
+
+	mouseInput.leftButton.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
+		MouseDelegate *strongSelf = weakSelf;
+		if (strongSelf == nil)
+			return;
+
+		strongSelf->mMouse->SetLeftPressed(pressed);
+	};
+
+	mouseInput.rightButton.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
+		MouseDelegate *strongSelf = weakSelf;
+		if (strongSelf == nil)
+			return;
+
+		strongSelf->mMouse->SetRightPressed(pressed);
+	};
+
+	mouseInput.middleButton.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
+		MouseDelegate *strongSelf = weakSelf;
+		if (strongSelf == nil)
+			return;
+
+		strongSelf->mMouse->SetMiddlePressed(pressed);
+	};
+}
+	
+@end
+
+bool MouseMacOS::Initialize(ApplicationWindow *inWindow)
+{
+	mWindow = static_cast<ApplicationWindowMacOS *>(inWindow);
+
+	// Install listener for mouse move callbacks
+	mWindow->SetMouseMovedCallback([this](int inX, int inY) { OnMouseMoved(inX, inY); });
+	
+	// Install listener for mouse delta callbacks (will work also when mouse is outside the window or at the edge of the screen)
+	MouseDelegate *delegate = [[MouseDelegate alloc] init: this];
+	[NSNotificationCenter.defaultCenter addObserver: delegate selector: @selector(mouseDidConnect:) name: GCMouseDidConnectNotification object:nil];
+	return true;
+}
+
+void MouseMacOS::Shutdown()
+{
+	if (mWindow != nullptr)
+	{
+		mWindow->SetMouseMovedCallback({});
+		mWindow = nullptr;
+	}
+}
+
+void MouseMacOS::Poll()
+{
+	mDeltaX = mDeltaXAcc;
+	mDeltaY = mDeltaYAcc;
+	
+	mDeltaXAcc = 0;
+	mDeltaYAcc = 0;
+}

+ 2 - 2
TestFramework/Input/Mouse.h

@@ -4,7 +4,7 @@
 
 #pragma once
 
-class Renderer;
+class ApplicationWindow;
 
 /// Mouse interface class, keeps track of the mouse button state and of the absolute and relative movements of the mouse.
 class Mouse
@@ -15,7 +15,7 @@ public:
 	virtual							~Mouse() = default;
 
 	/// Initialization / shutdown
-	virtual bool					Initialize(Renderer *inWindow) = 0;
+	virtual bool					Initialize(ApplicationWindow *inWindow) = 0;
 	virtual void					Shutdown() = 0;
 
 	/// Update the mouse state

+ 3 - 2
TestFramework/Input/Win/KeyboardWin.cpp

@@ -6,6 +6,7 @@
 
 #include <Input/Win/KeyboardWin.h>
 #include <Renderer/Renderer.h>
+#include <Window/ApplicationWindowWin.h>
 #include <Jolt/Core/Profiler.h>
 
 KeyboardWin::KeyboardWin()
@@ -34,7 +35,7 @@ void KeyboardWin::ResetKeyboard()
 	mCurrentPosition = 0;
 }
 
-bool KeyboardWin::Initialize(Renderer *inRenderer)
+bool KeyboardWin::Initialize(ApplicationWindow *inWindow)
 #ifdef JPH_COMPILER_CLANG
 	// DIPROP_BUFFERSIZE is a pointer to 1 which causes UBSan: runtime error: reference binding to misaligned address 0x000000000001
 	__attribute__((no_sanitize("alignment")))
@@ -62,7 +63,7 @@ bool KeyboardWin::Initialize(Renderer *inRenderer)
 	}
 
 	// Set cooperative level for keyboard
-	if (FAILED(mKeyboard->SetCooperativeLevel(inRenderer->GetWindowHandle(), DISCL_NONEXCLUSIVE | DISCL_FOREGROUND)))
+	if (FAILED(mKeyboard->SetCooperativeLevel(static_cast<ApplicationWindowWin *>(inWindow)->GetWindowHandle(), DISCL_NONEXCLUSIVE | DISCL_FOREGROUND)))
 	{
 		Trace("Unable to set cooperative level for keyboard");
 		return false;

+ 1 - 1
TestFramework/Input/Win/KeyboardWin.h

@@ -19,7 +19,7 @@ public:
 	virtual							~KeyboardWin() override;
 
 	/// Initialization / shutdown
-	virtual bool					Initialize(Renderer *inRenderer) override;
+	virtual bool					Initialize(ApplicationWindow *inWindow) override;
 	virtual void					Shutdown() override;
 
 	/// Update the keyboard state

+ 6 - 6
TestFramework/Input/Win/MouseWin.cpp

@@ -5,7 +5,7 @@
 #include <TestFramework.h>
 
 #include <Input/Win/MouseWin.h>
-#include <Renderer/Renderer.h>
+#include <Window/ApplicationWindowWin.h>
 #include <Jolt/Core/Profiler.h>
 
 MouseWin::MouseWin()
@@ -53,14 +53,14 @@ void MouseWin::DetectParsecRunning()
 	}
 }
 
-bool MouseWin::Initialize(Renderer *inRenderer)
+bool MouseWin::Initialize(ApplicationWindow *inWindow)
 #ifdef JPH_COMPILER_CLANG
 	// DIPROP_BUFFERSIZE is a pointer to 1 which causes UBSan: runtime error: reference binding to misaligned address 0x000000000001
 	__attribute__((no_sanitize("alignment")))
 #endif
 {
-	// Store renderer
-	mRenderer = inRenderer;
+	// Store window
+	mWindow = static_cast<ApplicationWindowWin *>(inWindow);
 
 	// Create direct input interface
 	if (FAILED(CoCreateInstance(CLSID_DirectInput8, nullptr, CLSCTX_INPROC_SERVER, IID_IDirectInput8W, (void **)&mDI)))
@@ -84,7 +84,7 @@ bool MouseWin::Initialize(Renderer *inRenderer)
 	}
 
 	// Set cooperative level for Mouse
-	if (FAILED(mMouse->SetCooperativeLevel(mRenderer->GetWindowHandle(), DISCL_NONEXCLUSIVE | DISCL_FOREGROUND)))
+	if (FAILED(mMouse->SetCooperativeLevel(mWindow->GetWindowHandle(), DISCL_NONEXCLUSIVE | DISCL_FOREGROUND)))
 		Trace("Failed to set cooperative level for mouse");
 
 	// Set data format
@@ -148,7 +148,7 @@ void MouseWin::Poll()
 	}
 
 	// Convert to window space
-	if (!ScreenToClient(mRenderer->GetWindowHandle(), &mMousePos))
+	if (!ScreenToClient(mWindow->GetWindowHandle(), &mMousePos))
 	{
 		ResetMouse();
 		return;

+ 4 - 2
TestFramework/Input/Win/MouseWin.h

@@ -10,6 +10,8 @@
 #define DIRECTINPUT_VERSION	0x0800
 #include <dinput.h>
 
+class ApplicationWindowWin;
+
 /// Mouse interface class, keeps track of the mouse button state and of the absolute and relative movements of the mouse.
 class MouseWin : public Mouse
 {
@@ -19,7 +21,7 @@ public:
 	virtual							~MouseWin() override;
 
 	/// Initialization / shutdown
-	virtual bool					Initialize(Renderer *inWindow) override;
+	virtual bool					Initialize(ApplicationWindow *inWindow) override;
 	virtual void					Shutdown() override;
 
 	/// Update the mouse state
@@ -47,7 +49,7 @@ private:
 		BUFFERSIZE					= 64,								///< Number of keys cached
 	};
 
-	Renderer *						mRenderer;
+	ApplicationWindowWin *			mWindow;
 	ComPtr<IDirectInput8>			mDI;
 	ComPtr<IDirectInputDevice8>		mMouse;
 	bool							mIsParsecRunning;					///< If the Parsec remote desktop solution is running, if so we can't trust the mouse movement information from DX and it will make the mouse too sensitive

+ 12 - 13
TestFramework/Renderer/DX12/RendererDX12.cpp

@@ -12,6 +12,7 @@
 #include <Renderer/DX12/TextureDX12.h>
 #include <Renderer/DX12/RenderInstancesDX12.h>
 #include <Renderer/DX12/FatalErrorIfFailedDX12.h>
+#include <Window/ApplicationWindowWin.h>
 #include <Jolt/Core/Profiler.h>
 #include <Utils/ReadData.h>
 #include <Utils/Log.h>
@@ -92,8 +93,8 @@ void RendererDX12::CreateDepthBuffer()
 	D3D12_RESOURCE_DESC depth_stencil_desc = {};
 	depth_stencil_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
 	depth_stencil_desc.Alignment = 0;
-	depth_stencil_desc.Width = mWindowWidth;
-	depth_stencil_desc.Height = mWindowHeight;
+	depth_stencil_desc.Width = mWindow->GetWindowWidth();
+	depth_stencil_desc.Height = mWindow->GetWindowHeight();
 	depth_stencil_desc.DepthOrArraySize = 1;
 	depth_stencil_desc.MipLevels = 1;
 	depth_stencil_desc.Format = DXGI_FORMAT_D32_FLOAT;
@@ -114,9 +115,9 @@ void RendererDX12::CreateDepthBuffer()
 	mDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), &depth_stencil_view_desc, mDepthStencilView);
 }
 
-void RendererDX12::Initialize()
+void RendererDX12::Initialize(ApplicationWindow *inWindow)
 {
-	Renderer::Initialize();
+	Renderer::Initialize(inWindow);
 
 #if defined(JPH_DEBUG)
 	// Enable the D3D12 debug layer
@@ -197,7 +198,7 @@ void RendererDX12::Initialize()
 #endif // JPH_DEBUG
 
 	// Disable full screen transitions
-	FatalErrorIfFailed(mDXGIFactory->MakeWindowAssociation(mhWnd, DXGI_MWA_NO_ALT_ENTER));
+	FatalErrorIfFailed(mDXGIFactory->MakeWindowAssociation(static_cast<ApplicationWindowWin *>(mWindow)->GetWindowHandle(), DXGI_MWA_NO_ALT_ENTER));
 
 	// Create heaps
 	mRTVHeap.Init(mDevice.Get(), D3D12_DESCRIPTOR_HEAP_TYPE_RTV, D3D12_DESCRIPTOR_HEAP_FLAG_NONE, 2);
@@ -217,12 +218,12 @@ void RendererDX12::Initialize()
 	// Describe and create the swap chain
 	DXGI_SWAP_CHAIN_DESC swap_chain_desc = {};
 	swap_chain_desc.BufferCount = cFrameCount;
-	swap_chain_desc.BufferDesc.Width = mWindowWidth;
-	swap_chain_desc.BufferDesc.Height = mWindowHeight;
+	swap_chain_desc.BufferDesc.Width = mWindow->GetWindowWidth();
+	swap_chain_desc.BufferDesc.Height = mWindow->GetWindowHeight();
 	swap_chain_desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
 	swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
 	swap_chain_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
-	swap_chain_desc.OutputWindow = mhWnd;
+	swap_chain_desc.OutputWindow = static_cast<ApplicationWindowWin *>(mWindow)->GetWindowHandle();
 	swap_chain_desc.SampleDesc.Count = 1;
 	swap_chain_desc.Windowed = TRUE;
 
@@ -333,8 +334,6 @@ void RendererDX12::Initialize()
 
 void RendererDX12::OnWindowResize()
 {
-	Renderer::OnWindowResize();
-
 	// Wait for the previous frame to be rendered
 	WaitForGpu();
 
@@ -346,7 +345,7 @@ void RendererDX12::OnWindowResize()
 	}
 
 	// Resize the swap chain buffers
-	FatalErrorIfFailed(mSwapChain->ResizeBuffers(cFrameCount, mWindowWidth, mWindowHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0));
+	FatalErrorIfFailed(mSwapChain->ResizeBuffers(cFrameCount, mWindow->GetWindowWidth(), mWindow->GetWindowHeight(), DXGI_FORMAT_R8G8B8A8_UNORM, 0));
 
 	// Back buffer index may have changed after the resize (it always seems to go to 0 again)
 	mFrameIndex = mSwapChain->GetCurrentBackBufferIndex();
@@ -433,11 +432,11 @@ void RendererDX12::EndShadowPass()
 	mCommandList->OMSetRenderTargets(1, &mRenderTargetViews[mFrameIndex], FALSE, &mDepthStencilView);
 
 	// Set viewport
-	D3D12_VIEWPORT viewport = { 0.0f, 0.0f, static_cast<float>(mWindowWidth), static_cast<float>(mWindowHeight), 0.0f, 1.0f };
+	D3D12_VIEWPORT viewport = { 0.0f, 0.0f, static_cast<float>(mWindow->GetWindowWidth()), static_cast<float>(mWindow->GetWindowHeight()), 0.0f, 1.0f };
 	mCommandList->RSSetViewports(1, &viewport);
 
 	// Set scissor rect
-	D3D12_RECT scissor_rect = { 0, 0, static_cast<LONG>(mWindowWidth), static_cast<LONG>(mWindowHeight) };
+	D3D12_RECT scissor_rect = { 0, 0, static_cast<LONG>(mWindow->GetWindowWidth()), static_cast<LONG>(mWindow->GetWindowHeight()) };
 	mCommandList->RSSetScissorRects(1, &scissor_rect);
 }
 

+ 1 - 1
TestFramework/Renderer/DX12/RendererDX12.h

@@ -19,7 +19,7 @@ public:
 	virtual							~RendererDX12() override;
 
 	// See: Renderer
-	virtual void					Initialize() override;
+	virtual void					Initialize(ApplicationWindow *inWindow) override;
 	virtual void					BeginFrame(const CameraState &inCamera, float inWorldScale) override;
 	virtual void					EndShadowPass() override;
 	virtual void					EndFrame() override;

+ 9 - 163
TestFramework/Renderer/Renderer.cpp

@@ -5,172 +5,18 @@
 #include <TestFramework.h>
 
 #include <Renderer/Renderer.h>
-#include <Utils/Log.h>
 
-static Renderer *sRenderer = nullptr;
-
-#ifdef JPH_PLATFORM_WINDOWS
-
-#include <shellscalingapi.h>
-
-//--------------------------------------------------------------------------------------
-// Called every time the application receives a message
-//--------------------------------------------------------------------------------------
-static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+Renderer::~Renderer()
 {
-	PAINTSTRUCT ps;
-
-	switch (message)
-	{
-	case WM_PAINT:
-		BeginPaint(hWnd, &ps);
-		EndPaint(hWnd, &ps);
-		break;
-
-	case WM_SIZE:
-		if (sRenderer != nullptr)
-			sRenderer->OnWindowResize();
-		break;
-
-	case WM_DESTROY:
-		PostQuitMessage(0);
-		break;
-
-	default:
-		return DefWindowProc(hWnd, message, wParam, lParam);
-	}
-
-	return 0;
+	if (mWindow != nullptr)
+		mWindow->SetWindowResizeListener({});
 }
 
-#endif // JPH_PLATFORM_WINDOWS
-
-void Renderer::Initialize()
+void Renderer::Initialize(ApplicationWindow *inWindow)
 {
-#ifdef JPH_PLATFORM_WINDOWS
-	// Prevent this window from auto scaling
-	SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
-
-	// Register class
-	WNDCLASSEX wcex;
-	wcex.cbSize = sizeof(WNDCLASSEX);
-	wcex.style = CS_HREDRAW | CS_VREDRAW;
-	wcex.lpfnWndProc = WndProc;
-	wcex.cbClsExtra = 0;
-	wcex.cbWndExtra = 0;
-	wcex.hInstance = GetModuleHandle(nullptr);
-	wcex.hIcon = nullptr;
-	wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
-	wcex.hbrBackground = nullptr;
-	wcex.lpszMenuName = nullptr;
-	wcex.lpszClassName = TEXT("TestFrameworkClass");
-	wcex.hIconSm = nullptr;
-	if (!RegisterClassEx(&wcex))
-		FatalError("Failed to register window class");
-
-	// Create window
-	RECT rc = { 0, 0, mWindowWidth, mWindowHeight };
-	AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);
-	mhWnd = CreateWindow(TEXT("TestFrameworkClass"), TEXT("TestFramework"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
-		rc.right - rc.left, rc.bottom - rc.top, nullptr, nullptr, wcex.hInstance, nullptr);
-	if (!mhWnd)
-		FatalError("Failed to create window");
-
-	// Show window
-	ShowWindow(mhWnd, SW_SHOW);
-#elif defined(JPH_PLATFORM_LINUX)
-	// Open connection to X server
-	mDisplay = XOpenDisplay(nullptr);
-	if (!mDisplay)
-		FatalError("Failed to open X display");
-
-	// Create a simple window
-	int screen = DefaultScreen(mDisplay);
-	mWindow = XCreateSimpleWindow(mDisplay, RootWindow(mDisplay, screen), 0, 0, mWindowWidth, mWindowHeight, 1, BlackPixel(mDisplay, screen), WhitePixel(mDisplay, screen));
-
-	// Select input events
-	XSelectInput(mDisplay, mWindow, ExposureMask | StructureNotifyMask | KeyPressMask);
-
-	// Set window title
-	XStoreName(mDisplay, mWindow, "TestFramework");
-
-	// Register WM_DELETE_WINDOW to handle the close button
-	mWmDeleteWindow = XInternAtom(mDisplay, "WM_DELETE_WINDOW", false);
-	XSetWMProtocols(mDisplay, mWindow, &mWmDeleteWindow, 1);
-
-	// Map the window (make it visible)
-	XMapWindow(mDisplay, mWindow);
-
-	// Flush the display to ensure commands are executed
-	XFlush(mDisplay);
-#else
-	#error Unsupported platform
-#endif
-
-	// Store global renderer now that we're done initializing
-	sRenderer = this;
-}
-
-bool Renderer::WindowUpdate()
-{
-#ifdef JPH_PLATFORM_WINDOWS
-	// Main message loop
-	MSG msg = {};
-	while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
-	{
-		TranslateMessage(&msg);
-		DispatchMessage(&msg);
-
-		if (msg.message == WM_QUIT)
-		{
-			// Handle quit events
-			return false;
-		}
-	}
-#elif defined(JPH_PLATFORM_LINUX)
-	while (XPending(mDisplay) > 0)
-	{
-		XEvent event;
-		XNextEvent(mDisplay, &event);
-
-		if (event.type == ClientMessage && static_cast<Atom>(event.xclient.data.l[0]) == mWmDeleteWindow)
-		{
-			// Handle quit events
-			return false;
-		}
-		else if (event.type == ConfigureNotify)
-		{
-			// Handle window resize events
-			XConfigureEvent xce = event.xconfigure;
-			if (xce.width != mWindowWidth || xce.height != mWindowHeight)
-			{
-				mWindowWidth = xce.width;
-				mWindowHeight = xce.height;
-				OnWindowResize();
-			}
-		}
-		else
-			mEventListener(event);
-	}
-#else
-	#error Unsupported platform
-#endif
-
-	// Application should continue
-	return true;
-}
-
-void Renderer::OnWindowResize()
-{
-	JPH_ASSERT(!mInFrame);
-
-#ifdef JPH_PLATFORM_WINDOWS
-	// Get new window size
-	RECT rc;
-	GetClientRect(mhWnd, &rc);
-	mWindowWidth = max<LONG>(rc.right - rc.left, 8);
-	mWindowHeight = max<LONG>(rc.bottom - rc.top, 8);
-#endif
+	// Store window
+	mWindow = inWindow;
+	mWindow->SetWindowResizeListener([this]() { OnWindowResize(); });
 }
 
 static Mat44 sPerspectiveInfiniteReverseZ(float inFovY, float inAspect, float inNear, float inYSign)
@@ -201,7 +47,7 @@ void Renderer::BeginFrame(const CameraState &inCamera, float inWorldScale)
 	// Camera properties
 	Vec3 cam_pos = Vec3(inCamera.mPos - mBaseOffset);
 	float camera_fovy = inCamera.mFOVY;
-	float camera_aspect = static_cast<float>(GetWindowWidth()) / GetWindowHeight();
+	float camera_aspect = static_cast<float>(mWindow->GetWindowWidth()) / mWindow->GetWindowHeight();
 	float camera_fovx = 2.0f * ATan(camera_aspect * Tan(0.5f * camera_fovy));
 	float camera_near = 0.01f * inWorldScale;
 
@@ -221,7 +67,7 @@ void Renderer::BeginFrame(const CameraState &inCamera, float inWorldScale)
 	mVSBuffer.mLightView = Mat44::sLookAt(light_pos, light_tgt, light_up);
 
 	// Camera ortho projection and view
-	mVSBufferOrtho.mProjection = Mat44(Vec4(2.0f / mWindowWidth, 0.0f, 0.0f, 0.0f), Vec4(0.0f, -mPerspectiveYSign * 2.0f / mWindowHeight, 0.0f, 0.0f), Vec4(0.0f, 0.0f, -1.0f, 0.0f), Vec4(-1.0f, mPerspectiveYSign * 1.0f, 0.0f, 1.0f));
+	mVSBufferOrtho.mProjection = Mat44(Vec4(2.0f / mWindow->GetWindowWidth(), 0.0f, 0.0f, 0.0f), Vec4(0.0f, -mPerspectiveYSign * 2.0f / mWindow->GetWindowHeight(), 0.0f, 0.0f), Vec4(0.0f, 0.0f, -1.0f, 0.0f), Vec4(-1.0f, mPerspectiveYSign * 1.0f, 0.0f, 1.0f));
 	mVSBufferOrtho.mView = Mat44::sIdentity();
 
 	// Light projection and view are unused in ortho mode

+ 9 - 33
TestFramework/Renderer/Renderer.h

@@ -5,6 +5,7 @@
 #pragma once
 
 #include <Image/Surface.h>
+#include <Window/ApplicationWindow.h>
 #include <Renderer/Frustum.h>
 #include <Renderer/PipelineState.h>
 #include <Renderer/VertexShader.h>
@@ -12,7 +13,6 @@
 #include <Renderer/RenderPrimitive.h>
 #include <Renderer/RenderInstances.h>
 #include <memory>
-#include <functional>
 
 // Forward declares
 class Texture;
@@ -33,28 +33,10 @@ class Renderer
 {
 public:
 	/// Destructor
-	virtual							~Renderer() = default;
+	virtual							~Renderer();
 
-	/// Initialize DirectX
-	virtual void					Initialize();
-
-	/// Get window size
-	int								GetWindowWidth()					{ return mWindowWidth; }
-	int								GetWindowHeight()					{ return mWindowHeight; }
-
-#ifdef JPH_PLATFORM_WINDOWS
-	/// Access to the window handle
-	HWND							GetWindowHandle() const				{ return mhWnd; }
-#elif defined(JPH_PLATFORM_LINUX)
-	/// Access to the window handle
-	Display *						GetDisplay() const					{ return mDisplay; }
-	Window							GetWindow() const					{ return mWindow; }
-	using EventListener = std::function<void(const XEvent &)>;
-	void							SetEventListener(const EventListener &inListener) { mEventListener = inListener; }
-#endif // JPH_PLATFORM_WINDOWS
-
-	/// Update the system window, returns false if the application should quit
-	bool							WindowUpdate();
+	/// Initialize renderer
+	virtual void					Initialize(ApplicationWindow *inWindow);
 
 	/// Start / end drawing a frame
 	virtual void					BeginFrame(const CameraState &inCamera, float inWorldScale);
@@ -106,8 +88,11 @@ public:
 	/// Which frame is currently rendering (to keep track of which buffers are free to overwrite)
 	uint							GetCurrentFrameIndex() const		{ JPH_ASSERT(mInFrame); return mFrameIndex; }
 
+	/// Get the window we're rendering to
+	ApplicationWindow *				GetWindow() const					{ return mWindow; }
+
 	/// Callback when the window resizes and the back buffer needs to be adjusted
-	virtual void					OnWindowResize();
+	virtual void					OnWindowResize() = 0;
 
 protected:
 	struct VertexShaderConstantBuffer
@@ -124,16 +109,7 @@ protected:
 		Vec4						mLightPos;
 	};
 
-#ifdef JPH_PLATFORM_WINDOWS
-	HWND							mhWnd;
-#elif defined(JPH_PLATFORM_LINUX)
-	Display *						mDisplay;
-	Window							mWindow;
-	Atom							mWmDeleteWindow;
-	EventListener					mEventListener;
-#endif
-	int								mWindowWidth = 1920;
-	int								mWindowHeight = 1080;
+	ApplicationWindow *				mWindow = nullptr;					///< The window we're rendering to
 	float							mPerspectiveYSign = 1.0f;			///< Sign for the Y coordinate in the projection matrix (1 for DX, -1 for Vulkan)
 	bool							mInFrame = false;					///< If we're within a BeginFrame() / EndFrame() pair
 	CameraState						mCameraState;

+ 27 - 11
TestFramework/Renderer/VK/RendererVK.cpp

@@ -20,8 +20,13 @@
 
 #ifdef JPH_PLATFORM_WINDOWS
 	#include <vulkan/vulkan_win32.h>
+	#include <Window/ApplicationWindowWin.h>
 #elif defined(JPH_PLATFORM_LINUX)
 	#include <vulkan/vulkan_xlib.h>
+	#include <Window/ApplicationWindowLinux.h>
+#elif defined(JPH_PLATFORM_MACOS)
+	#include <vulkan/vulkan_metal.h>
+	#include <Window/ApplicationWindowMacOS.h>
 #endif
 
 #ifdef JPH_DEBUG
@@ -107,9 +112,9 @@ RendererVK::~RendererVK()
 	 vkDestroyInstance(mInstance, nullptr);
 }
 
-void RendererVK::Initialize()
+void RendererVK::Initialize(ApplicationWindow *inWindow)
 {
-	Renderer::Initialize();
+	Renderer::Initialize(inWindow);
 
 	// Flip the sign of the projection matrix
 	mPerspectiveYSign = -1.0f;
@@ -121,11 +126,18 @@ void RendererVK::Initialize()
 	required_instance_extensions.push_back(VK_KHR_WIN32_SURFACE_EXTENSION_NAME);
 #elif defined(JPH_PLATFORM_LINUX)
 	required_instance_extensions.push_back(VK_KHR_XLIB_SURFACE_EXTENSION_NAME);
+#elif defined(JPH_PLATFORM_MACOS)
+	required_instance_extensions.push_back(VK_EXT_METAL_SURFACE_EXTENSION_NAME);
+	required_instance_extensions.push_back("VK_KHR_portability_enumeration");
+	required_instance_extensions.push_back("VK_KHR_get_physical_device_properties2");
 #endif
 
 	// Required device extensions
 	Array<const char *> required_device_extensions;
 	required_device_extensions.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
+#ifdef JPH_PLATFORM_MACOS
+	required_device_extensions.push_back("VK_KHR_portability_subset"); // VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME
+#endif
 
 	// Query supported instance extensions
 	uint32 instance_extension_count = 0;
@@ -143,6 +155,9 @@ void RendererVK::Initialize()
 	// Create Vulkan instance
 	VkInstanceCreateInfo instance_create_info = {};
 	instance_create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
+#ifdef JPH_PLATFORM_MACOS
+	instance_create_info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
+#endif
 
 #ifdef JPH_DEBUG
 	// Enable validation layer if supported
@@ -185,15 +200,21 @@ void RendererVK::Initialize()
 #ifdef JPH_PLATFORM_WINDOWS
 	VkWin32SurfaceCreateInfoKHR surface_create_info = {};
 	surface_create_info.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
-	surface_create_info.hwnd = mhWnd;
+	surface_create_info.hwnd = static_cast<ApplicationWindowWin *>(mWindow)->GetWindowHandle();
 	surface_create_info.hinstance = GetModuleHandle(nullptr);
 	FatalErrorIfFailed(vkCreateWin32SurfaceKHR(mInstance, &surface_create_info, nullptr, &mSurface));
 #elif defined(JPH_PLATFORM_LINUX)
 	VkXlibSurfaceCreateInfoKHR surface_create_info = {};
 	surface_create_info.sType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR;
-	surface_create_info.dpy = mDisplay;
-	surface_create_info.window = mWindow;
+	surface_create_info.dpy = static_cast<ApplicationWindowLinux *>(mWindow)->GetDisplay();
+	surface_create_info.window = static_cast<ApplicationWindowLinux *>(mWindow)->GetWindow();
 	FatalErrorIfFailed(vkCreateXlibSurfaceKHR(mInstance, &surface_create_info, nullptr, &mSurface));
+#elif defined(JPH_PLATFORM_MACOS)
+	VkMetalSurfaceCreateInfoEXT surface_create_info = {};
+	surface_create_info.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT;
+	surface_create_info.pNext = nullptr;
+	surface_create_info.pLayer = static_cast<ApplicationWindowMacOS *>(mWindow)->GetMetalLayer();
+	FatalErrorIfFailed(vkCreateMetalSurfaceEXT(mInstance, &surface_create_info, nullptr, &mSurface));
 #endif
 
 	// Select device
@@ -486,7 +507,6 @@ void RendererVK::Initialize()
 	sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
 	sampler_info.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
 	sampler_info.unnormalizedCoordinates = VK_FALSE;
-	sampler_info.compareEnable = VK_FALSE;
 	sampler_info.minLod = 0.0f;
 	sampler_info.maxLod = VK_LOD_CLAMP_NONE;
 	sampler_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
@@ -496,8 +516,6 @@ void RendererVK::Initialize()
 	sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
 	sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
 	sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
-	sampler_info.compareEnable = VK_TRUE;
-	sampler_info.compareOp = VK_COMPARE_OP_GREATER_OR_EQUAL;
 	FatalErrorIfFailed(vkCreateSampler(mDevice, &sampler_info, nullptr, &mTextureSamplerShadow));
 
 	{
@@ -647,7 +665,7 @@ void RendererVK::CreateSwapChain(VkPhysicalDevice inDevice)
 	vkGetPhysicalDeviceSurfaceCapabilitiesKHR(inDevice, mSurface, &capabilities);
 	mSwapChainExtent = capabilities.currentExtent;
 	if (mSwapChainExtent.width == UINT32_MAX || mSwapChainExtent.height == UINT32_MAX)
-		mSwapChainExtent = { uint32(mWindowWidth), uint32(mWindowHeight) };
+		mSwapChainExtent = { uint32(mWindow->GetWindowWidth()), uint32(mWindow->GetWindowHeight()) };
 	mSwapChainExtent.width = Clamp(mSwapChainExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width);
 	mSwapChainExtent.height = Clamp(mSwapChainExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height);
 
@@ -765,8 +783,6 @@ void RendererVK::DestroySwapChain()
 
 void RendererVK::OnWindowResize()
 {
-	Renderer::OnWindowResize();
-
 	vkDeviceWaitIdle(mDevice);
 	DestroySwapChain();
 	CreateSwapChain(mPhysicalDevice);

+ 1 - 1
TestFramework/Renderer/VK/RendererVK.h

@@ -19,7 +19,7 @@ public:
 	virtual							~RendererVK() override;
 
 	// See: Renderer
-	virtual void					Initialize() override;
+	virtual void					Initialize(ApplicationWindow *inWindow) override;
 	virtual void					BeginFrame(const CameraState &inCamera, float inWorldScale) override;
 	virtual void					EndShadowPass() override;
 	virtual void					EndFrame() override;

+ 29 - 0
TestFramework/TestFramework.cmake

@@ -79,6 +79,7 @@ if (NOT CROSS_COMPILE_ARM AND (Vulkan_FOUND OR WIN32))
 		${TEST_FRAMEWORK_ROOT}/Utils/Log.h
 		${TEST_FRAMEWORK_ROOT}/Utils/ReadData.cpp
 		${TEST_FRAMEWORK_ROOT}/Utils/ReadData.h
+		${TEST_FRAMEWORK_ROOT}/Window/ApplicationWindow.h
 	)
 
 	if (WIN32)
@@ -107,6 +108,8 @@ if (NOT CROSS_COMPILE_ARM AND (Vulkan_FOUND OR WIN32))
 			${TEST_FRAMEWORK_ROOT}/Renderer/DX12/TextureDX12.cpp
 			${TEST_FRAMEWORK_ROOT}/Renderer/DX12/TextureDX12.h
 			${TEST_FRAMEWORK_ROOT}/Renderer/DX12/VertexShaderDX12.h
+			${TEST_FRAMEWORK_ROOT}/Window/ApplicationWindowWin.cpp
+			${TEST_FRAMEWORK_ROOT}/Window/ApplicationWindowWin.h
 		)
 
 		# All shaders
@@ -146,6 +149,21 @@ if (NOT CROSS_COMPILE_ARM AND (Vulkan_FOUND OR WIN32))
 			${TEST_FRAMEWORK_ROOT}/Input/Linux/KeyboardLinux.h
 			${TEST_FRAMEWORK_ROOT}/Input/Linux/MouseLinux.cpp
 			${TEST_FRAMEWORK_ROOT}/Input/Linux/MouseLinux.h
+			${TEST_FRAMEWORK_ROOT}/Window/ApplicationWindowLinux.cpp
+			${TEST_FRAMEWORK_ROOT}/Window/ApplicationWindowLinux.h
+		)
+	endif()
+		
+	if ("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin")
+		# macOS source files
+		set(TEST_FRAMEWORK_SRC_FILES
+			${TEST_FRAMEWORK_SRC_FILES}
+			${TEST_FRAMEWORK_ROOT}/Input/MacOS/KeyboardMacOS.mm
+			${TEST_FRAMEWORK_ROOT}/Input/MacOS/KeyboardMacOS.h
+			${TEST_FRAMEWORK_ROOT}/Input/MacOS/MouseMacOS.mm
+			${TEST_FRAMEWORK_ROOT}/Input/MacOS/MouseMacOS.h
+			${TEST_FRAMEWORK_ROOT}/Window/ApplicationWindowMacOS.mm
+			${TEST_FRAMEWORK_ROOT}/Window/ApplicationWindowMacOS.h
 		)
 	endif()
 
@@ -233,6 +251,17 @@ if (NOT CROSS_COMPILE_ARM AND (Vulkan_FOUND OR WIN32))
 		# Linux configuration
 		target_link_libraries(TestFramework LINK_PUBLIC Jolt X11)
 	endif()
+	if ("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin")
+		# macOS configuration
+		target_link_libraries(TestFramework LINK_PUBLIC Jolt "-framework Cocoa -framework Metal -framework MetalKit -framework GameController")
+		
+		# Ignore PCH files for .mm files
+		foreach(SRC_FILE ${TEST_FRAMEWORK_SRC_FILES})
+			if (SRC_FILE MATCHES "\.mm")
+				set_source_files_properties(${SRC_FILE} PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
+			endif()
+		endforeach()
+	endif()
 else()
 	# No graphics framework found
 	set(TEST_FRAMEWORK_AVAILABLE FALSE)

+ 4 - 3
TestFramework/UI/UIAnimationSlide.cpp

@@ -5,6 +5,7 @@
 #include <TestFramework.h>
 
 #include <Renderer/Renderer.h>
+#include <Window/ApplicationWindow.h>
 #include <UI/UIAnimationSlide.h>
 #include <UI/UIElement.h>
 #include <UI/UIManager.h>
@@ -28,11 +29,11 @@ void UIAnimationSlide::Init(UIElement *inElement)
 	mTargetRelativeX = inElement->GetRelativeX();
 	mTargetRelativeY = inElement->GetRelativeY();
 
-	Renderer *renderer = inElement->GetManager()->GetRenderer();
+	ApplicationWindow *window = inElement->GetManager()->GetRenderer()->GetWindow();
 	int dl = inElement->GetX();
-	int dr = renderer->GetWindowWidth() - (inElement->GetX() + inElement->GetWidth());
+	int dr = window->GetWindowWidth() - (inElement->GetX() + inElement->GetWidth());
 	int dt = inElement->GetY();
-	int db = renderer->GetWindowHeight() - (inElement->GetY() + inElement->GetHeight());
+	int db = window->GetWindowHeight() - (inElement->GetY() + inElement->GetHeight());
 
 	if (min(dl, dr) < min(dt, db))
 	{

+ 6 - 4
TestFramework/UI/UIManager.cpp

@@ -25,8 +25,9 @@ UIManager::UIManager(Renderer *inRenderer) :
 	mManager = this;
 
 	// Set dimensions of the screen
-	SetWidth(mRenderer->GetWindowWidth());
-	SetHeight(mRenderer->GetWindowHeight());
+	ApplicationWindow *window = mRenderer->GetWindow();
+	SetWidth(window->GetWindowWidth());
+	SetHeight(window->GetWindowHeight());
 
 	// Create input layout
 	const PipelineState::EInputDescription vertex_desc[] =
@@ -153,14 +154,15 @@ void UIManager::GetMaxElementDistanceToScreenEdge(int &outMaxH, int &outMaxV)
 	outMaxH = 0;
 	outMaxV = 0;
 
+	ApplicationWindow *window = mRenderer->GetWindow();
 	for (const UIElement *e : mChildren)
 		if (e->HasDeactivateAnimation())
 		{
 			int dl = e->GetX() + e->GetWidth();
-			int dr = mRenderer->GetWindowWidth() - e->GetX();
+			int dr = window->GetWindowWidth() - e->GetX();
 			outMaxH = max(outMaxH, min(dl, dr));
 			int dt = e->GetY() + e->GetHeight();
-			int db = mRenderer->GetWindowHeight() - e->GetY();
+			int db = window->GetWindowHeight() - e->GetY();
 			outMaxV = max(outMaxV, min(dt, db));
 		}
 }

+ 38 - 0
TestFramework/Window/ApplicationWindow.h

@@ -0,0 +1,38 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <functional>
+
+// Responsible for opening the main window
+class ApplicationWindow
+{
+public:
+	/// Destructor
+	virtual							~ApplicationWindow() = default;
+
+	/// Initialize the window
+	virtual void					Initialize() = 0;
+
+	/// Get window size
+	int								GetWindowWidth()					{ return mWindowWidth; }
+	int								GetWindowHeight()					{ return mWindowHeight; }
+
+	/// Set callback when the window resizes
+	using WindowResizeListener = std::function<void()>;
+	void							SetWindowResizeListener(const WindowResizeListener &inListener) { mWindowResizeListener = inListener; }
+
+	/// Enter the main loop and keep rendering frames until the window is closed
+	using RenderCallback = std::function<bool()>;
+	virtual void					MainLoop(RenderCallback inRenderCallback) = 0;
+	
+	/// Function that will trigger the callback
+	void							OnWindowResized(int inWidth, int inHeight) { mWindowWidth = inWidth; mWindowHeight = inHeight; mWindowResizeListener(); }
+
+protected:
+	int								mWindowWidth = 1920;
+	int								mWindowHeight = 1080;
+	WindowResizeListener			mWindowResizeListener;
+};

+ 67 - 0
TestFramework/Window/ApplicationWindowLinux.cpp

@@ -0,0 +1,67 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Window/ApplicationWindowLinux.h>
+#include <Utils/Log.h>
+
+void ApplicationWindowLinux::Initialize()
+{
+	// Open connection to X server
+	mDisplay = XOpenDisplay(nullptr);
+	if (!mDisplay)
+		FatalError("Failed to open X display");
+
+	// Create a simple window
+	int screen = DefaultScreen(mDisplay);
+	mWindow = XCreateSimpleWindow(mDisplay, RootWindow(mDisplay, screen), 0, 0, mWindowWidth, mWindowHeight, 1, BlackPixel(mDisplay, screen), WhitePixel(mDisplay, screen));
+
+	// Select input events
+	XSelectInput(mDisplay, mWindow, ExposureMask | StructureNotifyMask | KeyPressMask);
+
+	// Set window title
+	XStoreName(mDisplay, mWindow, "TestFramework");
+
+	// Register WM_DELETE_WINDOW to handle the close button
+	mWmDeleteWindow = XInternAtom(mDisplay, "WM_DELETE_WINDOW", false);
+	XSetWMProtocols(mDisplay, mWindow, &mWmDeleteWindow, 1);
+
+	// Map the window (make it visible)
+	XMapWindow(mDisplay, mWindow);
+
+	// Flush the display to ensure commands are executed
+	XFlush(mDisplay);
+}
+
+void ApplicationWindowLinux::MainLoop(RenderCallback inRenderCallback)
+{
+	for (;;)
+	{
+		while (XPending(mDisplay) > 0)
+		{
+			XEvent event;
+			XNextEvent(mDisplay, &event);
+
+			if (event.type == ClientMessage && static_cast<Atom>(event.xclient.data.l[0]) == mWmDeleteWindow)
+			{
+				// Handle quit events
+				return;
+			}
+			else if (event.type == ConfigureNotify)
+			{
+				// Handle window resize events
+				XConfigureEvent xce = event.xconfigure;
+				if (xce.width != mWindowWidth || xce.height != mWindowHeight)
+					OnWindowResized(xce.width, xce.height);
+			}
+			else
+				mEventListener(event);
+		}
+
+		// Call the render callback
+		if (!inRenderCallback())
+			return;
+	}
+}

+ 32 - 0
TestFramework/Window/ApplicationWindowLinux.h

@@ -0,0 +1,32 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Window/ApplicationWindow.h>
+
+// Responsible for opening the main window
+class ApplicationWindowLinux : public ApplicationWindow
+{
+public:
+	/// Initialize the window
+	virtual void					Initialize() override;
+
+	/// Access to the window handle
+	Display *						GetDisplay() const					{ return mDisplay; }
+	Window							GetWindow() const					{ return mWindow; }
+
+	/// Event listener for the keyboard handler
+	using EventListener = std::function<void(const XEvent &)>;
+	void							SetEventListener(const EventListener &inListener) { mEventListener = inListener; }
+
+	/// Enter the main loop and keep rendering frames until the window is closed
+	void							MainLoop(RenderCallback inRenderCallback) override;
+
+protected:
+	Display *						mDisplay;
+	Window							mWindow;
+	Atom							mWmDeleteWindow;
+	EventListener					mEventListener;
+};

+ 41 - 0
TestFramework/Window/ApplicationWindowMacOS.h

@@ -0,0 +1,41 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Window/ApplicationWindow.h>
+
+#ifdef __OBJC__
+@class CAMetalLayer;
+#else
+typedef void CAMetalLayer;
+#endif
+
+// Responsible for opening the main window
+class ApplicationWindowMacOS : public ApplicationWindow
+{
+public:
+	/// Initialize the window
+	virtual void					Initialize() override;
+
+	/// Access to the metal layer
+	CAMetalLayer *					GetMetalLayer() const					{ return mMetalLayer; }
+
+	/// Enter the main loop and keep rendering frames until the window is closed
+	virtual void					MainLoop(RenderCallback inRenderCallback) override;
+	
+	/// Call the render callback
+	bool							RenderCallback()						{ return mRenderCallback(); }
+	
+	/// Subscribe to mouse move callbacks that supply window coordinates
+	using MouseMovedCallback = function<void(int, int)>;
+	void							SetMouseMovedCallback(MouseMovedCallback inCallback) { mMouseMovedCallback = inCallback; }
+	void							OnMouseMoved(int inX, int inY)			{ mMouseMovedCallback(inX, inY); }
+
+protected:
+	CAMetalLayer *					mMetalLayer = nullptr;
+	ApplicationWindow::RenderCallback mRenderCallback;
+	MouseMovedCallback				mMouseMovedCallback;
+};
+	

+ 109 - 0
TestFramework/Window/ApplicationWindowMacOS.mm

@@ -0,0 +1,109 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Window/ApplicationWindowMacOS.h>
+
+#import <MetalKit/MetalKit.h>
+
+// This class implements a metal view
+@interface MetalView : MTKView <MTKViewDelegate>
+@end
+
+@implementation MetalView
+{
+	ApplicationWindowMacOS *mWindow;
+}
+
+- (MetalView *)init:(ApplicationWindowMacOS *)window
+{
+	[super initWithFrame: NSMakeRect(0, 0, window->GetWindowWidth(), window->GetWindowHeight()) device: MTLCreateSystemDefaultDevice()];
+	
+	mWindow = window;
+	
+	self.delegate = self;
+	
+	return self;
+}
+
+- (bool)acceptsFirstResponder
+{
+	return YES;
+}
+
+- (bool)canBecomeKeyView
+{
+	return YES;
+}
+
+- (void)mouseMoved:(NSEvent *)event
+{
+	NSPoint location = [event locationInWindow];
+	mWindow->OnMouseMoved(location.x, mWindow->GetWindowHeight() - location.y);	
+}
+
+- (void)drawInMTKView:(MTKView *)view
+{
+	mWindow->RenderCallback();
+}
+
+- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size
+{
+	mWindow->OnWindowResized(size.width, size.height);
+}
+
+@end
+
+// This is the main application delegate that tells us if we're starting / stopping
+@interface AppDelegate : NSObject <NSApplicationDelegate>
+@end
+
+@implementation AppDelegate
+
+-(void)applicationDidFinishLaunching:(NSNotification *)notification
+{
+	// Add the Quit button to the first menu item on the toolbar
+	NSMenu *app_menu = [[NSApp mainMenu] itemAtIndex: 0].submenu;
+	NSMenuItem *quit_item = [[NSMenuItem alloc] initWithTitle:@"Quit" action:@selector(terminate:) keyEquivalent:@"q"];
+	[app_menu addItem:quit_item];
+}
+
+-(bool)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
+{
+	// Close the app when the window is closed
+	return YES;
+}
+
+@end
+
+void ApplicationWindowMacOS::Initialize()
+{
+	// Create metal view
+	MetalView *view = [[MetalView alloc] init: this];
+	mMetalLayer = (CAMetalLayer *)view.layer;
+
+	// Create window
+	NSWindow *window = [[NSWindow alloc] initWithContentRect: NSMakeRect(0, 0, mWindowWidth, mWindowHeight)
+												   styleMask: NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable
+													 backing: NSBackingStoreBuffered
+													   defer: NO];
+	window.contentView = view;
+	[window setAcceptsMouseMovedEvents: YES];
+	[window setTitle: @"TestFramework"];
+	[window makeKeyAndOrderFront: nil];
+}
+
+void ApplicationWindowMacOS::MainLoop(ApplicationWindow::RenderCallback inRenderCallback)
+{
+	mRenderCallback = inRenderCallback;
+	
+	@autoreleasepool
+	{
+		NSApplication *app = [NSApplication sharedApplication];
+		AppDelegate *delegate = [[AppDelegate alloc] init];
+		[app setDelegate:delegate];
+		[app run];
+	}
+}

+ 107 - 0
TestFramework/Window/ApplicationWindowWin.cpp

@@ -0,0 +1,107 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <Window/ApplicationWindowWin.h>
+#include <Utils/Log.h>
+#include <shellscalingapi.h>
+
+static ApplicationWindowWin *sWindow = nullptr;
+
+// Called every time the application receives a message
+static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+	PAINTSTRUCT ps;
+
+	switch (message)
+	{
+	case WM_PAINT:
+		BeginPaint(hWnd, &ps);
+		EndPaint(hWnd, &ps);
+		break;
+
+	case WM_SIZE:
+		if (sWindow != nullptr)
+		{
+			// Get new window size
+			RECT rc;
+			GetClientRect(hWnd, &rc);
+			int width = max<int>(rc.right - rc.left, 8);
+			int height = max<int>(rc.bottom - rc.top, 8);
+			sWindow->OnWindowResized(width, height);
+		}
+		break;
+
+	case WM_DESTROY:
+		PostQuitMessage(0);
+		break;
+
+	default:
+		return DefWindowProc(hWnd, message, wParam, lParam);
+	}
+
+	return 0;
+}
+
+void ApplicationWindowWin::Initialize()
+{
+	// Prevent this window from auto scaling
+	SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
+
+	// Register class
+	WNDCLASSEX wcex;
+	wcex.cbSize = sizeof(WNDCLASSEX);
+	wcex.style = CS_HREDRAW | CS_VREDRAW;
+	wcex.lpfnWndProc = WndProc;
+	wcex.cbClsExtra = 0;
+	wcex.cbWndExtra = 0;
+	wcex.hInstance = GetModuleHandle(nullptr);
+	wcex.hIcon = nullptr;
+	wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
+	wcex.hbrBackground = nullptr;
+	wcex.lpszMenuName = nullptr;
+	wcex.lpszClassName = TEXT("TestFrameworkClass");
+	wcex.hIconSm = nullptr;
+	if (!RegisterClassEx(&wcex))
+		FatalError("Failed to register window class");
+
+	// Create window
+	RECT rc = { 0, 0, mWindowWidth, mWindowHeight };
+	AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);
+	mhWnd = CreateWindow(TEXT("TestFrameworkClass"), TEXT("TestFramework"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
+		rc.right - rc.left, rc.bottom - rc.top, nullptr, nullptr, wcex.hInstance, nullptr);
+	if (!mhWnd)
+		FatalError("Failed to create window");
+
+	// Show window
+	ShowWindow(mhWnd, SW_SHOW);
+
+	// Store the window pointer for the message loop
+	sWindow = this;
+}
+
+void ApplicationWindowWin::MainLoop(RenderCallback inRenderCallback)
+{
+	for (;;)
+	{
+		// Main message loop
+		MSG msg = {};
+		while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
+		{
+			TranslateMessage(&msg);
+			DispatchMessage(&msg);
+
+			if (msg.message == WM_QUIT)
+			{
+				// Handle quit events
+				return;
+			}
+		}
+
+		// Call the render callback
+		if (!inRenderCallback())
+			return;
+	}
+}

+ 24 - 0
TestFramework/Window/ApplicationWindowWin.h

@@ -0,0 +1,24 @@
+// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
+// SPDX-FileCopyrightText: 2024 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Window/ApplicationWindow.h>
+
+// Responsible for opening the main window
+class ApplicationWindowWin : public ApplicationWindow
+{
+public:
+	/// Initialize the window
+	virtual void					Initialize() override;
+
+	/// Access to the window handle
+	HWND							GetWindowHandle() const				{ return mhWnd; }
+
+	/// Enter the main loop and keep rendering frames until the window is closed
+	virtual void					MainLoop(RenderCallback inRenderCallback) override;
+
+protected:
+	HWND							mhWnd;
+};