Backroomsとオデュッセイアを見た。

(ちょっとネタバレあるかも?)

この前のシルバーウィーク中、ある日に映画を見に行った。

Backroomsを午前中に見たのだが、正直あまりハマらなかった。

インターネット上のミームを元ネタとした作品とのことだったので、一応調べて予習してから行ったのだが、あまり役に立たなかった。ところどころ勉強していった内容と異なる、いつでもBackroomsに出入りできたり、Backroomsの空間が侵入した人間の記憶を反映する部分があったりと、多分映画オリジナルの設定があった(自分の予習不足なのかもだが)。

また、自分が良い歳こいてドパガキみたいな感性しているだけかもだが、展開がちょっと遅いように感じた。映画の話のほとんどが登場人物の紹介に使われていて、あまり話が次から次に進んでいくような感覚がなかった。Backroomsの探索を通じて、何か新しいことが徐々に解明されていくみたいな。ただ先述の通り、「侵入した人間の記憶を反映」という設定があるので、登場人物がある程度どのような背景のある人間なのかを描写しなければBackrooms内で起こることの理由が分かりづらくなることもあるだろうから、コレはしょうがないのかも知れない。

ただホラー演出はおもしろかった。死体を引きづりながらノロノロ追いかけてくるバケモノにはゾクゾクしたし、主人公がガスの噴出を直に浴びてしまい、意識が曖昧になった後明確に戻った直後にバケモノが目と鼻の先にいて主人公のことを食わんとしてくる演出はかなりビビった。

Backroomsでは満足できなかったので、このまま休日を終えたくなかったため、昼食のラーメンを啜りながらオデュッセイアを見ることに決めた。話題のちいかわと悩んだのだが、ちいかわはネットで見ていて、結末を知っていたので、全く知らない方が新鮮な驚きや面白さを感じられるかも知れないと思いオデュッセイアにした。

オデュッセイアは滅茶苦茶おもしろかった。 ホメロス叙事詩の方のオデュッセイアを元ネタにしており、こちらは事前知識なしで見たがある程度背景情報を察することができるように話が進んでいき、そこまで突っかかることなく見ることができた。

トロイア征伐後、オデュッセウスが帰国途中に諸々あって、遭難してしまい海岸に流れ着き、記憶喪失(正確には違うと思うが)になってしまい、ここに至るまでの過程を回想するような形式で話が進んでいく。この回想形式が映画のほとんどを占めるため、オデュッセイアはオデュッセイアで「この人はいつ自国に帰るんだ…?」となったが、退屈になることはなかった。

海岸に漂着して全てを思い出した後、帰国のために筏を作成して海に繰り出す直前、面倒を見てくれて懇意にしていた女性に「神を信じることがあなたにとっての初めての仕事よ」と言った旨のことを言われるが、正直ちょっとそれが突っかかっていた。ただ、思ってみれば神を信用していればトロイの木馬とかいう人々の信仰を逆手に取るような作戦を実行しようとは思わないだろうし、冥界の門を開いて予言をもらった時に「すべての苦難は俺が部下を統制することでどうにかする」と言った発言はしないだろう。恐れを抱くぐらいか、どうにかならないか預言者にさらに尋ねるかな気がする。

帰国後、自分のいない間に城を好き勝手にしていたならず者たちを殺すアクションシーンもあり、ストーリーも楽しめたし、アクション面も楽しめ、色々な人が見て楽しめる一面がある作品だなと思った。

ImageのLayerの中身を見てみたい

ContainerのFilesystemはLayerを重ねていくことで構成していくが、その1つ1つのLayerがどのような内容なのか気になったので調べることにした。

ContainerdのDocsページ(dockerのContainer runtimeのruncを作っているProject)を見るに基本的にはOCIというImage Formatで、Imageの中には以下の2つのjsonファイルが含まれるらしい

  • manifestファイル… imageに関する情報が書かれている
  • indexファイル…platform(architectureとOSの組み合わせ ex: amd6のLinux)ごとのmanifestファイルのhash情報

このうちmanifestファイルの方にLayerに関する情報が含まれているらしい。ひとまずmanifestファイルの中身を見ることを目的とする。

https://containerd.io/docs/2.3/content-flow/

まずは以下のDockerfileからビルドされるImageを考える。このDockerのFROMやRUNなどのコマンドで作成されるLayerを調べていく。

FROM ubuntu:latest
COPY ./hello.sh hello.sh
RUN sed 's/world/japan/' hello.sh
RUN ./hello.sh
RUN rm hello.sh
CMD tail -F /dev/null

以下のコマンドでビルドする。

cd <Dockerfileがあるディレクトリ>
docker build . -t myhello:1.0

# ビルドできていることを確認
docker images

以下のコマンドで、Imageの内容をtarファイルとして保存することができる。

docker save myhello:1.0 -o myhello.tar

tarを解凍して、treeで中身を見てみる。

-> % tree myhello
myhello
├── blobs
│   └── sha256
│       ├── 01e1d945f346dbd8b1913bffa3c3ea996d913a951183aeb8c0c8965ed0c092b5
│       ├── 2113f8d7eb32748b14581824c1b94cea9ed9a08456312a2e94eddd522d01b927
│       ├── 2fa82af77f18196350fcc21afbd40432eb16229341c651ade5f87b7cda7f09e9
│       ├── 4a7720058461eb4ae40ed203b9874ab3248bd34ffb9948193e99245229fdbd6f
│       ├── 4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1
│       ├── 566ace63b89828ed12cc70881f1cf7437aefc5c2aa8d067878e703a0b87c355a
│       ├── 5be7c372b23fc37302b3c89533b96dff48a15e1fcde7fd7efc91d9010a732893
│       ├── 90d5b56951945c685b4d8461e0dae70a913aa5d786f31ff84d9b5ef24dd0ad50
│       ├── a370b8776a1ae052c3f9200c279fc74714827d8b329e525174c3c60c729f8d9e
│       ├── a6b428496f7545f2fa12f4c5e53d052c63beb73dedd74c0166aa2b96fffaa7f7
│       └── bbafa4d094e00a398161a2b6641c275dc91a9600d54be605d517d4a5afc30656
├── index.json
├── manifest.json
└── oci-layout

3 directories, 14 files

blobディレクトリの中身をひとつずつ開けようとすると、開けたり開けなかったりしたが、01e1d945f346dbd8b1913bffa3c3ea996d913a951183aeb8c0c8965ed0c092b5がテキストデータであり、containerdのページで見たmanifest.jsonの中身そっくりであった。これがPlatformごとのmanifest.jsonなのだろう。

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:90d5b56951945c685b4d8461e0dae70a913aa5d786f31ff84d9b5ef24dd0ad50",
    "size": 5461
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:2113f8d7eb32748b14581824c1b94cea9ed9a08456312a2e94eddd522d01b927",
      "size": 40728955
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:4a7720058461eb4ae40ed203b9874ab3248bd34ffb9948193e99245229fdbd6f",
      "size": 390
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:5be7c372b23fc37302b3c89533b96dff48a15e1fcde7fd7efc91d9010a732893",
      "size": 133
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1",
      "size": 32
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1",
      "size": 32
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:566ace63b89828ed12cc70881f1cf7437aefc5c2aa8d067878e703a0b87c355a",
      "size": 78
    }
  ]
}

このjsonのlayerプロパティのdigestがblobのファイル名と一致するから、これらのファイルがLayerについてのデータと考えて良いだろう。

このファイル群はContainerdのmediatypeから察する時tar.gzで保存されているようなので、以下のプログラムでファイル内に圧縮されているファイル名を読み取ってみる。(golang)

package main 

import (
    "fmt"
    "io"
    "log"
    "archive/tar"
    "os"
    "compress/gzip"
)

func main() {
    // read layer tar.gz
    path := os.Args[1]
    layer, err := os.Open(path)

    // create layer file reader
    if err != nil {
        log.Fatal(err)
    }
    gr, err := gzip.NewReader(layer)
    if err != nil {
        log.Fatal(err)
    }
    tr := tar.NewReader(gr)

    // print file name in layer
    count := 0
    for {
        hdr, err := tr.Next()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println(hdr.Name)
        count++
    }
    fmt.Fprintln("file num: %v", count)
}

今回Module名はread_layerとした。 実行してみると大量のファイル名が表示された。多分ubuntuに必要がファイル群なのだろう。

-> % go build
-> % ./read_layer <blob dir>/90d5b56951945c685b4d8461e0dae70a913aa5d786f31ff84d9b5ef24dd0ad50

…
…
log/dpkg.log
var/log/lastlog
var/log/wtmp
var/mail/
var/opt/
var/run
var/spool/
var/spool/mail
var/tmp/
file num: 6588%

他のblobデータの中身を見てみると以下のようになった。

  • 90d5b56951945c685b4d8461e0dae70a913aa5d786f31ff84d9b5ef24dd0ad50: ubuntuに必要なファイル群
  • 4a7720058461eb4ae40ed203b9874ab3248bd34ffb9948193e99245229fdbd6f: .rock/metadata
  • 5be7c372b23fc37302b3c89533b96dff48a15e1fcde7fd7efc91d9010a732893 : hello.sh
  • 4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1: ファイルなしのLayer
  • 4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1: ファイルなしのLayer
  • 566ace63b89828ed12cc70881f1cf7437aefc5c2aa8d067878e703a0b87c355a: .wh.hello.sh

Dockerfileの内容と見比べて、よくわからないLayerがいくつか見受けられる。ubuntuに必要なファイル群がLayerに含まれることがわかるが、.rock/metadataというファイルはどこから来たのか。FROMコマンドがLayerを二つ作成しているのだろうか。また、Digestが同じことから、4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1の同一の、ファイルを含んでいないLayerが2回利用されていることがわかる。これは何か。出現箇所から見、RUN sed 's/world/japan/' hello.shとRUN ./hello.shに対応するLayerであり、そもそもファイルを作成していないので、ファイルを含まないLayerが出来上がることは理解できる。ただどちらのコマンド実施するのかの区別がつかない。これではどちらを先に実施するのかがわからない。

.wh.hello.shについては推察がつく。これはRUN rm hello.shに対応するlayerであり、layerを積み重ねていきすでに出来上がったcontainer用のfilesystemから、削除するべきファイルの識別子がこのprefixとして.wh.がついたファイルなのだろう。

ここまででわからないこととしては以下である

  • FROMが複数個のLayerを作成しているかどうか
  • 複数コマンドが同一のLayarに置き換えられる場合があるが、各々をどのように識別しているのか

調べてみると90d5b56951945c685b4d8461e0dae70a913aa5d786f31ff84d9b5ef24dd0ad50というファイルにこのImageを作成するのに必要なコマンド一覧が載っており、そこから情報を取ることができそうだ。

-> % cat 90d5b56951945c685b4d8461e0dae70a913aa5d786f31ff84d9b5ef24dd0ad50 | jq
{
  "architecture": "arm64",
  "config": {
    "Env": [
      "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    ],
    "Cmd": [
      "/bin/sh",
      "-c",
      "tail -F /dev/null"
    ],
    "Labels": {
      "org.opencontainers.image.created": "2026-04-21T15:27:26.117874+00:00",
      "org.opencontainers.image.description": "The Ubuntu container image maintained by Canonical\n\nUbuntu is a Debian-based Linux operating system that runs from the desktop to the cloud, to all your internet connected things.\nIt is the world's most popular operating system across public clouds and OpenStack clouds.\nIt is the number one platform for containers; from Docker to Kubernetes to LXD, Ubuntu can run your containers at scale.\nFast, secure and simple, Ubuntu powers millions of PCs worldwide.\n",
      "org.opencontainers.image.title": "ubuntu",
      "org.opencontainers.image.version": "26.04"
    },
    "ArgsEscaped": true
  },
  "created": "2026-06-30T02:28:49.333798304+09:00",
  "history": [
    {
      "created": "2026-04-21T15:27:25.729590696Z",
      "created_by": "umoci raw add-layer --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:rockcraft-base /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/.temp_layer.4553.tar --tag 26.04"
    },
    {
      "created": "2026-04-21T15:27:26.085571386Z",
      "created_by": "umoci config --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:26.04 --clear=config.entrypoint --clear=config.cmd",
      "comment": "Set entrypoint",
      "empty_layer": true
    },
    {
      "created": "2026-04-21T15:27:26.098832351Z",
      "created_by": "umoci config --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:26.04 --clear=config.cmd --config.cmd /bin/bash",
      "comment": "Set default commands",
      "empty_layer": true
    },
    {
      "created": "2026-04-21T15:27:26.110670037Z",
      "created_by": "umoci config --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:26.04 --config.env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
      "comment": "Set default PATH for bare-based rock",
      "empty_layer": true
    },
    {
      "created": "2026-04-21T15:27:26.122209875Z",
      "created_by": "umoci config --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:26.04 --clear=config.labels --config.label org.opencontainers.image.version=26.04 --config.label org.opencontainers.image.title=ubuntu --config.label org.opencontainers.image.created=2026-04-21T15:27:26.117874+00:00 --config.label org.opencontainers.image.description=The Ubuntu container image maintained by Canonical\n\nUbuntu is a Debian-based Linux operating system that runs from the desktop to the cloud, to all your internet connected things.\nIt is the world's most popular operating system across public clouds and OpenStack clouds.\nIt is the number one platform for containers; from Docker to Kubernetes to LXD, Ubuntu can run your containers at scale.\nFast, secure and simple, Ubuntu powers millions of PCs worldwide.\n",
      "comment": "Set labels",
      "empty_layer": true
    },
    {
      "created": "2026-04-21T15:27:26.137480535Z",
      "created_by": "umoci config --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:26.04 --clear=manifest.annotations --manifest.annotation org.opencontainers.image.version=26.04 --manifest.annotation org.opencontainers.image.title=ubuntu --manifest.annotation org.opencontainers.image.created=2026-04-21T15:27:26.117874+00:00 --manifest.annotation org.opencontainers.image.description=The Ubuntu container image maintained by Canonical\n\nUbuntu is a Debian-based Linux operating system that runs from the desktop to the cloud, to all your internet connected things.\nIt is the world's most popular operating system across public clouds and OpenStack clouds.\nIt is the number one platform for containers; from Docker to Kubernetes to LXD, Ubuntu can run your containers at scale.\nFast, secure and simple, Ubuntu powers millions of PCs worldwide.\n",
      "comment": "Set annotations",
      "empty_layer": true
    },
    {
      "created": "2026-04-21T15:27:26.156072607Z",
      "created_by": "umoci raw add-layer --image /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/ubuntu:26.04 /home/buildd/rockcraft-ubuntu-5691f940e215a35dc9b91fc1887cae39/images/.temp_layer.control_data.4553.tar",
      "comment": "Add rock control metadata"
    },
    {
      "created": "2026-06-07T14:29:34.708106501+09:00",
      "created_by": "COPY ./hello.sh hello.sh # buildkit",
      "comment": "buildkit.dockerfile.v0"
    },
    {
      "created": "2026-06-30T02:01:32.535475982+09:00",
      "created_by": "RUN /bin/sh -c sed 's/world/japan/' hello.sh # buildkit",
      "comment": "buildkit.dockerfile.v0"
    },
    {
      "created": "2026-06-30T02:01:33.028080274+09:00",
      "created_by": "RUN /bin/sh -c ./hello.sh # buildkit",
      "comment": "buildkit.dockerfile.v0"
    },
    {
      "created": "2026-06-30T02:28:49.333798304+09:00",
      "created_by": "RUN /bin/sh -c rm hello.sh # buildkit",
      "comment": "buildkit.dockerfile.v0"
    },
    {
      "created": "2026-06-30T02:28:49.333798304+09:00",
      "created_by": "CMD [\"/bin/sh\" \"-c\" \"tail -F /dev/null\"]",
      "comment": "buildkit.dockerfile.v0",
      "empty_layer": true
    }
  ],
  "os": "linux",
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:5cde16a3f2e15fb91f1e3d4002774c76fa956655928f652e9156d5bb63a2ad2f",
      "sha256:275233e882bbec676f42159b4909463115d4b5cef13410756a3918679a87cef8",
      "sha256:010b4cb38f12101b3503a8418425d475d19a28a6395153ca456954f4c8ed3e9d",
      "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
      "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",
      "sha256:0f5bef1ec05a01876b2373a074c20b74b6fcda535b3038ac62008b23282b3710"
    ]
  }
}

このファイル内におけるhistoryというのはFROMでベースとしたubuntuイメージを作る際に利用したコマンドも含めたコマンド履歴だろう。なので実際にはFROMを利用すると6つのLayerをRegistryからひっぱってくることになるのだと思う。

historyから見てみると、各履歴の中にempty_layerという項目があり、これがtrueの場合にはblobファイルは作成されないのだろう。emptyの数をhistoryの数からひくとdiff_idsの数と一致する。

同一Layerで示されるコマンドの順序についても、このhistoryの早く出てきた順序で実施すればよさそうだ。

rootfs.diff_idsプロパティというのは調べたところLayerファイルを解凍したもののDigest一覧であるらしい。 参照: https://github.com/opencontainers/image-spec/blob/main/config.md#layer-diffid

またこれら以外にも環境変数一覧やContainerをスタートする際に走らせるコマンドも記録されている。忘れていたが確かにこれらの情報もContainerを動かすのに必要だろう。察するにこのファイルというのContainerを動かすために必要な情報一覧だと思われる。

Layerを重ねていく方法についても以下の通りに行えば良いだろう。

  • historyから empty_layer: trueのものを取り除く。
  • historyとdiff_idsを上から順番に見ていき、対応するblobを展開してfilesystemに追加/削除を行い、RUNコマンドなどの場合は指定された処理を実施する。

覚書:PCメガネの選び方

PCメガネを選んだ時の観点をまとめる。

観点

  • ブルーライトカット率の計算にどの規格のものを利用しているか。
  • レンズが「コーティング式」か「練り込み式」か

ブルーライトカットの計算規格

以下の3つの規格が利用されることが多い。

  • JIS規格
  • EN規格
  • BS規格

JIS規格とEN規格はほぼ同様のものと思って良いが、BS規格については前者2つに比べて計算結果が大きな値になりやすい。

例えばJIS規格で50%cut、BS規格で50%cut、というように表示されていたとしてもBS規格→JIS規格に計算し直したときに40%cut程度あることがある。

Zoffのブルーライトカットレンズの紹介ページを見てみると、ページの最後の注釈に以下の記述がある。

ブルーライトカット率は旧英国規格(BS2724-1987)に基づき算出しています。

www.zoff.co.jp

使い心地というのは使ってみなければわからないものだとは思うが、正直商品を実際のものよりもよく見せたいために数値が大きく出る計算も方法を利用しているのかと思えるので、あまり効能を求めてZoffからPCメガネを買いたいとは思えない。

ただ、安い値段でファッション性の高いフレームが手に入ると思うので、その辺りを気にするならZoffを選択肢に入れるのは良いかもしれない。

「コーティング式」OR「練り込み式」

ブルーライトカット対応のレンズの製法には2種類ある

  • コーティング式
  • 練り込み式

コーティング式は普通のレンズの表面にブルーライトカット用のコーティングを施したもので、練り込み式はレンズ自体の素材にブルーライトをカットする素材を利用したもの。

コーティング式のものは安価であるというのが長所であるが、長年利用するとコーティングが剥がれてカット効果がなくなってしまったり、光源のライトなどが反射しやすく集中しづらいなどの短所がある。

練り込み式のものは逆で、カット効果がなくなってしまうこともなく、反射もしづらい。ただコーティング式のものよりかは高い。

使いやすさの観点もあると思うので、練り込み式の方が良いように思える。ただ、値段と相談したいとも思うので、安価なコーティング式のものを探すのもやむなしかなと思う。

MTLViewの描画の流れについてのドキュメント

これが主にMetalKitのMTLViewのセットアップについて書かれているドキュメント developer.apple.com

これが主にMetalKitのMTLViewでの描画について説明しているドキュメント developer.apple.com

MTLViewで表示を行うときの流れとしては以下の通り。先週書いたgpuで簡単な配列計算をした時とそこまで変わらないみたい。

  1. MTLViewインスタンスのdeviceプロパティに使いたいGPUのMTLDeviceインスタンスを割り当てる
  2. RenderPipelineを作成する。この時MTLRenderPipelineDescriptorを利用する。本来のMTLRenderPipelineDescriptorインスタンスからPipelineの一部を変更して、自分の描画したいものを描けるようなPipelineにする
  3. MTLDeviceインスタンスからMTLCommandQueueを作成する
  4. MTLView Delegateを作成し、MTLViewインスタンスに設定する
  5. MTLView Delegateのdrawメソッド内でCommandBufferの設定し、実行後に描画を行う
    1. 先に作成したPipelineを使用するCommandBufferを作成する。
    2. CommandEncoderを作成してPipelineのインプットし最後にviewのMTLViewのdrawableプロパティを作成したテクスチャで置き換える処理をPassに追加する
    3. 実行

RenderPipelineはいくつかのステップに分かれているので、そのステップのうちいくつかを自前のものに置き換えてやることで自分の描きたいテクスチャを表示することができる。今回ドキュメントでは、図形の描画をするため図形の頂点情報を出力する段階であるVertex functionと図形内部のピクセル情報を変更できる段階のFragment functionの変更を行なっていた。

感想

先週は体調悪くてコード書いて見るところまで行かんかったので、今週はMSLとか自分で書いてみたい。

Performing Calculations on a GPUを読んだ

将来的にMetalでiOSのAppのViewを表示したりしたいなあと思ったので、第一歩として以下のAppleのページを見ながらMetalを扱うためのコードを書いてみた。 developer.apple.com

以下は2つのIntのリストのそれぞれの要素を足し合わせる計算をGPUにやらせるコードである。

GitHub - rikinyan/MetalCalcSample

import Foundation
import Metal

guard let gpuDevice = MTLCreateSystemDefaultDevice() else {
    throw NSError(domain: "device", code: 1)
}

let intByte = Int.bitWidth / 8
let arrayLength = 10
let bufferLenght = intByte * arrayLength
var bufferA = gpuDevice.makeBuffer(length: bufferLenght, options: .storageModeShared)
var bufferB = gpuDevice.makeBuffer(length: bufferLenght, options: .storageModeShared)
var bufferResult = gpuDevice.makeBuffer(length: bufferLenght, options: .storageModeShared)

for index in 0..<arrayLength {
    let byteOffset = index * intByte
    bufferA?.contents().storeBytes(of: Int.random(in: 1...10), toByteOffset: byteOffset, as: Int.self)
    bufferB?.contents().storeBytes(of: Int.random(in: 1...10), toByteOffset: byteOffset, as: Int.self)
}

guard let shaderLibrary = gpuDevice.makeDefaultLibrary() else {
    throw NSError(domain: "lib", code: 1)
}

guard let addFunc = shaderLibrary.makeFunction(name: "add_arrays") else {
    throw NSError(domain: "func", code: 1)
}

guard let pipline = try? gpuDevice.makeComputePipelineState(function: addFunc) else {
    throw NSError(domain: "pipline", code: 1)
}

guard let commandQueue = gpuDevice.makeCommandQueue() else {
    throw NSError(domain: "queue", code: 1)
}

guard let commandBuffer = commandQueue.makeCommandBuffer() else {
    throw NSError(domain: "buffer", code: 1)
}

guard let commandEncoder = commandBuffer.makeComputeCommandEncoder() else {
    throw NSError(domain: "encoder", code: 1)
}

commandEncoder.setComputePipelineState(pipline)
commandEncoder.setBuffer(bufferA, offset: 0, index: 0)
commandEncoder.setBuffer(bufferB, offset: 0, index: 1)
commandEncoder.setBuffer(bufferResult, offset: 0, index: 2)

let threadPerGrid = MTLSize(width: arrayLength, height: 1, depth: 1)
var threadGroupSize = pipline.maxTotalThreadsPerThreadgroup

if threadGroupSize > arrayLength {
    threadGroupSize = arrayLength
}

commandEncoder.dispatchThreadgroups(
    threadPerGrid,
    threadsPerThreadgroup: MTLSize(width: threadGroupSize, height: 1, depth: 1)
)

commandEncoder.endEncoding()

commandBuffer.commit()

commandBuffer.waitUntilCompleted()

for index in 0..<arrayLength {
    let byteOffset = index * intByte
    print(
        bufferResult?.contents().load(fromByteOffset: byteOffset, as: Int.self)
    )
}

MSL:metalを介してGPUで実行されるコード

#include <metal_stdlib>
using namespace metal;

kernel void add_arrays(device const int* inA,
                       device const int* inB,
                       device int* result,
                       uint index [[thread_position_in_grid]])
{
    result[index] = inA[index] + inB[index];
}

全体の流れ

  1. 今デバイスが持っているGPUを扱うための、GPUを抽象化したオブジェクトを作成する。
  2. 計算に利用するデータを確保する。
  3. Appをビルド時に、自作のMSLの関数を持つライブラリが自動でAppに埋め込まれるので、その関数を参照するオブジェクトを作る
  4. 関数を利用するためのパイプラインを作る。
  5. GPUに命令を送るためのキューを作成する
  6. キューに入れる命令を作成する
  7. 命令で利用するスレッドの割り当て設定
  8. 実行

詳細

まず、MTLDeviceというオブジェクトを作成する。GPUを抽象的に表すものであり、GPUに関するさまざまなオブジェクトはMTLDeviceから作成することになる。

GPUで利用するデータはMTLResourceといい、今回はこれを継承したMTLBufferを利用し、事前に計算に必要なデータをセットしたListと計算結果を格納するListを確保した。Bufferを作成するときにオプションとして.storageModeSharedを設定しているが、これはCPU、GPUの両方からアクセスできるメモリとして領域を確保する設定をしている。

次にMSLで書かれた、GPUで実行する命令を参照するオブジェクトを作る。MSL(Metal shading language)というのがGPUで実行するためのコードを記述するためのC++からの派生の言語になる。プロジェクトをビルドするときにそのプロジェクトに含まれているMSLのコードも自動で埋め込まれ、MTLLibraryからMTLFunctionというオブジェクトで関数を参照することができる。また、GPUで実行されるコードのことを慣例的にシェーダと呼ぶらしい。

パイプラインというのが実際に実行されるコードになる。MTLFunctionはあくまでMSLで書かれたコードへの参照であるため、実行できる形式にコンパイルする必要がある。パイプライン作成時にGPUで実行される状態へMSLがコンパイルされる。

この作成したパイプラインをGPUに送るためにMTLCommandQueueというキューを作成する。また、このキューから送信する命令に関するデータのまとまりであるMTLCommandBufferを作成する。MTLCommandEncoderを用いてMTLCommandBufferに実行するパイプライン、パイプラインで利用する引数などの情報を設定する。

最後にMTLCommandEncoderで命令が実行されるスレッドの設定をする。MSLのコードをみて、Listの足し合わせをするのにループが存在しないが、これは各要素ごとの計算を複数スレッドで実行するためである。そのため実行するスレッド数は配列の要素と同じ数になる。threadgroupはメモリブロックを共有するスレッドの集まりであり、今回は全て同じグループとして扱っている。

ここまできたらMTLCommandEncoderを終了し、MTLCommandBufferをコミットして実行する。

CALayerでグラデーションのアニメーションを作成する。

以下のようなものを作成する。普段CALayerを利用して何かやる機会が少ないので、ちょっと久しぶりにいじってみたくなった次第。

UIKit gradetion animation · GitHub

UIViewはlayerというCALayerのプロパティを持っているが、これを派生タイプであるCAGradientLayerに置き換えたい。ただ、layerプロパティはget onlyであり、代入することができないため、今回はsublayerとしてCAGradientLayerを追加する。

//
//  GradientView.swift
//  GradientSample
//

import UIKit

class GradientView: UIView {
    var gradientLayer: CAGradientLayer! = nil
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }
    
    override class func awakeFromNib() {
        super.awakeFromNib()
    }
    
    private func setup() {
        gradientLayer = CAGradientLayer()
        
        gradientLayer.frame = CGRect(
            x: 0,
            y: 0,
            width: layer.bounds.width,
            height: layer.bounds.height
        )
        
        gradientLayer.colors = [
            UIColor.orange.cgColor,
            UIColor.blue.cgColor,
        ]
                
        gradientLayer.startPoint = CGPoint(x: 0, y: 0)
        gradientLayer.endPoint = CGPoint(x: 1, y: 1)
        
        layer.addSublayer(gradientLayer)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        gradientLayer.frame = CGRect(x: 0, y: 0, width: layer.bounds.width, height: layer.bounds.height)
    }
}

以下のようになる。ちなみにCAGradiationLayerのstartPointプロパティとendPointプロパティとでグラデーションの方向が決まる。

これをループするアニメーションにしていく。 とりあえず「1周期分のアニメーションを作成して、それを無限ループさせる」という考えてやっていく。 ここでの「1周期分のアニメーション」は、「左上のオレンジの部分が右下を通り過ぎ、再度左上に出てくるまで」ということである。

CALayerをアニメーションさせるにはCAAnimationを利用する。CAAnimation自体のインスタンスを生成は普通はしないため、今回はその派生タイプであるCABasicAnimationというのを利用し、アニメーションを作成する。

それぞれの色のグラデーションの終了点を表すlocationsプロパティでどこまでグラデーションをかけるのかが決まり、このプロパティをCAAnimationで徐々に変動させることでアニメーションにする。

、今回の場合はグラデーションに使う色を「橙、青、橙、青」を1列に用意しておき、その4つのlocationを短調増加させるだけで1ループ分のアニメーションとした。

わかりづらくて申し訳ないとは思う

これを実現するためにsetup関数の中身を変える。

    private func setup() {
        gradientLayer = CAGradientLayer()
        
        gradientLayer.frame = CGRect(
            x: 0,
            y: 0,
            width: layer.bounds.width,
            height: layer.bounds.height
        )
        
        gradientLayer.colors = [
            UIColor.orange.cgColor,
            UIColor.blue.cgColor,
            UIColor.orange.cgColor,
            UIColor.blue.cgColor,
        ]
        
        gradientLayer.locations = [
            -2,
            -1,
            0,
            1,
        ]
        
        gradientLayer.startPoint = CGPoint(x: 0, y: 0)
        gradientLayer.endPoint = CGPoint(x: 1, y: 1)
        
        layer.addSublayer(gradientLayer)
        
        let animation = CABasicAnimation(keyPath: "locations")
        animation.fromValue = [-2, -1, 0, 1]
        animation.toValue = [0, 1, 2, 3]
        animation.duration = 5
        gradientLayer.add(animation, forKey: nil)
    }

これで1ループ分のアニメーションができた。

あとはこれを無限ループさせるようにする。 CABasicAnimationにループ回数を指定するためのプロパティがあるので、これを無限回に指定すれば良い。

……setupの中身
        let animation = CABasicAnimation(keyPath: "locations")
        animation.fromValue = [-2, -1, 0, 1]
        animation.toValue = [0, 1, 2, 3]
        animation.duration = 5
        animation.repeatCount = .infinity // これ
        gradientLayer.add(animation, forKey: nil)
      }

感想

subLayerにCAGradientLayerを追加したが、そもそもUIViewのlayerClassを変更すれば良かったのではと思った。まああとの祭りやね。

Core Animation Basicsを読んだ。

運が良いことにこれまで複雑なViewのアニメーションやらの実装に遭遇することなく今まで仕事をして来れたので、「そういえばCALayer関連の基礎知識が抜けているなあ」と思ったので以下のDocumentを読んで、知らなかったことを書いていく。

developer.apple.com

Core AnimationはViewのコンテンツをbitmapに変換している

UIKitでは、描画するコンテンツとアニメーションの情報はCore AnimationというFrameworkで管理・操作している。 このFrameworkの内部で「Viewに表示するContentをbitmapとして保持して、指定の操作をbitmapに施し、実際にViewに表示する」ということをしている。

この時、bitmapについての情報を保持しているオブジェクトがCALayerであり、ユーザはこのCALayerに対して命令を出したりすることになる。(実際にはUIViewがCALayerのラッパーの役割をしており、簡単な操作なら直接CALayerを操作する必要はない)

このCALayerを介してbitmapを操作すると、Core Animationはグラフィック専用のHardwareを使って処理を行うらしく、たとえばViewを何十回も生成し直してアニメーションを作ったりするよりも、CALayerを介して「アニメーション時にパラメタの最終値と変化の流れ」を指定してあげる手法でアニメーションを作成する方が効率が良い。

CALayerは実際は3次元空間を想定している

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/Art/transform_basic_math_2x.png

座標操作時、CALayerでは以上のような行列式で(x, y, z)の値を変化させている。普段意識していなかったが、画像のようにz座標に対しての変換もCALayerでは想定している。

目的別の3つのLayer setがある

Core Animationで3つのLayer setを保持している。

  • model layer tree
    • アニメーションの終着点を保持しているLayer。ユーザが「このようなアニメーションをしたい」と思った時、実際アニメーションに利用するパラメタの最終値を設定することになるが、その値を保持しているLayer set。
  • presentation tree
    • アニメーション途中におけるパラメタを持っているLayer set。(一応このアニメーション途中のパラメタは修正できるようだが、するべきでないと書かれている)
  • render tree
    • 実際にアニメーションが行われているLayer set。(このLayer setはCore Animation内でPrivateになっておりユーザから値を修正することはできない)

なぜ3層に分かれているのか、という記述が特になかったが、単純に「ユーザがアニメーションの設定をする層」「アニメーション中のパラメタを参照出来る層」「Core Animationで実際に操作し、ユーザに直にいじらせたくない層」で役割分担をした結果ではと思う。

感想

思えば、UIViewの「Viewのカドを丸くしたい」みたいな時にしかCALayerを直接いじるみたいなことはしてこなかった気がする。

また、CALayerが3次元空間を想定しているみたいなので、Paypayアプリのカードのフリップアニメーションも簡単に実装できるのかもなと思ったので、こちらはちょっとやってみたい。