diff --git a/include/Pomdog/Platform/Cocoa/PomdogMetalViewController.hpp b/include/Pomdog/Platform/Cocoa/PomdogMetalViewController.hpp new file mode 100644 index 000000000..11d887d31 --- /dev/null +++ b/include/Pomdog/Platform/Cocoa/PomdogMetalViewController.hpp @@ -0,0 +1,28 @@ +// Copyright (c) 2013-2017 mogemimi. Distributed under the MIT license. + +#pragma once + +#include "Pomdog/Basic/Export.hpp" +#include "Pomdog/Signals/detail/ForwardDeclarations.hpp" +#include +#include + +#import +#import + +namespace Pomdog { + +class Game; +class GameHost; +class GraphicsDevice; +class GraphicsCommandQueue; + +} // namespace Pomdog + +POMDOG_EXPORT +@interface PomdogMetalViewController : NSViewController + +- (void)loadAssetsPomdog:(std::shared_ptr)gameHost; +- (void)startGame:(std::shared_ptr)game; + +@end diff --git a/src/Platform.Cocoa/GameHostMetal.hpp b/src/Platform.Cocoa/GameHostMetal.hpp new file mode 100644 index 000000000..50c7d19af --- /dev/null +++ b/src/Platform.Cocoa/GameHostMetal.hpp @@ -0,0 +1,68 @@ +// Copyright (c) 2013-2017 mogemimi. Distributed under the MIT license. + +#pragma once + +#include "Pomdog/Signals/EventQueue.hpp" +#include "Pomdog/Application/GameHost.hpp" +#include + +#import + +namespace Pomdog { + +class Game; +struct PresentationParameters; + +namespace Detail { +namespace Cocoa { + +class GameWindowCocoa; + +class GameHostMetal final : public GameHost { +public: + GameHostMetal( + MTKView* metalView, + const std::shared_ptr& window, + const std::shared_ptr& eventQueue, + const PresentationParameters& presentationParameters); + + ~GameHostMetal(); + + void InitializeGame( + const std::weak_ptr& game, + const std::function& onCompleted); + + void GameLoop(); + + bool IsMetalSupported() const; + + void Exit() override; + + std::shared_ptr GetWindow() override; + + std::shared_ptr GetClock() override; + + std::shared_ptr GetGraphicsDevice() override; + + std::shared_ptr GetGraphicsCommandQueue() override; + + std::shared_ptr GetAudioEngine() override; + + std::shared_ptr GetAssetManager() override; + + std::shared_ptr GetKeyboard() override; + + std::shared_ptr GetMouse() override; + + SurfaceFormat GetBackBufferSurfaceFormat() const override; + + DepthFormat GetBackBufferDepthStencilFormat() const override; + +private: + class Impl; + std::unique_ptr impl; +}; + +} // namespace Cocoa +} // namespace Detail +} // namespace Pomdog diff --git a/src/Platform.Cocoa/GameHostMetal.mm b/src/Platform.Cocoa/GameHostMetal.mm new file mode 100644 index 000000000..c94d3d30f --- /dev/null +++ b/src/Platform.Cocoa/GameHostMetal.mm @@ -0,0 +1,541 @@ +// Copyright (c) 2013-2017 mogemimi. Distributed under the MIT license. + +#include "GameHostMetal.hpp" +#include "GameWindowCocoa.hpp" +#include "KeyboardCocoa.hpp" +#include "MouseCocoa.hpp" +#include "../RenderSystem/GraphicsCommandQueueImmediate.hpp" +#include "../RenderSystem.Metal/GraphicsContextMetal.hpp" +#include "../RenderSystem.Metal/GraphicsDeviceMetal.hpp" +#include "../Application/SystemEvents.hpp" +#include "Pomdog/Application/Game.hpp" +#include "Pomdog/Application/GameClock.hpp" +#include "Pomdog/Audio/AudioEngine.hpp" +#include "Pomdog/Content/AssetManager.hpp" +#include "Pomdog/Signals/Event.hpp" +#include "Pomdog/Signals/ScopedConnection.hpp" +#include "Pomdog/Graphics/GraphicsCommandQueue.hpp" +#include "Pomdog/Graphics/GraphicsDevice.hpp" +#include "Pomdog/Graphics/PresentationParameters.hpp" +#include "Pomdog/Graphics/Viewport.hpp" +#include "Pomdog/Input/KeyState.hpp" +#include "Pomdog/Logging/Log.hpp" +#include "Pomdog/Utility/Assert.hpp" +#include "Pomdog/Utility/Exception.hpp" +#include "Pomdog/Utility/FileSystem.hpp" +#include "Pomdog/Utility/PathHelper.hpp" +#include "Pomdog/Utility/StringHelper.hpp" +#include +#include +#include +#include + +#import +#import + +using Pomdog::Detail::Metal::GraphicsContextMetal; +using Pomdog::Detail::Metal::GraphicsDeviceMetal; + +namespace Pomdog { +namespace Detail { +namespace Cocoa { +namespace { + +// MARK: This code is dup from 'src/RenderSystem.Metal/RenderTarget2DMetal.mm' +MTLPixelFormat ToMTLPixelFormat(DepthFormat depthFormat) +{ + POMDOG_ASSERT(depthFormat != DepthFormat::None); + POMDOG_ASSERT_MESSAGE(depthFormat != DepthFormat::Depth16, "Not supported"); + + switch (depthFormat) { + case DepthFormat::Depth16: return MTLPixelFormatDepth32Float; + case DepthFormat::Depth32: return MTLPixelFormatDepth32Float; +#if defined(MAC_OS_X_VERSION_10_11) && (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_11) + case DepthFormat::Depth24Stencil8: return MTLPixelFormatDepth24Unorm_Stencil8; +#else + case DepthFormat::Depth24Stencil8: return MTLPixelFormatDepth32Float_Stencil8; +#endif + case DepthFormat::Depth32_Float_Stencil8_Uint: return MTLPixelFormatDepth32Float_Stencil8; + case DepthFormat::None: return MTLPixelFormatInvalid; + } + return MTLPixelFormatDepth32Float; +} + +void SetupMetalView( + MTKView* view, + id device, + const PresentationParameters& presentationParameters) +{ + // Set the view to use the default device + view.device = device; + + // Setup the render target, choose values based on your app + view.sampleCount = presentationParameters.MultiSampleCount; + view.depthStencilPixelFormat = ToMTLPixelFormat(presentationParameters.DepthStencilFormat); +} + +} // unnamed namespace + +// MARK: GameHostMetal::Impl + +class GameHostMetal::Impl final { +public: + Impl( + MTKView* metalView, + const std::shared_ptr& window, + const std::shared_ptr& eventQueue, + const PresentationParameters& presentationParameters); + + ~Impl(); + + void InitializeGame( + const std::weak_ptr& game, + const std::function& onCompleted); + + void GameLoop(); + + bool IsMetalSupported() const; + + void Exit(); + + std::shared_ptr GetWindow(); + + std::shared_ptr GetClock(std::shared_ptr && gameHost); + + std::shared_ptr GetGraphicsDevice(); + + std::shared_ptr GetGraphicsCommandQueue(); + + std::shared_ptr GetAssetManager(std::shared_ptr && gameHost); + + std::shared_ptr GetAudioEngine(); + + std::shared_ptr GetKeyboard(); + + std::shared_ptr GetMouse(); + + SurfaceFormat GetBackBufferSurfaceFormat() const noexcept; + + DepthFormat GetBackBufferDepthStencilFormat() const noexcept; + +private: + void RenderFrame(); + + void DoEvents(); + + void ProcessSystemEvents(const Event& event); + + void ClientSizeChanged(); + + void GameWillExit(); + +private: + GameClock clock; + ScopedConnection systemEventConnection; + std::atomic_bool viewLiveResizing; + std::function onCompleted; + + std::weak_ptr weakGame; + std::shared_ptr eventQueue; + std::shared_ptr window; + std::shared_ptr graphicsDevice; + std::shared_ptr graphicsContext; + std::shared_ptr graphicsCommandQueue; + std::shared_ptr audioEngine; + std::unique_ptr assetManager; + std::shared_ptr keyboard; + std::shared_ptr mouse; + + __weak MTKView* metalView; + Duration presentationInterval; + SurfaceFormat backBufferSurfaceFormat; + DepthFormat backBufferDepthStencilFormat; + bool exitRequest; +}; + +GameHostMetal::Impl::Impl( + MTKView* metalViewIn, + const std::shared_ptr& windowIn, + const std::shared_ptr& eventQueueIn, + const PresentationParameters& presentationParameters) + : viewLiveResizing(false) + , eventQueue(eventQueueIn) + , window(windowIn) + , metalView(metalViewIn) + , presentationInterval(Duration(1) / 60) + , backBufferSurfaceFormat(presentationParameters.BackBufferFormat) + , backBufferDepthStencilFormat(presentationParameters.DepthStencilFormat) + , exitRequest(false) +{ + POMDOG_ASSERT(window); + POMDOG_ASSERT(metalView != nil); + + window->SetView(metalView); + + // Create graphics device + graphicsDevice = std::make_shared( + std::make_unique()); + + // Get MTLDevice object + POMDOG_ASSERT(graphicsDevice); + auto graphicsDeviceMetal = dynamic_cast( + graphicsDevice->GetNativeGraphicsDevice()); + + POMDOG_ASSERT(graphicsDeviceMetal); + id metalDevice = graphicsDeviceMetal->GetMTLDevice(); + + if (metalDevice == nil) { + POMDOG_THROW_EXCEPTION(std::runtime_error, + "Error: Metal is not supported on this device"); + } + + // Setup metal view + SetupMetalView(metalView, metalDevice, presentationParameters); + + POMDOG_ASSERT(metalDevice != nil); + + // Create graphics context + graphicsContext = std::make_shared(metalDevice); + graphicsContext->SetMTKView(metalView); + + // Create graphics command queue + graphicsCommandQueue = std::make_shared( + std::make_unique(graphicsContext)); + + // Create subsystems + audioEngine = std::make_shared(); + keyboard = std::make_shared(); + mouse = std::make_shared(); + + // Connect to system event signal + POMDOG_ASSERT(eventQueue); + systemEventConnection = eventQueue->Connect( + [this](const Event& event) { ProcessSystemEvents(event); }); + + Detail::AssetLoaderContext loaderContext; + loaderContext.RootDirectory = PathHelper::Join(FileSystem::GetResourceDirectoryPath(), "Content"); + loaderContext.GraphicsDevice = graphicsDevice; + assetManager = std::make_unique(std::move(loaderContext)); + + POMDOG_ASSERT(presentationParameters.PresentationInterval > 0); + presentationInterval = Duration(1) / presentationParameters.PresentationInterval; +} + +GameHostMetal::Impl::~Impl() +{ + systemEventConnection.Disconnect(); + assetManager.reset(); + keyboard.reset(); + mouse.reset(); + audioEngine.reset(); + graphicsCommandQueue.reset(); + graphicsContext.reset(); + graphicsDevice.reset(); + window.reset(); + eventQueue.reset(); + metalView = nil; +} + +void GameHostMetal::Impl::InitializeGame( + const std::weak_ptr& weakGameIn, + const std::function& onCompletedIn) +{ + POMDOG_ASSERT(!weakGameIn.expired()); + POMDOG_ASSERT(onCompletedIn); + weakGame = weakGameIn; + onCompleted = onCompletedIn; + + POMDOG_ASSERT(!weakGame.expired()); + auto game = weakGame.lock(); + + game->Initialize(); + + if (exitRequest) { + GameWillExit(); + } +} + +bool GameHostMetal::Impl::IsMetalSupported() const +{ + if (!graphicsDevice) { + return false; + } + + // Get MTLDevice object + POMDOG_ASSERT(graphicsDevice); + auto graphicsDeviceMetal = dynamic_cast( + graphicsDevice->GetNativeGraphicsDevice()); + + POMDOG_ASSERT(graphicsDeviceMetal); + id metalDevice = graphicsDeviceMetal->GetMTLDevice(); + + return metalDevice != nil; +} + +void GameHostMetal::Impl::GameWillExit() +{ + if (window) { + window->SetView(nil); + } + + if (onCompleted) { + dispatch_async(dispatch_get_main_queue(), [=] { + onCompleted(); + }); + } +} + +void GameHostMetal::Impl::Exit() +{ + exitRequest = true; + GameWillExit(); +} + +void GameHostMetal::Impl::GameLoop() +{ + POMDOG_ASSERT(graphicsContext != nullptr); + + if (exitRequest) { + return; + } + + graphicsContext->DispatchSemaphoreWait(); + + POMDOG_ASSERT(!exitRequest); + POMDOG_ASSERT(!weakGame.expired()); + + auto game = weakGame.lock(); + POMDOG_ASSERT(game); + + clock.Tick(); + DoEvents(); + + if (exitRequest) { + return; + } + + game->Update(); + + if (!viewLiveResizing.load()) { + RenderFrame(); + } +} + +void GameHostMetal::Impl::RenderFrame() +{ + POMDOG_ASSERT(window); + POMDOG_ASSERT(!weakGame.expired()); + + bool skipRender = (!window || window->IsMinimized() + || [NSApp isHidden] == YES); + + if (skipRender) { + return; + } + + auto game = weakGame.lock(); + + POMDOG_ASSERT(game); + + game->Draw(); +} + +void GameHostMetal::Impl::DoEvents() +{ + eventQueue->Emit(); +} + +void GameHostMetal::Impl::ProcessSystemEvents(const Event& event) +{ + if (event.Is()) + { + Log::Internal("WindowShouldCloseEvent"); + this->Exit(); + } + else if (event.Is()) + { + Log::Internal("WindowWillCloseEvent"); + } + else if (event.Is()) + { + auto rect = window->GetClientBounds(); + Log::Internal(StringHelper::Format( + "ViewWillStartLiveResizeEvent: {w: %d, h: %d}", + rect.Width, rect.Height)); + } + else if (event.Is()) + { + auto rect = window->GetClientBounds(); + Log::Internal(StringHelper::Format( + "ViewDidEndLiveResizeEvent: {w: %d, h: %d}", + rect.Width, rect.Height)); + + ClientSizeChanged(); + } + else { + POMDOG_ASSERT(keyboard); + POMDOG_ASSERT(mouse); + keyboard->HandleEvent(event); + mouse->HandleEvent(event); + } +} + +void GameHostMetal::Impl::ClientSizeChanged() +{ + auto bounds = window->GetClientBounds(); + window->ClientSizeChanged(bounds.Width, bounds.Height); +} + +std::shared_ptr GameHostMetal::Impl::GetWindow() +{ + return window; +} + +std::shared_ptr GameHostMetal::Impl::GetClock(std::shared_ptr && gameHost) +{ + std::shared_ptr sharedClock(gameHost, &clock); + return sharedClock; +} + +std::shared_ptr GameHostMetal::Impl::GetGraphicsDevice() +{ + return graphicsDevice; +} + +std::shared_ptr GameHostMetal::Impl::GetGraphicsCommandQueue() +{ + return graphicsCommandQueue; +} + +std::shared_ptr GameHostMetal::Impl::GetAudioEngine() +{ + return audioEngine; +} + +std::shared_ptr GameHostMetal::Impl::GetAssetManager(std::shared_ptr && gameHost) +{ + std::shared_ptr sharedAssetManager(gameHost, assetManager.get()); + return sharedAssetManager; +} + +std::shared_ptr GameHostMetal::Impl::GetKeyboard() +{ + return keyboard; +} + +std::shared_ptr GameHostMetal::Impl::GetMouse() +{ + return mouse; +} + +SurfaceFormat GameHostMetal::Impl::GetBackBufferSurfaceFormat() const noexcept +{ + return backBufferSurfaceFormat; +} + +DepthFormat GameHostMetal::Impl::GetBackBufferDepthStencilFormat() const noexcept +{ + return backBufferDepthStencilFormat; +} + +// MARK: GameHostMetal + +GameHostMetal::GameHostMetal( + MTKView* metalView, + const std::shared_ptr& window, + const std::shared_ptr& eventQueue, + const PresentationParameters& presentationParameters) + : impl(std::make_unique(metalView, window, eventQueue, presentationParameters)) +{} + +GameHostMetal::~GameHostMetal() = default; + +void GameHostMetal::InitializeGame( + const std::weak_ptr& game, + const std::function& onCompleted) +{ + POMDOG_ASSERT(impl); + impl->InitializeGame(game, onCompleted); +} + +void GameHostMetal::GameLoop() +{ + POMDOG_ASSERT(impl); + impl->GameLoop(); +} + +bool GameHostMetal::IsMetalSupported() const +{ + POMDOG_ASSERT(impl); + return impl->IsMetalSupported(); +} + +void GameHostMetal::Exit() +{ + POMDOG_ASSERT(impl); + impl->Exit(); +} + +std::shared_ptr GameHostMetal::GetWindow() +{ + POMDOG_ASSERT(impl); + return impl->GetWindow(); +} + +std::shared_ptr GameHostMetal::GetClock() +{ + POMDOG_ASSERT(impl); + return impl->GetClock(shared_from_this()); +} + +std::shared_ptr GameHostMetal::GetGraphicsDevice() +{ + POMDOG_ASSERT(impl); + return impl->GetGraphicsDevice(); +} + +std::shared_ptr GameHostMetal::GetGraphicsCommandQueue() +{ + POMDOG_ASSERT(impl); + return impl->GetGraphicsCommandQueue(); +} + +std::shared_ptr GameHostMetal::GetAudioEngine() +{ + POMDOG_ASSERT(impl); + return impl->GetAudioEngine(); +} + +std::shared_ptr GameHostMetal::GetAssetManager() +{ + POMDOG_ASSERT(impl); + return impl->GetAssetManager(shared_from_this()); +} + +std::shared_ptr GameHostMetal::GetKeyboard() +{ + POMDOG_ASSERT(impl); + return impl->GetKeyboard(); +} + +std::shared_ptr GameHostMetal::GetMouse() +{ + POMDOG_ASSERT(impl); + return impl->GetMouse(); +} + +SurfaceFormat GameHostMetal::GetBackBufferSurfaceFormat() const +{ + POMDOG_ASSERT(impl); + return impl->GetBackBufferSurfaceFormat(); +} + +DepthFormat GameHostMetal::GetBackBufferDepthStencilFormat() const +{ + POMDOG_ASSERT(impl); + return impl->GetBackBufferDepthStencilFormat(); +} + +} // namespace Cocoa +} // namespace Detail +} // namespace Pomdog diff --git a/src/Platform.Cocoa/PomdogMetalViewController.mm b/src/Platform.Cocoa/PomdogMetalViewController.mm new file mode 100644 index 000000000..7674fc122 --- /dev/null +++ b/src/Platform.Cocoa/PomdogMetalViewController.mm @@ -0,0 +1,117 @@ +// Copyright (c) 2013-2017 mogemimi. Distributed under the MIT license. + +#import "Pomdog/Platform/Cocoa/PomdogMetalViewController.hpp" + +#include "GameHostMetal.hpp" +#include "GameWindowCocoa.hpp" +#include "../Application/SystemEvents.hpp" +#include "../RenderSystem/GraphicsCommandQueueImmediate.hpp" +#include "../RenderSystem.Metal/GraphicsContextMetal.hpp" +#include "../RenderSystem.Metal/GraphicsDeviceMetal.hpp" +#include "Pomdog/Graphics/GraphicsCommandQueue.hpp" +#include "Pomdog/Graphics/GraphicsDevice.hpp" +#include "Pomdog/Graphics/PresentationParameters.hpp" + +#import +#import + +using Pomdog::SurfaceFormat; +using Pomdog::DepthFormat; +using Pomdog::PresentationParameters; +using Pomdog::GraphicsCommandQueue; +using Pomdog::GraphicsDevice; +using Pomdog::EventQueue; +using Pomdog::Detail::Cocoa::GameHostMetal; +using Pomdog::Detail::Cocoa::GameWindowCocoa; + +@implementation PomdogMetalViewController +{ + std::shared_ptr gameHost; + std::shared_ptr eventQueue; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + ///@todo MSAA is not implemented yet + constexpr int multiSampleCount = 1; + + // Setup presentation parameters + PresentationParameters presentationParameters; + presentationParameters.BackBufferWidth = self.view.bounds.size.width; + presentationParameters.BackBufferHeight = self.view.bounds.size.height; + presentationParameters.PresentationInterval = 60; + presentationParameters.MultiSampleCount = multiSampleCount; + presentationParameters.BackBufferFormat = SurfaceFormat::B8G8R8A8_UNorm; + presentationParameters.DepthStencilFormat = DepthFormat::Depth32_Float_Stencil8_Uint; + presentationParameters.IsFullScreen = false; + + [self _setupMetal:presentationParameters]; + + if (gameHost->IsMetalSupported()) { + MTKView* metalView = static_cast(self.view); + metalView.delegate = self; + [self loadAssetsPomdog:gameHost]; + } + else { + // Fallback to a blank NSView, an application could also fallback to OpenGL here. + NSLog(@"Metal is not supported on this device"); + self.view = [[NSView alloc] initWithFrame:self.view.frame]; + } +} + +- (void)_setupMetal:(const PresentationParameters&)presentationParameters +{ + MTKView* metalView = static_cast(self.view); + NSWindow* nativeWindow = [metalView window]; + POMDOG_ASSERT(nativeWindow != nil); + + eventQueue = std::make_shared(); + auto gameWindow = std::make_shared(nativeWindow, eventQueue); + + gameHost = std::make_shared( + metalView, gameWindow, eventQueue, presentationParameters); +} + +- (void)loadAssetsPomdog:(std::shared_ptr)gameHost +{ +} + +- (void)startGame:(std::shared_ptr)game +{ + std::function onCompleted = [=] { + [[self.view window] close]; + + // Shutdown your application + [NSApp terminate:nil]; + }; + + gameHost->InitializeGame(game, std::move(onCompleted)); +} + +- (void)_render +{ + gameHost->GameLoop(); +} + +- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size +{ +// using Pomdog::Detail::ViewWillStartLiveResizeEvent; + using Pomdog::Detail::ViewDidEndLiveResizeEvent; +// if (eventQueue) { +// eventQueue->Enqueue(); +// } + if (eventQueue) { + eventQueue->Enqueue(); + } +} + +- (void)drawInMTKView:(nonnull MTKView *)view +{ + @autoreleasepool { + [self _render]; + } +} + +@end