Metal-cppとGLFWの開発環境をセットアップする

# はじめに
OpenGLという有名なグラフィックスライブラリがありますが、かなり前からAppleの端末では非推奨のライブラリとなっています。
では何を使えば良いのかというと、Apple製のグラフィックスライブラリであるMetalを使うのが推奨されているようです。
このライブラリは長らく公式ではObjCかSwiftを使って書くのが唯一の方法で、他の言語向けのバインディングなどは提供されていませんでした。
なのですが、いつの間にか公式から[Metal-cpp](https://developer.apple.com/jp/metal/cpp/)というC++バインディングが提供されていました。
これとGLFWを使って基本的な描画処理を実装します。
参考：[glfw-metal-example.m](https://gist.github.com/gcatlin/987be74e2d58da96093a7598f3fbfb27)


# 開発環境
````.sh
macOS Monterey(v12.5.1)
````

# 事前準備

`metal-cpp_macOS12_iOS15.zip` をダウンロードして展開する。
下記コマンドでシングルヘッダーにまとめる。
````.sh
./SingleHeader/MakeSingleHeader.py Foundation/Foundation.hpp QuartzCore/QuartzCore.hpp Metal/Metal.hpp
````

ただし、この変換スクリプトの中身を見るとわかるのですが、
`/usr/bin/python` を実行するよう指定しています。
新しめのMacOSでは組み込みの `python` が削除されていて、`/usr/bin/python`は存在しません。
````.py
#!/usr/bin/python
````

以下のように書き換えるとよしなに `python` のパスを解決してくれます。
````.py
#!/usr/bin/env python
````

# ビルド設定

適当な場所にリポジトリ作成
````.sh
mkdir Metal-Study
cd Metal-Study
git init
gibo dump CMake c++ VisualStudioCode > .gitignore
git add .
git commit -m "Add: 最初のコミット"
````

諸々の環境をCMakeでセットアップ
※[CMake で GLFW を見つける](https://qiita.com/benikabocha/items/152bc3a34a0c1e270252)のライブラリ検索スクリプトを使います。
````.sh
mkdir CMake
curl https://raw.githubusercontent.com/benikabocha/FindGLFW_Test/master/cmake/FindGLFW.cmake > CMake/FindGLFW.cmake
touch CMakeLists.txt
touch main.cpp
````

CMakeの内容はこんな感じに設定。
````CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(
    MetalStudy
    VERSION 0.0.1
    LANGUAGES CXX
)
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
#find_package(OpenGL REQUIRED)
find_package(GLFW REQUIRED)

add_executable(
    MetalStudy
    main.cpp
)
target_compile_definitions(
    MetalStudy
    PRIVATE
    $<$<CONFIG:Release>:MetalStudy_NDEBUG>
    $<$<CONFIG:Debug>:MetalStudy_DEBUG _DEBUG>
)
target_compile_options(
    MetalStudy
    PRIVATE
    $<$<CONFIG:Release>:-O3>
    $<$<CONFIG:Debug>:-O0 -g -Wall -Werror -fsanitize=address>
)
target_link_options(
    MetalStudy
    PRIVATE
    $<$<CONFIG:Release>:>
    $<$<CONFIG:Debug>: -fsanitize=address>
)
target_compile_features(MetalStudy PRIVATE cxx_std_17)

target_include_directories(
    MetalStudy
    PRIVATE
    #${OPENGL_INCLUDE_DIR}
    ${GLFW_INCLUDE_DIR}
)

target_link_libraries(
    MetalStudy
    PRIVATE
    #${OPENGL_LIBRARIES}
    ${GLFW_LIBRARIES}
)
````

まずはこんな感じのコードを用意してGLFWの動作をひとまず確認。
````main.cpp
#include <GLFW/glfw3.h>
#include <iostream>
#include <string>

int gl_init(int argc, char *argv[], const char *title, int width, int height,
            bool fullScreen, GLFWwindow **outWindow) {
  (*outWindow) = NULL;
  if (!glfwInit())
    return -1;
  glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
  // create window
  GLFWwindow *window = NULL;
  if (fullScreen) {
    GLFWmonitor *monitor = glfwGetPrimaryMonitor();
    const GLFWvidmode *mode = glfwGetVideoMode(monitor);
    glfwWindowHint(GLFW_RED_BITS, mode->redBits);
    glfwWindowHint(GLFW_GREEN_BITS, mode->greenBits);
    glfwWindowHint(GLFW_BLUE_BITS, mode->blueBits);
    glfwWindowHint(GLFW_REFRESH_RATE, mode->refreshRate);
    window = glfwCreateWindow(mode->width, mode->height, title, monitor, NULL);
  } else {
    glfwWindowHint(GLFW_RESIZABLE, 0);
    window = glfwCreateWindow(width, height, title, NULL, NULL);
  }
  if (!window) {
    glfwTerminate();
    return -1;
  }
  glfwMakeContextCurrent(window);
  glfwSetTime(0.0);
  glfwSwapInterval(1);
  (*outWindow) = window;
  return 0;
}

int main(int argc, char *argv[]) {
  GLFWwindow *window;
  int status = gl_init(argc, argv, "Sample", 1280, 720, false, &window);
  if (status != 0) {
    return status;
  }

  while (!glfwWindowShouldClose(window)) {
    glfwSwapBuffers(window);
    glfwPollEvents();
  }
  glfwDestroyWindow(window);
  glfwTerminate();
  return 0;
}
````

こんな感じでビルドして実行できるはず。
````.sh
cmake .
make
./MetalStudy
````

一旦コミット。
````.sh
git add .
git commit -m "Add: GLFWの動作確認できるところまで"
````

# Metalも組み込む
事前準備で用意したヘッダーをリポジトリにコピーして、コードからも読み込みます。
````main.cpp
// Metal
#define NS_PRIVATE_IMPLEMENTATION
#define CA_PRIVATE_IMPLEMENTATION
#define MTL_PRIVATE_IMPLEMENTATION
#include "Metal.hpp"
// GLFW
#include <GLFW/glfw3.h>
#include <iostream>
#include <string>
````

これだけだとまだビルドできないので、CMakeも変更が必要です。
※ビルドして実行しても結果は特に変わりません。
````CMakeLists.txt
target_link_libraries(
    MetalStudy
    PRIVATE
    #${OPENGL_LIBRARIES}
    "-framework Cocoa"
    "-framework Foundation"
    "-framework Metal"
    "-framework QuartzCore"
    ${GLFW_LIBRARIES}
)
````

一旦この時点でのファイル構成を以下に示します。
````.sh
> ls
 CMake                 CMakeFiles       Makefile
 cmake_install.cmake   CMakeLists.txt   Metal.hpp
 CMakeCache.txt        main.cpp         MetalStudy
````

一旦コミット。
````.sh
git add .
git commit -m "Add: Metalの組み込み"
````

# 足りない機能を追加
`Metal-cpp` は基本的に `Metal` の主要な機能を提供してくれていますが、
`Metal`とウィンドウシステムのつなぎ込みの部分？は提供していないようです。
もしかしたら私が知らないだけで他に適切な方法があるのかもしれないのですが、私はいくつか追加でコードを用意して解決しました。
`Metal-cpp` は内部的にはObjC経由で `Metal` を呼び出しているだけなのですが、
できるだけC++のスタイルとフィットするように簡単なラップ用の機能を提供しています。
例えば、ObjCのメソッド呼び出しやプロパティアクセスは `Object::sendMessage` で実装できます。
他にもObjCではお馴染みの alloc, init も `Object` クラスに対応するメソッドが用意されています。
基本的には `Metal-cpp` のコードを参考にしながらこれらのラップ用機能を使えば実装は難しくありません。

**追記**
この記事を投稿してから気づいたのですが、[こちら](https://developer.apple.com/jp/metal/)のページの `サンプルコードをダウンロード` のリンクから以下で説明するNSView,NSWindowの拡張と同じようなものがダウンロードできるようです。
こちらはウィンドウ自体にGLFWではなくMac用の専用APIを使うのと、CAMetalLayerではなくMTKViewを使うようになってるので微妙に違うのですが、こちらも大変参考になるので目を通しておいた方がよさそうです。

## CocoaWindow
まず `Metal` とウィンドウシステムを繋ぎ込むにあたって `NSWindow` を取得する必要があります。
これは単に `GLFW` の `glfwGetCocoaWindow` を呼び出すだけなのですが、一つ問題があります。
MacOSX.sdkでは次のように `id` 型が宣言されています。
````objc.h
/*
 * Copyright (c) 1999-2007 Apple Inc.  All Rights Reserved.
 * 
 * @APPLE_LICENSE_HEADER_START@
 * 
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this
 * file.
 * 
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 * 
 * @APPLE_LICENSE_HEADER_END@
 */

//
// 中略...
//

typedef struct objc_object *id;
````

そして `GLFW` でも次のように `id` 型が宣言されています。
````glfw3native.h
/*************************************************************************
 * GLFW 3.3 - www.glfw.org
 * A library for OpenGL, window and input
 *------------------------------------------------------------------------
 * Copyright (c) 2002-2006 Marcus Geelnard
 * Copyright (c) 2006-2018 Camilla Löwy <elmindreda@glfw.org>
 *
 * This software is provided 'as-is', without any express or implied
 * warranty. In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would
 *    be appreciated but is not required.
 *
 * 2. Altered source versions must be plainly marked as such, and must not
 *    be misrepresented as being the original software.
 *
 * 3. This notice may not be removed or altered from any source
 *    distribution.
 *
 *************************************************************************/

//
// 中略...
//

typedef void* id;
````

というわけで、この二つのヘッダーを公開ヘッダーからインクルードしてしまうと同じ名前で二つの型が存在することになり、
コンパイルエラーとなってしまいます。対処としてはごく単純で、`.cpp`からのみ`id`が参照されるような作りにすればOKです。

ヘッダーでは基本ヘッダーのみをインクルードして
````CocoaWindow.hpp
#pragma once
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

namespace GLFW::Private {
void *getCocoaWindow(GLFWwindow *window);
}
````

実装ファイルからのみネイティブ機能のヘッダー(idを定義しているヘッダー）をインクルードします。
````CocoaWindow.cpp
#include "CocoaWindow.hpp"
#define GLFW_EXPOSE_NATIVE_COCOA
#include <GLFW/glfw3native.h>

namespace GLFW::Private {
void *getCocoaWindow(GLFWwindow *window) { return glfwGetCocoaWindow(window); }
} // namespace GLFW::Private
````

この `CocoaWindow.hpp` と `Metal.hpp` なら、同時にインクルードしてもid型の衝突が起こりません。

## NSWindow
この記事の冒頭でも参考コードとしてリンクを貼ってあるのですが、
`Metal`とウィンドウシステムを繋ぎ込むためには`NSWindow.cotentView.layer`を`CAMetalLayer`で置き換える必要があります。
これらのクラスは `Metal-cpp` で提供されていないので自分で用意する必要があります。

まずヘッダー側でクラスとセレクタを事前に定義しておく必要があります。
````NSWindow.hpp
#pragma once
#include "Metal.hpp"

namespace NS::Private::Class {
_NS_PRIVATE_DEF_CLS(NSWindow);
} // namespace NS::Private::Class

namespace NS::Private::Selector {
_NS_PRIVATE_DEF_SEL(contentView_, "contentView");
} // namespace NS::Private::Selector

namespace NS {
class View;
class Window : public Referencing<Window> {
public:
  static Window *bridgingCast(const void *ptr);
  class View *contentView() const;
};
} // namespace NS
````

ヘッダーで定義したセレクタを使って `sendMessage` を呼び出すことでObjCとの繋ぎ込みを実装できます。
````NSWindow.cpp
#include "NSWindow.hpp"
#include "NSView.hpp"

namespace NS {
Window *Window::bridgingCast(const void *ptr) {
  return Object::bridgingCast<Window *>(ptr);
}
View *Window::contentView() const {
  return Object::sendMessage<View *>(this, _NS_PRIVATE_SEL(contentView_));
}
} // namespace NS
````

セレクタとして定義する文字列などは[Appleの公式ドキュメント](https://developer.apple.com/documentation/appkit/nswindow)を参考にしてください。


## NSView
`NSWindow.contentView` に対応するクラスの定義です。
ほぼ先ほどと同じなのであまり説明することはありません。
````NSView.hpp
#pragma once
#include "Metal.hpp"

namespace NS::Private::Class {
_NS_PRIVATE_DEF_CLS(NSView);
} // namespace NS::Private::Class

namespace NS::Private::Selector {
_NS_PRIVATE_DEF_SEL(setLayer_, "setLayer:");
_NS_PRIVATE_DEF_SEL(setWantsLayer_, "setWantsLayer:");
} // namespace NS::Private::Selector

namespace CA {
class MetalLayer;
}
namespace NS {
class View : public Referencing<View> {
public:
  void setLayer(const CA::MetalLayer *layer);
  void setWantsLayer(bool yes);
};
} // namespace NS
````


`sendMessage`は可変長引数を取るようになっているのでそのまま引数を渡せます。
````NSView.cpp
#include "NSView.hpp"

namespace NS {
void View::setLayer(const CA::MetalLayer *layer) {
  Object::sendMessage<void>(this, _NS_PRIVATE_SEL(setLayer_), layer);
}
void View::setWantsLayer(bool yes) {
  Object::sendMessage<void>(this, _NS_PRIVATE_SEL(setWantsLayer_), yes);
}
} // namespace NS
````

## CAMetalLayer
最後に `CAMetalLayer` を定義します。
これも基本的には今までと同じなのですが、使うマクロが違います。
````CAMetalLayer.hpp
#pragma once
#include "Metal.hpp"

namespace CA::Private::Class {
_CA_PRIVATE_DEF_CLS(CAMetalLayer);
} // namespace CA::Private::Class

namespace CA::Private::Selector {
_CA_PRIVATE_DEF_SEL(setDevice_, "setDevice:");
_CA_PRIVATE_DEF_SEL(setOpaque_, "setOpaque:");
_CA_PRIVATE_DEF_SEL(nextDrawable_, "nextDrawable");
} // namespace CA::Private::Selector

namespace CA {
class MetalLayer : public NS::Referencing<MetalLayer> {
public:
  static MetalLayer *alloc();
  MetalLayer *init();
  void setDevice(const MTL::Device *device);
  void setOpaque(bool yes);
  CA::MetalDrawable *nextDrawable();
};
} // namespace CA
````

alloc/initは `sendMessage` とは別で専用のメソッドが用意されています。
````CAMetalLayer.cpp
#include "CAMetalLayer.hpp"

namespace CA {
MetalLayer *MetalLayer::alloc() {
  return NS::Object::alloc<MetalLayer>(_CA_PRIVATE_CLS(CAMetalLayer));
}

MetalLayer *MetalLayer::init() { return NS::Object::init<MetalLayer>(); }
void MetalLayer::setDevice(const MTL::Device *device) {
  Object::sendMessage<void>(this, _CA_PRIVATE_SEL(setDevice_), device);
}
void MetalLayer::setOpaque(bool yes) {
  Object::sendMessage<void>(this, _CA_PRIVATE_SEL(setOpaque_), yes);
}
CA::MetalDrawable *MetalLayer::nextDrawable() {
  return Object::sendMessage<CA::MetalDrawable *>(
      this, _CA_PRIVATE_SEL(nextDrawable_));
}
} // namespace CA
````

一旦コミット。
````.sh
git add .
git commit -m "Add: 繋ぎ込みのための機能を追加"
````

# 最後に

あとは冒頭の参考コードをそのままC++の機能に置き換えていくだけです。
````main.cpp
// Metal
#define NS_PRIVATE_IMPLEMENTATION
#define CA_PRIVATE_IMPLEMENTATION
#define MTL_PRIVATE_IMPLEMENTATION
#include "Metal.hpp"
// Metal拡張
#include "CAMetalLayer.hpp"
#include "CocoaWindow.hpp"
#include "NSView.hpp"
#include "NSWindow.hpp"
// その他標準ライブラリ
#include <iostream>
#include <string>

int gl_init(int argc, char *argv[], const char *title, int width, int height,
            bool fullScreen, GLFWwindow **outWindow) {
  (*outWindow) = NULL;
  if (!glfwInit())
    return -1;
  glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
  // create window
  GLFWwindow *window = NULL;
  if (fullScreen) {
    GLFWmonitor *monitor = glfwGetPrimaryMonitor();
    const GLFWvidmode *mode = glfwGetVideoMode(monitor);
    glfwWindowHint(GLFW_RED_BITS, mode->redBits);
    glfwWindowHint(GLFW_GREEN_BITS, mode->greenBits);
    glfwWindowHint(GLFW_BLUE_BITS, mode->blueBits);
    glfwWindowHint(GLFW_REFRESH_RATE, mode->refreshRate);
    window = glfwCreateWindow(mode->width, mode->height, title, monitor, NULL);
  } else {
    glfwWindowHint(GLFW_RESIZABLE, 0);
    window = glfwCreateWindow(width, height, title, NULL, NULL);
  }
  if (!window) {
    glfwTerminate();
    return -1;
  }
  glfwMakeContextCurrent(window);
  glfwSetTime(0.0);
  glfwSwapInterval(1);
  (*outWindow) = window;
  return 0;
}

int main(int argc, char *argv[]) {
  GLFWwindow *window;
  int status = gl_init(argc, argv, "Sample", 1280, 720, false, &window);
  if (status != 0) {
    return status;
  }

  MTL::Device *device = MTL::CreateSystemDefaultDevice();
  MTL::CommandQueue *queue = device->newCommandQueue();
  CA::MetalLayer *layer = CA::MetalLayer::alloc();
  layer->init();
  layer->setDevice(device);
  layer->setOpaque(true);
  NS::Window *nsWindow =
      NS::Window::bridgingCast(GLFW::Private::getCocoaWindow(window));
  NS::View *contentView = nsWindow->contentView();
  contentView->setLayer(layer);
  contentView->setWantsLayer(true);

  while (!glfwWindowShouldClose(window)) {
    CA::MetalDrawable *surface = layer->nextDrawable();

    MTL::RenderPassDescriptor *pass = MTL::RenderPassDescriptor::alloc();
    pass->init();
    MTL::RenderPassColorAttachmentDescriptor *desc =
        pass->colorAttachments()->object(0);
    desc->setClearColor(MTL::ClearColor(1.0, 0.0, 0.0, 1.0));
    desc->setLoadAction(MTL::LoadAction::LoadActionClear);
    desc->setStoreAction(MTL::StoreAction::StoreActionStore);
    desc->setTexture(surface->texture());

    MTL::CommandBuffer *buffer = queue->commandBuffer();
    MTL::RenderCommandEncoder *encoder = buffer->renderCommandEncoder(pass);
    encoder->endEncoding();
    buffer->presentDrawable(surface);
    buffer->commit();
    glfwPollEvents();
  }
  glfwDestroyWindow(window);
  glfwTerminate();
  return 0;
}
````

CMakeListsにも忘れずにcppを追加します。
````CMakeLists.txt
add_executable(
    MetalStudy
    main.cpp
    CocoaWindow.cpp
    NSWindow.cpp
    NSView.cpp
    CAMetalLayer.cpp
)
````

実行すると真っ赤に塗りつぶされたウィンドウが表示されるはずです。
![2022-11-05_16-17-49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263901/2aa8968d-b771-41e1-8672-5bcb6b142fdd.png)

サンプルリポジトリは[こちら](https://github.com/desktopgame/Metal-Study)に置いています。