diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml
index 3f89bd702..a1cc2af19 100644
--- a/.github/workflows/release-pr.yml
+++ b/.github/workflows/release-pr.yml
@@ -94,7 +94,7 @@ jobs:
- name: Build Ghostty.app
run: |
cd macos
- sudo xcode-select -s /Applications/Xcode_16.4.app
+ sudo xcode-select -s /Applications/Xcode_26.0.app
xcodebuild -target Ghostty -configuration Release
# We inject the "build number" as simply the number of commits since HEAD.
@@ -246,7 +246,7 @@ jobs:
- name: Build Ghostty.app
run: |
cd macos
- sudo xcode-select -s /Applications/Xcode_16.4.app
+ sudo xcode-select -s /Applications/Xcode_26.0.app
xcodebuild -target Ghostty -configuration Release
# We inject the "build number" as simply the number of commits since HEAD.
diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml
index 6c6399afd..2a3277ea6 100644
--- a/.github/workflows/release-tip.yml
+++ b/.github/workflows/release-tip.yml
@@ -173,7 +173,7 @@ jobs:
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: XCode Select
- run: sudo xcode-select -s /Applications/Xcode_16.4.app
+ run: sudo xcode-select -s /Applications/Xcode_26.0.app
# Setup Sparkle
- name: Setup Sparkle
@@ -388,7 +388,7 @@ jobs:
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: XCode Select
- run: sudo xcode-select -s /Applications/Xcode_16.4.app
+ run: sudo xcode-select -s /Applications/Xcode_26.0.app
# Setup Sparkle
- name: Setup Sparkle
@@ -563,7 +563,7 @@ jobs:
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: XCode Select
- run: sudo xcode-select -s /Applications/Xcode_16.4.app
+ run: sudo xcode-select -s /Applications/Xcode_26.0.app
# Setup Sparkle
- name: Setup Sparkle
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 814acec8f..4d09603f4 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -286,7 +286,7 @@ jobs:
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Xcode Select
- run: sudo xcode-select -s /Applications/Xcode_16.4.app
+ run: sudo xcode-select -s /Applications/Xcode_26.0.app
- name: get the Zig deps
id: deps
@@ -328,17 +328,6 @@ jobs:
- name: Xcode Select
run: sudo xcode-select -s /Applications/Xcode_26.0.app
- # TODO(tahoe):
- # https://developer.apple.com/documentation/xcode-release-notes/xcode-26-release-notes#Interface-Builder
- # We allow this step to fail because if our image already has
- # the workaround in place this will fail.
- - name: Xcode 26 Beta 17A5241e Metal Workaround
- continue-on-error: true
- run: |
- xcodebuild -downloadComponent metalToolchain -exportPath /tmp/MyMetalExport/
- sed -i '' -e 's/17A5241c/17A5241e/g' /tmp/MyMetalExport/MetalToolchain-17A5241c.exportedBundle/ExportMetadata.plist
- xcodebuild -importComponent metalToolchain -importPath /tmp/MyMetalExport/MetalToolchain-17A5241c.exportedBundle
-
- name: get the Zig deps
id: deps
run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT
@@ -377,7 +366,7 @@ jobs:
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Xcode Select
- run: sudo xcode-select -s /Applications/Xcode_16.4.app
+ run: sudo xcode-select -s /Applications/Xcode_26.0.app
- name: get the Zig deps
id: deps
@@ -695,7 +684,7 @@ jobs:
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Xcode Select
- run: sudo xcode-select -s /Applications/Xcode_16.4.app
+ run: sudo xcode-select -s /Applications/Xcode_26.0.app
- name: get the Zig deps
id: deps
diff --git a/.prettierignore b/.prettierignore
index 490538680..f131a5edc 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -11,6 +11,9 @@ zig-out/
# macos is managed by XCode GUI
macos/
+# produced by Icon Composer on macOS
+images/Ghostty.icon/icon.json
+
# website dev run
website/.next
diff --git a/README.md b/README.md
index d5c9dba02..b59964e61 100644
--- a/README.md
+++ b/README.md
@@ -224,6 +224,28 @@ macOS users don't require any additional dependencies.
> source tarballs, see the
> [website](http://ghostty.org/docs/install/build).
+### Xcode Version and SDKs
+
+Building the Ghostty macOS app requires that Xcode, the macOS SDK,
+and the iOS SDK are all installed.
+
+A common issue is that the incorrect version of Xcode is either
+installed or selected. Use the `xcode-select` command to
+ensure that the correct version of Xcode is selected:
+
+```shell-session
+sudo xcode-select --switch /Applications/Xcode-beta.app
+```
+
+> [!IMPORTANT]
+>
+> Main branch development of Ghostty is preparing for the next major
+> macOS release, Tahoe (macOS 26). Therefore, the main branch requires
+> **Xcode 26 and the macOS 26 SDK**.
+>
+> You do not need to be running on macOS 26 to build Ghostty, you can
+> still use Xcode 26 beta on macOS 15 stable.
+
### Linting
#### Prettier
diff --git a/build.zig.zon b/build.zig.zon
index fa071dbfe..e85958aaf 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -8,8 +8,8 @@
.libxev = .{
// mitchellh/libxev
- .url = "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz",
- .hash = "libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz",
+ .url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
+ .hash = "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3",
.lazy = true,
},
.vaxis = .{
@@ -103,8 +103,8 @@
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
- .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz",
- .hash = "N-V-__8AAMJWXQSVe0xzU_4UVRTDVERqnNpQU4wfS0FRGRRj",
+ .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz",
+ .hash = "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX",
.lazy = true,
},
},
diff --git a/build.zig.zon.json b/build.zig.zon.json
index ee2f14508..4217c17aa 100644
--- a/build.zig.zon.json
+++ b/build.zig.zon.json
@@ -54,20 +54,20 @@
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
},
- "N-V-__8AAMJWXQSVe0xzU_4UVRTDVERqnNpQU4wfS0FRGRRj": {
+ "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX": {
"name": "iterm2_themes",
- "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz",
- "hash": "sha256-TcrTZUCVetvAFGX0fBqg3zlG90flljScNr/OYR/MJ5Y="
+ "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz",
+ "hash": "sha256-C93MSyNgyB+uhvzMQETDXr7839hFyX7NfTMp4HUKs3U="
},
"N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": {
"name": "libpng",
"url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
"hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="
},
- "libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz": {
+ "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3": {
"name": "libxev",
- "url": "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz",
- "hash": "sha256-oKZqA9d79jHnp/HsqJWQE33Ffn5Ee5G4VnlQepQuY4o="
+ "url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
+ "hash": "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU="
},
"N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": {
"name": "libxml2",
diff --git a/build.zig.zon.nix b/build.zig.zon.nix
index e28a2a0dd..46345871b 100644
--- a/build.zig.zon.nix
+++ b/build.zig.zon.nix
@@ -170,11 +170,11 @@ in
};
}
{
- name = "N-V-__8AAMJWXQSVe0xzU_4UVRTDVERqnNpQU4wfS0FRGRRj";
+ name = "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX";
path = fetchZigArtifact {
name = "iterm2_themes";
- url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz";
- hash = "sha256-TcrTZUCVetvAFGX0fBqg3zlG90flljScNr/OYR/MJ5Y=";
+ url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz";
+ hash = "sha256-C93MSyNgyB+uhvzMQETDXr7839hFyX7NfTMp4HUKs3U=";
};
}
{
@@ -186,11 +186,11 @@ in
};
}
{
- name = "libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz";
+ name = "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3";
path = fetchZigArtifact {
name = "libxev";
- url = "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz";
- hash = "sha256-oKZqA9d79jHnp/HsqJWQE33Ffn5Ee5G4VnlQepQuY4o=";
+ url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz";
+ hash = "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU=";
};
}
{
diff --git a/build.zig.zon.txt b/build.zig.zon.txt
index 3335b9574..b7cb2772f 100644
--- a/build.zig.zon.txt
+++ b/build.zig.zon.txt
@@ -27,8 +27,8 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz
https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst
-https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz
-https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz
+https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz
+https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz
https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz
diff --git a/dist/macos/Ghostty.icns b/dist/macos/Ghostty.icns
deleted file mode 100644
index 44a44711a..000000000
Binary files a/dist/macos/Ghostty.icns and /dev/null differ
diff --git a/dist/macos/Info.plist b/dist/macos/Info.plist
deleted file mode 100644
index 8283cc529..000000000
--- a/dist/macos/Info.plist
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
- CFBundleExecutable
- ghostty
- CFBundleIdentifier
- com.mitchellh.ghostty
- CFBundleName
- Ghostty
- CFBundleDisplayName
- Ghostty
- CFBundleIconFile
- Ghostty.icns
-
-
-
diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json
index fb032fe82..32bd8bd54 100644
--- a/flatpak/zig-packages.json
+++ b/flatpak/zig-packages.json
@@ -67,9 +67,9 @@
},
{
"type": "archive",
- "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz",
- "dest": "vendor/p/N-V-__8AAMJWXQSVe0xzU_4UVRTDVERqnNpQU4wfS0FRGRRj",
- "sha256": "4dcad36540957adbc01465f47c1aa0df3946f747e596349c36bfce611fcc2796"
+ "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz",
+ "dest": "vendor/p/N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX",
+ "sha256": "0bddcc4b2360c81fae86fccc4044c35ebefcdfd845c97ecd7d3329e0750ab375"
},
{
"type": "archive",
@@ -79,9 +79,9 @@
},
{
"type": "archive",
- "url": "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz",
- "dest": "vendor/p/libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz",
- "sha256": "a0a66a03d77bf631e7a7f1eca89590137dc57e7e447b91b85679507a942e638a"
+ "url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
+ "dest": "vendor/p/libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3",
+ "sha256": "570141c83a29b6a88de5492894543b4db461c0c11145e0fd12bda98f14c26085"
},
{
"type": "archive",
diff --git a/images/Ghostty.icon/Assets/Ghostty.png b/images/Ghostty.icon/Assets/Ghostty.png
new file mode 100644
index 000000000..49795c006
Binary files /dev/null and b/images/Ghostty.icon/Assets/Ghostty.png differ
diff --git a/images/Ghostty.icon/Assets/Inner Bevel 6px.png b/images/Ghostty.icon/Assets/Inner Bevel 6px.png
new file mode 100644
index 000000000..678193779
Binary files /dev/null and b/images/Ghostty.icon/Assets/Inner Bevel 6px.png differ
diff --git a/images/Ghostty.icon/Assets/Screen Effects.png b/images/Ghostty.icon/Assets/Screen Effects.png
new file mode 100644
index 000000000..0af7d3338
Binary files /dev/null and b/images/Ghostty.icon/Assets/Screen Effects.png differ
diff --git a/images/Ghostty.icon/Assets/Screen.png b/images/Ghostty.icon/Assets/Screen.png
new file mode 100644
index 000000000..2023b6ffa
Binary files /dev/null and b/images/Ghostty.icon/Assets/Screen.png differ
diff --git a/images/Ghostty.icon/Assets/gloss.png b/images/Ghostty.icon/Assets/gloss.png
new file mode 100644
index 000000000..f11196010
Binary files /dev/null and b/images/Ghostty.icon/Assets/gloss.png differ
diff --git a/images/Ghostty.icon/icon.json b/images/Ghostty.icon/icon.json
new file mode 100644
index 000000000..b29c9d81f
--- /dev/null
+++ b/images/Ghostty.icon/icon.json
@@ -0,0 +1,170 @@
+{
+ "color-space-for-untagged-svg-colors" : "display-p3",
+ "fill" : {
+ "linear-gradient" : [
+ "display-p3:0.87945,0.87945,0.87945,1.00000",
+ "display-p3:0.40000,0.40000,0.40392,1.00000"
+ ]
+ },
+ "groups" : [
+ {
+ "blend-mode" : "normal",
+ "layers" : [
+ {
+ "blend-mode" : "overlay",
+ "fill" : {
+ "linear-gradient" : [
+ "srgb:1.00000,1.00000,1.00000,1.00000",
+ "srgb:0.00000,0.00000,0.00000,1.00000"
+ ]
+ },
+ "hidden" : false,
+ "image-name" : "gloss.png",
+ "name" : "GlossTop",
+ "opacity" : 0.25,
+ "position" : {
+ "scale" : 0.98,
+ "translation-in-points" : [
+ 0.90625,
+ -236.4609375
+ ]
+ }
+ },
+ {
+ "blend-mode" : "normal",
+ "fill" : "automatic",
+ "hidden" : false,
+ "image-name" : "gloss.png",
+ "name" : "gloss",
+ "position" : {
+ "scale" : 0.98,
+ "translation-in-points" : [
+ 0.90625,
+ -236.4609375
+ ]
+ }
+ }
+ ],
+ "lighting" : "individual",
+ "name" : "Group 4",
+ "shadow" : {
+ "kind" : "neutral",
+ "opacity" : 0.5
+ },
+ "translucency" : {
+ "enabled" : true,
+ "value" : 0.5
+ }
+ },
+ {
+ "blend-mode" : "overlay",
+ "layers" : [
+ {
+ "blend-mode" : "overlay",
+ "fill" : "automatic",
+ "glass" : false,
+ "hidden" : false,
+ "image-name" : "Screen Effects.png",
+ "name" : "Screen Effects"
+ },
+ {
+ "blend-mode" : "overlay",
+ "fill" : "automatic",
+ "glass" : true,
+ "hidden" : false,
+ "image-name" : "Screen Effects.png",
+ "name" : "Screen Effects"
+ }
+ ],
+ "lighting" : "individual",
+ "name" : "Group 3",
+ "shadow" : {
+ "kind" : "neutral",
+ "opacity" : 0.5
+ },
+ "translucency" : {
+ "enabled" : false,
+ "value" : 0.5
+ }
+ },
+ {
+ "blur-material" : null,
+ "layers" : [
+ {
+ "blend-mode" : "normal",
+ "fill" : "automatic",
+ "hidden" : false,
+ "image-name" : "Ghostty.png",
+ "name" : "Ghostty",
+ "position" : {
+ "scale" : 1,
+ "translation-in-points" : [
+ -185.015625,
+ -143.8359375
+ ]
+ }
+ },
+ {
+ "blend-mode" : "normal",
+ "fill" : {
+ "solid" : "extended-srgb:0.00000,0.47843,1.00000,1.00000"
+ },
+ "glass" : true,
+ "hidden" : false,
+ "image-name" : "Ghostty.png",
+ "name" : "GhosttyBlur",
+ "position" : {
+ "scale" : 1,
+ "translation-in-points" : [
+ -186.59375,
+ -143.8359375
+ ]
+ }
+ },
+ {
+ "hidden" : false,
+ "image-name" : "Screen.png",
+ "name" : "Screen"
+ }
+ ],
+ "lighting" : "individual",
+ "name" : "Group 2",
+ "shadow" : {
+ "kind" : "neutral",
+ "opacity" : 0.5
+ },
+ "translucency" : {
+ "enabled" : false,
+ "value" : 0.5
+ }
+ },
+ {
+ "blend-mode" : "normal",
+ "blur-material" : null,
+ "hidden" : false,
+ "layers" : [
+ {
+ "image-name" : "Inner Bevel 6px.png",
+ "name" : "Inner Bevel 6px"
+ }
+ ],
+ "lighting" : "individual",
+ "name" : "Group 1",
+ "shadow" : {
+ "kind" : "layer-color",
+ "opacity" : 0.2
+ },
+ "specular" : false,
+ "translucency" : {
+ "enabled" : false,
+ "value" : 0.5
+ }
+ }
+ ],
+ "supported-platforms" : {
+ "circles" : [
+ "watchOS"
+ ],
+ "squares" : "shared"
+ }
+}
\ No newline at end of file
diff --git a/images/icons/icon_1024.png b/images/icons/icon_1024.png
index a0b716c87..22361edcb 100644
Binary files a/images/icons/icon_1024.png and b/images/icons/icon_1024.png differ
diff --git a/images/icons/icon_1024@2x.png b/images/icons/icon_1024@2x.png
new file mode 100644
index 000000000..22361edcb
Binary files /dev/null and b/images/icons/icon_1024@2x.png differ
diff --git a/images/icons/icon_128.png b/images/icons/icon_128.png
index bad0eb891..317ad9f0f 100644
Binary files a/images/icons/icon_128.png and b/images/icons/icon_128.png differ
diff --git a/images/icons/icon_256.png b/images/icons/icon_256.png
index 803224416..9988ac11e 100644
Binary files a/images/icons/icon_256.png and b/images/icons/icon_256.png differ
diff --git a/images/icons/icon_256@2x.png b/images/icons/icon_256@2x.png
index b51b8d7dc..9988ac11e 100644
Binary files a/images/icons/icon_256@2x.png and b/images/icons/icon_256@2x.png differ
diff --git a/images/icons/icon_512.png b/images/icons/icon_512.png
index b51b8d7dc..759511f68 100644
Binary files a/images/icons/icon_512.png and b/images/icons/icon_512.png differ
diff --git a/images/icons/icon_512@2x.png b/images/icons/icon_512@2x.png
new file mode 100644
index 000000000..759511f68
Binary files /dev/null and b/images/icons/icon_512@2x.png differ
diff --git a/include/ghostty.h b/include/ghostty.h
index fc2c915cb..181f7b7f8 100644
--- a/include/ghostty.h
+++ b/include/ghostty.h
@@ -385,6 +385,11 @@ typedef struct {
bool rectangle;
} ghostty_selection_s;
+typedef struct {
+ const char* key;
+ const char* value;
+} ghostty_env_var_s;
+
typedef struct {
void* nsview;
} ghostty_platform_macos_s;
@@ -406,6 +411,9 @@ typedef struct {
float font_size;
const char* working_directory;
const char* command;
+ ghostty_env_var_s* env_vars;
+ size_t env_var_count;
+ const char* initial_input;
} ghostty_surface_config_s;
typedef struct {
@@ -807,7 +815,8 @@ void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e);
ghostty_surface_config_s ghostty_surface_config_new();
-ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*);
+ghostty_surface_t ghostty_surface_new(ghostty_app_t,
+ const ghostty_surface_config_s*);
void ghostty_surface_free(ghostty_surface_t);
void* ghostty_surface_userdata(ghostty_surface_t);
ghostty_app_t ghostty_surface_app(ghostty_surface_t);
diff --git a/macos/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index 9c6bc2e81..000000000
--- a/macos/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,74 +0,0 @@
-{
- "images" : [
- {
- "filename" : "macOS-AppIcon-1024px.png",
- "idiom" : "universal",
- "platform" : "ios",
- "size" : "1024x1024"
- },
- {
- "filename" : "macOS-AppIcon-16px-16pt@1x.png",
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "16x16"
- },
- {
- "filename" : "macOS-AppIcon-32px-16pt@2x.png",
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "16x16"
- },
- {
- "filename" : "macOS-AppIcon-32px-32pt@1x.png",
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "32x32"
- },
- {
- "filename" : "macOS-AppIcon-64px-32pt@2x.png",
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "32x32"
- },
- {
- "filename" : "macOS-AppIcon-128px-128pt@1x.png",
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "128x128"
- },
- {
- "filename" : "macOS-AppIcon-256px-128pt@2x.png",
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "128x128"
- },
- {
- "filename" : "macOS-AppIcon-256px-128pt@2x 1.png",
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "256x256"
- },
- {
- "filename" : "macOS-AppIcon-512px-256pt@2x.png",
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "256x256"
- },
- {
- "filename" : "macOS-AppIcon-512px.png",
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "512x512"
- },
- {
- "filename" : "macOS-AppIcon-1024px 1.png",
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "512x512"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-1024px 1.png b/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-1024px 1.png
deleted file mode 100644
index a0b716c87..000000000
Binary files a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-1024px 1.png and /dev/null differ
diff --git a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-1024px.png b/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-1024px.png
deleted file mode 100644
index a0b716c87..000000000
Binary files a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-1024px.png and /dev/null differ
diff --git a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-128px-128pt@1x.png b/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-128px-128pt@1x.png
deleted file mode 100644
index bad0eb891..000000000
Binary files a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-128px-128pt@1x.png and /dev/null differ
diff --git a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-16px-16pt@1x.png b/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-16px-16pt@1x.png
deleted file mode 100644
index cacff7a54..000000000
Binary files a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-16px-16pt@1x.png and /dev/null differ
diff --git a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-256px-128pt@2x 1.png b/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-256px-128pt@2x 1.png
deleted file mode 100644
index 46c3f7050..000000000
Binary files a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-256px-128pt@2x 1.png and /dev/null differ
diff --git a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-256px-128pt@2x.png b/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-256px-128pt@2x.png
deleted file mode 100644
index 46c3f7050..000000000
Binary files a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-256px-128pt@2x.png and /dev/null differ
diff --git a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-32px-16pt@2x.png b/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-32px-16pt@2x.png
deleted file mode 100644
index c8011a605..000000000
Binary files a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-32px-16pt@2x.png and /dev/null differ
diff --git a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-32px-32pt@1x.png b/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-32px-32pt@1x.png
deleted file mode 100644
index 5e68d5fd0..000000000
Binary files a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-32px-32pt@1x.png and /dev/null differ
diff --git a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-512px-256pt@2x.png b/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-512px-256pt@2x.png
deleted file mode 100644
index b51b8d7dc..000000000
Binary files a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-512px-256pt@2x.png and /dev/null differ
diff --git a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-512px.png b/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-512px.png
deleted file mode 100644
index f302b40bb..000000000
Binary files a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-512px.png and /dev/null differ
diff --git a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-64px-32pt@2x.png b/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-64px-32pt@2x.png
deleted file mode 100644
index e394a5170..000000000
Binary files a/macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-64px-32pt@2x.png and /dev/null differ
diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj
index a5663202b..cf806c7bd 100644
--- a/macos/Ghostty.xcodeproj/project.pbxproj
+++ b/macos/Ghostty.xcodeproj/project.pbxproj
@@ -13,6 +13,11 @@
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; };
+ A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A511940E2E050590007258CC /* CloseTerminalIntent.swift */; };
+ A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194102E05A480007258CC /* QuickTerminalIntent.swift */; };
+ A51194132E05D006007258CC /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194122E05D003007258CC /* Optional+Extension.swift */; };
+ A51194172E05D964007258CC /* PermissionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194162E05D95E007258CC /* PermissionRequest.swift */; };
+ A51194192E05DFC4007258CC /* IntentPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194182E05DFBB007258CC /* IntentPermission.swift */; };
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
@@ -53,7 +58,10 @@
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */; };
A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */; };
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; };
- A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
+ A553F4062E05E93000257779 /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194122E05D003007258CC /* Optional+Extension.swift */; };
+ A553F4072E05E93D00257779 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366A2DF0A98900E04A10 /* Array+Extension.swift */; };
+ A553F4132E06EB1600257779 /* Ghostty.icon in Resources */ = {isa = PBXBuildFile; fileRef = A553F4122E06EB1600257779 /* Ghostty.icon */; };
+ A553F4142E06EB1600257779 /* Ghostty.icon in Resources */ = {isa = PBXBuildFile; fileRef = A553F4122E06EB1600257779 /* Ghostty.icon */; };
A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */; };
A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */; };
A5593FE32DF8D78600B47B10 /* TerminalHiddenTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */; };
@@ -119,6 +127,18 @@
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; };
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; };
+ A5E4082A2E022E9E0035FEAC /* TabGroupCloseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */; };
+ A5E4082E2E0237460035FEAC /* NewTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */; };
+ A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */; };
+ A5E408322E02FEDF0035FEAC /* TerminalEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */; };
+ A5E408342E0320140035FEAC /* GetTerminalDetailsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */; };
+ A5E408382E03C7DA0035FEAC /* Ghostty.Surface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */; };
+ A5E4083A2E0449BD0035FEAC /* Ghostty.Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */; };
+ A5E4083C2E044DB50035FEAC /* Ghostty.Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */; };
+ A5E408402E04532C0035FEAC /* CommandEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083F2E04532A0035FEAC /* CommandEntity.swift */; };
+ A5E408432E047D0B0035FEAC /* CommandPaletteIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */; };
+ A5E408452E0483FD0035FEAC /* KeybindIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408442E0483F80035FEAC /* KeybindIntent.swift */; };
+ A5E408472E04852B0035FEAC /* InputIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408462E0485270035FEAC /* InputIntent.swift */; };
A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; };
AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; };
C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; };
@@ -138,6 +158,11 @@
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; };
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; };
A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; };
+ A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = ""; };
+ A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = ""; };
+ A51194122E05D003007258CC /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = ""; };
+ A51194162E05D95E007258CC /* PermissionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionRequest.swift; sourceTree = ""; };
+ A51194182E05DFBB007258CC /* IntentPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentPermission.swift; sourceTree = ""; };
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; };
A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarTabsTahoeTerminalWindow.swift; sourceTree = ""; };
A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebarTahoe.xib; sourceTree = ""; };
@@ -170,7 +195,7 @@
A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIcon.swift; sourceTree = ""; };
A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconImage.swift; sourceTree = ""; };
A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = ""; };
- A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; };
+ A553F4122E06EB1600257779 /* Ghostty.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; name = Ghostty.icon; path = ../images/Ghostty.icon; sourceTree = SOURCE_ROOT; };
A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = ""; };
A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenTitlebarTerminalWindow.swift; sourceTree = ""; };
A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalHiddenTitlebar.xib; sourceTree = ""; };
@@ -238,6 +263,18 @@
A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClipboardConfirmation.xib; sourceTree = ""; };
A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = ""; };
A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationView.swift; sourceTree = ""; };
+ A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabGroupCloseCoordinator.swift; sourceTree = ""; };
+ A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTerminalIntent.swift; sourceTree = ""; };
+ A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyIntentError.swift; sourceTree = ""; };
+ A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalEntity.swift; sourceTree = ""; };
+ A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTerminalDetailsIntent.swift; sourceTree = ""; };
+ A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Surface.swift; sourceTree = ""; };
+ A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Command.swift; sourceTree = ""; };
+ A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Error.swift; sourceTree = ""; };
+ A5E4083F2E04532A0035FEAC /* CommandEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandEntity.swift; sourceTree = ""; };
+ A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteIntent.swift; sourceTree = ""; };
+ A5E408442E0483F80035FEAC /* KeybindIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeybindIntent.swift; sourceTree = ""; };
+ A5E408462E0485270035FEAC /* InputIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputIntent.swift; sourceTree = ""; };
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = ""; };
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = ""; };
@@ -297,6 +334,7 @@
A56D58872ACDE6BE00508D2C /* Services */,
A59630982AEE1C4400D64628 /* Terminal */,
A5CBD05A2CA0C5910017A1AE /* QuickTerminal */,
+ A5E4082C2E0237270035FEAC /* App Intents */,
A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */,
A57D79252C9C8782001D522E /* Secure Input */,
A58636622DEF955100E04A10 /* Splits */,
@@ -320,12 +358,14 @@
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
+ A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
A58636702DF298F700E04A10 /* ExpiringUndoManager.swift */,
A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */,
A59630962AEE163600D64628 /* HostingWindow.swift */,
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */,
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */,
- A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
+ A51194162E05D95E007258CC /* PermissionRequest.swift */,
+ A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */,
A5CA378D2D31D6C100931030 /* Weak.swift */,
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */,
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */,
@@ -428,12 +468,14 @@
A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */,
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */,
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */,
+ A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */,
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */,
A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */,
+ A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */,
A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */,
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */,
A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */,
- A55685DF29A03A9F004303CE /* AppError.swift */,
+ A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */,
A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */,
A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */,
);
@@ -476,6 +518,7 @@
A586366E2DF25D8300E04A10 /* Duration+Extension.swift */,
A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */,
A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */,
+ A51194122E05D003007258CC /* Optional+Extension.swift */,
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */,
@@ -536,6 +579,7 @@
children = (
A571AB1C2A206FC600248498 /* Ghostty-Info.plist */,
A5B30538299BEAAB0047F10C /* Assets.xcassets */,
+ A553F4122E06EB1600257779 /* Ghostty.icon */,
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */,
A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */,
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */,
@@ -595,6 +639,32 @@
path = ClipboardConfirmation;
sourceTree = "";
};
+ A5E4082C2E0237270035FEAC /* App Intents */ = {
+ isa = PBXGroup;
+ children = (
+ A5E408412E0453370035FEAC /* Entities */,
+ A511940E2E050590007258CC /* CloseTerminalIntent.swift */,
+ A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */,
+ A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */,
+ A51194102E05A480007258CC /* QuickTerminalIntent.swift */,
+ A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */,
+ A5E408462E0485270035FEAC /* InputIntent.swift */,
+ A5E408442E0483F80035FEAC /* KeybindIntent.swift */,
+ A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */,
+ A51194182E05DFBB007258CC /* IntentPermission.swift */,
+ );
+ path = "App Intents";
+ sourceTree = "";
+ };
+ A5E408412E0453370035FEAC /* Entities */ = {
+ isa = PBXGroup;
+ children = (
+ A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */,
+ A5E4083F2E04532A0035FEAC /* CommandEntity.swift */,
+ );
+ path = Entities;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -682,6 +752,7 @@
buildActionMask = 2147483647;
files = (
FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */,
+ A553F4142E06EB1600257779 /* Ghostty.icon in Resources */,
A5593FE52DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib in Resources */,
29C15B1D2CDC3B2900520DD4 /* bat in Resources */,
A586167C2B7703CC009BDB1D /* fish in Resources */,
@@ -710,6 +781,7 @@
buildActionMask = 2147483647;
files = (
A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */,
+ A553F4132E06EB1600257779 /* Ghostty.icon in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -721,6 +793,7 @@
buildActionMask = 2147483647;
files = (
A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */,
+ A5E408432E047D0B0035FEAC /* CommandPaletteIntent.swift in Sources */,
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */,
@@ -730,17 +803,22 @@
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
+ A51194132E05D006007258CC /* Optional+Extension.swift in Sources */,
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
A586366F2DF25D8600E04A10 /* Duration+Extension.swift in Sources */,
A5CF66D42D289CEE00139794 /* NSEvent+Extension.swift in Sources */,
+ A5E408342E0320140035FEAC /* GetTerminalDetailsIntent.swift in Sources */,
A5CBD0642CA122E70017A1AE /* QuickTerminalPosition.swift in Sources */,
A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */,
+ A5E408322E02FEDF0035FEAC /* TerminalEntity.swift in Sources */,
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */,
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
+ A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */,
+ A5E408382E03C7DA0035FEAC /* Ghostty.Surface.swift in Sources */,
A5593FE72DF927D200B47B10 /* TransparentTitlebarTerminalWindow.swift in Sources */,
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */,
A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */,
@@ -749,6 +827,7 @@
A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */,
A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */,
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
+ A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */,
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */,
A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */,
A58636712DF298FB00E04A10 /* ExpiringUndoManager.swift in Sources */,
@@ -756,6 +835,8 @@
A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */,
A5CBD06B2CA322430017A1AE /* GlobalEventTap.swift in Sources */,
AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */,
+ A51194172E05D964007258CC /* PermissionRequest.swift in Sources */,
+ A51194192E05DFC4007258CC /* IntentPermission.swift in Sources */,
A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */,
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */,
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */,
@@ -769,29 +850,36 @@
A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */,
A5874D992DAD751B00E83852 /* CGS.swift in Sources */,
A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */,
+ A5E408472E04852B0035FEAC /* InputIntent.swift in Sources */,
A51544FE2DFB111C009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift in Sources */,
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */,
+ A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */,
+ A5E4083A2E0449BD0035FEAC /* Ghostty.Command.swift in Sources */,
+ A5E408452E0483FD0035FEAC /* KeybindIntent.swift in Sources */,
A5FEB3002ABB69450068369E /* main.swift in Sources */,
A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */,
+ A5E4082E2E0237460035FEAC /* NewTerminalIntent.swift in Sources */,
A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */,
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */,
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */,
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */,
+ A5E4083C2E044DB50035FEAC /* Ghostty.Error.swift in Sources */,
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */,
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */,
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */,
A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */,
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
- A55685E029A03A9F004303CE /* AppError.swift in Sources */,
A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */,
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */,
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */,
+ A5E408402E04532C0035FEAC /* CommandEntity.swift in Sources */,
+ A5E4082A2E022E9E0035FEAC /* TabGroupCloseCoordinator.swift in Sources */,
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
A53A29882DB69D2F00B6E02C /* TerminalCommandPalette.swift in Sources */,
A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */,
@@ -812,6 +900,7 @@
buildActionMask = 2147483647;
files = (
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */,
+ A553F4062E05E93000257779 /* Optional+Extension.swift in Sources */,
A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */,
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */,
A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */,
@@ -821,6 +910,7 @@
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */,
A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */,
A5985CD82C320C4500C57AD3 /* String+Extension.swift in Sources */,
+ A553F4072E05E93D00257779 /* Array+Extension.swift in Sources */,
C159E89D2B69A2EF00FDFE9C /* OSColor+Extension.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -886,7 +976,7 @@
3B39CAA32B33946300DABEB8 /* ReleaseLocal */ = {
isa = XCBuildConfiguration;
buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
@@ -1056,7 +1146,7 @@
A5B30541299BEAAB0047F10C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
@@ -1110,7 +1200,7 @@
A5B30542299BEAAB0047F10C /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
@@ -1163,7 +1253,7 @@
A5D449A82B53AE7B000F5B83 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
@@ -1202,7 +1292,7 @@
A5D449A92B53AE7B000F5B83 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
@@ -1241,7 +1331,7 @@
A5D449AA2B53AE7B000F5B83 /* ReleaseLocal */ = {
isa = XCBuildConfiguration;
buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift
index c56d7c3ac..734fcbc20 100644
--- a/macos/Sources/App/macOS/AppDelegate.swift
+++ b/macos/Sources/App/macOS/AppDelegate.swift
@@ -92,7 +92,10 @@ class AppDelegate: NSObject,
lazy var undoManager = ExpiringUndoManager()
/// Our quick terminal. This starts out uninitialized and only initializes if used.
- private var quickController: QuickTerminalController? = nil
+ private(set) lazy var quickController = QuickTerminalController(
+ ghostty,
+ position: derivedConfig.quickTerminalPosition
+ )
/// Manages updates
let updaterController: SPUStandardUpdaterController
@@ -167,7 +170,7 @@ class AppDelegate: NSObject,
// This registers the Ghostty => Services menu to exist.
NSApp.servicesMenu = menuServices
-
+
// Setup a local event monitor for app-level keyboard shortcuts. See
// localEventHandler for more info why.
_ = NSEvent.addLocalMonitorForEvents(
@@ -286,7 +289,7 @@ class AppDelegate: NSObject,
// NOTE(mitchellh): I don't think we need this check at all anymore. I'm keeping it
// here because I don't want to remove it in a patch release cycle but we should
// target removing it soon.
- if (self.quickController == nil && windows.allSatisfy { !$0.isVisible }) {
+ if (windows.allSatisfy { !$0.isVisible }) {
return .terminateNow
}
@@ -381,10 +384,17 @@ class AppDelegate: NSObject,
config.workingDirectory = filename
_ = TerminalController.newTab(ghostty, withBaseConfig: config)
} else {
- // When opening a file, open a new window with that file as the command,
- // and its parent directory as the working directory.
- config.command = filename
+ // When opening a file, we want to execute the file. To do this, we
+ // don't override the command directly, because it won't load the
+ // profile/rc files for the shell, which is super important on macOS
+ // due to things like Homebrew. Instead, we set the command to
+ // `; exit` which is what Terminal and iTerm2 do.
+ config.initialInput = "\(filename); exit\n"
+
+ // Set the parent directory to our working directory so that relative
+ // paths in scripts work.
config.workingDirectory = (filename as NSString).deletingLastPathComponent
+
_ = TerminalController.newWindow(ghostty, withBaseConfig: config)
}
@@ -919,14 +929,6 @@ class AppDelegate: NSObject,
}
@IBAction func toggleQuickTerminal(_ sender: Any) {
- if quickController == nil {
- quickController = QuickTerminalController(
- ghostty,
- position: derivedConfig.quickTerminalPosition
- )
- }
-
- guard let quickController = self.quickController else { return }
quickController.toggle()
}
diff --git a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift
new file mode 100644
index 000000000..923d22c97
--- /dev/null
+++ b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift
@@ -0,0 +1,35 @@
+import AppKit
+import AppIntents
+import GhosttyKit
+
+struct CloseTerminalIntent: AppIntent {
+ static var title: LocalizedStringResource = "Close Terminal"
+ static var description = IntentDescription("Close an existing terminal.")
+
+ @Parameter(
+ title: "Terminal",
+ description: "The terminal to close.",
+ )
+ var terminal: TerminalEntity
+
+ @available(macOS 26.0, *)
+ static var supportedModes: IntentModes = .background
+
+ @MainActor
+ func perform() async throws -> some IntentResult {
+ guard await requestIntentPermission() else {
+ throw GhosttyIntentError.permissionDenied
+ }
+
+ guard let surfaceView = terminal.surfaceView else {
+ throw GhosttyIntentError.surfaceNotFound
+ }
+
+ guard let controller = surfaceView.window?.windowController as? BaseTerminalController else {
+ return .result()
+ }
+
+ controller.closeSurface(surfaceView, withConfirmation: false)
+ return .result()
+ }
+}
diff --git a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift
new file mode 100644
index 000000000..fa983054b
--- /dev/null
+++ b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift
@@ -0,0 +1,38 @@
+import AppKit
+import AppIntents
+
+/// App intent that invokes a command palette entry.
+@available(macOS 14.0, *)
+struct CommandPaletteIntent: AppIntent {
+ static var title: LocalizedStringResource = "Invoke Command Palette Action"
+
+ @Parameter(
+ title: "Terminal",
+ description: "The terminal to base available commands from."
+ )
+ var terminal: TerminalEntity
+
+ @Parameter(
+ title: "Command",
+ description: "The command to invoke.",
+ optionsProvider: CommandQuery()
+ )
+ var command: CommandEntity
+
+ @available(macOS 26.0, *)
+ static var supportedModes: IntentModes = .background
+
+ @MainActor
+ func perform() async throws -> some IntentResult & ReturnsValue {
+ guard await requestIntentPermission() else {
+ throw GhosttyIntentError.permissionDenied
+ }
+
+ guard let surface = terminal.surfaceModel else {
+ throw GhosttyIntentError.surfaceNotFound
+ }
+
+ let performed = surface.perform(action: command.action)
+ return .result(value: performed)
+ }
+}
diff --git a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift
new file mode 100644
index 000000000..f7abcc6de
--- /dev/null
+++ b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift
@@ -0,0 +1,128 @@
+import AppIntents
+
+// MARK: AppEntity
+
+@available(macOS 14.0, *)
+struct CommandEntity: AppEntity {
+ let id: ID
+
+ // Note: for macOS 26 we can move all the properties to @ComputedProperty.
+
+ @Property(title: "Title")
+ var title: String
+
+ @Property(title: "Description")
+ var description: String
+
+ @Property(title: "Action")
+ var action: String
+
+ /// The underlying data model
+ let command: Ghostty.Command
+
+ /// A command identifier is a composite key based on the terminal and action.
+ struct ID: Hashable {
+ let terminalId: TerminalEntity.ID
+ let actionKey: String
+
+ init(terminalId: TerminalEntity.ID, actionKey: String) {
+ self.terminalId = terminalId
+ self.actionKey = actionKey
+ }
+ }
+
+ static var typeDisplayRepresentation: TypeDisplayRepresentation {
+ TypeDisplayRepresentation(name: "Command Palette Command")
+ }
+
+ var displayRepresentation: DisplayRepresentation {
+ DisplayRepresentation(
+ title: LocalizedStringResource(stringLiteral: command.title),
+ subtitle: LocalizedStringResource(stringLiteral: command.description),
+ )
+ }
+
+ static var defaultQuery = CommandQuery()
+
+ init(_ command: Ghostty.Command, for terminal: TerminalEntity) {
+ self.id = .init(terminalId: terminal.id, actionKey: command.actionKey)
+ self.command = command
+ self.title = command.title
+ self.description = command.description
+ self.action = command.action
+ }
+}
+
+@available(macOS 14.0, *)
+extension CommandEntity.ID: RawRepresentable {
+ var rawValue: String {
+ return "\(terminalId):\(actionKey)"
+ }
+
+ init?(rawValue: String) {
+ let components = rawValue.split(separator: ":", maxSplits: 1)
+ guard components.count == 2 else { return nil }
+
+ guard let terminalId = TerminalEntity.ID(uuidString: String(components[0])) else {
+ return nil
+ }
+
+ self.terminalId = terminalId
+ self.actionKey = String(components[1])
+ }
+}
+
+// Required by AppEntity
+@available(macOS 14.0, *)
+extension CommandEntity.ID: EntityIdentifierConvertible {
+ static func entityIdentifier(for entityIdentifierString: String) -> CommandEntity.ID? {
+ .init(rawValue: entityIdentifierString)
+ }
+
+ var entityIdentifierString: String {
+ rawValue
+ }
+}
+
+// MARK: EntityQuery
+
+@available(macOS 14.0, *)
+struct CommandQuery: EntityQuery {
+ // Inject our terminal parameter from our command palette intent.
+ @IntentParameterDependency(\.$terminal)
+ var commandPaletteIntent
+
+ @MainActor
+ func entities(for identifiers: [CommandEntity.ID]) async throws -> [CommandEntity] {
+ // Extract unique terminal IDs to avoid fetching duplicates
+ let terminalIds = Set(identifiers.map(\.terminalId))
+ let terminals = try await TerminalEntity.defaultQuery.entities(for: Array(terminalIds))
+
+ // Build a cache of terminals and their available commands
+ // This avoids repeated command fetching for the same terminal
+ typealias Tuple = (terminal: TerminalEntity, commands: [Ghostty.Command])
+ let commandMap: [TerminalEntity.ID: Tuple] =
+ terminals.reduce(into: [:]) { result, terminal in
+ guard let commands = try? terminal.surfaceModel?.commands() else { return }
+ result[terminal.id] = (terminal: terminal, commands: commands)
+ }
+
+ // Map each identifier to its corresponding CommandEntity. If a command doesn't
+ // exist it maps to nil and is removed via compactMap.
+ return identifiers.compactMap { id in
+ guard let (terminal, commands) = commandMap[id.terminalId],
+ let command = commands.first(where: { $0.actionKey == id.actionKey }) else {
+ return nil
+ }
+
+ return CommandEntity(command, for: terminal)
+ }
+ }
+
+ @MainActor
+ func suggestedEntities() async throws -> [CommandEntity] {
+ guard let terminal = commandPaletteIntent?.terminal,
+ let surface = terminal.surfaceModel else { return [] }
+ return try surface.commands().map { CommandEntity($0, for: terminal) }
+ }
+}
diff --git a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift
new file mode 100644
index 000000000..e29fbba3f
--- /dev/null
+++ b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift
@@ -0,0 +1,139 @@
+import AppKit
+import AppIntents
+import SwiftUI
+
+struct TerminalEntity: AppEntity {
+ let id: UUID
+
+ @Property(title: "Title")
+ var title: String
+
+ @Property(title: "Working Directory")
+ var workingDirectory: String?
+
+ @Property(title: "Kind")
+ var kind: Kind
+
+ @MainActor
+ @DeferredProperty(title: "Full Contents")
+ @available(macOS 26.0, *)
+ var screenContents: String? {
+ get async {
+ guard let surfaceView else { return nil }
+ return surfaceView.cachedScreenContents.get()
+ }
+ }
+
+ @MainActor
+ @DeferredProperty(title: "Visible Contents")
+ @available(macOS 26.0, *)
+ var visibleContents: String? {
+ get async {
+ guard let surfaceView else { return nil }
+ return surfaceView.cachedVisibleContents.get()
+ }
+ }
+
+ var screenshot: Image?
+
+ static var typeDisplayRepresentation: TypeDisplayRepresentation {
+ TypeDisplayRepresentation(name: "Terminal")
+ }
+
+ @MainActor
+ var displayRepresentation: DisplayRepresentation {
+ var rep = DisplayRepresentation(title: "\(title)")
+ if let screenshot,
+ let nsImage = ImageRenderer(content: screenshot).nsImage,
+ let data = nsImage.tiffRepresentation {
+ rep.image = .init(data: data)
+ }
+
+ return rep
+ }
+
+ /// Returns the view associated with this entity. This may no longer exist.
+ @MainActor
+ var surfaceView: Ghostty.SurfaceView? {
+ Self.defaultQuery.all.first { $0.uuid == self.id }
+ }
+
+ @MainActor
+ var surfaceModel: Ghostty.Surface? {
+ surfaceView?.surfaceModel
+ }
+
+ static var defaultQuery = TerminalQuery()
+
+ init(_ view: Ghostty.SurfaceView) {
+ self.id = view.uuid
+ self.title = view.title
+ self.workingDirectory = view.pwd
+ self.screenshot = view.screenshot()
+
+ // Determine the kind based on the window controller type
+ if view.window?.windowController is QuickTerminalController {
+ self.kind = .quick
+ } else {
+ self.kind = .normal
+ }
+ }
+}
+
+extension TerminalEntity {
+ enum Kind: String, AppEnum {
+ case normal
+ case quick
+
+ static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Kind")
+
+ static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [
+ .normal: .init(title: "Normal"),
+ .quick: .init(title: "Quick")
+ ]
+ }
+}
+
+struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery {
+ @MainActor
+ func entities(for identifiers: [TerminalEntity.ID]) async throws -> [TerminalEntity] {
+ return all.filter {
+ identifiers.contains($0.uuid)
+ }.map {
+ TerminalEntity($0)
+ }
+ }
+
+ @MainActor
+ func entities(matching string: String) async throws -> [TerminalEntity] {
+ return all.filter {
+ $0.title.localizedCaseInsensitiveContains(string)
+ }.map {
+ TerminalEntity($0)
+ }
+ }
+
+ @MainActor
+ func allEntities() async throws -> [TerminalEntity] {
+ return all.map { TerminalEntity($0) }
+ }
+
+ @MainActor
+ func suggestedEntities() async throws -> [TerminalEntity] {
+ return try await allEntities()
+ }
+
+ @MainActor
+ var all: [Ghostty.SurfaceView] {
+ // Find all of our terminal windows. This will include the quick terminal
+ // but only if it was previously opened.
+ let controllers = NSApp.windows.compactMap {
+ $0.windowController as? BaseTerminalController
+ }
+
+ // Get all our surfaces
+ return controllers.flatMap {
+ $0.surfaceTree.root?.leaves() ?? []
+ }
+ }
+}
diff --git a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift
new file mode 100644
index 000000000..1cbaa9d68
--- /dev/null
+++ b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift
@@ -0,0 +1,69 @@
+import AppKit
+import AppIntents
+
+/// App intent that retrieves details about a specific terminal.
+struct GetTerminalDetailsIntent: AppIntent {
+ static var title: LocalizedStringResource = "Get Details of Terminal"
+
+ @Parameter(
+ title: "Detail",
+ description: "The detail to extract about a terminal."
+ )
+ var detail: TerminalDetail
+
+ @Parameter(
+ title: "Terminal",
+ description: "The terminal to extract information about."
+ )
+ var terminal: TerminalEntity
+
+ @available(macOS 26.0, *)
+ static var supportedModes: IntentModes = .background
+
+ static var parameterSummary: some ParameterSummary {
+ Summary("Get \(\.$detail) from \(\.$terminal)")
+ }
+
+ @MainActor
+ func perform() async throws -> some IntentResult & ReturnsValue {
+ guard await requestIntentPermission() else {
+ throw GhosttyIntentError.permissionDenied
+ }
+
+ switch detail {
+ case .title: return .result(value: terminal.title)
+ case .workingDirectory: return .result(value: terminal.workingDirectory)
+ case .allContents:
+ guard let view = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound }
+ return .result(value: view.cachedScreenContents.get())
+ case .selectedText:
+ guard let view = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound }
+ return .result(value: view.accessibilitySelectedText())
+ case .visibleText:
+ guard let view = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound }
+ return .result(value: view.cachedVisibleContents.get())
+ }
+ }
+}
+
+// MARK: TerminalDetail
+
+enum TerminalDetail: String {
+ case title
+ case workingDirectory
+ case allContents
+ case selectedText
+ case visibleText
+}
+
+extension TerminalDetail: AppEnum {
+ static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Detail")
+
+ static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [
+ .title: .init(title: "Title"),
+ .workingDirectory: .init(title: "Working Directory"),
+ .allContents: .init(title: "Full Contents"),
+ .selectedText: .init(title: "Selected Text"),
+ .visibleText: .init(title: "Visible Text"),
+ ]
+}
diff --git a/macos/Sources/Features/App Intents/GhosttyIntentError.swift b/macos/Sources/Features/App Intents/GhosttyIntentError.swift
new file mode 100644
index 000000000..c52b7a52e
--- /dev/null
+++ b/macos/Sources/Features/App Intents/GhosttyIntentError.swift
@@ -0,0 +1,13 @@
+enum GhosttyIntentError: Error, CustomLocalizedStringResourceConvertible {
+ case appUnavailable
+ case surfaceNotFound
+ case permissionDenied
+
+ var localizedStringResource: LocalizedStringResource {
+ switch self {
+ case .appUnavailable: "The Ghostty app isn't properly initialized."
+ case .surfaceNotFound: "The terminal no longer exists."
+ case .permissionDenied: "Ghostty doesn't allow Shortcuts."
+ }
+ }
+}
diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift
new file mode 100644
index 000000000..17c97fbbb
--- /dev/null
+++ b/macos/Sources/Features/App Intents/InputIntent.swift
@@ -0,0 +1,317 @@
+import AppKit
+import AppIntents
+
+/// App intent to input text in a terminal.
+struct InputTextIntent: AppIntent {
+ static var title: LocalizedStringResource = "Input Text to Terminal"
+
+ @Parameter(
+ title: "Text",
+ description: "The text to input to the terminal. The text will be inputted as if it was pasted.",
+ inputOptions: String.IntentInputOptions(
+ capitalizationType: .none,
+ multiline: true,
+ autocorrect: false,
+ smartQuotes: false,
+ smartDashes: false
+ )
+ )
+ var text: String
+
+ @Parameter(
+ title: "Terminal",
+ description: "The terminal to scope this action to."
+ )
+ var terminal: TerminalEntity
+
+ @available(macOS 26.0, *)
+ static var supportedModes: IntentModes = [.background, .foreground]
+
+ @MainActor
+ func perform() async throws -> some IntentResult {
+ guard await requestIntentPermission() else {
+ throw GhosttyIntentError.permissionDenied
+ }
+
+ guard let surface = terminal.surfaceModel else {
+ throw GhosttyIntentError.surfaceNotFound
+ }
+
+ surface.sendText(text)
+ return .result()
+ }
+}
+
+/// App intent to trigger a keyboard event.
+struct KeyEventIntent: AppIntent {
+ static var title: LocalizedStringResource = "Send Keyboard Event to Terminal"
+ static var description = IntentDescription("Simulate a keyboard event. This will not handle text encoding; use the 'Input Text' action for that.")
+
+ @Parameter(
+ title: "Key",
+ description: "The key to send to the terminal.",
+ default: .enter
+ )
+ var key: Ghostty.Input.Key
+
+ @Parameter(
+ title: "Modifier(s)",
+ description: "The modifiers to send with the key event.",
+ default: []
+ )
+ var mods: [KeyEventMods]
+
+ @Parameter(
+ title: "Event Type",
+ description: "A key press or release.",
+ default: .press
+ )
+ var action: Ghostty.Input.Action
+
+ @Parameter(
+ title: "Terminal",
+ description: "The terminal to scope this action to."
+ )
+ var terminal: TerminalEntity
+
+ @available(macOS 26.0, *)
+ static var supportedModes: IntentModes = [.background, .foreground]
+
+ @MainActor
+ func perform() async throws -> some IntentResult {
+ guard await requestIntentPermission() else {
+ throw GhosttyIntentError.permissionDenied
+ }
+
+ guard let surface = terminal.surfaceModel else {
+ throw GhosttyIntentError.surfaceNotFound
+ }
+
+ // Convert KeyEventMods array to Ghostty.Input.Mods
+ let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in
+ result.union(mod.ghosttyMod)
+ }
+
+ let keyEvent = Ghostty.Input.KeyEvent(
+ key: key,
+ action: action,
+ mods: ghosttyMods
+ )
+ surface.sendKeyEvent(keyEvent)
+
+ return .result()
+ }
+}
+
+// MARK: MouseButtonIntent
+
+/// App intent to trigger a mouse button event.
+struct MouseButtonIntent: AppIntent {
+ static var title: LocalizedStringResource = "Send Mouse Button Event to Terminal"
+
+ @Parameter(
+ title: "Button",
+ description: "The mouse button to press or release.",
+ default: .left
+ )
+ var button: Ghostty.Input.MouseButton
+
+ @Parameter(
+ title: "Action",
+ description: "Whether to press or release the button.",
+ default: .press
+ )
+ var action: Ghostty.Input.MouseState
+
+ @Parameter(
+ title: "Modifier(s)",
+ description: "The modifiers to send with the mouse event.",
+ default: []
+ )
+ var mods: [KeyEventMods]
+
+ @Parameter(
+ title: "Terminal",
+ description: "The terminal to scope this action to."
+ )
+ var terminal: TerminalEntity
+
+ @available(macOS 26.0, *)
+ static var supportedModes: IntentModes = [.background, .foreground]
+
+ @MainActor
+ func perform() async throws -> some IntentResult {
+ guard await requestIntentPermission() else {
+ throw GhosttyIntentError.permissionDenied
+ }
+
+ guard let surface = terminal.surfaceModel else {
+ throw GhosttyIntentError.surfaceNotFound
+ }
+
+ // Convert KeyEventMods array to Ghostty.Input.Mods
+ let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in
+ result.union(mod.ghosttyMod)
+ }
+
+ let mouseEvent = Ghostty.Input.MouseButtonEvent(
+ action: action,
+ button: button,
+ mods: ghosttyMods
+ )
+ surface.sendMouseButton(mouseEvent)
+
+ return .result()
+ }
+}
+
+/// App intent to send a mouse position event.
+struct MousePosIntent: AppIntent {
+ static var title: LocalizedStringResource = "Send Mouse Position Event to Terminal"
+ static var description = IntentDescription("Send a mouse position event to the terminal. This reports the cursor position for mouse tracking.")
+
+ @Parameter(
+ title: "X Position",
+ description: "The horizontal position of the mouse cursor in pixels.",
+ default: 0
+ )
+ var x: Double
+
+ @Parameter(
+ title: "Y Position",
+ description: "The vertical position of the mouse cursor in pixels.",
+ default: 0
+ )
+ var y: Double
+
+ @Parameter(
+ title: "Modifier(s)",
+ description: "The modifiers to send with the mouse position event.",
+ default: []
+ )
+ var mods: [KeyEventMods]
+
+ @Parameter(
+ title: "Terminal",
+ description: "The terminal to scope this action to."
+ )
+ var terminal: TerminalEntity
+
+ @available(macOS 26.0, *)
+ static var supportedModes: IntentModes = [.background, .foreground]
+
+ @MainActor
+ func perform() async throws -> some IntentResult {
+ guard await requestIntentPermission() else {
+ throw GhosttyIntentError.permissionDenied
+ }
+
+ guard let surface = terminal.surfaceModel else {
+ throw GhosttyIntentError.surfaceNotFound
+ }
+
+ // Convert KeyEventMods array to Ghostty.Input.Mods
+ let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in
+ result.union(mod.ghosttyMod)
+ }
+
+ let mousePosEvent = Ghostty.Input.MousePosEvent(
+ x: x,
+ y: y,
+ mods: ghosttyMods
+ )
+ surface.sendMousePos(mousePosEvent)
+
+ return .result()
+ }
+}
+
+/// App intent to send a mouse scroll event.
+struct MouseScrollIntent: AppIntent {
+ static var title: LocalizedStringResource = "Send Mouse Scroll Event to Terminal"
+ static var description = IntentDescription("Send a mouse scroll event to the terminal with configurable precision and momentum.")
+
+ @Parameter(
+ title: "X Scroll Delta",
+ description: "The horizontal scroll amount.",
+ default: 0
+ )
+ var x: Double
+
+ @Parameter(
+ title: "Y Scroll Delta",
+ description: "The vertical scroll amount.",
+ default: 0
+ )
+ var y: Double
+
+ @Parameter(
+ title: "High Precision",
+ description: "Whether this is a high-precision scroll event (e.g., from trackpad).",
+ default: false
+ )
+ var precision: Bool
+
+ @Parameter(
+ title: "Momentum Phase",
+ description: "The momentum phase for inertial scrolling.",
+ default: Ghostty.Input.Momentum.none
+ )
+ var momentum: Ghostty.Input.Momentum
+
+ @Parameter(
+ title: "Terminal",
+ description: "The terminal to scope this action to."
+ )
+ var terminal: TerminalEntity
+
+ @available(macOS 26.0, *)
+ static var supportedModes: IntentModes = [.background, .foreground]
+
+ @MainActor
+ func perform() async throws -> some IntentResult {
+ guard await requestIntentPermission() else {
+ throw GhosttyIntentError.permissionDenied
+ }
+
+ guard let surface = terminal.surfaceModel else {
+ throw GhosttyIntentError.surfaceNotFound
+ }
+
+ let scrollEvent = Ghostty.Input.MouseScrollEvent(
+ x: x,
+ y: y,
+ mods: .init(precision: precision, momentum: momentum)
+ )
+ surface.sendMouseScroll(scrollEvent)
+
+ return .result()
+ }
+}
+
+// MARK: Mods
+
+enum KeyEventMods: String, AppEnum, CaseIterable {
+ case shift
+ case control
+ case option
+ case command
+
+ static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Modifier Key")
+
+ static var caseDisplayRepresentations: [KeyEventMods : DisplayRepresentation] = [
+ .shift: "Shift",
+ .control: "Control",
+ .option: "Option",
+ .command: "Command"
+ ]
+
+ var ghosttyMod: Ghostty.Input.Mods {
+ switch self {
+ case .shift: .shift
+ case .control: .ctrl
+ case .option: .alt
+ case .command: .super
+ }
+ }
+}
diff --git a/macos/Sources/Features/App Intents/IntentPermission.swift b/macos/Sources/Features/App Intents/IntentPermission.swift
new file mode 100644
index 000000000..210d2cb2e
--- /dev/null
+++ b/macos/Sources/Features/App Intents/IntentPermission.swift
@@ -0,0 +1,57 @@
+import AppKit
+
+/// Requests permission for Shortcuts app to interact with Ghostty
+///
+/// This function displays a permission dialog asking the user to allow Shortcuts
+/// to interact with Ghostty. The permission is automatically cached for 10 minutes
+/// if the user selects "Allow", meaning subsequent intent calls won't show the dialog
+/// again during that time period.
+///
+/// The permission uses a shared UserDefaults key across all intents, so granting
+/// permission for one intent allows all Ghostty intents to execute without additional
+/// prompts for the duration of the cache period.
+///
+/// - Returns: `true` if permission is granted, `false` if denied
+///
+/// ## Usage
+/// Add this check at the beginning of any App Intent's `perform()` method:
+/// ```swift
+/// @MainActor
+/// func perform() async throws -> some IntentResult {
+/// guard await requestIntentPermission() else {
+/// throw GhosttyIntentError.permissionDenied
+/// }
+/// // ... continue with intent implementation
+/// }
+/// ```
+func requestIntentPermission() async -> Bool {
+ await withCheckedContinuation { continuation in
+ Task { @MainActor in
+ if let delegate = NSApp.delegate as? AppDelegate {
+ switch (delegate.ghostty.config.macosShortcuts) {
+ case .allow:
+ continuation.resume(returning: true)
+ return
+
+ case .deny:
+ continuation.resume(returning: false)
+ return
+
+ case .ask:
+ // Continue with the permission dialog
+ break
+ }
+ }
+
+
+ PermissionRequest.show(
+ "com.mitchellh.ghostty.shortcutsPermission",
+ message: "Allow Shortcuts to interact with Ghostty?",
+ allowDuration: .forever,
+ rememberDuration: nil,
+ ) { response in
+ continuation.resume(returning: response)
+ }
+ }
+ }
+}
diff --git a/macos/Sources/Features/App Intents/KeybindIntent.swift b/macos/Sources/Features/App Intents/KeybindIntent.swift
new file mode 100644
index 000000000..b31da4a50
--- /dev/null
+++ b/macos/Sources/Features/App Intents/KeybindIntent.swift
@@ -0,0 +1,35 @@
+import AppKit
+import AppIntents
+
+struct KeybindIntent: AppIntent {
+ static var title: LocalizedStringResource = "Invoke a Keybind Action"
+
+ @Parameter(
+ title: "Terminal",
+ description: "The terminal to invoke the action on."
+ )
+ var terminal: TerminalEntity
+
+ @Parameter(
+ title: "Action",
+ description: "The keybind action to invoke. This can be any valid keybind action you could put in a configuration file."
+ )
+ var action: String
+
+ @available(macOS 26.0, *)
+ static var supportedModes: IntentModes = [.background, .foreground]
+
+ @MainActor
+ func perform() async throws -> some IntentResult & ReturnsValue {
+ guard await requestIntentPermission() else {
+ throw GhosttyIntentError.permissionDenied
+ }
+
+ guard let surface = terminal.surfaceModel else {
+ throw GhosttyIntentError.surfaceNotFound
+ }
+
+ let performed = surface.perform(action: action)
+ return .result(value: performed)
+ }
+}
diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift
new file mode 100644
index 000000000..9b95208bb
--- /dev/null
+++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift
@@ -0,0 +1,168 @@
+import AppKit
+import AppIntents
+import GhosttyKit
+
+/// App intent that allows creating a new terminal window or tab.
+///
+/// This requires macOS 15 or greater because we use features of macOS 15 here.
+@available(macOS 15.0, *)
+struct NewTerminalIntent: AppIntent {
+ static var title: LocalizedStringResource = "New Terminal"
+ static var description = IntentDescription("Create a new terminal.")
+
+ @Parameter(
+ title: "Location",
+ description: "The location that the terminal should be created.",
+ default: .window
+ )
+ var location: NewTerminalLocation
+
+ @Parameter(
+ title: "Command",
+ description: "Command to execute within your configured shell.",
+ )
+ var command: String?
+
+ @Parameter(
+ title: "Working Directory",
+ description: "The working directory to open in the terminal.",
+ supportedContentTypes: [.folder]
+ )
+ var workingDirectory: IntentFile?
+
+ @Parameter(
+ title: "Environment Variables",
+ description: "Environment variables in `KEY=VALUE` format.",
+ default: []
+ )
+ var env: [String]
+
+ @Parameter(
+ title: "Parent Terminal",
+ description: "The terminal to inherit the base configuration from."
+ )
+ var parent: TerminalEntity?
+
+ @available(macOS 26.0, *)
+ static var supportedModes: IntentModes = .foreground(.immediate)
+
+ @available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes")
+ static var openAppWhenRun = true
+
+ @MainActor
+ func perform() async throws -> some IntentResult & ReturnsValue {
+ guard await requestIntentPermission() else {
+ throw GhosttyIntentError.permissionDenied
+ }
+ guard let appDelegate = NSApp.delegate as? AppDelegate else {
+ throw GhosttyIntentError.appUnavailable
+ }
+ let ghostty = appDelegate.ghostty
+
+ var config = Ghostty.SurfaceConfiguration()
+
+ // We don't run command as "command" and instead use "initialInput" so
+ // that we can get all the login scripts to setup things like PATH.
+ if let command {
+ config.initialInput = "\(command); exit\n"
+ }
+
+ // If we were given a working directory then open that directory
+ if let url = workingDirectory?.fileURL {
+ let dir = url.hasDirectoryPath ? url : url.deletingLastPathComponent()
+ config.workingDirectory = dir.path(percentEncoded: false)
+ }
+
+ // Parse environment variables from KEY=VALUE format
+ for envVar in env {
+ if let separatorIndex = envVar.firstIndex(of: "=") {
+ let key = String(envVar[...NewDirection? {
+ switch self {
+ case .splitLeft: return .left
+ case .splitRight: return .right
+ case .splitUp: return .up
+ case .splitDown: return .down
+ default: return nil
+ }
+ }
+}
+
+extension NewTerminalLocation: AppEnum {
+ static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Location")
+
+ static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [
+ .tab: .init(title: "Tab"),
+ .window: .init(title: "Window"),
+ .splitLeft: .init(title: "Split Left"),
+ .splitRight: .init(title: "Split Right"),
+ .splitUp: .init(title: "Split Up"),
+ .splitDown: .init(title: "Split Down"),
+ ]
+}
diff --git a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift
new file mode 100644
index 000000000..2e6c9850c
--- /dev/null
+++ b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift
@@ -0,0 +1,32 @@
+import AppKit
+import AppIntents
+
+struct QuickTerminalIntent: AppIntent {
+ static var title: LocalizedStringResource = "Open the Quick Terminal"
+ static var description = IntentDescription("Open the Quick Terminal. If it is already open, then do nothing.")
+
+ @available(macOS 26.0, *)
+ static var supportedModes: IntentModes = .background
+
+ @MainActor
+ func perform() async throws -> some IntentResult & ReturnsValue<[TerminalEntity]> {
+ guard await requestIntentPermission() else {
+ throw GhosttyIntentError.permissionDenied
+ }
+
+ guard let delegate = NSApp.delegate as? AppDelegate else {
+ throw GhosttyIntentError.appUnavailable
+ }
+
+ // This is safe to call even if it is already shown.
+ let c = delegate.quickController
+ c.animateIn()
+
+ // Grab all our terminals
+ let terminals = c.surfaceTree.root?.leaves().map {
+ TerminalEntity($0)
+ } ?? []
+
+ return .result(value: terminals)
+ }
+}
diff --git a/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIconImage.swift b/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIconImage.swift
index 4d522067e..8a461699f 100644
--- a/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIconImage.swift
+++ b/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIconImage.swift
@@ -4,12 +4,26 @@ extension View {
/// Returns the ghostty icon to use for views.
func ghosttyIconImage() -> Image {
#if os(macOS)
+ // If we have a specific icon set, then use that
if let delegate = NSApplication.shared.delegate as? AppDelegate,
let nsImage = delegate.appIcon {
return Image(nsImage: nsImage)
}
+
+ // Grab the icon from the running application. This is the best way
+ // I've found so far to get the proper icon for our current icon
+ // tinting and so on with macOS Tahoe
+ if let icon = NSRunningApplication.current.icon {
+ return Image(nsImage: icon)
+ }
+
+ // Get our defined application icon image.
+ if let nsImage = NSApp.applicationIconImage {
+ return Image(nsImage: nsImage)
+ }
#endif
+ // Fall back to a static representation
return Image("AppIconImage")
}
}
diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift
index 47f2baf23..d02828494 100644
--- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift
+++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift
@@ -17,33 +17,19 @@ struct TerminalCommandPaletteView: View {
// The commands available to the command palette.
private var commandOptions: [CommandOption] {
- guard let surface = surfaceView.surface else { return [] }
-
- var ptr: UnsafeMutablePointer? = nil
- var count: Int = 0
- ghostty_surface_commands(surface, &ptr, &count)
- guard let ptr else { return [] }
-
- let buffer = UnsafeBufferPointer(start: ptr, count: count)
- return Array(buffer).filter { c in
- let key = String(cString: c.action_key)
- switch (key) {
- case "toggle_tab_overview",
- "toggle_window_decorations",
- "show_gtk_inspector":
- return false
- default:
- return true
- }
- }.map { c in
- let action = String(cString: c.action)
- return CommandOption(
- title: String(cString: c.title),
- description: String(cString: c.description),
- symbols: ghosttyConfig.keyboardShortcut(for: action)?.keyList
- ) {
- onAction(action)
+ guard let surface = surfaceView.surfaceModel else { return [] }
+ do {
+ return try surface.commands().map { c in
+ return CommandOption(
+ title: c.title,
+ description: c.description,
+ symbols: ghosttyConfig.keyboardShortcut(for: c.action)?.keyList
+ ) {
+ onAction(c.action)
+ }
}
+ } catch {
+ return []
}
}
diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
index 28dea9579..3bd8bc18f 100644
--- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
+++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift
@@ -42,7 +42,11 @@ class QuickTerminalController: BaseTerminalController {
) {
self.position = position
self.derivedConfig = DerivedConfig(ghostty.config)
- super.init(ghostty, baseConfig: base, surfaceTree: tree)
+
+ // Important detail here: we initialize with an empty surface tree so
+ // that we don't start a terminal process. This gets started when the
+ // first terminal is shown in `animateIn`.
+ super.init(ghostty, baseConfig: base, surfaceTree: .init())
// Setup our notifications for behaviors
let center = NotificationCenter.default
@@ -218,19 +222,19 @@ class QuickTerminalController: BaseTerminalController {
}
}
- override func closeSurfaceNode(
+ override func closeSurface(
_ node: SplitTree.Node,
withConfirmation: Bool = true
) {
// If this isn't the root then we're dealing with a split closure.
if surfaceTree.root != node {
- super.closeSurfaceNode(node, withConfirmation: withConfirmation)
+ super.closeSurface(node, withConfirmation: withConfirmation)
return
}
// If this isn't a final leaf then we're dealing with a split closure
guard case .leaf(let surface) = node else {
- super.closeSurfaceNode(node, withConfirmation: withConfirmation)
+ super.closeSurface(node, withConfirmation: withConfirmation)
return
}
diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift
index bc91b920e..c93a9450d 100644
--- a/macos/Sources/Features/Terminal/BaseTerminalController.swift
+++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift
@@ -193,6 +193,46 @@ class BaseTerminalController: NSWindowController,
}
}
+ // MARK: Methods
+
+ /// Create a new split.
+ @discardableResult
+ func newSplit(
+ at oldView: Ghostty.SurfaceView,
+ direction: SplitTree.NewDirection,
+ baseConfig config: Ghostty.SurfaceConfiguration? = nil
+ ) -> Ghostty.SurfaceView? {
+ // We can only create new splits for surfaces in our tree.
+ guard surfaceTree.root?.node(view: oldView) != nil else { return nil }
+
+ // Create a new surface view
+ guard let ghostty_app = ghostty.app else { return nil }
+ let newView = Ghostty.SurfaceView(ghostty_app, baseConfig: config)
+
+ // Do the split
+ let newTree: SplitTree
+ do {
+ newTree = try surfaceTree.insert(
+ view: newView,
+ at: oldView,
+ direction: direction)
+ } catch {
+ // If splitting fails for any reason (it should not), then we just log
+ // and return. The new view we created will be deinitialized and its
+ // no big deal.
+ Ghostty.logger.warning("failed to insert split: \(error)")
+ return nil
+ }
+
+ replaceSurfaceTree(
+ newTree,
+ moveFocusTo: newView,
+ moveFocusFrom: oldView,
+ undoAction: "New Split")
+
+ return newView
+ }
+
/// Called when the surfaceTree variable changed.
///
/// Subclasses should call super first.
@@ -260,6 +300,46 @@ class BaseTerminalController: NSWindowController,
self.alert = alert
}
+ /// Close a surface from a view.
+ func closeSurface(
+ _ view: Ghostty.SurfaceView,
+ withConfirmation: Bool = true
+ ) {
+ guard let node = surfaceTree.root?.node(view: view) else { return }
+ closeSurface(node, withConfirmation: withConfirmation)
+ }
+
+ /// Close a surface node (which may contain splits), requesting confirmation if necessary.
+ ///
+ /// This will also insert the proper undo stack information in.
+ func closeSurface(
+ _ node: SplitTree.Node,
+ withConfirmation: Bool = true
+ ) {
+ // This node must be part of our tree
+ guard surfaceTree.contains(node) else { return }
+
+ // If the child process is not alive, then we exit immediately
+ guard withConfirmation else {
+ removeSurfaceNode(node)
+ return
+ }
+
+ // Confirm close. We use an NSAlert instead of a SwiftUI confirmationDialog
+ // due to SwiftUI bugs (see Ghostty #560). To repeat from #560, the bug is that
+ // confirmationDialog allows the user to Cmd-W close the alert, but when doing
+ // so SwiftUI does not update any of the bindings to note that window is no longer
+ // being shown, and provides no callback to detect this.
+ confirmClose(
+ messageText: "Close Terminal?",
+ informativeText: "The terminal still has a running process. If you close the terminal the process will be killed."
+ ) { [weak self] in
+ if let self {
+ self.removeSurfaceNode(node)
+ }
+ }
+ }
+
// MARK: Split Tree Management
/// Find the next surface to focus when a node is being closed.
@@ -420,42 +500,11 @@ class BaseTerminalController: NSWindowController,
@objc private func ghosttyDidCloseSurface(_ notification: Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard let node = surfaceTree.root?.node(view: target) else { return }
- closeSurfaceNode(
+ closeSurface(
node,
withConfirmation: (notification.userInfo?["process_alive"] as? Bool) ?? false)
}
- /// Close a surface node (which may contain splits), requesting confirmation if necessary.
- ///
- /// This will also insert the proper undo stack information in.
- func closeSurfaceNode(
- _ node: SplitTree.Node,
- withConfirmation: Bool = true
- ) {
- // This node must be part of our tree
- guard surfaceTree.contains(node) else { return }
-
- // If the child process is not alive, then we exit immediately
- guard withConfirmation else {
- removeSurfaceNode(node)
- return
- }
-
- // Confirm close. We use an NSAlert instead of a SwiftUI confirmationDialog
- // due to SwiftUI bugs (see Ghostty #560). To repeat from #560, the bug is that
- // confirmationDialog allows the user to Cmd-W close the alert, but when doing
- // so SwiftUI does not update any of the bindings to note that window is no longer
- // being shown, and provides no callback to detect this.
- confirmClose(
- messageText: "Close Terminal?",
- informativeText: "The terminal still has a running process. If you close the terminal the process will be killed."
- ) { [weak self] in
- if let self {
- self.removeSurfaceNode(node)
- }
- }
- }
-
@objc private func ghosttyDidNewSplit(_ notification: Notification) {
// The target must be within our tree
guard let oldView = notification.object as? Ghostty.SurfaceView else { return }
@@ -477,30 +526,7 @@ class BaseTerminalController: NSWindowController,
default: return
}
- // Create a new surface view
- guard let ghostty_app = ghostty.app else { return }
- let newView = Ghostty.SurfaceView(ghostty_app, baseConfig: config)
-
- // Do the split
- let newTree: SplitTree
- do {
- newTree = try surfaceTree.insert(
- view: newView,
- at: oldView,
- direction: splitDirection)
- } catch {
- // If splitting fails for any reason (it should not), then we just log
- // and return. The new view we created will be deinitialized and its
- // no big deal.
- Ghostty.logger.warning("failed to insert split: \(error)")
- return
- }
-
- replaceSurfaceTree(
- newTree,
- moveFocusTo: newView,
- moveFocusFrom: oldView,
- undoAction: "New Split")
+ newSplit(at: oldView, direction: splitDirection, baseConfig: config)
}
@objc private func ghosttyDidEqualizeSplits(_ notification: Notification) {
diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift
index 49b3fea34..c5e1c413f 100644
--- a/macos/Sources/Features/Terminal/TerminalController.swift
+++ b/macos/Sources/Features/Terminal/TerminalController.swift
@@ -5,18 +5,25 @@ import Combine
import GhosttyKit
/// A classic, tabbed terminal experience.
-class TerminalController: BaseTerminalController {
+class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Controller {
override var windowNibName: NSNib.Name? {
let defaultValue = "Terminal"
guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue }
let config = appDelegate.ghostty.config
+
+ // If we have no window decorations, there's no reason to do anything but
+ // the default titlebar (because there will be no titlebar).
+ if !config.windowDecorations {
+ return defaultValue
+ }
+
let nib = switch config.macosTitlebarStyle {
case "native": "Terminal"
case "hidden": "TerminalHiddenTitlebar"
case "transparent": "TerminalTransparentTitlebar"
case "tabs":
- if #available(macOS 26.0, *), hasLiquidGlass() {
+ if #available(macOS 26.0, *) {
"TerminalTabsTitlebarTahoe"
} else {
"TerminalTabsTitlebarVentura"
@@ -169,7 +176,7 @@ class TerminalController: BaseTerminalController {
private static var lastCascadePoint = NSPoint(x: 0, y: 0)
// The preferred parent terminal controller.
- private static var preferredParent: TerminalController? {
+ static var preferredParent: TerminalController? {
all.first {
$0.window?.isMainWindow ?? false
} ?? all.last
@@ -519,13 +526,13 @@ class TerminalController: BaseTerminalController {
}
/// This is called anytime a node in the surface tree is being removed.
- override func closeSurfaceNode(
+ override func closeSurface(
_ node: SplitTree.Node,
withConfirmation: Bool = true
) {
// If this isn't the root then we're dealing with a split closure.
if surfaceTree.root != node {
- super.closeSurfaceNode(node, withConfirmation: withConfirmation)
+ super.closeSurface(node, withConfirmation: withConfirmation)
return
}
@@ -882,14 +889,20 @@ class TerminalController: BaseTerminalController {
ghostty.newTab(surface: surface)
}
- //MARK: - NSWindowDelegate
+ // MARK: NSWindowDelegate
+
+ // TabGroupCloseCoordinator.Controller
+ lazy private(set) var tabGroupCloseCoordinator = TabGroupCloseCoordinator()
override func windowShouldClose(_ sender: NSWindow) -> Bool {
- // If we have tabs, then this should only close the tab.
- if window?.tabGroup?.windows.count ?? 0 > 1 {
- closeTab(sender)
- } else {
- closeWindow(sender)
+ tabGroupCloseCoordinator.windowShouldClose(sender) { [weak self] scope in
+ guard let self else { return }
+ switch (scope) {
+ case .tab: closeTab(nil)
+ case .window:
+ guard self.window?.isFirstWindowInTabGroup ?? false else { return }
+ closeWindow(nil)
+ }
}
// We will always explicitly close the window using the above
@@ -1001,20 +1014,14 @@ class TerminalController: BaseTerminalController {
@IBAction override func closeWindow(_ sender: Any?) {
guard let window = window else { return }
- guard let tabGroup = window.tabGroup else {
- // No tabs, no tab group, just perform a normal close.
- closeWindowImmediately()
- return
- }
- // If have one window then we just do a normal close
- if tabGroup.windows.count == 1 {
- closeWindowImmediately()
- return
- }
+ // We need to check all the windows in our tab group for confirmation
+ // if we're closing the window. If we don't have a tabgroup for any
+ // reason we check ourselves.
+ let windows: [NSWindow] = window.tabGroup?.windows ?? [window]
// Check if any windows require close confirmation.
- let needsConfirm = tabGroup.windows.contains { tabWindow in
+ let needsConfirm = windows.contains { tabWindow in
guard let controller = tabWindow.windowController as? TerminalController else {
return false
}
@@ -1270,4 +1277,3 @@ extension TerminalController: NSMenuItemValidation {
}
}
}
-
diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift
index e24323113..cec85f06e 100644
--- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift
+++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift
@@ -58,16 +58,19 @@ class TerminalWindow: NSWindow {
hideWindowButtons()
}
- // Create our reset zoom titlebar accessory.
- resetZoomAccessory.layoutAttribute = .right
- resetZoomAccessory.view = NSHostingView(rootView: ResetZoomAccessoryView(
- viewModel: viewModel,
- action: { [weak self] in
- guard let self else { return }
- self.terminalController?.splitZoom(self)
- }))
- addTitlebarAccessoryViewController(resetZoomAccessory)
- resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false
+ // Create our reset zoom titlebar accessory. We have to have a title
+ // to do this or AppKit triggers an assertion.
+ if styleMask.contains(.titled) {
+ resetZoomAccessory.layoutAttribute = .right
+ resetZoomAccessory.view = NSHostingView(rootView: ResetZoomAccessoryView(
+ viewModel: viewModel,
+ action: { [weak self] in
+ guard let self else { return }
+ self.terminalController?.splitZoom(self)
+ }))
+ addTitlebarAccessoryViewController(resetZoomAccessory)
+ resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false
+ }
// Setup the accessory view for tabs that shows our keyboard shortcuts,
// zoomed state, etc. Note I tried to use SwiftUI here but ran into issues
@@ -447,7 +450,7 @@ extension TerminalWindow {
// The padding from the top that the view appears. This was all just manually
// measured based on the OS.
var topPadding: CGFloat {
- if #available(macOS 26.0, *), hasLiquidGlass() {
+ if #available(macOS 26.0, *) {
return viewModel.hasToolbar ? 10 : 5
} else {
return viewModel.hasToolbar ? 9 : 4
diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift
index 0d064a7f7..f6ad6e56c 100644
--- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift
+++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift
@@ -45,11 +45,13 @@ class TransparentTitlebarTerminalWindow: TerminalWindow {
override func update() {
super.update()
-
+
// On macOS 13 to 15, we need to hide the NSVisualEffectView in order to allow our
// titlebar to be truly transparent.
- if !effectViewIsHidden && !hasLiquidGlass() {
- hideEffectView()
+ if #unavailable(macOS 26) {
+ if !effectViewIsHidden {
+ hideEffectView()
+ }
}
}
@@ -65,7 +67,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow {
// references changed (e.g. tabGroup is new).
setupKVO()
- if #available(macOS 26.0, *), hasLiquidGlass() {
+ if #available(macOS 26.0, *) {
syncAppearanceTahoe(surfaceConfig)
} else {
syncAppearanceVentura(surfaceConfig)
diff --git a/macos/Sources/Ghostty/AppError.swift b/macos/Sources/Ghostty/AppError.swift
deleted file mode 100644
index 55f191d3d..000000000
--- a/macos/Sources/Ghostty/AppError.swift
+++ /dev/null
@@ -1,3 +0,0 @@
-enum AppError: Error {
- case surfaceCreateError
-}
diff --git a/macos/Sources/Ghostty/Ghostty.Command.swift b/macos/Sources/Ghostty/Ghostty.Command.swift
new file mode 100644
index 000000000..1479ae92d
--- /dev/null
+++ b/macos/Sources/Ghostty/Ghostty.Command.swift
@@ -0,0 +1,46 @@
+import GhosttyKit
+
+extension Ghostty {
+ /// `ghostty_command_s`
+ struct Command: Sendable {
+ private let cValue: ghostty_command_s
+
+ /// The title of the command.
+ var title: String {
+ String(cString: cValue.title)
+ }
+
+ /// Human-friendly description of what this command will do.
+ var description: String {
+ String(cString: cValue.description)
+ }
+
+ /// The full action that must be performed to invoke this command.
+ var action: String {
+ String(cString: cValue.action)
+ }
+
+ /// Only the key portion of the action so you can compare action types, e.g. `goto_split`
+ /// instead of `goto_split:left`.
+ var actionKey: String {
+ String(cString: cValue.action_key)
+ }
+
+ /// True if this can be performed on this target.
+ var isSupported: Bool {
+ !Self.unsupportedActionKeys.contains(actionKey)
+ }
+
+ /// Unsupported action keys, because they either don't make sense in the context of our
+ /// target platform or they just aren't implemented yet.
+ static let unsupportedActionKeys: [String] = [
+ "toggle_tab_overview",
+ "toggle_window_decorations",
+ "show_gtk_inspector",
+ ]
+
+ init(cValue: ghostty_command_s) {
+ self.cValue = cValue
+ }
+ }
+}
diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift
index fcbea2a12..241c10632 100644
--- a/macos/Sources/Ghostty/Ghostty.Config.swift
+++ b/macos/Sources/Ghostty/Ghostty.Config.swift
@@ -558,6 +558,17 @@ extension Ghostty {
_ = ghostty_config_get(config, &v, key, UInt(key.count))
return v
}
+
+ var macosShortcuts: MacShortcuts {
+ let defaultValue = MacShortcuts.ask
+ guard let config = self.config else { return defaultValue }
+ var v: UnsafePointer? = nil
+ let key = "macos-shortcuts"
+ guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
+ guard let ptr = v else { return defaultValue }
+ let str = String(cString: ptr)
+ return MacShortcuts(rawValue: str) ?? defaultValue
+ }
}
}
@@ -584,6 +595,12 @@ extension Ghostty.Config {
case always
}
+ enum MacShortcuts: String {
+ case allow
+ case deny
+ case ask
+ }
+
enum ResizeOverlay : String {
case always
case never
diff --git a/macos/Sources/Ghostty/Ghostty.Error.swift b/macos/Sources/Ghostty/Ghostty.Error.swift
new file mode 100644
index 000000000..66f6857bf
--- /dev/null
+++ b/macos/Sources/Ghostty/Ghostty.Error.swift
@@ -0,0 +1,12 @@
+extension Ghostty {
+ /// Possible errors from internal Ghostty calls.
+ enum Error: Swift.Error, CustomLocalizedStringResourceConvertible {
+ case apiFailed
+
+ var localizedStringResource: LocalizedStringResource {
+ switch self {
+ case .apiFailed: return "libghostty API call failed"
+ }
+ }
+ }
+}
diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift
index 942ca5973..e05911c06 100644
--- a/macos/Sources/Ghostty/Ghostty.Input.swift
+++ b/macos/Sources/Ghostty/Ghostty.Input.swift
@@ -1,8 +1,11 @@
+import AppIntents
import Cocoa
import SwiftUI
import GhosttyKit
extension Ghostty {
+ struct Input {}
+
// MARK: Keyboard Shortcuts
/// Return the key equivalent for the given trigger.
@@ -91,121 +94,1156 @@ extension Ghostty {
GHOSTTY_KEY_BACKSPACE: .delete,
GHOSTTY_KEY_SPACE: .space,
]
-
- // Mapping of event keyCode to ghostty input key values. This is cribbed from
- // glfw mostly since we started as a glfw-based app way back in the day!
- static let keycodeToKey: [UInt16 : ghostty_input_key_e] = [
- 0x1D: GHOSTTY_KEY_DIGIT_0,
- 0x12: GHOSTTY_KEY_DIGIT_1,
- 0x13: GHOSTTY_KEY_DIGIT_2,
- 0x14: GHOSTTY_KEY_DIGIT_3,
- 0x15: GHOSTTY_KEY_DIGIT_4,
- 0x17: GHOSTTY_KEY_DIGIT_5,
- 0x16: GHOSTTY_KEY_DIGIT_6,
- 0x1A: GHOSTTY_KEY_DIGIT_7,
- 0x1C: GHOSTTY_KEY_DIGIT_8,
- 0x19: GHOSTTY_KEY_DIGIT_9,
- 0x00: GHOSTTY_KEY_A,
- 0x0B: GHOSTTY_KEY_B,
- 0x08: GHOSTTY_KEY_C,
- 0x02: GHOSTTY_KEY_D,
- 0x0E: GHOSTTY_KEY_E,
- 0x03: GHOSTTY_KEY_F,
- 0x05: GHOSTTY_KEY_G,
- 0x04: GHOSTTY_KEY_H,
- 0x22: GHOSTTY_KEY_I,
- 0x26: GHOSTTY_KEY_J,
- 0x28: GHOSTTY_KEY_K,
- 0x25: GHOSTTY_KEY_L,
- 0x2E: GHOSTTY_KEY_M,
- 0x2D: GHOSTTY_KEY_N,
- 0x1F: GHOSTTY_KEY_O,
- 0x23: GHOSTTY_KEY_P,
- 0x0C: GHOSTTY_KEY_Q,
- 0x0F: GHOSTTY_KEY_R,
- 0x01: GHOSTTY_KEY_S,
- 0x11: GHOSTTY_KEY_T,
- 0x20: GHOSTTY_KEY_U,
- 0x09: GHOSTTY_KEY_V,
- 0x0D: GHOSTTY_KEY_W,
- 0x07: GHOSTTY_KEY_X,
- 0x10: GHOSTTY_KEY_Y,
- 0x06: GHOSTTY_KEY_Z,
-
- 0x27: GHOSTTY_KEY_QUOTE,
- 0x2A: GHOSTTY_KEY_BACKSLASH,
- 0x2B: GHOSTTY_KEY_COMMA,
- 0x18: GHOSTTY_KEY_EQUAL,
- 0x32: GHOSTTY_KEY_BACKQUOTE,
- 0x21: GHOSTTY_KEY_BRACKET_LEFT,
- 0x1B: GHOSTTY_KEY_MINUS,
- 0x2F: GHOSTTY_KEY_PERIOD,
- 0x1E: GHOSTTY_KEY_BRACKET_RIGHT,
- 0x29: GHOSTTY_KEY_SEMICOLON,
- 0x2C: GHOSTTY_KEY_SLASH,
-
- 0x33: GHOSTTY_KEY_BACKSPACE,
- 0x39: GHOSTTY_KEY_CAPS_LOCK,
- 0x75: GHOSTTY_KEY_DELETE,
- 0x7D: GHOSTTY_KEY_ARROW_DOWN,
- 0x77: GHOSTTY_KEY_END,
- 0x24: GHOSTTY_KEY_ENTER,
- 0x35: GHOSTTY_KEY_ESCAPE,
- 0x7A: GHOSTTY_KEY_F1,
- 0x78: GHOSTTY_KEY_F2,
- 0x63: GHOSTTY_KEY_F3,
- 0x76: GHOSTTY_KEY_F4,
- 0x60: GHOSTTY_KEY_F5,
- 0x61: GHOSTTY_KEY_F6,
- 0x62: GHOSTTY_KEY_F7,
- 0x64: GHOSTTY_KEY_F8,
- 0x65: GHOSTTY_KEY_F9,
- 0x6D: GHOSTTY_KEY_F10,
- 0x67: GHOSTTY_KEY_F11,
- 0x6F: GHOSTTY_KEY_F12,
- 0x69: GHOSTTY_KEY_PRINT_SCREEN,
- 0x6B: GHOSTTY_KEY_F14,
- 0x71: GHOSTTY_KEY_F15,
- 0x6A: GHOSTTY_KEY_F16,
- 0x40: GHOSTTY_KEY_F17,
- 0x4F: GHOSTTY_KEY_F18,
- 0x50: GHOSTTY_KEY_F19,
- 0x5A: GHOSTTY_KEY_F20,
- 0x73: GHOSTTY_KEY_HOME,
- 0x72: GHOSTTY_KEY_INSERT,
- 0x7B: GHOSTTY_KEY_ARROW_LEFT,
- 0x3A: GHOSTTY_KEY_ALT_LEFT,
- 0x3B: GHOSTTY_KEY_CONTROL_LEFT,
- 0x38: GHOSTTY_KEY_SHIFT_LEFT,
- 0x37: GHOSTTY_KEY_META_LEFT,
- 0x47: GHOSTTY_KEY_NUM_LOCK,
- 0x79: GHOSTTY_KEY_PAGE_DOWN,
- 0x74: GHOSTTY_KEY_PAGE_UP,
- 0x7C: GHOSTTY_KEY_ARROW_RIGHT,
- 0x3D: GHOSTTY_KEY_ALT_RIGHT,
- 0x3E: GHOSTTY_KEY_CONTROL_RIGHT,
- 0x3C: GHOSTTY_KEY_SHIFT_RIGHT,
- 0x36: GHOSTTY_KEY_META_RIGHT,
- 0x31: GHOSTTY_KEY_SPACE,
- 0x30: GHOSTTY_KEY_TAB,
- 0x7E: GHOSTTY_KEY_ARROW_UP,
-
- 0x52: GHOSTTY_KEY_NUMPAD_0,
- 0x53: GHOSTTY_KEY_NUMPAD_1,
- 0x54: GHOSTTY_KEY_NUMPAD_2,
- 0x55: GHOSTTY_KEY_NUMPAD_3,
- 0x56: GHOSTTY_KEY_NUMPAD_4,
- 0x57: GHOSTTY_KEY_NUMPAD_5,
- 0x58: GHOSTTY_KEY_NUMPAD_6,
- 0x59: GHOSTTY_KEY_NUMPAD_7,
- 0x5B: GHOSTTY_KEY_NUMPAD_8,
- 0x5C: GHOSTTY_KEY_NUMPAD_9,
- 0x45: GHOSTTY_KEY_NUMPAD_ADD,
- 0x41: GHOSTTY_KEY_NUMPAD_DECIMAL,
- 0x4B: GHOSTTY_KEY_NUMPAD_DIVIDE,
- 0x4C: GHOSTTY_KEY_NUMPAD_ENTER,
- 0x51: GHOSTTY_KEY_NUMPAD_EQUAL,
- 0x43: GHOSTTY_KEY_NUMPAD_MULTIPLY,
- 0x4E: GHOSTTY_KEY_NUMPAD_SUBTRACT,
- ];
+}
+
+// MARK: Ghostty.Input.KeyEvent
+
+extension Ghostty.Input {
+ /// `ghostty_input_key_s`
+ struct KeyEvent {
+ let action: Action
+ let key: Key
+ let text: String?
+ let composing: Bool
+ let mods: Mods
+ let consumedMods: Mods
+ let unshiftedCodepoint: UInt32
+
+ init(
+ key: Key,
+ action: Action = .press,
+ text: String? = nil,
+ composing: Bool = false,
+ mods: Mods = [],
+ consumedMods: Mods = [],
+ unshiftedCodepoint: UInt32 = 0
+ ) {
+ self.key = key
+ self.action = action
+ self.text = text
+ self.composing = composing
+ self.mods = mods
+ self.consumedMods = consumedMods
+ self.unshiftedCodepoint = unshiftedCodepoint
+ }
+
+ init?(cValue: ghostty_input_key_s) {
+ // Convert action
+ switch cValue.action {
+ case GHOSTTY_ACTION_PRESS: self.action = .press
+ case GHOSTTY_ACTION_RELEASE: self.action = .release
+ case GHOSTTY_ACTION_REPEAT: self.action = .repeat
+ default: self.action = .press
+ }
+
+ // Convert key from keycode
+ guard let key = Key(keyCode: UInt16(cValue.keycode)) else { return nil }
+ self.key = key
+
+ // Convert text
+ if let textPtr = cValue.text {
+ self.text = String(cString: textPtr)
+ } else {
+ self.text = nil
+ }
+
+ // Set composing state
+ self.composing = cValue.composing
+
+ // Convert modifiers
+ self.mods = Mods(cMods: cValue.mods)
+ self.consumedMods = Mods(cMods: cValue.consumed_mods)
+
+ // Set unshifted codepoint
+ self.unshiftedCodepoint = cValue.unshifted_codepoint
+ }
+
+ /// Executes a closure with a temporary C representation of this KeyEvent.
+ ///
+ /// This method safely converts the Swift KeyEntity to a C `ghostty_input_key_s` struct
+ /// and passes it to the provided closure. The C struct is only valid within the closure's
+ /// execution scope. The text field's C string pointer is managed automatically and will
+ /// be invalid after the closure returns.
+ ///
+ /// - Parameter execute: A closure that receives the C struct and returns a value
+ /// - Returns: The value returned by the closure
+ @discardableResult
+ func withCValue(execute: (ghostty_input_key_s) -> T) -> T {
+ var keyEvent = ghostty_input_key_s()
+ keyEvent.action = action.cAction
+ keyEvent.keycode = UInt32(key.keyCode ?? 0)
+ keyEvent.composing = composing
+ keyEvent.mods = mods.cMods
+ keyEvent.consumed_mods = consumedMods.cMods
+ keyEvent.unshifted_codepoint = unshiftedCodepoint
+
+ // Handle text with proper memory management
+ if let text = text {
+ return text.withCString { textPtr in
+ keyEvent.text = textPtr
+ return execute(keyEvent)
+ }
+ } else {
+ keyEvent.text = nil
+ return execute(keyEvent)
+ }
+ }
+ }
+}
+
+// MARK: Ghostty.Input.Action
+
+extension Ghostty.Input {
+ /// `ghostty_input_action_e`
+ enum Action: String, CaseIterable {
+ case release
+ case press
+ case `repeat`
+
+ var cAction: ghostty_input_action_e {
+ switch self {
+ case .release: GHOSTTY_ACTION_RELEASE
+ case .press: GHOSTTY_ACTION_PRESS
+ case .repeat: GHOSTTY_ACTION_REPEAT
+ }
+ }
+ }
+}
+
+extension Ghostty.Input.Action: AppEnum {
+ static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Key Action")
+
+ static var caseDisplayRepresentations: [Ghostty.Input.Action : DisplayRepresentation] = [
+ .release: "Release",
+ .press: "Press",
+ .repeat: "Repeat"
+ ]
+}
+
+// MARK: Ghostty.Input.MouseEvent
+
+extension Ghostty.Input {
+ /// Represents a mouse input event with button state, button type, and modifier keys.
+ struct MouseButtonEvent {
+ let action: MouseState
+ let button: MouseButton
+ let mods: Mods
+
+ init(
+ action: MouseState,
+ button: MouseButton,
+ mods: Mods = []
+ ) {
+ self.action = action
+ self.button = button
+ self.mods = mods
+ }
+
+ /// Creates a MouseEvent from C enum values.
+ ///
+ /// This initializer converts C-style mouse input enums to Swift types.
+ /// Returns nil if any of the C enum values are invalid or unsupported.
+ ///
+ /// - Parameters:
+ /// - state: The mouse button state (press/release)
+ /// - button: The mouse button that was pressed/released
+ /// - mods: The modifier keys held during the mouse event
+ init?(state: ghostty_input_mouse_state_e, button: ghostty_input_mouse_button_e, mods: ghostty_input_mods_e) {
+ // Convert state
+ switch state {
+ case GHOSTTY_MOUSE_RELEASE: self.action = .release
+ case GHOSTTY_MOUSE_PRESS: self.action = .press
+ default: return nil
+ }
+
+ // Convert button
+ switch button {
+ case GHOSTTY_MOUSE_UNKNOWN: self.button = .unknown
+ case GHOSTTY_MOUSE_LEFT: self.button = .left
+ case GHOSTTY_MOUSE_RIGHT: self.button = .right
+ case GHOSTTY_MOUSE_MIDDLE: self.button = .middle
+ default: return nil
+ }
+
+ // Convert modifiers
+ self.mods = Mods(cMods: mods)
+ }
+ }
+
+ /// Represents a mouse position/movement event with coordinates and modifier keys.
+ struct MousePosEvent {
+ let x: Double
+ let y: Double
+ let mods: Mods
+
+ init(
+ x: Double,
+ y: Double,
+ mods: Mods = []
+ ) {
+ self.x = x
+ self.y = y
+ self.mods = mods
+ }
+ }
+
+ /// Represents a mouse scroll event with scroll deltas and modifier keys.
+ struct MouseScrollEvent {
+ let x: Double
+ let y: Double
+ let mods: ScrollMods
+
+ init(
+ x: Double,
+ y: Double,
+ mods: ScrollMods = .init(rawValue: 0)
+ ) {
+ self.x = x
+ self.y = y
+ self.mods = mods
+ }
+ }
+}
+
+// MARK: Ghostty.Input.MouseState
+
+extension Ghostty.Input {
+ /// `ghostty_input_mouse_state_e`
+ enum MouseState: String, CaseIterable {
+ case release
+ case press
+
+ var cMouseState: ghostty_input_mouse_state_e {
+ switch self {
+ case .release: GHOSTTY_MOUSE_RELEASE
+ case .press: GHOSTTY_MOUSE_PRESS
+ }
+ }
+ }
+}
+
+extension Ghostty.Input.MouseState: AppEnum {
+ static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Mouse State")
+
+ static var caseDisplayRepresentations: [Ghostty.Input.MouseState : DisplayRepresentation] = [
+ .release: "Release",
+ .press: "Press"
+ ]
+}
+
+// MARK: Ghostty.Input.MouseButton
+
+extension Ghostty.Input {
+ /// `ghostty_input_mouse_button_e`
+ enum MouseButton: String, CaseIterable {
+ case unknown
+ case left
+ case right
+ case middle
+
+ var cMouseButton: ghostty_input_mouse_button_e {
+ switch self {
+ case .unknown: GHOSTTY_MOUSE_UNKNOWN
+ case .left: GHOSTTY_MOUSE_LEFT
+ case .right: GHOSTTY_MOUSE_RIGHT
+ case .middle: GHOSTTY_MOUSE_MIDDLE
+ }
+ }
+ }
+}
+
+extension Ghostty.Input.MouseButton: AppEnum {
+ static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Mouse Button")
+
+ static var caseDisplayRepresentations: [Ghostty.Input.MouseButton : DisplayRepresentation] = [
+ .unknown: "Unknown",
+ .left: "Left",
+ .right: "Right",
+ .middle: "Middle"
+ ]
+
+ static var allCases: [Ghostty.Input.MouseButton] = [
+ .left,
+ .right,
+ .middle,
+ ]
+}
+
+// MARK: Ghostty.Input.ScrollMods
+
+extension Ghostty.Input {
+ /// `ghostty_input_scroll_mods_t` - Scroll event modifiers
+ ///
+ /// This is a packed bitmask that contains precision and momentum information
+ /// for scroll events, matching the Zig `ScrollMods` packed struct.
+ struct ScrollMods {
+ let rawValue: Int32
+
+ /// True if this is a high-precision scroll event (e.g., trackpad, Magic Mouse)
+ var precision: Bool {
+ rawValue & 0b0000_0001 != 0
+ }
+
+ /// The momentum phase of the scroll event for inertial scrolling
+ var momentum: Momentum {
+ let momentumBits = (rawValue >> 1) & 0b0000_0111
+ return Momentum(rawValue: UInt8(momentumBits)) ?? .none
+ }
+
+ init(precision: Bool = false, momentum: Momentum = .none) {
+ var value: Int32 = 0
+ if precision {
+ value |= 0b0000_0001
+ }
+ value |= Int32(momentum.rawValue) << 1
+ self.rawValue = value
+ }
+
+ init(rawValue: Int32) {
+ self.rawValue = rawValue
+ }
+
+ var cScrollMods: ghostty_input_scroll_mods_t {
+ rawValue
+ }
+ }
+}
+
+// MARK: Ghostty.Input.Momentum
+
+extension Ghostty.Input {
+ /// `ghostty_input_mouse_momentum_e` - Momentum phase for scroll events
+ enum Momentum: UInt8, CaseIterable {
+ case none = 0
+ case began = 1
+ case stationary = 2
+ case changed = 3
+ case ended = 4
+ case cancelled = 5
+ case mayBegin = 6
+
+ var cMomentum: ghostty_input_mouse_momentum_e {
+ switch self {
+ case .none: GHOSTTY_MOUSE_MOMENTUM_NONE
+ case .began: GHOSTTY_MOUSE_MOMENTUM_BEGAN
+ case .stationary: GHOSTTY_MOUSE_MOMENTUM_STATIONARY
+ case .changed: GHOSTTY_MOUSE_MOMENTUM_CHANGED
+ case .ended: GHOSTTY_MOUSE_MOMENTUM_ENDED
+ case .cancelled: GHOSTTY_MOUSE_MOMENTUM_CANCELLED
+ case .mayBegin: GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN
+ }
+ }
+ }
+}
+
+extension Ghostty.Input.Momentum: AppEnum {
+ static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scroll Momentum")
+
+ static var caseDisplayRepresentations: [Ghostty.Input.Momentum : DisplayRepresentation] = [
+ .none: "None",
+ .began: "Began",
+ .stationary: "Stationary",
+ .changed: "Changed",
+ .ended: "Ended",
+ .cancelled: "Cancelled",
+ .mayBegin: "May Begin"
+ ]
+}
+
+#if canImport(AppKit)
+import AppKit
+
+extension Ghostty.Input.Momentum {
+ /// Create a Momentum from an NSEvent.Phase
+ init(_ phase: NSEvent.Phase) {
+ switch phase {
+ case .began: self = .began
+ case .stationary: self = .stationary
+ case .changed: self = .changed
+ case .ended: self = .ended
+ case .cancelled: self = .cancelled
+ case .mayBegin: self = .mayBegin
+ default: self = .none
+ }
+ }
+}
+#endif
+
+// MARK: Ghostty.Input.Mods
+
+extension Ghostty.Input {
+ /// `ghostty_input_mods_e`
+ struct Mods: OptionSet {
+ let rawValue: UInt32
+
+ static let none = Mods(rawValue: GHOSTTY_MODS_NONE.rawValue)
+ static let shift = Mods(rawValue: GHOSTTY_MODS_SHIFT.rawValue)
+ static let ctrl = Mods(rawValue: GHOSTTY_MODS_CTRL.rawValue)
+ static let alt = Mods(rawValue: GHOSTTY_MODS_ALT.rawValue)
+ static let `super` = Mods(rawValue: GHOSTTY_MODS_SUPER.rawValue)
+ static let caps = Mods(rawValue: GHOSTTY_MODS_CAPS.rawValue)
+ static let shiftRight = Mods(rawValue: GHOSTTY_MODS_SHIFT_RIGHT.rawValue)
+ static let ctrlRight = Mods(rawValue: GHOSTTY_MODS_CTRL_RIGHT.rawValue)
+ static let altRight = Mods(rawValue: GHOSTTY_MODS_ALT_RIGHT.rawValue)
+ static let superRight = Mods(rawValue: GHOSTTY_MODS_SUPER_RIGHT.rawValue)
+
+ var cMods: ghostty_input_mods_e {
+ ghostty_input_mods_e(rawValue)
+ }
+
+ init(rawValue: UInt32) {
+ self.rawValue = rawValue
+ }
+
+ init(cMods: ghostty_input_mods_e) {
+ self.rawValue = cMods.rawValue
+ }
+
+ init(nsFlags: NSEvent.ModifierFlags) {
+ self.init(cMods: Ghostty.ghosttyMods(nsFlags))
+ }
+
+ var nsFlags: NSEvent.ModifierFlags {
+ Ghostty.eventModifierFlags(mods: cMods)
+ }
+ }
+}
+
+// MARK: Ghostty.Input.Key
+
+extension Ghostty.Input {
+ /// `ghostty_input_key_e`
+ enum Key: String {
+ // Writing System Keys
+ case backquote
+ case backslash
+ case bracketLeft
+ case bracketRight
+ case comma
+ case digit0
+ case digit1
+ case digit2
+ case digit3
+ case digit4
+ case digit5
+ case digit6
+ case digit7
+ case digit8
+ case digit9
+ case equal
+ case intlBackslash
+ case intlRo
+ case intlYen
+ case a
+ case b
+ case c
+ case d
+ case e
+ case f
+ case g
+ case h
+ case i
+ case j
+ case k
+ case l
+ case m
+ case n
+ case o
+ case p
+ case q
+ case r
+ case s
+ case t
+ case u
+ case v
+ case w
+ case x
+ case y
+ case z
+ case minus
+ case period
+ case quote
+ case semicolon
+ case slash
+
+ // Functional Keys
+ case altLeft
+ case altRight
+ case backspace
+ case capsLock
+ case contextMenu
+ case controlLeft
+ case controlRight
+ case enter
+ case metaLeft
+ case metaRight
+ case shiftLeft
+ case shiftRight
+ case space
+ case tab
+ case convert
+ case kanaMode
+ case nonConvert
+
+ // Control Pad Section
+ case delete
+ case end
+ case help
+ case home
+ case insert
+ case pageDown
+ case pageUp
+
+ // Arrow Pad Section
+ case arrowDown
+ case arrowLeft
+ case arrowRight
+ case arrowUp
+
+ // Numpad Section
+ case numLock
+ case numpad0
+ case numpad1
+ case numpad2
+ case numpad3
+ case numpad4
+ case numpad5
+ case numpad6
+ case numpad7
+ case numpad8
+ case numpad9
+ case numpadAdd
+ case numpadBackspace
+ case numpadClear
+ case numpadClearEntry
+ case numpadComma
+ case numpadDecimal
+ case numpadDivide
+ case numpadEnter
+ case numpadEqual
+ case numpadMemoryAdd
+ case numpadMemoryClear
+ case numpadMemoryRecall
+ case numpadMemoryStore
+ case numpadMemorySubtract
+ case numpadMultiply
+ case numpadParenLeft
+ case numpadParenRight
+ case numpadSubtract
+ case numpadSeparator
+ case numpadUp
+ case numpadDown
+ case numpadRight
+ case numpadLeft
+ case numpadBegin
+ case numpadHome
+ case numpadEnd
+ case numpadInsert
+ case numpadDelete
+ case numpadPageUp
+ case numpadPageDown
+
+ // Function Section
+ case escape
+ case f1
+ case f2
+ case f3
+ case f4
+ case f5
+ case f6
+ case f7
+ case f8
+ case f9
+ case f10
+ case f11
+ case f12
+ case f13
+ case f14
+ case f15
+ case f16
+ case f17
+ case f18
+ case f19
+ case f20
+ case f21
+ case f22
+ case f23
+ case f24
+ case f25
+ case fn
+ case fnLock
+ case printScreen
+ case scrollLock
+ case pause
+
+ // Media Keys
+ case browserBack
+ case browserFavorites
+ case browserForward
+ case browserHome
+ case browserRefresh
+ case browserSearch
+ case browserStop
+ case eject
+ case launchApp1
+ case launchApp2
+ case launchMail
+ case mediaPlayPause
+ case mediaSelect
+ case mediaStop
+ case mediaTrackNext
+ case mediaTrackPrevious
+ case power
+ case sleep
+ case audioVolumeDown
+ case audioVolumeMute
+ case audioVolumeUp
+ case wakeUp
+
+ // Legacy, Non-standard, and Special Keys
+ case copy
+ case cut
+ case paste
+
+ /// Get a key from a keycode
+ init?(keyCode: UInt16) {
+ if let key = Key.allCases.first(where: { $0.keyCode == keyCode }) {
+ self = key
+ return
+ }
+
+ return nil
+ }
+
+ var cKey: ghostty_input_key_e {
+ switch self {
+ // Writing System Keys
+ case .backquote: GHOSTTY_KEY_BACKQUOTE
+ case .backslash: GHOSTTY_KEY_BACKSLASH
+ case .bracketLeft: GHOSTTY_KEY_BRACKET_LEFT
+ case .bracketRight: GHOSTTY_KEY_BRACKET_RIGHT
+ case .comma: GHOSTTY_KEY_COMMA
+ case .digit0: GHOSTTY_KEY_DIGIT_0
+ case .digit1: GHOSTTY_KEY_DIGIT_1
+ case .digit2: GHOSTTY_KEY_DIGIT_2
+ case .digit3: GHOSTTY_KEY_DIGIT_3
+ case .digit4: GHOSTTY_KEY_DIGIT_4
+ case .digit5: GHOSTTY_KEY_DIGIT_5
+ case .digit6: GHOSTTY_KEY_DIGIT_6
+ case .digit7: GHOSTTY_KEY_DIGIT_7
+ case .digit8: GHOSTTY_KEY_DIGIT_8
+ case .digit9: GHOSTTY_KEY_DIGIT_9
+ case .equal: GHOSTTY_KEY_EQUAL
+ case .intlBackslash: GHOSTTY_KEY_INTL_BACKSLASH
+ case .intlRo: GHOSTTY_KEY_INTL_RO
+ case .intlYen: GHOSTTY_KEY_INTL_YEN
+ case .a: GHOSTTY_KEY_A
+ case .b: GHOSTTY_KEY_B
+ case .c: GHOSTTY_KEY_C
+ case .d: GHOSTTY_KEY_D
+ case .e: GHOSTTY_KEY_E
+ case .f: GHOSTTY_KEY_F
+ case .g: GHOSTTY_KEY_G
+ case .h: GHOSTTY_KEY_H
+ case .i: GHOSTTY_KEY_I
+ case .j: GHOSTTY_KEY_J
+ case .k: GHOSTTY_KEY_K
+ case .l: GHOSTTY_KEY_L
+ case .m: GHOSTTY_KEY_M
+ case .n: GHOSTTY_KEY_N
+ case .o: GHOSTTY_KEY_O
+ case .p: GHOSTTY_KEY_P
+ case .q: GHOSTTY_KEY_Q
+ case .r: GHOSTTY_KEY_R
+ case .s: GHOSTTY_KEY_S
+ case .t: GHOSTTY_KEY_T
+ case .u: GHOSTTY_KEY_U
+ case .v: GHOSTTY_KEY_V
+ case .w: GHOSTTY_KEY_W
+ case .x: GHOSTTY_KEY_X
+ case .y: GHOSTTY_KEY_Y
+ case .z: GHOSTTY_KEY_Z
+ case .minus: GHOSTTY_KEY_MINUS
+ case .period: GHOSTTY_KEY_PERIOD
+ case .quote: GHOSTTY_KEY_QUOTE
+ case .semicolon: GHOSTTY_KEY_SEMICOLON
+ case .slash: GHOSTTY_KEY_SLASH
+
+ // Functional Keys
+ case .altLeft: GHOSTTY_KEY_ALT_LEFT
+ case .altRight: GHOSTTY_KEY_ALT_RIGHT
+ case .backspace: GHOSTTY_KEY_BACKSPACE
+ case .capsLock: GHOSTTY_KEY_CAPS_LOCK
+ case .contextMenu: GHOSTTY_KEY_CONTEXT_MENU
+ case .controlLeft: GHOSTTY_KEY_CONTROL_LEFT
+ case .controlRight: GHOSTTY_KEY_CONTROL_RIGHT
+ case .enter: GHOSTTY_KEY_ENTER
+ case .metaLeft: GHOSTTY_KEY_META_LEFT
+ case .metaRight: GHOSTTY_KEY_META_RIGHT
+ case .shiftLeft: GHOSTTY_KEY_SHIFT_LEFT
+ case .shiftRight: GHOSTTY_KEY_SHIFT_RIGHT
+ case .space: GHOSTTY_KEY_SPACE
+ case .tab: GHOSTTY_KEY_TAB
+ case .convert: GHOSTTY_KEY_CONVERT
+ case .kanaMode: GHOSTTY_KEY_KANA_MODE
+ case .nonConvert: GHOSTTY_KEY_NON_CONVERT
+
+ // Control Pad Section
+ case .delete: GHOSTTY_KEY_DELETE
+ case .end: GHOSTTY_KEY_END
+ case .help: GHOSTTY_KEY_HELP
+ case .home: GHOSTTY_KEY_HOME
+ case .insert: GHOSTTY_KEY_INSERT
+ case .pageDown: GHOSTTY_KEY_PAGE_DOWN
+ case .pageUp: GHOSTTY_KEY_PAGE_UP
+
+ // Arrow Pad Section
+ case .arrowDown: GHOSTTY_KEY_ARROW_DOWN
+ case .arrowLeft: GHOSTTY_KEY_ARROW_LEFT
+ case .arrowRight: GHOSTTY_KEY_ARROW_RIGHT
+ case .arrowUp: GHOSTTY_KEY_ARROW_UP
+
+ // Numpad Section
+ case .numLock: GHOSTTY_KEY_NUM_LOCK
+ case .numpad0: GHOSTTY_KEY_NUMPAD_0
+ case .numpad1: GHOSTTY_KEY_NUMPAD_1
+ case .numpad2: GHOSTTY_KEY_NUMPAD_2
+ case .numpad3: GHOSTTY_KEY_NUMPAD_3
+ case .numpad4: GHOSTTY_KEY_NUMPAD_4
+ case .numpad5: GHOSTTY_KEY_NUMPAD_5
+ case .numpad6: GHOSTTY_KEY_NUMPAD_6
+ case .numpad7: GHOSTTY_KEY_NUMPAD_7
+ case .numpad8: GHOSTTY_KEY_NUMPAD_8
+ case .numpad9: GHOSTTY_KEY_NUMPAD_9
+ case .numpadAdd: GHOSTTY_KEY_NUMPAD_ADD
+ case .numpadBackspace: GHOSTTY_KEY_NUMPAD_BACKSPACE
+ case .numpadClear: GHOSTTY_KEY_NUMPAD_CLEAR
+ case .numpadClearEntry: GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY
+ case .numpadComma: GHOSTTY_KEY_NUMPAD_COMMA
+ case .numpadDecimal: GHOSTTY_KEY_NUMPAD_DECIMAL
+ case .numpadDivide: GHOSTTY_KEY_NUMPAD_DIVIDE
+ case .numpadEnter: GHOSTTY_KEY_NUMPAD_ENTER
+ case .numpadEqual: GHOSTTY_KEY_NUMPAD_EQUAL
+ case .numpadMemoryAdd: GHOSTTY_KEY_NUMPAD_MEMORY_ADD
+ case .numpadMemoryClear: GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR
+ case .numpadMemoryRecall: GHOSTTY_KEY_NUMPAD_MEMORY_RECALL
+ case .numpadMemoryStore: GHOSTTY_KEY_NUMPAD_MEMORY_STORE
+ case .numpadMemorySubtract: GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT
+ case .numpadMultiply: GHOSTTY_KEY_NUMPAD_MULTIPLY
+ case .numpadParenLeft: GHOSTTY_KEY_NUMPAD_PAREN_LEFT
+ case .numpadParenRight: GHOSTTY_KEY_NUMPAD_PAREN_RIGHT
+ case .numpadSubtract: GHOSTTY_KEY_NUMPAD_SUBTRACT
+ case .numpadSeparator: GHOSTTY_KEY_NUMPAD_SEPARATOR
+ case .numpadUp: GHOSTTY_KEY_NUMPAD_UP
+ case .numpadDown: GHOSTTY_KEY_NUMPAD_DOWN
+ case .numpadRight: GHOSTTY_KEY_NUMPAD_RIGHT
+ case .numpadLeft: GHOSTTY_KEY_NUMPAD_LEFT
+ case .numpadBegin: GHOSTTY_KEY_NUMPAD_BEGIN
+ case .numpadHome: GHOSTTY_KEY_NUMPAD_HOME
+ case .numpadEnd: GHOSTTY_KEY_NUMPAD_END
+ case .numpadInsert: GHOSTTY_KEY_NUMPAD_INSERT
+ case .numpadDelete: GHOSTTY_KEY_NUMPAD_DELETE
+ case .numpadPageUp: GHOSTTY_KEY_NUMPAD_PAGE_UP
+ case .numpadPageDown: GHOSTTY_KEY_NUMPAD_PAGE_DOWN
+
+ // Function Section
+ case .escape: GHOSTTY_KEY_ESCAPE
+ case .f1: GHOSTTY_KEY_F1
+ case .f2: GHOSTTY_KEY_F2
+ case .f3: GHOSTTY_KEY_F3
+ case .f4: GHOSTTY_KEY_F4
+ case .f5: GHOSTTY_KEY_F5
+ case .f6: GHOSTTY_KEY_F6
+ case .f7: GHOSTTY_KEY_F7
+ case .f8: GHOSTTY_KEY_F8
+ case .f9: GHOSTTY_KEY_F9
+ case .f10: GHOSTTY_KEY_F10
+ case .f11: GHOSTTY_KEY_F11
+ case .f12: GHOSTTY_KEY_F12
+ case .f13: GHOSTTY_KEY_F13
+ case .f14: GHOSTTY_KEY_F14
+ case .f15: GHOSTTY_KEY_F15
+ case .f16: GHOSTTY_KEY_F16
+ case .f17: GHOSTTY_KEY_F17
+ case .f18: GHOSTTY_KEY_F18
+ case .f19: GHOSTTY_KEY_F19
+ case .f20: GHOSTTY_KEY_F20
+ case .f21: GHOSTTY_KEY_F21
+ case .f22: GHOSTTY_KEY_F22
+ case .f23: GHOSTTY_KEY_F23
+ case .f24: GHOSTTY_KEY_F24
+ case .f25: GHOSTTY_KEY_F25
+ case .fn: GHOSTTY_KEY_FN
+ case .fnLock: GHOSTTY_KEY_FN_LOCK
+ case .printScreen: GHOSTTY_KEY_PRINT_SCREEN
+ case .scrollLock: GHOSTTY_KEY_SCROLL_LOCK
+ case .pause: GHOSTTY_KEY_PAUSE
+
+ // Media Keys
+ case .browserBack: GHOSTTY_KEY_BROWSER_BACK
+ case .browserFavorites: GHOSTTY_KEY_BROWSER_FAVORITES
+ case .browserForward: GHOSTTY_KEY_BROWSER_FORWARD
+ case .browserHome: GHOSTTY_KEY_BROWSER_HOME
+ case .browserRefresh: GHOSTTY_KEY_BROWSER_REFRESH
+ case .browserSearch: GHOSTTY_KEY_BROWSER_SEARCH
+ case .browserStop: GHOSTTY_KEY_BROWSER_STOP
+ case .eject: GHOSTTY_KEY_EJECT
+ case .launchApp1: GHOSTTY_KEY_LAUNCH_APP_1
+ case .launchApp2: GHOSTTY_KEY_LAUNCH_APP_2
+ case .launchMail: GHOSTTY_KEY_LAUNCH_MAIL
+ case .mediaPlayPause: GHOSTTY_KEY_MEDIA_PLAY_PAUSE
+ case .mediaSelect: GHOSTTY_KEY_MEDIA_SELECT
+ case .mediaStop: GHOSTTY_KEY_MEDIA_STOP
+ case .mediaTrackNext: GHOSTTY_KEY_MEDIA_TRACK_NEXT
+ case .mediaTrackPrevious: GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS
+ case .power: GHOSTTY_KEY_POWER
+ case .sleep: GHOSTTY_KEY_SLEEP
+ case .audioVolumeDown: GHOSTTY_KEY_AUDIO_VOLUME_DOWN
+ case .audioVolumeMute: GHOSTTY_KEY_AUDIO_VOLUME_MUTE
+ case .audioVolumeUp: GHOSTTY_KEY_AUDIO_VOLUME_UP
+ case .wakeUp: GHOSTTY_KEY_WAKE_UP
+
+ // Legacy, Non-standard, and Special Keys
+ case .copy: GHOSTTY_KEY_COPY
+ case .cut: GHOSTTY_KEY_CUT
+ case .paste: GHOSTTY_KEY_PASTE
+ }
+ }
+
+ // Based on src/input/keycodes.zig
+ var keyCode: UInt16? {
+ switch self {
+ // Writing System Keys
+ case .backquote: return 0x0032
+ case .backslash: return 0x002a
+ case .bracketLeft: return 0x0021
+ case .bracketRight: return 0x001e
+ case .comma: return 0x002b
+ case .digit0: return 0x001d
+ case .digit1: return 0x0012
+ case .digit2: return 0x0013
+ case .digit3: return 0x0014
+ case .digit4: return 0x0015
+ case .digit5: return 0x0017
+ case .digit6: return 0x0016
+ case .digit7: return 0x001a
+ case .digit8: return 0x001c
+ case .digit9: return 0x0019
+ case .equal: return 0x0018
+ case .intlBackslash: return 0x000a
+ case .intlRo: return 0x005e
+ case .intlYen: return 0x005d
+ case .a: return 0x0000
+ case .b: return 0x000b
+ case .c: return 0x0008
+ case .d: return 0x0002
+ case .e: return 0x000e
+ case .f: return 0x0003
+ case .g: return 0x0005
+ case .h: return 0x0004
+ case .i: return 0x0022
+ case .j: return 0x0026
+ case .k: return 0x0028
+ case .l: return 0x0025
+ case .m: return 0x002e
+ case .n: return 0x002d
+ case .o: return 0x001f
+ case .p: return 0x0023
+ case .q: return 0x000c
+ case .r: return 0x000f
+ case .s: return 0x0001
+ case .t: return 0x0011
+ case .u: return 0x0020
+ case .v: return 0x0009
+ case .w: return 0x000d
+ case .x: return 0x0007
+ case .y: return 0x0010
+ case .z: return 0x0006
+ case .minus: return 0x001b
+ case .period: return 0x002f
+ case .quote: return 0x0027
+ case .semicolon: return 0x0029
+ case .slash: return 0x002c
+
+ // Functional Keys
+ case .altLeft: return 0x003a
+ case .altRight: return 0x003d
+ case .backspace: return 0x0033
+ case .capsLock: return 0x0039
+ case .contextMenu: return 0x006e
+ case .controlLeft: return 0x003b
+ case .controlRight: return 0x003e
+ case .enter: return 0x0024
+ case .metaLeft: return 0x0037
+ case .metaRight: return 0x0036
+ case .shiftLeft: return 0x0038
+ case .shiftRight: return 0x003c
+ case .space: return 0x0031
+ case .tab: return 0x0030
+ case .convert: return nil // No Mac keycode
+ case .kanaMode: return nil // No Mac keycode
+ case .nonConvert: return nil // No Mac keycode
+
+ // Control Pad Section
+ case .delete: return 0x0075
+ case .end: return 0x0077
+ case .help: return nil // No Mac keycode
+ case .home: return 0x0073
+ case .insert: return 0x0072
+ case .pageDown: return 0x0079
+ case .pageUp: return 0x0074
+
+ // Arrow Pad Section
+ case .arrowDown: return 0x007d
+ case .arrowLeft: return 0x007b
+ case .arrowRight: return 0x007c
+ case .arrowUp: return 0x007e
+
+ // Numpad Section
+ case .numLock: return 0x0047
+ case .numpad0: return 0x0052
+ case .numpad1: return 0x0053
+ case .numpad2: return 0x0054
+ case .numpad3: return 0x0055
+ case .numpad4: return 0x0056
+ case .numpad5: return 0x0057
+ case .numpad6: return 0x0058
+ case .numpad7: return 0x0059
+ case .numpad8: return 0x005b
+ case .numpad9: return 0x005c
+ case .numpadAdd: return 0x0045
+ case .numpadBackspace: return nil // No Mac keycode
+ case .numpadClear: return nil // No Mac keycode
+ case .numpadClearEntry: return nil // No Mac keycode
+ case .numpadComma: return 0x005f
+ case .numpadDecimal: return 0x0041
+ case .numpadDivide: return 0x004b
+ case .numpadEnter: return 0x004c
+ case .numpadEqual: return 0x0051
+ case .numpadMemoryAdd: return nil // No Mac keycode
+ case .numpadMemoryClear: return nil // No Mac keycode
+ case .numpadMemoryRecall: return nil // No Mac keycode
+ case .numpadMemoryStore: return nil // No Mac keycode
+ case .numpadMemorySubtract: return nil // No Mac keycode
+ case .numpadMultiply: return 0x0043
+ case .numpadParenLeft: return nil // No Mac keycode
+ case .numpadParenRight: return nil // No Mac keycode
+ case .numpadSubtract: return 0x004e
+ case .numpadSeparator: return nil // No Mac keycode
+ case .numpadUp: return nil // No Mac keycode
+ case .numpadDown: return nil // No Mac keycode
+ case .numpadRight: return nil // No Mac keycode
+ case .numpadLeft: return nil // No Mac keycode
+ case .numpadBegin: return nil // No Mac keycode
+ case .numpadHome: return nil // No Mac keycode
+ case .numpadEnd: return nil // No Mac keycode
+ case .numpadInsert: return nil // No Mac keycode
+ case .numpadDelete: return nil // No Mac keycode
+ case .numpadPageUp: return nil // No Mac keycode
+ case .numpadPageDown: return nil // No Mac keycode
+
+ // Function Section
+ case .escape: return 0x0035
+ case .f1: return 0x007a
+ case .f2: return 0x0078
+ case .f3: return 0x0063
+ case .f4: return 0x0076
+ case .f5: return 0x0060
+ case .f6: return 0x0061
+ case .f7: return 0x0062
+ case .f8: return 0x0064
+ case .f9: return 0x0065
+ case .f10: return 0x006d
+ case .f11: return 0x0067
+ case .f12: return 0x006f
+ case .f13: return 0x0069
+ case .f14: return 0x006b
+ case .f15: return 0x0071
+ case .f16: return 0x006a
+ case .f17: return 0x0040
+ case .f18: return 0x004f
+ case .f19: return 0x0050
+ case .f20: return 0x005a
+ case .f21: return nil // No Mac keycode
+ case .f22: return nil // No Mac keycode
+ case .f23: return nil // No Mac keycode
+ case .f24: return nil // No Mac keycode
+ case .f25: return nil // No Mac keycode
+ case .fn: return nil // No Mac keycode
+ case .fnLock: return nil // No Mac keycode
+ case .printScreen: return nil // No Mac keycode
+ case .scrollLock: return nil // No Mac keycode
+ case .pause: return nil // No Mac keycode
+
+ // Media Keys
+ case .browserBack: return nil // No Mac keycode
+ case .browserFavorites: return nil // No Mac keycode
+ case .browserForward: return nil // No Mac keycode
+ case .browserHome: return nil // No Mac keycode
+ case .browserRefresh: return nil // No Mac keycode
+ case .browserSearch: return nil // No Mac keycode
+ case .browserStop: return nil // No Mac keycode
+ case .eject: return nil // No Mac keycode
+ case .launchApp1: return nil // No Mac keycode
+ case .launchApp2: return nil // No Mac keycode
+ case .launchMail: return nil // No Mac keycode
+ case .mediaPlayPause: return nil // No Mac keycode
+ case .mediaSelect: return nil // No Mac keycode
+ case .mediaStop: return nil // No Mac keycode
+ case .mediaTrackNext: return nil // No Mac keycode
+ case .mediaTrackPrevious: return nil // No Mac keycode
+ case .power: return nil // No Mac keycode
+ case .sleep: return nil // No Mac keycode
+ case .audioVolumeDown: return 0x0049
+ case .audioVolumeMute: return 0x004a
+ case .audioVolumeUp: return 0x0048
+ case .wakeUp: return nil // No Mac keycode
+
+ // Legacy, Non-standard, and Special Keys
+ case .copy: return nil // No Mac keycode
+ case .cut: return nil // No Mac keycode
+ case .paste: return nil // No Mac keycode
+ }
+ }
+ }
+}
+
+extension Ghostty.Input.Key: AppEnum {
+ static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Key")
+
+ // Only include keys that have Mac keycodes for App Intents
+ static var allCases: [Ghostty.Input.Key] {
+ return [
+ // Letters (A-Z)
+ .a, .b, .c, .d, .e, .f, .g, .h, .i, .j, .k, .l, .m, .n, .o, .p, .q, .r, .s, .t, .u, .v, .w, .x, .y, .z,
+
+ // Numbers (0-9)
+ .digit0, .digit1, .digit2, .digit3, .digit4, .digit5, .digit6, .digit7, .digit8, .digit9,
+
+ // Common Control Keys
+ .space, .enter, .tab, .backspace, .escape, .delete,
+
+ // Arrow Keys
+ .arrowUp, .arrowDown, .arrowLeft, .arrowRight,
+
+ // Navigation Keys
+ .home, .end, .pageUp, .pageDown, .insert,
+
+ // Function Keys (F1-F20)
+ .f1, .f2, .f3, .f4, .f5, .f6, .f7, .f8, .f9, .f10, .f11, .f12,
+ .f13, .f14, .f15, .f16, .f17, .f18, .f19, .f20,
+
+ // Modifier Keys
+ .shiftLeft, .shiftRight, .controlLeft, .controlRight, .altLeft, .altRight,
+ .metaLeft, .metaRight, .capsLock,
+
+ // Punctuation & Symbols
+ .minus, .equal, .backquote, .bracketLeft, .bracketRight, .backslash,
+ .semicolon, .quote, .comma, .period, .slash,
+
+ // Numpad
+ .numLock, .numpad0, .numpad1, .numpad2, .numpad3, .numpad4, .numpad5,
+ .numpad6, .numpad7, .numpad8, .numpad9, .numpadAdd, .numpadSubtract,
+ .numpadMultiply, .numpadDivide, .numpadDecimal, .numpadEqual,
+ .numpadEnter, .numpadComma,
+
+ // Media Keys
+ .audioVolumeUp, .audioVolumeDown, .audioVolumeMute,
+
+ // International Keys
+ .intlBackslash, .intlRo, .intlYen,
+
+ // Other
+ .contextMenu
+ ]
+ }
+
+ static var caseDisplayRepresentations: [Ghostty.Input.Key : DisplayRepresentation] = [
+ // Letters (A-Z)
+ .a: "A", .b: "B", .c: "C", .d: "D", .e: "E", .f: "F", .g: "G", .h: "H", .i: "I", .j: "J",
+ .k: "K", .l: "L", .m: "M", .n: "N", .o: "O", .p: "P", .q: "Q", .r: "R", .s: "S", .t: "T",
+ .u: "U", .v: "V", .w: "W", .x: "X", .y: "Y", .z: "Z",
+
+ // Numbers (0-9)
+ .digit0: "0", .digit1: "1", .digit2: "2", .digit3: "3", .digit4: "4",
+ .digit5: "5", .digit6: "6", .digit7: "7", .digit8: "8", .digit9: "9",
+
+ // Common Control Keys
+ .space: "Space",
+ .enter: "Enter",
+ .tab: "Tab",
+ .backspace: "Backspace",
+ .escape: "Escape",
+ .delete: "Delete",
+
+ // Arrow Keys
+ .arrowUp: "Up Arrow",
+ .arrowDown: "Down Arrow",
+ .arrowLeft: "Left Arrow",
+ .arrowRight: "Right Arrow",
+
+ // Navigation Keys
+ .home: "Home",
+ .end: "End",
+ .pageUp: "Page Up",
+ .pageDown: "Page Down",
+ .insert: "Insert",
+
+ // Function Keys (F1-F20)
+ .f1: "F1", .f2: "F2", .f3: "F3", .f4: "F4", .f5: "F5", .f6: "F6",
+ .f7: "F7", .f8: "F8", .f9: "F9", .f10: "F10", .f11: "F11", .f12: "F12",
+ .f13: "F13", .f14: "F14", .f15: "F15", .f16: "F16", .f17: "F17",
+ .f18: "F18", .f19: "F19", .f20: "F20",
+
+ // Modifier Keys
+ .shiftLeft: "Left Shift",
+ .shiftRight: "Right Shift",
+ .controlLeft: "Left Control",
+ .controlRight: "Right Control",
+ .altLeft: "Left Alt",
+ .altRight: "Right Alt",
+ .metaLeft: "Left Command",
+ .metaRight: "Right Command",
+ .capsLock: "Caps Lock",
+
+ // Punctuation & Symbols
+ .minus: "Minus (-)",
+ .equal: "Equal (=)",
+ .backquote: "Backtick (`)",
+ .bracketLeft: "Left Bracket ([)",
+ .bracketRight: "Right Bracket (])",
+ .backslash: "Backslash (\\)",
+ .semicolon: "Semicolon (;)",
+ .quote: "Quote (')",
+ .comma: "Comma (,)",
+ .period: "Period (.)",
+ .slash: "Slash (/)",
+
+ // Numpad
+ .numLock: "Num Lock",
+ .numpad0: "Numpad 0", .numpad1: "Numpad 1", .numpad2: "Numpad 2",
+ .numpad3: "Numpad 3", .numpad4: "Numpad 4", .numpad5: "Numpad 5",
+ .numpad6: "Numpad 6", .numpad7: "Numpad 7", .numpad8: "Numpad 8", .numpad9: "Numpad 9",
+ .numpadAdd: "Numpad Add (+)",
+ .numpadSubtract: "Numpad Subtract (-)",
+ .numpadMultiply: "Numpad Multiply (×)",
+ .numpadDivide: "Numpad Divide (÷)",
+ .numpadDecimal: "Numpad Decimal",
+ .numpadEqual: "Numpad Equal",
+ .numpadEnter: "Numpad Enter",
+ .numpadComma: "Numpad Comma",
+
+ // Media Keys
+ .audioVolumeUp: "Volume Up",
+ .audioVolumeDown: "Volume Down",
+ .audioVolumeMute: "Volume Mute",
+
+ // International Keys
+ .intlBackslash: "International Backslash",
+ .intlRo: "International Ro",
+ .intlYen: "International Yen",
+
+ // Other
+ .contextMenu: "Context Menu"
+ ]
}
diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift
new file mode 100644
index 000000000..c7198e147
--- /dev/null
+++ b/macos/Sources/Ghostty/Ghostty.Surface.swift
@@ -0,0 +1,149 @@
+import GhosttyKit
+
+extension Ghostty {
+ /// Represents a single surface within Ghostty.
+ ///
+ /// NOTE(mitchellh): This is a work-in-progress class as part of a general refactor
+ /// of our Ghostty data model. At the time of writing there's still a ton of surface
+ /// functionality that is not encapsulated in this class. It is planned to migrate that
+ /// all over.
+ ///
+ /// Wraps a `ghostty_surface_t`
+ final class Surface: Sendable {
+ private let surface: ghostty_surface_t
+
+ /// Read the underlying C value for this surface. This is unsafe because the value will be
+ /// freed when the Surface class is deinitialized.
+ var unsafeCValue: ghostty_surface_t {
+ surface
+ }
+
+ /// Initialize from the C structure.
+ init(cSurface: ghostty_surface_t) {
+ self.surface = cSurface
+ }
+
+ deinit {
+ // deinit is not guaranteed to happen on the main actor and our API
+ // calls into libghostty must happen there so we capture the surface
+ // value so we don't capture `self` and then we detach it in a task.
+ // We can't wait for the task to succeed so this will happen sometime
+ // but that's okay.
+ let surface = self.surface
+ Task.detached { @MainActor in
+ ghostty_surface_free(surface)
+ }
+ }
+
+ /// Send text to the terminal as if it was typed. This doesn't send the key events so keyboard
+ /// shortcuts and other encodings do not take effect.
+ @MainActor
+ func sendText(_ text: String) {
+ let len = text.utf8CString.count
+ if (len == 0) { return }
+
+ text.withCString { ptr in
+ // len includes the null terminator so we do len - 1
+ ghostty_surface_text(surface, ptr, UInt(len - 1))
+ }
+ }
+
+ /// Send a key event to the terminal.
+ ///
+ /// This sends the full key event including modifiers, action type, and text to the terminal.
+ /// Unlike `sendText`, this method processes keyboard shortcuts, key bindings, and terminal
+ /// encoding based on the complete key event information.
+ ///
+ /// - Parameter event: The key event to send to the terminal
+ @MainActor
+ func sendKeyEvent(_ event: Input.KeyEvent) {
+ event.withCValue { cEvent in
+ ghostty_surface_key(surface, cEvent)
+ }
+ }
+
+ /// Whether the terminal has captured mouse input.
+ ///
+ /// When the mouse is captured, the terminal application is receiving mouse events
+ /// directly rather than the host system handling them. This typically occurs when
+ /// a terminal application enables mouse reporting mode.
+ @MainActor
+ var mouseCaptured: Bool {
+ ghostty_surface_mouse_captured(surface)
+ }
+
+ /// Send a mouse button event to the terminal.
+ ///
+ /// This sends a complete mouse button event including the button state (press/release),
+ /// which button was pressed, and any modifier keys that were held during the event.
+ /// The terminal processes this event according to its mouse handling configuration.
+ ///
+ /// - Parameter event: The mouse button event to send to the terminal
+ @MainActor
+ func sendMouseButton(_ event: Input.MouseButtonEvent) {
+ ghostty_surface_mouse_button(
+ surface,
+ event.action.cMouseState,
+ event.button.cMouseButton,
+ event.mods.cMods)
+ }
+
+ /// Send a mouse position event to the terminal.
+ ///
+ /// This reports the current mouse position to the terminal, which may be used
+ /// for mouse tracking, hover effects, or other position-dependent features.
+ /// The terminal will only receive these events if mouse reporting is enabled.
+ ///
+ /// - Parameter event: The mouse position event to send to the terminal
+ @MainActor
+ func sendMousePos(_ event: Input.MousePosEvent) {
+ ghostty_surface_mouse_pos(
+ surface,
+ event.x,
+ event.y,
+ event.mods.cMods)
+ }
+
+ /// Send a mouse scroll event to the terminal.
+ ///
+ /// This sends scroll wheel input to the terminal with delta values for both
+ /// horizontal and vertical scrolling, along with precision and momentum information.
+ /// The terminal processes this according to its scroll handling configuration.
+ ///
+ /// - Parameter event: The mouse scroll event to send to the terminal
+ @MainActor
+ func sendMouseScroll(_ event: Input.MouseScrollEvent) {
+ ghostty_surface_mouse_scroll(
+ surface,
+ event.x,
+ event.y,
+ event.mods.cScrollMods)
+ }
+
+ /// Perform a keybinding action.
+ ///
+ /// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4`
+ /// you can perform `goto_tab:4` with this.
+ ///
+ /// Returns true if the action was performed. Invalid actions return false.
+ @MainActor
+ func perform(action: String) -> Bool {
+ let len = action.utf8CString.count
+ if (len == 0) { return false }
+ return action.withCString { cString in
+ ghostty_surface_binding_action(surface, cString, UInt(len - 1))
+ }
+ }
+
+ /// Command options for this surface.
+ @MainActor
+ func commands() throws -> [Command] {
+ var ptr: UnsafeMutablePointer? = nil
+ var count: Int = 0
+ ghostty_surface_commands(surface, &ptr, &count)
+ guard let ptr else { throw Error.apiFailed }
+ let buffer = UnsafeBufferPointer(start: ptr, count: count)
+ return Array(buffer).map { Command(cValue: $0) }.filter { $0.isSupported }
+ }
+ }
+}
diff --git a/macos/Sources/Ghostty/InspectorView.swift b/macos/Sources/Ghostty/InspectorView.swift
index a6e80bd47..8008e49c2 100644
--- a/macos/Sources/Ghostty/InspectorView.swift
+++ b/macos/Sources/Ghostty/InspectorView.swift
@@ -337,9 +337,9 @@ extension Ghostty {
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
guard let inspector = self.inspector else { return }
- guard let key = Ghostty.keycodeToKey[event.keyCode] else { return }
+ guard let key = Ghostty.Input.Key(keyCode: event.keyCode) else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags)
- ghostty_inspector_key(inspector, action, key, mods)
+ ghostty_inspector_key(inspector, action, key.cKey, mods)
}
// MARK: NSTextInputClient
diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift
index 82721c17e..125a09825 100644
--- a/macos/Sources/Ghostty/Package.swift
+++ b/macos/Sources/Ghostty/Package.swift
@@ -19,6 +19,15 @@ struct Ghostty {
static let userNotificationActionShow = "com.mitchellh.ghostty.userNotification.Show"
}
+// MARK: C Extensions
+
+/// A command is fully self-contained so it is Sendable.
+extension ghostty_command_s: @unchecked @retroactive Sendable {}
+
+/// A surface is sendable because it is just a reference type. Using the surface in parameters
+/// may be unsafe but the value itself is safe to send across threads.
+extension ghostty_surface_t: @unchecked @retroactive Sendable {}
+
// MARK: Build Info
extension Ghostty {
diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift
index f830da4ef..aa4de5178 100644
--- a/macos/Sources/Ghostty/SurfaceView.swift
+++ b/macos/Sources/Ghostty/SurfaceView.swift
@@ -79,7 +79,7 @@ extension Ghostty {
let pubResign = center.publisher(for: NSWindow.didResignKeyNotification)
#endif
- Surface(view: surfaceView, size: geo.size)
+ SurfaceRepresentable(view: surfaceView, size: geo.size)
.focused($surfaceFocus)
.focusedValue(\.ghosttySurfacePwd, surfaceView.pwd)
.focusedValue(\.ghosttySurfaceView, surfaceView)
@@ -381,7 +381,7 @@ extension Ghostty {
/// We just wrap an AppKit NSView here at the moment so that we can behave as low level as possible
/// since that is what the Metal renderer in Ghostty expects. In the future, it may make more sense to
/// wrap an MTKView and use that, but for legacy reasons we didn't do that to begin with.
- struct Surface: OSViewRepresentable {
+ struct SurfaceRepresentable: OSViewRepresentable {
/// The view to render for the terminal surface.
let view: SurfaceView
@@ -418,28 +418,48 @@ extension Ghostty {
/// Explicit command to set
var command: String? = nil
+
+ /// Environment variables to set for the terminal
+ var environmentVariables: [String: String] = [:]
+
+ /// Extra input to send as stdin
+ var initialInput: String? = nil
init() {}
init(from config: ghostty_surface_config_s) {
self.fontSize = config.font_size
- self.workingDirectory = String.init(cString: config.working_directory, encoding: .utf8)
- self.command = String.init(cString: config.command, encoding: .utf8)
+ if let workingDirectory = config.working_directory {
+ self.workingDirectory = String.init(cString: workingDirectory, encoding: .utf8)
+ }
+ if let command = config.command {
+ self.command = String.init(cString: command, encoding: .utf8)
+ }
+
+ // Convert the C env vars to Swift dictionary
+ if config.env_var_count > 0, let envVars = config.env_vars {
+ for i in 0.. ghostty_surface_config_s {
+ /// Provides a C-compatible ghostty configuration within a closure. The configuration
+ /// and all its string pointers are only valid within the closure.
+ func withCValue(view: SurfaceView, _ body: (inout ghostty_surface_config_s) throws -> T) rethrows -> T {
var config = ghostty_surface_config_new()
config.userdata = Unmanaged.passUnretained(view).toOpaque()
- #if os(macOS)
+#if os(macOS)
config.platform_tag = GHOSTTY_PLATFORM_MACOS
config.platform = ghostty_platform_u(macos: ghostty_platform_macos_s(
nsview: Unmanaged.passUnretained(view).toOpaque()
))
config.scale_factor = NSScreen.main!.backingScaleFactor
-
- #elseif os(iOS)
+#elseif os(iOS)
config.platform_tag = GHOSTTY_PLATFORM_IOS
config.platform = ghostty_platform_u(ios: ghostty_platform_ios_s(
uiview: Unmanaged.passUnretained(view).toOpaque()
@@ -449,19 +469,50 @@ extension Ghostty {
// probably set this to some default, then modify the scale factor through
// libghostty APIs when a UIView is attached to a window/scene. TODO.
config.scale_factor = UIScreen.main.scale
- #else
- #error("unsupported target")
- #endif
+#else
+#error("unsupported target")
+#endif
- if let fontSize = fontSize { config.font_size = fontSize }
- if let workingDirectory = workingDirectory {
- config.working_directory = (workingDirectory as NSString).utf8String
- }
- if let command = command {
- config.command = (command as NSString).utf8String
- }
+ // Zero is our default value that means to inherit the font size.
+ config.font_size = fontSize ?? 0
- return config
+ // Use withCString to ensure strings remain valid for the duration of the closure
+ return try workingDirectory.withCString { cWorkingDir in
+ config.working_directory = cWorkingDir
+
+ return try command.withCString { cCommand in
+ config.command = cCommand
+
+ return try initialInput.withCString { cInput in
+ config.initial_input = cInput
+
+ // Convert dictionary to arrays for easier processing
+ let keys = Array(environmentVariables.keys)
+ let values = Array(environmentVariables.values)
+
+ // Create C strings for all keys and values
+ return try keys.withCStrings { keyCStrings in
+ return try values.withCStrings { valueCStrings in
+ // Create array of ghostty_env_var_s
+ var envVars = Array()
+ envVars.reserveCapacity(environmentVariables.count)
+ for i in 0.. = []
- private(set) var surface: ghostty_surface_t?
private var markedText: NSMutableAttributedString
private(set) var focused: Bool = true
private var prevPressureStage: Int = 0
@@ -139,7 +149,8 @@ extension Ghostty {
private var titleFromTerminal: String?
// The cached contents of the screen.
- private var cachedScreenContents: CachedValue
+ private(set) var cachedScreenContents: CachedValue
+ private(set) var cachedVisibleContents: CachedValue
/// Event monitor (see individual events for why)
private var eventMonitor: Any? = nil
@@ -147,10 +158,6 @@ extension Ghostty {
// We need to support being a first responder so that we can get input events
override var acceptsFirstResponder: Bool { return true }
- // I don't think we need this but this lets us know we should redraw our layer
- // so we'll use that to tell ghostty to refresh.
- override var wantsUpdateLayer: Bool { return true }
-
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
self.markedText = NSMutableAttributedString()
self.uuid = uuid ?? .init()
@@ -166,6 +173,7 @@ extension Ghostty {
// it back up later so we can reference `self`. This is a hack we should
// fix at some point.
self.cachedScreenContents = .init(duration: .milliseconds(500)) { "" }
+ self.cachedVisibleContents = self.cachedScreenContents
// Initialize with some default frame size. The important thing is that this
// is non-zero so that our layer bounds are non-zero so that our renderer
@@ -193,6 +201,26 @@ extension Ghostty {
defer { ghostty_surface_free_text(surface, &text) }
return String(cString: text.text)
}
+ cachedVisibleContents = .init(duration: .milliseconds(500)) { [weak self] in
+ guard let self else { return "" }
+ guard let surface = self.surface else { return "" }
+ var text = ghostty_text_s()
+ let sel = ghostty_selection_s(
+ top_left: ghostty_point_s(
+ tag: GHOSTTY_POINT_VIEWPORT,
+ coord: GHOSTTY_POINT_COORD_TOP_LEFT,
+ x: 0,
+ y: 0),
+ bottom_right: ghostty_point_s(
+ tag: GHOSTTY_POINT_VIEWPORT,
+ coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
+ x: 0,
+ y: 0),
+ rectangle: false)
+ guard ghostty_surface_read_text(surface, sel, &text) else { return "" }
+ defer { ghostty_surface_free_text(surface, &text) }
+ return String(cString: text.text)
+ }
// Set a timer to show the ghost emoji after 500ms if no title is set
titleFallbackTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
@@ -258,12 +286,14 @@ extension Ghostty {
// Setup our surface. This will also initialize all the terminal IO.
let surface_cfg = baseConfig ?? SurfaceConfiguration()
- var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
- guard let surface = ghostty_surface_new(app, &surface_cfg_c) else {
- self.error = AppError.surfaceCreateError
+ let surface = surface_cfg.withCValue(view: self) { surface_cfg_c in
+ ghostty_surface_new(app, &surface_cfg_c)
+ }
+ guard let surface = surface else {
+ self.error = Ghostty.Error.apiFailed
return
}
- self.surface = surface;
+ self.surfaceModel = Ghostty.Surface(cSurface: surface)
// Setup our tracking area so we get mouse moved events
updateTrackingAreas()
@@ -318,11 +348,6 @@ extension Ghostty {
// Remove any notifications associated with this surface
let identifiers = Array(self.notificationIdentifiers)
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
-
- // Free our core surface resources
- if let surface = self.surface {
- ghostty_surface_free(surface)
- }
}
func focusDidChange(_ focused: Bool) {
@@ -703,11 +728,6 @@ extension Ghostty {
setSurfaceSize(width: UInt32(fbFrame.size.width), height: UInt32(fbFrame.size.height))
}
- override func updateLayer() {
- guard let surface = self.surface else { return }
- ghostty_surface_draw(surface);
- }
-
override func mouseDown(with event: NSEvent) {
guard let surface = self.surface else { return }
let mods = Ghostty.ghosttyMods(event.modifierFlags)
@@ -781,19 +801,23 @@ extension Ghostty {
override func mouseEntered(with event: NSEvent) {
super.mouseEntered(with: event)
- guard let surface = self.surface else { return }
+ guard let surfaceModel else { return }
// On mouse enter we need to reset our cursor position. This is
// super important because we set it to -1/-1 on mouseExit and
// lots of mouse logic (i.e. whether to send mouse reports) depend
// on the position being in the viewport if it is.
let pos = self.convert(event.locationInWindow, from: nil)
- let mods = Ghostty.ghosttyMods(event.modifierFlags)
- ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods)
+ let mouseEvent = Ghostty.Input.MousePosEvent(
+ x: pos.x,
+ y: frame.height - pos.y,
+ mods: .init(nsFlags: event.modifierFlags)
+ )
+ surfaceModel.sendMousePos(mouseEvent)
}
override func mouseExited(with event: NSEvent) {
- guard let surface = self.surface else { return }
+ guard let surfaceModel else { return }
// If the mouse is being dragged then we don't have to emit
// this because we get mouse drag events even if we've already
@@ -803,17 +827,25 @@ extension Ghostty {
}
// Negative values indicate cursor has left the viewport
- let mods = Ghostty.ghosttyMods(event.modifierFlags)
- ghostty_surface_mouse_pos(surface, -1, -1, mods)
+ let mouseEvent = Ghostty.Input.MousePosEvent(
+ x: -1,
+ y: -1,
+ mods: .init(nsFlags: event.modifierFlags)
+ )
+ surfaceModel.sendMousePos(mouseEvent)
}
override func mouseMoved(with event: NSEvent) {
- guard let surface = self.surface else { return }
+ guard let surfaceModel else { return }
// Convert window position to view position. Note (0, 0) is bottom left.
let pos = self.convert(event.locationInWindow, from: nil)
- let mods = Ghostty.ghosttyMods(event.modifierFlags)
- ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods)
+ let mouseEvent = Ghostty.Input.MousePosEvent(
+ x: pos.x,
+ y: frame.height - pos.y,
+ mods: .init(nsFlags: event.modifierFlags)
+ )
+ surfaceModel.sendMousePos(mouseEvent)
// Handle focus-follows-mouse
if let window,
@@ -839,16 +871,13 @@ extension Ghostty {
}
override func scrollWheel(with event: NSEvent) {
- guard let surface = self.surface else { return }
-
- // Builds up the "input.ScrollMods" bitmask
- var mods: Int32 = 0
+ guard let surfaceModel else { return }
var x = event.scrollingDeltaX
var y = event.scrollingDeltaY
- if event.hasPreciseScrollingDeltas {
- mods = 1
-
+ let precision = event.hasPreciseScrollingDeltas
+
+ if precision {
// We do a 2x speed multiplier. This is subjective, it "feels" better to me.
x *= 2;
y *= 2;
@@ -856,29 +885,12 @@ extension Ghostty {
// TODO(mitchellh): do we have to scale the x/y here by window scale factor?
}
- // Determine our momentum value
- var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE
- switch (event.momentumPhase) {
- case .began:
- momentum = GHOSTTY_MOUSE_MOMENTUM_BEGAN
- case .stationary:
- momentum = GHOSTTY_MOUSE_MOMENTUM_STATIONARY
- case .changed:
- momentum = GHOSTTY_MOUSE_MOMENTUM_CHANGED
- case .ended:
- momentum = GHOSTTY_MOUSE_MOMENTUM_ENDED
- case .cancelled:
- momentum = GHOSTTY_MOUSE_MOMENTUM_CANCELLED
- case .mayBegin:
- momentum = GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN
- default:
- break
- }
-
- // Pack our momentum value into the mods bitmask
- mods |= Int32(momentum.rawValue) << 1
-
- ghostty_surface_mouse_scroll(surface, x, y, mods)
+ let scrollEvent = Ghostty.Input.MouseScrollEvent(
+ x: x,
+ y: y,
+ mods: .init(precision: precision, momentum: .init(event.momentumPhase))
+ )
+ surfaceModel.sendMouseScroll(scrollEvent)
}
override func pressureChange(with event: NSEvent) {
@@ -1285,8 +1297,8 @@ extension Ghostty {
// In this case, AppKit calls menu BEFORE calling any mouse events.
// If mouse capturing is enabled then we never show the context menu
// so that we can handle ctrl+left-click in the terminal app.
- guard let surface = self.surface else { return nil }
- if ghostty_surface_mouse_captured(surface) {
+ guard let surfaceModel else { return nil }
+ if surfaceModel.mouseCaptured {
return nil
}
@@ -1296,13 +1308,10 @@ extension Ghostty {
//
// Note this never sounds a right mouse up event but that's the
// same as normal right-click with capturing disabled from AppKit.
- let mods = Ghostty.ghosttyMods(event.modifierFlags)
- ghostty_surface_mouse_button(
- surface,
- GHOSTTY_MOUSE_PRESS,
- GHOSTTY_MOUSE_RIGHT,
- mods
- )
+ surfaceModel.sendMouseButton(.init(
+ action: .press,
+ button: .right,
+ mods: .init(nsFlags: event.modifierFlags)))
default:
return nil
@@ -1673,7 +1682,7 @@ extension Ghostty.SurfaceView: NSTextInputClient {
func insertText(_ string: Any, replacementRange: NSRange) {
// We must have an associated event
guard NSApp.currentEvent != nil else { return }
- guard let surface = self.surface else { return }
+ guard let surfaceModel else { return }
// We want the string view of the any value
var chars = ""
@@ -1697,13 +1706,7 @@ extension Ghostty.SurfaceView: NSTextInputClient {
return
}
- let len = chars.utf8CString.count
- if (len == 0) { return }
-
- chars.withCString { ptr in
- // len includes the null terminator so we do len - 1
- ghostty_surface_text(surface, ptr, UInt(len - 1))
- }
+ surfaceModel.sendText(chars)
}
/// This function needs to exist for two reasons:
@@ -1979,7 +1982,7 @@ extension Ghostty.SurfaceView {
/// Caches a value for some period of time, evicting it automatically when that time expires.
/// We use this to cache our surface content. This probably should be extracted some day
/// to a more generic helper.
-fileprivate class CachedValue {
+class CachedValue {
private var value: T?
private let fetch: () -> T
private let duration: Duration
diff --git a/macos/Sources/Ghostty/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/SurfaceView_UIKit.swift
index 8d5b3038f..e88ec82e2 100644
--- a/macos/Sources/Ghostty/SurfaceView_UIKit.swift
+++ b/macos/Sources/Ghostty/SurfaceView_UIKit.swift
@@ -57,8 +57,10 @@ extension Ghostty {
// Setup our surface. This will also initialize all the terminal IO.
let surface_cfg = baseConfig ?? SurfaceConfiguration()
- var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
- guard let surface = ghostty_surface_new(app, &surface_cfg_c) else {
+ let surface = surface_cfg.withCValue(view: self) { surface_cfg_c in
+ ghostty_surface_new(app, &surface_cfg_c)
+ }
+ guard let surface = surface else {
// TODO
return
}
diff --git a/macos/Sources/Helpers/AppInfo.swift b/macos/Sources/Helpers/AppInfo.swift
index cf66e332d..281bad18b 100644
--- a/macos/Sources/Helpers/AppInfo.swift
+++ b/macos/Sources/Helpers/AppInfo.swift
@@ -8,37 +8,3 @@ func isRunningInXcode() -> Bool {
return false
}
-
-/// True if we have liquid glass available.
-func hasLiquidGlass() -> Bool {
- // Can't have liquid glass unless we're in macOS 26+
- if #unavailable(macOS 26.0) {
- return false
- }
-
- // If we aren't running SDK 26.0 or later then we definitely
- // do not have liquid glass.
- guard let sdkName = Bundle.main.infoDictionary?["DTSDKName"] as? String else {
- // If we don't have this, we assume we're built against the latest
- // since we're on macOS 26+
- return true
- }
-
- // If the SDK doesn't start with macosx then we just assume we
- // have it because we already verified we're on macOS above.
- guard sdkName.hasPrefix("macosx") else {
- return true
- }
-
- // The SDK version must be at least 26
- let versionString = String(sdkName.dropFirst("macosx".count))
- guard let major = if let dotIndex = versionString.firstIndex(of: ".") {
- Int(String(versionString[..= 26
-}
diff --git a/macos/Sources/Helpers/Extensions/Array+Extension.swift b/macos/Sources/Helpers/Extensions/Array+Extension.swift
index 12f2de43d..4e8e39918 100644
--- a/macos/Sources/Helpers/Extensions/Array+Extension.swift
+++ b/macos/Sources/Helpers/Extensions/Array+Extension.swift
@@ -21,3 +21,28 @@ extension Array {
return i + 1
}
}
+
+extension Array where Element == String {
+ /// Executes a closure with an array of C string pointers.
+ func withCStrings(_ body: ([UnsafePointer?]) throws -> T) rethrows -> T {
+ // Handle empty array
+ if isEmpty {
+ return try body([])
+ }
+
+ // Recursive helper to process strings
+ func helper(index: Int, accumulated: [UnsafePointer?], body: ([UnsafePointer?]) throws -> T) rethrows -> T {
+ if index == count {
+ return try body(accumulated)
+ }
+
+ return try self[index].withCString { cStr in
+ var newAccumulated = accumulated
+ newAccumulated.append(cStr)
+ return try helper(index: index + 1, accumulated: newAccumulated, body: body)
+ }
+ }
+
+ return try helper(index: 0, accumulated: [], body: body)
+ }
+}
diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift
index b3628d406..fb209e4ac 100644
--- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift
+++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift
@@ -1,4 +1,5 @@
import AppKit
+import SwiftUI
extension NSView {
/// Returns true if this view is currently in the responder chain
@@ -15,6 +16,24 @@ extension NSView {
}
}
+// MARK: Screenshot
+
+extension NSView {
+ /// Take a screenshot of just this view.
+ func screenshot() -> NSImage? {
+ guard let bitmapRep = bitmapImageRepForCachingDisplay(in: bounds) else { return nil }
+ cacheDisplay(in: bounds, to: bitmapRep)
+ let image = NSImage(size: bounds.size)
+ image.addRepresentation(bitmapRep)
+ return image
+ }
+
+ func screenshot() -> Image? {
+ guard let nsImage: NSImage = self.screenshot() else { return nil }
+ return Image(nsImage: nsImage)
+ }
+}
+
// MARK: View Traversal and Search
extension NSView {
diff --git a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift
index 06a9fa4e0..f9ed364aa 100644
--- a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift
+++ b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift
@@ -9,4 +9,10 @@ extension NSWindow {
guard windowNumber > 0 else { return nil }
return CGWindowID(windowNumber)
}
+
+ /// True if this is the first window in the tab group.
+ var isFirstWindowInTabGroup: Bool {
+ guard let firstWindow = tabGroup?.windows.first else { return true }
+ return firstWindow === self
+ }
}
diff --git a/macos/Sources/Helpers/Extensions/Optional+Extension.swift b/macos/Sources/Helpers/Extensions/Optional+Extension.swift
new file mode 100644
index 000000000..a844c0fe9
--- /dev/null
+++ b/macos/Sources/Helpers/Extensions/Optional+Extension.swift
@@ -0,0 +1,10 @@
+extension Optional where Wrapped == String {
+ /// Executes a closure with a C string pointer, handling nil gracefully.
+ func withCString(_ body: (UnsafePointer?) throws -> T) rethrows -> T {
+ if let string = self {
+ return try string.withCString(body)
+ } else {
+ return try body(nil)
+ }
+ }
+}
diff --git a/macos/Sources/Helpers/PermissionRequest.swift b/macos/Sources/Helpers/PermissionRequest.swift
new file mode 100644
index 000000000..9c16c7163
--- /dev/null
+++ b/macos/Sources/Helpers/PermissionRequest.swift
@@ -0,0 +1,213 @@
+import AppKit
+import Foundation
+
+/// Displays a permission request dialog with optional caching of user decisions
+class PermissionRequest {
+ /// Specifies how long a permission decision should be cached
+ enum AllowDuration {
+ case once
+ case forever
+ case duration(Duration)
+ }
+
+ /// Shows a permission request dialog with customizable caching behavior
+ /// - Parameters:
+ /// - key: Unique identifier for storing/retrieving cached decisions in UserDefaults
+ /// - message: The message to display in the alert dialog
+ /// - allowText: Custom text for the allow button (defaults to "Allow")
+ /// - allowDuration: If provided, automatically cache "Allow" responses for this duration
+ /// - rememberDuration: If provided, shows a checkbox to remember the decision for this duration
+ /// - window: If provided, shows the alert as a sheet attached to this window
+ /// - completion: Called with the user's decision (true for allow, false for deny)
+ ///
+ /// Caching behavior:
+ /// - If rememberDuration is provided and user checks "Remember my decision", both allow/deny are cached for that duration
+ /// - If allowDuration is provided and user selects allow (without checkbox), decision is cached for that duration
+ /// - Cached decisions are automatically returned without showing the dialog
+ @MainActor
+ static func show(
+ _ key: String,
+ message: String,
+ informative: String = "",
+ allowText: String = "Allow",
+ allowDuration: AllowDuration = .once,
+ rememberDuration: Duration? = .seconds(86400),
+ window: NSWindow? = nil,
+ completion: @escaping (Bool) -> Void
+ ) {
+ // Check if we have a stored decision that hasn't expired
+ if let storedResult = getStoredResult(for: key) {
+ completion(storedResult)
+ return
+ }
+
+ let alert = NSAlert()
+ alert.messageText = message
+ alert.informativeText = informative
+ alert.alertStyle = .informational
+
+ // Add buttons (they appear in reverse order)
+ alert.addButton(withTitle: allowText)
+ alert.addButton(withTitle: "Don't Allow")
+
+ // Create checkbox for remembering if duration is provided
+ var checkbox: NSButton?
+ if let rememberDuration = rememberDuration {
+ let checkboxTitle = formatRememberText(for: rememberDuration)
+ checkbox = NSButton(
+ checkboxWithTitle: checkboxTitle,
+ target: nil,
+ action: nil)
+ checkbox!.state = .off
+
+ // Set checkbox as accessory view
+ alert.accessoryView = checkbox
+ }
+
+ // Show the alert
+ if let window = window {
+ alert.beginSheetModal(for: window) { response in
+ handleResponse(response, rememberDecision: checkbox?.state == .on, key: key, allowDuration: allowDuration, rememberDuration: rememberDuration, completion: completion)
+ }
+ } else {
+ let response = alert.runModal()
+ handleResponse(response, rememberDecision: checkbox?.state == .on, key: key, allowDuration: allowDuration, rememberDuration: rememberDuration, completion: completion)
+ }
+ }
+
+ /// Handles the alert response and processes caching logic
+ /// - Parameters:
+ /// - response: The alert response from the user
+ /// - rememberDecision: Whether the remember checkbox was checked
+ /// - key: The UserDefaults key for caching
+ /// - allowDuration: Optional duration for auto-caching allow responses
+ /// - rememberDuration: Optional duration for the remember checkbox
+ /// - completion: Completion handler to call with the result
+ private static func handleResponse(
+ _ response: NSApplication.ModalResponse,
+ rememberDecision: Bool,
+ key: String,
+ allowDuration: AllowDuration,
+ rememberDuration: Duration?,
+ completion: @escaping (Bool) -> Void) {
+
+ let result: Bool
+ switch response {
+ case .alertFirstButtonReturn: // Allow
+ result = true
+ case .alertSecondButtonReturn: // Don't Allow
+ result = false
+ default:
+ result = false
+ }
+
+ // Store the result if checkbox is checked or if "Allow" was selected and allowDuration is set
+ if rememberDecision, let rememberDuration = rememberDuration {
+ storeResult(result, for: key, duration: rememberDuration)
+ } else if result {
+ switch allowDuration {
+ case .once:
+ // Don't store anything for once
+ break
+ case .forever:
+ // Store for a very long time (100 years). When the bug comes in that
+ // 100 years has passed and their forever permission expired I'll be
+ // dead so it won't be my problem.
+ storeResult(result, for: key, duration: .seconds(3153600000))
+ case .duration(let duration):
+ storeResult(result, for: key, duration: duration)
+ }
+ }
+
+ completion(result)
+ }
+
+ /// Retrieves a cached permission decision if it hasn't expired
+ /// - Parameter key: The UserDefaults key to check
+ /// - Returns: The cached decision, or nil if no valid cached decision exists
+ private static func getStoredResult(for key: String) -> Bool? {
+ let userDefaults = UserDefaults.standard
+ guard let data = userDefaults.data(forKey: key),
+ let storedPermission = try? NSKeyedUnarchiver.unarchivedObject(
+ ofClass: StoredPermission.self, from: data) else {
+ return nil
+ }
+
+ if Date() > storedPermission.expiry {
+ // Decision has expired, remove stored value
+ userDefaults.removeObject(forKey: key)
+ return nil
+ }
+
+ return storedPermission.result
+ }
+
+ /// Stores a permission decision in UserDefaults with an expiration date
+ /// - Parameters:
+ /// - result: The permission decision to store
+ /// - key: The UserDefaults key to store under
+ /// - duration: How long the decision should be cached
+ private static func storeResult(_ result: Bool, for key: String, duration: Duration) {
+ let expiryDate = Date().addingTimeInterval(duration.timeInterval)
+ let storedPermission = StoredPermission(result: result, expiry: expiryDate)
+ if let data = try? NSKeyedArchiver.archivedData(withRootObject: storedPermission, requiringSecureCoding: true) {
+ let userDefaults = UserDefaults.standard
+ userDefaults.set(data, forKey: key)
+ }
+ }
+
+ /// Formats the remember checkbox text based on the duration
+ /// - Parameter duration: The duration to format
+ /// - Returns: A human-readable string for the checkbox
+ private static func formatRememberText(for duration: Duration) -> String {
+ let seconds = duration.timeInterval
+
+ // Warning: this probably isn't localization friendly at all so we're
+ // going to have to redo this for that.
+ switch seconds {
+ case 0..<60:
+ return "Remember my decision for \(Int(seconds)) seconds"
+ case 60..<3600:
+ let minutes = Int(seconds / 60)
+ return "Remember my decision for \(minutes) minute\(minutes == 1 ? "" : "s")"
+ case 3600..<86400:
+ let hours = Int(seconds / 3600)
+ return "Remember my decision for \(hours) hour\(hours == 1 ? "" : "s")"
+ case 86400:
+ return "Remember my decision for one day"
+ default:
+ let days = Int(seconds / 86400)
+ return "Remember my decision for \(days) day\(days == 1 ? "" : "s")"
+ }
+ }
+
+ /// Internal class for storing permission decisions with expiration dates in UserDefaults
+ /// Conforms to NSSecureCoding for safe archiving/unarchiving
+ @objc(StoredPermission)
+ private class StoredPermission: NSObject, NSSecureCoding {
+ static var supportsSecureCoding: Bool = true
+
+ let result: Bool
+ let expiry: Date
+
+ init(result: Bool, expiry: Date) {
+ self.result = result
+ self.expiry = expiry
+ super.init()
+ }
+
+ required init?(coder: NSCoder) {
+ self.result = coder.decodeBool(forKey: "result")
+ guard let expiry = coder.decodeObject(of: NSDate.self, forKey: "expiry") as? Date else {
+ return nil
+ }
+ self.expiry = expiry
+ super.init()
+ }
+
+ func encode(with coder: NSCoder) {
+ coder.encode(result, forKey: "result")
+ coder.encode(expiry, forKey: "expiry")
+ }
+ }
+}
diff --git a/macos/Sources/Helpers/TabGroupCloseCoordinator.swift b/macos/Sources/Helpers/TabGroupCloseCoordinator.swift
new file mode 100644
index 000000000..ca41bf89c
--- /dev/null
+++ b/macos/Sources/Helpers/TabGroupCloseCoordinator.swift
@@ -0,0 +1,124 @@
+import AppKit
+
+/// Coordinates close operations for windows that are part of a tab group.
+///
+/// This coordinator helps distinguish between closing a single tab versus closing
+/// an entire window (with all its tabs). When macOS native tabs are used, close
+/// operations can be ambiguous - this coordinator tracks close requests across
+/// multiple windows in a tab group to determine the user's intent.
+class TabGroupCloseCoordinator {
+ /// The scope of a close operation.
+ enum CloseScope {
+ case tab
+ case window
+ }
+
+ /// Protocol that window controllers must implement to use the coordinator.
+ protocol Controller {
+ /// The tab group close coordinator instance for this controller.
+ var tabGroupCloseCoordinator: TabGroupCloseCoordinator { get }
+ }
+
+ /// Callback type for close operations.
+ typealias Callback = (CloseScope) -> Void
+
+ // We use weak vars and ObjectIdentifiers below because we don't want to
+ // create any strong reference cycles during coordination.
+
+ /// The tab group being coordinated. Weak reference to avoid cycles.
+ private weak var tabGroup: NSWindowTabGroup?
+
+ /// Map of window identifiers to their close callbacks.
+ private var closeRequests: [ObjectIdentifier: Callback] = [:]
+
+ /// Timer used to debounce close requests and determine intent.
+ private var debounceTimer: Timer?
+
+ deinit {
+ trigger(.tab)
+ }
+
+ /// Call this from the windowShouldClose override in order to track whether
+ /// a window close event is from a tab or a window. If this window already
+ /// requested a close then only the latest will be called.
+ func windowShouldClose(
+ _ window: NSWindow,
+ callback: @escaping Callback
+ ) {
+ // If this window isn't part of a tab group we assume its a window
+ // close for the window and let our timer keep running for the rest.
+ guard let tabGroup = window.tabGroup else {
+ callback(.window)
+ return
+ }
+
+ // Forward to the proper coordinator
+ if let firstController = tabGroup.windows.first?.windowController as? Controller,
+ firstController.tabGroupCloseCoordinator !== self {
+ let coordinator = firstController.tabGroupCloseCoordinator
+ coordinator.windowShouldClose(window, callback: callback)
+ return
+ }
+
+ // If our tab group is nil then we either are seeing this for the first
+ // time or our weak ref expired and we should fire our callbacks.
+ if self.tabGroup == nil {
+ self.tabGroup = tabGroup
+ debounceTimer?.fire()
+ debounceTimer = nil
+ }
+
+ // No matter what, we cancel our debounce and restart this. This opens
+ // us up to a DoS if close requests are looped but this would only
+ // happen in hostile scenarios that are self-inflicted.
+ debounceTimer?.invalidate()
+ debounceTimer = nil
+
+ // If this tab group doesn't match then I don't really know what to
+ // do. This shouldn't happen. So we just assume it's a tab close
+ // and trigger the rest. No right answer here as far as I know.
+ if self.tabGroup != tabGroup {
+ callback(.tab)
+ trigger(.tab)
+ return
+ }
+
+ // Add the request
+ closeRequests[ObjectIdentifier(window)] = callback
+
+ // If close requests matches all our windows then we are done.
+ if closeRequests.count == tabGroup.windows.count {
+ let allWindows = Set(tabGroup.windows.map { ObjectIdentifier($0) })
+ if Set(closeRequests.keys) == allWindows {
+ trigger(.window)
+ return
+ }
+ }
+
+ // Setup our new timer
+ debounceTimer = Timer.scheduledTimer(
+ withTimeInterval: Duration.milliseconds(100).timeInterval,
+ repeats: false
+ ) { [weak self] _ in
+ self?.trigger(.tab)
+ }
+ }
+
+ /// Triggers all pending close callbacks with the given scope.
+ ///
+ /// This method is called when the coordinator has determined the user's intent
+ /// (either closing a tab or the entire window). It executes all pending callbacks
+ /// and resets the coordinator's state.
+ ///
+ /// - Parameter scope: The determined scope of the close operation.
+ private func trigger(_ scope: CloseScope) {
+ // Reset our state
+ tabGroup = nil
+ debounceTimer?.invalidate()
+ debounceTimer = nil
+
+ // Trigger all of our callbacks
+ closeRequests.forEach { $0.value(scope) }
+ closeRequests = [:]
+ }
+}
diff --git a/pkg/fontconfig/build.zig b/pkg/fontconfig/build.zig
index 77e8df549..9e4173da8 100644
--- a/pkg/fontconfig/build.zig
+++ b/pkg/fontconfig/build.zig
@@ -164,11 +164,23 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu
"-DHAVE_SYS_STATVFS_H",
"-DFC_CACHEDIR=\"/var/cache/fontconfig\"",
- "-DFC_TEMPLATEDIR=\"/usr/share/fontconfig/conf.avail\"",
- "-DFONTCONFIG_PATH=\"/etc/fonts\"",
- "-DCONFIGDIR=\"/usr/local/fontconfig/conf.d\"",
"-DFC_DEFAULT_FONTS=\"/usr/share/fonts/usr/local/share/fonts\"",
});
+
+ if (target.result.os.tag == .freebsd) {
+ try flags.appendSlice(&.{
+ "-DFC_TEMPLATEDIR=\"/usr/local/etc/fonts/conf.avail\"",
+ "-DFONTCONFIG_PATH=\"/usr/local/etc/fonts\"",
+ "-DCONFIGDIR=\"/usr/local/etc/fonts/conf.d\"",
+ });
+ } else {
+ try flags.appendSlice(&.{
+ "-DFC_TEMPLATEDIR=\"/usr/share/fontconfig/conf.avail\"",
+ "-DFONTCONFIG_PATH=\"/etc/fonts\"",
+ "-DCONFIGDIR=\"/usr/local/fontconfig/conf.d\"",
+ });
+ }
+
if (target.result.os.tag == .linux) {
try flags.appendSlice(&.{
"-DHAVE_SYS_STATFS_H",
diff --git a/pkg/macos/animation.zig b/pkg/macos/animation.zig
index 5c3c8fd30..247f97605 100644
--- a/pkg/macos/animation.zig
+++ b/pkg/macos/animation.zig
@@ -2,6 +2,8 @@ pub const c = @import("animation/c.zig").c;
/// https://developer.apple.com/documentation/quartzcore/calayer/contents_gravity_values?language=objc
pub extern "c" const kCAGravityTopLeft: *anyopaque;
+pub extern "c" const kCAGravityBottomLeft: *anyopaque;
+pub extern "c" const kCAGravityCenter: *anyopaque;
test {
@import("std").testing.refAllDecls(@This());
diff --git a/pkg/macos/build.zig b/pkg/macos/build.zig
index df76da9b4..3e0a97d1a 100644
--- a/pkg/macos/build.zig
+++ b/pkg/macos/build.zig
@@ -33,6 +33,7 @@ pub fn build(b: *std.Build) !void {
lib.linkFramework("CoreText");
lib.linkFramework("CoreVideo");
lib.linkFramework("QuartzCore");
+ lib.linkFramework("IOSurface");
if (target.result.os.tag == .macos) {
lib.linkFramework("Carbon");
module.linkFramework("Carbon", .{});
@@ -44,6 +45,7 @@ pub fn build(b: *std.Build) !void {
module.linkFramework("CoreText", .{});
module.linkFramework("CoreVideo", .{});
module.linkFramework("QuartzCore", .{});
+ module.linkFramework("IOSurface", .{});
try apple_sdk.addPaths(b, lib);
}
diff --git a/pkg/macos/dispatch.zig b/pkg/macos/dispatch.zig
index 2bc7e8396..3add9c0e9 100644
--- a/pkg/macos/dispatch.zig
+++ b/pkg/macos/dispatch.zig
@@ -3,6 +3,16 @@ pub const data = @import("dispatch/data.zig");
pub const queue = @import("dispatch/queue.zig");
pub const Data = data.Data;
+pub extern "c" fn dispatch_sync(
+ queue: *anyopaque,
+ block: *anyopaque,
+) void;
+
+pub extern "c" fn dispatch_async(
+ queue: *anyopaque,
+ block: *anyopaque,
+) void;
+
test {
@import("std").testing.refAllDecls(@This());
}
diff --git a/pkg/macos/foundation.zig b/pkg/macos/foundation.zig
index 85562faf0..d4f634091 100644
--- a/pkg/macos/foundation.zig
+++ b/pkg/macos/foundation.zig
@@ -30,6 +30,7 @@ pub const stringGetSurrogatePairForLongCharacter = string.stringGetSurrogatePair
pub const URL = url.URL;
pub const URLPathStyle = url.URLPathStyle;
pub const CFRelease = typepkg.CFRelease;
+pub const CFRetain = typepkg.CFRetain;
test {
@import("std").testing.refAllDecls(@This());
diff --git a/pkg/macos/foundation/type.zig b/pkg/macos/foundation/type.zig
index e3ee150f2..45bd09054 100644
--- a/pkg/macos/foundation/type.zig
+++ b/pkg/macos/foundation/type.zig
@@ -1 +1,2 @@
pub extern "c" fn CFRelease(*anyopaque) void;
+pub extern "c" fn CFRetain(*anyopaque) void;
diff --git a/pkg/macos/iosurface.zig b/pkg/macos/iosurface.zig
new file mode 100644
index 000000000..9d2e750cf
--- /dev/null
+++ b/pkg/macos/iosurface.zig
@@ -0,0 +1,8 @@
+const iosurface = @import("iosurface/iosurface.zig");
+
+pub const c = @import("iosurface/c.zig").c;
+pub const IOSurface = iosurface.IOSurface;
+
+test {
+ @import("std").testing.refAllDecls(@This());
+}
diff --git a/pkg/macos/iosurface/c.zig b/pkg/macos/iosurface/c.zig
new file mode 100644
index 000000000..1a7d1627e
--- /dev/null
+++ b/pkg/macos/iosurface/c.zig
@@ -0,0 +1 @@
+pub const c = @import("../main.zig").c;
diff --git a/pkg/macos/iosurface/iosurface.zig b/pkg/macos/iosurface/iosurface.zig
new file mode 100644
index 000000000..37f8712ba
--- /dev/null
+++ b/pkg/macos/iosurface/iosurface.zig
@@ -0,0 +1,136 @@
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const c = @import("c.zig").c;
+const foundation = @import("../foundation.zig");
+const graphics = @import("../graphics.zig");
+const video = @import("../video.zig");
+
+pub const IOSurface = opaque {
+ pub const Error = error{
+ InvalidOperation,
+ };
+
+ pub const Properties = struct {
+ width: c_int,
+ height: c_int,
+ pixel_format: video.PixelFormat,
+ bytes_per_element: c_int,
+ colorspace: ?*graphics.ColorSpace,
+ };
+
+ pub fn init(properties: Properties) Allocator.Error!*IOSurface {
+ var w = try foundation.Number.create(.int, &properties.width);
+ defer w.release();
+ var h = try foundation.Number.create(.int, &properties.height);
+ defer h.release();
+ var pf = try foundation.Number.create(.int, &@as(c_int, @intFromEnum(properties.pixel_format)));
+ defer pf.release();
+ var bpe = try foundation.Number.create(.int, &properties.bytes_per_element);
+ defer bpe.release();
+
+ var properties_dict = try foundation.Dictionary.create(
+ &[_]?*const anyopaque{
+ c.kIOSurfaceWidth,
+ c.kIOSurfaceHeight,
+ c.kIOSurfacePixelFormat,
+ c.kIOSurfaceBytesPerElement,
+ },
+ &[_]?*const anyopaque{ w, h, pf, bpe },
+ );
+ defer properties_dict.release();
+
+ var surface = @as(?*IOSurface, @ptrFromInt(@intFromPtr(
+ c.IOSurfaceCreate(@ptrCast(properties_dict)),
+ ))) orelse return error.OutOfMemory;
+
+ if (properties.colorspace) |space| {
+ surface.setColorSpace(space);
+ }
+
+ return surface;
+ }
+
+ pub fn deinit(self: *IOSurface) void {
+ // We mark it purgeable so that it is immediately unloaded, so that we
+ // don't have to wait for CoreFoundation garbage collection to trigger.
+ _ = c.IOSurfaceSetPurgeable(
+ @ptrCast(self),
+ c.kIOSurfacePurgeableEmpty,
+ null,
+ );
+ foundation.CFRelease(self);
+ }
+
+ pub fn retain(self: *IOSurface) void {
+ foundation.CFRetain(self);
+ }
+
+ pub fn release(self: *IOSurface) void {
+ foundation.CFRelease(self);
+ }
+
+ pub fn setColorSpace(self: *IOSurface, colorspace: *graphics.ColorSpace) void {
+ const serialized_colorspace = graphics.c.CGColorSpaceCopyPropertyList(
+ @ptrCast(colorspace),
+ ).?;
+ defer foundation.CFRelease(@constCast(serialized_colorspace));
+
+ c.IOSurfaceSetValue(
+ @ptrCast(self),
+ c.kIOSurfaceColorSpace,
+ @ptrCast(serialized_colorspace),
+ );
+ }
+
+ pub inline fn lock(self: *IOSurface) void {
+ c.IOSurfaceLock(
+ @ptrCast(self),
+ 0,
+ null,
+ );
+ }
+ pub inline fn unlock(self: *IOSurface) void {
+ c.IOSurfaceUnlock(
+ @ptrCast(self),
+ 0,
+ null,
+ );
+ }
+
+ pub inline fn getAllocSize(self: *IOSurface) usize {
+ return c.IOSurfaceGetAllocSize(@ptrCast(self));
+ }
+
+ pub inline fn getWidth(self: *IOSurface) usize {
+ return c.IOSurfaceGetWidth(@ptrCast(self));
+ }
+
+ pub inline fn getHeight(self: *IOSurface) usize {
+ return c.IOSurfaceGetHeight(@ptrCast(self));
+ }
+
+ pub inline fn getBytesPerElement(self: *IOSurface) usize {
+ return c.IOSurfaceGetBytesPerElement(@ptrCast(self));
+ }
+
+ pub inline fn getBytesPerRow(self: *IOSurface) usize {
+ return c.IOSurfaceGetBytesPerRow(@ptrCast(self));
+ }
+
+ pub inline fn getBaseAddress(self: *IOSurface) ?[*]u8 {
+ return @ptrCast(c.IOSurfaceGetBaseAddress(@ptrCast(self)));
+ }
+
+ pub inline fn getElementWidth(self: *IOSurface) usize {
+ return c.IOSurfaceGetElementWidth(@ptrCast(self));
+ }
+
+ pub inline fn getElementHeight(self: *IOSurface) usize {
+ return c.IOSurfaceGetElementHeight(@ptrCast(self));
+ }
+
+ pub inline fn getPixelFormat(self: *IOSurface) video.PixelFormat {
+ return @enumFromInt(c.IOSurfaceGetPixelFormat(@ptrCast(self)));
+ }
+};
diff --git a/pkg/macos/main.zig b/pkg/macos/main.zig
index d094b987e..42253ba48 100644
--- a/pkg/macos/main.zig
+++ b/pkg/macos/main.zig
@@ -8,6 +8,7 @@ pub const graphics = @import("graphics.zig");
pub const os = @import("os.zig");
pub const text = @import("text.zig");
pub const video = @import("video.zig");
+pub const iosurface = @import("iosurface.zig");
// All of our C imports consolidated into one place. We used to
// import them one by one in each package but Zig 0.14 has some
@@ -17,7 +18,9 @@ pub const c = @cImport({
@cInclude("CoreGraphics/CoreGraphics.h");
@cInclude("CoreText/CoreText.h");
@cInclude("CoreVideo/CoreVideo.h");
+ @cInclude("CoreVideo/CVPixelBuffer.h");
@cInclude("QuartzCore/CALayer.h");
+ @cInclude("IOSurface/IOSurfaceRef.h");
@cInclude("dispatch/dispatch.h");
@cInclude("os/log.h");
diff --git a/pkg/macos/video.zig b/pkg/macos/video.zig
index 0f5cbc4d6..d0b1125ab 100644
--- a/pkg/macos/video.zig
+++ b/pkg/macos/video.zig
@@ -1,7 +1,9 @@
const display_link = @import("video/display_link.zig");
+const pixel_format = @import("video/pixel_format.zig");
pub const c = @import("video/c.zig").c;
pub const DisplayLink = display_link.DisplayLink;
+pub const PixelFormat = pixel_format.PixelFormat;
test {
@import("std").testing.refAllDecls(@This());
diff --git a/pkg/macos/video/pixel_format.zig b/pkg/macos/video/pixel_format.zig
new file mode 100644
index 000000000..78091daa3
--- /dev/null
+++ b/pkg/macos/video/pixel_format.zig
@@ -0,0 +1,171 @@
+const c = @import("c.zig").c;
+
+pub const PixelFormat = enum(c_int) {
+ /// 1 bit indexed
+ @"1Monochrome" = c.kCVPixelFormatType_1Monochrome,
+ /// 2 bit indexed
+ @"2Indexed" = c.kCVPixelFormatType_2Indexed,
+ /// 4 bit indexed
+ @"4Indexed" = c.kCVPixelFormatType_4Indexed,
+ /// 8 bit indexed
+ @"8Indexed" = c.kCVPixelFormatType_8Indexed,
+ /// 1 bit indexed gray, white is zero
+ @"1IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_1IndexedGray_WhiteIsZero,
+ /// 2 bit indexed gray, white is zero
+ @"2IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_2IndexedGray_WhiteIsZero,
+ /// 4 bit indexed gray, white is zero
+ @"4IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_4IndexedGray_WhiteIsZero,
+ /// 8 bit indexed gray, white is zero
+ @"8IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_8IndexedGray_WhiteIsZero,
+ /// 16 bit BE RGB 555
+ @"16BE555" = c.kCVPixelFormatType_16BE555,
+ /// 16 bit LE RGB 555
+ @"16LE555" = c.kCVPixelFormatType_16LE555,
+ /// 16 bit LE RGB 5551
+ @"16LE5551" = c.kCVPixelFormatType_16LE5551,
+ /// 16 bit BE RGB 565
+ @"16BE565" = c.kCVPixelFormatType_16BE565,
+ /// 16 bit LE RGB 565
+ @"16LE565" = c.kCVPixelFormatType_16LE565,
+ /// 24 bit RGB
+ @"24RGB" = c.kCVPixelFormatType_24RGB,
+ /// 24 bit BGR
+ @"24BGR" = c.kCVPixelFormatType_24BGR,
+ /// 32 bit ARGB
+ @"32ARGB" = c.kCVPixelFormatType_32ARGB,
+ /// 32 bit BGRA
+ @"32BGRA" = c.kCVPixelFormatType_32BGRA,
+ /// 32 bit ABGR
+ @"32ABGR" = c.kCVPixelFormatType_32ABGR,
+ /// 32 bit RGBA
+ @"32RGBA" = c.kCVPixelFormatType_32RGBA,
+ /// 64 bit ARGB, 16-bit big-endian samples
+ @"64ARGB" = c.kCVPixelFormatType_64ARGB,
+ /// 64 bit RGBA, 16-bit little-endian full-range (0-65535) samples
+ @"64RGBALE" = c.kCVPixelFormatType_64RGBALE,
+ /// 48 bit RGB, 16-bit big-endian samples
+ @"48RGB" = c.kCVPixelFormatType_48RGB,
+ /// 32 bit AlphaGray, 16-bit big-endian samples, black is zero
+ @"32AlphaGray" = c.kCVPixelFormatType_32AlphaGray,
+ /// 16 bit Grayscale, 16-bit big-endian samples, black is zero
+ @"16Gray" = c.kCVPixelFormatType_16Gray,
+ /// 30 bit RGB, 10-bit big-endian samples, 2 unused padding bits (at least significant end).
+ @"30RGB" = c.kCVPixelFormatType_30RGB,
+ /// 30 bit RGB, 10-bit big-endian samples, 2 unused padding bits (at most significant end), video-range (64-940).
+ @"30RGB_r210" = c.kCVPixelFormatType_30RGB_r210,
+ /// Component Y'CbCr 8-bit 4:2:2, ordered Cb Y'0 Cr Y'1
+ @"422YpCbCr8" = c.kCVPixelFormatType_422YpCbCr8,
+ /// Component Y'CbCrA 8-bit 4:4:4:4, ordered Cb Y' Cr A
+ @"4444YpCbCrA8" = c.kCVPixelFormatType_4444YpCbCrA8,
+ /// Component Y'CbCrA 8-bit 4:4:4:4, rendering format. full range alpha, zero biased YUV, ordered A Y' Cb Cr
+ @"4444YpCbCrA8R" = c.kCVPixelFormatType_4444YpCbCrA8R,
+ /// Component Y'CbCrA 8-bit 4:4:4:4, ordered A Y' Cb Cr, full range alpha, video range Y'CbCr.
+ @"4444AYpCbCr8" = c.kCVPixelFormatType_4444AYpCbCr8,
+ /// Component Y'CbCrA 16-bit 4:4:4:4, ordered A Y' Cb Cr, full range alpha, video range Y'CbCr, 16-bit little-endian samples.
+ @"4444AYpCbCr16" = c.kCVPixelFormatType_4444AYpCbCr16,
+ /// Component AY'CbCr single precision floating-point 4:4:4:4
+ @"4444AYpCbCrFloat" = c.kCVPixelFormatType_4444AYpCbCrFloat,
+ /// Component Y'CbCr 8-bit 4:4:4, ordered Cr Y' Cb, video range Y'CbCr
+ @"444YpCbCr8" = c.kCVPixelFormatType_444YpCbCr8,
+ /// Component Y'CbCr 10,12,14,16-bit 4:2:2
+ @"422YpCbCr16" = c.kCVPixelFormatType_422YpCbCr16,
+ /// Component Y'CbCr 10-bit 4:2:2
+ @"422YpCbCr10" = c.kCVPixelFormatType_422YpCbCr10,
+ /// Component Y'CbCr 10-bit 4:4:4
+ @"444YpCbCr10" = c.kCVPixelFormatType_444YpCbCr10,
+ /// Planar Component Y'CbCr 8-bit 4:2:0. baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrPlanar struct
+ @"420YpCbCr8Planar" = c.kCVPixelFormatType_420YpCbCr8Planar,
+ /// Planar Component Y'CbCr 8-bit 4:2:0, full range. baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrPlanar struct
+ @"420YpCbCr8PlanarFullRange" = c.kCVPixelFormatType_420YpCbCr8PlanarFullRange,
+ /// First plane: Video-range Component Y'CbCr 8-bit 4:2:2, ordered Cb Y'0 Cr Y'1; second plane: alpha 8-bit 0-255
+ @"422YpCbCr_4A_8BiPlanar" = c.kCVPixelFormatType_422YpCbCr_4A_8BiPlanar,
+ /// Bi-Planar Component Y'CbCr 8-bit 4:2:0, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
+ @"420YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
+ /// Bi-Planar Component Y'CbCr 8-bit 4:2:0, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
+ @"420YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
+ /// Bi-Planar Component Y'CbCr 8-bit 4:2:2, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
+ @"422YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange,
+ /// Bi-Planar Component Y'CbCr 8-bit 4:2:2, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
+ @"422YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_422YpCbCr8BiPlanarFullRange,
+ /// Bi-Planar Component Y'CbCr 8-bit 4:4:4, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
+ @"444YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange,
+ /// Bi-Planar Component Y'CbCr 8-bit 4:4:4, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
+ @"444YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_444YpCbCr8BiPlanarFullRange,
+ /// Component Y'CbCr 8-bit 4:2:2, ordered Y'0 Cb Y'1 Cr
+ @"422YpCbCr8_yuvs" = c.kCVPixelFormatType_422YpCbCr8_yuvs,
+ /// Component Y'CbCr 8-bit 4:2:2, full range, ordered Y'0 Cb Y'1 Cr
+ @"422YpCbCr8FullRange" = c.kCVPixelFormatType_422YpCbCr8FullRange,
+ /// 8 bit one component, black is zero
+ OneComponent8 = c.kCVPixelFormatType_OneComponent8,
+ /// 8 bit two component, black is zero
+ TwoComponent8 = c.kCVPixelFormatType_TwoComponent8,
+ /// little-endian RGB101010, 2 MSB are ignored, wide-gamut (384-895)
+ @"30RGBLEPackedWideGamut" = c.kCVPixelFormatType_30RGBLEPackedWideGamut,
+ /// little-endian ARGB2101010 full-range ARGB
+ ARGB2101010LEPacked = c.kCVPixelFormatType_ARGB2101010LEPacked,
+ /// little-endian ARGB10101010, each 10 bits in the MSBs of 16bits, wide-gamut (384-895, including alpha)
+ @"40ARGBLEWideGamut" = c.kCVPixelFormatType_40ARGBLEWideGamut,
+ /// little-endian ARGB10101010, each 10 bits in the MSBs of 16bits, wide-gamut (384-895, including alpha). Alpha premultiplied
+ @"40ARGBLEWideGamutPremultiplied" = c.kCVPixelFormatType_40ARGBLEWideGamutPremultiplied,
+ /// 10 bit little-endian one component, stored as 10 MSBs of 16 bits, black is zero
+ OneComponent10 = c.kCVPixelFormatType_OneComponent10,
+ /// 12 bit little-endian one component, stored as 12 MSBs of 16 bits, black is zero
+ OneComponent12 = c.kCVPixelFormatType_OneComponent12,
+ /// 16 bit little-endian one component, black is zero
+ OneComponent16 = c.kCVPixelFormatType_OneComponent16,
+ /// 16 bit little-endian two component, black is zero
+ TwoComponent16 = c.kCVPixelFormatType_TwoComponent16,
+ /// 16 bit one component IEEE half-precision float, 16-bit little-endian samples
+ OneComponent16Half = c.kCVPixelFormatType_OneComponent16Half,
+ /// 32 bit one component IEEE float, 32-bit little-endian samples
+ OneComponent32Float = c.kCVPixelFormatType_OneComponent32Float,
+ /// 16 bit two component IEEE half-precision float, 16-bit little-endian samples
+ TwoComponent16Half = c.kCVPixelFormatType_TwoComponent16Half,
+ /// 32 bit two component IEEE float, 32-bit little-endian samples
+ TwoComponent32Float = c.kCVPixelFormatType_TwoComponent32Float,
+ /// 64 bit RGBA IEEE half-precision float, 16-bit little-endian samples
+ @"64RGBAHalf" = c.kCVPixelFormatType_64RGBAHalf,
+ /// 128 bit RGBA IEEE float, 32-bit little-endian samples
+ @"128RGBAFloat" = c.kCVPixelFormatType_128RGBAFloat,
+ /// Bayer 14-bit Little-Endian, packed in 16-bits, ordered G R G R... alternating with B G B G...
+ @"14Bayer_GRBG" = c.kCVPixelFormatType_14Bayer_GRBG,
+ /// Bayer 14-bit Little-Endian, packed in 16-bits, ordered R G R G... alternating with G B G B...
+ @"14Bayer_RGGB" = c.kCVPixelFormatType_14Bayer_RGGB,
+ /// Bayer 14-bit Little-Endian, packed in 16-bits, ordered B G B G... alternating with G R G R...
+ @"14Bayer_BGGR" = c.kCVPixelFormatType_14Bayer_BGGR,
+ /// Bayer 14-bit Little-Endian, packed in 16-bits, ordered G B G B... alternating with R G R G...
+ @"14Bayer_GBRG" = c.kCVPixelFormatType_14Bayer_GBRG,
+ /// IEEE754-2008 binary16 (half float), describing the normalized shift when comparing two images. Units are 1/meters: ( pixelShift / (pixelFocalLength * baselineInMeters) )
+ DisparityFloat16 = c.kCVPixelFormatType_DisparityFloat16,
+ /// IEEE754-2008 binary32 float, describing the normalized shift when comparing two images. Units are 1/meters: ( pixelShift / (pixelFocalLength * baselineInMeters) )
+ DisparityFloat32 = c.kCVPixelFormatType_DisparityFloat32,
+ /// IEEE754-2008 binary16 (half float), describing the depth (distance to an object) in meters
+ DepthFloat16 = c.kCVPixelFormatType_DepthFloat16,
+ /// IEEE754-2008 binary32 float, describing the depth (distance to an object) in meters
+ DepthFloat32 = c.kCVPixelFormatType_DepthFloat32,
+ /// 2 plane YCbCr10 4:2:0, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960])
+ @"420YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange,
+ /// 2 plane YCbCr10 4:2:2, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960])
+ @"422YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange,
+ /// 2 plane YCbCr10 4:4:4, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960])
+ @"444YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange,
+ /// 2 plane YCbCr10 4:2:0, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023)
+ @"420YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_420YpCbCr10BiPlanarFullRange,
+ /// 2 plane YCbCr10 4:2:2, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023)
+ @"422YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_422YpCbCr10BiPlanarFullRange,
+ /// 2 plane YCbCr10 4:4:4, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023)
+ @"444YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_444YpCbCr10BiPlanarFullRange,
+ /// first and second planes as per 420YpCbCr8BiPlanarVideoRange (420v), alpha 8 bits in third plane full-range. No CVPlanarPixelBufferInfo struct.
+ @"420YpCbCr8VideoRange_8A_TriPlanar" = c.kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar,
+ /// Single plane Bayer 16-bit little-endian sensor element ("sensel".*) samples from full-size decoding of ProRes RAW images; Bayer pattern (sensel ordering) and other raw conversion information is described via buffer attachments
+ @"16VersatileBayer" = c.kCVPixelFormatType_16VersatileBayer,
+ /// Single plane 64-bit RGBA (16-bit little-endian samples) from downscaled decoding of ProRes RAW images; components--which may not be co-sited with one another--are sensel values and require raw conversion, information for which is described via buffer attachments
+ @"64RGBA_DownscaledProResRAW" = c.kCVPixelFormatType_64RGBA_DownscaledProResRAW,
+ /// 2 plane YCbCr16 4:2:2, video-range (luma=[4096,60160] chroma=[4096,61440])
+ @"422YpCbCr16BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr16BiPlanarVideoRange,
+ /// 2 plane YCbCr16 4:4:4, video-range (luma=[4096,60160] chroma=[4096,61440])
+ @"444YpCbCr16BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr16BiPlanarVideoRange,
+ /// 3 plane video-range YCbCr16 4:4:4 with 16-bit full-range alpha (luma=[4096,60160] chroma=[4096,61440] alpha=[0,65535]). No CVPlanarPixelBufferInfo struct.
+ @"444YpCbCr16VideoRange_16A_TriPlanar" = c.kCVPixelFormatType_444YpCbCr16VideoRange_16A_TriPlanar,
+ _,
+};
diff --git a/pkg/opengl/Buffer.zig b/pkg/opengl/Buffer.zig
index 3e55410b7..609342958 100644
--- a/pkg/opengl/Buffer.zig
+++ b/pkg/opengl/Buffer.zig
@@ -51,7 +51,7 @@ pub const Binding = struct {
data: anytype,
usage: Usage,
) !void {
- const info = dataInfo(&data);
+ const info = dataInfo(data);
glad.context.BufferData.?(
@intFromEnum(b.target),
info.size,
@@ -136,10 +136,6 @@ pub const Binding = struct {
};
}
- pub fn enableAttribArray(_: Binding, idx: c.GLuint) !void {
- glad.context.EnableVertexAttribArray.?(idx);
- }
-
/// Shorthand for vertexAttribPointer that is specialized towards the
/// common use case of specifying an array of homogeneous types that
/// don't need normalization. This also enables the attribute at idx.
@@ -230,6 +226,7 @@ pub const Target = enum(c_uint) {
array = c.GL_ARRAY_BUFFER,
element_array = c.GL_ELEMENT_ARRAY_BUFFER,
uniform = c.GL_UNIFORM_BUFFER,
+ storage = c.GL_SHADER_STORAGE_BUFFER,
_,
};
diff --git a/pkg/opengl/Framebuffer.zig b/pkg/opengl/Framebuffer.zig
index c5d659f98..ea1f0d2ba 100644
--- a/pkg/opengl/Framebuffer.zig
+++ b/pkg/opengl/Framebuffer.zig
@@ -5,6 +5,7 @@ const c = @import("c.zig").c;
const errors = @import("errors.zig");
const glad = @import("glad.zig");
const Texture = @import("Texture.zig");
+const Renderbuffer = @import("Renderbuffer.zig");
id: c.GLuint,
@@ -86,6 +87,29 @@ pub const Binding = struct {
try errors.getError();
}
+ pub fn renderbuffer(
+ self: Binding,
+ attachment: Attachment,
+ buffer: Renderbuffer,
+ ) !void {
+ glad.context.FramebufferRenderbuffer.?(
+ @intFromEnum(self.target),
+ @intFromEnum(attachment),
+ c.GL_RENDERBUFFER,
+ buffer.id,
+ );
+ try errors.getError();
+ }
+
+ pub fn drawBuffers(
+ self: Binding,
+ bufs: []Attachment,
+ ) !void {
+ _ = self;
+ glad.context.DrawBuffers.?(@intCast(bufs.len), bufs.ptr);
+ try errors.getError();
+ }
+
pub fn checkStatus(self: Binding) Status {
return @enumFromInt(glad.context.CheckFramebufferStatus.?(@intFromEnum(self.target)));
}
diff --git a/pkg/opengl/Renderbuffer.zig b/pkg/opengl/Renderbuffer.zig
new file mode 100644
index 000000000..ef21287f7
--- /dev/null
+++ b/pkg/opengl/Renderbuffer.zig
@@ -0,0 +1,56 @@
+const Renderbuffer = @This();
+
+const std = @import("std");
+const c = @import("c.zig").c;
+const errors = @import("errors.zig");
+const glad = @import("glad.zig");
+
+const Texture = @import("Texture.zig");
+
+id: c.GLuint,
+
+/// Create a single buffer.
+pub fn create() !Renderbuffer {
+ var rbo: c.GLuint = undefined;
+ glad.context.GenRenderbuffers.?(1, &rbo);
+ return .{ .id = rbo };
+}
+
+pub fn destroy(v: Renderbuffer) void {
+ glad.context.DeleteRenderbuffers.?(1, &v.id);
+}
+
+pub fn bind(v: Renderbuffer) !Binding {
+ // Keep track of the previous binding so we can restore it in unbind.
+ var current: c.GLint = undefined;
+ glad.context.GetIntegerv.?(c.GL_RENDERBUFFER_BINDING, ¤t);
+ glad.context.BindRenderbuffer.?(c.GL_RENDERBUFFER, v.id);
+ return .{ .previous = @intCast(current) };
+}
+
+pub const Binding = struct {
+ previous: c.GLuint,
+
+ pub fn unbind(self: Binding) void {
+ glad.context.BindRenderbuffer.?(
+ c.GL_RENDERBUFFER,
+ self.previous,
+ );
+ }
+
+ pub fn storage(
+ self: Binding,
+ format: Texture.InternalFormat,
+ width: c.GLsizei,
+ height: c.GLsizei,
+ ) !void {
+ _ = self;
+ glad.context.RenderbufferStorage.?(
+ c.GL_RENDERBUFFER,
+ @intCast(@intFromEnum(format)),
+ width,
+ height,
+ );
+ try errors.getError();
+ }
+};
diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig
index fa5cf770b..4a1d61433 100644
--- a/pkg/opengl/Texture.zig
+++ b/pkg/opengl/Texture.zig
@@ -7,15 +7,16 @@ const glad = @import("glad.zig");
id: c.GLuint,
-pub fn active(target: c.GLenum) !void {
- glad.context.ActiveTexture.?(target);
+pub fn active(index: c_uint) errors.Error!void {
+ glad.context.ActiveTexture.?(index + c.GL_TEXTURE0);
try errors.getError();
}
/// Create a single texture.
-pub fn create() !Texture {
+pub fn create() errors.Error!Texture {
var id: c.GLuint = undefined;
glad.context.GenTextures.?(1, &id);
+ try errors.getError();
return .{ .id = id };
}
@@ -30,7 +31,7 @@ pub fn destroy(v: Texture) void {
glad.context.DeleteTextures.?(1, &v.id);
}
-/// Enun for possible texture binding targets.
+/// Enum for possible texture binding targets.
pub const Target = enum(c_uint) {
@"1D" = c.GL_TEXTURE_1D,
@"2D" = c.GL_TEXTURE_2D,
@@ -67,11 +68,11 @@ pub const Parameter = enum(c_uint) {
/// Internal format enum for texture images.
pub const InternalFormat = enum(c_int) {
red = c.GL_RED,
- rgb = c.GL_RGB,
- rgba = c.GL_RGBA,
+ rgb = c.GL_RGB8,
+ rgba = c.GL_RGBA8,
- srgb = c.GL_SRGB,
- srgba = c.GL_SRGB_ALPHA,
+ srgb = c.GL_SRGB8,
+ srgba = c.GL_SRGB8_ALPHA8,
// There are so many more that I haven't filled in.
_,
@@ -107,7 +108,7 @@ pub const Binding = struct {
glad.context.GenerateMipmap.?(@intFromEnum(b.target));
}
- pub fn parameter(b: Binding, name: Parameter, value: anytype) !void {
+ pub fn parameter(b: Binding, name: Parameter, value: anytype) errors.Error!void {
switch (@TypeOf(value)) {
c.GLint => glad.context.TexParameteri.?(
@intFromEnum(b.target),
@@ -116,6 +117,7 @@ pub const Binding = struct {
),
else => unreachable,
}
+ try errors.getError();
}
pub fn image2D(
@@ -128,7 +130,7 @@ pub const Binding = struct {
format: Format,
typ: DataType,
data: ?*const anyopaque,
- ) !void {
+ ) errors.Error!void {
glad.context.TexImage2D.?(
@intFromEnum(b.target),
level,
@@ -140,6 +142,7 @@ pub const Binding = struct {
@intFromEnum(typ),
data,
);
+ try errors.getError();
}
pub fn subImage2D(
@@ -152,7 +155,7 @@ pub const Binding = struct {
format: Format,
typ: DataType,
data: ?*const anyopaque,
- ) !void {
+ ) errors.Error!void {
glad.context.TexSubImage2D.?(
@intFromEnum(b.target),
level,
@@ -164,6 +167,7 @@ pub const Binding = struct {
@intFromEnum(typ),
data,
);
+ try errors.getError();
}
pub fn copySubImage2D(
@@ -175,7 +179,17 @@ pub const Binding = struct {
y: c.GLint,
width: c.GLsizei,
height: c.GLsizei,
- ) !void {
- glad.context.CopyTexSubImage2D.?(@intFromEnum(b.target), level, xoffset, yoffset, x, y, width, height);
+ ) errors.Error!void {
+ glad.context.CopyTexSubImage2D.?(
+ @intFromEnum(b.target),
+ level,
+ xoffset,
+ yoffset,
+ x,
+ y,
+ width,
+ height,
+ );
+ try errors.getError();
}
};
diff --git a/pkg/opengl/VertexArray.zig b/pkg/opengl/VertexArray.zig
index 4a6a37576..44bf31621 100644
--- a/pkg/opengl/VertexArray.zig
+++ b/pkg/opengl/VertexArray.zig
@@ -29,4 +29,88 @@ pub const Binding = struct {
_ = self;
glad.context.BindVertexArray.?(0);
}
+
+ pub fn enableAttribArray(_: Binding, idx: c.GLuint) !void {
+ glad.context.EnableVertexAttribArray.?(idx);
+ try errors.getError();
+ }
+
+ pub fn bindingDivisor(_: Binding, idx: c.GLuint, divisor: c.GLuint) !void {
+ glad.context.VertexBindingDivisor.?(idx, divisor);
+ try errors.getError();
+ }
+
+ pub fn attributeBinding(
+ _: Binding,
+ attrib_idx: c.GLuint,
+ binding_idx: c.GLuint,
+ ) !void {
+ glad.context.VertexAttribBinding.?(attrib_idx, binding_idx);
+ try errors.getError();
+ }
+
+ pub fn attributeFormat(
+ _: Binding,
+ idx: c.GLuint,
+ size: c.GLint,
+ typ: c.GLenum,
+ normalized: bool,
+ offset: c.GLuint,
+ ) !void {
+ glad.context.VertexAttribFormat.?(
+ idx,
+ size,
+ typ,
+ @intCast(@intFromBool(normalized)),
+ offset,
+ );
+ try errors.getError();
+ }
+
+ pub fn attributeIFormat(
+ _: Binding,
+ idx: c.GLuint,
+ size: c.GLint,
+ typ: c.GLenum,
+ offset: c.GLuint,
+ ) !void {
+ glad.context.VertexAttribIFormat.?(
+ idx,
+ size,
+ typ,
+ offset,
+ );
+ try errors.getError();
+ }
+
+ pub fn attributeLFormat(
+ _: Binding,
+ idx: c.GLuint,
+ size: c.GLint,
+ offset: c.GLuint,
+ ) !void {
+ glad.context.VertexAttribLFormat.?(
+ idx,
+ size,
+ c.GL_DOUBLE,
+ offset,
+ );
+ try errors.getError();
+ }
+
+ pub fn bindVertexBuffer(
+ _: Binding,
+ idx: c.GLuint,
+ buffer: c.GLuint,
+ offset: c.GLintptr,
+ stride: c.GLsizei,
+ ) !void {
+ glad.context.BindVertexBuffer.?(
+ idx,
+ buffer,
+ offset,
+ stride,
+ );
+ try errors.getError();
+ }
};
diff --git a/pkg/opengl/draw.zig b/pkg/opengl/draw.zig
index 866511c32..50110f605 100644
--- a/pkg/opengl/draw.zig
+++ b/pkg/opengl/draw.zig
@@ -1,6 +1,7 @@
const c = @import("c.zig").c;
const errors = @import("errors.zig");
const glad = @import("glad.zig");
+const Primitive = @import("primitives.zig").Primitive;
pub fn clearColor(r: f32, g: f32, b: f32, a: f32) void {
glad.context.ClearColor.?(r, g, b, a);
@@ -15,6 +16,21 @@ pub fn drawArrays(mode: c.GLenum, first: c.GLint, count: c.GLsizei) !void {
try errors.getError();
}
+pub fn drawArraysInstanced(
+ mode: Primitive,
+ first: c.GLint,
+ count: c.GLsizei,
+ primcount: c.GLsizei,
+) !void {
+ glad.context.DrawArraysInstanced.?(
+ @intCast(@intFromEnum(mode)),
+ first,
+ count,
+ primcount,
+ );
+ try errors.getError();
+}
+
pub fn drawElements(mode: c.GLenum, count: c.GLsizei, typ: c.GLenum, offset: usize) !void {
const offsetPtr = if (offset == 0) null else @as(*const anyopaque, @ptrFromInt(offset));
glad.context.DrawElements.?(mode, count, typ, offsetPtr);
@@ -25,9 +41,15 @@ pub fn drawElementsInstanced(
mode: c.GLenum,
count: c.GLsizei,
typ: c.GLenum,
- primcount: usize,
+ primcount: c.GLsizei,
) !void {
- glad.context.DrawElementsInstanced.?(mode, count, typ, null, @intCast(primcount));
+ glad.context.DrawElementsInstanced.?(
+ mode,
+ count,
+ typ,
+ null,
+ primcount,
+ );
try errors.getError();
}
@@ -36,6 +58,11 @@ pub fn enable(cap: c.GLenum) !void {
try errors.getError();
}
+pub fn disable(cap: c.GLenum) !void {
+ glad.context.Disable.?(cap);
+ try errors.getError();
+}
+
pub fn frontFace(mode: c.GLenum) !void {
glad.context.FrontFace.?(mode);
try errors.getError();
@@ -57,3 +84,11 @@ pub fn pixelStore(mode: c.GLenum, value: anytype) !void {
}
try errors.getError();
}
+
+pub fn finish() void {
+ glad.context.Finish.?();
+}
+
+pub fn flush() void {
+ glad.context.Flush.?();
+}
diff --git a/pkg/opengl/main.zig b/pkg/opengl/main.zig
index 19cd750d0..7165ad3ab 100644
--- a/pkg/opengl/main.zig
+++ b/pkg/opengl/main.zig
@@ -16,20 +16,29 @@ pub const glad = @import("glad.zig");
pub const ext = @import("extensions.zig");
pub const Buffer = @import("Buffer.zig");
pub const Framebuffer = @import("Framebuffer.zig");
+pub const Renderbuffer = @import("Renderbuffer.zig");
pub const Program = @import("Program.zig");
pub const Shader = @import("Shader.zig");
pub const Texture = @import("Texture.zig");
pub const VertexArray = @import("VertexArray.zig");
+pub const errors = @import("errors.zig");
+
+pub const Primitive = @import("primitives.zig").Primitive;
+
const draw = @import("draw.zig");
pub const blendFunc = draw.blendFunc;
pub const clear = draw.clear;
pub const clearColor = draw.clearColor;
pub const drawArrays = draw.drawArrays;
+pub const drawArraysInstanced = draw.drawArraysInstanced;
pub const drawElements = draw.drawElements;
pub const drawElementsInstanced = draw.drawElementsInstanced;
pub const enable = draw.enable;
+pub const disable = draw.disable;
pub const frontFace = draw.frontFace;
pub const pixelStore = draw.pixelStore;
pub const viewport = draw.viewport;
+pub const flush = draw.flush;
+pub const finish = draw.finish;
diff --git a/pkg/opengl/primitives.zig b/pkg/opengl/primitives.zig
new file mode 100644
index 000000000..e12f51a66
--- /dev/null
+++ b/pkg/opengl/primitives.zig
@@ -0,0 +1,18 @@
+pub const c = @import("c.zig").c;
+
+pub const Primitive = enum(c_int) {
+ point = c.GL_POINTS,
+ line = c.GL_LINES,
+ line_strip = c.GL_LINE_STRIP,
+ triangle = c.GL_TRIANGLES,
+ triangle_strip = c.GL_TRIANGLE_STRIP,
+
+ // Commented out primitive types are excluded for parity with Metal.
+ //
+ // line_loop = c.GL_LINE_LOOP,
+ // line_adjacency = c.GL_LINES_ADJACENCY,
+ // line_strip_adjacency = c.GL_LINE_STRIP_ADJACENCY,
+ // triangle_fan = c.GL_TRIANGLE_FAN,
+ // triangle_adjacency = c.GL_TRIANGLES_ADJACENCY,
+ // triangle_strip_adjacency = c.GL_TRIANGLE_STRIP_ADJACENCY,
+};
diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po
index d2ba0e693..ba13f4460 100644
--- a/po/pt_BR.UTF-8.po
+++ b/po/pt_BR.UTF-8.po
@@ -9,8 +9,8 @@ msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-04-22 08:57-0700\n"
-"PO-Revision-Date: 2025-03-28 11:04-0300\n"
-"Last-Translator: Gustavo Peres \n"
+"PO-Revision-Date: 2025-06-20 10:19-0300\n"
+"Last-Translator: Mário Victor Ribeiro Silva \n"
"Language-Team: Brazilian Portuguese \n"
"Language: pt_BR\n"
@@ -217,7 +217,7 @@ msgstr "Visualizar abas abertas"
#: src/apprt/gtk/Window.zig:249
msgid "New Split"
-msgstr ""
+msgstr "Nova divisão"
#: src/apprt/gtk/Window.zig:312
msgid ""
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index b57411a6c..d7fc63712 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -72,8 +72,6 @@ parts:
build-packages:
- libgtk-4-dev
- libadwaita-1-dev
- # TODO: Add when the Snap is updated to Ubuntu 24.10+
- # - gtk4-layer-shell
- libxml2-utils
- git
- patchelf
@@ -82,7 +80,10 @@ parts:
craftctl set version=$(cat VERSION)
$CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast -Dcpu=baseline
cp -rp zig-out/* $CRAFT_PART_INSTALL/
- sed -i 's|Icon=com.mitchellh.ghostty|Icon=/snap/ghostty/current/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png|g' $CRAFT_PART_INSTALL/share/applications/com.mitchellh.ghostty.desktop
+ # Install libgtk4-layer-shell.so
+ mkdir -p $CRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR
+ cp .zig-cache/*/*/libgtk4-layer-shell.so $CRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/
+ sed -i 's|Icon=com.mitchellh.ghostty|Icon=${SNAP}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png|g' $CRAFT_PART_INSTALL/share/applications/com.mitchellh.ghostty.desktop
libs:
plugin: nil
diff --git a/src/Command.zig b/src/Command.zig
index 281dcce40..7ed026efe 100644
--- a/src/Command.zig
+++ b/src/Command.zig
@@ -323,7 +323,7 @@ fn setupFd(src: File.Handle, target: i32) !void {
}
}
},
- .ios, .macos => {
+ .freebsd, .ios, .macos => {
// Mac doesn't support dup3 so we use dup2. We purposely clear
// CLO_ON_EXEC for this fd.
const flags = try posix.fcntl(src, posix.F.GETFD, 0);
diff --git a/src/Surface.zig b/src/Surface.zig
index 41d40125a..a25b200f7 100644
--- a/src/Surface.zig
+++ b/src/Surface.zig
@@ -468,6 +468,7 @@ pub fn init(
.size = size,
.surface_mailbox = .{ .surface = self, .app = app_mailbox },
.rt_surface = rt_surface,
+ .thread = &self.renderer_thread,
});
errdefer renderer_impl.deinit();
@@ -726,7 +727,9 @@ pub fn close(self: *Surface) void {
/// is in the middle of animation (such as a resize, etc.) or when
/// the render timer is managed manually by the apprt.
pub fn draw(self: *Surface) !void {
- try self.renderer_thread.draw_now.notify();
+ // Renderers are required to support `drawFrame` being called from
+ // the main thread, so that they can update contents during resize.
+ try self.renderer.drawFrame(true);
}
/// Activate the inspector. This will begin collecting inspection data.
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
index a61c75e96..77c22c7f5 100644
--- a/src/apprt/embedded.zig
+++ b/src/apprt/embedded.zig
@@ -376,6 +376,14 @@ pub const PlatformTag = enum(c_int) {
ios = 2,
};
+pub const EnvVar = extern struct {
+ /// The name of the environment variable.
+ key: [*:0]const u8,
+
+ /// The value of the environment variable.
+ value: [*:0]const u8,
+};
+
pub const Surface = struct {
app: *App,
platform: Platform,
@@ -407,7 +415,7 @@ pub const Surface = struct {
font_size: f32 = 0,
/// The working directory to load into.
- working_directory: [*:0]const u8 = "",
+ working_directory: ?[*:0]const u8 = null,
/// The command to run in the new surface. If this is set then
/// the "wait-after-command" option is also automatically set to true,
@@ -417,7 +425,14 @@ pub const Surface = struct {
/// despite Ghostty allowing directly executed commands via config.
/// This is a legacy thing and we should probably change it in the
/// future once we have a concrete use case.
- command: [*:0]const u8 = "",
+ command: ?[*:0]const u8 = null,
+
+ /// Extra environment variables to set for the surface.
+ env_vars: ?[*]EnvVar = null,
+ env_var_count: usize = 0,
+
+ /// Input to send to the command after it is started.
+ initial_input: ?[*:0]const u8 = null,
};
pub fn init(self: *Surface, app: *App, opts: Options) !void {
@@ -443,41 +458,72 @@ pub const Surface = struct {
defer config.deinit();
// If we have a working directory from the options then we set it.
- const wd = std.mem.sliceTo(opts.working_directory, 0);
- if (wd.len > 0) wd: {
- var dir = std.fs.openDirAbsolute(wd, .{}) catch |err| {
- log.warn(
- "error opening requested working directory dir={s} err={}",
- .{ wd, err },
- );
- break :wd;
- };
- defer dir.close();
+ if (opts.working_directory) |c_wd| {
+ const wd = std.mem.sliceTo(c_wd, 0);
+ if (wd.len > 0) wd: {
+ var dir = std.fs.openDirAbsolute(wd, .{}) catch |err| {
+ log.warn(
+ "error opening requested working directory dir={s} err={}",
+ .{ wd, err },
+ );
+ break :wd;
+ };
+ defer dir.close();
- const stat = dir.stat() catch |err| {
- log.warn(
- "failed to stat requested working directory dir={s} err={}",
- .{ wd, err },
- );
- break :wd;
- };
+ const stat = dir.stat() catch |err| {
+ log.warn(
+ "failed to stat requested working directory dir={s} err={}",
+ .{ wd, err },
+ );
+ break :wd;
+ };
- if (stat.kind != .directory) {
- log.warn(
- "requested working directory is not a directory dir={s}",
- .{wd},
- );
- break :wd;
+ if (stat.kind != .directory) {
+ log.warn(
+ "requested working directory is not a directory dir={s}",
+ .{wd},
+ );
+ break :wd;
+ }
+
+ config.@"working-directory" = wd;
}
-
- config.@"working-directory" = wd;
}
// If we have a command from the options then we set it.
- const cmd = std.mem.sliceTo(opts.command, 0);
- if (cmd.len > 0) {
- config.command = .{ .shell = cmd };
- config.@"wait-after-command" = true;
+ if (opts.command) |c_command| {
+ const cmd = std.mem.sliceTo(c_command, 0);
+ if (cmd.len > 0) {
+ config.command = .{ .shell = cmd };
+ config.@"wait-after-command" = true;
+ }
+ }
+
+ // Apply any environment variables that were requested.
+ if (opts.env_var_count > 0) {
+ const alloc = config.arenaAlloc();
+ for (opts.env_vars.?[0..opts.env_var_count]) |env_var| {
+ const key = std.mem.sliceTo(env_var.key, 0);
+ const value = std.mem.sliceTo(env_var.value, 0);
+ try config.env.map.put(
+ alloc,
+ try alloc.dupeZ(u8, key),
+ try alloc.dupeZ(u8, value),
+ );
+ }
+ }
+
+ // If we have an initial input then we set it.
+ if (opts.initial_input) |c_input| {
+ const alloc = config.arenaAlloc();
+ config.input.list.clearRetainingCapacity();
+ try config.input.list.append(
+ alloc,
+ .{ .raw = try alloc.dupeZ(u8, std.mem.sliceTo(
+ c_input,
+ 0,
+ )) },
+ );
}
// Initialize our surface right away. We're given a view that is
@@ -1837,12 +1883,10 @@ pub const CAPI = struct {
return false;
};
- _ = ptr.core_surface.performBindingAction(action) catch |err| {
+ return ptr.core_surface.performBindingAction(action) catch |err| {
log.err("error performing binding action action={} err={}", .{ action, err });
return false;
};
-
- return true;
}
/// Complete a clipboard read request started via the read callback.
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index 099a051a4..7c9c15191 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -55,6 +55,11 @@ pub const c = @cImport({
const log = std.log.scoped(.gtk);
+/// This is detected by the Renderer, in which case it sends a `redraw_surface`
+/// message so that we can call `drawFrame` ourselves from the app thread,
+/// because GTK's `GLArea` does not support drawing from a different thread.
+pub const must_draw_from_app_thread = true;
+
pub const Options = struct {};
core_app: *CoreApp,
@@ -143,8 +148,8 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
if (config.@"async-backend" != .auto) {
const result: bool = switch (config.@"async-backend") {
.auto => unreachable,
- .epoll => xev.prefer(.epoll),
- .io_uring => xev.prefer(.io_uring),
+ .epoll => if (comptime xev.dynamic) xev.prefer(.epoll) else false,
+ .io_uring => if (comptime xev.dynamic) xev.prefer(.io_uring) else false,
};
if (result) {
diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig
index 1e5b1bfe8..cf8d651dd 100644
--- a/src/apprt/gtk/Surface.zig
+++ b/src/apprt/gtk/Surface.zig
@@ -41,10 +41,6 @@ const adw_version = @import("adw_version.zig");
const log = std.log.scoped(.gtk_surface);
-/// This is detected by the OpenGL renderer to move to a single-threaded
-/// draw operation. This basically puts locks around our draw path.
-pub const opengl_single_threaded_draw = true;
-
pub const Options = struct {
/// The parent surface to inherit settings such as font size, working
/// directory, etc. from.
@@ -394,7 +390,10 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
// Various other GL properties
gl_area_widget.setCursorFromName("text");
- gl_area.setRequiredVersion(3, 3);
+ gl_area.setRequiredVersion(
+ renderer.OpenGL.MIN_VERSION_MAJOR,
+ renderer.OpenGL.MIN_VERSION_MINOR,
+ );
gl_area.setHasStencilBuffer(0);
gl_area.setHasDepthBuffer(0);
gl_area.setUseEs(0);
@@ -683,12 +682,13 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
fn realize(self: *Surface) !void {
// If this surface has already been realized, then we don't need to
- // reinitialize. This can happen if a surface is moved from one GDK surface
- // to another (i.e. a tab is pulled out into a window).
+ // reinitialize. This can happen if a surface is moved from one GDK
+ // surface to another (i.e. a tab is pulled out into a window).
if (self.realized) {
// If we have no OpenGL state though, we do need to reinitialize.
- // We allow the renderer to figure that out
- try self.core_surface.renderer.displayRealize();
+ // We allow the renderer to figure that out, and then queue a draw.
+ try self.core_surface.renderer.displayRealized();
+ self.redraw();
return;
}
@@ -794,7 +794,7 @@ pub fn primaryWidget(self: *Surface) *gtk.Widget {
}
fn render(self: *Surface) !void {
- try self.core_surface.renderer.drawFrame(self);
+ try self.core_surface.renderer.drawFrame(true);
}
/// Called by core surface to get the cgroup.
diff --git a/src/build/GhosttyI18n.zig b/src/build/GhosttyI18n.zig
index a1852bb96..e0f6b5611 100644
--- a/src/build/GhosttyI18n.zig
+++ b/src/build/GhosttyI18n.zig
@@ -1,6 +1,7 @@
const GhosttyI18n = @This();
const std = @import("std");
+const builtin = @import("builtin");
const Config = @import("Config.zig");
const gresource = @import("../apprt/gtk/gresource.zig");
const internal_os = @import("../os/main.zig");
@@ -21,6 +22,14 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n {
defer steps.deinit();
inline for (internal_os.i18n.locales) |locale| {
+ // There is no encoding suffix in the LC_MESSAGES path on FreeBSD,
+ // so we need to remove it from `locale` to have a correct destination string.
+ // (/usr/local/share/locale/en_AU/LC_MESSAGES)
+ const target_locale = comptime if (builtin.target.os.tag == .freebsd)
+ std.mem.trimRight(u8, locale, ".UTF-8")
+ else
+ locale;
+
const msgfmt = b.addSystemCommand(&.{ "msgfmt", "-o", "-" });
msgfmt.addFileArg(b.path("po/" ++ locale ++ ".po"));
@@ -28,7 +37,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyI18n {
msgfmt.captureStdOut(),
std.fmt.comptimePrint(
"share/locale/{s}/LC_MESSAGES/{s}.mo",
- .{ locale, domain },
+ .{ target_locale, domain },
),
).step);
}
diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig
index 3d6b99a34..640491fd6 100644
--- a/src/build/GhosttyResources.zig
+++ b/src/build/GhosttyResources.zig
@@ -1,6 +1,7 @@
const GhosttyResources = @This();
const std = @import("std");
+const builtin = @import("builtin");
const buildpkg = @import("main.zig");
const Config = @import("Config.zig");
const config_vim = @import("../config/vim.zig");
@@ -16,6 +17,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
// Terminfo
terminfo: {
+ const os_tag = cfg.target.result.os.tag;
+ const terminfo_share_dir = if (os_tag == .freebsd)
+ "site-terminfo"
+ else
+ "terminfo";
+
// Encode our terminfo
var str = std.ArrayList(u8).init(b.allocator);
defer str.deinit();
@@ -26,12 +33,19 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
const source = wf.add("ghostty.terminfo", str.items);
if (cfg.emit_terminfo) {
- const source_install = b.addInstallFile(source, "share/terminfo/ghostty.terminfo");
+ const source_install = b.addInstallFile(
+ source,
+ if (os_tag == .freebsd)
+ "share/site-terminfo/ghostty.terminfo"
+ else
+ "share/terminfo/ghostty.terminfo",
+ );
+
try steps.append(&source_install.step);
}
// Windows doesn't have the binaries below.
- if (cfg.target.result.os.tag == .windows) break :terminfo;
+ if (os_tag == .windows) break :terminfo;
// Convert to termcap source format if thats helpful to people and
// install it. The resulting value here is the termcap source in case
@@ -43,7 +57,14 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
const out_source = run_step.captureStdOut();
_ = run_step.captureStdErr(); // so we don't see stderr
- const cap_install = b.addInstallFile(out_source, "share/terminfo/ghostty.termcap");
+ const cap_install = b.addInstallFile(
+ out_source,
+ if (os_tag == .freebsd)
+ "share/site-terminfo/ghostty.termcap"
+ else
+ "share/terminfo/ghostty.termcap",
+ );
+
try steps.append(&cap_install.step);
}
@@ -51,7 +72,8 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
{
const run_step = RunStep.create(b, "tic");
run_step.addArgs(&.{ "tic", "-x", "-o" });
- const path = run_step.addOutputFileArg("terminfo");
+ const path = run_step.addOutputFileArg(terminfo_share_dir);
+
run_step.addFileArg(source);
_ = run_step.captureStdErr(); // so we don't see stderr
@@ -63,7 +85,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
.windows => mkdir_step.addArgs(&.{"mkdir"}),
else => mkdir_step.addArgs(&.{ "mkdir", "-p" }),
}
- mkdir_step.addArg(b.fmt("{s}/share/terminfo", .{b.install_path}));
+
+ mkdir_step.addArg(b.fmt(
+ "{s}/share/{s}",
+ .{ b.install_path, terminfo_share_dir },
+ ));
+
try steps.append(&mkdir_step.step);
// Use cp -R instead of Step.InstallDir because we need to preserve
diff --git a/src/build/MetallibStep.zig b/src/build/MetallibStep.zig
index bac3a72c5..6999f8f31 100644
--- a/src/build/MetallibStep.zig
+++ b/src/build/MetallibStep.zig
@@ -22,10 +22,24 @@ step: *Step,
output: LazyPath,
pub fn create(b: *std.Build, opts: Options) ?*MetallibStep {
- switch (opts.target.result.os.tag) {
- .macos, .ios => {},
- else => return null, // Only macOS and iOS are supported.
- }
+ const sdk = switch (opts.target.result.os.tag) {
+ .macos => "macosx",
+ .ios => switch (opts.target.result.abi) {
+ // The iOS simulator uses the same SDK for Metal as the device,
+ // but the minimum version tag causes different behaviors.
+ .simulator => "iphoneos",
+ else => "iphoneos",
+ },
+ else => return null,
+ };
+ const platform_version_arg = switch (opts.target.result.os.tag) {
+ .macos => "-mmacos-version-min",
+ .ios => switch (opts.target.result.abi) {
+ .simulator => "-mios-simulator-version-min",
+ else => "-mios-version-min",
+ },
+ else => null,
+ };
const self = b.allocator.create(MetallibStep) catch @panic("OOM");
@@ -37,51 +51,26 @@ pub fn create(b: *std.Build, opts: Options) ?*MetallibStep {
else => unreachable,
};
- // Find the metal and metallib executables. The Apple docs
- // at the time of writing (June 2025) say to use
- // `xcrun --sdk metal` but this doesn't work with Xcode 26.
- //
- // I don't know if this is a bug but the xcodebuild approach also
- // works with Xcode 15 so it seems safe to use this instead.
- //
- // Reported bug: FB17874042.
- var code: u8 = undefined;
- const metal_exe = std.mem.trim(u8, b.runAllowFail(
- &.{ "xcodebuild", "-find-executable", "metal" },
- &code,
- .Ignore,
- ) catch return null, "\r\n ");
- const metallib_exe = std.mem.trim(u8, b.runAllowFail(
- &.{ "xcodebuild", "-find-executable", "metallib" },
- &code,
- .Ignore,
- ) catch return null, "\r\n ");
-
const run_ir = RunStep.create(
b,
b.fmt("metal {s}", .{opts.name}),
);
- run_ir.addArgs(&.{ metal_exe, "-o" });
+ run_ir.addArgs(&.{ "/usr/bin/xcrun", "-sdk", sdk, "metal", "-o" });
const output_ir = run_ir.addOutputFileArg(b.fmt("{s}.ir", .{opts.name}));
run_ir.addArgs(&.{"-c"});
for (opts.sources) |source| run_ir.addFileArg(source);
- switch (opts.target.result.os.tag) {
- .ios => run_ir.addArgs(&.{b.fmt(
- "-mios-version-min={s}",
- .{min_version},
- )}),
- .macos => run_ir.addArgs(&.{b.fmt(
- "-mmacos-version-min={s}",
- .{min_version},
- )}),
- else => {},
+ if (platform_version_arg) |arg| {
+ run_ir.addArgs(&.{b.fmt(
+ "{s}={s}",
+ .{ arg, min_version },
+ )});
}
const run_lib = RunStep.create(
b,
b.fmt("metallib {s}", .{opts.name}),
);
- run_lib.addArgs(&.{ metallib_exe, "-o" });
+ run_lib.addArgs(&.{ "/usr/bin/xcrun", "-sdk", sdk, "metallib", "-o" });
const output_lib = run_lib.addOutputFileArg(b.fmt("{s}.metallib", .{opts.name}));
run_lib.addFileArg(output_ir);
run_lib.step.dependOn(&run_ir.step);
diff --git a/src/cli/args.zig b/src/cli/args.zig
index 68972a622..3c34e17fe 100644
--- a/src/cli/args.zig
+++ b/src/cli/args.zig
@@ -414,7 +414,7 @@ pub fn parseIntoField(
return error.InvalidField;
}
-fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T {
+pub fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T {
const info = @typeInfo(T).@"union";
assert(@typeInfo(info.tag_type.?) == .@"enum");
@@ -1090,6 +1090,7 @@ test "parseIntoField: tagged union" {
b: u8,
c: void,
d: []const u8,
+ e: [:0]const u8,
} = undefined,
} = .{};
@@ -1108,6 +1109,10 @@ test "parseIntoField: tagged union" {
// Set string field
try parseIntoField(@TypeOf(data), alloc, &data, "value", "d:hello");
try testing.expectEqualStrings("hello", data.value.d);
+
+ // Set sentinel string field
+ try parseIntoField(@TypeOf(data), alloc, &data, "value", "e:hello");
+ try testing.expectEqualStrings("hello", data.value.e);
}
test "parseIntoField: tagged union unknown filed" {
diff --git a/src/config.zig b/src/config.zig
index fb7359b3e..018d0e6e8 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -3,6 +3,7 @@ const builtin = @import("builtin");
const formatter = @import("config/formatter.zig");
pub const Config = @import("config/Config.zig");
pub const conditional = @import("config/conditional.zig");
+pub const io = @import("config/io.zig");
pub const string = @import("config/string.zig");
pub const edit = @import("config/edit.zig");
pub const url = @import("config/url.zig");
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 2df66ba45..6bc3a7f23 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -34,6 +34,7 @@ const ErrorList = @import("ErrorList.zig");
const MetricModifier = fontpkg.Metrics.Modifier;
const help_strings = @import("help_strings");
pub const Command = @import("command.zig").Command;
+const RepeatableReadableIO = @import("io.zig").RepeatableReadableIO;
const RepeatableStringMap = @import("RepeatableStringMap.zig");
pub const Path = @import("path.zig").Path;
pub const RepeatablePath = @import("path.zig").RepeatablePath;
@@ -266,6 +267,9 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// This affects the appearance of text and of any images with transparency.
/// Additionally, custom shaders will receive colors in the configured space.
///
+/// On macOS the default is `native`, on all other platforms the default is
+/// `linear-corrected`.
+///
/// Valid values:
///
/// * `native` - Perform alpha blending in the native color space for the OS.
@@ -276,12 +280,15 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// when certain color combinations are used (e.g. red / green), but makes
/// dark text look much thinner than normal and light text much thicker.
/// This is also sometimes known as "gamma correction".
-/// (Currently only supported on macOS. Has no effect on Linux.)
///
/// * `linear-corrected` - Same as `linear`, but with a correction step applied
/// for text that makes it look nearly or completely identical to `native`,
/// but without any of the darkening artifacts.
-@"alpha-blending": AlphaBlending = .native,
+@"alpha-blending": AlphaBlending =
+ if (builtin.os.tag == .macos)
+ .native
+ else
+ .@"linear-corrected",
/// All of the configurations behavior adjust various metrics determined by the
/// font. The values can be integers (1, -1, etc.) or a percentage (20%, -15%,
@@ -801,6 +808,47 @@ command: ?Command = null,
/// browser.
env: RepeatableStringMap = .{},
+/// Data to send as input to the command on startup.
+///
+/// The configured `command` will be launched using the typical rules,
+/// then the data specified as this input will be written to the pty
+/// before any other input can be provided.
+///
+/// The bytes are sent as-is with no additional encoding. Therefore, be
+/// cautious about input that can contain control characters, because this
+/// can be used to execute programs in a shell.
+///
+/// The format of this value is:
+///
+/// * `raw:` - Send raw text as-is. This uses Zig string literal
+/// syntax so you can specify control characters and other standard
+/// escapes.
+///
+/// * `path:` - Read a filepath and send the contents. The path
+/// must be to a file with finite length. e.g. don't use a device
+/// such as `/dev/stdin` or `/dev/urandom` as these will block
+/// terminal startup indefinitely. Files are limited to 10MB
+/// in size to prevent excessive memory usage. If you have files
+/// larger than this you should write a script to read the file
+/// and send it to the terminal.
+///
+/// If no valid prefix is found, it is assumed to be a `raw:` input.
+/// This is an ergonomic choice to allow you to simply write
+/// `input = "Hello, world!"` (a common case) without needing to prefix
+/// every value with `raw:`.
+///
+/// This can be repeated multiple times to send more data. The data
+/// is concatenated directly with no separator characters in between
+/// (e.g. no newline).
+///
+/// If any of the input sources do not exist, then none of the input
+/// will be sent. Input sources are not verified until the terminal
+/// is starting, so missing paths will not show up in config validation.
+///
+/// Changing this configuration at runtime will only affect new
+/// terminals.
+input: RepeatableReadableIO = .{},
+
/// If true, keep the terminal open after the command exits. Normally, the
/// terminal window closes when the running command (such as a shell) exits.
/// With this true, the terminal window will stay open until any keypress is
@@ -1823,7 +1871,7 @@ keybind: Keybinds = .{},
/// Automatically hide the quick terminal when focus shifts to another window.
/// Set it to false for the quick terminal to remain open even when it loses focus.
///
-/// Defaults to true on macOS and on false on Linux. This is because global
+/// Defaults to true on macOS and on false on Linux/BSD. This is because global
/// shortcuts on Linux require system configuration and are considerably less
/// accessible than on macOS, meaning that it is more preferable to keep the
/// quick terminal open until the user has completed their task.
@@ -1961,9 +2009,6 @@ keybind: Keybinds = .{},
/// causing the window to be completely black. If this happens, you can
/// unset this configuration to disable the shader.
///
-/// On Linux, this requires OpenGL 4.2. Ghostty typically only requires
-/// OpenGL 3.3, but custom shaders push that requirement up to 4.2.
-///
/// The shader API is identical to the Shadertoy API: you specify a `mainImage`
/// function and the available uniforms match Shadertoy. The iChannel0 uniform
/// is a texture containing the rendered terminal screen.
@@ -1977,8 +2022,7 @@ keybind: Keybinds = .{},
/// This can be repeated multiple times to load multiple shaders. The shaders
/// will be run in the order they are specified.
///
-/// Changing this value at runtime and reloading the configuration will only
-/// affect new windows, tabs, and splits.
+/// This can be changed at runtime and will affect all open terminals.
@"custom-shader": RepeatablePath = .{},
/// If `true` (default), the focused terminal surface will run an animation
@@ -1996,8 +2040,7 @@ keybind: Keybinds = .{},
/// will use more CPU per terminal surface and can become quite expensive
/// depending on the shader and your terminal usage.
///
-/// This value can be changed at runtime and will affect all currently
-/// open terminals.
+/// This can be changed at runtime and will affect all open terminals.
@"custom-shader-animation": CustomShaderAnimation = .true,
/// Bell features to enable if bell support is available in your runtime. Not
@@ -2355,6 +2398,29 @@ keybind: Keybinds = .{},
///
@"macos-icon-screen-color": ?ColorList = null,
+/// Whether macOS Shortcuts are allowed to control Ghostty.
+///
+/// Ghostty exposes a number of actions that allow Shortcuts to
+/// control and interact with Ghostty. This includes creating new
+/// terminals, sending text to terminals, running commands, invoking
+/// any keybind action, etc.
+///
+/// This is a powerful feature but can be a security risk if a malicious
+/// shortcut is able to be installed and executed. Therefore, this
+/// configuration allows you to disable this feature.
+///
+/// Valid values are:
+///
+/// * `ask` - Ask the user whether for permission. Ghostty will remember
+/// this choice and never ask again. This is similar to other macOS
+/// permissions such as microphone access, camera access, etc.
+///
+/// * `allow` - Allow Shortcuts to control Ghostty without asking.
+///
+/// * `deny` - Deny Shortcuts from controlling Ghostty.
+///
+@"macos-shortcuts": MacShortcuts = .ask,
+
/// Put every surface (tab, split, window) into a dedicated Linux cgroup.
///
/// This makes it so that resource management can be done on a per-surface
@@ -2381,7 +2447,10 @@ keybind: Keybinds = .{},
/// * `single-instance` - Enable cgroups only for Ghostty instances launched
/// as single-instance applications (see gtk-single-instance).
///
-@"linux-cgroup": LinuxCgroup = .@"single-instance",
+@"linux-cgroup": LinuxCgroup = if (builtin.os.tag == .linux)
+ .@"single-instance"
+else
+ .never,
/// Memory limit for any individual terminal process (tab, split, window,
/// etc.) in bytes. If this is unset then no memory limit will be set.
@@ -2794,7 +2863,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
.windows => {},
// Fast-path if we are Linux and have no args.
- .linux => if (std.os.argv.len <= 1) return,
+ .linux, .freebsd => if (std.os.argv.len <= 1) return,
// Everything else we have to at least try because it may
// not use std.os.argv.
@@ -2812,7 +2881,7 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
// styling, etc. based on the command.
//
// See: https://github.com/Vladimir-csp/xdg-terminal-exec
- if (comptime builtin.os.tag == .linux) {
+ if ((comptime builtin.os.tag == .linux) or (comptime builtin.os.tag == .freebsd)) {
if (internal_os.xdg.parseTerminalExec(std.os.argv)) |args| {
const arena_alloc = self._arena.?.allocator();
@@ -3004,6 +3073,11 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
}
}
+/// Get the arena allocator associated with the configuration.
+pub fn arenaAlloc(self: *Config) Allocator {
+ return self._arena.?.allocator();
+}
+
/// Change the state of conditionals and reload the configuration
/// based on the new state. This returns a new configuration based
/// on the new state. The caller must free the old configuration if they
@@ -5956,6 +6030,13 @@ pub const MacAppIconFrame = enum {
chrome,
};
+/// See macos-shortcuts
+pub const MacShortcuts = enum {
+ allow,
+ deny,
+ ask,
+};
+
/// See gtk-single-instance
pub const GtkSingleInstance = enum {
desktop,
diff --git a/src/config/io.zig b/src/config/io.zig
new file mode 100644
index 000000000..8be4be551
--- /dev/null
+++ b/src/config/io.zig
@@ -0,0 +1,256 @@
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const ArenaAllocator = std.heap.ArenaAllocator;
+const string = @import("string.zig");
+const formatterpkg = @import("formatter.zig");
+const cli = @import("../cli.zig");
+
+/// ReadableIO is some kind of IO source that is readable.
+///
+/// It can be either a direct string or a filepath. The filepath will
+/// be deferred and read later, so it won't be checked for existence
+/// or readability at configuration time. This allows using a path that
+/// might be produced in an intermediate state.
+pub const ReadableIO = union(enum) {
+ const Self = @This();
+
+ raw: [:0]const u8,
+ path: [:0]const u8,
+
+ pub fn parseCLI(
+ self: *Self,
+ alloc: Allocator,
+ input_: ?[]const u8,
+ ) !void {
+ const input = input_ orelse return error.ValueRequired;
+ if (input.len == 0) return error.ValueRequired;
+
+ // We create a buffer only to do string parsing and validate
+ // it works. We store the value as raw so that our formatting
+ // can recreate it.
+ {
+ const buf = try alloc.alloc(u8, input.len);
+ defer alloc.free(buf);
+ _ = try string.parse(buf, input);
+ }
+
+ // Next, parse the tagged union using normal rules.
+ self.* = cli.args.parseTaggedUnion(
+ Self,
+ alloc,
+ input,
+ ) catch |err| switch (err) {
+ // Invalid values in the tagged union are interpreted as
+ // raw values. This lets users pass in simple string values
+ // without needing to tag them.
+ error.InvalidValue => .{ .raw = try alloc.dupeZ(u8, input) },
+ else => return err,
+ };
+ }
+
+ pub fn clone(self: Self, alloc: Allocator) Allocator.Error!Self {
+ return switch (self) {
+ .raw => |v| .{ .raw = try alloc.dupeZ(u8, v) },
+ .path => |v| .{ .path = try alloc.dupeZ(u8, v) },
+ };
+ }
+
+ /// Same as clone but also parses the values as Zig strings in
+ /// the final resulting value all at once so we can avoid extra
+ /// allocations.
+ pub fn cloneParsed(
+ self: Self,
+ alloc: Allocator,
+ ) Allocator.Error!Self {
+ switch (self) {
+ inline else => |v, tag| {
+ // Parsing can't fail because we validate it in parseCLI
+ const copied = try alloc.dupeZ(u8, v);
+ const parsed = string.parse(copied, v) catch unreachable;
+ assert(copied.ptr == parsed.ptr);
+
+ // If we parsed less than our original length we need
+ // to keep it null-terminated.
+ if (parsed.len < copied.len) copied[parsed.len] = 0;
+
+ return @unionInit(
+ Self,
+ @tagName(tag),
+ copied[0..parsed.len :0],
+ );
+ },
+ }
+ }
+
+ pub fn equal(self: Self, other: Self) bool {
+ if (std.meta.activeTag(self) != std.meta.activeTag(other)) {
+ return false;
+ }
+
+ return switch (self) {
+ .raw => |v| std.mem.eql(u8, v, other.raw),
+ .path => |v| std.mem.eql(u8, v, other.path),
+ };
+ }
+
+ pub fn formatEntry(self: Self, formatter: anytype) !void {
+ var buf: [4096]u8 = undefined;
+ var fbs = std.io.fixedBufferStream(&buf);
+ const writer = fbs.writer();
+ switch (self) {
+ inline else => |v, tag| {
+ writer.writeAll(@tagName(tag)) catch return error.OutOfMemory;
+ writer.writeByte(':') catch return error.OutOfMemory;
+ writer.writeAll(v) catch return error.OutOfMemory;
+ },
+ }
+
+ const written = fbs.getWritten();
+ try formatter.formatEntry(
+ []const u8,
+ written,
+ );
+ }
+
+ test "parseCLI" {
+ const testing = std.testing;
+ var arena = ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+ {
+ var io: Self = undefined;
+ try Self.parseCLI(&io, alloc, "foo");
+ try testing.expect(io == .raw);
+ try testing.expectEqualStrings("foo", io.raw);
+ }
+ {
+ var io: Self = undefined;
+ try Self.parseCLI(&io, alloc, "raw:foo");
+ try testing.expect(io == .raw);
+ try testing.expectEqualStrings("foo", io.raw);
+ }
+ {
+ var io: Self = undefined;
+ try Self.parseCLI(&io, alloc, "path:foo");
+ try testing.expect(io == .path);
+ try testing.expectEqualStrings("foo", io.path);
+ }
+ }
+
+ test "formatEntry" {
+ const testing = std.testing;
+ var arena = ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ var buf = std.ArrayList(u8).init(alloc);
+ defer buf.deinit();
+
+ var v: Self = undefined;
+ try v.parseCLI(alloc, "raw:foo");
+ try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
+ try std.testing.expectEqualSlices(u8, "a = raw:foo\n", buf.items);
+ }
+};
+
+pub const RepeatableReadableIO = struct {
+ const Self = @This();
+
+ // Allocator for the list is the arena for the parent config.
+ list: std.ArrayListUnmanaged(ReadableIO) = .{},
+
+ pub fn parseCLI(
+ self: *Self,
+ alloc: Allocator,
+ input: ?[]const u8,
+ ) !void {
+ const value = input orelse return error.ValueRequired;
+
+ // Empty value resets the list
+ if (value.len == 0) {
+ self.list.clearRetainingCapacity();
+ return;
+ }
+
+ var io: ReadableIO = undefined;
+ try ReadableIO.parseCLI(&io, alloc, value);
+ try self.list.append(alloc, io);
+ }
+
+ /// Deep copy of the struct. Required by Config.
+ pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self {
+ var list = try std.ArrayListUnmanaged(ReadableIO).initCapacity(
+ alloc,
+ self.list.items.len,
+ );
+ for (self.list.items) |item| {
+ const copy = try item.clone(alloc);
+ list.appendAssumeCapacity(copy);
+ }
+
+ return .{ .list = list };
+ }
+
+ /// See ReadableIO.cloneParsed
+ pub fn cloneParsed(
+ self: *const Self,
+ alloc: Allocator,
+ ) Allocator.Error!Self {
+ var list = try std.ArrayListUnmanaged(ReadableIO).initCapacity(
+ alloc,
+ self.list.items.len,
+ );
+ for (self.list.items) |item| {
+ const copy = try item.cloneParsed(alloc);
+ list.appendAssumeCapacity(copy);
+ }
+
+ return .{ .list = list };
+ }
+
+ /// Compare if two of our value are requal. Required by Config.
+ pub fn equal(self: Self, other: Self) bool {
+ const itemsA = self.list.items;
+ const itemsB = other.list.items;
+ if (itemsA.len != itemsB.len) return false;
+ for (itemsA, itemsB) |a, b| {
+ if (!a.equal(b)) return false;
+ } else return true;
+ }
+
+ /// Used by Formatter
+ pub fn formatEntry(
+ self: Self,
+ formatter: anytype,
+ ) !void {
+ if (self.list.items.len == 0) {
+ try formatter.formatEntry(void, {});
+ return;
+ }
+
+ for (self.list.items) |value| {
+ try formatter.formatEntry(ReadableIO, value);
+ }
+ }
+
+ test "parseCLI" {
+ const testing = std.testing;
+ var arena = ArenaAllocator.init(testing.allocator);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ var list: Self = .{};
+ try list.parseCLI(alloc, "raw:A");
+ try list.parseCLI(alloc, "path:B");
+ try testing.expectEqual(@as(usize, 2), list.list.items.len);
+
+ try list.parseCLI(alloc, "");
+ try testing.expectEqual(@as(usize, 0), list.list.items.len);
+ }
+};
+
+test {
+ _ = ReadableIO;
+ _ = RepeatableReadableIO;
+}
diff --git a/src/config/string.zig b/src/config/string.zig
index 5e0d40e55..71826f005 100644
--- a/src/config/string.zig
+++ b/src/config/string.zig
@@ -3,7 +3,7 @@ const std = @import("std");
/// Parse a string literal into a byte array. The string can contain
/// any valid Zig string literal escape sequences.
///
-/// The output buffer never needs sto be larger than the input buffer.
+/// The output buffer never needs to be larger than the input buffer.
/// The buffers may alias.
pub fn parse(out: []u8, bytes: []const u8) ![]u8 {
var dst_i: usize = 0;
diff --git a/src/crash/sentry.zig b/src/crash/sentry.zig
index c29184020..820c3e9a1 100644
--- a/src/crash/sentry.zig
+++ b/src/crash/sentry.zig
@@ -81,6 +81,13 @@ pub fn init(gpa: Allocator) !void {
fn initThread(gpa: Allocator) !void {
if (comptime !build_options.sentry) return;
+ // Right now, on Darwin, `std.Thread.setName` can only name the current
+ // thread, and we have no way to get the current thread from within it,
+ // so instead we use this code to name the thread instead.
+ if (builtin.os.tag.isDarwin()) {
+ internal_os.macos.pthread_setname_np(&"sentry-init".*);
+ }
+
var arena = std.heap.ArenaAllocator.init(gpa);
defer arena.deinit();
const alloc = arena.allocator();
diff --git a/src/font/sprite/cursor.zig b/src/font/sprite/cursor.zig
index 62195316e..d63db624a 100644
--- a/src/font/sprite/cursor.zig
+++ b/src/font/sprite/cursor.zig
@@ -50,7 +50,11 @@ pub fn renderGlyph(
const region = try canvas.writeAtlas(alloc, atlas);
return font.Glyph{
- .width = width,
+ // HACK: Set the width for the bar cursor to just the thickness,
+ // this is just for the benefit of the custom shader cursor
+ // uniform code. -- In the future code will be introduced to
+ // auto-crop the canvas so that this isn't needed.
+ .width = if (sprite == .cursor_bar) thickness else width,
.height = height,
.offset_x = 0,
.offset_y = @intCast(height),
diff --git a/src/input/keycodes.zig b/src/input/keycodes.zig
index a85f36d31..2fa0665ea 100644
--- a/src/input/keycodes.zig
+++ b/src/input/keycodes.zig
@@ -11,7 +11,7 @@ pub const entries: []const Entry = entries: {
const native_idx = switch (builtin.os.tag) {
.ios, .macos => 4, // mac
.windows => 3, // win
- .linux => 2, // xkb
+ .freebsd, .linux => 2, // xkb
else => @compileError("unsupported platform"),
};
diff --git a/src/os/cf_release_thread.zig b/src/os/cf_release_thread.zig
index dbf8e6592..445dc4864 100644
--- a/src/os/cf_release_thread.zig
+++ b/src/os/cf_release_thread.zig
@@ -8,6 +8,7 @@ const std = @import("std");
const builtin = @import("builtin");
const macos = @import("macos");
+const internal_os = @import("../os/main.zig");
const xev = @import("../global.zig").xev;
const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue;
@@ -119,6 +120,13 @@ pub fn threadMain(self: *Thread) void {
fn threadMain_(self: *Thread) !void {
defer log.debug("cf release thread exited", .{});
+ // Right now, on Darwin, `std.Thread.setName` can only name the current
+ // thread, and we have no way to get the current thread from within it,
+ // so instead we use this code to name the thread instead.
+ if (builtin.os.tag.isDarwin()) {
+ internal_os.macos.pthread_setname_np(&"cf_release".*);
+ }
+
// Start the async handlers. We start these first so that they're
// registered even if anything below fails so we can drain the mailbox.
self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback);
diff --git a/src/os/desktop.zig b/src/os/desktop.zig
index c73f150e0..3bc843e5c 100644
--- a/src/os/desktop.zig
+++ b/src/os/desktop.zig
@@ -30,24 +30,24 @@ pub fn launchedFromDesktop() bool {
break :macos c.getppid() == 1;
},
- // On Linux, GTK sets GIO_LAUNCHED_DESKTOP_FILE and
+ // On Linux and BSD, GTK sets GIO_LAUNCHED_DESKTOP_FILE and
// GIO_LAUNCHED_DESKTOP_FILE_PID. We only check the latter to see if
// we match the PID and assume that if we do, we were launched from
// the desktop file. Pid comparing catches the scenario where
// another terminal was launched from a desktop file and then launches
// Ghostty and Ghostty inherits the env.
- .linux => linux: {
+ .linux, .freebsd => ul: {
const gio_pid_str = posix.getenv("GIO_LAUNCHED_DESKTOP_FILE_PID") orelse
- break :linux false;
+ break :ul false;
const pid = c.getpid();
const gio_pid = std.fmt.parseInt(
@TypeOf(pid),
gio_pid_str,
10,
- ) catch break :linux false;
+ ) catch break :ul false;
- break :linux gio_pid == pid;
+ break :ul gio_pid == pid;
},
// TODO: This should have some logic to detect this. Perhaps std.builtin.subsystem
@@ -71,14 +71,14 @@ pub const DesktopEnvironment = enum {
};
/// Detect what desktop environment we are running under. This is mainly used
-/// on Linux to enable or disable certain features but there may be more uses in
+/// on Linux and BSD to enable or disable certain features but there may be more uses in
/// the future.
pub fn desktopEnvironment() DesktopEnvironment {
return switch (comptime builtin.os.tag) {
.macos => .macos,
.windows => .windows,
- .linux => de: {
- if (@inComptime()) @compileError("Checking for the desktop environment on Linux must be done at runtime.");
+ .linux, .freebsd => de: {
+ if (@inComptime()) @compileError("Checking for the desktop environment on Linux/BSD must be done at runtime.");
// Use $XDG_SESSION_DESKTOP to determine what DE we are using on Linux
// https://www.freedesktop.org/software/systemd/man/latest/pam_systemd.html#desktop=
@@ -110,7 +110,7 @@ test "desktop environment" {
switch (builtin.os.tag) {
.macos => try testing.expectEqual(.macos, desktopEnvironment()),
.windows => try testing.expectEqual(.windows, desktopEnvironment()),
- .linux => {
+ .linux, .freebsd => {
const getenv = std.posix.getenv;
const setenv = @import("env.zig").setenv;
const unsetenv = @import("env.zig").unsetenv;
diff --git a/src/os/homedir.zig b/src/os/homedir.zig
index b5629fd65..f3d6e4498 100644
--- a/src/os/homedir.zig
+++ b/src/os/homedir.zig
@@ -14,7 +14,7 @@ const Error = error{
/// is generally an expensive process so the value should be cached.
pub inline fn home(buf: []u8) !?[]const u8 {
return switch (builtin.os.tag) {
- inline .linux, .macos => try homeUnix(buf),
+ inline .linux, .freebsd, .macos => try homeUnix(buf),
.windows => try homeWindows(buf),
// iOS doesn't have a user-writable home directory
@@ -122,7 +122,7 @@ pub const ExpandError = error{
/// than `buf.len`.
pub fn expandHome(path: []const u8, buf: []u8) ExpandError![]const u8 {
return switch (builtin.os.tag) {
- .linux, .macos => try expandHomeUnix(path, buf),
+ .linux, .freebsd, .macos => try expandHomeUnix(path, buf),
.ios => return path,
else => @compileError("unimplemented"),
};
diff --git a/src/os/i18n.zig b/src/os/i18n.zig
index 483db3673..f262de579 100644
--- a/src/os/i18n.zig
+++ b/src/os/i18n.zig
@@ -69,23 +69,27 @@ pub const InitError = error{
/// want to set the domain for the entire application since this is also
/// used by libghostty.
pub fn init(resources_dir: []const u8) InitError!void {
- // i18n is unsupported on Windows
- if (builtin.os.tag == .windows) return;
+ switch (builtin.os.tag) {
+ // i18n is unsupported on Windows
+ .windows => return,
- // Our resources dir is always nested below the share dir that
- // is standard for translations.
- const share_dir = std.fs.path.dirname(resources_dir) orelse
- return error.InvalidResourcesDir;
+ else => {
+ // Our resources dir is always nested below the share dir that
+ // is standard for translations.
+ const share_dir = std.fs.path.dirname(resources_dir) orelse
+ return error.InvalidResourcesDir;
- // Build our locale path
- var buf: [std.fs.max_path_bytes]u8 = undefined;
- const path = std.fmt.bufPrintZ(&buf, "{s}/locale", .{share_dir}) catch
- return error.OutOfMemory;
+ // Build our locale path
+ var buf: [std.fs.max_path_bytes]u8 = undefined;
+ const path = std.fmt.bufPrintZ(&buf, "{s}/locale", .{share_dir}) catch
+ return error.OutOfMemory;
- // Bind our bundle ID to the given locale path
- log.debug("binding domain={s} path={s}", .{ build_config.bundle_id, path });
- _ = bindtextdomain(build_config.bundle_id, path.ptr) orelse
- return error.OutOfMemory;
+ // Bind our bundle ID to the given locale path
+ log.debug("binding domain={s} path={s}", .{ build_config.bundle_id, path });
+ _ = bindtextdomain(build_config.bundle_id, path.ptr) orelse
+ return error.OutOfMemory;
+ },
+ }
}
/// Set the global gettext domain to our bundle ID, allowing unqualified
diff --git a/src/os/macos.zig b/src/os/macos.zig
index ca7c81a47..100d0fe44 100644
--- a/src/os/macos.zig
+++ b/src/os/macos.zig
@@ -88,6 +88,10 @@ extern "c" fn pthread_set_qos_class_self_np(
relative_priority: c_int,
) c_int;
+pub extern "c" fn pthread_setname_np(
+ name: [*:0]const u8,
+) void;
+
pub const NSOperatingSystemVersion = extern struct {
major: i64,
minor: i64,
diff --git a/src/os/open.zig b/src/os/open.zig
index f7eadd06e..39f28036f 100644
--- a/src/os/open.zig
+++ b/src/os/open.zig
@@ -19,7 +19,7 @@ pub fn open(
url: []const u8,
) !void {
const cmd: OpenCommand = switch (builtin.os.tag) {
- .linux => .{ .child = std.process.Child.init(
+ .linux, .freebsd => .{ .child = std.process.Child.init(
&.{ "xdg-open", url },
alloc,
) },
diff --git a/src/os/resourcesdir.zig b/src/os/resourcesdir.zig
index 6f69b91d3..0ef92d3b3 100644
--- a/src/os/resourcesdir.zig
+++ b/src/os/resourcesdir.zig
@@ -32,6 +32,7 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
const sentinels = switch (comptime builtin.target.os.tag) {
.windows => .{"terminfo/ghostty.terminfo"},
.macos => .{"terminfo/78/xterm-ghostty"},
+ .freebsd => .{ "site-terminfo/g/ghostty", "site-terminfo/x/xterm-ghostty" },
else => .{ "terminfo/g/ghostty", "terminfo/x/xterm-ghostty" },
};
@@ -54,11 +55,16 @@ pub fn resourcesDir(alloc: std.mem.Allocator) !?[]const u8 {
}
}
- // On all platforms, we look for a /usr/share style path. This
+ // On all platforms (except BSD), we look for a /usr/share style path. This
// is valid even on Mac since there is nothing that requires
// Ghostty to be in an app bundle.
inline for (sentinels) |sentinel| {
- if (try maybeDir(&dir_buf, dir, "share", sentinel)) |v| {
+ if (try maybeDir(
+ &dir_buf,
+ dir,
+ if (builtin.target.os.tag == .freebsd) "local/share" else "share",
+ sentinel,
+ )) |v| {
return try std.fs.path.join(alloc, &.{ v, "ghostty" });
}
}
diff --git a/src/pty.zig b/src/pty.zig
index a36de9adc..02906b778 100644
--- a/src/pty.zig
+++ b/src/pty.zig
@@ -99,6 +99,10 @@ const PosixPty = struct {
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("util.h"); // openpty()
}),
+ .freebsd => @cImport({
+ @cInclude("termios.h"); // ioctl and constants
+ @cInclude("libutil.h"); // openpty()
+ }),
else => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("pty.h");
diff --git a/src/renderer.zig b/src/renderer.zig
index 61d9a4e53..e3ed070b6 100644
--- a/src/renderer.zig
+++ b/src/renderer.zig
@@ -16,6 +16,7 @@ const cursor = @import("renderer/cursor.zig");
const message = @import("renderer/message.zig");
const size = @import("renderer/size.zig");
pub const shadertoy = @import("renderer/shadertoy.zig");
+pub const GenericRenderer = @import("renderer/generic.zig").Renderer;
pub const Metal = @import("renderer/Metal.zig");
pub const OpenGL = @import("renderer/OpenGL.zig");
pub const WebGL = @import("renderer/WebGL.zig");
@@ -56,8 +57,8 @@ pub const Impl = enum {
/// The implementation to use for the renderer. This is comptime chosen
/// so that every build has exactly one renderer implementation.
pub const Renderer = switch (build_config.renderer) {
- .metal => Metal,
- .opengl => OpenGL,
+ .metal => GenericRenderer(Metal),
+ .opengl => GenericRenderer(OpenGL),
.webgl => WebGL,
};
diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig
index 99dbc838e..a001ca08d 100644
--- a/src/renderer/Metal.zig
+++ b/src/renderer/Metal.zig
@@ -1,547 +1,88 @@
-//! Renderer implementation for Metal.
-//!
-//! Open questions:
-//!
+//! Graphics API wrapper for Metal.
pub const Metal = @This();
const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const glfw = @import("glfw");
const objc = @import("objc");
const macos = @import("macos");
-const imgui = @import("imgui");
-const glslang = @import("glslang");
-const xev = @import("../global.zig").xev;
-const apprt = @import("../apprt.zig");
-const configpkg = @import("../config.zig");
-const font = @import("../font/main.zig");
-const os = @import("../os/main.zig");
-const terminal = @import("../terminal/main.zig");
-const renderer = @import("../renderer.zig");
-const math = @import("../math.zig");
-const Surface = @import("../Surface.zig");
-const link = @import("link.zig");
const graphics = macos.graphics;
-const fgMode = @import("cell.zig").fgMode;
-const isCovering = @import("cell.zig").isCovering;
+const apprt = @import("../apprt.zig");
+const font = @import("../font/main.zig");
+const configpkg = @import("../config.zig");
+const rendererpkg = @import("../renderer.zig");
+const Renderer = rendererpkg.GenericRenderer(Metal);
const shadertoy = @import("shadertoy.zig");
-const assert = std.debug.assert;
-const Allocator = std.mem.Allocator;
-const ArenaAllocator = std.heap.ArenaAllocator;
-const CFReleaseThread = os.CFReleaseThread;
-const Terminal = terminal.Terminal;
-const Health = renderer.Health;
const mtl = @import("metal/api.zig");
-const mtl_buffer = @import("metal/buffer.zig");
-const mtl_cell = @import("metal/cell.zig");
-const mtl_image = @import("metal/image.zig");
-const mtl_sampler = @import("metal/sampler.zig");
-const mtl_shaders = @import("metal/shaders.zig");
-const Image = mtl_image.Image;
-const ImageMap = mtl_image.ImageMap;
-const Shaders = mtl_shaders.Shaders;
+const IOSurfaceLayer = @import("metal/IOSurfaceLayer.zig");
-const ImageBuffer = mtl_buffer.Buffer(mtl_shaders.Image);
-const InstanceBuffer = mtl_buffer.Buffer(u16);
+pub const GraphicsAPI = Metal;
+pub const Target = @import("metal/Target.zig");
+pub const Frame = @import("metal/Frame.zig");
+pub const RenderPass = @import("metal/RenderPass.zig");
+pub const Pipeline = @import("metal/Pipeline.zig");
+const bufferpkg = @import("metal/buffer.zig");
+pub const Buffer = bufferpkg.Buffer;
+pub const Texture = @import("metal/Texture.zig");
+pub const shaders = @import("metal/shaders.zig");
-const ImagePlacementList = std.ArrayListUnmanaged(mtl_image.Placement);
+pub const cellpkg = @import("metal/cell.zig");
+pub const imagepkg = @import("metal/image.zig");
-const DisplayLink = switch (builtin.os.tag) {
- .macos => *macos.video.DisplayLink,
- else => void,
-};
+pub const custom_shader_target: shadertoy.Target = .msl;
+// The fragCoord for Metal shaders is +Y = down.
+pub const custom_shader_y_is_down = true;
+
+/// Triple buffering.
+pub const swap_chain_count = 3;
+
+const log = std.log.scoped(.metal);
// Get native API access on certain platforms so we can do more customization.
const glfwNative = glfw.Native(.{
.cocoa = builtin.os.tag == .macos,
});
-const log = std.log.scoped(.metal);
+layer: IOSurfaceLayer,
-/// Allocator that can be used
-alloc: std.mem.Allocator,
+/// MTLDevice
+device: objc.Object,
+/// MTLCommandQueue
+queue: objc.Object,
-/// The configuration we need derived from the main config.
-config: DerivedConfig,
+/// Alpha blending mode
+blending: configpkg.Config.AlphaBlending,
-/// The mailbox for communicating with the window.
-surface_mailbox: apprt.surface.Mailbox,
-
-/// Current font metrics defining our grid.
-grid_metrics: font.Metrics,
-
-/// The size of everything.
-size: renderer.Size,
-
-/// True if the window is focused
-focused: bool,
-
-/// The foreground color set by an OSC 10 sequence. If unset then
-/// default_foreground_color is used.
-foreground_color: ?terminal.color.RGB,
-
-/// Foreground color set in the user's config file.
-default_foreground_color: terminal.color.RGB,
-
-/// The background color set by an OSC 11 sequence. If unset then
-/// default_background_color is used.
-background_color: ?terminal.color.RGB,
-
-/// Background color set in the user's config file.
-default_background_color: terminal.color.RGB,
-
-/// The cursor color set by an OSC 12 sequence. If unset then
-/// default_cursor_color is used.
-cursor_color: ?terminal.color.RGB,
-
-/// Default cursor color when no color is set explicitly by an OSC 12 command.
-/// This is cursor color as set in the user's config, if any. If no cursor color
-/// is set in the user's config, then the cursor color is determined by the
-/// current foreground color.
-default_cursor_color: ?terminal.color.RGB,
-
-/// When `cursor_color` is null, swap the foreground and background colors of
-/// the cell under the cursor for the cursor color. Otherwise, use the default
-/// foreground color as the cursor color.
-cursor_invert: bool,
-
-/// The current set of cells to render. This is rebuilt on every frame
-/// but we keep this around so that we don't reallocate. Each set of
-/// cells goes into a separate shader.
-cells: mtl_cell.Contents,
-
-/// The last viewport that we based our rebuild off of. If this changes,
-/// then we do a full rebuild of the cells. The pointer values in this pin
-/// are NOT SAFE to read because they may be modified, freed, etc from the
-/// termio thread. We treat the pointers as integers for comparison only.
-cells_viewport: ?terminal.Pin = null,
-
-/// Set to true after rebuildCells is called. This can be used
-/// to determine if any possible changes have been made to the
-/// cells for the draw call.
-cells_rebuilt: bool = false,
-
-/// The current GPU uniform values.
-uniforms: mtl_shaders.Uniforms,
-
-/// The font structures.
-font_grid: *font.SharedGrid,
-font_shaper: font.Shaper,
-font_shaper_cache: font.ShaperCache,
-
-/// The images that we may render.
-images: ImageMap = .{},
-image_placements: ImagePlacementList = .{},
-image_bg_end: u32 = 0,
-image_text_end: u32 = 0,
-image_virtual: bool = false,
-
-/// Metal state
-shaders: Shaders, // Compiled shaders
-
-/// Metal objects
-layer: objc.Object, // CAMetalLayer
-
-/// The CVDisplayLink used to drive the rendering loop in sync
-/// with the display. This is void on platforms that don't support
-/// a display link.
-display_link: ?DisplayLink = null,
-
-/// The `CGColorSpace` that represents our current terminal color space
-terminal_colorspace: *graphics.ColorSpace,
-
-/// Custom shader state. This is only set if we have custom shaders.
-custom_shader_state: ?CustomShaderState = null,
-
-/// Health of the last frame. Note that when we do double/triple buffering
-/// this will have to be part of the frame state.
-health: std.atomic.Value(Health) = .{ .raw = .healthy },
-
-/// Our GPU state
-gpu_state: GPUState,
-
-/// State we need for the GPU that is shared between all frames.
-pub const GPUState = struct {
- // The count of buffers we use for double/triple buffering. If
- // this is one then we don't do any double+ buffering at all. This
- // is comptime because there isn't a good reason to change this at
- // runtime and there is a lot of complexity to support it. For comptime,
- // this is useful for debugging.
- const BufferCount = 3;
-
- /// The frame data, the current frame index, and the semaphore protecting
- /// the frame data. This is used to implement double/triple/etc. buffering.
- frames: [BufferCount]FrameState,
- frame_index: std.math.IntFittingRange(0, BufferCount) = 0,
- frame_sema: std.Thread.Semaphore = .{ .permits = BufferCount },
-
- device: objc.Object, // MTLDevice
- queue: objc.Object, // MTLCommandQueue
-
- /// This buffer is written exactly once so we can use it globally.
- instance: InstanceBuffer, // MTLBuffer
-
- /// The default storage mode to use for resources created with our device.
- ///
- /// This is based on whether the device is a discrete GPU or not, since
- /// discrete GPUs do not have unified memory and therefore do not support
- /// the "shared" storage mode, instead we have to use the "managed" mode.
- default_storage_mode: mtl.MTLResourceOptions.StorageMode,
-
- pub fn init() !GPUState {
- const device = try chooseDevice();
- const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{});
- errdefer queue.release();
-
- // We determine whether our device is a discrete GPU based on these:
- // - We're on macOS (iOS, iPadOS, etc. are guaranteed to be integrated).
- // - We're not on aarch64 (Apple Silicon, therefore integrated).
- // - The device reports that it does not have unified memory.
- const is_discrete =
- builtin.target.os.tag == .macos and
- builtin.target.cpu.arch != .aarch64 and
- !device.getProperty(bool, "hasUnifiedMemory");
-
- const default_storage_mode: mtl.MTLResourceOptions.StorageMode =
- if (is_discrete) .managed else .shared;
-
- var instance = try InstanceBuffer.initFill(device, &.{
- 0, 1, 3, // Top-left triangle
- 1, 2, 3, // Bottom-right triangle
- }, .{ .storage_mode = default_storage_mode });
- errdefer instance.deinit();
-
- var result: GPUState = .{
- .device = device,
- .queue = queue,
- .instance = instance,
- .frames = undefined,
- .default_storage_mode = default_storage_mode,
- };
-
- // Initialize all of our frame state.
- for (&result.frames) |*frame| {
- frame.* = try FrameState.init(result.device, default_storage_mode);
- }
-
- return result;
- }
-
- fn chooseDevice() error{NoMetalDevice}!objc.Object {
- var chosen_device: ?objc.Object = null;
-
- switch (comptime builtin.os.tag) {
- .macos => {
- const devices = objc.Object.fromId(mtl.MTLCopyAllDevices());
- defer devices.release();
-
- var iter = devices.iterate();
- while (iter.next()) |device| {
- // We want a GPU that’s connected to a display.
- if (device.getProperty(bool, "isHeadless")) continue;
- chosen_device = device;
- // If the user has an eGPU plugged in, they probably want
- // to use it. Otherwise, integrated GPUs are better for
- // battery life and thermals.
- if (device.getProperty(bool, "isRemovable") or
- device.getProperty(bool, "isLowPower")) break;
- }
- },
- .ios => {
- chosen_device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice());
- },
- else => @compileError("unsupported target for Metal"),
- }
-
- const device = chosen_device orelse return error.NoMetalDevice;
- return device.retain();
- }
-
- pub fn deinit(self: *GPUState) void {
- // Wait for all of our inflight draws to complete so that
- // we can cleanly deinit our GPU state.
- for (0..BufferCount) |_| self.frame_sema.wait();
- for (&self.frames) |*frame| frame.deinit();
- self.instance.deinit();
- self.queue.release();
- self.device.release();
- }
-
- /// Get the next frame state to draw to. This will wait on the
- /// semaphore to ensure that the frame is available. This must
- /// always be paired with a call to releaseFrame.
- pub fn nextFrame(self: *GPUState) *FrameState {
- self.frame_sema.wait();
- errdefer self.frame_sema.post();
- self.frame_index = (self.frame_index + 1) % BufferCount;
- return &self.frames[self.frame_index];
- }
-
- /// This should be called when the frame has completed drawing.
- pub fn releaseFrame(self: *GPUState) void {
- self.frame_sema.post();
- }
-};
-
-/// State we need duplicated for every frame. Any state that could be
-/// in a data race between the GPU and CPU while a frame is being
-/// drawn should be in this struct.
+/// The default storage mode to use for resources created with our device.
///
-/// While a draw is in-process, we "lock" the state (via a semaphore)
-/// and prevent the CPU from updating the state until Metal reports
-/// that the frame is complete.
-///
-/// This is used to implement double/triple buffering.
-pub const FrameState = struct {
- uniforms: UniformBuffer,
- cells: CellTextBuffer,
- cells_bg: CellBgBuffer,
+/// This is based on whether the device is a discrete GPU or not, since
+/// discrete GPUs do not have unified memory and therefore do not support
+/// the "shared" storage mode, instead we have to use the "managed" mode.
+default_storage_mode: mtl.MTLResourceOptions.StorageMode,
- grayscale: objc.Object, // MTLTexture
- grayscale_modified: usize = 0,
- color: objc.Object, // MTLTexture
- color_modified: usize = 0,
+/// We start an AutoreleasePool before `drawFrame` and end it afterwards.
+autorelease_pool: ?*objc.AutoreleasePool = null,
- /// A buffer containing the uniform data.
- const UniformBuffer = mtl_buffer.Buffer(mtl_shaders.Uniforms);
- const CellBgBuffer = mtl_buffer.Buffer(mtl_shaders.CellBg);
- const CellTextBuffer = mtl_buffer.Buffer(mtl_shaders.CellText);
-
- pub fn init(
- device: objc.Object,
- /// Storage mode for buffers and textures.
- storage_mode: mtl.MTLResourceOptions.StorageMode,
- ) !FrameState {
- // Uniform buffer contains exactly 1 uniform struct. The
- // uniform data will be undefined so this must be set before
- // a frame is drawn.
- var uniforms = try UniformBuffer.init(
- device,
- 1,
- .{
- // Indicate that the CPU writes to this resource but never reads it.
- .cpu_cache_mode = .write_combined,
- .storage_mode = storage_mode,
- },
- );
- errdefer uniforms.deinit();
-
- // Create the buffers for our vertex data. The preallocation size
- // is likely too small but our first frame update will resize it.
- var cells = try CellTextBuffer.init(
- device,
- 10 * 10,
- .{
- // Indicate that the CPU writes to this resource but never reads it.
- .cpu_cache_mode = .write_combined,
- .storage_mode = storage_mode,
- },
- );
- errdefer cells.deinit();
- var cells_bg = try CellBgBuffer.init(
- device,
- 10 * 10,
- .{
- // Indicate that the CPU writes to this resource but never reads it.
- .cpu_cache_mode = .write_combined,
- .storage_mode = storage_mode,
- },
- );
-
- errdefer cells_bg.deinit();
-
- // Initialize our textures for our font atlas.
- const grayscale = try initAtlasTexture(device, &.{
- .data = undefined,
- .size = 8,
- .format = .grayscale,
- }, storage_mode);
- errdefer grayscale.release();
- const color = try initAtlasTexture(device, &.{
- .data = undefined,
- .size = 8,
- .format = .rgba,
- }, storage_mode);
- errdefer color.release();
-
- return .{
- .uniforms = uniforms,
- .cells = cells,
- .cells_bg = cells_bg,
- .grayscale = grayscale,
- .color = color,
- };
- }
-
- pub fn deinit(self: *FrameState) void {
- self.uniforms.deinit();
- self.cells.deinit();
- self.cells_bg.deinit();
- self.grayscale.release();
- self.color.release();
- }
-};
-
-pub const CustomShaderState = struct {
- /// When we have a custom shader state, we maintain a front
- /// and back texture which we use as a swap chain to render
- /// between when multiple custom shaders are defined.
- front_texture: objc.Object, // MTLTexture
- back_texture: objc.Object, // MTLTexture
-
- sampler: mtl_sampler.Sampler,
- uniforms: mtl_shaders.PostUniforms,
-
- /// The first time a frame was drawn.
- /// This is used to update the time uniform.
- first_frame_time: std.time.Instant,
-
- /// The last time a frame was drawn.
- /// This is used to update the time uniform.
- last_frame_time: std.time.Instant,
-
- /// Swap the front and back textures.
- pub fn swap(self: *CustomShaderState) void {
- std.mem.swap(objc.Object, &self.front_texture, &self.back_texture);
- }
-
- pub fn deinit(self: *CustomShaderState) void {
- self.front_texture.release();
- self.back_texture.release();
- self.sampler.deinit();
- }
-};
-
-/// The configuration for this renderer that is derived from the main
-/// configuration. This must be exported so that we don't need to
-/// pass around Config pointers which makes memory management a pain.
-pub const DerivedConfig = struct {
- arena: ArenaAllocator,
-
- font_thicken: bool,
- font_thicken_strength: u8,
- font_features: std.ArrayListUnmanaged([:0]const u8),
- font_styles: font.CodepointResolver.StyleStatus,
- cursor_color: ?terminal.color.RGB,
- cursor_invert: bool,
- cursor_opacity: f64,
- cursor_text: ?terminal.color.RGB,
- background: terminal.color.RGB,
- background_opacity: f64,
- foreground: terminal.color.RGB,
- selection_background: ?terminal.color.RGB,
- selection_foreground: ?terminal.color.RGB,
- invert_selection_fg_bg: bool,
- bold_is_bright: bool,
- min_contrast: f32,
- padding_color: configpkg.WindowPaddingColor,
- custom_shaders: configpkg.RepeatablePath,
- links: link.Set,
- vsync: bool,
- colorspace: configpkg.Config.WindowColorspace,
- blending: configpkg.Config.AlphaBlending,
-
- pub fn init(
- alloc_gpa: Allocator,
- config: *const configpkg.Config,
- ) !DerivedConfig {
- var arena = ArenaAllocator.init(alloc_gpa);
- errdefer arena.deinit();
- const alloc = arena.allocator();
-
- // Copy our shaders
- const custom_shaders = try config.@"custom-shader".clone(alloc);
-
- // Copy our font features
- const font_features = try config.@"font-feature".clone(alloc);
-
- // Get our font styles
- var font_styles = font.CodepointResolver.StyleStatus.initFill(true);
- font_styles.set(.bold, config.@"font-style-bold" != .false);
- font_styles.set(.italic, config.@"font-style-italic" != .false);
- font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
-
- // Our link configs
- const links = try link.Set.fromConfig(
- alloc,
- config.link.links.items,
- );
-
- const cursor_invert = config.@"cursor-invert-fg-bg";
-
- return .{
- .background_opacity = @max(0, @min(1, config.@"background-opacity")),
- .font_thicken = config.@"font-thicken",
- .font_thicken_strength = config.@"font-thicken-strength",
- .font_features = font_features.list,
- .font_styles = font_styles,
-
- .cursor_color = if (!cursor_invert and config.@"cursor-color" != null)
- config.@"cursor-color".?.toTerminalRGB()
- else
- null,
-
- .cursor_invert = cursor_invert,
-
- .cursor_text = if (config.@"cursor-text") |txt|
- txt.toTerminalRGB()
- else
- null,
-
- .cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")),
-
- .background = config.background.toTerminalRGB(),
- .foreground = config.foreground.toTerminalRGB(),
- .invert_selection_fg_bg = config.@"selection-invert-fg-bg",
- .bold_is_bright = config.@"bold-is-bright",
- .min_contrast = @floatCast(config.@"minimum-contrast"),
- .padding_color = config.@"window-padding-color",
-
- .selection_background = if (config.@"selection-background") |bg|
- bg.toTerminalRGB()
- else
- null,
-
- .selection_foreground = if (config.@"selection-foreground") |bg|
- bg.toTerminalRGB()
- else
- null,
-
- .custom_shaders = custom_shaders,
- .links = links,
- .vsync = config.@"window-vsync",
- .colorspace = config.@"window-colorspace",
- .blending = config.@"alpha-blending",
- .arena = arena,
- };
- }
-
- pub fn deinit(self: *DerivedConfig) void {
- const alloc = self.arena.allocator();
- self.links.deinit(alloc);
- self.arena.deinit();
- }
-};
-
-/// Returns the hints that we want for this
-pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints {
- return .{
- .client_api = .no_api,
- .transparent_framebuffer = config.@"background-opacity" < 1,
+pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal {
+ comptime switch (builtin.os.tag) {
+ .macos, .ios => {},
+ else => @compileError("unsupported platform for Metal"),
};
-}
-/// This is called early right after window creation to setup our
-/// window surface as necessary.
-pub fn surfaceInit(surface: *apprt.Surface) !void {
- _ = surface;
+ _ = alloc;
- // We don't do anything else here because we want to set everything
- // else up during actual initialization.
-}
+ // Choose our MTLDevice and create a MTLCommandQueue for that device.
+ const device = try chooseDevice();
+ errdefer device.release();
+ const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{});
+ errdefer queue.release();
+
+ const default_storage_mode: mtl.MTLResourceOptions.StorageMode =
+ if (device.getProperty(bool, "hasUnifiedMemory")) .shared else .managed;
-pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
const ViewInfo = struct {
view: objc.Object,
scaleFactor: f64,
@@ -553,7 +94,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
// Everything in glfw is window-oriented so we grab the backing
// window, then derive everything from that.
const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(
- options.rt_surface.window,
+ opts.rt_surface.window,
).?);
const contentView = objc.Object.fromId(
@@ -571,8 +112,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
},
apprt.embedded => .{
- .scaleFactor = @floatCast(options.rt_surface.content_scale.x),
- .view = switch (options.rt_surface.platform) {
+ .scaleFactor = @floatCast(opts.rt_surface.content_scale.x),
+ .view = switch (opts.rt_surface.platform) {
.macos => |v| v.nsview,
.ios => |v| v.uiview,
},
@@ -581,2768 +122,254 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
else => @compileError("unsupported apprt for metal"),
};
- // Initialize our metal stuff
- var gpu_state = try GPUState.init();
- errdefer gpu_state.deinit();
+ // Create an IOSurfaceLayer which we can assign to the view to make
+ // it in to a "layer-hosting view", so that we can manually control
+ // the layer contents.
+ var layer = try IOSurfaceLayer.init();
+ errdefer layer.release();
- // Get our CAMetalLayer
- const layer: objc.Object = switch (builtin.os.tag) {
- .macos => layer: {
- const CAMetalLayer = objc.getClass("CAMetalLayer").?;
- break :layer CAMetalLayer.msgSend(objc.Object, objc.sel("layer"), .{});
+ // Add our layer to the view.
+ //
+ // On macOS we do this by making the view "layer-hosting"
+ // by assigning it to the view's `layer` property BEFORE
+ // setting `wantsLayer` to `true`.
+ //
+ // On iOS, views are always layer-backed, and `layer`
+ // is readonly, so instead we add it as a sublayer.
+ switch (comptime builtin.os.tag) {
+ .macos => {
+ info.view.setProperty("layer", layer.layer.value);
+ info.view.setProperty("wantsLayer", true);
},
- // iOS is always layer-backed so we don't need to do anything here.
- .ios => info.view.getProperty(objc.Object, "layer"),
+ .ios => {
+ info.view.msgSend(void, objc.sel("addSublayer"), .{layer.layer.value});
+ },
else => @compileError("unsupported target for Metal"),
- };
- layer.setProperty("device", gpu_state.device.value);
- layer.setProperty("opaque", options.config.background_opacity >= 1);
- layer.setProperty("displaySyncEnabled", options.config.vsync);
-
- // Set our layer's pixel format appropriately.
- layer.setProperty(
- "pixelFormat",
- // Using an `*_srgb` pixel format makes Metal gamma encode
- // the pixels written to it *after* blending, which means
- // we get linear alpha blending rather than gamma-incorrect
- // blending.
- if (options.config.blending.isLinear())
- @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
- else
- @intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
- );
-
- // Set our layer's color space to Display P3.
- // This allows us to have "Apple-style" alpha blending,
- // since it seems to be the case that Apple apps like
- // Terminal and TextEdit render text in the display's
- // color space using converted colors, which reduces,
- // but does not fully eliminate blending artifacts.
- const colorspace = try graphics.ColorSpace.createNamed(.displayP3);
- defer colorspace.release();
- layer.setProperty("colorspace", colorspace);
-
- // Create a colorspace the represents our terminal colors
- // this will allow us to create e.g. `CGColor`s for things
- // like the current background color.
- const terminal_colorspace = try graphics.ColorSpace.createNamed(
- switch (options.config.colorspace) {
- .@"display-p3" => .displayP3,
- .srgb => .sRGB,
- },
- );
- errdefer terminal_colorspace.release();
-
- // Make our view layer-backed with our Metal layer. On iOS views are
- // always layer backed so we don't need to do this. But on iOS the
- // caller MUST be sure to set the layerClass to CAMetalLayer.
- if (comptime builtin.os.tag == .macos) {
- info.view.setProperty("layer", layer.value);
- info.view.setProperty("wantsLayer", true);
-
- // The layer gravity is set to top-left so that when we resize
- // the view, the contents aren't stretched before a redraw.
- layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft);
}
- // Ensure that our metal layer has a content scale set to match the
- // scale factor of the window. This avoids magnification issues leading
- // to blurry rendering.
- layer.setProperty("contentsScale", info.scaleFactor);
+ // Ensure that if our layer is oversized it
+ // does not overflow the bounds of the view.
+ info.view.setProperty("clipsToBounds", true);
- // Create the font shaper. We initially create a shaper that can support
- // a width of 160 which is a common width for modern screens to help
- // avoid allocations later.
- var font_shaper = try font.Shaper.init(alloc, .{
- .features = options.config.font_features.items,
- });
- errdefer font_shaper.deinit();
+ // Ensure that our layer has a content scale set to
+ // match the scale factor of the window. This avoids
+ // magnification issues leading to blurry rendering.
+ layer.layer.setProperty("contentsScale", info.scaleFactor);
- // Initialize all the data that requires a critical font section.
- const font_critical: struct {
- metrics: font.Metrics,
- } = font_critical: {
- const grid = options.font_grid;
- grid.lock.lockShared();
- defer grid.lock.unlockShared();
- break :font_critical .{
- .metrics = grid.metrics,
- };
- };
+ // This makes it so that our display callback will actually be called.
+ layer.layer.setProperty("needsDisplayOnBoundsChange", true);
- const display_link: ?DisplayLink = switch (builtin.os.tag) {
- .macos => if (options.config.vsync)
- try macos.video.DisplayLink.createWithActiveCGDisplays()
- else
- null,
- else => null,
- };
- errdefer if (display_link) |v| v.release();
-
- var result: Metal = .{
- .alloc = alloc,
- .config = options.config,
- .surface_mailbox = options.surface_mailbox,
- .grid_metrics = font_critical.metrics,
- .size = options.size,
- .focused = true,
- .foreground_color = null,
- .default_foreground_color = options.config.foreground,
- .background_color = null,
- .default_background_color = options.config.background,
- .cursor_color = null,
- .default_cursor_color = options.config.cursor_color,
- .cursor_invert = options.config.cursor_invert,
-
- // Render state
- .cells = .{},
- .uniforms = .{
- .projection_matrix = undefined,
- .cell_size = undefined,
- .grid_size = undefined,
- .grid_padding = undefined,
- .padding_extend = .{},
- .min_contrast = options.config.min_contrast,
- .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) },
- .cursor_color = undefined,
- .bg_color = .{
- options.config.background.r,
- options.config.background.g,
- options.config.background.b,
- @intFromFloat(@round(options.config.background_opacity * 255.0)),
- },
- .cursor_wide = false,
- .use_display_p3 = options.config.colorspace == .@"display-p3",
- .use_linear_blending = options.config.blending.isLinear(),
- .use_linear_correction = options.config.blending == .@"linear-corrected",
- },
-
- // Fonts
- .font_grid = options.font_grid,
- .font_shaper = font_shaper,
- .font_shaper_cache = font.ShaperCache.init(),
-
- // Shaders (initialized below)
- .shaders = undefined,
-
- // Metal stuff
+ return .{
.layer = layer,
- .display_link = display_link,
- .terminal_colorspace = terminal_colorspace,
- .custom_shader_state = null,
- .gpu_state = gpu_state,
+ .device = device,
+ .queue = queue,
+ .blending = opts.config.blending,
+ .default_storage_mode = default_storage_mode,
};
-
- try result.initShaders();
-
- // Do an initialize screen size setup to ensure our undefined values
- // above are initialized.
- try result.setScreenSize(result.size);
-
- return result;
}
pub fn deinit(self: *Metal) void {
- self.gpu_state.deinit();
-
- if (DisplayLink != void) {
- if (self.display_link) |display_link| {
- display_link.stop() catch {};
- display_link.release();
- }
- }
-
- self.terminal_colorspace.release();
-
- self.cells.deinit(self.alloc);
-
- self.font_shaper.deinit();
- self.font_shaper_cache.deinit(self.alloc);
-
- self.config.deinit();
-
- {
- var it = self.images.iterator();
- while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc);
- self.images.deinit(self.alloc);
- }
- self.image_placements.deinit(self.alloc);
-
- self.deinitShaders();
-
- self.* = undefined;
+ self.queue.release();
+ self.device.release();
+ self.layer.release();
}
-fn deinitShaders(self: *Metal) void {
- if (self.custom_shader_state) |*state| state.deinit();
-
- self.shaders.deinit(self.alloc);
+pub fn loopEnter(self: *Metal) void {
+ const renderer: *align(1) Renderer = @fieldParentPtr("api", self);
+ self.layer.setDisplayCallback(
+ @ptrCast(&displayCallback),
+ @ptrCast(renderer),
+ );
}
-fn initShaders(self: *Metal) !void {
- var arena = ArenaAllocator.init(self.alloc);
- defer arena.deinit();
- const arena_alloc = arena.allocator();
-
- // Load our custom shaders
- const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles(
- arena_alloc,
- self.config.custom_shaders,
- .msl,
- ) catch |err| err: {
- log.warn("error loading custom shaders err={}", .{err});
- break :err &.{};
+fn displayCallback(renderer: *Renderer) align(8) void {
+ renderer.drawFrame(true) catch |err| {
+ log.warn("Error drawing frame in display callback, err={}", .{err});
};
+}
- var custom_shader_state: ?CustomShaderState = state: {
- if (custom_shaders.len == 0) break :state null;
+/// Actions taken before doing anything in `drawFrame`.
+///
+/// Right now we use this to start an AutoreleasePool.
+pub fn drawFrameStart(self: *Metal) void {
+ assert(self.autorelease_pool == null);
+ self.autorelease_pool = .init();
+}
- // Build our sampler for our texture
- var sampler = try mtl_sampler.Sampler.init(self.gpu_state.device);
- errdefer sampler.deinit();
+/// Actions taken after `drawFrame` is done.
+///
+/// Right now we use this to end our AutoreleasePool.
+pub fn drawFrameEnd(self: *Metal) void {
+ assert(self.autorelease_pool != null);
+ self.autorelease_pool.?.deinit();
+ self.autorelease_pool = null;
+}
- break :state .{
- // Resolution and screen textures will be fixed up by first
- // call to setScreenSize. Draw calls will bail out early if
- // the screen size hasn't been set yet, so it won't error.
- .front_texture = undefined,
- .back_texture = undefined,
- .sampler = sampler,
- .uniforms = .{
- .resolution = .{ 0, 0, 1 },
- .time = 1,
- .time_delta = 1,
- .frame_rate = 1,
- .frame = 1,
- .channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
- .channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
- .mouse = .{ 0, 0, 0, 0 },
- .date = .{ 0, 0, 0, 0 },
- .sample_rate = 1,
- },
-
- .first_frame_time = try std.time.Instant.now(),
- .last_frame_time = try std.time.Instant.now(),
- };
- };
- errdefer if (custom_shader_state) |*state| state.deinit();
-
- var shaders = try Shaders.init(
- self.alloc,
- self.gpu_state.device,
+pub fn initShaders(
+ self: *const Metal,
+ alloc: Allocator,
+ custom_shaders: []const [:0]const u8,
+) !shaders.Shaders {
+ return try shaders.Shaders.init(
+ alloc,
+ self.device,
custom_shaders,
// Using an `*_srgb` pixel format makes Metal gamma encode
// the pixels written to it *after* blending, which means
// we get linear alpha blending rather than gamma-incorrect
// blending.
- if (self.config.blending.isLinear())
+ if (self.blending.isLinear())
mtl.MTLPixelFormat.bgra8unorm_srgb
else
mtl.MTLPixelFormat.bgra8unorm,
);
- errdefer shaders.deinit(self.alloc);
-
- self.shaders = shaders;
- self.custom_shader_state = custom_shader_state;
}
-/// This is called just prior to spinning up the renderer thread for
-/// final main thread setup requirements.
-pub fn finalizeSurfaceInit(self: *Metal, surface: *apprt.Surface) !void {
- _ = self;
- _ = surface;
-
- // Metal doesn't have to do anything here. OpenGL has to do things
- // like release the context but Metal doesn't have anything like that.
-}
-
-/// Callback called by renderer.Thread when it begins.
-pub fn threadEnter(self: *const Metal, surface: *apprt.Surface) !void {
- _ = self;
- _ = surface;
-
- // Metal requires no per-thread state.
-}
-
-/// Callback called by renderer.Thread when it exits.
-pub fn threadExit(self: *const Metal) void {
- _ = self;
-
- // Metal requires no per-thread state.
-}
-
-/// Called by renderer.Thread when it starts the main loop.
-pub fn loopEnter(self: *Metal, thr: *renderer.Thread) !void {
- // If we don't support a display link we have no work to do.
- if (comptime DisplayLink == void) return;
-
- // This is when we know our "self" pointer is stable so we can
- // setup the display link. To setup the display link we set our
- // callback and we can start it immediately.
- const display_link = self.display_link orelse return;
- try display_link.setOutputCallback(
- xev.Async,
- &displayLinkCallback,
- &thr.draw_now,
- );
- display_link.start() catch {};
-}
-
-/// Called by renderer.Thread when it exits the main loop.
-pub fn loopExit(self: *Metal) void {
- // If we don't support a display link we have no work to do.
- if (comptime DisplayLink == void) return;
-
- // Stop our display link. If this fails its okay it just means
- // that we either never started it or the view its attached to
- // is gone which is fine.
- const display_link = self.display_link orelse return;
- display_link.stop() catch {};
-}
-
-fn displayLinkCallback(
- _: *macos.video.DisplayLink,
- ud: ?*xev.Async,
-) void {
- const draw_now = ud orelse return;
- draw_now.notify() catch |err| {
- log.err("error notifying draw_now err={}", .{err});
+/// Get the current size of the runtime surface.
+pub fn surfaceSize(self: *const Metal) !struct { width: u32, height: u32 } {
+ const bounds = self.layer.layer.getProperty(graphics.Rect, "bounds");
+ const scale = self.layer.layer.getProperty(f64, "contentsScale");
+ return .{
+ .width = @intFromFloat(bounds.size.width * scale),
+ .height = @intFromFloat(bounds.size.height * scale),
};
}
-/// Mark the full screen as dirty so that we redraw everything.
-pub fn markDirty(self: *Metal) void {
- // This is how we force a full rebuild with metal.
- self.cells_viewport = null;
-}
-
-/// Called when we get an updated display ID for our display link.
-pub fn setMacOSDisplayID(self: *Metal, id: u32) !void {
- if (comptime DisplayLink == void) return;
- const display_link = self.display_link orelse return;
- log.info("updating display link display id={}", .{id});
- display_link.setCurrentCGDisplay(id) catch |err| {
- log.warn("error setting display link display id err={}", .{err});
- };
-}
-
-/// True if our renderer has animations so that a higher frequency
-/// timer is used.
-pub fn hasAnimations(self: *const Metal) bool {
- return self.custom_shader_state != null;
-}
-
-/// True if our renderer is using vsync. If true, the renderer or apprt
-/// is responsible for triggering draw_now calls to the render thread. That
-/// is the only way to trigger a drawFrame.
-pub fn hasVsync(self: *const Metal) bool {
- if (comptime DisplayLink == void) return false;
- const display_link = self.display_link orelse return false;
- return display_link.isRunning();
-}
-
-/// Callback when the focus changes for the terminal this is rendering.
-///
-/// Must be called on the render thread.
-pub fn setFocus(self: *Metal, focus: bool) !void {
- self.focused = focus;
-
- // If we're not focused, then we want to stop the display link
- // because it is a waste of resources and we can move to pure
- // change-driven updates.
- if (comptime DisplayLink != void) link: {
- const display_link = self.display_link orelse break :link;
- if (focus) {
- display_link.start() catch {};
- } else {
- display_link.stop() catch {};
- }
- }
-}
-
-/// Callback when the window is visible or occluded.
-///
-/// Must be called on the render thread.
-pub fn setVisible(self: *Metal, visible: bool) void {
- // If we're not visible, then we want to stop the display link
- // because it is a waste of resources and we can move to pure
- // change-driven updates.
- if (comptime DisplayLink != void) link: {
- const display_link = self.display_link orelse break :link;
- if (visible and self.focused) {
- display_link.start() catch {};
- } else {
- display_link.stop() catch {};
- }
- }
-}
-
-/// Set the new font grid.
-///
-/// Must be called on the render thread.
-pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) void {
- // Update our grid
- self.font_grid = grid;
-
- // Update all our textures so that they sync on the next frame.
- // We can modify this without a lock because the GPU does not
- // touch this data.
- for (&self.gpu_state.frames) |*frame| {
- frame.grayscale_modified = 0;
- frame.color_modified = 0;
- }
-
- // Get our metrics from the grid. This doesn't require a lock because
- // the metrics are never recalculated.
- const metrics = grid.metrics;
- self.grid_metrics = metrics;
-
- // Reset our shaper cache. If our font changed (not just the size) then
- // the data in the shaper cache may be invalid and cannot be used, so we
- // always clear the cache just in case.
- const font_shaper_cache = font.ShaperCache.init();
- self.font_shaper_cache.deinit(self.alloc);
- self.font_shaper_cache = font_shaper_cache;
-
- // Run a screen size update since this handles a lot of our uniforms
- // that are grid size dependent and changing the font grid can change
- // the grid size.
- //
- // If the screen size isn't set, it will be eventually so that'll call
- // the setScreenSize automatically.
- self.setScreenSize(self.size) catch |err| {
- // The setFontGrid function can't fail but resizing our cell
- // buffer definitely can fail. If it does, our renderer is probably
- // screwed but let's just log it and continue until we can figure
- // out a better way to handle this.
- log.err("error resizing cells buffer err={}", .{err});
- };
-
- // Reset our viewport to force a rebuild, since `setScreenSize` only
- // does this when the number of cells changes, which isn't guaranteed.
- self.cells_viewport = null;
-}
-
-/// Update the frame data.
-pub fn updateFrame(
- self: *Metal,
- surface: *apprt.Surface,
- state: *renderer.State,
- cursor_blink_visible: bool,
-) !void {
- _ = surface;
-
- // Data we extract out of the critical area.
- const Critical = struct {
- bg: terminal.color.RGB,
- screen: terminal.Screen,
- screen_type: terminal.ScreenType,
- mouse: renderer.State.Mouse,
- preedit: ?renderer.State.Preedit,
- cursor_style: ?renderer.CursorStyle,
- color_palette: terminal.color.Palette,
- viewport_pin: terminal.Pin,
-
- /// If true, rebuild the full screen.
- full_rebuild: bool,
- };
-
- // Update all our data as tightly as possible within the mutex.
- var critical: Critical = critical: {
- // const start = try std.time.Instant.now();
- // const start_micro = std.time.microTimestamp();
- // defer {
- // const end = std.time.Instant.now() catch unreachable;
- // // "[updateFrame critical time] \t"
- // std.log.err("[updateFrame critical time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us});
- // }
-
- state.mutex.lock();
- defer state.mutex.unlock();
-
- // If we're in a synchronized output state, we pause all rendering.
- if (state.terminal.modes.get(.synchronized_output)) {
- log.debug("synchronized output started, skipping render", .{});
- return;
- }
-
- // Swap bg/fg if the terminal is reversed
- const bg = self.background_color orelse self.default_background_color;
- const fg = self.foreground_color orelse self.default_foreground_color;
- defer {
- if (self.background_color) |*c| {
- c.* = bg;
- } else {
- self.default_background_color = bg;
- }
-
- if (self.foreground_color) |*c| {
- c.* = fg;
- } else {
- self.default_foreground_color = fg;
- }
- }
-
- if (state.terminal.modes.get(.reverse_colors)) {
- if (self.background_color) |*c| {
- c.* = fg;
- } else {
- self.default_background_color = fg;
- }
-
- if (self.foreground_color) |*c| {
- c.* = bg;
- } else {
- self.default_foreground_color = bg;
- }
- }
-
- // Get the viewport pin so that we can compare it to the current.
- const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
-
- // We used to share terminal state, but we've since learned through
- // analysis that it is faster to copy the terminal state than to
- // hold the lock while rebuilding GPU cells.
- var screen_copy = try state.terminal.screen.clone(
- self.alloc,
- .{ .viewport = .{} },
- null,
- );
- errdefer screen_copy.deinit();
-
- // Whether to draw our cursor or not.
- const cursor_style = if (state.terminal.flags.password_input)
- .lock
+/// Initialize a new render target which can be presented by this API.
+pub fn initTarget(self: *const Metal, width: usize, height: usize) !Target {
+ return Target.init(.{
+ .device = self.device,
+ // Using an `*_srgb` pixel format makes Metal gamma encode the pixels
+ // written to it *after* blending, which means we get linear alpha
+ // blending rather than gamma-incorrect blending.
+ .pixel_format = if (self.blending.isLinear())
+ .bgra8unorm_srgb
else
- renderer.cursorStyle(
- state,
- self.focused,
- cursor_blink_visible,
- );
-
- // Get our preedit state
- const preedit: ?renderer.State.Preedit = preedit: {
- if (cursor_style == null) break :preedit null;
- const p = state.preedit orelse break :preedit null;
- break :preedit try p.clone(self.alloc);
- };
- errdefer if (preedit) |p| p.deinit(self.alloc);
-
- // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path.
- // We only do this if the Kitty image state is dirty meaning only if
- // it changes.
- //
- // If we have any virtual references, we must also rebuild our
- // kitty state on every frame because any cell change can move
- // an image.
- if (state.terminal.screen.kitty_images.dirty or
- self.image_virtual)
- {
- try self.prepKittyGraphics(state.terminal);
- }
-
- // If we have any terminal dirty flags set then we need to rebuild
- // the entire screen. This can be optimized in the future.
- const full_rebuild: bool = rebuild: {
- {
- const Int = @typeInfo(terminal.Terminal.Dirty).@"struct".backing_integer.?;
- const v: Int = @bitCast(state.terminal.flags.dirty);
- if (v > 0) break :rebuild true;
- }
- {
- const Int = @typeInfo(terminal.Screen.Dirty).@"struct".backing_integer.?;
- const v: Int = @bitCast(state.terminal.screen.dirty);
- if (v > 0) break :rebuild true;
- }
-
- // If our viewport changed then we need to rebuild the entire
- // screen because it means we scrolled. If we have no previous
- // viewport then we must rebuild.
- const prev_viewport = self.cells_viewport orelse break :rebuild true;
- if (!prev_viewport.eql(viewport_pin)) break :rebuild true;
-
- break :rebuild false;
- };
-
- // Reset the dirty flags in the terminal and screen. We assume
- // that our rebuild will be successful since so we optimize for
- // success and reset while we hold the lock. This is much easier
- // than coordinating row by row or as changes are persisted.
- state.terminal.flags.dirty = .{};
- state.terminal.screen.dirty = .{};
- {
- var it = state.terminal.screen.pages.pageIterator(
- .right_down,
- .{ .screen = .{} },
- null,
- );
- while (it.next()) |chunk| {
- var dirty_set = chunk.node.data.dirtyBitSet();
- dirty_set.unsetAll();
- }
- }
-
- break :critical .{
- .bg = self.background_color orelse self.default_background_color,
- .screen = screen_copy,
- .screen_type = state.terminal.active_screen,
- .mouse = state.mouse,
- .preedit = preedit,
- .cursor_style = cursor_style,
- .color_palette = state.terminal.color_palette.colors,
- .viewport_pin = viewport_pin,
- .full_rebuild = full_rebuild,
- };
- };
- defer {
- critical.screen.deinit();
- if (critical.preedit) |p| p.deinit(self.alloc);
- }
-
- // Build our GPU cells
- try self.rebuildCells(
- critical.full_rebuild,
- &critical.screen,
- critical.screen_type,
- critical.mouse,
- critical.preedit,
- critical.cursor_style,
- &critical.color_palette,
- );
-
- // Notify our shaper we're done for the frame. For some shapers like
- // CoreText this triggers off-thread cleanup logic.
- self.font_shaper.endFrame();
-
- // Update our viewport pin
- self.cells_viewport = critical.viewport_pin;
-
- // Update our background color
- self.uniforms.bg_color = .{
- critical.bg.r,
- critical.bg.g,
- critical.bg.b,
- @intFromFloat(@round(self.config.background_opacity * 255.0)),
- };
-
- // Update the background color on our layer
- //
- // TODO: Is this expensive? Should we be checking if our
- // bg color has changed first before doing this work?
- {
- const color = graphics.c.CGColorCreate(
- @ptrCast(self.terminal_colorspace),
- &[4]f64{
- @as(f64, @floatFromInt(critical.bg.r)) / 255.0,
- @as(f64, @floatFromInt(critical.bg.g)) / 255.0,
- @as(f64, @floatFromInt(critical.bg.b)) / 255.0,
- self.config.background_opacity,
- },
- );
- defer graphics.c.CGColorRelease(color);
-
- // We use a CATransaction so that Core Animation knows that we
- // updated the background color property. Otherwise it behaves
- // weird, not updating the color until we resize.
- const CATransaction = objc.getClass("CATransaction").?;
- CATransaction.msgSend(void, "begin", .{});
- defer CATransaction.msgSend(void, "commit", .{});
-
- self.layer.setProperty("backgroundColor", color);
- }
-
- // Go through our images and see if we need to setup any textures.
- {
- var image_it = self.images.iterator();
- while (image_it.next()) |kv| {
- switch (kv.value_ptr.image) {
- .ready => {},
-
- .pending_gray,
- .pending_gray_alpha,
- .pending_rgb,
- .pending_rgba,
- .replace_gray,
- .replace_gray_alpha,
- .replace_rgb,
- .replace_rgba,
- => try kv.value_ptr.image.upload(
- self.alloc,
- self.gpu_state.device,
- self.gpu_state.default_storage_mode,
- ),
-
- .unload_pending,
- .unload_replace,
- .unload_ready,
- => {
- kv.value_ptr.image.deinit(self.alloc);
- self.images.removeByPtr(kv.key_ptr);
- },
- }
- }
- }
-}
-
-/// Draw the frame to the screen.
-pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
- _ = surface;
-
- // If we have no cells rebuilt we can usually skip drawing since there
- // is no changed data. However, if we have active animations we still
- // need to draw so that we can update the time uniform and render the
- // changes.
- if (!self.cells_rebuilt and !self.hasAnimations()) return;
- self.cells_rebuilt = false;
-
- // Wait for a frame to be available.
- const frame = self.gpu_state.nextFrame();
- errdefer self.gpu_state.releaseFrame();
- // log.debug("drawing frame index={}", .{self.gpu_state.frame_index});
-
- // Setup our frame data
- try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms});
- try frame.cells_bg.sync(self.gpu_state.device, self.cells.bg_cells);
- const fg_count = try frame.cells.syncFromArrayLists(self.gpu_state.device, self.cells.fg_rows.lists);
-
- // If we have custom shaders, update the animation time.
- if (self.custom_shader_state) |*state| {
- const now = std.time.Instant.now() catch state.first_frame_time;
- const since_ns: f32 = @floatFromInt(now.since(state.first_frame_time));
- const delta_ns: f32 = @floatFromInt(now.since(state.last_frame_time));
- state.uniforms.time = since_ns / std.time.ns_per_s;
- state.uniforms.time_delta = delta_ns / std.time.ns_per_s;
- state.last_frame_time = now;
- }
-
- // @autoreleasepool {}
- const pool = objc.AutoreleasePool.init();
- defer pool.deinit();
-
- // Get our drawable (CAMetalDrawable)
- const drawable = self.layer.msgSend(objc.Object, objc.sel("nextDrawable"), .{});
-
- // Get our screen texture. If we don't have a dedicated screen texture
- // then we just use the drawable texture.
- const screen_texture = if (self.custom_shader_state) |state|
- state.back_texture
- else tex: {
- const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{});
- break :tex objc.Object.fromId(texture);
- };
-
- // If our font atlas changed, sync the texture data
- texture: {
- const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
- if (modified <= frame.grayscale_modified) break :texture;
- self.font_grid.lock.lockShared();
- defer self.font_grid.lock.unlockShared();
- frame.grayscale_modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
- try syncAtlasTexture(
- self.gpu_state.device,
- &self.font_grid.atlas_grayscale,
- &frame.grayscale,
- self.gpu_state.default_storage_mode,
- );
- }
- texture: {
- const modified = self.font_grid.atlas_color.modified.load(.monotonic);
- if (modified <= frame.color_modified) break :texture;
- self.font_grid.lock.lockShared();
- defer self.font_grid.lock.unlockShared();
- frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic);
- try syncAtlasTexture(
- self.gpu_state.device,
- &self.font_grid.atlas_color,
- &frame.color,
- self.gpu_state.default_storage_mode,
- );
- }
-
- // Command buffer (MTLCommandBuffer)
- const buffer = self.gpu_state.queue.msgSend(objc.Object, objc.sel("commandBuffer"), .{});
-
- {
- // MTLRenderPassDescriptor
- const desc = desc: {
- const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?;
- const desc = MTLRenderPassDescriptor.msgSend(
- objc.Object,
- objc.sel("renderPassDescriptor"),
- .{},
- );
-
- // Set our color attachment to be our drawable surface.
- const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
- {
- const attachment = attachments.msgSend(
- objc.Object,
- objc.sel("objectAtIndexedSubscript:"),
- .{@as(c_ulong, 0)},
- );
-
- attachment.setProperty("loadAction", @intFromEnum(mtl.MTLLoadAction.clear));
- attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store));
- attachment.setProperty("texture", screen_texture.value);
- attachment.setProperty("clearColor", mtl.MTLClearColor{
- .red = 0.0,
- .green = 0.0,
- .blue = 0.0,
- .alpha = 0.0,
- });
- }
-
- break :desc desc;
- };
-
- // MTLRenderCommandEncoder
- const encoder = buffer.msgSend(
- objc.Object,
- objc.sel("renderCommandEncoderWithDescriptor:"),
- .{desc.value},
- );
- defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
-
- // Draw background images first
- try self.drawImagePlacements(encoder, frame, self.image_placements.items[0..self.image_bg_end]);
-
- // Then draw background cells
- try self.drawCellBgs(encoder, frame);
-
- // Then draw images under text
- try self.drawImagePlacements(encoder, frame, self.image_placements.items[self.image_bg_end..self.image_text_end]);
-
- // Then draw fg cells
- try self.drawCellFgs(encoder, frame, fg_count);
-
- // Then draw remaining images
- try self.drawImagePlacements(encoder, frame, self.image_placements.items[self.image_text_end..]);
- }
-
- // If we have custom shaders, then we render them.
- if (self.custom_shader_state) |*state| {
- // MTLRenderPassDescriptor
- const desc = desc: {
- const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?;
- const desc = MTLRenderPassDescriptor.msgSend(
- objc.Object,
- objc.sel("renderPassDescriptor"),
- .{},
- );
-
- break :desc desc;
- };
-
- // Prepare our color attachment (output).
- const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
- const attachment = attachments.msgSend(
- objc.Object,
- objc.sel("objectAtIndexedSubscript:"),
- .{@as(c_ulong, 0)},
- );
- attachment.setProperty("loadAction", @intFromEnum(mtl.MTLLoadAction.clear));
- attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store));
- attachment.setProperty("clearColor", mtl.MTLClearColor{
- .red = 0,
- .green = 0,
- .blue = 0,
- .alpha = 1,
- });
-
- const post_len = self.shaders.post_pipelines.len;
-
- for (self.shaders.post_pipelines[0 .. post_len - 1]) |pipeline| {
- // Set our color attachment to be our front texture.
- attachment.setProperty("texture", state.front_texture.value);
-
- // MTLRenderCommandEncoder
- const encoder = buffer.msgSend(
- objc.Object,
- objc.sel("renderCommandEncoderWithDescriptor:"),
- .{desc.value},
- );
- defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
-
- // Draw shader
- try self.drawPostShader(encoder, pipeline, state);
- // Swap the front and back textures.
- state.swap();
- }
-
- // Draw the final shader directly to the drawable.
- {
- // Set our color attachment to be our drawable.
- //
- // Texture is a property of CAMetalDrawable but if you run
- // Ghostty in XCode in debug mode it returns a CaptureMTLDrawable
- // which ironically doesn't implement CAMetalDrawable as a
- // property so we just send a message.
- const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{});
- attachment.setProperty("texture", texture);
-
- // MTLRenderCommandEncoder
- const encoder = buffer.msgSend(
- objc.Object,
- objc.sel("renderCommandEncoderWithDescriptor:"),
- .{desc.value},
- );
- defer encoder.msgSend(void, objc.sel("endEncoding"), .{});
-
- try self.drawPostShader(
- encoder,
- self.shaders.post_pipelines[post_len - 1],
- state,
- );
- }
- }
-
- buffer.msgSend(void, objc.sel("presentDrawable:"), .{drawable.value});
-
- // Create our block to register for completion updates. This is used
- // so we can detect failures. The block is deallocated by the objC
- // runtime on success.
- const block = try CompletionBlock.init(.{ .self = self }, &bufferCompleted);
- errdefer block.deinit();
- buffer.msgSend(void, objc.sel("addCompletedHandler:"), .{block.context});
-
- buffer.msgSend(void, objc.sel("commit"), .{});
-}
-
-/// This is the block type used for the addCompletedHandler call.back.
-const CompletionBlock = objc.Block(struct { self: *Metal }, .{
- objc.c.id, // MTLCommandBuffer
-}, void);
-
-/// This is the callback called by the CompletionBlock invocation for
-/// addCompletedHandler.
-///
-/// Note: this is USUALLY called on a separate thread because the renderer
-/// thread and the Apple event loop threads are usually different. Therefore,
-/// we need to be mindful of thread safety here.
-fn bufferCompleted(
- block: *const CompletionBlock.Context,
- buffer_id: objc.c.id,
-) callconv(.c) void {
- const self = block.self;
- const buffer = objc.Object.fromId(buffer_id);
-
- // Get our command buffer status. If it is anything other than error
- // then we don't care and just return right away. We're looking for
- // errors so that we can log them.
- const status = buffer.getProperty(mtl.MTLCommandBufferStatus, "status");
- const health: Health = switch (status) {
- .@"error" => .unhealthy,
- else => .healthy,
- };
-
- // If our health value hasn't changed, then we do nothing. We don't
- // do a cmpxchg here because strict atomicity isn't important.
- if (self.health.load(.seq_cst) != health) {
- self.health.store(health, .seq_cst);
-
- // Our health value changed, so we notify the surface so that it
- // can do something about it.
- _ = self.surface_mailbox.push(.{
- .renderer_health = health,
- }, .{ .forever = {} });
- }
-
- // Always release our semaphore
- self.gpu_state.releaseFrame();
-}
-
-fn drawPostShader(
- self: *Metal,
- encoder: objc.Object,
- pipeline: objc.Object,
- state: *const CustomShaderState,
-) !void {
- _ = self;
-
- // Use our custom shader pipeline
- encoder.msgSend(
- void,
- objc.sel("setRenderPipelineState:"),
- .{pipeline.value},
- );
-
- // Set our sampler
- encoder.msgSend(
- void,
- objc.sel("setFragmentSamplerState:atIndex:"),
- .{ state.sampler.sampler.value, @as(c_ulong, 0) },
- );
-
- // Set our uniforms
- encoder.msgSend(
- void,
- objc.sel("setFragmentBytes:length:atIndex:"),
- .{
- @as(*const anyopaque, @ptrCast(&state.uniforms)),
- @as(c_ulong, @sizeOf(@TypeOf(state.uniforms))),
- @as(c_ulong, 0),
- },
- );
-
- // Screen texture
- encoder.msgSend(
- void,
- objc.sel("setFragmentTexture:atIndex:"),
- .{
- state.back_texture.value,
- @as(c_ulong, 0),
- },
- );
-
- // Draw!
- encoder.msgSend(
- void,
- objc.sel("drawPrimitives:vertexStart:vertexCount:"),
- .{
- @intFromEnum(mtl.MTLPrimitiveType.triangle),
- @as(c_ulong, 0),
- @as(c_ulong, 3),
- },
- );
-}
-
-fn drawImagePlacements(
- self: *Metal,
- encoder: objc.Object,
- frame: *const FrameState,
- placements: []const mtl_image.Placement,
-) !void {
- if (placements.len == 0) return;
-
- // Use our image shader pipeline
- encoder.msgSend(
- void,
- objc.sel("setRenderPipelineState:"),
- .{self.shaders.image_pipeline.value},
- );
-
- // Set our uniforms
- encoder.msgSend(
- void,
- objc.sel("setVertexBuffer:offset:atIndex:"),
- .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
- );
- encoder.msgSend(
- void,
- objc.sel("setFragmentBuffer:offset:atIndex:"),
- .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
- );
-
- for (placements) |placement| {
- try self.drawImagePlacement(encoder, placement);
- }
-}
-
-fn drawImagePlacement(
- self: *Metal,
- encoder: objc.Object,
- p: mtl_image.Placement,
-) !void {
- // Look up the image
- const image = self.images.get(p.image_id) orelse {
- log.warn("image not found for placement image_id={}", .{p.image_id});
- return;
- };
-
- // Get the texture
- const texture = switch (image.image) {
- .ready => |t| t,
- else => {
- log.warn("image not ready for placement image_id={}", .{p.image_id});
- return;
- },
- };
-
- // Create our vertex buffer, which is always exactly one item.
- // future(mitchellh): we can group rendering multiple instances of a single image
- const Buffer = mtl_buffer.Buffer(mtl_shaders.Image);
- var buf = try Buffer.initFill(self.gpu_state.device, &.{.{
- .grid_pos = .{
- @as(f32, @floatFromInt(p.x)),
- @as(f32, @floatFromInt(p.y)),
- },
-
- .cell_offset = .{
- @as(f32, @floatFromInt(p.cell_offset_x)),
- @as(f32, @floatFromInt(p.cell_offset_y)),
- },
-
- .source_rect = .{
- @as(f32, @floatFromInt(p.source_x)),
- @as(f32, @floatFromInt(p.source_y)),
- @as(f32, @floatFromInt(p.source_width)),
- @as(f32, @floatFromInt(p.source_height)),
- },
-
- .dest_size = .{
- @as(f32, @floatFromInt(p.width)),
- @as(f32, @floatFromInt(p.height)),
- },
- }}, .{
- // Indicate that the CPU writes to this resource but never reads it.
- .cpu_cache_mode = .write_combined,
- .storage_mode = self.gpu_state.default_storage_mode,
- });
- defer buf.deinit();
-
- // Set our buffer
- encoder.msgSend(
- void,
- objc.sel("setVertexBuffer:offset:atIndex:"),
- .{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) },
- );
-
- // Set our texture
- encoder.msgSend(
- void,
- objc.sel("setVertexTexture:atIndex:"),
- .{
- texture.value,
- @as(c_ulong, 0),
- },
- );
- encoder.msgSend(
- void,
- objc.sel("setFragmentTexture:atIndex:"),
- .{
- texture.value,
- @as(c_ulong, 0),
- },
- );
-
- // Draw!
- encoder.msgSend(
- void,
- objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"),
- .{
- @intFromEnum(mtl.MTLPrimitiveType.triangle),
- @as(c_ulong, 6),
- @intFromEnum(mtl.MTLIndexType.uint16),
- self.gpu_state.instance.buffer.value,
- @as(c_ulong, 0),
- @as(c_ulong, 1),
- },
- );
-
- // log.debug("drawImagePlacement: {}", .{p});
-}
-
-/// Draw the cell backgrounds.
-fn drawCellBgs(
- self: *Metal,
- encoder: objc.Object,
- frame: *const FrameState,
-) !void {
- // Use our shader pipeline
- encoder.msgSend(
- void,
- objc.sel("setRenderPipelineState:"),
- .{self.shaders.cell_bg_pipeline.value},
- );
-
- // Set our buffers
- encoder.msgSend(
- void,
- objc.sel("setVertexBuffer:offset:atIndex:"),
- .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
- );
- encoder.msgSend(
- void,
- objc.sel("setFragmentBuffer:offset:atIndex:"),
- .{ frame.cells_bg.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) },
- );
- encoder.msgSend(
- void,
- objc.sel("setFragmentBuffer:offset:atIndex:"),
- .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
- );
-
- encoder.msgSend(
- void,
- objc.sel("drawPrimitives:vertexStart:vertexCount:"),
- .{
- @intFromEnum(mtl.MTLPrimitiveType.triangle),
- @as(c_ulong, 0),
- @as(c_ulong, 3),
- },
- );
-}
-
-/// Draw the cell foregrounds using the text shader.
-fn drawCellFgs(
- self: *Metal,
- encoder: objc.Object,
- frame: *const FrameState,
- len: usize,
-) !void {
- // This triggers an assertion in the Metal API if we try to draw
- // with an instance count of 0 so just bail.
- if (len == 0) return;
-
- // Use our shader pipeline
- encoder.msgSend(
- void,
- objc.sel("setRenderPipelineState:"),
- .{self.shaders.cell_text_pipeline.value},
- );
-
- // Set our buffers
- encoder.msgSend(
- void,
- objc.sel("setVertexBuffer:offset:atIndex:"),
- .{ frame.cells.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) },
- );
- encoder.msgSend(
- void,
- objc.sel("setVertexBuffer:offset:atIndex:"),
- .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) },
- );
- encoder.msgSend(
- void,
- objc.sel("setVertexBuffer:offset:atIndex:"),
- .{ frame.cells_bg.buffer.value, @as(c_ulong, 0), @as(c_ulong, 2) },
- );
- encoder.msgSend(
- void,
- objc.sel("setFragmentTexture:atIndex:"),
- .{ frame.grayscale.value, @as(c_ulong, 0) },
- );
- encoder.msgSend(
- void,
- objc.sel("setFragmentTexture:atIndex:"),
- .{ frame.color.value, @as(c_ulong, 1) },
- );
- encoder.msgSend(
- void,
- objc.sel("setFragmentBuffer:offset:atIndex:"),
- .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 2) },
- );
-
- encoder.msgSend(
- void,
- objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"),
- .{
- @intFromEnum(mtl.MTLPrimitiveType.triangle),
- @as(c_ulong, 6),
- @intFromEnum(mtl.MTLIndexType.uint16),
- self.gpu_state.instance.buffer.value,
- @as(c_ulong, 0),
- @as(c_ulong, len),
- },
- );
-}
-
-/// This goes through the Kitty graphic placements and accumulates the
-/// placements we need to render on our viewport. It also ensures that
-/// the visible images are loaded on the GPU.
-fn prepKittyGraphics(
- self: *Metal,
- t: *terminal.Terminal,
-) !void {
- const storage = &t.screen.kitty_images;
- defer storage.dirty = false;
-
- // We always clear our previous placements no matter what because
- // we rebuild them from scratch.
- self.image_placements.clearRetainingCapacity();
- self.image_virtual = false;
-
- // Go through our known images and if there are any that are no longer
- // in use then mark them to be freed.
- //
- // This never conflicts with the below because a placement can't
- // reference an image that doesn't exist.
- {
- var it = self.images.iterator();
- while (it.next()) |kv| {
- if (storage.imageById(kv.key_ptr.*) == null) {
- kv.value_ptr.image.markForUnload();
- }
- }
- }
-
- // The top-left and bottom-right corners of our viewport in screen
- // points. This lets us determine offsets and containment of placements.
- const top = t.screen.pages.getTopLeft(.viewport);
- const bot = t.screen.pages.getBottomRight(.viewport).?;
- const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y;
- const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y;
-
- // Go through the placements and ensure the image is loaded on the GPU.
- var it = storage.placements.iterator();
- while (it.next()) |kv| {
- const p = kv.value_ptr;
-
- // Special logic based on location
- switch (p.location) {
- .pin => {},
- .virtual => {
- // We need to mark virtual placements on our renderer so that
- // we know to rebuild in more scenarios since cell changes can
- // now trigger placement changes.
- self.image_virtual = true;
-
- // We also continue out because virtual placements are
- // only triggered by the unicode placeholder, not by the
- // placement itself.
- continue;
- },
- }
-
- // Get the image for the placement
- const image = storage.imageById(kv.key_ptr.image_id) orelse {
- log.warn(
- "missing image for placement, ignoring image_id={}",
- .{kv.key_ptr.image_id},
- );
- continue;
- };
-
- try self.prepKittyPlacement(t, top_y, bot_y, &image, p);
- }
-
- // If we have virtual placements then we need to scan for placeholders.
- if (self.image_virtual) {
- var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot);
- while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement(
- t,
- &virtual_p,
- );
- }
-
- // Sort the placements by their Z value.
- std.mem.sortUnstable(
- mtl_image.Placement,
- self.image_placements.items,
- {},
- struct {
- fn lessThan(
- ctx: void,
- lhs: mtl_image.Placement,
- rhs: mtl_image.Placement,
- ) bool {
- _ = ctx;
- return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id);
- }
- }.lessThan,
- );
-
- // Find our indices. The values are sorted by z so we can find the
- // first placement out of bounds to find the limits.
- var bg_end: ?u32 = null;
- var text_end: ?u32 = null;
- const bg_limit = std.math.minInt(i32) / 2;
- for (self.image_placements.items, 0..) |p, i| {
- if (bg_end == null and p.z >= bg_limit) {
- bg_end = @intCast(i);
- }
- if (text_end == null and p.z >= 0) {
- text_end = @intCast(i);
- }
- }
-
- self.image_bg_end = bg_end orelse 0;
- self.image_text_end = text_end orelse self.image_bg_end;
-}
-
-fn prepKittyVirtualPlacement(
- self: *Metal,
- t: *terminal.Terminal,
- p: *const terminal.kitty.graphics.unicode.Placement,
-) !void {
- const storage = &t.screen.kitty_images;
- const image = storage.imageById(p.image_id) orelse {
- log.warn(
- "missing image for virtual placement, ignoring image_id={}",
- .{p.image_id},
- );
- return;
- };
-
- const rp = p.renderPlacement(
- storage,
- &image,
- self.grid_metrics.cell_width,
- self.grid_metrics.cell_height,
- ) catch |err| {
- log.warn("error rendering virtual placement err={}", .{err});
- return;
- };
-
- // If our placement is zero sized then we don't do anything.
- if (rp.dest_width == 0 or rp.dest_height == 0) return;
-
- const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
- .viewport,
- rp.top_left,
- ) orelse {
- // This is unreachable with virtual placements because we should
- // only ever be looking at virtual placements that are in our
- // viewport in the renderer and virtual placements only ever take
- // up one row.
- unreachable;
- };
-
- // Send our image to the GPU and store the placement for rendering.
- try self.prepKittyImage(&image);
- try self.image_placements.append(self.alloc, .{
- .image_id = image.id,
- .x = @intCast(rp.top_left.x),
- .y = @intCast(viewport.viewport.y),
- .z = -1,
- .width = rp.dest_width,
- .height = rp.dest_height,
- .cell_offset_x = rp.offset_x,
- .cell_offset_y = rp.offset_y,
- .source_x = rp.source_x,
- .source_y = rp.source_y,
- .source_width = rp.source_width,
- .source_height = rp.source_height,
+ .bgra8unorm,
+ .storage_mode = self.default_storage_mode,
+ .width = width,
+ .height = height,
});
}
-fn prepKittyPlacement(
- self: *Metal,
- t: *terminal.Terminal,
- top_y: u32,
- bot_y: u32,
- image: *const terminal.kitty.graphics.Image,
- p: *const terminal.kitty.graphics.ImageStorage.Placement,
-) !void {
- // Get the rect for the placement. If this placement doesn't have
- // a rect then its virtual or something so skip it.
- const rect = p.rect(image.*, t) orelse return;
-
- // This is expensive but necessary.
- const img_top_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
- const img_bot_y = t.screen.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y;
-
- // If the selection isn't within our viewport then skip it.
- if (img_top_y > bot_y) return;
- if (img_bot_y < top_y) return;
-
- // We need to prep this image for upload if it isn't in the cache OR
- // it is in the cache but the transmit time doesn't match meaning this
- // image is different.
- try self.prepKittyImage(image);
-
- // Calculate the dimensions of our image, taking in to
- // account the rows / columns specified by the placement.
- const dest_size = p.calculatedSize(image.*, t);
-
- // Calculate the source rectangle
- const source_x = @min(image.width, p.source_x);
- const source_y = @min(image.height, p.source_y);
- const source_width = if (p.source_width > 0)
- @min(image.width - source_x, p.source_width)
- else
- image.width;
- const source_height = if (p.source_height > 0)
- @min(image.height - source_y, p.source_height)
- else
- image.height;
-
- // Get the viewport-relative Y position of the placement.
- const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y));
-
- // Accumulate the placement
- if (dest_size.width > 0 and dest_size.height > 0) {
- try self.image_placements.append(self.alloc, .{
- .image_id = image.id,
- .x = @intCast(rect.top_left.x),
- .y = y_pos,
- .z = p.z,
- .width = dest_size.width,
- .height = dest_size.height,
- .cell_offset_x = p.x_offset,
- .cell_offset_y = p.y_offset,
- .source_x = source_x,
- .source_y = source_y,
- .source_width = source_width,
- .source_height = source_height,
- });
- }
-}
-
-fn prepKittyImage(
- self: *Metal,
- image: *const terminal.kitty.graphics.Image,
-) !void {
- // If this image exists and its transmit time is the same we assume
- // it is the identical image so we don't need to send it to the GPU.
- const gop = try self.images.getOrPut(self.alloc, image.id);
- if (gop.found_existing and
- gop.value_ptr.transmit_time.order(image.transmit_time) == .eq)
- {
- return;
- }
-
- // Copy the data into the pending state.
- const data = try self.alloc.dupe(u8, image.data);
- errdefer self.alloc.free(data);
-
- // Store it in the map
- const pending: Image.Pending = .{
- .width = image.width,
- .height = image.height,
- .data = data.ptr,
- };
-
- const new_image: Image = switch (image.format) {
- .gray => .{ .pending_gray = pending },
- .gray_alpha => .{ .pending_gray_alpha = pending },
- .rgb => .{ .pending_rgb = pending },
- .rgba => .{ .pending_rgba = pending },
- .png => unreachable, // should be decoded by now
- };
-
- if (!gop.found_existing) {
- gop.value_ptr.* = .{
- .image = new_image,
- .transmit_time = undefined,
- };
+/// Present the provided target.
+pub inline fn present(self: *Metal, target: Target, sync: bool) !void {
+ if (sync) {
+ self.layer.setSurfaceSync(target.surface);
} else {
- try gop.value_ptr.image.markForReplace(
- self.alloc,
- new_image,
- );
- }
-
- gop.value_ptr.transmit_time = image.transmit_time;
-}
-
-/// Update the configuration.
-pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
- // We always redo the font shaper in case font features changed. We
- // could check to see if there was an actual config change but this is
- // easier and rare enough to not cause performance issues.
- {
- var font_shaper = try font.Shaper.init(self.alloc, .{
- .features = config.font_features.items,
- });
- errdefer font_shaper.deinit();
- self.font_shaper.deinit();
- self.font_shaper = font_shaper;
- }
-
- // We also need to reset the shaper cache so shaper info
- // from the previous font isn't re-used for the new font.
- const font_shaper_cache = font.ShaperCache.init();
- self.font_shaper_cache.deinit(self.alloc);
- self.font_shaper_cache = font_shaper_cache;
-
- // Set our new minimum contrast
- self.uniforms.min_contrast = config.min_contrast;
-
- // Set our new color space and blending
- self.uniforms.use_display_p3 = config.colorspace == .@"display-p3";
- self.uniforms.use_linear_blending = config.blending.isLinear();
- self.uniforms.use_linear_correction = config.blending == .@"linear-corrected";
-
- // Set our new colors
- self.default_background_color = config.background;
- self.default_foreground_color = config.foreground;
- self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
- self.cursor_invert = config.cursor_invert;
-
- // Update our layer's opaqueness and display sync in case they changed.
- {
- // We use a CATransaction so that Core Animation knows that we
- // updated the opaque property. Otherwise it behaves weird, not
- // properly going from opaque to transparent unless we resize.
- const CATransaction = objc.getClass("CATransaction").?;
- CATransaction.msgSend(void, "begin", .{});
- defer CATransaction.msgSend(void, "commit", .{});
-
- self.layer.setProperty("opaque", config.background_opacity >= 1);
- self.layer.setProperty("displaySyncEnabled", config.vsync);
- }
-
- // Update our terminal colorspace if it changed
- if (self.config.colorspace != config.colorspace) {
- const terminal_colorspace = try graphics.ColorSpace.createNamed(
- switch (config.colorspace) {
- .@"display-p3" => .displayP3,
- .srgb => .sRGB,
- },
- );
- errdefer terminal_colorspace.release();
- self.terminal_colorspace.release();
- self.terminal_colorspace = terminal_colorspace;
- }
-
- const old_blending = self.config.blending;
- const old_custom_shaders = self.config.custom_shaders;
-
- self.config.deinit();
- self.config = config.*;
-
- // Reset our viewport to force a rebuild, in case of a font change.
- self.cells_viewport = null;
-
- // We reinitialize our shaders if our
- // blending or custom shaders changed.
- if (old_blending != config.blending or
- !old_custom_shaders.equal(config.custom_shaders))
- {
- self.deinitShaders();
- try self.initShaders();
- // We call setScreenSize to reinitialize
- // the textures used for custom shaders.
- if (self.custom_shader_state != null) {
- try self.setScreenSize(self.size);
- }
- // And we update our layer's pixel format appropriately.
- self.layer.setProperty(
- "pixelFormat",
- if (config.blending.isLinear())
- @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
- else
- @intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
- );
+ try self.layer.setSurface(target.surface);
}
}
-/// Resize the screen.
-pub fn setScreenSize(
- self: *Metal,
- size: renderer.Size,
-) !void {
- // Store our sizes
- self.size = size;
- const grid_size = size.grid();
- const terminal_size = size.terminal();
-
- // Blank space around the grid.
- const blank: renderer.Padding = size.screen.blankPadding(
- size.padding,
- grid_size,
- size.cell,
- ).add(size.padding);
-
- var padding_extend = self.uniforms.padding_extend;
- switch (self.config.padding_color) {
- .extend => {
- // If padding extension is enabled, we extend left and right always
- // because there is no downside to this. Up/down is dependent
- // on some heuristics (see rebuildCells).
- padding_extend.left = true;
- padding_extend.right = true;
- },
-
- .@"extend-always" => {
- padding_extend.up = true;
- padding_extend.down = true;
- padding_extend.left = true;
- padding_extend.right = true;
- },
-
- .background => {
- // Otherwise, disable all padding extension.
- padding_extend = .{};
- },
- }
-
- // Set the size of the drawable surface to the bounds
- self.layer.setProperty("drawableSize", graphics.Size{
- .width = @floatFromInt(size.screen.width),
- .height = @floatFromInt(size.screen.height),
- });
-
- // Setup our uniforms
- const old = self.uniforms;
- self.uniforms = .{
- .projection_matrix = math.ortho2d(
- -1 * @as(f32, @floatFromInt(size.padding.left)),
- @floatFromInt(terminal_size.width + size.padding.right),
- @floatFromInt(terminal_size.height + size.padding.bottom),
- -1 * @as(f32, @floatFromInt(size.padding.top)),
- ),
- .cell_size = .{
- @floatFromInt(self.grid_metrics.cell_width),
- @floatFromInt(self.grid_metrics.cell_height),
- },
- .grid_size = .{
- grid_size.columns,
- grid_size.rows,
- },
- .grid_padding = .{
- @floatFromInt(blank.top),
- @floatFromInt(blank.right),
- @floatFromInt(blank.bottom),
- @floatFromInt(blank.left),
- },
- .padding_extend = padding_extend,
- .min_contrast = old.min_contrast,
- .cursor_pos = old.cursor_pos,
- .cursor_color = old.cursor_color,
- .bg_color = old.bg_color,
- .cursor_wide = old.cursor_wide,
- .use_display_p3 = old.use_display_p3,
- .use_linear_blending = old.use_linear_blending,
- .use_linear_correction = old.use_linear_correction,
- };
-
- // Reset our cell contents if our grid size has changed.
- if (!self.cells.size.equals(grid_size)) {
- try self.cells.resize(self.alloc, grid_size);
-
- // Reset our viewport to force a rebuild
- self.cells_viewport = null;
- }
-
- // If we have custom shaders then we update the state
- if (self.custom_shader_state) |*state| {
- // Only free our previous texture if this isn't our first
- // time setting the custom shader state.
- if (state.uniforms.resolution[0] > 0) {
- state.front_texture.release();
- state.back_texture.release();
- }
-
- state.uniforms.resolution = .{
- @floatFromInt(size.screen.width),
- @floatFromInt(size.screen.height),
- 1,
- };
-
- state.front_texture = texture: {
- // This texture is the size of our drawable but supports being a
- // render target AND reading so that the custom shaders can read from it.
- const desc = init: {
- const Class = objc.getClass("MTLTextureDescriptor").?;
- const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
- const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
- break :init id_init;
- };
- desc.setProperty(
- "pixelFormat",
- // Using an `*_srgb` pixel format makes Metal gamma encode
- // the pixels written to it *after* blending, which means
- // we get linear alpha blending rather than gamma-incorrect
- // blending.
- if (self.config.blending.isLinear())
- @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
- else
- @intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
- );
- desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width)));
- desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height)));
- desc.setProperty(
- "usage",
- @intFromEnum(mtl.MTLTextureUsage.render_target) |
- @intFromEnum(mtl.MTLTextureUsage.shader_read) |
- @intFromEnum(mtl.MTLTextureUsage.shader_write),
- );
-
- // If we fail to create the texture, then we just don't have a screen
- // texture and our custom shaders won't run.
- const id = self.gpu_state.device.msgSend(
- ?*anyopaque,
- objc.sel("newTextureWithDescriptor:"),
- .{desc},
- ) orelse return error.MetalFailed;
-
- break :texture objc.Object.fromId(id);
- };
-
- state.back_texture = texture: {
- // This texture is the size of our drawable but supports being a
- // render target AND reading so that the custom shaders can read from it.
- const desc = init: {
- const Class = objc.getClass("MTLTextureDescriptor").?;
- const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
- const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
- break :init id_init;
- };
- desc.setProperty(
- "pixelFormat",
- // Using an `*_srgb` pixel format makes Metal gamma encode
- // the pixels written to it *after* blending, which means
- // we get linear alpha blending rather than gamma-incorrect
- // blending.
- if (self.config.blending.isLinear())
- @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb)
- else
- @intFromEnum(mtl.MTLPixelFormat.bgra8unorm),
- );
- desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width)));
- desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height)));
- desc.setProperty(
- "usage",
- @intFromEnum(mtl.MTLTextureUsage.render_target) |
- @intFromEnum(mtl.MTLTextureUsage.shader_read) |
- @intFromEnum(mtl.MTLTextureUsage.shader_write),
- );
-
- // If we fail to create the texture, then we just don't have a screen
- // texture and our custom shaders won't run.
- const id = self.gpu_state.device.msgSend(
- ?*anyopaque,
- objc.sel("newTextureWithDescriptor:"),
- .{desc},
- ) orelse return error.MetalFailed;
-
- break :texture objc.Object.fromId(id);
- };
- }
-
- log.debug("screen size size={}", .{size});
+/// Present the last presented target again. (noop for Metal)
+pub inline fn presentLastTarget(self: *Metal) !void {
+ _ = self;
}
-/// Convert the terminal state to GPU cells stored in CPU memory. These
-/// are then synced to the GPU in the next frame. This only updates CPU
-/// memory and doesn't touch the GPU.
-fn rebuildCells(
- self: *Metal,
- rebuild: bool,
- screen: *terminal.Screen,
- screen_type: terminal.ScreenType,
- mouse: renderer.State.Mouse,
- preedit: ?renderer.State.Preedit,
- cursor_style_: ?renderer.CursorStyle,
- color_palette: *const terminal.color.Palette,
-) !void {
- // const start = try std.time.Instant.now();
- // const start_micro = std.time.microTimestamp();
- // defer {
- // const end = std.time.Instant.now() catch unreachable;
- // // "[rebuildCells time] \t"
- // std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us});
- // }
-
- _ = screen_type; // we might use this again later so not deleting it yet
-
- // Create an arena for all our temporary allocations while rebuilding
- var arena = ArenaAllocator.init(self.alloc);
- defer arena.deinit();
- const arena_alloc = arena.allocator();
-
- // Create our match set for the links.
- var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet(
- arena_alloc,
- screen,
- mouse_pt,
- mouse.mods,
- ) else .{};
-
- // Determine our x/y range for preedit. We don't want to render anything
- // here because we will render the preedit separately.
- const preedit_range: ?struct {
- y: terminal.size.CellCountInt,
- x: [2]terminal.size.CellCountInt,
- cp_offset: usize,
- } = if (preedit) |preedit_v| preedit: {
- const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1);
- break :preedit .{
- .y = screen.cursor.y,
- .x = .{ range.start, range.end },
- .cp_offset = range.cp_offset,
- };
- } else null;
-
- if (rebuild) {
- // If we are doing a full rebuild, then we clear the entire cell buffer.
- self.cells.reset();
-
- // We also reset our padding extension depending on the screen type
- switch (self.config.padding_color) {
- .background => {},
-
- // For extension, assume we are extending in all directions.
- // For "extend" this may be disabled due to heuristics below.
- .extend, .@"extend-always" => {
- self.uniforms.padding_extend = .{
- .up = true,
- .down = true,
- .left = true,
- .right = true,
- };
- },
- }
- }
-
- // We rebuild the cells row-by-row because we
- // do font shaping and dirty tracking by row.
- var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
- // If our cell contents buffer is shorter than the screen viewport,
- // we render the rows that fit, starting from the bottom. If instead
- // the viewport is shorter than the cell contents buffer, we align
- // the top of the viewport with the top of the contents buffer.
- var y: terminal.size.CellCountInt = @min(
- screen.pages.rows,
- self.cells.size.rows,
- );
- while (row_it.next()) |row| {
- // The viewport may have more rows than our cell contents,
- // so we need to break from the loop early if we hit y = 0.
- if (y == 0) break;
-
- y -= 1;
-
- if (!rebuild) {
- // Only rebuild if we are doing a full rebuild or this row is dirty.
- if (!row.isDirty()) continue;
-
- // Clear the cells if the row is dirty
- self.cells.clear(y);
- }
-
- // True if we want to do font shaping around the cursor. We want to
- // do font shaping as long as the cursor is enabled.
- const shape_cursor = screen.viewportIsBottom() and
- y == screen.cursor.y;
-
- // We need to get this row's selection if there is one for proper
- // run splitting.
- const row_selection = sel: {
- const sel = screen.selection orelse break :sel null;
- const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse
- break :sel null;
- break :sel sel.containedRow(screen, pin) orelse null;
- };
-
- // On primary screen, we still apply vertical padding extension
- // under certain conditions we feel are safe. This helps make some
- // scenarios look better while avoiding scenarios we know do NOT look
- // good.
- switch (self.config.padding_color) {
- // These already have the correct values set above.
- .background, .@"extend-always" => {},
-
- // Apply heuristics for padding extension.
- .extend => if (y == 0) {
- self.uniforms.padding_extend.up = !row.neverExtendBg(
- color_palette,
- self.background_color orelse self.default_background_color,
- );
- } else if (y == self.cells.size.rows - 1) {
- self.uniforms.padding_extend.down = !row.neverExtendBg(
- color_palette,
- self.background_color orelse self.default_background_color,
- );
- },
- }
-
- // Iterator of runs for shaping.
- var run_iter = self.font_shaper.runIterator(
- self.font_grid,
- screen,
- row,
- row_selection,
- if (shape_cursor) screen.cursor.x else null,
- );
- var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc);
- var shaper_cells: ?[]const font.shape.Cell = null;
- var shaper_cells_i: usize = 0;
-
- const row_cells_all = row.cells(.all);
-
- // If our viewport is wider than our cell contents buffer,
- // we still only process cells up to the width of the buffer.
- const row_cells = row_cells_all[0..@min(row_cells_all.len, self.cells.size.columns)];
-
- for (row_cells, 0..) |*cell, x| {
- // If this cell falls within our preedit range then we
- // skip this because preedits are setup separately.
- if (preedit_range) |range| preedit: {
- // We're not on the preedit line, no actions necessary.
- if (range.y != y) break :preedit;
- // We're before the preedit range, no actions necessary.
- if (x < range.x[0]) break :preedit;
- // We're in the preedit range, skip this cell.
- if (x <= range.x[1]) continue;
- // After exiting the preedit range we need to catch
- // the run position up because of the missed cells.
- // In all other cases, no action is necessary.
- if (x != range.x[1] + 1) break :preedit;
-
- // Step the run iterator until we find a run that ends
- // after the current cell, which will be the soonest run
- // that might contain glyphs for our cell.
- while (shaper_run) |run| {
- if (run.offset + run.cells > x) break;
- shaper_run = try run_iter.next(self.alloc);
- shaper_cells = null;
- shaper_cells_i = 0;
- }
-
- const run = shaper_run orelse break :preedit;
-
- // If we haven't shaped this run, do so now.
- shaper_cells = shaper_cells orelse
- // Try to read the cells from the shaping cache if we can.
- self.font_shaper_cache.get(run) orelse
- cache: {
- // Otherwise we have to shape them.
- const cells = try self.font_shaper.shape(run);
-
- // Try to cache them. If caching fails for any reason we
- // continue because it is just a performance optimization,
- // not a correctness issue.
- self.font_shaper_cache.put(
- self.alloc,
- run,
- cells,
- ) catch |err| {
- log.warn(
- "error caching font shaping results err={}",
- .{err},
- );
- };
-
- // The cells we get from direct shaping are always owned
- // by the shaper and valid until the next shaping call so
- // we can safely use them.
- break :cache cells;
- };
-
- // Advance our index until we reach or pass
- // our current x position in the shaper cells.
- while (shaper_cells.?[shaper_cells_i].x < x) {
- shaper_cells_i += 1;
- }
- }
-
- const wide = cell.wide;
-
- const style = row.style(cell);
-
- const cell_pin: terminal.Pin = cell: {
- var copy = row;
- copy.x = @intCast(x);
- break :cell copy;
- };
-
- // True if this cell is selected
- const selected: bool = if (screen.selection) |sel|
- sel.contains(screen, .{
- .node = row.node,
- .y = row.y,
- .x = @intCast(
- // Spacer tails should show the selection
- // state of the wide cell they belong to.
- if (wide == .spacer_tail)
- x -| 1
- else
- x,
- ),
- })
- else
- false;
-
- const bg_style = style.bg(cell, color_palette);
- const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color;
-
- // The final background color for the cell.
- const bg = bg: {
- if (selected) {
- break :bg if (self.config.invert_selection_fg_bg)
- if (style.flags.inverse)
- // Cell is selected with invert selection fg/bg
- // enabled, and the cell has the inverse style
- // flag, so they cancel out and we get the normal
- // bg color.
- bg_style
- else
- // If it doesn't have the inverse style
- // flag then we use the fg color instead.
- fg_style
- else
- // If we don't have invert selection fg/bg set then we
- // just use the selection background if set, otherwise
- // the default fg color.
- break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color;
- }
-
- // Not selected
- break :bg if (style.flags.inverse != isCovering(cell.codepoint()))
- // Two cases cause us to invert (use the fg color as the bg)
- // - The "inverse" style flag.
- // - A "covering" glyph; we use fg for bg in that case to
- // help make sure that padding extension works correctly.
- // If one of these is true (but not the other)
- // then we use the fg style color for the bg.
- fg_style
- else
- // Otherwise they cancel out.
- bg_style;
- };
-
- const fg = fg: {
- if (selected and !self.config.invert_selection_fg_bg) {
- // If we don't have invert selection fg/bg set
- // then we just use the selection foreground if
- // set, otherwise the default bg color.
- break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color;
- }
-
- // Whether we need to use the bg color as our fg color:
- // - Cell is inverted and not selected
- // - Cell is selected and not inverted
- // Note: if selected then invert sel fg / bg must be
- // false since we separately handle it if true above.
- break :fg if (style.flags.inverse != selected)
- bg_style orelse self.background_color orelse self.default_background_color
- else
- fg_style;
- };
-
- // Foreground alpha for this cell.
- const alpha: u8 = if (style.flags.faint) 175 else 255;
-
- // Set the cell's background color.
- {
- const rgb = bg orelse self.background_color orelse self.default_background_color;
-
- // Determine our background alpha. If we have transparency configured
- // then this is dynamic depending on some situations. This is all
- // in an attempt to make transparency look the best for various
- // situations. See inline comments.
- const bg_alpha: u8 = bg_alpha: {
- const default: u8 = 255;
-
- if (self.config.background_opacity >= 1) break :bg_alpha default;
-
- // Cells that are selected should be fully opaque.
- if (selected) break :bg_alpha default;
-
- // Cells that are reversed should be fully opaque.
- if (style.flags.inverse) break :bg_alpha default;
-
- // Cells that have an explicit bg color should be fully opaque.
- if (bg_style != null) {
- break :bg_alpha default;
- }
-
- // Otherwise, we use the configured background opacity.
- break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0));
- };
-
- self.cells.bgCell(y, x).* = .{
- rgb.r, rgb.g, rgb.b, bg_alpha,
- };
- }
-
- // If the invisible flag is set on this cell then we
- // don't need to render any foreground elements, so
- // we just skip all glyphs with this x coordinate.
- //
- // NOTE: This behavior matches xterm. Some other terminal
- // emulators, e.g. Alacritty, still render text decorations
- // and only make the text itself invisible. The decision
- // has been made here to match xterm's behavior for this.
- if (style.flags.invisible) {
- continue;
- }
-
- // Give links a single underline, unless they already have
- // an underline, in which case use a double underline to
- // distinguish them.
- const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin))
- if (style.flags.underline == .single)
- .double
- else
- .single
- else
- style.flags.underline;
-
- // We draw underlines first so that they layer underneath text.
- // This improves readability when a colored underline is used
- // which intersects parts of the text (descenders).
- if (underline != .none) self.addUnderline(
- @intCast(x),
- @intCast(y),
- underline,
- style.underlineColor(color_palette) orelse fg,
- alpha,
- ) catch |err| {
- log.warn(
- "error adding underline to cell, will be invalid x={} y={}, err={}",
- .{ x, y, err },
- );
- };
-
- if (style.flags.overline) self.addOverline(@intCast(x), @intCast(y), fg, alpha) catch |err| {
- log.warn(
- "error adding overline to cell, will be invalid x={} y={}, err={}",
- .{ x, y, err },
- );
- };
-
- // If we're at or past the end of our shaper run then
- // we need to get the next run from the run iterator.
- if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) {
- shaper_run = try run_iter.next(self.alloc);
- shaper_cells = null;
- shaper_cells_i = 0;
- }
-
- if (shaper_run) |run| glyphs: {
- // If we haven't shaped this run yet, do so.
- shaper_cells = shaper_cells orelse
- // Try to read the cells from the shaping cache if we can.
- self.font_shaper_cache.get(run) orelse
- cache: {
- // Otherwise we have to shape them.
- const cells = try self.font_shaper.shape(run);
-
- // Try to cache them. If caching fails for any reason we
- // continue because it is just a performance optimization,
- // not a correctness issue.
- self.font_shaper_cache.put(
- self.alloc,
- run,
- cells,
- ) catch |err| {
- log.warn(
- "error caching font shaping results err={}",
- .{err},
- );
- };
-
- // The cells we get from direct shaping are always owned
- // by the shaper and valid until the next shaping call so
- // we can safely use them.
- break :cache cells;
- };
-
- const cells = shaper_cells orelse break :glyphs;
-
- // If there are no shaper cells for this run, ignore it.
- // This can occur for runs of empty cells, and is fine.
- if (cells.len == 0) break :glyphs;
-
- // If we encounter a shaper cell to the left of the current
- // cell then we have some problems. This logic relies on x
- // position monotonically increasing.
- assert(cells[shaper_cells_i].x >= x);
-
- // NOTE: An assumption is made here that a single cell will never
- // be present in more than one shaper run. If that assumption is
- // violated, this logic breaks.
-
- while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({
- shaper_cells_i += 1;
- }) {
- self.addGlyph(
- @intCast(x),
- @intCast(y),
- cell_pin,
- cells[shaper_cells_i],
- shaper_run.?,
- fg,
- alpha,
- ) catch |err| {
- log.warn(
- "error adding glyph to cell, will be invalid x={} y={}, err={}",
- .{ x, y, err },
- );
- };
- }
- }
-
- // Finally, draw a strikethrough if necessary.
- if (style.flags.strikethrough) self.addStrikethrough(
- @intCast(x),
- @intCast(y),
- fg,
- alpha,
- ) catch |err| {
- log.warn(
- "error adding strikethrough to cell, will be invalid x={} y={}, err={}",
- .{ x, y, err },
- );
- };
- }
- }
-
- // Setup our cursor rendering information.
- cursor: {
- // By default, we don't handle cursor inversion on the shader.
- self.cells.setCursor(null);
- self.uniforms.cursor_pos = .{
- std.math.maxInt(u16),
- std.math.maxInt(u16),
- };
-
- // If we have preedit text, we don't setup a cursor
- if (preedit != null) break :cursor;
-
- // Prepare the cursor cell contents.
- const style = cursor_style_ orelse break :cursor;
- const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: {
- if (self.cursor_invert) {
- // Use the foreground color from the cell under the cursor, if any.
- const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
- break :color if (sty.flags.inverse)
- // If the cell is reversed, use background color instead.
- (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color)
- else
- (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color);
- } else {
- break :color self.foreground_color orelse self.default_foreground_color;
- }
- };
-
- self.addCursor(screen, style, cursor_color);
-
- // If the cursor is visible then we set our uniforms.
- if (style == .block and screen.viewportIsBottom()) {
- const wide = screen.cursor.page_cell.wide;
-
- self.uniforms.cursor_pos = .{
- // If we are a spacer tail of a wide cell, our cursor needs
- // to move back one cell. The saturate is to ensure we don't
- // overflow but this shouldn't happen with well-formed input.
- switch (wide) {
- .narrow, .spacer_head, .wide => screen.cursor.x,
- .spacer_tail => screen.cursor.x -| 1,
- },
- screen.cursor.y,
- };
-
- self.uniforms.cursor_wide = switch (wide) {
- .narrow, .spacer_head => false,
- .wide, .spacer_tail => true,
- };
-
- const uniform_color = if (self.cursor_invert) blk: {
- // Use the background color from the cell under the cursor, if any.
- const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
- break :blk if (sty.flags.inverse)
- // If the cell is reversed, use foreground color instead.
- (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color)
- else
- (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color);
- } else if (self.config.cursor_text) |txt|
- txt
- else
- self.background_color orelse self.default_background_color;
-
- self.uniforms.cursor_color = .{
- uniform_color.r,
- uniform_color.g,
- uniform_color.b,
- 255,
- };
- }
- }
-
- // Setup our preedit text.
- if (preedit) |preedit_v| {
- const range = preedit_range.?;
- var x = range.x[0];
- for (preedit_v.codepoints[range.cp_offset..]) |cp| {
- self.addPreeditCell(cp, .{ .x = x, .y = range.y }) catch |err| {
- log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{
- x,
- range.y,
- err,
- });
- };
-
- x += if (cp.wide) 2 else 1;
- }
- }
-
- // Update that our cells rebuilt
- self.cells_rebuilt = true;
-
- // Log some things
- // log.debug("rebuildCells complete cached_runs={}", .{
- // self.font_shaper_cache.count(),
- // });
-}
-
-/// Add an underline decoration to the specified cell
-fn addUnderline(
- self: *Metal,
- x: terminal.size.CellCountInt,
- y: terminal.size.CellCountInt,
- style: terminal.Attribute.Underline,
- color: terminal.color.RGB,
- alpha: u8,
-) !void {
- const sprite: font.Sprite = switch (style) {
- .none => unreachable,
- .single => .underline,
- .double => .underline_double,
- .dotted => .underline_dotted,
- .dashed => .underline_dashed,
- .curly => .underline_curly,
- };
-
- const render = try self.font_grid.renderGlyph(
- self.alloc,
- font.sprite_index,
- @intFromEnum(sprite),
- .{
- .cell_width = 1,
- .grid_metrics = self.grid_metrics,
- },
- );
-
- try self.cells.add(self.alloc, .underline, .{
- .mode = .fg,
- .grid_pos = .{ @intCast(x), @intCast(y) },
- .constraint_width = 1,
- .color = .{ color.r, color.g, color.b, alpha },
- .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
- .glyph_size = .{ render.glyph.width, render.glyph.height },
- .bearings = .{
- @intCast(render.glyph.offset_x),
- @intCast(render.glyph.offset_y),
- },
- });
-}
-
-/// Add a overline decoration to the specified cell
-fn addOverline(
- self: *Metal,
- x: terminal.size.CellCountInt,
- y: terminal.size.CellCountInt,
- color: terminal.color.RGB,
- alpha: u8,
-) !void {
- const render = try self.font_grid.renderGlyph(
- self.alloc,
- font.sprite_index,
- @intFromEnum(font.Sprite.overline),
- .{
- .cell_width = 1,
- .grid_metrics = self.grid_metrics,
- },
- );
-
- try self.cells.add(self.alloc, .overline, .{
- .mode = .fg,
- .grid_pos = .{ @intCast(x), @intCast(y) },
- .constraint_width = 1,
- .color = .{ color.r, color.g, color.b, alpha },
- .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
- .glyph_size = .{ render.glyph.width, render.glyph.height },
- .bearings = .{
- @intCast(render.glyph.offset_x),
- @intCast(render.glyph.offset_y),
- },
- });
-}
-
-/// Add a strikethrough decoration to the specified cell
-fn addStrikethrough(
- self: *Metal,
- x: terminal.size.CellCountInt,
- y: terminal.size.CellCountInt,
- color: terminal.color.RGB,
- alpha: u8,
-) !void {
- const render = try self.font_grid.renderGlyph(
- self.alloc,
- font.sprite_index,
- @intFromEnum(font.Sprite.strikethrough),
- .{
- .cell_width = 1,
- .grid_metrics = self.grid_metrics,
- },
- );
-
- try self.cells.add(self.alloc, .strikethrough, .{
- .mode = .fg,
- .grid_pos = .{ @intCast(x), @intCast(y) },
- .constraint_width = 1,
- .color = .{ color.r, color.g, color.b, alpha },
- .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
- .glyph_size = .{ render.glyph.width, render.glyph.height },
- .bearings = .{
- @intCast(render.glyph.offset_x),
- @intCast(render.glyph.offset_y),
- },
- });
-}
-
-// Add a glyph to the specified cell.
-fn addGlyph(
- self: *Metal,
- x: terminal.size.CellCountInt,
- y: terminal.size.CellCountInt,
- cell_pin: terminal.Pin,
- shaper_cell: font.shape.Cell,
- shaper_run: font.shape.TextRun,
- color: terminal.color.RGB,
- alpha: u8,
-) !void {
- const rac = cell_pin.rowAndCell();
- const cell = rac.cell;
-
- // Render
- const render = try self.font_grid.renderGlyph(
- self.alloc,
- shaper_run.font_index,
- shaper_cell.glyph_index,
- .{
- .grid_metrics = self.grid_metrics,
- .thicken = self.config.font_thicken,
- .thicken_strength = self.config.font_thicken_strength,
- },
- );
-
- // If the glyph is 0 width or height, it will be invisible
- // when drawn, so don't bother adding it to the buffer.
- if (render.glyph.width == 0 or render.glyph.height == 0) {
- return;
- }
-
- const mode: mtl_shaders.CellText.Mode = switch (try fgMode(
- render.presentation,
- cell_pin,
- )) {
- .normal => .fg,
- .color => .fg_color,
- .constrained => .fg_constrained,
- .powerline => .fg_powerline,
- };
-
- try self.cells.add(self.alloc, .text, .{
- .mode = mode,
- .grid_pos = .{ @intCast(x), @intCast(y) },
- .constraint_width = cell.gridWidth(),
- .color = .{ color.r, color.g, color.b, alpha },
- .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
- .glyph_size = .{ render.glyph.width, render.glyph.height },
- .bearings = .{
- @intCast(render.glyph.offset_x + shaper_cell.x_offset),
- @intCast(render.glyph.offset_y + shaper_cell.y_offset),
- },
- });
-}
-
-fn addCursor(
- self: *Metal,
- screen: *terminal.Screen,
- cursor_style: renderer.CursorStyle,
- cursor_color: terminal.color.RGB,
-) void {
- // Add the cursor. We render the cursor over the wide character if
- // we're on the wide character tail.
- const wide, const x = cell: {
- // The cursor goes over the screen cursor position.
- const cell = screen.cursor.page_cell;
- if (cell.wide != .spacer_tail or screen.cursor.x == 0)
- break :cell .{ cell.wide == .wide, screen.cursor.x };
-
- // If we're part of a wide character, we move the cursor back to
- // the actual character.
- const prev_cell = screen.cursorCellLeft(1);
- break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 };
- };
-
- const alpha: u8 = if (!self.focused) 255 else alpha: {
- const alpha = 255 * self.config.cursor_opacity;
- break :alpha @intFromFloat(@ceil(alpha));
- };
-
- const render = switch (cursor_style) {
- .block,
- .block_hollow,
- .bar,
- .underline,
- => render: {
- const sprite: font.Sprite = switch (cursor_style) {
- .block => .cursor_rect,
- .block_hollow => .cursor_hollow_rect,
- .bar => .cursor_bar,
- .underline => .underline,
- .lock => unreachable,
- };
-
- break :render self.font_grid.renderGlyph(
- self.alloc,
- font.sprite_index,
- @intFromEnum(sprite),
- .{
- .cell_width = if (wide) 2 else 1,
- .grid_metrics = self.grid_metrics,
- },
- ) catch |err| {
- log.warn("error rendering cursor glyph err={}", .{err});
- return;
- };
- },
-
- .lock => self.font_grid.renderCodepoint(
- self.alloc,
- 0xF023, // lock symbol
- .regular,
- .text,
- .{
- .cell_width = if (wide) 2 else 1,
- .grid_metrics = self.grid_metrics,
- },
- ) catch |err| {
- log.warn("error rendering cursor glyph err={}", .{err});
- return;
- } orelse {
- // This should never happen because we embed nerd
- // fonts so we just log and return instead of fallback.
- log.warn("failed to find lock symbol for cursor codepoint=0xF023", .{});
- return;
+/// Returns the options to use when constructing buffers.
+pub inline fn bufferOptions(self: Metal) bufferpkg.Options {
+ return .{
+ .device = self.device,
+ .resource_options = .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = self.default_storage_mode,
},
};
-
- self.cells.setCursor(.{
- .mode = .cursor,
- .grid_pos = .{ x, screen.cursor.y },
- .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha },
- .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
- .glyph_size = .{ render.glyph.width, render.glyph.height },
- .bearings = .{
- @intCast(render.glyph.offset_x),
- @intCast(render.glyph.offset_y),
- },
- });
}
-fn addPreeditCell(
- self: *Metal,
- cp: renderer.State.Preedit.Codepoint,
- coord: terminal.Coordinate,
-) !void {
- // Preedit is rendered inverted
- const bg = self.foreground_color orelse self.default_foreground_color;
- const fg = self.background_color orelse self.default_background_color;
+pub const instanceBufferOptions = bufferOptions;
+pub const uniformBufferOptions = bufferOptions;
+pub const fgBufferOptions = bufferOptions;
+pub const bgBufferOptions = bufferOptions;
+pub const imageBufferOptions = bufferOptions;
- // Render the glyph for our preedit text
- const render_ = self.font_grid.renderCodepoint(
- self.alloc,
- @intCast(cp.codepoint),
- .regular,
- .text,
- .{ .grid_metrics = self.grid_metrics },
- ) catch |err| {
- log.warn("error rendering preedit glyph err={}", .{err});
- return;
- };
- const render = render_ orelse {
- log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint});
- return;
- };
-
- // Add our opaque background cell
- self.cells.bgCell(coord.y, coord.x).* = .{
- bg.r, bg.g, bg.b, 255,
- };
- if (cp.wide and coord.x < self.cells.size.columns - 1) {
- self.cells.bgCell(coord.y, coord.x + 1).* = .{
- bg.r, bg.g, bg.b, 255,
- };
- }
-
- // Add our text
- try self.cells.add(self.alloc, .text, .{
- .mode = .fg,
- .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
- .color = .{ fg.r, fg.g, fg.b, 255 },
- .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
- .glyph_size = .{ render.glyph.width, render.glyph.height },
- .bearings = .{
- @intCast(render.glyph.offset_x),
- @intCast(render.glyph.offset_y),
+/// Returns the options to use when constructing textures.
+pub inline fn textureOptions(self: Metal) Texture.Options {
+ return .{
+ .device = self.device,
+ // Using an `*_srgb` pixel format makes Metal gamma encode the pixels
+ // written to it *after* blending, which means we get linear alpha
+ // blending rather than gamma-incorrect blending.
+ .pixel_format = if (self.blending.isLinear())
+ .bgra8unorm_srgb
+ else
+ .bgra8unorm,
+ .resource_options = .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = self.default_storage_mode,
},
- });
+ };
}
-/// Sync the atlas data to the given texture. This copies the bytes
-/// associated with the atlas to the given texture. If the atlas no longer
-/// fits into the texture, the texture will be resized.
-fn syncAtlasTexture(
- device: objc.Object,
+/// Initializes a Texture suitable for the provided font atlas.
+pub fn initAtlasTexture(
+ self: *const Metal,
atlas: *const font.Atlas,
- texture: *objc.Object,
- /// Storage mode for the MTLTexture object
- storage_mode: mtl.MTLResourceOptions.StorageMode,
-) !void {
- const width = texture.getProperty(c_ulong, "width");
- if (atlas.size > width) {
- // Free our old texture
- texture.*.release();
-
- // Reallocate
- texture.* = try initAtlasTexture(device, atlas, storage_mode);
- }
-
- texture.msgSend(
- void,
- objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"),
- .{
- mtl.MTLRegion{
- .origin = .{ .x = 0, .y = 0, .z = 0 },
- .size = .{
- .width = @intCast(atlas.size),
- .height = @intCast(atlas.size),
- .depth = 1,
- },
- },
- @as(c_ulong, 0),
- @as(*const anyopaque, atlas.data.ptr),
- @as(c_ulong, atlas.format.depth() * atlas.size),
- },
- );
-}
-
-/// Initialize a MTLTexture object for the given atlas.
-fn initAtlasTexture(
- device: objc.Object,
- atlas: *const font.Atlas,
- /// Storage mode for the MTLTexture object
- storage_mode: mtl.MTLResourceOptions.StorageMode,
-) !objc.Object {
- // Determine our pixel format
+) Texture.Error!Texture {
const pixel_format: mtl.MTLPixelFormat = switch (atlas.format) {
.grayscale => .r8unorm,
.rgba => .bgra8unorm,
else => @panic("unsupported atlas format for Metal texture"),
};
- // Create our descriptor
- const desc = init: {
- const Class = objc.getClass("MTLTextureDescriptor").?;
- const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
- const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
- break :init id_init;
- };
-
- // Set our properties
- desc.setProperty("pixelFormat", @intFromEnum(pixel_format));
- desc.setProperty("width", @as(c_ulong, @intCast(atlas.size)));
- desc.setProperty("height", @as(c_ulong, @intCast(atlas.size)));
-
- desc.setProperty(
- "resourceOptions",
- mtl.MTLResourceOptions{
- // Indicate that the CPU writes to this resource but never reads it.
- .cpu_cache_mode = .write_combined,
- .storage_mode = storage_mode,
+ return try Texture.init(
+ .{
+ .device = self.device,
+ .pixel_format = pixel_format,
+ .resource_options = .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = self.default_storage_mode,
+ },
},
+ atlas.size,
+ atlas.size,
+ null,
);
-
- // Initialize
- const id = device.msgSend(
- ?*anyopaque,
- objc.sel("newTextureWithDescriptor:"),
- .{desc},
- ) orelse return error.MetalFailed;
-
- return objc.Object.fromId(id);
}
-test {
- _ = mtl_cell;
+/// Begin a frame.
+pub inline fn beginFrame(
+ self: *const Metal,
+ /// Once the frame has been completed, the `frameCompleted` method
+ /// on the renderer is called with the health status of the frame.
+ renderer: *Renderer,
+ /// The target is presented via the provided renderer's API when completed.
+ target: *Target,
+) !Frame {
+ return try Frame.begin(.{ .queue = self.queue }, renderer, target);
+}
+
+fn chooseDevice() error{NoMetalDevice}!objc.Object {
+ var chosen_device: ?objc.Object = null;
+
+ switch (comptime builtin.os.tag) {
+ .macos => {
+ const devices = objc.Object.fromId(mtl.MTLCopyAllDevices());
+ defer devices.release();
+
+ var iter = devices.iterate();
+ while (iter.next()) |device| {
+ // We want a GPU that’s connected to a display.
+ if (device.getProperty(bool, "isHeadless")) continue;
+ chosen_device = device;
+ // If the user has an eGPU plugged in, they probably want
+ // to use it. Otherwise, integrated GPUs are better for
+ // battery life and thermals.
+ if (device.getProperty(bool, "isRemovable") or
+ device.getProperty(bool, "isLowPower")) break;
+ }
+ },
+ .ios => {
+ chosen_device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice());
+ },
+ else => @compileError("unsupported target for Metal"),
+ }
+
+ const device = chosen_device orelse return error.NoMetalDevice;
+ return device.retain();
}
diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig
index d0222a390..dcc295eaf 100644
--- a/src/renderer/OpenGL.zig
+++ b/src/renderer/OpenGL.zig
@@ -1,452 +1,178 @@
-//! Rendering implementation for OpenGL.
+//! Graphics API wrapper for OpenGL.
pub const OpenGL = @This();
const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const glfw = @import("glfw");
-const assert = std.debug.assert;
-const testing = std.testing;
-const Allocator = std.mem.Allocator;
-const ArenaAllocator = std.heap.ArenaAllocator;
-const link = @import("link.zig");
-const isCovering = @import("cell.zig").isCovering;
-const fgMode = @import("cell.zig").fgMode;
+const gl = @import("opengl");
const shadertoy = @import("shadertoy.zig");
const apprt = @import("../apprt.zig");
-const configpkg = @import("../config.zig");
const font = @import("../font/main.zig");
-const imgui = @import("imgui");
-const renderer = @import("../renderer.zig");
-const terminal = @import("../terminal/main.zig");
-const Terminal = terminal.Terminal;
-const gl = @import("opengl");
-const math = @import("../math.zig");
-const Surface = @import("../Surface.zig");
+const configpkg = @import("../config.zig");
+const rendererpkg = @import("../renderer.zig");
+const Renderer = rendererpkg.GenericRenderer(OpenGL);
-const CellProgram = @import("opengl/CellProgram.zig");
-const ImageProgram = @import("opengl/ImageProgram.zig");
-const gl_image = @import("opengl/image.zig");
-const custom = @import("opengl/custom.zig");
-const Image = gl_image.Image;
-const ImageMap = gl_image.ImageMap;
-const ImagePlacementList = std.ArrayListUnmanaged(gl_image.Placement);
+pub const GraphicsAPI = OpenGL;
+pub const Target = @import("opengl/Target.zig");
+pub const Frame = @import("opengl/Frame.zig");
+pub const RenderPass = @import("opengl/RenderPass.zig");
+pub const Pipeline = @import("opengl/Pipeline.zig");
+const bufferpkg = @import("opengl/buffer.zig");
+pub const Buffer = bufferpkg.Buffer;
+pub const Texture = @import("opengl/Texture.zig");
+pub const shaders = @import("opengl/shaders.zig");
-const log = std.log.scoped(.grid);
+pub const cellpkg = @import("opengl/cell.zig");
+pub const imagepkg = @import("opengl/image.zig");
-/// The runtime can request a single-threaded draw by setting this boolean
-/// to true. In this case, the renderer.draw() call is expected to be called
-/// from the runtime.
-pub const single_threaded_draw = if (@hasDecl(apprt.Surface, "opengl_single_threaded_draw"))
- apprt.Surface.opengl_single_threaded_draw
-else
- false;
-const DrawMutex = if (single_threaded_draw) std.Thread.Mutex else void;
-const drawMutexZero: DrawMutex = if (DrawMutex == void) void{} else .{};
+pub const custom_shader_target: shadertoy.Target = .glsl;
+// The fragCoord for OpenGL shaders is +Y = up.
+pub const custom_shader_y_is_down = false;
+
+/// Because OpenGL's frame completion is always
+/// sync, we have no need for multi-buffering.
+pub const swap_chain_count = 1;
+
+const log = std.log.scoped(.opengl);
+
+/// We require at least OpenGL 4.3
+pub const MIN_VERSION_MAJOR = 4;
+pub const MIN_VERSION_MINOR = 3;
alloc: std.mem.Allocator,
-/// The configuration we need derived from the main config.
-config: DerivedConfig,
+/// Alpha blending mode
+blending: configpkg.Config.AlphaBlending,
-/// Current font metrics defining our grid.
-grid_metrics: font.Metrics,
+/// The most recently presented target, in case we need to present it again.
+last_target: ?Target = null,
-/// The size of everything.
-size: renderer.Size,
-
-/// The current set of cells to render. Each set of cells goes into
-/// a separate shader call.
-cells_bg: std.ArrayListUnmanaged(CellProgram.Cell),
-cells: std.ArrayListUnmanaged(CellProgram.Cell),
-
-/// The last viewport that we based our rebuild off of. If this changes,
-/// then we do a full rebuild of the cells. The pointer values in this pin
-/// are NOT SAFE to read because they may be modified, freed, etc from the
-/// termio thread. We treat the pointers as integers for comparison only.
-cells_viewport: ?terminal.Pin = null,
-
-/// The size of the cells list that was sent to the GPU. This is used
-/// to detect when the cells array was reallocated/resized and handle that
-/// accordingly.
-gl_cells_size: usize = 0,
-
-/// The last length of the cells that was written to the GPU. This is used to
-/// determine what data needs to be rewritten on the GPU.
-gl_cells_written: usize = 0,
-
-/// Shader program for cell rendering.
-gl_state: ?GLState = null,
-
-/// The font structures.
-font_grid: *font.SharedGrid,
-font_shaper: font.Shaper,
-font_shaper_cache: font.ShaperCache,
-texture_grayscale_modified: usize = 0,
-texture_grayscale_resized: usize = 0,
-texture_color_modified: usize = 0,
-texture_color_resized: usize = 0,
-
-/// True if the window is focused
-focused: bool,
-
-/// The foreground color set by an OSC 10 sequence. If unset then the default
-/// value from the config file is used.
-foreground_color: ?terminal.color.RGB,
-
-/// Foreground color set in the user's config file.
-default_foreground_color: terminal.color.RGB,
-
-/// The background color set by an OSC 11 sequence. If unset then the default
-/// value from the config file is used.
-background_color: ?terminal.color.RGB,
-
-/// Background color set in the user's config file.
-default_background_color: terminal.color.RGB,
-
-/// The cursor color set by an OSC 12 sequence. If unset then
-/// default_cursor_color is used.
-cursor_color: ?terminal.color.RGB,
-
-/// Default cursor color when no color is set explicitly by an OSC 12 command.
-/// This is cursor color as set in the user's config, if any. If no cursor color
-/// is set in the user's config, then the cursor color is determined by the
-/// current foreground color.
-default_cursor_color: ?terminal.color.RGB,
-
-/// When `cursor_color` is null, swap the foreground and background colors of
-/// the cell under the cursor for the cursor color. Otherwise, use the default
-/// foreground color as the cursor color.
-cursor_invert: bool,
-
-/// The mailbox for communicating with the window.
-surface_mailbox: apprt.surface.Mailbox,
-
-/// Deferred operations. This is used to apply changes to the OpenGL context.
-/// Some runtimes (GTK) do not support multi-threading so to keep our logic
-/// simple we apply all OpenGL context changes in the render() call.
-deferred_screen_size: ?SetScreenSize = null,
-deferred_font_size: ?SetFontSize = null,
-deferred_config: ?SetConfig = null,
-
-/// If we're drawing with single threaded operations
-draw_mutex: DrawMutex = drawMutexZero,
-
-/// Current background to draw. This may not match self.background if the
-/// terminal is in reversed mode.
-draw_background: terminal.color.RGB,
-
-/// Whether we're doing padding extension for vertical sides.
-padding_extend_top: bool = true,
-padding_extend_bottom: bool = true,
-
-/// The images that we may render.
-images: ImageMap = .{},
-image_placements: ImagePlacementList = .{},
-image_bg_end: u32 = 0,
-image_text_end: u32 = 0,
-image_virtual: bool = false,
-
-/// Deferred OpenGL operation to update the screen size.
-const SetScreenSize = struct {
- size: renderer.Size,
-
- fn apply(self: SetScreenSize, r: *OpenGL) !void {
- const gl_state: *GLState = if (r.gl_state) |*v|
- v
- else
- return error.OpenGLUninitialized;
-
- // Apply our padding
- const grid_size = self.size.grid();
- const terminal_size = self.size.terminal();
-
- // Blank space around the grid.
- const blank: renderer.Padding = switch (r.config.padding_color) {
- // We can use zero padding because the background color is our
- // clear color.
- .background => .{},
-
- .extend, .@"extend-always" => self.size.screen.blankPadding(
- self.size.padding,
- grid_size,
- self.size.cell,
- ).add(self.size.padding),
- };
-
- // Update our viewport for this context to be the entire window.
- // OpenGL works in pixels, so we have to use the pixel size.
- try gl.viewport(
- 0,
- 0,
- @intCast(self.size.screen.width),
- @intCast(self.size.screen.height),
- );
-
- // Update the projection uniform within our shader
- inline for (.{ "cell_program", "image_program" }) |name| {
- const program = @field(gl_state, name);
- const bind = try program.program.use();
- defer bind.unbind();
- try program.program.setUniform(
- "projection",
-
- // 2D orthographic projection with the full w/h
- math.ortho2d(
- -1 * @as(f32, @floatFromInt(self.size.padding.left)),
- @floatFromInt(terminal_size.width + self.size.padding.right),
- @floatFromInt(terminal_size.height + self.size.padding.bottom),
- -1 * @as(f32, @floatFromInt(self.size.padding.top)),
- ),
- );
- }
-
- // Setup our grid padding
- {
- const program = gl_state.cell_program;
- const bind = try program.program.use();
- defer bind.unbind();
- try program.program.setUniform(
- "grid_padding",
- @Vector(4, f32){
- @floatFromInt(blank.top),
- @floatFromInt(blank.right),
- @floatFromInt(blank.bottom),
- @floatFromInt(blank.left),
- },
- );
- try program.program.setUniform(
- "grid_size",
- @Vector(2, f32){
- @floatFromInt(grid_size.columns),
- @floatFromInt(grid_size.rows),
- },
- );
- }
-
- // Update our custom shader resolution
- if (gl_state.custom) |*custom_state| {
- try custom_state.setScreenSize(self.size);
- }
- }
-};
-
-const SetFontSize = struct {
- metrics: font.Metrics,
-
- fn apply(self: SetFontSize, r: *const OpenGL) !void {
- const gl_state = r.gl_state orelse return error.OpenGLUninitialized;
-
- inline for (.{ "cell_program", "image_program" }) |name| {
- const program = @field(gl_state, name);
- const bind = try program.program.use();
- defer bind.unbind();
- try program.program.setUniform(
- "cell_size",
- @Vector(2, f32){
- @floatFromInt(self.metrics.cell_width),
- @floatFromInt(self.metrics.cell_height),
- },
- );
- }
- }
-};
-
-const SetConfig = struct {
- fn apply(self: SetConfig, r: *const OpenGL) !void {
- _ = self;
- const gl_state = r.gl_state orelse return error.OpenGLUninitialized;
-
- const bind = try gl_state.cell_program.program.use();
- defer bind.unbind();
- try gl_state.cell_program.program.setUniform(
- "min_contrast",
- r.config.min_contrast,
- );
- }
-};
-
-/// The configuration for this renderer that is derived from the main
-/// configuration. This must be exported so that we don't need to
-/// pass around Config pointers which makes memory management a pain.
-pub const DerivedConfig = struct {
- arena: ArenaAllocator,
-
- font_thicken: bool,
- font_thicken_strength: u8,
- font_features: std.ArrayListUnmanaged([:0]const u8),
- font_styles: font.CodepointResolver.StyleStatus,
- cursor_color: ?terminal.color.RGB,
- cursor_invert: bool,
- cursor_text: ?terminal.color.RGB,
- cursor_opacity: f64,
- background: terminal.color.RGB,
- background_opacity: f64,
- foreground: terminal.color.RGB,
- selection_background: ?terminal.color.RGB,
- selection_foreground: ?terminal.color.RGB,
- invert_selection_fg_bg: bool,
- bold_is_bright: bool,
- min_contrast: f32,
- padding_color: configpkg.WindowPaddingColor,
- custom_shaders: configpkg.RepeatablePath,
- links: link.Set,
-
- pub fn init(
- alloc_gpa: Allocator,
- config: *const configpkg.Config,
- ) !DerivedConfig {
- var arena = ArenaAllocator.init(alloc_gpa);
- errdefer arena.deinit();
- const alloc = arena.allocator();
-
- // Copy our shaders
- const custom_shaders = try config.@"custom-shader".clone(alloc);
-
- // Copy our font features
- const font_features = try config.@"font-feature".clone(alloc);
-
- // Get our font styles
- var font_styles = font.CodepointResolver.StyleStatus.initFill(true);
- font_styles.set(.bold, config.@"font-style-bold" != .false);
- font_styles.set(.italic, config.@"font-style-italic" != .false);
- font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
-
- // Our link configs
- const links = try link.Set.fromConfig(
- alloc,
- config.link.links.items,
- );
-
- const cursor_invert = config.@"cursor-invert-fg-bg";
-
- return .{
- .background_opacity = @max(0, @min(1, config.@"background-opacity")),
- .font_thicken = config.@"font-thicken",
- .font_thicken_strength = config.@"font-thicken-strength",
- .font_features = font_features.list,
- .font_styles = font_styles,
-
- .cursor_color = if (!cursor_invert and config.@"cursor-color" != null)
- config.@"cursor-color".?.toTerminalRGB()
- else
- null,
-
- .cursor_invert = cursor_invert,
-
- .cursor_text = if (config.@"cursor-text") |txt|
- txt.toTerminalRGB()
- else
- null,
-
- .cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")),
-
- .background = config.background.toTerminalRGB(),
- .foreground = config.foreground.toTerminalRGB(),
- .invert_selection_fg_bg = config.@"selection-invert-fg-bg",
- .bold_is_bright = config.@"bold-is-bright",
- .min_contrast = @floatCast(config.@"minimum-contrast"),
- .padding_color = config.@"window-padding-color",
-
- .selection_background = if (config.@"selection-background") |bg|
- bg.toTerminalRGB()
- else
- null,
-
- .selection_foreground = if (config.@"selection-foreground") |bg|
- bg.toTerminalRGB()
- else
- null,
-
- .custom_shaders = custom_shaders,
- .links = links,
-
- .arena = arena,
- };
- }
-
- pub fn deinit(self: *DerivedConfig) void {
- const alloc = self.arena.allocator();
- self.links.deinit(alloc);
- self.arena.deinit();
- }
-};
-
-pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
- // Create the initial font shaper
- var shaper = try font.Shaper.init(alloc, .{
- .features = options.config.font_features.items,
- });
- errdefer shaper.deinit();
-
- // For the remainder of the setup we lock our font grid data because
- // we're reading it.
- const grid = options.font_grid;
- grid.lock.lockShared();
- defer grid.lock.unlockShared();
-
- var gl_state = try GLState.init(alloc, options.config, grid);
- errdefer gl_state.deinit();
-
- return OpenGL{
+/// NOTE: This is an error{}!OpenGL instead of just OpenGL for parity with
+/// Metal, since it needs to be fallible so does this, even though it
+/// can't actually fail.
+pub fn init(alloc: Allocator, opts: rendererpkg.Options) error{}!OpenGL {
+ return .{
.alloc = alloc,
- .config = options.config,
- .cells_bg = .{},
- .cells = .{},
- .grid_metrics = grid.metrics,
- .size = options.size,
- .gl_state = gl_state,
- .font_grid = grid,
- .font_shaper = shaper,
- .font_shaper_cache = font.ShaperCache.init(),
- .draw_background = options.config.background,
- .focused = true,
- .foreground_color = null,
- .default_foreground_color = options.config.foreground,
- .background_color = null,
- .default_background_color = options.config.background,
- .cursor_color = null,
- .default_cursor_color = options.config.cursor_color,
- .cursor_invert = options.config.cursor_invert,
- .surface_mailbox = options.surface_mailbox,
- .deferred_font_size = .{ .metrics = grid.metrics },
- .deferred_config = .{},
+ .blending = opts.config.blending,
};
}
pub fn deinit(self: *OpenGL) void {
- self.font_shaper.deinit();
- self.font_shaper_cache.deinit(self.alloc);
-
- {
- var it = self.images.iterator();
- while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc);
- self.images.deinit(self.alloc);
- }
- self.image_placements.deinit(self.alloc);
-
- if (self.gl_state) |*v| v.deinit(self.alloc);
-
- self.cells.deinit(self.alloc);
- self.cells_bg.deinit(self.alloc);
-
- self.config.deinit();
-
self.* = undefined;
}
/// Returns the hints that we want for this
pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints {
+ _ = config;
return .{
- .context_version_major = 3,
- .context_version_minor = 3,
+ .context_version_major = MIN_VERSION_MAJOR,
+ .context_version_minor = MIN_VERSION_MINOR,
.opengl_profile = .opengl_core_profile,
.opengl_forward_compat = true,
- .cocoa_graphics_switching = builtin.os.tag == .macos,
- .cocoa_retina_framebuffer = true,
- .transparent_framebuffer = config.@"background-opacity" < 1,
+ .transparent_framebuffer = true,
};
}
+/// 32-bit windows cross-compilation breaks with `.c` for some reason, so...
+const gl_debug_proc_callconv =
+ @typeInfo(
+ @typeInfo(
+ @typeInfo(
+ gl.c.GLDEBUGPROC,
+ ).optional.child,
+ ).pointer.child,
+ ).@"fn".calling_convention;
+
+fn glDebugMessageCallback(
+ src: gl.c.GLenum,
+ typ: gl.c.GLenum,
+ id: gl.c.GLuint,
+ severity: gl.c.GLenum,
+ len: gl.c.GLsizei,
+ msg: [*c]const gl.c.GLchar,
+ user_param: ?*const anyopaque,
+) callconv(gl_debug_proc_callconv) void {
+ _ = user_param;
+
+ const src_str: []const u8 = switch (src) {
+ gl.c.GL_DEBUG_SOURCE_API => "OpenGL API",
+ gl.c.GL_DEBUG_SOURCE_WINDOW_SYSTEM => "Window System",
+ gl.c.GL_DEBUG_SOURCE_SHADER_COMPILER => "Shader Compiler",
+ gl.c.GL_DEBUG_SOURCE_THIRD_PARTY => "Third Party",
+ gl.c.GL_DEBUG_SOURCE_APPLICATION => "User",
+ gl.c.GL_DEBUG_SOURCE_OTHER => "Other",
+ else => "Unknown",
+ };
+
+ const typ_str: []const u8 = switch (typ) {
+ gl.c.GL_DEBUG_TYPE_ERROR => "Error",
+ gl.c.GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR => "Deprecated Behavior",
+ gl.c.GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR => "Undefined Behavior",
+ gl.c.GL_DEBUG_TYPE_PORTABILITY => "Portability Issue",
+ gl.c.GL_DEBUG_TYPE_PERFORMANCE => "Performance Issue",
+ gl.c.GL_DEBUG_TYPE_MARKER => "Marker",
+ gl.c.GL_DEBUG_TYPE_PUSH_GROUP => "Group Push",
+ gl.c.GL_DEBUG_TYPE_POP_GROUP => "Group Pop",
+ gl.c.GL_DEBUG_TYPE_OTHER => "Other",
+ else => "Unknown",
+ };
+
+ const msg_str = msg[0..@intCast(len)];
+
+ (switch (severity) {
+ gl.c.GL_DEBUG_SEVERITY_HIGH => log.err(
+ "[{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ gl.c.GL_DEBUG_SEVERITY_MEDIUM => log.warn(
+ "[{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ gl.c.GL_DEBUG_SEVERITY_LOW => log.info(
+ "[{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ gl.c.GL_DEBUG_SEVERITY_NOTIFICATION => log.debug(
+ "[{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ else => log.warn(
+ "UNKNOWN SEVERITY [{d}] ({s}: {s}) {s}",
+ .{ id, src_str, typ_str, msg_str },
+ ),
+ });
+}
+
+/// Prepares the provided GL context, loading it with glad.
+fn prepareContext(getProcAddress: anytype) !void {
+ const version = try gl.glad.load(getProcAddress);
+ const major = gl.glad.versionMajor(@intCast(version));
+ const minor = gl.glad.versionMinor(@intCast(version));
+ errdefer gl.glad.unload();
+ log.info("loaded OpenGL {}.{}", .{ major, minor });
+
+ // Enable debug output for the context.
+ try gl.enable(gl.c.GL_DEBUG_OUTPUT);
+
+ // Register our debug message callback with the OpenGL context.
+ gl.glad.context.DebugMessageCallback.?(glDebugMessageCallback, null);
+
+ // Enable SRGB framebuffer for linear blending support.
+ try gl.enable(gl.c.GL_FRAMEBUFFER_SRGB);
+
+ if (major < MIN_VERSION_MAJOR or
+ (major == MIN_VERSION_MAJOR and minor < MIN_VERSION_MINOR))
+ {
+ log.warn(
+ "OpenGL version is too old. Ghostty requires OpenGL {d}.{d}",
+ .{ MIN_VERSION_MAJOR, MIN_VERSION_MINOR },
+ );
+ return error.OpenGLOutdated;
+ }
+}
+
/// This is called early right after surface creation.
pub fn surfaceInit(surface: *apprt.Surface) !void {
// Treat this like a thread entry
@@ -455,20 +181,8 @@ pub fn surfaceInit(surface: *apprt.Surface) !void {
switch (apprt.runtime) {
else => @compileError("unsupported app runtime for OpenGL"),
- apprt.gtk => {
- // GTK uses global OpenGL context so we load from null.
- const version = try gl.glad.load(null);
- const major = gl.glad.versionMajor(@intCast(version));
- const minor = gl.glad.versionMinor(@intCast(version));
- errdefer gl.glad.unload();
- log.info("loaded OpenGL {}.{}", .{ major, minor });
-
- // We require at least OpenGL 3.3
- if (major < 3 or (major == 3 and minor < 3)) {
- log.warn("OpenGL version is too old. Ghostty requires OpenGL 3.3", .{});
- return error.OpenGLOutdated;
- }
- },
+ // GTK uses global OpenGL context so we load from null.
+ apprt.gtk => try prepareContext(null),
apprt.glfw => try self.threadEnter(surface),
@@ -489,69 +203,19 @@ pub fn surfaceInit(surface: *apprt.Surface) !void {
// }
}
-/// This is called just prior to spinning up the renderer thread for
-/// final main thread setup requirements.
+/// This is called just prior to spinning up the renderer
+/// thread for final main thread setup requirements.
pub fn finalizeSurfaceInit(self: *const OpenGL, surface: *apprt.Surface) !void {
_ = self;
_ = surface;
- // For GLFW, we grabbed the OpenGL context in surfaceInit and we
- // need to release it before we start the renderer thread.
+ // For GLFW, we grabbed the OpenGL context in surfaceInit and
+ // we need to release it before we start the renderer thread.
if (apprt.runtime == apprt.glfw) {
glfw.makeContextCurrent(null);
}
}
-/// Called when the OpenGL context is made invalid, so we need to free
-/// all previous resources and stop rendering.
-pub fn displayUnrealized(self: *OpenGL) void {
- if (single_threaded_draw) self.draw_mutex.lock();
- defer if (single_threaded_draw) self.draw_mutex.unlock();
-
- if (self.gl_state) |*v| {
- v.deinit(self.alloc);
- self.gl_state = null;
- }
-}
-
-/// Called when the OpenGL is ready to be initialized.
-pub fn displayRealize(self: *OpenGL) !void {
- if (single_threaded_draw) self.draw_mutex.lock();
- defer if (single_threaded_draw) self.draw_mutex.unlock();
-
- // Make our new state
- var gl_state = gl_state: {
- self.font_grid.lock.lockShared();
- defer self.font_grid.lock.unlockShared();
- break :gl_state try GLState.init(
- self.alloc,
- self.config,
- self.font_grid,
- );
- };
- errdefer gl_state.deinit();
-
- // Unrealize if we have to
- if (self.gl_state) |*v| v.deinit(self.alloc);
-
- // Set our new state
- self.gl_state = gl_state;
-
- // Make sure we invalidate all the fields so that we
- // reflush everything
- self.gl_cells_size = 0;
- self.gl_cells_written = 0;
- self.texture_grayscale_modified = 0;
- self.texture_color_modified = 0;
- self.texture_grayscale_resized = 0;
- self.texture_color_resized = 0;
-
- // We need to reset our uniforms
- self.deferred_screen_size = .{ .size = self.size };
- self.deferred_font_size = .{ .metrics = self.grid_metrics };
- self.deferred_config = .{};
-}
-
/// Callback called by renderer.Thread when it begins.
pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
_ = self;
@@ -568,22 +232,17 @@ pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void {
apprt.glfw => {
// We need to make the OpenGL context current. OpenGL requires
- // that a single thread own the a single OpenGL context (if any). This
- // ensures that the context switches over to our thread. Important:
- // the prior thread MUST have detached the context prior to calling
- // this entrypoint.
+ // that a single thread own the a single OpenGL context (if any).
+ // This ensures that the context switches over to our thread.
+ // Important: the prior thread MUST have detached the context
+ // prior to calling this entrypoint.
glfw.makeContextCurrent(surface.window);
errdefer glfw.makeContextCurrent(null);
glfw.swapInterval(1);
// Load OpenGL bindings. This API is context-aware so this sets
// a threadlocal context for these pointers.
- const version = try gl.glad.load(&glfw.getProcAddress);
- errdefer gl.glad.unload();
- log.info("loaded OpenGL {}.{}", .{
- gl.glad.versionMajor(@intCast(version)),
- gl.glad.versionMinor(@intCast(version)),
- });
+ try prepareContext(&glfw.getProcAddress);
},
apprt.embedded => {
@@ -617,2068 +276,166 @@ pub fn threadExit(self: *const OpenGL) void {
}
}
-/// True if our renderer has animations so that a higher frequency
-/// timer is used.
-pub fn hasAnimations(self: *const OpenGL) bool {
- const state = self.gl_state orelse return false;
- return state.custom != null;
-}
-
-/// See Metal
-pub fn hasVsync(self: *const OpenGL) bool {
+pub fn displayRealized(self: *const OpenGL) void {
_ = self;
- // OpenGL currently never has vsync
- return false;
-}
-
-/// See Metal.
-pub fn markDirty(self: *OpenGL) void {
- // Do nothing, we don't have dirty tracking yet.
- _ = self;
-}
-
-/// Callback when the focus changes for the terminal this is rendering.
-///
-/// Must be called on the render thread.
-pub fn setFocus(self: *OpenGL, focus: bool) !void {
- self.focused = focus;
-}
-
-/// Callback when the window is visible or occluded.
-///
-/// Must be called on the render thread.
-pub fn setVisible(self: *OpenGL, visible: bool) void {
- _ = self;
- _ = visible;
-}
-
-/// Set the new font grid.
-///
-/// Must be called on the render thread.
-pub fn setFontGrid(self: *OpenGL, grid: *font.SharedGrid) void {
- if (single_threaded_draw) self.draw_mutex.lock();
- defer if (single_threaded_draw) self.draw_mutex.unlock();
-
- // Reset our font grid
- self.font_grid = grid;
- self.grid_metrics = grid.metrics;
- self.texture_grayscale_modified = 0;
- self.texture_grayscale_resized = 0;
- self.texture_color_modified = 0;
- self.texture_color_resized = 0;
-
- // Reset our shaper cache. If our font changed (not just the size) then
- // the data in the shaper cache may be invalid and cannot be used, so we
- // always clear the cache just in case.
- const font_shaper_cache = font.ShaperCache.init();
- self.font_shaper_cache.deinit(self.alloc);
- self.font_shaper_cache = font_shaper_cache;
-
- // Update our screen size because the font grid can affect grid
- // metrics which update uniforms.
- self.deferred_screen_size = .{ .size = self.size };
-
- // Defer our GPU updates
- self.deferred_font_size = .{ .metrics = grid.metrics };
-}
-
-/// The primary render callback that is completely thread-safe.
-pub fn updateFrame(
- self: *OpenGL,
- surface: *apprt.Surface,
- state: *renderer.State,
- cursor_blink_visible: bool,
-) !void {
- _ = surface;
-
- // Data we extract out of the critical area.
- const Critical = struct {
- full_rebuild: bool,
- gl_bg: terminal.color.RGB,
- screen: terminal.Screen,
- screen_type: terminal.ScreenType,
- mouse: renderer.State.Mouse,
- preedit: ?renderer.State.Preedit,
- cursor_style: ?renderer.CursorStyle,
- color_palette: terminal.color.Palette,
- };
-
- // Update all our data as tightly as possible within the mutex.
- var critical: Critical = critical: {
- state.mutex.lock();
- defer state.mutex.unlock();
-
- // If we're in a synchronized output state, we pause all rendering.
- if (state.terminal.modes.get(.synchronized_output)) {
- log.debug("synchronized output started, skipping render", .{});
- return;
- }
-
- // Swap bg/fg if the terminal is reversed
- const bg = self.background_color orelse self.default_background_color;
- const fg = self.foreground_color orelse self.default_foreground_color;
- defer {
- if (self.background_color) |*c| {
- c.* = bg;
- } else {
- self.default_background_color = bg;
- }
-
- if (self.foreground_color) |*c| {
- c.* = fg;
- } else {
- self.default_foreground_color = fg;
- }
- }
-
- if (state.terminal.modes.get(.reverse_colors)) {
- if (self.background_color) |*c| {
- c.* = fg;
- } else {
- self.default_background_color = fg;
- }
-
- if (self.foreground_color) |*c| {
- c.* = bg;
- } else {
- self.default_foreground_color = bg;
- }
- }
-
- // Get the viewport pin so that we can compare it to the current.
- const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
-
- // We used to share terminal state, but we've since learned through
- // analysis that it is faster to copy the terminal state than to
- // hold the lock while rebuilding GPU cells.
- var screen_copy = try state.terminal.screen.clone(
- self.alloc,
- .{ .viewport = .{} },
- null,
- );
- errdefer screen_copy.deinit();
-
- // Whether to draw our cursor or not.
- const cursor_style = if (state.terminal.flags.password_input)
- .lock
- else
- renderer.cursorStyle(
- state,
- self.focused,
- cursor_blink_visible,
- );
-
- // Get our preedit state
- const preedit: ?renderer.State.Preedit = preedit: {
- if (cursor_style == null) break :preedit null;
- const p = state.preedit orelse break :preedit null;
- break :preedit try p.clone(self.alloc);
- };
- errdefer if (preedit) |p| p.deinit(self.alloc);
-
- // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path.
- // We only do this if the Kitty image state is dirty meaning only if
- // it changes.
- //
- // If we have any virtual references, we must also rebuild our
- // kitty state on every frame because any cell change can move
- // an image.
- if (state.terminal.screen.kitty_images.dirty or
- self.image_virtual)
- {
- // prepKittyGraphics touches self.images which is also used
- // in drawFrame so if we're drawing on a separate thread we need
- // to lock this.
- if (single_threaded_draw) self.draw_mutex.lock();
- defer if (single_threaded_draw) self.draw_mutex.unlock();
- try self.prepKittyGraphics(state.terminal);
- }
-
- // If we have any terminal dirty flags set then we need to rebuild
- // the entire screen. This can be optimized in the future.
- const full_rebuild: bool = rebuild: {
- {
- const Int = @typeInfo(terminal.Terminal.Dirty).@"struct".backing_integer.?;
- const v: Int = @bitCast(state.terminal.flags.dirty);
- if (v > 0) break :rebuild true;
- }
- {
- const Int = @typeInfo(terminal.Screen.Dirty).@"struct".backing_integer.?;
- const v: Int = @bitCast(state.terminal.screen.dirty);
- if (v > 0) break :rebuild true;
- }
-
- // If our viewport changed then we need to rebuild the entire
- // screen because it means we scrolled. If we have no previous
- // viewport then we must rebuild.
- const prev_viewport = self.cells_viewport orelse break :rebuild true;
- if (!prev_viewport.eql(viewport_pin)) break :rebuild true;
-
- break :rebuild false;
- };
-
- // Reset the dirty flags in the terminal and screen. We assume
- // that our rebuild will be successful since so we optimize for
- // success and reset while we hold the lock. This is much easier
- // than coordinating row by row or as changes are persisted.
- state.terminal.flags.dirty = .{};
- state.terminal.screen.dirty = .{};
- {
- var it = state.terminal.screen.pages.pageIterator(
- .right_down,
- .{ .screen = .{} },
- null,
- );
- while (it.next()) |chunk| {
- var dirty_set = chunk.node.data.dirtyBitSet();
- dirty_set.unsetAll();
- }
- }
-
- // Update our viewport pin for dirty tracking
- self.cells_viewport = viewport_pin;
-
- break :critical .{
- .full_rebuild = full_rebuild,
- .gl_bg = self.background_color orelse self.default_background_color,
- .screen = screen_copy,
- .screen_type = state.terminal.active_screen,
- .mouse = state.mouse,
- .preedit = preedit,
- .cursor_style = cursor_style,
- .color_palette = state.terminal.color_palette.colors,
- };
- };
- defer {
- critical.screen.deinit();
- if (critical.preedit) |p| p.deinit(self.alloc);
- }
-
- // Grab our draw mutex if we have it and update our data
- {
- if (single_threaded_draw) self.draw_mutex.lock();
- defer if (single_threaded_draw) self.draw_mutex.unlock();
-
- // Set our draw data
- self.draw_background = critical.gl_bg;
-
- // Build our GPU cells
- try self.rebuildCells(
- critical.full_rebuild,
- &critical.screen,
- critical.screen_type,
- critical.mouse,
- critical.preedit,
- critical.cursor_style,
- &critical.color_palette,
- );
-
- // Notify our shaper we're done for the frame. For some shapers like
- // CoreText this triggers off-thread cleanup logic.
- self.font_shaper.endFrame();
- }
-}
-
-/// This goes through the Kitty graphic placements and accumulates the
-/// placements we need to render on our viewport. It also ensures that
-/// the visible images are loaded on the GPU.
-fn prepKittyGraphics(
- self: *OpenGL,
- t: *terminal.Terminal,
-) !void {
- const storage = &t.screen.kitty_images;
- defer storage.dirty = false;
-
- // We always clear our previous placements no matter what because
- // we rebuild them from scratch.
- self.image_placements.clearRetainingCapacity();
- self.image_virtual = false;
-
- // Go through our known images and if there are any that are no longer
- // in use then mark them to be freed.
- //
- // This never conflicts with the below because a placement can't
- // reference an image that doesn't exist.
- {
- var it = self.images.iterator();
- while (it.next()) |kv| {
- if (storage.imageById(kv.key_ptr.*) == null) {
- kv.value_ptr.image.markForUnload();
- }
- }
- }
-
- // The top-left and bottom-right corners of our viewport in screen
- // points. This lets us determine offsets and containment of placements.
- const top = t.screen.pages.getTopLeft(.viewport);
- const bot = t.screen.pages.getBottomRight(.viewport).?;
- const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y;
- const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y;
-
- // Go through the placements and ensure the image is loaded on the GPU.
- var it = storage.placements.iterator();
- while (it.next()) |kv| {
- // Find the image in storage
- const p = kv.value_ptr;
-
- // Special logic based on location
- switch (p.location) {
- .pin => {},
- .virtual => {
- // We need to mark virtual placements on our renderer so that
- // we know to rebuild in more scenarios since cell changes can
- // now trigger placement changes.
- self.image_virtual = true;
-
- // We also continue out because virtual placements are
- // only triggered by the unicode placeholder, not by the
- // placement itself.
- continue;
- },
- }
-
- const image = storage.imageById(kv.key_ptr.image_id) orelse {
- log.warn(
- "missing image for placement, ignoring image_id={}",
- .{kv.key_ptr.image_id},
- );
- continue;
- };
-
- try self.prepKittyPlacement(t, top_y, bot_y, &image, p);
- }
-
- // If we have virtual placements then we need to scan for placeholders.
- if (self.image_virtual) {
- var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot);
- while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement(
- t,
- &virtual_p,
- );
- }
-
- // Sort the placements by their Z value.
- std.mem.sortUnstable(
- gl_image.Placement,
- self.image_placements.items,
- {},
- struct {
- fn lessThan(
- ctx: void,
- lhs: gl_image.Placement,
- rhs: gl_image.Placement,
- ) bool {
- _ = ctx;
- return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id);
- }
- }.lessThan,
- );
-
- // Find our indices. The values are sorted by z so we can find the
- // first placement out of bounds to find the limits.
- var bg_end: ?u32 = null;
- var text_end: ?u32 = null;
- const bg_limit = std.math.minInt(i32) / 2;
- for (self.image_placements.items, 0..) |p, i| {
- if (bg_end == null and p.z >= bg_limit) {
- bg_end = @intCast(i);
- }
- if (text_end == null and p.z >= 0) {
- text_end = @intCast(i);
- }
- }
-
- self.image_bg_end = bg_end orelse 0;
- self.image_text_end = text_end orelse self.image_bg_end;
-}
-
-fn prepKittyVirtualPlacement(
- self: *OpenGL,
- t: *terminal.Terminal,
- p: *const terminal.kitty.graphics.unicode.Placement,
-) !void {
- const storage = &t.screen.kitty_images;
- const image = storage.imageById(p.image_id) orelse {
- log.warn(
- "missing image for virtual placement, ignoring image_id={}",
- .{p.image_id},
- );
- return;
- };
-
- const rp = p.renderPlacement(
- storage,
- &image,
- self.grid_metrics.cell_width,
- self.grid_metrics.cell_height,
- ) catch |err| {
- log.warn("error rendering virtual placement err={}", .{err});
- return;
- };
-
- // If our placement is zero sized then we don't do anything.
- if (rp.dest_width == 0 or rp.dest_height == 0) return;
-
- const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
- .viewport,
- rp.top_left,
- ) orelse {
- // This is unreachable with virtual placements because we should
- // only ever be looking at virtual placements that are in our
- // viewport in the renderer and virtual placements only ever take
- // up one row.
- unreachable;
- };
-
- // Send our image to the GPU and store the placement for rendering.
- try self.prepKittyImage(&image);
- try self.image_placements.append(self.alloc, .{
- .image_id = image.id,
- .x = @intCast(rp.top_left.x),
- .y = @intCast(viewport.viewport.y),
- .z = -1,
- .width = rp.dest_width,
- .height = rp.dest_height,
- .cell_offset_x = rp.offset_x,
- .cell_offset_y = rp.offset_y,
- .source_x = rp.source_x,
- .source_y = rp.source_y,
- .source_width = rp.source_width,
- .source_height = rp.source_height,
- });
-}
-
-fn prepKittyPlacement(
- self: *OpenGL,
- t: *terminal.Terminal,
- top_y: u32,
- bot_y: u32,
- image: *const terminal.kitty.graphics.Image,
- p: *const terminal.kitty.graphics.ImageStorage.Placement,
-) !void {
- // Get the rect for the placement. If this placement doesn't have
- // a rect then its virtual or something so skip it.
- const rect = p.rect(image.*, t) orelse return;
-
- // This is expensive but necessary.
- const img_top_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
- const img_bot_y = t.screen.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y;
-
- // If the selection isn't within our viewport then skip it.
- if (img_top_y > bot_y) return;
- if (img_bot_y < top_y) return;
-
- // We need to prep this image for upload if it isn't in the cache OR
- // it is in the cache but the transmit time doesn't match meaning this
- // image is different.
- try self.prepKittyImage(image);
-
- // Calculate the dimensions of our image, taking in to
- // account the rows / columns specified by the placement.
- const dest_size = p.calculatedSize(image.*, t);
-
- // Calculate the source rectangle
- const source_x = @min(image.width, p.source_x);
- const source_y = @min(image.height, p.source_y);
- const source_width = if (p.source_width > 0)
- @min(image.width - source_x, p.source_width)
- else
- image.width;
- const source_height = if (p.source_height > 0)
- @min(image.height - source_y, p.source_height)
- else
- image.height;
-
- // Get the viewport-relative Y position of the placement.
- const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y));
-
- // Accumulate the placement
- if (dest_size.width > 0 and dest_size.height > 0) {
- try self.image_placements.append(self.alloc, .{
- .image_id = image.id,
- .x = @intCast(rect.top_left.x),
- .y = y_pos,
- .z = p.z,
- .width = dest_size.width,
- .height = dest_size.height,
- .cell_offset_x = p.x_offset,
- .cell_offset_y = p.y_offset,
- .source_x = source_x,
- .source_y = source_y,
- .source_width = source_width,
- .source_height = source_height,
- });
- }
-}
-
-fn prepKittyImage(
- self: *OpenGL,
- image: *const terminal.kitty.graphics.Image,
-) !void {
- // We need to prep this image for upload if it isn't in the cache OR
- // it is in the cache but the transmit time doesn't match meaning this
- // image is different.
- const gop = try self.images.getOrPut(self.alloc, image.id);
- if (gop.found_existing and
- gop.value_ptr.transmit_time.order(image.transmit_time) == .eq)
- {
- return;
- }
-
- // Copy the data into the pending state.
- const data = try self.alloc.dupe(u8, image.data);
- errdefer self.alloc.free(data);
-
- // Store it in the map
- const pending: Image.Pending = .{
- .width = image.width,
- .height = image.height,
- .data = data.ptr,
- };
-
- const new_image: Image = switch (image.format) {
- .gray => .{ .pending_gray = pending },
- .gray_alpha => .{ .pending_gray_alpha = pending },
- .rgb => .{ .pending_rgb = pending },
- .rgba => .{ .pending_rgba = pending },
- .png => unreachable, // should be decoded by now
- };
-
- if (!gop.found_existing) {
- gop.value_ptr.* = .{
- .image = new_image,
- .transmit_time = undefined,
- };
- } else {
- try gop.value_ptr.image.markForReplace(
- self.alloc,
- new_image,
- );
- }
-
- gop.value_ptr.transmit_time = image.transmit_time;
-}
-
-/// rebuildCells rebuilds all the GPU cells from our CPU state. This is a
-/// slow operation but ensures that the GPU state exactly matches the CPU state.
-/// In steady-state operation, we use some GPU tricks to send down stale data
-/// that is ignored. This accumulates more memory; rebuildCells clears it.
-///
-/// Note this doesn't have to typically be manually called. Internally,
-/// the renderer will do this when it needs more memory space.
-pub fn rebuildCells(
- self: *OpenGL,
- rebuild: bool,
- screen: *terminal.Screen,
- screen_type: terminal.ScreenType,
- mouse: renderer.State.Mouse,
- preedit: ?renderer.State.Preedit,
- cursor_style_: ?renderer.CursorStyle,
- color_palette: *const terminal.color.Palette,
-) !void {
- _ = screen_type;
-
- // Bg cells at most will need space for the visible screen size
- self.cells_bg.clearRetainingCapacity();
- self.cells.clearRetainingCapacity();
-
- // Create an arena for all our temporary allocations while rebuilding
- var arena = ArenaAllocator.init(self.alloc);
- defer arena.deinit();
- const arena_alloc = arena.allocator();
-
- // We've written no data to the GPU, refresh it all
- self.gl_cells_written = 0;
-
- // Create our match set for the links.
- var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet(
- arena_alloc,
- screen,
- mouse_pt,
- mouse.mods,
- ) else .{};
-
- // Determine our x/y range for preedit. We don't want to render anything
- // here because we will render the preedit separately.
- const preedit_range: ?struct {
- y: terminal.size.CellCountInt,
- x: [2]terminal.size.CellCountInt,
- cp_offset: usize,
- } = if (preedit) |preedit_v| preedit: {
- const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1);
- break :preedit .{
- .y = screen.cursor.y,
- .x = .{ range.start, range.end },
- .cp_offset = range.cp_offset,
- };
- } else null;
-
- // These are all the foreground cells underneath the cursor.
- //
- // We keep track of these so that we can invert the colors and move them
- // in front of the block cursor so that the character remains visible.
- //
- // We init with a capacity of 4 to account for decorations such
- // as underline and strikethrough, as well as combining chars.
- var cursor_cells = try std.ArrayListUnmanaged(CellProgram.Cell).initCapacity(arena_alloc, 4);
- defer cursor_cells.deinit(arena_alloc);
-
- if (rebuild) {
- switch (self.config.padding_color) {
- .background => {},
-
- .extend, .@"extend-always" => {
- self.padding_extend_top = true;
- self.padding_extend_bottom = true;
- },
- }
- }
-
- const grid_size = self.size.grid();
-
- // We rebuild the cells row-by-row because we do font shaping by row.
- var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
- // If our cell contents buffer is shorter than the screen viewport,
- // we render the rows that fit, starting from the bottom. If instead
- // the viewport is shorter than the cell contents buffer, we align
- // the top of the viewport with the top of the contents buffer.
- var y: terminal.size.CellCountInt = @min(
- screen.pages.rows,
- grid_size.rows,
- );
- while (row_it.next()) |row| {
- // The viewport may have more rows than our cell contents,
- // so we need to break from the loop early if we hit y = 0.
- if (y == 0) break;
-
- y -= 1;
-
- // True if we want to do font shaping around the cursor. We want to
- // do font shaping as long as the cursor is enabled.
- const shape_cursor = screen.viewportIsBottom() and
- y == screen.cursor.y;
-
- // If this is the row with our cursor, then we may have to modify
- // the cell with the cursor.
- const start_i: usize = self.cells.items.len;
- defer if (shape_cursor and cursor_style_ == .block) {
- const x = screen.cursor.x;
- const wide = row.cells(.all)[x].wide;
- const min_x = switch (wide) {
- .narrow, .spacer_head, .wide => x,
- .spacer_tail => x -| 1,
- };
- const max_x = switch (wide) {
- .narrow, .spacer_head, .spacer_tail => x,
- .wide => x +| 1,
- };
- for (self.cells.items[start_i..]) |cell| {
- if (cell.grid_col < min_x or cell.grid_col > max_x) continue;
- if (cell.mode.isFg()) {
- cursor_cells.append(arena_alloc, cell) catch {
- // We silently ignore if this fails because
- // worst case scenario some combining glyphs
- // aren't visible under the cursor '\_('-')_/'
- };
- }
- }
- };
-
- // We need to get this row's selection if there is one for proper
- // run splitting.
- const row_selection = sel: {
- const sel = screen.selection orelse break :sel null;
- const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse
- break :sel null;
- break :sel sel.containedRow(screen, pin) orelse null;
- };
-
- // On primary screen, we still apply vertical padding extension
- // under certain conditions we feel are safe. This helps make some
- // scenarios look better while avoiding scenarios we know do NOT look
- // good.
- switch (self.config.padding_color) {
- // These already have the correct values set above.
- .background, .@"extend-always" => {},
-
- // Apply heuristics for padding extension.
- .extend => if (y == 0) {
- self.padding_extend_top = !row.neverExtendBg(
- color_palette,
- self.background_color orelse self.default_background_color,
- );
- } else if (y == self.size.grid().rows - 1) {
- self.padding_extend_bottom = !row.neverExtendBg(
- color_palette,
- self.background_color orelse self.default_background_color,
- );
- },
- }
-
- // Iterator of runs for shaping.
- var run_iter = self.font_shaper.runIterator(
- self.font_grid,
- screen,
- row,
- row_selection,
- if (shape_cursor) screen.cursor.x else null,
- );
- var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc);
- var shaper_cells: ?[]const font.shape.Cell = null;
- var shaper_cells_i: usize = 0;
-
- const row_cells_all = row.cells(.all);
-
- // If our viewport is wider than our cell contents buffer,
- // we still only process cells up to the width of the buffer.
- const row_cells = row_cells_all[0..@min(row_cells_all.len, grid_size.columns)];
-
- for (row_cells, 0..) |*cell, x| {
- // If this cell falls within our preedit range then we
- // skip this because preedits are setup separately.
- if (preedit_range) |range| preedit: {
- // We're not on the preedit line, no actions necessary.
- if (range.y != y) break :preedit;
- // We're before the preedit range, no actions necessary.
- if (x < range.x[0]) break :preedit;
- // We're in the preedit range, skip this cell.
- if (x <= range.x[1]) continue;
- // After exiting the preedit range we need to catch
- // the run position up because of the missed cells.
- // In all other cases, no action is necessary.
- if (x != range.x[1] + 1) break :preedit;
-
- // Step the run iterator until we find a run that ends
- // after the current cell, which will be the soonest run
- // that might contain glyphs for our cell.
- while (shaper_run) |run| {
- if (run.offset + run.cells > x) break;
- shaper_run = try run_iter.next(self.alloc);
- shaper_cells = null;
- shaper_cells_i = 0;
- }
-
- const run = shaper_run orelse break :preedit;
-
- // If we haven't shaped this run, do so now.
- shaper_cells = shaper_cells orelse
- // Try to read the cells from the shaping cache if we can.
- self.font_shaper_cache.get(run) orelse
- cache: {
- // Otherwise we have to shape them.
- const cells = try self.font_shaper.shape(run);
-
- // Try to cache them. If caching fails for any reason we
- // continue because it is just a performance optimization,
- // not a correctness issue.
- self.font_shaper_cache.put(
- self.alloc,
- run,
- cells,
- ) catch |err| {
- log.warn(
- "error caching font shaping results err={}",
- .{err},
- );
- };
-
- // The cells we get from direct shaping are always owned
- // by the shaper and valid until the next shaping call so
- // we can safely use them.
- break :cache cells;
- };
-
- // Advance our index until we reach or pass
- // our current x position in the shaper cells.
- while (shaper_cells.?[shaper_cells_i].x < x) {
- shaper_cells_i += 1;
- }
- }
-
- const wide = cell.wide;
-
- const style = row.style(cell);
-
- const cell_pin: terminal.Pin = cell: {
- var copy = row;
- copy.x = @intCast(x);
- break :cell copy;
- };
-
- // True if this cell is selected
- const selected: bool = if (screen.selection) |sel|
- sel.contains(screen, .{
- .node = row.node,
- .y = row.y,
- .x = @intCast(
- // Spacer tails should show the selection
- // state of the wide cell they belong to.
- if (wide == .spacer_tail)
- x -| 1
- else
- x,
- ),
- })
- else
- false;
-
- const bg_style = style.bg(cell, color_palette);
- const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color;
-
- // The final background color for the cell.
- const bg = bg: {
- if (selected) {
- break :bg if (self.config.invert_selection_fg_bg)
- if (style.flags.inverse)
- // Cell is selected with invert selection fg/bg
- // enabled, and the cell has the inverse style
- // flag, so they cancel out and we get the normal
- // bg color.
- bg_style
- else
- // If it doesn't have the inverse style
- // flag then we use the fg color instead.
- fg_style
- else
- // If we don't have invert selection fg/bg set then we
- // just use the selection background if set, otherwise
- // the default fg color.
- break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color;
- }
-
- // Not selected
- break :bg if (style.flags.inverse != isCovering(cell.codepoint()))
- // Two cases cause us to invert (use the fg color as the bg)
- // - The "inverse" style flag.
- // - A "covering" glyph; we use fg for bg in that case to
- // help make sure that padding extension works correctly.
- // If one of these is true (but not the other)
- // then we use the fg style color for the bg.
- fg_style
- else
- // Otherwise they cancel out.
- bg_style;
- };
-
- const fg = fg: {
- if (selected and !self.config.invert_selection_fg_bg) {
- // If we don't have invert selection fg/bg set
- // then we just use the selection foreground if
- // set, otherwise the default bg color.
- break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color;
- }
-
- // Whether we need to use the bg color as our fg color:
- // - Cell is inverted and not selected
- // - Cell is selected and not inverted
- // Note: if selected then invert sel fg / bg must be
- // false since we separately handle it if true above.
- break :fg if (style.flags.inverse != selected)
- bg_style orelse self.background_color orelse self.default_background_color
- else
- fg_style;
- };
-
- // Foreground alpha for this cell.
- const alpha: u8 = if (style.flags.faint) 175 else 255;
-
- // If the cell has a background color, set it.
- const bg_color: [4]u8 = if (bg) |rgb| bg: {
- // Determine our background alpha. If we have transparency configured
- // then this is dynamic depending on some situations. This is all
- // in an attempt to make transparency look the best for various
- // situations. See inline comments.
- const bg_alpha: u8 = bg_alpha: {
- const default: u8 = 255;
-
- if (self.config.background_opacity >= 1) break :bg_alpha default;
-
- // If we're selected, we do not apply background opacity
- if (selected) break :bg_alpha default;
-
- // If we're reversed, do not apply background opacity
- if (style.flags.inverse) break :bg_alpha default;
-
- // If we have a background and its not the default background
- // then we apply background opacity
- if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color orelse self.default_background_color)) {
- break :bg_alpha default;
- }
-
- // We apply background opacity.
- var bg_alpha: f64 = @floatFromInt(default);
- bg_alpha *= self.config.background_opacity;
- bg_alpha = @ceil(bg_alpha);
- break :bg_alpha @intFromFloat(bg_alpha);
- };
-
- try self.cells_bg.append(self.alloc, .{
- .mode = .bg,
- .grid_col = @intCast(x),
- .grid_row = @intCast(y),
- .grid_width = cell.gridWidth(),
- .glyph_x = 0,
- .glyph_y = 0,
- .glyph_width = 0,
- .glyph_height = 0,
- .glyph_offset_x = 0,
- .glyph_offset_y = 0,
- .r = rgb.r,
- .g = rgb.g,
- .b = rgb.b,
- .a = bg_alpha,
- .bg_r = 0,
- .bg_g = 0,
- .bg_b = 0,
- .bg_a = 0,
- });
-
- break :bg .{
- rgb.r, rgb.g, rgb.b, bg_alpha,
- };
- } else .{
- self.draw_background.r,
- self.draw_background.g,
- self.draw_background.b,
- @intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))),
- };
-
- // If the invisible flag is set on this cell then we
- // don't need to render any foreground elements, so
- // we just skip all glyphs with this x coordinate.
- //
- // NOTE: This behavior matches xterm. Some other terminal
- // emulators, e.g. Alacritty, still render text decorations
- // and only make the text itself invisible. The decision
- // has been made here to match xterm's behavior for this.
- if (style.flags.invisible) {
- continue;
- }
-
- // Give links a single underline, unless they already have
- // an underline, in which case use a double underline to
- // distinguish them.
- const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin))
- if (style.flags.underline == .single)
- .double
- else
- .single
- else
- style.flags.underline;
-
- // We draw underlines first so that they layer underneath text.
- // This improves readability when a colored underline is used
- // which intersects parts of the text (descenders).
- if (underline != .none) self.addUnderline(
- @intCast(x),
- @intCast(y),
- underline,
- style.underlineColor(color_palette) orelse fg,
- alpha,
- bg_color,
- ) catch |err| {
- log.warn(
- "error adding underline to cell, will be invalid x={} y={}, err={}",
- .{ x, y, err },
- );
- };
-
- if (style.flags.overline) self.addOverline(
- @intCast(x),
- @intCast(y),
- fg,
- alpha,
- bg_color,
- ) catch |err| {
- log.warn(
- "error adding overline to cell, will be invalid x={} y={}, err={}",
- .{ x, y, err },
- );
- };
-
- // If we're at or past the end of our shaper run then
- // we need to get the next run from the run iterator.
- if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) {
- shaper_run = try run_iter.next(self.alloc);
- shaper_cells = null;
- shaper_cells_i = 0;
- }
-
- if (shaper_run) |run| glyphs: {
- // If we haven't shaped this run yet, do so.
- shaper_cells = shaper_cells orelse
- // Try to read the cells from the shaping cache if we can.
- self.font_shaper_cache.get(run) orelse
- cache: {
- // Otherwise we have to shape them.
- const cells = try self.font_shaper.shape(run);
-
- // Try to cache them. If caching fails for any reason we
- // continue because it is just a performance optimization,
- // not a correctness issue.
- self.font_shaper_cache.put(
- self.alloc,
- run,
- cells,
- ) catch |err| {
- log.warn(
- "error caching font shaping results err={}",
- .{err},
- );
- };
-
- // The cells we get from direct shaping are always owned
- // by the shaper and valid until the next shaping call so
- // we can safely use them.
- break :cache cells;
- };
-
- const cells = shaper_cells orelse break :glyphs;
-
- // If there are no shaper cells for this run, ignore it.
- // This can occur for runs of empty cells, and is fine.
- if (cells.len == 0) break :glyphs;
-
- // If we encounter a shaper cell to the left of the current
- // cell then we have some problems. This logic relies on x
- // position monotonically increasing.
- assert(cells[shaper_cells_i].x >= x);
-
- // NOTE: An assumption is made here that a single cell will never
- // be present in more than one shaper run. If that assumption is
- // violated, this logic breaks.
-
- while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({
- shaper_cells_i += 1;
- }) {
- self.addGlyph(
- @intCast(x),
- @intCast(y),
- cell_pin,
- cells[shaper_cells_i],
- shaper_run.?,
- fg,
- alpha,
- bg_color,
- ) catch |err| {
- log.warn(
- "error adding glyph to cell, will be invalid x={} y={}, err={}",
- .{ x, y, err },
- );
- };
- }
- }
-
- // Finally, draw a strikethrough if necessary.
- if (style.flags.strikethrough) self.addStrikethrough(
- @intCast(x),
- @intCast(y),
- fg,
- alpha,
- bg_color,
- ) catch |err| {
- log.warn(
- "error adding strikethrough to cell, will be invalid x={} y={}, err={}",
- .{ x, y, err },
- );
- };
- }
- }
-
- // Add the cursor at the end so that it overlays everything. If we have
- // a cursor cell then we invert the colors on that and add it in so
- // that we can always see it.
- if (cursor_style_) |cursor_style| cursor_style: {
- // If we have a preedit, we try to render the preedit text on top
- // of the cursor.
- if (preedit) |preedit_v| {
- const range = preedit_range.?;
- var x = range.x[0];
- for (preedit_v.codepoints[range.cp_offset..]) |cp| {
- self.addPreeditCell(cp, x, range.y) catch |err| {
- log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{
- x,
- range.y,
- err,
- });
- };
-
- x += if (cp.wide) 2 else 1;
- }
-
- // Preedit hides the cursor
- break :cursor_style;
- }
-
- const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: {
- if (self.cursor_invert) {
- // Use the foreground color from the cell under the cursor, if any.
- const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
- break :color if (sty.flags.inverse)
- // If the cell is reversed, use background color instead.
- (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color)
- else
- (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color);
- } else {
- break :color self.foreground_color orelse self.default_foreground_color;
- }
- };
-
- _ = try self.addCursor(screen, cursor_style, cursor_color);
- for (cursor_cells.items) |*cell| {
- if (cell.mode.isFg() and cell.mode != .fg_color) {
- const cell_color = if (self.cursor_invert) blk: {
- // Use the background color from the cell under the cursor, if any.
- const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
- break :blk if (sty.flags.inverse)
- // If the cell is reversed, use foreground color instead.
- (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color)
- else
- (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color);
- } else if (self.config.cursor_text) |txt|
- txt
- else
- self.background_color orelse self.default_background_color;
-
- cell.r = cell_color.r;
- cell.g = cell_color.g;
- cell.b = cell_color.b;
- cell.a = 255;
- }
- try self.cells.append(self.alloc, cell.*);
- }
- }
-
- // Free up memory, generally in case where surface has shrunk.
- // If more than half of the capacity is unused, remove all unused capacity.
- if (self.cells.items.len * 2 < self.cells.capacity) {
- self.cells.shrinkAndFree(self.alloc, self.cells.items.len);
- }
- if (self.cells_bg.items.len * 2 < self.cells_bg.capacity) {
- self.cells_bg.shrinkAndFree(self.alloc, self.cells_bg.items.len);
- }
-
- // Some debug mode safety checks
- if (std.debug.runtime_safety) {
- for (self.cells_bg.items) |cell| assert(cell.mode == .bg);
- for (self.cells.items) |cell| assert(cell.mode != .bg);
- }
-}
-
-fn addPreeditCell(
- self: *OpenGL,
- cp: renderer.State.Preedit.Codepoint,
- x: usize,
- y: usize,
-) !void {
- // Preedit is rendered inverted
- const bg = self.foreground_color orelse self.default_foreground_color;
- const fg = self.background_color orelse self.default_background_color;
-
- // Render the glyph for our preedit text
- const render_ = self.font_grid.renderCodepoint(
- self.alloc,
- @intCast(cp.codepoint),
- .regular,
- .text,
- .{ .grid_metrics = self.grid_metrics },
- ) catch |err| {
- log.warn("error rendering preedit glyph err={}", .{err});
- return;
- };
- const render = render_ orelse {
- log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint});
- return;
- };
-
- // Add our opaque background cell
- try self.cells_bg.append(self.alloc, .{
- .mode = .bg,
- .grid_col = @intCast(x),
- .grid_row = @intCast(y),
- .grid_width = if (cp.wide) 2 else 1,
- .glyph_x = 0,
- .glyph_y = 0,
- .glyph_width = 0,
- .glyph_height = 0,
- .glyph_offset_x = 0,
- .glyph_offset_y = 0,
- .r = bg.r,
- .g = bg.g,
- .b = bg.b,
- .a = 255,
- .bg_r = 0,
- .bg_g = 0,
- .bg_b = 0,
- .bg_a = 0,
- });
-
- // Add our text
- try self.cells.append(self.alloc, .{
- .mode = .fg,
- .grid_col = @intCast(x),
- .grid_row = @intCast(y),
- .grid_width = if (cp.wide) 2 else 1,
- .glyph_x = render.glyph.atlas_x,
- .glyph_y = render.glyph.atlas_y,
- .glyph_width = render.glyph.width,
- .glyph_height = render.glyph.height,
- .glyph_offset_x = render.glyph.offset_x,
- .glyph_offset_y = render.glyph.offset_y,
- .r = fg.r,
- .g = fg.g,
- .b = fg.b,
- .a = 255,
- .bg_r = bg.r,
- .bg_g = bg.g,
- .bg_b = bg.b,
- .bg_a = 255,
- });
-}
-
-fn addCursor(
- self: *OpenGL,
- screen: *terminal.Screen,
- cursor_style: renderer.CursorStyle,
- cursor_color: terminal.color.RGB,
-) !?*const CellProgram.Cell {
- // Add the cursor. We render the cursor over the wide character if
- // we're on the wide character tail.
- const wide, const x = cell: {
- // The cursor goes over the screen cursor position.
- const cell = screen.cursor.page_cell;
- if (cell.wide != .spacer_tail or screen.cursor.x == 0)
- break :cell .{ cell.wide == .wide, screen.cursor.x };
-
- // If we're part of a wide character, we move the cursor back to
- // the actual character.
- const prev_cell = screen.cursorCellLeft(1);
- break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 };
- };
-
- const alpha: u8 = if (!self.focused) 255 else alpha: {
- const alpha = 255 * self.config.cursor_opacity;
- break :alpha @intFromFloat(@ceil(alpha));
- };
-
- const render = switch (cursor_style) {
- .block,
- .block_hollow,
- .bar,
- .underline,
- => render: {
- const sprite: font.Sprite = switch (cursor_style) {
- .block => .cursor_rect,
- .block_hollow => .cursor_hollow_rect,
- .bar => .cursor_bar,
- .underline => .underline,
- .lock => unreachable,
- };
-
- break :render self.font_grid.renderGlyph(
- self.alloc,
- font.sprite_index,
- @intFromEnum(sprite),
- .{
- .cell_width = if (wide) 2 else 1,
- .grid_metrics = self.grid_metrics,
- },
- ) catch |err| {
- log.warn("error rendering cursor glyph err={}", .{err});
- return null;
- };
- },
-
- .lock => self.font_grid.renderCodepoint(
- self.alloc,
- 0xF023, // lock symbol
- .regular,
- .text,
- .{
- .cell_width = if (wide) 2 else 1,
- .grid_metrics = self.grid_metrics,
- },
- ) catch |err| {
- log.warn("error rendering cursor glyph err={}", .{err});
- return null;
- } orelse {
- // This should never happen because we embed nerd
- // fonts so we just log and return instead of fallback.
- log.warn("failed to find lock symbol for cursor codepoint=0xF023", .{});
- return null;
- },
- };
-
- try self.cells.append(self.alloc, .{
- .mode = .fg,
- .grid_col = @intCast(x),
- .grid_row = @intCast(screen.cursor.y),
- .grid_width = if (wide) 2 else 1,
- .r = cursor_color.r,
- .g = cursor_color.g,
- .b = cursor_color.b,
- .a = alpha,
- .bg_r = 0,
- .bg_g = 0,
- .bg_b = 0,
- .bg_a = 0,
- .glyph_x = render.glyph.atlas_x,
- .glyph_y = render.glyph.atlas_y,
- .glyph_width = render.glyph.width,
- .glyph_height = render.glyph.height,
- .glyph_offset_x = render.glyph.offset_x,
- .glyph_offset_y = render.glyph.offset_y,
- });
-
- return &self.cells.items[self.cells.items.len - 1];
-}
-
-/// Add an underline decoration to the specified cell
-fn addUnderline(
- self: *OpenGL,
- x: terminal.size.CellCountInt,
- y: terminal.size.CellCountInt,
- style: terminal.Attribute.Underline,
- color: terminal.color.RGB,
- alpha: u8,
- bg: [4]u8,
-) !void {
- const sprite: font.Sprite = switch (style) {
- .none => unreachable,
- .single => .underline,
- .double => .underline_double,
- .dotted => .underline_dotted,
- .dashed => .underline_dashed,
- .curly => .underline_curly,
- };
-
- const render = try self.font_grid.renderGlyph(
- self.alloc,
- font.sprite_index,
- @intFromEnum(sprite),
- .{
- .cell_width = 1,
- .grid_metrics = self.grid_metrics,
- },
- );
-
- try self.cells.append(self.alloc, .{
- .mode = .fg,
- .grid_col = @intCast(x),
- .grid_row = @intCast(y),
- .grid_width = 1,
- .glyph_x = render.glyph.atlas_x,
- .glyph_y = render.glyph.atlas_y,
- .glyph_width = render.glyph.width,
- .glyph_height = render.glyph.height,
- .glyph_offset_x = render.glyph.offset_x,
- .glyph_offset_y = render.glyph.offset_y,
- .r = color.r,
- .g = color.g,
- .b = color.b,
- .a = alpha,
- .bg_r = bg[0],
- .bg_g = bg[1],
- .bg_b = bg[2],
- .bg_a = bg[3],
- });
-}
-
-/// Add an overline decoration to the specified cell
-fn addOverline(
- self: *OpenGL,
- x: terminal.size.CellCountInt,
- y: terminal.size.CellCountInt,
- color: terminal.color.RGB,
- alpha: u8,
- bg: [4]u8,
-) !void {
- const render = try self.font_grid.renderGlyph(
- self.alloc,
- font.sprite_index,
- @intFromEnum(font.Sprite.overline),
- .{
- .cell_width = 1,
- .grid_metrics = self.grid_metrics,
- },
- );
-
- try self.cells.append(self.alloc, .{
- .mode = .fg,
- .grid_col = @intCast(x),
- .grid_row = @intCast(y),
- .grid_width = 1,
- .glyph_x = render.glyph.atlas_x,
- .glyph_y = render.glyph.atlas_y,
- .glyph_width = render.glyph.width,
- .glyph_height = render.glyph.height,
- .glyph_offset_x = render.glyph.offset_x,
- .glyph_offset_y = render.glyph.offset_y,
- .r = color.r,
- .g = color.g,
- .b = color.b,
- .a = alpha,
- .bg_r = bg[0],
- .bg_g = bg[1],
- .bg_b = bg[2],
- .bg_a = bg[3],
- });
-}
-
-/// Add a strikethrough decoration to the specified cell
-fn addStrikethrough(
- self: *OpenGL,
- x: terminal.size.CellCountInt,
- y: terminal.size.CellCountInt,
- color: terminal.color.RGB,
- alpha: u8,
- bg: [4]u8,
-) !void {
- const render = try self.font_grid.renderGlyph(
- self.alloc,
- font.sprite_index,
- @intFromEnum(font.Sprite.strikethrough),
- .{
- .cell_width = 1,
- .grid_metrics = self.grid_metrics,
- },
- );
-
- try self.cells.append(self.alloc, .{
- .mode = .fg,
- .grid_col = @intCast(x),
- .grid_row = @intCast(y),
- .grid_width = 1,
- .glyph_x = render.glyph.atlas_x,
- .glyph_y = render.glyph.atlas_y,
- .glyph_width = render.glyph.width,
- .glyph_height = render.glyph.height,
- .glyph_offset_x = render.glyph.offset_x,
- .glyph_offset_y = render.glyph.offset_y,
- .r = color.r,
- .g = color.g,
- .b = color.b,
- .a = alpha,
- .bg_r = bg[0],
- .bg_g = bg[1],
- .bg_b = bg[2],
- .bg_a = bg[3],
- });
-}
-
-// Add a glyph to the specified cell.
-fn addGlyph(
- self: *OpenGL,
- x: terminal.size.CellCountInt,
- y: terminal.size.CellCountInt,
- cell_pin: terminal.Pin,
- shaper_cell: font.shape.Cell,
- shaper_run: font.shape.TextRun,
- color: terminal.color.RGB,
- alpha: u8,
- bg: [4]u8,
-) !void {
- const rac = cell_pin.rowAndCell();
- const cell = rac.cell;
-
- // Render
- const render = try self.font_grid.renderGlyph(
- self.alloc,
- shaper_run.font_index,
- shaper_cell.glyph_index,
- .{
- .cell_width = if (cell.wide == .wide) 2 else 1,
- .grid_metrics = self.grid_metrics,
- .thicken = self.config.font_thicken,
- .thicken_strength = self.config.font_thicken_strength,
- },
- );
-
- // If the glyph is 0 width or height, it will be invisible
- // when drawn, so don't bother adding it to the buffer.
- if (render.glyph.width == 0 or render.glyph.height == 0) {
- return;
- }
-
- // If we're rendering a color font, we use the color atlas
- const mode: CellProgram.CellMode = switch (try fgMode(
- render.presentation,
- cell_pin,
- )) {
- .normal => .fg,
- .color => .fg_color,
- .constrained => .fg_constrained,
- .powerline => .fg_powerline,
- };
-
- try self.cells.append(self.alloc, .{
- .mode = mode,
- .grid_col = @intCast(x),
- .grid_row = @intCast(y),
- .grid_width = cell.gridWidth(),
- .glyph_x = render.glyph.atlas_x,
- .glyph_y = render.glyph.atlas_y,
- .glyph_width = render.glyph.width,
- .glyph_height = render.glyph.height,
- .glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset,
- .glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset,
- .r = color.r,
- .g = color.g,
- .b = color.b,
- .a = alpha,
- .bg_r = bg[0],
- .bg_g = bg[1],
- .bg_b = bg[2],
- .bg_a = bg[3],
- });
-}
-
-/// Update the configuration.
-pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void {
- // We always redo the font shaper in case font features changed. We
- // could check to see if there was an actual config change but this is
- // easier and rare enough to not cause performance issues.
- {
- var font_shaper = try font.Shaper.init(self.alloc, .{
- .features = config.font_features.items,
- });
- errdefer font_shaper.deinit();
- self.font_shaper.deinit();
- self.font_shaper = font_shaper;
- }
-
- // We also need to reset the shaper cache so shaper info
- // from the previous font isn't re-used for the new font.
- const font_shaper_cache = font.ShaperCache.init();
- self.font_shaper_cache.deinit(self.alloc);
- self.font_shaper_cache = font_shaper_cache;
-
- // Set our new colors
- self.default_background_color = config.background;
- self.default_foreground_color = config.foreground;
- self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
- self.cursor_invert = config.cursor_invert;
-
- // Update our uniforms
- self.deferred_config = .{};
-
- self.config.deinit();
- self.config = config.*;
-}
-
-/// Set the screen size for rendering. This will update the projection
-/// used for the shader so that the scaling of the grid is correct.
-pub fn setScreenSize(
- self: *OpenGL,
- size: renderer.Size,
-) !void {
- if (single_threaded_draw) self.draw_mutex.lock();
- defer if (single_threaded_draw) self.draw_mutex.unlock();
-
- // Store our screen size
- self.size = size;
-
- // Defer our OpenGL updates
- self.deferred_screen_size = .{ .size = size };
-
- log.debug("screen size size={}", .{size});
-}
-
-/// Updates the font texture atlas if it is dirty.
-fn flushAtlas(self: *OpenGL) !void {
- const gl_state = self.gl_state orelse return;
- try flushAtlasSingle(
- &self.font_grid.lock,
- gl_state.texture,
- &self.font_grid.atlas_grayscale,
- &self.texture_grayscale_modified,
- &self.texture_grayscale_resized,
- .red,
- .red,
- );
- try flushAtlasSingle(
- &self.font_grid.lock,
- gl_state.texture_color,
- &self.font_grid.atlas_color,
- &self.texture_color_modified,
- &self.texture_color_resized,
- .rgba,
- .bgra,
- );
-}
-
-/// Flush a single atlas, grabbing all necessary locks, checking for
-/// changes, etc.
-fn flushAtlasSingle(
- lock: *std.Thread.RwLock,
- texture: gl.Texture,
- atlas: *font.Atlas,
- modified: *usize,
- resized: *usize,
- internal_format: gl.Texture.InternalFormat,
- format: gl.Texture.Format,
-) !void {
- // If the texture isn't modified we do nothing
- const new_modified = atlas.modified.load(.monotonic);
- if (new_modified <= modified.*) return;
-
- // If it is modified we need to grab a read-lock
- lock.lockShared();
- defer lock.unlockShared();
-
- var texbind = try texture.bind(.@"2D");
- defer texbind.unbind();
-
- const new_resized = atlas.resized.load(.monotonic);
- if (new_resized > resized.*) {
- try texbind.image2D(
- 0,
- internal_format,
- @intCast(atlas.size),
- @intCast(atlas.size),
- 0,
- format,
- .UnsignedByte,
- atlas.data.ptr,
- );
-
- // Only update the resized number after successful resize
- resized.* = new_resized;
- } else {
- try texbind.subImage2D(
- 0,
- 0,
- 0,
- @intCast(atlas.size),
- @intCast(atlas.size),
- format,
- .UnsignedByte,
- atlas.data.ptr,
- );
- }
-
- // Update our modified tracker after successful update
- modified.* = atlas.modified.load(.monotonic);
-}
-
-/// Render renders the current cell state. This will not modify any of
-/// the cells.
-pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void {
- // If we're in single-threaded more we grab a lock since we use shared data.
- if (single_threaded_draw) self.draw_mutex.lock();
- defer if (single_threaded_draw) self.draw_mutex.unlock();
- const gl_state: *GLState = if (self.gl_state) |*v| v else return;
-
- // Go through our images and see if we need to setup any textures.
- {
- var image_it = self.images.iterator();
- while (image_it.next()) |kv| {
- switch (kv.value_ptr.image) {
- .ready => {},
-
- .pending_gray,
- .pending_gray_alpha,
- .pending_rgb,
- .pending_rgba,
- .replace_gray,
- .replace_gray_alpha,
- .replace_rgb,
- .replace_rgba,
- => try kv.value_ptr.image.upload(self.alloc),
-
- .unload_pending,
- .unload_replace,
- .unload_ready,
- => {
- kv.value_ptr.image.deinit(self.alloc);
- self.images.removeByPtr(kv.key_ptr);
- },
- }
- }
- }
-
- // In the "OpenGL Programming Guide for Mac" it explains that: "When you
- // use an NSOpenGLView object with OpenGL calls that are issued from a
- // thread other than the main one, you must set up mutex locking."
- // This locks the context and avoids crashes that can happen due to
- // races with the underlying Metal layer that Apple is using to
- // implement OpenGL.
- const is_darwin = builtin.target.os.tag.isDarwin();
- const ogl = if (comptime is_darwin) @cImport({
- @cInclude("OpenGL/OpenGL.h");
- }) else {};
- const cgl_ctx = if (comptime is_darwin) ogl.CGLGetCurrentContext();
- if (comptime is_darwin) _ = ogl.CGLLockContext(cgl_ctx);
- defer _ = if (comptime is_darwin) ogl.CGLUnlockContext(cgl_ctx);
-
- // If our viewport size doesn't match the saved screen size then
- // we need to update it. We rely on this over setScreenSize because
- // we can pull it directly from the OpenGL context instead of relying
- // on the eventual message.
- {
- var viewport: [4]gl.c.GLint = undefined;
- gl.glad.context.GetIntegerv.?(gl.c.GL_VIEWPORT, &viewport);
- const screen: renderer.ScreenSize = .{
- .width = @intCast(viewport[2]),
- .height = @intCast(viewport[3]),
- };
- if (!screen.equals(self.size.screen)) {
- self.size.screen = screen;
- self.deferred_screen_size = .{ .size = self.size };
- }
- }
-
- // Draw our terminal cells
- try self.drawCellProgram(gl_state);
-
- // Draw our custom shaders
- if (gl_state.custom) |*custom_state| {
- try self.drawCustomPrograms(custom_state);
- }
-
- // Swap our window buffers
switch (apprt.runtime) {
- apprt.glfw => surface.window.swapBuffers(),
- apprt.gtk => {},
- apprt.embedded => {},
- else => @compileError("unsupported runtime"),
+ apprt.gtk => prepareContext(null) catch |err| {
+ log.warn(
+ "Error preparing GL context in displayRealized, err={}",
+ .{err},
+ );
+ },
+
+ else => @compileError("only GTK should be calling displayRealized"),
}
}
-/// Draw the custom shaders.
-fn drawCustomPrograms(self: *OpenGL, custom_state: *custom.State) !void {
- _ = self;
- assert(custom_state.programs.len > 0);
-
- // Bind our state that is global to all custom shaders
- const custom_bind = try custom_state.bind();
- defer custom_bind.unbind();
-
- // Setup the new frame
- try custom_state.newFrame();
-
- // Go through each custom shader and draw it.
- for (custom_state.programs) |program| {
- const bind = try program.bind();
- defer bind.unbind();
- try bind.draw();
- try custom_state.copyFramebuffer();
- }
-}
-
-/// Runs the cell program (shaders) to draw the terminal grid.
-fn drawCellProgram(
- self: *OpenGL,
- gl_state: *const GLState,
-) !void {
- // Try to flush our atlas, this will only do something if there
- // are changes to the atlas.
- try self.flushAtlas();
-
- // If we have custom shaders, then we draw to the custom
- // shader framebuffer.
- const fbobind: ?gl.Framebuffer.Binding = fbobind: {
- const state = gl_state.custom orelse break :fbobind null;
- break :fbobind try state.fbo.bind(.framebuffer);
- };
- defer if (fbobind) |v| v.unbind();
-
- // Clear the surface
- gl.clearColor(
- @floatCast(@as(f32, @floatFromInt(self.draw_background.r)) / 255 * self.config.background_opacity),
- @floatCast(@as(f32, @floatFromInt(self.draw_background.g)) / 255 * self.config.background_opacity),
- @floatCast(@as(f32, @floatFromInt(self.draw_background.b)) / 255 * self.config.background_opacity),
- @floatCast(self.config.background_opacity),
- );
- gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
-
- // If we have deferred operations, run them.
- if (self.deferred_screen_size) |v| {
- try v.apply(self);
- self.deferred_screen_size = null;
- }
- if (self.deferred_font_size) |v| {
- try v.apply(self);
- self.deferred_font_size = null;
- }
- if (self.deferred_config) |v| {
- try v.apply(self);
- self.deferred_config = null;
- }
-
- // Apply our padding extension fields
- {
- const program = gl_state.cell_program;
- const bind = try program.program.use();
- defer bind.unbind();
- try program.program.setUniform(
- "padding_vertical_top",
- self.padding_extend_top,
- );
- try program.program.setUniform(
- "padding_vertical_bottom",
- self.padding_extend_bottom,
- );
- }
-
- // Draw background images first
- try self.drawImages(
- gl_state,
- self.image_placements.items[0..self.image_bg_end],
- );
-
- // Draw our background
- try self.drawCells(gl_state, self.cells_bg);
-
- // Then draw images under text
- try self.drawImages(
- gl_state,
- self.image_placements.items[self.image_bg_end..self.image_text_end],
- );
-
- // Drag foreground
- try self.drawCells(gl_state, self.cells);
-
- // Draw remaining images
- try self.drawImages(
- gl_state,
- self.image_placements.items[self.image_text_end..],
- );
-}
-
-/// Runs the image program to draw images.
-fn drawImages(
- self: *OpenGL,
- gl_state: *const GLState,
- placements: []const gl_image.Placement,
-) !void {
- if (placements.len == 0) return;
-
- // Bind our image program
- const bind = try gl_state.image_program.bind();
- defer bind.unbind();
-
- // For each placement we need to bind the texture
- for (placements) |p| {
- // Get the image and image texture
- const image = self.images.get(p.image_id) orelse {
- log.warn("image not found for placement image_id={}", .{p.image_id});
- continue;
- };
-
- const texture = switch (image.image) {
- .ready => |t| t,
- else => {
- log.warn("image not ready for placement image_id={}", .{p.image_id});
- continue;
- },
- };
-
- // Bind the texture
- try gl.Texture.active(gl.c.GL_TEXTURE0);
- var texbind = try texture.bind(.@"2D");
- defer texbind.unbind();
-
- // Setup our data
- try bind.vbo.setData(ImageProgram.Input{
- .grid_col = p.x,
- .grid_row = p.y,
- .cell_offset_x = p.cell_offset_x,
- .cell_offset_y = p.cell_offset_y,
- .source_x = p.source_x,
- .source_y = p.source_y,
- .source_width = p.source_width,
- .source_height = p.source_height,
- .dest_width = p.width,
- .dest_height = p.height,
- }, .static_draw);
-
- try gl.drawElementsInstanced(
- gl.c.GL_TRIANGLES,
- 6,
- gl.c.GL_UNSIGNED_BYTE,
- 1,
- );
- }
-}
-
-/// Loads some set of cell data into our buffer and issues a draw call.
-/// This expects all the OpenGL state to be setup.
+/// Actions taken before doing anything in `drawFrame`.
///
-/// Future: when we move to multiple shaders, this will go away and
-/// we'll have a draw call per-shader.
-fn drawCells(
- self: *OpenGL,
- gl_state: *const GLState,
- cells: std.ArrayListUnmanaged(CellProgram.Cell),
-) !void {
- // If we have no cells to render, then we render nothing.
- if (cells.items.len == 0) return;
+/// Right now there's nothing we need to do for OpenGL.
+pub fn drawFrameStart(self: *OpenGL) void {
+ _ = self;
+}
- // Todo: get rid of this completely
- self.gl_cells_written = 0;
+/// Actions taken after `drawFrame` is done.
+///
+/// Right now there's nothing we need to do for OpenGL.
+pub fn drawFrameEnd(self: *OpenGL) void {
+ _ = self;
+}
- // Bind our cell program state, buffers
- const bind = try gl_state.cell_program.bind();
- defer bind.unbind();
-
- // Bind our textures
- try gl.Texture.active(gl.c.GL_TEXTURE0);
- var texbind = try gl_state.texture.bind(.@"2D");
- defer texbind.unbind();
-
- try gl.Texture.active(gl.c.GL_TEXTURE1);
- var texbind1 = try gl_state.texture_color.bind(.@"2D");
- defer texbind1.unbind();
-
- // Our allocated buffer on the GPU is smaller than our capacity.
- // We reallocate a new buffer with the full new capacity.
- if (self.gl_cells_size < cells.capacity) {
- log.info("reallocating GPU buffer old={} new={}", .{
- self.gl_cells_size,
- cells.capacity,
- });
-
- try bind.vbo.setDataNullManual(
- @sizeOf(CellProgram.Cell) * cells.capacity,
- .static_draw,
- );
-
- self.gl_cells_size = cells.capacity;
- self.gl_cells_written = 0;
- }
-
- // If we have data to write to the GPU, send it.
- if (self.gl_cells_written < cells.items.len) {
- const data = cells.items[self.gl_cells_written..];
- // log.info("sending {} cells to GPU", .{data.len});
- try bind.vbo.setSubData(self.gl_cells_written * @sizeOf(CellProgram.Cell), data);
-
- self.gl_cells_written += data.len;
- assert(data.len > 0);
- assert(self.gl_cells_written <= cells.items.len);
- }
-
- try gl.drawElementsInstanced(
- gl.c.GL_TRIANGLES,
- 6,
- gl.c.GL_UNSIGNED_BYTE,
- cells.items.len,
+pub fn initShaders(
+ self: *const OpenGL,
+ alloc: Allocator,
+ custom_shaders: []const [:0]const u8,
+) !shaders.Shaders {
+ _ = alloc;
+ return try shaders.Shaders.init(
+ self.alloc,
+ custom_shaders,
);
}
-/// The OpenGL objects that are associated with a renderer. This makes it
-/// easy to create/destroy these as a set in situations i.e. where the
-/// OpenGL context is replaced.
-const GLState = struct {
- cell_program: CellProgram,
- image_program: ImageProgram,
- texture: gl.Texture,
- texture_color: gl.Texture,
- custom: ?custom.State,
+/// Get the current size of the runtime surface.
+pub fn surfaceSize(self: *const OpenGL) !struct { width: u32, height: u32 } {
+ _ = self;
+ var viewport: [4]gl.c.GLint = undefined;
+ gl.glad.context.GetIntegerv.?(gl.c.GL_VIEWPORT, &viewport);
+ return .{
+ .width = @intCast(viewport[2]),
+ .height = @intCast(viewport[3]),
+ };
+}
- pub fn init(
- alloc: Allocator,
- config: DerivedConfig,
- font_grid: *font.SharedGrid,
- ) !GLState {
- var arena = ArenaAllocator.init(alloc);
- defer arena.deinit();
- const arena_alloc = arena.allocator();
+/// Initialize a new render target which can be presented by this API.
+pub fn initTarget(self: *const OpenGL, width: usize, height: usize) !Target {
+ return Target.init(.{
+ .internal_format = if (self.blending.isLinear()) .srgba else .rgba,
+ .width = width,
+ .height = height,
+ });
+}
- // Load our custom shaders
- const custom_state: ?custom.State = custom: {
- const shaders: []const [:0]const u8 = shadertoy.loadFromFiles(
- arena_alloc,
- config.custom_shaders,
- .glsl,
- ) catch |err| err: {
- log.warn("error loading custom shaders err={}", .{err});
- break :err &.{};
- };
- if (shaders.len == 0) break :custom null;
+/// Present the provided target.
+pub fn present(self: *OpenGL, target: Target) !void {
+ // In order to present a target we blit it to the default framebuffer.
- break :custom custom.State.init(
- alloc,
- shaders,
- ) catch |err| err: {
- log.warn("error initializing custom shaders err={}", .{err});
- break :err null;
- };
+ // We disable GL_FRAMEBUFFER_SRGB while doing this blit, otherwise the
+ // values may be linearized as they're copied, but even though the draw
+ // framebuffer has a linear internal format, the values in it should be
+ // sRGB, not linear!
+ try gl.disable(gl.c.GL_FRAMEBUFFER_SRGB);
+ defer gl.enable(gl.c.GL_FRAMEBUFFER_SRGB) catch |err| {
+ log.err("Error re-enabling GL_FRAMEBUFFER_SRGB, err={}", .{err});
+ };
+
+ // Bind the target for reading.
+ const fbobind = try target.framebuffer.bind(.read);
+ defer fbobind.unbind();
+
+ // Blit
+ gl.glad.context.BlitFramebuffer.?(
+ 0,
+ 0,
+ @intCast(target.width),
+ @intCast(target.height),
+ 0,
+ 0,
+ @intCast(target.width),
+ @intCast(target.height),
+ gl.c.GL_COLOR_BUFFER_BIT,
+ gl.c.GL_NEAREST,
+ );
+
+ // Keep track of this target in case we need to repeat it.
+ self.last_target = target;
+}
+
+/// Present the last presented target again.
+pub fn presentLastTarget(self: *OpenGL) !void {
+ if (self.last_target) |target| try self.present(target);
+}
+
+/// Returns the options to use when constructing buffers.
+pub inline fn bufferOptions(self: OpenGL) bufferpkg.Options {
+ _ = self;
+ return .{
+ .target = .array,
+ .usage = .dynamic_draw,
+ };
+}
+
+pub const instanceBufferOptions = bufferOptions;
+pub const uniformBufferOptions = bufferOptions;
+pub const fgBufferOptions = bufferOptions;
+pub const bgBufferOptions = bufferOptions;
+pub const imageBufferOptions = bufferOptions;
+
+/// Returns the options to use when constructing textures.
+pub inline fn textureOptions(self: OpenGL) Texture.Options {
+ _ = self;
+ return .{
+ .format = .rgba,
+ .internal_format = .srgba,
+ .target = .@"2D",
+ };
+}
+
+/// Initializes a Texture suitable for the provided font atlas.
+pub fn initAtlasTexture(
+ self: *const OpenGL,
+ atlas: *const font.Atlas,
+) Texture.Error!Texture {
+ _ = self;
+ const format: gl.Texture.Format, const internal_format: gl.Texture.InternalFormat =
+ switch (atlas.format) {
+ .grayscale => .{ .red, .red },
+ .rgba => .{ .rgba, .srgba },
+ else => @panic("unsupported atlas format for OpenGL texture"),
};
- // Blending for text. We use GL_ONE here because we should be using
- // premultiplied alpha for all our colors in our fragment shaders.
- // This avoids having a blurry border where transparency is expected on
- // pixels.
- try gl.enable(gl.c.GL_BLEND);
- try gl.blendFunc(gl.c.GL_ONE, gl.c.GL_ONE_MINUS_SRC_ALPHA);
+ return try Texture.init(
+ .{
+ .format = format,
+ .internal_format = internal_format,
+ .target = .Rectangle,
+ },
+ atlas.size,
+ atlas.size,
+ null,
+ );
+}
- // Build our texture
- const tex = try gl.Texture.create();
- errdefer tex.destroy();
- {
- const texbind = try tex.bind(.@"2D");
- try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
- try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
- try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
- try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
- try texbind.image2D(
- 0,
- .red,
- @intCast(font_grid.atlas_grayscale.size),
- @intCast(font_grid.atlas_grayscale.size),
- 0,
- .red,
- .UnsignedByte,
- font_grid.atlas_grayscale.data.ptr,
- );
- }
-
- // Build our color texture
- const tex_color = try gl.Texture.create();
- errdefer tex_color.destroy();
- {
- const texbind = try tex_color.bind(.@"2D");
- try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
- try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
- try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
- try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
- try texbind.image2D(
- 0,
- .rgba,
- @intCast(font_grid.atlas_color.size),
- @intCast(font_grid.atlas_color.size),
- 0,
- .bgra,
- .UnsignedByte,
- font_grid.atlas_color.data.ptr,
- );
- }
-
- // Build our cell renderer
- const cell_program = try CellProgram.init();
- errdefer cell_program.deinit();
-
- // Build our image renderer
- const image_program = try ImageProgram.init();
- errdefer image_program.deinit();
-
- return .{
- .cell_program = cell_program,
- .image_program = image_program,
- .texture = tex,
- .texture_color = tex_color,
- .custom = custom_state,
- };
- }
-
- pub fn deinit(self: *GLState, alloc: Allocator) void {
- if (self.custom) |v| v.deinit(alloc);
- self.texture.destroy();
- self.texture_color.destroy();
- self.image_program.deinit();
- self.cell_program.deinit();
- }
-};
+/// Begin a frame.
+pub inline fn beginFrame(
+ self: *const OpenGL,
+ /// Once the frame has been completed, the `frameCompleted` method
+ /// on the renderer is called with the health status of the frame.
+ renderer: *Renderer,
+ /// The target is presented via the provided renderer's API when completed.
+ target: *Target,
+) !Frame {
+ _ = self;
+ return try Frame.begin(.{}, renderer, target);
+}
diff --git a/src/renderer/Options.zig b/src/renderer/Options.zig
index e7d9b3a42..85ff8e310 100644
--- a/src/renderer/Options.zig
+++ b/src/renderer/Options.zig
@@ -20,3 +20,6 @@ surface_mailbox: apprt.surface.Mailbox,
/// The apprt surface.
rt_surface: *apprt.Surface,
+
+/// The renderer thread.
+thread: *renderer.Thread,
diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig
index 1e9c29b26..b8884f2fb 100644
--- a/src/renderer/Thread.zig
+++ b/src/renderer/Thread.zig
@@ -20,6 +20,16 @@ const log = std.log.scoped(.renderer_thread);
const DRAW_INTERVAL = 8; // 120 FPS
const CURSOR_BLINK_INTERVAL = 600;
+/// Whether calls to `drawFrame` must be done from the app thread.
+///
+/// If this is `true` then we send a `redraw_surface` message to the apprt
+/// whenever we need to draw instead of calling `drawFrame` directly.
+const must_draw_from_app_thread =
+ if (@hasDecl(apprt.App, "must_draw_from_app_thread"))
+ apprt.App.must_draw_from_app_thread
+ else
+ false;
+
/// The type used for sending messages to the IO thread. For now this is
/// hardcoded with a capacity. We can make this a comptime parameter in
/// the future if we want it configurable.
@@ -198,6 +208,13 @@ pub fn threadMain(self: *Thread) void {
fn threadMain_(self: *Thread) !void {
defer log.debug("renderer thread exited", .{});
+ // Right now, on Darwin, `std.Thread.setName` can only name the current
+ // thread, and we have no way to get the current thread from within it,
+ // so instead we use this code to name the thread instead.
+ if (builtin.os.tag.isDarwin()) {
+ internal_os.macos.pthread_setname_np(&"renderer".*);
+ }
+
// Setup our crash metadata
crash.sentry.thread_state = .{
.type = .renderer,
@@ -307,6 +324,16 @@ fn stopDrawTimer(self: *Thread) void {
/// Drain the mailbox.
fn drainMailbox(self: *Thread) !void {
+ // There's probably a more elegant way to do this...
+ //
+ // This is effectively an @autoreleasepool{} block, which we need in
+ // order to ensure that autoreleased objects are properly released.
+ const pool = if (builtin.os.tag.isDarwin())
+ @import("objc").AutoreleasePool.init()
+ else
+ void;
+ defer if (builtin.os.tag.isDarwin()) pool.deinit();
+
while (self.mailbox.pop()) |message| {
log.debug("mailbox message={}", .{message});
switch (message) {
@@ -425,7 +452,7 @@ fn drainMailbox(self: *Thread) !void {
self.renderer.markDirty();
},
- .resize => |v| try self.renderer.setScreenSize(v),
+ .resize => |v| self.renderer.setScreenSize(v),
.change_config => |config| {
defer config.alloc.destroy(config.thread);
@@ -461,20 +488,16 @@ fn drawFrame(self: *Thread, now: bool) void {
if (!self.flags.visible) return;
// If the renderer is managing a vsync on its own, we only draw
- // when we're forced to via now.
+ // when we're forced to via `now`.
if (!now and self.renderer.hasVsync()) return;
- // If we're doing single-threaded GPU calls then we just wake up the
- // app thread to redraw at this point.
- if (rendererpkg.Renderer == rendererpkg.OpenGL and
- rendererpkg.OpenGL.single_threaded_draw)
- {
+ if (must_draw_from_app_thread) {
_ = self.app_mailbox.push(
.{ .redraw_surface = self.surface },
.{ .instant = {} },
);
} else {
- self.renderer.drawFrame(self.surface) catch |err|
+ self.renderer.drawFrame(false) catch |err|
log.warn("error drawing err={}", .{err});
}
}
@@ -582,7 +605,6 @@ fn renderCallback(
// Update our frame data
t.renderer.updateFrame(
- t.surface,
t.state,
t.flags.cursor_blink_visible,
) catch |err|
diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig
new file mode 100644
index 000000000..c0091cbf6
--- /dev/null
+++ b/src/renderer/generic.zig
@@ -0,0 +1,2950 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const glfw = @import("glfw");
+const xev = @import("xev");
+const apprt = @import("../apprt.zig");
+const configpkg = @import("../config.zig");
+const font = @import("../font/main.zig");
+const os = @import("../os/main.zig");
+const terminal = @import("../terminal/main.zig");
+const renderer = @import("../renderer.zig");
+const math = @import("../math.zig");
+const Surface = @import("../Surface.zig");
+const link = @import("link.zig");
+const fgMode = @import("cell.zig").fgMode;
+const isCovering = @import("cell.zig").isCovering;
+const shadertoy = @import("shadertoy.zig");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const ArenaAllocator = std.heap.ArenaAllocator;
+const Terminal = terminal.Terminal;
+const Health = renderer.Health;
+
+const macos = switch (builtin.os.tag) {
+ .macos => @import("macos"),
+ else => void,
+};
+
+const DisplayLink = switch (builtin.os.tag) {
+ .macos => *macos.video.DisplayLink,
+ else => void,
+};
+
+const log = std.log.scoped(.generic_renderer);
+
+/// Create a renderer type with the provided graphics API wrapper.
+///
+/// The graphics API wrapper must provide the interface outlined below.
+/// Specific details for the interfaces are documented on the existing
+/// implementations (`Metal` and `OpenGL`).
+///
+/// Hierarchy of graphics abstractions:
+///
+/// [ GraphicsAPI ] - Responsible for configuring the runtime surface
+/// | | and providing render `Target`s that draw to it,
+/// | | as well as `Frame`s and `Pipeline`s.
+/// | V
+/// | [ Target ] - Represents an abstract target for rendering, which
+/// | could be a surface directly but is also used as an
+/// | abstraction for off-screen frame buffers.
+/// V
+/// [ Frame ] - Represents the context for drawing a given frame,
+/// | provides `RenderPass`es for issuing draw commands
+/// | to, and reports the frame health when complete.
+/// V
+/// [ RenderPass ] - Represents a render pass in a frame, consisting of
+/// : one or more `Step`s applied to the same target(s),
+/// [ Step ] - - - - each describing the input buffers and textures and
+/// : the vertex/fragment functions and geometry to use.
+/// :_ _ _ _ _ _ _ _ _ _/
+/// v
+/// [ Pipeline ] - Describes a vertex and fragment function to be used
+/// for a `Step`; the `GraphicsAPI` is responsible for
+/// these and they should be constructed and cached
+/// ahead of time.
+///
+/// [ Buffer ] - An abstraction over a GPU buffer.
+///
+/// [ Texture ] - An abstraction over a GPU texture.
+///
+pub fn Renderer(comptime GraphicsAPI: type) type {
+ return struct {
+ const Self = @This();
+
+ const Target = GraphicsAPI.Target;
+ const Buffer = GraphicsAPI.Buffer;
+ const Texture = GraphicsAPI.Texture;
+ const RenderPass = GraphicsAPI.RenderPass;
+ const shaderpkg = GraphicsAPI.shaders;
+
+ const cellpkg = GraphicsAPI.cellpkg;
+ const imagepkg = GraphicsAPI.imagepkg;
+ const Image = imagepkg.Image;
+ const ImageMap = imagepkg.ImageMap;
+
+ const Shaders = shaderpkg.Shaders;
+
+ const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement);
+
+ /// Allocator that can be used
+ alloc: std.mem.Allocator,
+
+ /// This mutex must be held whenever any state used in `drawFrame` is
+ /// being modified, and also when it's being accessed in `drawFrame`.
+ draw_mutex: std.Thread.Mutex = .{},
+
+ /// The configuration we need derived from the main config.
+ config: DerivedConfig,
+
+ /// The mailbox for communicating with the window.
+ surface_mailbox: apprt.surface.Mailbox,
+
+ /// Current font metrics defining our grid.
+ grid_metrics: font.Metrics,
+
+ /// The size of everything.
+ size: renderer.Size,
+
+ /// True if the window is focused
+ focused: bool,
+
+ /// The foreground color set by an OSC 10 sequence. If unset then
+ /// default_foreground_color is used.
+ foreground_color: ?terminal.color.RGB,
+
+ /// Foreground color set in the user's config file.
+ default_foreground_color: terminal.color.RGB,
+
+ /// The background color set by an OSC 11 sequence. If unset then
+ /// default_background_color is used.
+ background_color: ?terminal.color.RGB,
+
+ /// Background color set in the user's config file.
+ default_background_color: terminal.color.RGB,
+
+ /// The cursor color set by an OSC 12 sequence. If unset then
+ /// default_cursor_color is used.
+ cursor_color: ?terminal.color.RGB,
+
+ /// Default cursor color when no color is set explicitly by an OSC 12 command.
+ /// This is cursor color as set in the user's config, if any. If no cursor color
+ /// is set in the user's config, then the cursor color is determined by the
+ /// current foreground color.
+ default_cursor_color: ?terminal.color.RGB,
+
+ /// When `cursor_color` is null, swap the foreground and background colors of
+ /// the cell under the cursor for the cursor color. Otherwise, use the default
+ /// foreground color as the cursor color.
+ cursor_invert: bool,
+
+ /// The current set of cells to render. This is rebuilt on every frame
+ /// but we keep this around so that we don't reallocate. Each set of
+ /// cells goes into a separate shader.
+ cells: cellpkg.Contents,
+
+ /// The last viewport that we based our rebuild off of. If this changes,
+ /// then we do a full rebuild of the cells. The pointer values in this pin
+ /// are NOT SAFE to read because they may be modified, freed, etc from the
+ /// termio thread. We treat the pointers as integers for comparison only.
+ cells_viewport: ?terminal.Pin = null,
+
+ /// Set to true after rebuildCells is called. This can be used
+ /// to determine if any possible changes have been made to the
+ /// cells for the draw call.
+ cells_rebuilt: bool = false,
+
+ /// The current GPU uniform values.
+ uniforms: shaderpkg.Uniforms,
+
+ /// Custom shader uniform values.
+ custom_shader_uniforms: shadertoy.Uniforms,
+
+ /// Timestamp we rendered out first frame.
+ ///
+ /// This is used when updating custom shader uniforms.
+ first_frame_time: ?std.time.Instant = null,
+
+ /// Timestamp when we rendered out more recent frame.
+ ///
+ /// This is used when updating custom shader uniforms.
+ last_frame_time: ?std.time.Instant = null,
+
+ /// The font structures.
+ font_grid: *font.SharedGrid,
+ font_shaper: font.Shaper,
+ font_shaper_cache: font.ShaperCache,
+
+ /// The images that we may render.
+ images: ImageMap = .{},
+ image_placements: ImagePlacementList = .{},
+ image_bg_end: u32 = 0,
+ image_text_end: u32 = 0,
+ image_virtual: bool = false,
+
+ /// Graphics API state.
+ api: GraphicsAPI,
+
+ /// The CVDisplayLink used to drive the rendering loop in
+ /// sync with the display. This is void on platforms that
+ /// don't support a display link.
+ display_link: ?DisplayLink = null,
+
+ /// Health of the most recently completed frame.
+ health: std.atomic.Value(Health) = .{ .raw = .healthy },
+
+ /// Our swap chain (multiple buffering)
+ swap_chain: SwapChain,
+
+ /// This value is used to force-update swap chain targets in the
+ /// event of a config change that requires it (such as blending mode).
+ target_config_modified: usize = 0,
+
+ /// If something happened that requires us to reinitialize our shaders,
+ /// this is set to true so that we can do that whenever possible.
+ reinitialize_shaders: bool = false,
+
+ /// Whether or not we have custom shaders.
+ has_custom_shaders: bool = false,
+
+ /// Our shader pipelines.
+ shaders: Shaders,
+
+ /// Swap chain which maintains multiple copies of the state needed to
+ /// render a frame, so that we can start building the next frame while
+ /// the previous frame is still being processed on the GPU.
+ const SwapChain = struct {
+ // The count of buffers we use for double/triple buffering.
+ // If this is one then we don't do any double+ buffering at all.
+ // This is comptime because there isn't a good reason to change
+ // this at runtime and there is a lot of complexity to support it.
+ const buf_count = GraphicsAPI.swap_chain_count;
+
+ /// `buf_count` structs that can hold the
+ /// data needed by the GPU to draw a frame.
+ frames: [buf_count]FrameState,
+ /// Index of the most recently used frame state struct.
+ frame_index: std.math.IntFittingRange(0, buf_count) = 0,
+ /// Semaphore that we wait on to make sure we have an available
+ /// frame state struct so we can start working on a new frame.
+ frame_sema: std.Thread.Semaphore = .{ .permits = buf_count },
+
+ /// Set to true when deinited, if you try to deinit a defunct
+ /// swap chain it will just be ignored, to prevent double-free.
+ ///
+ /// This is required because of `displayUnrealized`, since it
+ /// `deinits` the swapchain, which leads to a double-free if
+ /// the renderer is deinited after that.
+ defunct: bool = false,
+
+ pub fn init(api: GraphicsAPI, custom_shaders: bool) !SwapChain {
+ var result: SwapChain = .{ .frames = undefined };
+
+ // Initialize all of our frame state.
+ for (&result.frames) |*frame| {
+ frame.* = try FrameState.init(api, custom_shaders);
+ }
+
+ return result;
+ }
+
+ pub fn deinit(self: *SwapChain) void {
+ if (self.defunct) return;
+ self.defunct = true;
+
+ // Wait for all of our inflight draws to complete
+ // so that we can cleanly deinit our GPU state.
+ for (0..buf_count) |_| self.frame_sema.wait();
+ for (&self.frames) |*frame| frame.deinit();
+ }
+
+ /// Get the next frame state to draw to. This will wait on the
+ /// semaphore to ensure that the frame is available. This must
+ /// always be paired with a call to releaseFrame.
+ pub fn nextFrame(self: *SwapChain) error{Defunct}!*FrameState {
+ if (self.defunct) return error.Defunct;
+
+ self.frame_sema.wait();
+ errdefer self.frame_sema.post();
+ self.frame_index = (self.frame_index + 1) % buf_count;
+ return &self.frames[self.frame_index];
+ }
+
+ /// This should be called when the frame has completed drawing.
+ pub fn releaseFrame(self: *SwapChain) void {
+ self.frame_sema.post();
+ }
+ };
+
+ /// State we need duplicated for every frame. Any state that could be
+ /// in a data race between the GPU and CPU while a frame is being drawn
+ /// should be in this struct.
+ ///
+ /// While a draw is in-process, we "lock" the state (via a semaphore)
+ /// and prevent the CPU from updating the state until our graphics API
+ /// reports that the frame is complete.
+ ///
+ /// This is used to implement double/triple buffering.
+ const FrameState = struct {
+ uniforms: UniformBuffer,
+ cells: CellTextBuffer,
+ cells_bg: CellBgBuffer,
+
+ grayscale: Texture,
+ grayscale_modified: usize = 0,
+ color: Texture,
+ color_modified: usize = 0,
+
+ target: Target,
+ /// See property of same name on Renderer for explanation.
+ target_config_modified: usize = 0,
+
+ /// Custom shader state, this is null if we have no custom shaders.
+ custom_shader_state: ?CustomShaderState = null,
+
+ /// A buffer containing the uniform data.
+ const UniformBuffer = Buffer(shaderpkg.Uniforms);
+ const CellBgBuffer = Buffer(shaderpkg.CellBg);
+ const CellTextBuffer = Buffer(shaderpkg.CellText);
+
+ pub fn init(api: GraphicsAPI, custom_shaders: bool) !FrameState {
+ // Uniform buffer contains exactly 1 uniform struct. The
+ // uniform data will be undefined so this must be set before
+ // a frame is drawn.
+ var uniforms = try UniformBuffer.init(api.uniformBufferOptions(), 1);
+ errdefer uniforms.deinit();
+
+ // Create GPU buffers for our cells.
+ //
+ // We start them off with a size of 1, which will of course be
+ // too small, but they will be resized as needed. This is a bit
+ // wasteful but since it's a one-time thing it's not really a
+ // huge concern.
+ var cells = try CellTextBuffer.init(api.fgBufferOptions(), 1);
+ errdefer cells.deinit();
+ var cells_bg = try CellBgBuffer.init(api.bgBufferOptions(), 1);
+ errdefer cells_bg.deinit();
+
+ // Initialize our textures for our font atlas.
+ //
+ // As with the buffers above, we start these off as small
+ // as possible since they'll inevitably be resized anyway.
+ const grayscale = try api.initAtlasTexture(&.{
+ .data = undefined,
+ .size = 1,
+ .format = .grayscale,
+ });
+ errdefer grayscale.deinit();
+ const color = try api.initAtlasTexture(&.{
+ .data = undefined,
+ .size = 1,
+ .format = .rgba,
+ });
+ errdefer color.deinit();
+
+ var custom_shader_state =
+ if (custom_shaders)
+ try CustomShaderState.init(api)
+ else
+ null;
+ errdefer if (custom_shader_state) |*state| state.deinit();
+
+ // Initialize the target. Just as with the other resources,
+ // start it off as small as we can since it'll be resized.
+ const target = try api.initTarget(1, 1);
+
+ return .{
+ .uniforms = uniforms,
+ .cells = cells,
+ .cells_bg = cells_bg,
+ .grayscale = grayscale,
+ .color = color,
+ .target = target,
+ .custom_shader_state = custom_shader_state,
+ };
+ }
+
+ pub fn deinit(self: *FrameState) void {
+ self.uniforms.deinit();
+ self.cells.deinit();
+ self.cells_bg.deinit();
+ self.grayscale.deinit();
+ self.color.deinit();
+ if (self.custom_shader_state) |*state| state.deinit();
+ }
+
+ pub fn resize(
+ self: *FrameState,
+ api: GraphicsAPI,
+ width: usize,
+ height: usize,
+ ) !void {
+ if (self.custom_shader_state) |*state| {
+ try state.resize(api, width, height);
+ }
+ const target = try api.initTarget(width, height);
+ self.target.deinit();
+ self.target = target;
+ }
+ };
+
+ /// State relevant to our custom shaders if we have any.
+ const CustomShaderState = struct {
+ /// When we have a custom shader state, we maintain a front
+ /// and back texture which we use as a swap chain to render
+ /// between when multiple custom shaders are defined.
+ front_texture: Texture,
+ back_texture: Texture,
+
+ /// Swap the front and back textures.
+ pub fn swap(self: *CustomShaderState) void {
+ std.mem.swap(Texture, &self.front_texture, &self.back_texture);
+ }
+
+ pub fn init(api: GraphicsAPI) !CustomShaderState {
+ // Initialize the front and back textures at 1x1 px, this
+ // is slightly wasteful but it's only done once so whatever.
+ const front_texture = try Texture.init(
+ api.textureOptions(),
+ 1,
+ 1,
+ null,
+ );
+ errdefer front_texture.deinit();
+ const back_texture = try Texture.init(
+ api.textureOptions(),
+ 1,
+ 1,
+ null,
+ );
+ errdefer back_texture.deinit();
+ return .{
+ .front_texture = front_texture,
+ .back_texture = back_texture,
+ };
+ }
+
+ pub fn deinit(self: *CustomShaderState) void {
+ self.front_texture.deinit();
+ self.back_texture.deinit();
+ }
+
+ pub fn resize(
+ self: *CustomShaderState,
+ api: GraphicsAPI,
+ width: usize,
+ height: usize,
+ ) !void {
+ const front_texture = try Texture.init(
+ api.textureOptions(),
+ @intCast(width),
+ @intCast(height),
+ null,
+ );
+ errdefer front_texture.deinit();
+ const back_texture = try Texture.init(
+ api.textureOptions(),
+ @intCast(width),
+ @intCast(height),
+ null,
+ );
+ errdefer back_texture.deinit();
+
+ self.front_texture.deinit();
+ self.back_texture.deinit();
+
+ self.front_texture = front_texture;
+ self.back_texture = back_texture;
+ }
+ };
+
+ /// The configuration for this renderer that is derived from the main
+ /// configuration. This must be exported so that we don't need to
+ /// pass around Config pointers which makes memory management a pain.
+ pub const DerivedConfig = struct {
+ arena: ArenaAllocator,
+
+ font_thicken: bool,
+ font_thicken_strength: u8,
+ font_features: std.ArrayListUnmanaged([:0]const u8),
+ font_styles: font.CodepointResolver.StyleStatus,
+ cursor_color: ?terminal.color.RGB,
+ cursor_invert: bool,
+ cursor_opacity: f64,
+ cursor_text: ?terminal.color.RGB,
+ background: terminal.color.RGB,
+ background_opacity: f64,
+ foreground: terminal.color.RGB,
+ selection_background: ?terminal.color.RGB,
+ selection_foreground: ?terminal.color.RGB,
+ invert_selection_fg_bg: bool,
+ bold_is_bright: bool,
+ min_contrast: f32,
+ padding_color: configpkg.WindowPaddingColor,
+ custom_shaders: configpkg.RepeatablePath,
+ links: link.Set,
+ vsync: bool,
+ colorspace: configpkg.Config.WindowColorspace,
+ blending: configpkg.Config.AlphaBlending,
+
+ pub fn init(
+ alloc_gpa: Allocator,
+ config: *const configpkg.Config,
+ ) !DerivedConfig {
+ var arena = ArenaAllocator.init(alloc_gpa);
+ errdefer arena.deinit();
+ const alloc = arena.allocator();
+
+ // Copy our shaders
+ const custom_shaders = try config.@"custom-shader".clone(alloc);
+
+ // Copy our font features
+ const font_features = try config.@"font-feature".clone(alloc);
+
+ // Get our font styles
+ var font_styles = font.CodepointResolver.StyleStatus.initFill(true);
+ font_styles.set(.bold, config.@"font-style-bold" != .false);
+ font_styles.set(.italic, config.@"font-style-italic" != .false);
+ font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false);
+
+ // Our link configs
+ const links = try link.Set.fromConfig(
+ alloc,
+ config.link.links.items,
+ );
+
+ const cursor_invert = config.@"cursor-invert-fg-bg";
+
+ return .{
+ .background_opacity = @max(0, @min(1, config.@"background-opacity")),
+ .font_thicken = config.@"font-thicken",
+ .font_thicken_strength = config.@"font-thicken-strength",
+ .font_features = font_features.list,
+ .font_styles = font_styles,
+
+ .cursor_color = if (!cursor_invert and config.@"cursor-color" != null)
+ config.@"cursor-color".?.toTerminalRGB()
+ else
+ null,
+
+ .cursor_invert = cursor_invert,
+
+ .cursor_text = if (config.@"cursor-text") |txt|
+ txt.toTerminalRGB()
+ else
+ null,
+
+ .cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")),
+
+ .background = config.background.toTerminalRGB(),
+ .foreground = config.foreground.toTerminalRGB(),
+ .invert_selection_fg_bg = config.@"selection-invert-fg-bg",
+ .bold_is_bright = config.@"bold-is-bright",
+ .min_contrast = @floatCast(config.@"minimum-contrast"),
+ .padding_color = config.@"window-padding-color",
+
+ .selection_background = if (config.@"selection-background") |bg|
+ bg.toTerminalRGB()
+ else
+ null,
+
+ .selection_foreground = if (config.@"selection-foreground") |bg|
+ bg.toTerminalRGB()
+ else
+ null,
+
+ .custom_shaders = custom_shaders,
+ .links = links,
+ .vsync = config.@"window-vsync",
+ .colorspace = config.@"window-colorspace",
+ .blending = config.@"alpha-blending",
+ .arena = arena,
+ };
+ }
+
+ pub fn deinit(self: *DerivedConfig) void {
+ const alloc = self.arena.allocator();
+ self.links.deinit(alloc);
+ self.arena.deinit();
+ }
+ };
+
+ /// Returns the hints that we want for this window.
+ pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints {
+ // If our graphics API provides hints, use them,
+ // otherwise fall back to generic hints.
+ if (@hasDecl(GraphicsAPI, "glfwWindowHints")) {
+ return GraphicsAPI.glfwWindowHints(config);
+ }
+
+ return .{
+ .client_api = .no_api,
+ .transparent_framebuffer = config.@"background-opacity" < 1,
+ };
+ }
+
+ pub fn init(alloc: Allocator, options: renderer.Options) !Self {
+ // Initialize our graphics API wrapper, this will prepare the
+ // surface provided by the apprt and set up any API-specific
+ // GPU resources.
+ var api = try GraphicsAPI.init(alloc, options);
+ errdefer api.deinit();
+
+ const has_custom_shaders = options.config.custom_shaders.value.items.len > 0;
+
+ // Prepare our swap chain
+ var swap_chain = try SwapChain.init(
+ api,
+ has_custom_shaders,
+ );
+ errdefer swap_chain.deinit();
+
+ // Create the font shaper.
+ var font_shaper = try font.Shaper.init(alloc, .{
+ .features = options.config.font_features.items,
+ });
+ errdefer font_shaper.deinit();
+
+ // Initialize all the data that requires a critical font section.
+ const font_critical: struct {
+ metrics: font.Metrics,
+ } = font_critical: {
+ const grid: *font.SharedGrid = options.font_grid;
+ grid.lock.lockShared();
+ defer grid.lock.unlockShared();
+ break :font_critical .{
+ .metrics = grid.metrics,
+ };
+ };
+
+ const display_link: ?DisplayLink = switch (builtin.os.tag) {
+ .macos => if (options.config.vsync)
+ try macos.video.DisplayLink.createWithActiveCGDisplays()
+ else
+ null,
+ else => null,
+ };
+ errdefer if (display_link) |v| v.release();
+
+ var result: Self = .{
+ .alloc = alloc,
+ .config = options.config,
+ .surface_mailbox = options.surface_mailbox,
+ .grid_metrics = font_critical.metrics,
+ .size = options.size,
+ .focused = true,
+ .foreground_color = null,
+ .default_foreground_color = options.config.foreground,
+ .background_color = null,
+ .default_background_color = options.config.background,
+ .cursor_color = null,
+ .default_cursor_color = options.config.cursor_color,
+ .cursor_invert = options.config.cursor_invert,
+
+ // Render state
+ .cells = .{},
+ .uniforms = .{
+ .projection_matrix = undefined,
+ .cell_size = undefined,
+ .grid_size = undefined,
+ .grid_padding = undefined,
+ .padding_extend = .{},
+ .min_contrast = options.config.min_contrast,
+ .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) },
+ .cursor_color = undefined,
+ .bg_color = .{
+ options.config.background.r,
+ options.config.background.g,
+ options.config.background.b,
+ @intFromFloat(@round(options.config.background_opacity * 255.0)),
+ },
+ .bools = .{
+ .cursor_wide = false,
+ .use_display_p3 = options.config.colorspace == .@"display-p3",
+ .use_linear_blending = options.config.blending.isLinear(),
+ .use_linear_correction = options.config.blending == .@"linear-corrected",
+ },
+ },
+ .custom_shader_uniforms = .{
+ .resolution = .{ 0, 0, 1 },
+ .time = 0,
+ .time_delta = 0,
+ .frame_rate = 60, // not currently updated
+ .frame = 0,
+ .channel_time = @splat(@splat(0)),
+ .channel_resolution = @splat(@splat(0)),
+ .mouse = @splat(0), // not currently updated
+ .date = @splat(0), // not currently updated
+ .sample_rate = 0, // N/A, we don't have any audio
+ .current_cursor = @splat(0),
+ .previous_cursor = @splat(0),
+ .current_cursor_color = @splat(0),
+ .previous_cursor_color = @splat(0),
+ .cursor_change_time = 0,
+ },
+
+ // Fonts
+ .font_grid = options.font_grid,
+ .font_shaper = font_shaper,
+ .font_shaper_cache = font.ShaperCache.init(),
+
+ // Shaders (initialized below)
+ .shaders = undefined,
+
+ // Graphics API stuff
+ .api = api,
+ .swap_chain = swap_chain,
+ .display_link = display_link,
+ };
+
+ try result.initShaders();
+
+ // Ensure our undefined values above are correctly initialized.
+ result.updateFontGridUniforms();
+ result.updateScreenSizeUniforms();
+
+ return result;
+ }
+
+ pub fn deinit(self: *Self) void {
+ self.swap_chain.deinit();
+
+ if (DisplayLink != void) {
+ if (self.display_link) |display_link| {
+ display_link.stop() catch {};
+ display_link.release();
+ }
+ }
+
+ self.cells.deinit(self.alloc);
+
+ self.font_shaper.deinit();
+ self.font_shaper_cache.deinit(self.alloc);
+
+ self.config.deinit();
+
+ {
+ var it = self.images.iterator();
+ while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc);
+ self.images.deinit(self.alloc);
+ }
+ self.image_placements.deinit(self.alloc);
+
+ self.deinitShaders();
+
+ self.api.deinit();
+
+ self.* = undefined;
+ }
+
+ fn deinitShaders(self: *Self) void {
+ self.shaders.deinit(self.alloc);
+ }
+
+ fn initShaders(self: *Self) !void {
+ var arena = ArenaAllocator.init(self.alloc);
+ defer arena.deinit();
+ const arena_alloc = arena.allocator();
+
+ // Load our custom shaders
+ const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles(
+ arena_alloc,
+ self.config.custom_shaders,
+ GraphicsAPI.custom_shader_target,
+ ) catch |err| err: {
+ log.warn("error loading custom shaders err={}", .{err});
+ break :err &.{};
+ };
+
+ const has_custom_shaders = custom_shaders.len > 0;
+
+ var shaders = try self.api.initShaders(
+ self.alloc,
+ custom_shaders,
+ );
+ errdefer shaders.deinit(self.alloc);
+
+ self.shaders = shaders;
+ self.has_custom_shaders = has_custom_shaders;
+ }
+
+ /// This is called early right after surface creation.
+ pub fn surfaceInit(surface: *apprt.Surface) !void {
+ // If our API has to do things here, let it.
+ if (@hasDecl(GraphicsAPI, "surfaceInit")) {
+ try GraphicsAPI.surfaceInit(surface);
+ }
+ }
+
+ /// This is called just prior to spinning up the renderer thread for
+ /// final main thread setup requirements.
+ pub fn finalizeSurfaceInit(self: *Self, surface: *apprt.Surface) !void {
+ // If our API has to do things to finalize surface init, let it.
+ if (@hasDecl(GraphicsAPI, "finalizeSurfaceInit")) {
+ try self.api.finalizeSurfaceInit(surface);
+ }
+ }
+
+ /// Callback called by renderer.Thread when it begins.
+ pub fn threadEnter(self: *const Self, surface: *apprt.Surface) !void {
+ // If our API has to do things on thread enter, let it.
+ if (@hasDecl(GraphicsAPI, "threadEnter")) {
+ try self.api.threadEnter(surface);
+ }
+ }
+
+ /// Callback called by renderer.Thread when it exits.
+ pub fn threadExit(self: *const Self) void {
+ // If our API has to do things on thread exit, let it.
+ if (@hasDecl(GraphicsAPI, "threadExit")) {
+ self.api.threadExit();
+ }
+ }
+
+ /// Called by renderer.Thread when it starts the main loop.
+ pub fn loopEnter(self: *Self, thr: *renderer.Thread) !void {
+ // If our API has to do things on loop enter, let it.
+ if (@hasDecl(GraphicsAPI, "loopEnter")) {
+ self.api.loopEnter();
+ }
+
+ // If we don't support a display link we have no work to do.
+ if (comptime DisplayLink == void) return;
+
+ // This is when we know our "self" pointer is stable so we can
+ // setup the display link. To setup the display link we set our
+ // callback and we can start it immediately.
+ const display_link = self.display_link orelse return;
+ try display_link.setOutputCallback(
+ xev.Async,
+ &displayLinkCallback,
+ &thr.draw_now,
+ );
+ display_link.start() catch {};
+ }
+
+ /// Called by renderer.Thread when it exits the main loop.
+ pub fn loopExit(self: *Self) void {
+ // If our API has to do things on loop exit, let it.
+ if (@hasDecl(GraphicsAPI, "loopExit")) {
+ self.api.loopExit();
+ }
+
+ // If we don't support a display link we have no work to do.
+ if (comptime DisplayLink == void) return;
+
+ // Stop our display link. If this fails its okay it just means
+ // that we either never started it or the view its attached to
+ // is gone which is fine.
+ const display_link = self.display_link orelse return;
+ display_link.stop() catch {};
+ }
+
+ /// This is called by the GTK apprt after the surface is
+ /// reinitialized due to any of the events mentioned in
+ /// the doc comment for `displayUnrealized`.
+ pub fn displayRealized(self: *Self) !void {
+ // If our API has to do things on realize, let it.
+ if (@hasDecl(GraphicsAPI, "displayRealized")) {
+ self.api.displayRealized();
+ }
+
+ // Lock the draw mutex so that we can
+ // safely reinitialize our GPU resources.
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // We assume that the swap chain was deinited in
+ // `displayUnrealized`, in which case it should be
+ // marked defunct. If not, we have a problem.
+ assert(self.swap_chain.defunct);
+
+ // We reinitialize our shaders and our swap chain.
+ try self.initShaders();
+ self.swap_chain = try SwapChain.init(
+ self.api,
+ self.has_custom_shaders,
+ );
+ self.reinitialize_shaders = false;
+ self.target_config_modified = 1;
+ }
+
+ /// This is called by the GTK apprt when the surface is being destroyed.
+ /// This can happen because the surface is being closed but also when
+ /// moving the window between displays or splitting.
+ pub fn displayUnrealized(self: *Self) void {
+ // If our API has to do things on unrealize, let it.
+ if (@hasDecl(GraphicsAPI, "displayUnrealized")) {
+ self.api.displayUnrealized();
+ }
+
+ // Lock the draw mutex so that we can
+ // safely deinitialize our GPU resources.
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // We deinit our swap chain and shaders.
+ //
+ // This will mark them as defunct so that they
+ // can't be double-freed or used in draw calls.
+ self.swap_chain.deinit();
+ self.shaders.deinit(self.alloc);
+ }
+
+ fn displayLinkCallback(
+ _: *macos.video.DisplayLink,
+ ud: ?*xev.Async,
+ ) void {
+ const draw_now = ud orelse return;
+ draw_now.notify() catch |err| {
+ log.err("error notifying draw_now err={}", .{err});
+ };
+ }
+
+ /// Mark the full screen as dirty so that we redraw everything.
+ pub fn markDirty(self: *Self) void {
+ self.cells_viewport = null;
+ }
+
+ /// Called when we get an updated display ID for our display link.
+ pub fn setMacOSDisplayID(self: *Self, id: u32) !void {
+ if (comptime DisplayLink == void) return;
+ const display_link = self.display_link orelse return;
+ log.info("updating display link display id={}", .{id});
+ display_link.setCurrentCGDisplay(id) catch |err| {
+ log.warn("error setting display link display id err={}", .{err});
+ };
+ }
+
+ /// True if our renderer has animations so that a higher frequency
+ /// timer is used.
+ pub fn hasAnimations(self: *const Self) bool {
+ return self.has_custom_shaders;
+ }
+
+ /// True if our renderer is using vsync. If true, the renderer or apprt
+ /// is responsible for triggering draw_now calls to the render thread.
+ /// That is the only way to trigger a drawFrame.
+ pub fn hasVsync(self: *const Self) bool {
+ if (comptime DisplayLink == void) return false;
+ const display_link = self.display_link orelse return false;
+ return display_link.isRunning();
+ }
+
+ /// Callback when the focus changes for the terminal this is rendering.
+ ///
+ /// Must be called on the render thread.
+ pub fn setFocus(self: *Self, focus: bool) !void {
+ self.focused = focus;
+
+ // If we're not focused, then we want to stop the display link
+ // because it is a waste of resources and we can move to pure
+ // change-driven updates.
+ if (comptime DisplayLink != void) link: {
+ const display_link = self.display_link orelse break :link;
+ if (focus) {
+ display_link.start() catch {};
+ } else {
+ display_link.stop() catch {};
+ }
+ }
+ }
+
+ /// Callback when the window is visible or occluded.
+ ///
+ /// Must be called on the render thread.
+ pub fn setVisible(self: *Self, visible: bool) void {
+ // If we're not visible, then we want to stop the display link
+ // because it is a waste of resources and we can move to pure
+ // change-driven updates.
+ if (comptime DisplayLink != void) link: {
+ const display_link = self.display_link orelse break :link;
+ if (visible and self.focused) {
+ display_link.start() catch {};
+ } else {
+ display_link.stop() catch {};
+ }
+ }
+ }
+
+ /// Set the new font grid.
+ ///
+ /// Must be called on the render thread.
+ pub fn setFontGrid(self: *Self, grid: *font.SharedGrid) void {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // Update our grid
+ self.font_grid = grid;
+
+ // Update all our textures so that they sync on the next frame.
+ // We can modify this without a lock because the GPU does not
+ // touch this data.
+ for (&self.swap_chain.frames) |*frame| {
+ frame.grayscale_modified = 0;
+ frame.color_modified = 0;
+ }
+
+ // Get our metrics from the grid. This doesn't require a lock because
+ // the metrics are never recalculated.
+ const metrics = grid.metrics;
+ self.grid_metrics = metrics;
+
+ // Reset our shaper cache. If our font changed (not just the size) then
+ // the data in the shaper cache may be invalid and cannot be used, so we
+ // always clear the cache just in case.
+ const font_shaper_cache = font.ShaperCache.init();
+ self.font_shaper_cache.deinit(self.alloc);
+ self.font_shaper_cache = font_shaper_cache;
+
+ // Update cell size.
+ self.size.cell = .{
+ .width = metrics.cell_width,
+ .height = metrics.cell_height,
+ };
+
+ // Update relevant uniforms
+ self.updateFontGridUniforms();
+ }
+
+ /// Update uniforms that are based on the font grid.
+ ///
+ /// Caller must hold the draw mutex.
+ fn updateFontGridUniforms(self: *Self) void {
+ self.uniforms.cell_size = .{
+ @floatFromInt(self.grid_metrics.cell_width),
+ @floatFromInt(self.grid_metrics.cell_height),
+ };
+ }
+
+ /// Update the frame data.
+ pub fn updateFrame(
+ self: *Self,
+ state: *renderer.State,
+ cursor_blink_visible: bool,
+ ) !void {
+ // Data we extract out of the critical area.
+ const Critical = struct {
+ bg: terminal.color.RGB,
+ screen: terminal.Screen,
+ screen_type: terminal.ScreenType,
+ mouse: renderer.State.Mouse,
+ preedit: ?renderer.State.Preedit,
+ cursor_style: ?renderer.CursorStyle,
+ color_palette: terminal.color.Palette,
+
+ /// If true, rebuild the full screen.
+ full_rebuild: bool,
+ };
+
+ // Update all our data as tightly as possible within the mutex.
+ var critical: Critical = critical: {
+ // const start = try std.time.Instant.now();
+ // const start_micro = std.time.microTimestamp();
+ // defer {
+ // const end = std.time.Instant.now() catch unreachable;
+ // // "[updateFrame critical time] \t"
+ // std.log.err("[updateFrame critical time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us});
+ // }
+
+ state.mutex.lock();
+ defer state.mutex.unlock();
+
+ // If we're in a synchronized output state, we pause all rendering.
+ if (state.terminal.modes.get(.synchronized_output)) {
+ log.debug("synchronized output started, skipping render", .{});
+ return;
+ }
+
+ // Swap bg/fg if the terminal is reversed
+ const bg = self.background_color orelse self.default_background_color;
+ const fg = self.foreground_color orelse self.default_foreground_color;
+ defer {
+ if (self.background_color) |*c| {
+ c.* = bg;
+ } else {
+ self.default_background_color = bg;
+ }
+
+ if (self.foreground_color) |*c| {
+ c.* = fg;
+ } else {
+ self.default_foreground_color = fg;
+ }
+ }
+
+ if (state.terminal.modes.get(.reverse_colors)) {
+ if (self.background_color) |*c| {
+ c.* = fg;
+ } else {
+ self.default_background_color = fg;
+ }
+
+ if (self.foreground_color) |*c| {
+ c.* = bg;
+ } else {
+ self.default_foreground_color = bg;
+ }
+ }
+
+ // Get the viewport pin so that we can compare it to the current.
+ const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
+
+ // We used to share terminal state, but we've since learned through
+ // analysis that it is faster to copy the terminal state than to
+ // hold the lock while rebuilding GPU cells.
+ var screen_copy = try state.terminal.screen.clone(
+ self.alloc,
+ .{ .viewport = .{} },
+ null,
+ );
+ errdefer screen_copy.deinit();
+
+ // Whether to draw our cursor or not.
+ const cursor_style = if (state.terminal.flags.password_input)
+ .lock
+ else
+ renderer.cursorStyle(
+ state,
+ self.focused,
+ cursor_blink_visible,
+ );
+
+ // Get our preedit state
+ const preedit: ?renderer.State.Preedit = preedit: {
+ if (cursor_style == null) break :preedit null;
+ const p = state.preedit orelse break :preedit null;
+ break :preedit try p.clone(self.alloc);
+ };
+ errdefer if (preedit) |p| p.deinit(self.alloc);
+
+ // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path.
+ // We only do this if the Kitty image state is dirty meaning only if
+ // it changes.
+ //
+ // If we have any virtual references, we must also rebuild our
+ // kitty state on every frame because any cell change can move
+ // an image.
+ if (state.terminal.screen.kitty_images.dirty or
+ self.image_virtual)
+ {
+ try self.prepKittyGraphics(state.terminal);
+ }
+
+ // If we have any terminal dirty flags set then we need to rebuild
+ // the entire screen. This can be optimized in the future.
+ const full_rebuild: bool = rebuild: {
+ {
+ const Int = @typeInfo(terminal.Terminal.Dirty).@"struct".backing_integer.?;
+ const v: Int = @bitCast(state.terminal.flags.dirty);
+ if (v > 0) break :rebuild true;
+ }
+ {
+ const Int = @typeInfo(terminal.Screen.Dirty).@"struct".backing_integer.?;
+ const v: Int = @bitCast(state.terminal.screen.dirty);
+ if (v > 0) break :rebuild true;
+ }
+
+ // If our viewport changed then we need to rebuild the entire
+ // screen because it means we scrolled. If we have no previous
+ // viewport then we must rebuild.
+ const prev_viewport = self.cells_viewport orelse break :rebuild true;
+ if (!prev_viewport.eql(viewport_pin)) break :rebuild true;
+
+ break :rebuild false;
+ };
+
+ // Reset the dirty flags in the terminal and screen. We assume
+ // that our rebuild will be successful since so we optimize for
+ // success and reset while we hold the lock. This is much easier
+ // than coordinating row by row or as changes are persisted.
+ state.terminal.flags.dirty = .{};
+ state.terminal.screen.dirty = .{};
+ {
+ var it = state.terminal.screen.pages.pageIterator(
+ .right_down,
+ .{ .screen = .{} },
+ null,
+ );
+ while (it.next()) |chunk| {
+ var dirty_set = chunk.node.data.dirtyBitSet();
+ dirty_set.unsetAll();
+ }
+ }
+
+ // Update our viewport pin
+ self.cells_viewport = viewport_pin;
+
+ break :critical .{
+ .bg = self.background_color orelse self.default_background_color,
+ .screen = screen_copy,
+ .screen_type = state.terminal.active_screen,
+ .mouse = state.mouse,
+ .preedit = preedit,
+ .cursor_style = cursor_style,
+ .color_palette = state.terminal.color_palette.colors,
+ .full_rebuild = full_rebuild,
+ };
+ };
+ defer {
+ critical.screen.deinit();
+ if (critical.preedit) |p| p.deinit(self.alloc);
+ }
+
+ // Build our GPU cells
+ try self.rebuildCells(
+ critical.full_rebuild,
+ &critical.screen,
+ critical.screen_type,
+ critical.mouse,
+ critical.preedit,
+ critical.cursor_style,
+ &critical.color_palette,
+ );
+
+ // Notify our shaper we're done for the frame. For some shapers,
+ // such as CoreText, this triggers off-thread cleanup logic.
+ self.font_shaper.endFrame();
+
+ // Acquire the draw mutex because we're modifying state here.
+ {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // Update our background color
+ self.uniforms.bg_color = .{
+ critical.bg.r,
+ critical.bg.g,
+ critical.bg.b,
+ @intFromFloat(@round(self.config.background_opacity * 255.0)),
+ };
+ }
+ }
+
+ /// Draw the frame to the screen.
+ ///
+ /// If `sync` is true, this will synchronously block until
+ /// the frame is finished drawing and has been presented.
+ pub fn drawFrame(
+ self: *Self,
+ sync: bool,
+ ) !void {
+ // We hold a the draw mutex to prevent changes to any
+ // data we access while we're in the middle of drawing.
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // Let our graphics API do any bookkeeping, etc.
+ // that it needs to do before / after `drawFrame`.
+ self.api.drawFrameStart();
+ defer self.api.drawFrameEnd();
+
+ // Retrieve the most up-to-date surface size from the Graphics API
+ const surface_size = try self.api.surfaceSize();
+
+ // If either of our surface dimensions is zero
+ // then drawing is absurd, so we just return.
+ if (surface_size.width == 0 or surface_size.height == 0) return;
+
+ const size_changed =
+ self.size.screen.width != surface_size.width or
+ self.size.screen.height != surface_size.height;
+
+ // Conditions under which we need to draw the frame, otherwise we
+ // don't need to since the previous frame should be identical.
+ const needs_redraw =
+ size_changed or
+ self.cells_rebuilt or
+ self.hasAnimations() or
+ sync;
+
+ if (!needs_redraw) {
+ // We still need to present the last target again, because the
+ // apprt may be swapping buffers and display an outdated frame
+ // if we don't draw something new.
+ try self.api.presentLastTarget();
+ return;
+ }
+ self.cells_rebuilt = false;
+
+ // Wait for a frame to be available.
+ const frame = try self.swap_chain.nextFrame();
+ errdefer self.swap_chain.releaseFrame();
+ // log.debug("drawing frame index={}", .{self.swap_chain.frame_index});
+
+ // If we need to reinitialize our shaders, do so.
+ if (self.reinitialize_shaders) {
+ self.reinitialize_shaders = false;
+ self.shaders.deinit(self.alloc);
+ try self.initShaders();
+ }
+
+ // Our shaders should not be defunct at this point.
+ assert(!self.shaders.defunct);
+
+ // If we have custom shaders, make sure we have the
+ // custom shader state in our frame state, otherwise
+ // if we have a state but don't need it we remove it.
+ if (self.has_custom_shaders) {
+ if (frame.custom_shader_state == null) {
+ frame.custom_shader_state = try .init(self.api);
+ try frame.custom_shader_state.?.resize(
+ self.api,
+ surface_size.width,
+ surface_size.height,
+ );
+ }
+ } else if (frame.custom_shader_state) |*state| {
+ state.deinit();
+ frame.custom_shader_state = null;
+ }
+
+ // If our stored size doesn't match the
+ // surface size we need to update it.
+ if (size_changed) {
+ self.size.screen = .{
+ .width = surface_size.width,
+ .height = surface_size.height,
+ };
+ self.updateScreenSizeUniforms();
+ }
+
+ // If this frame's target isn't the correct size, or the target
+ // config has changed (such as when the blending mode changes),
+ // remove it and replace it with a new one with the right values.
+ if (frame.target.width != self.size.screen.width or
+ frame.target.height != self.size.screen.height or
+ frame.target_config_modified != self.target_config_modified)
+ {
+ try frame.resize(
+ self.api,
+ self.size.screen.width,
+ self.size.screen.height,
+ );
+ frame.target_config_modified = self.target_config_modified;
+ }
+
+ // Upload images to the GPU as necessary.
+ {
+ var image_it = self.images.iterator();
+ while (image_it.next()) |kv| {
+ switch (kv.value_ptr.image) {
+ .ready => {},
+
+ .pending_gray,
+ .pending_gray_alpha,
+ .pending_rgb,
+ .pending_rgba,
+ .replace_gray,
+ .replace_gray_alpha,
+ .replace_rgb,
+ .replace_rgba,
+ => try kv.value_ptr.image.upload(self.alloc, &self.api),
+
+ .unload_pending,
+ .unload_replace,
+ .unload_ready,
+ => {
+ kv.value_ptr.image.deinit(self.alloc);
+ self.images.removeByPtr(kv.key_ptr);
+ },
+ }
+ }
+ }
+
+ // Update custom shader uniforms if necessary.
+ try self.updateCustomShaderUniforms();
+
+ // Setup our frame data
+ try frame.uniforms.sync(&.{self.uniforms});
+ try frame.cells_bg.sync(self.cells.bg_cells);
+ const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows.lists);
+
+ // If our font atlas changed, sync the texture data
+ texture: {
+ const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
+ if (modified <= frame.grayscale_modified) break :texture;
+ self.font_grid.lock.lockShared();
+ defer self.font_grid.lock.unlockShared();
+ frame.grayscale_modified = self.font_grid.atlas_grayscale.modified.load(.monotonic);
+ try self.syncAtlasTexture(&self.font_grid.atlas_grayscale, &frame.grayscale);
+ }
+ texture: {
+ const modified = self.font_grid.atlas_color.modified.load(.monotonic);
+ if (modified <= frame.color_modified) break :texture;
+ self.font_grid.lock.lockShared();
+ defer self.font_grid.lock.unlockShared();
+ frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic);
+ try self.syncAtlasTexture(&self.font_grid.atlas_color, &frame.color);
+ }
+
+ // Get a frame context from the graphics API.
+ var frame_ctx = try self.api.beginFrame(self, &frame.target);
+ defer frame_ctx.complete(sync);
+
+ {
+ var pass = frame_ctx.renderPass(&.{.{
+ .target = if (frame.custom_shader_state) |state|
+ .{ .texture = state.back_texture }
+ else
+ .{ .target = frame.target },
+ .clear_color = .{ 0.0, 0.0, 0.0, 0.0 },
+ }});
+ defer pass.complete();
+
+ // bg images
+ try self.drawImagePlacements(&pass, self.image_placements.items[0..self.image_bg_end]);
+ // bg
+ pass.step(.{
+ .pipeline = self.shaders.cell_bg_pipeline,
+ .uniforms = frame.uniforms.buffer,
+ .buffers = &.{ null, frame.cells_bg.buffer },
+ .draw = .{
+ .type = .triangle,
+ .vertex_count = 3,
+ },
+ });
+ // mg images
+ try self.drawImagePlacements(&pass, self.image_placements.items[self.image_bg_end..self.image_text_end]);
+ // text
+ pass.step(.{
+ .pipeline = self.shaders.cell_text_pipeline,
+ .uniforms = frame.uniforms.buffer,
+ .buffers = &.{
+ frame.cells.buffer,
+ frame.cells_bg.buffer,
+ },
+ .textures = &.{
+ frame.grayscale,
+ frame.color,
+ },
+ .draw = .{
+ .type = .triangle_strip,
+ .vertex_count = 4,
+ .instance_count = fg_count,
+ },
+ });
+ // fg images
+ try self.drawImagePlacements(&pass, self.image_placements.items[self.image_text_end..]);
+ }
+
+ // If we have custom shaders, then we render them.
+ if (frame.custom_shader_state) |*state| {
+ // We create a buffer on the GPU for our post uniforms.
+ // TODO: This should be a part of the frame state tbqh.
+ const PostBuffer = Buffer(shadertoy.Uniforms);
+ const uniform_buffer = try PostBuffer.initFill(
+ self.api.bufferOptions(),
+ &.{self.custom_shader_uniforms},
+ );
+ defer uniform_buffer.deinit();
+
+ for (self.shaders.post_pipelines, 0..) |pipeline, i| {
+ defer state.swap();
+
+ var pass = frame_ctx.renderPass(&.{.{
+ .target = if (i < self.shaders.post_pipelines.len - 1)
+ .{ .texture = state.front_texture }
+ else
+ .{ .target = frame.target },
+ .clear_color = .{ 0.0, 0.0, 0.0, 0.0 },
+ }});
+ defer pass.complete();
+
+ pass.step(.{
+ .pipeline = pipeline,
+ .uniforms = uniform_buffer.buffer,
+ .textures = &.{state.back_texture},
+ .draw = .{
+ .type = .triangle,
+ .vertex_count = 3,
+ },
+ });
+ }
+ }
+ }
+
+ // Callback from the graphics API when a frame is completed.
+ pub fn frameCompleted(
+ self: *Self,
+ health: Health,
+ ) void {
+ // If our health value hasn't changed, then we do nothing. We don't
+ // do a cmpxchg here because strict atomicity isn't important.
+ if (self.health.load(.seq_cst) != health) {
+ self.health.store(health, .seq_cst);
+
+ // Our health value changed, so we notify the surface so that it
+ // can do something about it.
+ _ = self.surface_mailbox.push(.{
+ .renderer_health = health,
+ }, .{ .forever = {} });
+ }
+
+ // Always release our semaphore
+ self.swap_chain.releaseFrame();
+ }
+
+ fn drawImagePlacements(
+ self: *Self,
+ pass: *RenderPass,
+ placements: []const imagepkg.Placement,
+ ) !void {
+ if (placements.len == 0) return;
+
+ for (placements) |p| {
+
+ // Look up the image
+ const image = self.images.get(p.image_id) orelse {
+ log.warn("image not found for placement image_id={}", .{p.image_id});
+ return;
+ };
+
+ // Get the texture
+ const texture = switch (image.image) {
+ .ready => |t| t,
+ else => {
+ log.warn("image not ready for placement image_id={}", .{p.image_id});
+ return;
+ },
+ };
+
+ // Create our vertex buffer, which is always exactly one item.
+ // future(mitchellh): we can group rendering multiple instances of a single image
+ var buf = try Buffer(shaderpkg.Image).initFill(
+ self.api.imageBufferOptions(),
+ &.{.{
+ .grid_pos = .{
+ @as(f32, @floatFromInt(p.x)),
+ @as(f32, @floatFromInt(p.y)),
+ },
+
+ .cell_offset = .{
+ @as(f32, @floatFromInt(p.cell_offset_x)),
+ @as(f32, @floatFromInt(p.cell_offset_y)),
+ },
+
+ .source_rect = .{
+ @as(f32, @floatFromInt(p.source_x)),
+ @as(f32, @floatFromInt(p.source_y)),
+ @as(f32, @floatFromInt(p.source_width)),
+ @as(f32, @floatFromInt(p.source_height)),
+ },
+
+ .dest_size = .{
+ @as(f32, @floatFromInt(p.width)),
+ @as(f32, @floatFromInt(p.height)),
+ },
+ }},
+ );
+ defer buf.deinit();
+
+ pass.step(.{
+ .pipeline = self.shaders.image_pipeline,
+ .buffers = &.{buf.buffer},
+ .textures = &.{texture},
+ .draw = .{
+ .type = .triangle_strip,
+ .vertex_count = 4,
+ },
+ });
+ }
+ }
+
+ /// This goes through the Kitty graphic placements and accumulates the
+ /// placements we need to render on our viewport. It also ensures that
+ /// the visible images are loaded on the GPU.
+ fn prepKittyGraphics(
+ self: *Self,
+ t: *terminal.Terminal,
+ ) !void {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ const storage = &t.screen.kitty_images;
+ defer storage.dirty = false;
+
+ // We always clear our previous placements no matter what because
+ // we rebuild them from scratch.
+ self.image_placements.clearRetainingCapacity();
+ self.image_virtual = false;
+
+ // Go through our known images and if there are any that are no longer
+ // in use then mark them to be freed.
+ //
+ // This never conflicts with the below because a placement can't
+ // reference an image that doesn't exist.
+ {
+ var it = self.images.iterator();
+ while (it.next()) |kv| {
+ if (storage.imageById(kv.key_ptr.*) == null) {
+ kv.value_ptr.image.markForUnload();
+ }
+ }
+ }
+
+ // The top-left and bottom-right corners of our viewport in screen
+ // points. This lets us determine offsets and containment of placements.
+ const top = t.screen.pages.getTopLeft(.viewport);
+ const bot = t.screen.pages.getBottomRight(.viewport).?;
+ const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y;
+ const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y;
+
+ // Go through the placements and ensure the image is loaded on the GPU.
+ var it = storage.placements.iterator();
+ while (it.next()) |kv| {
+ const p = kv.value_ptr;
+
+ // Special logic based on location
+ switch (p.location) {
+ .pin => {},
+ .virtual => {
+ // We need to mark virtual placements on our renderer so that
+ // we know to rebuild in more scenarios since cell changes can
+ // now trigger placement changes.
+ self.image_virtual = true;
+
+ // We also continue out because virtual placements are
+ // only triggered by the unicode placeholder, not by the
+ // placement itself.
+ continue;
+ },
+ }
+
+ // Get the image for the placement
+ const image = storage.imageById(kv.key_ptr.image_id) orelse {
+ log.warn(
+ "missing image for placement, ignoring image_id={}",
+ .{kv.key_ptr.image_id},
+ );
+ continue;
+ };
+
+ try self.prepKittyPlacement(t, top_y, bot_y, &image, p);
+ }
+
+ // If we have virtual placements then we need to scan for placeholders.
+ if (self.image_virtual) {
+ var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot);
+ while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement(
+ t,
+ &virtual_p,
+ );
+ }
+
+ // Sort the placements by their Z value.
+ std.mem.sortUnstable(
+ imagepkg.Placement,
+ self.image_placements.items,
+ {},
+ struct {
+ fn lessThan(
+ ctx: void,
+ lhs: imagepkg.Placement,
+ rhs: imagepkg.Placement,
+ ) bool {
+ _ = ctx;
+ return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id);
+ }
+ }.lessThan,
+ );
+
+ // Find our indices. The values are sorted by z so we can find the
+ // first placement out of bounds to find the limits.
+ var bg_end: ?u32 = null;
+ var text_end: ?u32 = null;
+ const bg_limit = std.math.minInt(i32) / 2;
+ for (self.image_placements.items, 0..) |p, i| {
+ if (bg_end == null and p.z >= bg_limit) {
+ bg_end = @intCast(i);
+ }
+ if (text_end == null and p.z >= 0) {
+ text_end = @intCast(i);
+ }
+ }
+
+ self.image_bg_end = bg_end orelse 0;
+ self.image_text_end = text_end orelse self.image_bg_end;
+ }
+
+ fn prepKittyVirtualPlacement(
+ self: *Self,
+ t: *terminal.Terminal,
+ p: *const terminal.kitty.graphics.unicode.Placement,
+ ) !void {
+ const storage = &t.screen.kitty_images;
+ const image = storage.imageById(p.image_id) orelse {
+ log.warn(
+ "missing image for virtual placement, ignoring image_id={}",
+ .{p.image_id},
+ );
+ return;
+ };
+
+ const rp = p.renderPlacement(
+ storage,
+ &image,
+ self.grid_metrics.cell_width,
+ self.grid_metrics.cell_height,
+ ) catch |err| {
+ log.warn("error rendering virtual placement err={}", .{err});
+ return;
+ };
+
+ // If our placement is zero sized then we don't do anything.
+ if (rp.dest_width == 0 or rp.dest_height == 0) return;
+
+ const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
+ .viewport,
+ rp.top_left,
+ ) orelse {
+ // This is unreachable with virtual placements because we should
+ // only ever be looking at virtual placements that are in our
+ // viewport in the renderer and virtual placements only ever take
+ // up one row.
+ unreachable;
+ };
+
+ // Send our image to the GPU and store the placement for rendering.
+ try self.prepKittyImage(&image);
+ try self.image_placements.append(self.alloc, .{
+ .image_id = image.id,
+ .x = @intCast(rp.top_left.x),
+ .y = @intCast(viewport.viewport.y),
+ .z = -1,
+ .width = rp.dest_width,
+ .height = rp.dest_height,
+ .cell_offset_x = rp.offset_x,
+ .cell_offset_y = rp.offset_y,
+ .source_x = rp.source_x,
+ .source_y = rp.source_y,
+ .source_width = rp.source_width,
+ .source_height = rp.source_height,
+ });
+ }
+
+ fn prepKittyPlacement(
+ self: *Self,
+ t: *terminal.Terminal,
+ top_y: u32,
+ bot_y: u32,
+ image: *const terminal.kitty.graphics.Image,
+ p: *const terminal.kitty.graphics.ImageStorage.Placement,
+ ) !void {
+ // Get the rect for the placement. If this placement doesn't have
+ // a rect then its virtual or something so skip it.
+ const rect = p.rect(image.*, t) orelse return;
+
+ // This is expensive but necessary.
+ const img_top_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
+ const img_bot_y = t.screen.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y;
+
+ // If the selection isn't within our viewport then skip it.
+ if (img_top_y > bot_y) return;
+ if (img_bot_y < top_y) return;
+
+ // We need to prep this image for upload if it isn't in the cache OR
+ // it is in the cache but the transmit time doesn't match meaning this
+ // image is different.
+ try self.prepKittyImage(image);
+
+ // Calculate the dimensions of our image, taking in to
+ // account the rows / columns specified by the placement.
+ const dest_size = p.calculatedSize(image.*, t);
+
+ // Calculate the source rectangle
+ const source_x = @min(image.width, p.source_x);
+ const source_y = @min(image.height, p.source_y);
+ const source_width = if (p.source_width > 0)
+ @min(image.width - source_x, p.source_width)
+ else
+ image.width;
+ const source_height = if (p.source_height > 0)
+ @min(image.height - source_y, p.source_height)
+ else
+ image.height;
+
+ // Get the viewport-relative Y position of the placement.
+ const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y));
+
+ // Accumulate the placement
+ if (dest_size.width > 0 and dest_size.height > 0) {
+ try self.image_placements.append(self.alloc, .{
+ .image_id = image.id,
+ .x = @intCast(rect.top_left.x),
+ .y = y_pos,
+ .z = p.z,
+ .width = dest_size.width,
+ .height = dest_size.height,
+ .cell_offset_x = p.x_offset,
+ .cell_offset_y = p.y_offset,
+ .source_x = source_x,
+ .source_y = source_y,
+ .source_width = source_width,
+ .source_height = source_height,
+ });
+ }
+ }
+
+ fn prepKittyImage(
+ self: *Self,
+ image: *const terminal.kitty.graphics.Image,
+ ) !void {
+ // If this image exists and its transmit time is the same we assume
+ // it is the identical image so we don't need to send it to the GPU.
+ const gop = try self.images.getOrPut(self.alloc, image.id);
+ if (gop.found_existing and
+ gop.value_ptr.transmit_time.order(image.transmit_time) == .eq)
+ {
+ return;
+ }
+
+ // Copy the data into the pending state.
+ const data = try self.alloc.dupe(u8, image.data);
+ errdefer self.alloc.free(data);
+
+ // Store it in the map
+ const pending: Image.Pending = .{
+ .width = image.width,
+ .height = image.height,
+ .data = data.ptr,
+ };
+
+ const new_image: Image = switch (image.format) {
+ .gray => .{ .pending_gray = pending },
+ .gray_alpha => .{ .pending_gray_alpha = pending },
+ .rgb => .{ .pending_rgb = pending },
+ .rgba => .{ .pending_rgba = pending },
+ .png => unreachable, // should be decoded by now
+ };
+
+ if (!gop.found_existing) {
+ gop.value_ptr.* = .{
+ .image = new_image,
+ .transmit_time = undefined,
+ };
+ } else {
+ try gop.value_ptr.image.markForReplace(
+ self.alloc,
+ new_image,
+ );
+ }
+
+ gop.value_ptr.transmit_time = image.transmit_time;
+ }
+
+ /// Update the configuration.
+ pub fn changeConfig(self: *Self, config: *DerivedConfig) !void {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // We always redo the font shaper in case font features changed. We
+ // could check to see if there was an actual config change but this is
+ // easier and rare enough to not cause performance issues.
+ {
+ var font_shaper = try font.Shaper.init(self.alloc, .{
+ .features = config.font_features.items,
+ });
+ errdefer font_shaper.deinit();
+ self.font_shaper.deinit();
+ self.font_shaper = font_shaper;
+ }
+
+ // We also need to reset the shaper cache so shaper info
+ // from the previous font isn't re-used for the new font.
+ const font_shaper_cache = font.ShaperCache.init();
+ self.font_shaper_cache.deinit(self.alloc);
+ self.font_shaper_cache = font_shaper_cache;
+
+ // Set our new minimum contrast
+ self.uniforms.min_contrast = config.min_contrast;
+
+ // Set our new color space and blending
+ self.uniforms.bools.use_display_p3 = config.colorspace == .@"display-p3";
+ self.uniforms.bools.use_linear_blending = config.blending.isLinear();
+ self.uniforms.bools.use_linear_correction = config.blending == .@"linear-corrected";
+
+ // Set our new colors
+ self.default_background_color = config.background;
+ self.default_foreground_color = config.foreground;
+ self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
+ self.cursor_invert = config.cursor_invert;
+
+ const old_blending = self.config.blending;
+ const custom_shaders_changed = !self.config.custom_shaders.equal(config.custom_shaders);
+
+ self.config.deinit();
+ self.config = config.*;
+
+ // Reset our viewport to force a rebuild, in case of a font change.
+ self.cells_viewport = null;
+
+ const blending_changed = old_blending != config.blending;
+
+ if (blending_changed) {
+ // We update our API's blending mode.
+ self.api.blending = config.blending;
+ // And indicate that we need to reinitialize our shaders.
+ self.reinitialize_shaders = true;
+ // And indicate that our swap chain targets need to
+ // be re-created to account for the new blending mode.
+ self.target_config_modified +%= 1;
+ }
+
+ if (custom_shaders_changed) {
+ self.reinitialize_shaders = true;
+ }
+ }
+
+ /// Resize the screen.
+ pub fn setScreenSize(
+ self: *Self,
+ size: renderer.Size,
+ ) void {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // We only actually need the padding from this,
+ // everything else is derived elsewhere.
+ self.size.padding = size.padding;
+
+ self.updateScreenSizeUniforms();
+
+ log.debug("screen size size={}", .{size});
+ }
+
+ /// Update uniforms that are based on the screen size.
+ ///
+ /// Caller must hold the draw mutex.
+ fn updateScreenSizeUniforms(self: *Self) void {
+ const terminal_size = self.size.terminal();
+
+ // Blank space around the grid.
+ const blank: renderer.Padding = self.size.screen.blankPadding(
+ self.size.padding,
+ .{
+ .columns = self.cells.size.columns,
+ .rows = self.cells.size.rows,
+ },
+ .{
+ .width = self.grid_metrics.cell_width,
+ .height = self.grid_metrics.cell_height,
+ },
+ ).add(self.size.padding);
+
+ // Setup our uniforms
+ self.uniforms.projection_matrix = math.ortho2d(
+ -1 * @as(f32, @floatFromInt(self.size.padding.left)),
+ @floatFromInt(terminal_size.width + self.size.padding.right),
+ @floatFromInt(terminal_size.height + self.size.padding.bottom),
+ -1 * @as(f32, @floatFromInt(self.size.padding.top)),
+ );
+ self.uniforms.grid_padding = .{
+ @floatFromInt(blank.top),
+ @floatFromInt(blank.right),
+ @floatFromInt(blank.bottom),
+ @floatFromInt(blank.left),
+ };
+ }
+
+ /// Update uniforms for the custom shaders, if necessary.
+ ///
+ /// This should be called exactly once per frame, inside `drawFrame`.
+ fn updateCustomShaderUniforms(
+ self: *Self,
+ ) !void {
+ // We only need to do this if we have custom shaders.
+ if (!self.has_custom_shaders) return;
+
+ const now = try std.time.Instant.now();
+ defer self.last_frame_time = now;
+ const first_frame_time = self.first_frame_time orelse t: {
+ self.first_frame_time = now;
+ break :t now;
+ };
+ const last_frame_time = self.last_frame_time orelse now;
+
+ const since_ns: f32 = @floatFromInt(now.since(first_frame_time));
+ self.custom_shader_uniforms.time = since_ns / std.time.ns_per_s;
+
+ const delta_ns: f32 = @floatFromInt(now.since(last_frame_time));
+ self.custom_shader_uniforms.time_delta = delta_ns / std.time.ns_per_s;
+
+ self.custom_shader_uniforms.frame += 1;
+
+ const screen = self.size.screen;
+ const padding = self.size.padding;
+ const cell = self.size.cell;
+
+ self.custom_shader_uniforms.resolution = .{
+ @floatFromInt(screen.width),
+ @floatFromInt(screen.height),
+ 1,
+ };
+ self.custom_shader_uniforms.channel_resolution[0] = .{
+ @floatFromInt(screen.width),
+ @floatFromInt(screen.height),
+ 1,
+ 0,
+ };
+
+ // Update custom cursor uniforms, if we have a cursor.
+ if (self.cells.fg_rows.lists[0].items.len > 0) {
+ const cursor: shaderpkg.CellText =
+ self.cells.fg_rows.lists[0].items[0];
+
+ const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]);
+ const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]);
+
+ var pixel_x: f32 = @floatFromInt(
+ cursor.grid_pos[0] * cell.width + padding.left,
+ );
+ var pixel_y: f32 = @floatFromInt(
+ cursor.grid_pos[1] * cell.height + padding.top,
+ );
+
+ pixel_x += @floatFromInt(cursor.bearings[0]);
+ pixel_y += @floatFromInt(cursor.bearings[1]);
+
+ // If +Y is up in our shaders, we need to flip the coordinate.
+ if (!GraphicsAPI.custom_shader_y_is_down) {
+ pixel_y = @as(f32, @floatFromInt(screen.height)) - pixel_y;
+ // We need to add the cursor height because we need the +Y
+ // edge for the Y coordinate, and flipping means that it's
+ // the -Y edge now.
+ pixel_y += cursor_height;
+ }
+
+ const new_cursor: [4]f32 = .{
+ pixel_x,
+ pixel_y,
+ cursor_width,
+ cursor_height,
+ };
+ const cursor_color: [4]f32 = .{
+ @as(f32, @floatFromInt(cursor.color[0])) / 255.0,
+ @as(f32, @floatFromInt(cursor.color[1])) / 255.0,
+ @as(f32, @floatFromInt(cursor.color[2])) / 255.0,
+ @as(f32, @floatFromInt(cursor.color[3])) / 255.0,
+ };
+
+ const uniforms = &self.custom_shader_uniforms;
+
+ const cursor_changed: bool =
+ !std.meta.eql(new_cursor, uniforms.current_cursor) or
+ !std.meta.eql(cursor_color, uniforms.current_cursor_color);
+
+ if (cursor_changed) {
+ uniforms.previous_cursor = uniforms.current_cursor;
+ uniforms.previous_cursor_color = uniforms.current_cursor_color;
+ uniforms.current_cursor = new_cursor;
+ uniforms.current_cursor_color = cursor_color;
+ uniforms.cursor_change_time = uniforms.time;
+ }
+ }
+ }
+
+ /// Convert the terminal state to GPU cells stored in CPU memory. These
+ /// are then synced to the GPU in the next frame. This only updates CPU
+ /// memory and doesn't touch the GPU.
+ fn rebuildCells(
+ self: *Self,
+ wants_rebuild: bool,
+ screen: *terminal.Screen,
+ screen_type: terminal.ScreenType,
+ mouse: renderer.State.Mouse,
+ preedit: ?renderer.State.Preedit,
+ cursor_style_: ?renderer.CursorStyle,
+ color_palette: *const terminal.color.Palette,
+ ) !void {
+ self.draw_mutex.lock();
+ defer self.draw_mutex.unlock();
+
+ // const start = try std.time.Instant.now();
+ // const start_micro = std.time.microTimestamp();
+ // defer {
+ // const end = std.time.Instant.now() catch unreachable;
+ // // "[rebuildCells time] \t"
+ // std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us});
+ // }
+
+ _ = screen_type; // we might use this again later so not deleting it yet
+
+ // Create an arena for all our temporary allocations while rebuilding
+ var arena = ArenaAllocator.init(self.alloc);
+ defer arena.deinit();
+ const arena_alloc = arena.allocator();
+
+ // Create our match set for the links.
+ var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet(
+ arena_alloc,
+ screen,
+ mouse_pt,
+ mouse.mods,
+ ) else .{};
+
+ // Determine our x/y range for preedit. We don't want to render anything
+ // here because we will render the preedit separately.
+ const preedit_range: ?struct {
+ y: terminal.size.CellCountInt,
+ x: [2]terminal.size.CellCountInt,
+ cp_offset: usize,
+ } = if (preedit) |preedit_v| preedit: {
+ const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1);
+ break :preedit .{
+ .y = screen.cursor.y,
+ .x = .{ range.start, range.end },
+ .cp_offset = range.cp_offset,
+ };
+ } else null;
+
+ const grid_size_diff =
+ self.cells.size.rows != screen.pages.rows or
+ self.cells.size.columns != screen.pages.cols;
+
+ if (grid_size_diff) {
+ var new_size = self.cells.size;
+ new_size.rows = screen.pages.rows;
+ new_size.columns = screen.pages.cols;
+ try self.cells.resize(self.alloc, new_size);
+
+ // Update our uniforms accordingly, otherwise
+ // our background cells will be out of place.
+ self.uniforms.grid_size = .{ new_size.columns, new_size.rows };
+ }
+
+ const rebuild = wants_rebuild or grid_size_diff;
+
+ if (rebuild) {
+ // If we are doing a full rebuild, then we clear the entire cell buffer.
+ self.cells.reset();
+
+ // We also reset our padding extension depending on the screen type
+ switch (self.config.padding_color) {
+ .background => {},
+
+ // For extension, assume we are extending in all directions.
+ // For "extend" this may be disabled due to heuristics below.
+ .extend, .@"extend-always" => {
+ self.uniforms.padding_extend = .{
+ .up = true,
+ .down = true,
+ .left = true,
+ .right = true,
+ };
+ },
+ }
+ }
+
+ // We rebuild the cells row-by-row because we
+ // do font shaping and dirty tracking by row.
+ var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
+ // If our cell contents buffer is shorter than the screen viewport,
+ // we render the rows that fit, starting from the bottom. If instead
+ // the viewport is shorter than the cell contents buffer, we align
+ // the top of the viewport with the top of the contents buffer.
+ var y: terminal.size.CellCountInt = @min(
+ screen.pages.rows,
+ self.cells.size.rows,
+ );
+ while (row_it.next()) |row| {
+ // The viewport may have more rows than our cell contents,
+ // so we need to break from the loop early if we hit y = 0.
+ if (y == 0) break;
+
+ y -= 1;
+
+ if (!rebuild) {
+ // Only rebuild if we are doing a full rebuild or this row is dirty.
+ if (!row.isDirty()) continue;
+
+ // Clear the cells if the row is dirty
+ self.cells.clear(y);
+ }
+
+ // True if we want to do font shaping around the cursor.
+ // We want to do font shaping as long as the cursor is enabled.
+ const shape_cursor = screen.viewportIsBottom() and
+ y == screen.cursor.y;
+
+ // We need to get this row's selection, if
+ // there is one, for proper run splitting.
+ const row_selection = sel: {
+ const sel = screen.selection orelse break :sel null;
+ const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse
+ break :sel null;
+ break :sel sel.containedRow(screen, pin) orelse null;
+ };
+
+ // On primary screen, we still apply vertical padding
+ // extension under certain conditions we feel are safe.
+ //
+ // This helps make some scenarios look better while
+ // avoiding scenarios we know do NOT look good.
+ switch (self.config.padding_color) {
+ // These already have the correct values set above.
+ .background, .@"extend-always" => {},
+
+ // Apply heuristics for padding extension.
+ .extend => if (y == 0) {
+ self.uniforms.padding_extend.up = !row.neverExtendBg(
+ color_palette,
+ self.background_color orelse self.default_background_color,
+ );
+ } else if (y == self.cells.size.rows - 1) {
+ self.uniforms.padding_extend.down = !row.neverExtendBg(
+ color_palette,
+ self.background_color orelse self.default_background_color,
+ );
+ },
+ }
+
+ // Iterator of runs for shaping.
+ var run_iter = self.font_shaper.runIterator(
+ self.font_grid,
+ screen,
+ row,
+ row_selection,
+ if (shape_cursor) screen.cursor.x else null,
+ );
+ var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc);
+ var shaper_cells: ?[]const font.shape.Cell = null;
+ var shaper_cells_i: usize = 0;
+
+ const row_cells_all = row.cells(.all);
+
+ // If our viewport is wider than our cell contents buffer,
+ // we still only process cells up to the width of the buffer.
+ const row_cells = row_cells_all[0..@min(row_cells_all.len, self.cells.size.columns)];
+
+ for (row_cells, 0..) |*cell, x| {
+ // If this cell falls within our preedit range then we
+ // skip this because preedits are setup separately.
+ if (preedit_range) |range| preedit: {
+ // We're not on the preedit line, no actions necessary.
+ if (range.y != y) break :preedit;
+ // We're before the preedit range, no actions necessary.
+ if (x < range.x[0]) break :preedit;
+ // We're in the preedit range, skip this cell.
+ if (x <= range.x[1]) continue;
+ // After exiting the preedit range we need to catch
+ // the run position up because of the missed cells.
+ // In all other cases, no action is necessary.
+ if (x != range.x[1] + 1) break :preedit;
+
+ // Step the run iterator until we find a run that ends
+ // after the current cell, which will be the soonest run
+ // that might contain glyphs for our cell.
+ while (shaper_run) |run| {
+ if (run.offset + run.cells > x) break;
+ shaper_run = try run_iter.next(self.alloc);
+ shaper_cells = null;
+ shaper_cells_i = 0;
+ }
+
+ const run = shaper_run orelse break :preedit;
+
+ // If we haven't shaped this run, do so now.
+ shaper_cells = shaper_cells orelse
+ // Try to read the cells from the shaping cache if we can.
+ self.font_shaper_cache.get(run) orelse
+ cache: {
+ // Otherwise we have to shape them.
+ const cells = try self.font_shaper.shape(run);
+
+ // Try to cache them. If caching fails for any reason we
+ // continue because it is just a performance optimization,
+ // not a correctness issue.
+ self.font_shaper_cache.put(
+ self.alloc,
+ run,
+ cells,
+ ) catch |err| {
+ log.warn(
+ "error caching font shaping results err={}",
+ .{err},
+ );
+ };
+
+ // The cells we get from direct shaping are always owned
+ // by the shaper and valid until the next shaping call so
+ // we can safely use them.
+ break :cache cells;
+ };
+
+ // Advance our index until we reach or pass
+ // our current x position in the shaper cells.
+ while (shaper_cells.?[shaper_cells_i].x < x) {
+ shaper_cells_i += 1;
+ }
+ }
+
+ const wide = cell.wide;
+
+ const style = row.style(cell);
+
+ const cell_pin: terminal.Pin = cell: {
+ var copy = row;
+ copy.x = @intCast(x);
+ break :cell copy;
+ };
+
+ // True if this cell is selected
+ const selected: bool = if (screen.selection) |sel|
+ sel.contains(screen, .{
+ .node = row.node,
+ .y = row.y,
+ .x = @intCast(
+ // Spacer tails should show the selection
+ // state of the wide cell they belong to.
+ if (wide == .spacer_tail)
+ x -| 1
+ else
+ x,
+ ),
+ })
+ else
+ false;
+
+ const bg_style = style.bg(cell, color_palette);
+ const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color;
+
+ // The final background color for the cell.
+ const bg = bg: {
+ if (selected) {
+ break :bg if (self.config.invert_selection_fg_bg)
+ if (style.flags.inverse)
+ // Cell is selected with invert selection fg/bg
+ // enabled, and the cell has the inverse style
+ // flag, so they cancel out and we get the normal
+ // bg color.
+ bg_style
+ else
+ // If it doesn't have the inverse style
+ // flag then we use the fg color instead.
+ fg_style
+ else
+ // If we don't have invert selection fg/bg set then we
+ // just use the selection background if set, otherwise
+ // the default fg color.
+ break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color;
+ }
+
+ // Not selected
+ break :bg if (style.flags.inverse != isCovering(cell.codepoint()))
+ // Two cases cause us to invert (use the fg color as the bg)
+ // - The "inverse" style flag.
+ // - A "covering" glyph; we use fg for bg in that
+ // case to help make sure that padding extension
+ // works correctly.
+ //
+ // If one of these is true (but not the other)
+ // then we use the fg style color for the bg.
+ fg_style
+ else
+ // Otherwise they cancel out.
+ bg_style;
+ };
+
+ const fg = fg: {
+ if (selected and !self.config.invert_selection_fg_bg) {
+ // If we don't have invert selection fg/bg set
+ // then we just use the selection foreground if
+ // set, otherwise the default bg color.
+ break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color;
+ }
+
+ // Whether we need to use the bg color as our fg color:
+ // - Cell is inverted and not selected
+ // - Cell is selected and not inverted
+ // Note: if selected then invert sel fg / bg must be
+ // false since we separately handle it if true above.
+ break :fg if (style.flags.inverse != selected)
+ bg_style orelse self.background_color orelse self.default_background_color
+ else
+ fg_style;
+ };
+
+ // Foreground alpha for this cell.
+ const alpha: u8 = if (style.flags.faint) 175 else 255;
+
+ // Set the cell's background color.
+ {
+ const rgb = bg orelse self.background_color orelse self.default_background_color;
+
+ // Determine our background alpha. If we have transparency configured
+ // then this is dynamic depending on some situations. This is all
+ // in an attempt to make transparency look the best for various
+ // situations. See inline comments.
+ const bg_alpha: u8 = bg_alpha: {
+ const default: u8 = 255;
+
+ if (self.config.background_opacity >= 1) break :bg_alpha default;
+
+ // Cells that are selected should be fully opaque.
+ if (selected) break :bg_alpha default;
+
+ // Cells that are reversed should be fully opaque.
+ if (style.flags.inverse) break :bg_alpha default;
+
+ // Cells that have an explicit bg color should be fully opaque.
+ if (bg_style != null) {
+ break :bg_alpha default;
+ }
+
+ // Otherwise, we use the configured background opacity.
+ break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0));
+ };
+
+ self.cells.bgCell(y, x).* = .{
+ rgb.r, rgb.g, rgb.b, bg_alpha,
+ };
+ }
+
+ // If the invisible flag is set on this cell then we
+ // don't need to render any foreground elements, so
+ // we just skip all glyphs with this x coordinate.
+ //
+ // NOTE: This behavior matches xterm. Some other terminal
+ // emulators, e.g. Alacritty, still render text decorations
+ // and only make the text itself invisible. The decision
+ // has been made here to match xterm's behavior for this.
+ if (style.flags.invisible) {
+ continue;
+ }
+
+ // Give links a single underline, unless they already have
+ // an underline, in which case use a double underline to
+ // distinguish them.
+ const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin))
+ if (style.flags.underline == .single)
+ .double
+ else
+ .single
+ else
+ style.flags.underline;
+
+ // We draw underlines first so that they layer underneath text.
+ // This improves readability when a colored underline is used
+ // which intersects parts of the text (descenders).
+ if (underline != .none) self.addUnderline(
+ @intCast(x),
+ @intCast(y),
+ underline,
+ style.underlineColor(color_palette) orelse fg,
+ alpha,
+ ) catch |err| {
+ log.warn(
+ "error adding underline to cell, will be invalid x={} y={}, err={}",
+ .{ x, y, err },
+ );
+ };
+
+ if (style.flags.overline) self.addOverline(@intCast(x), @intCast(y), fg, alpha) catch |err| {
+ log.warn(
+ "error adding overline to cell, will be invalid x={} y={}, err={}",
+ .{ x, y, err },
+ );
+ };
+
+ // If we're at or past the end of our shaper run then
+ // we need to get the next run from the run iterator.
+ if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) {
+ shaper_run = try run_iter.next(self.alloc);
+ shaper_cells = null;
+ shaper_cells_i = 0;
+ }
+
+ if (shaper_run) |run| glyphs: {
+ // If we haven't shaped this run yet, do so.
+ shaper_cells = shaper_cells orelse
+ // Try to read the cells from the shaping cache if we can.
+ self.font_shaper_cache.get(run) orelse
+ cache: {
+ // Otherwise we have to shape them.
+ const cells = try self.font_shaper.shape(run);
+
+ // Try to cache them. If caching fails for any reason we
+ // continue because it is just a performance optimization,
+ // not a correctness issue.
+ self.font_shaper_cache.put(
+ self.alloc,
+ run,
+ cells,
+ ) catch |err| {
+ log.warn(
+ "error caching font shaping results err={}",
+ .{err},
+ );
+ };
+
+ // The cells we get from direct shaping are always owned
+ // by the shaper and valid until the next shaping call so
+ // we can safely use them.
+ break :cache cells;
+ };
+
+ const cells = shaper_cells orelse break :glyphs;
+
+ // If there are no shaper cells for this run, ignore it.
+ // This can occur for runs of empty cells, and is fine.
+ if (cells.len == 0) break :glyphs;
+
+ // If we encounter a shaper cell to the left of the current
+ // cell then we have some problems. This logic relies on x
+ // position monotonically increasing.
+ assert(cells[shaper_cells_i].x >= x);
+
+ // NOTE: An assumption is made here that a single cell will never
+ // be present in more than one shaper run. If that assumption is
+ // violated, this logic breaks.
+
+ while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({
+ shaper_cells_i += 1;
+ }) {
+ self.addGlyph(
+ @intCast(x),
+ @intCast(y),
+ cell_pin,
+ cells[shaper_cells_i],
+ shaper_run.?,
+ fg,
+ alpha,
+ ) catch |err| {
+ log.warn(
+ "error adding glyph to cell, will be invalid x={} y={}, err={}",
+ .{ x, y, err },
+ );
+ };
+ }
+ }
+
+ // Finally, draw a strikethrough if necessary.
+ if (style.flags.strikethrough) self.addStrikethrough(
+ @intCast(x),
+ @intCast(y),
+ fg,
+ alpha,
+ ) catch |err| {
+ log.warn(
+ "error adding strikethrough to cell, will be invalid x={} y={}, err={}",
+ .{ x, y, err },
+ );
+ };
+ }
+ }
+
+ // Setup our cursor rendering information.
+ cursor: {
+ // By default, we don't handle cursor inversion on the shader.
+ self.cells.setCursor(null);
+ self.uniforms.cursor_pos = .{
+ std.math.maxInt(u16),
+ std.math.maxInt(u16),
+ };
+
+ // If we have preedit text, we don't setup a cursor
+ if (preedit != null) break :cursor;
+
+ // Prepare the cursor cell contents.
+ const style = cursor_style_ orelse break :cursor;
+ const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: {
+ if (self.cursor_invert) {
+ // Use the foreground color from the cell under the cursor, if any.
+ const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
+ break :color if (sty.flags.inverse)
+ // If the cell is reversed, use background color instead.
+ (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color)
+ else
+ (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color);
+ } else {
+ break :color self.foreground_color orelse self.default_foreground_color;
+ }
+ };
+
+ self.addCursor(screen, style, cursor_color);
+
+ // If the cursor is visible then we set our uniforms.
+ if (style == .block and screen.viewportIsBottom()) {
+ const wide = screen.cursor.page_cell.wide;
+
+ self.uniforms.cursor_pos = .{
+ // If we are a spacer tail of a wide cell, our cursor needs
+ // to move back one cell. The saturate is to ensure we don't
+ // overflow but this shouldn't happen with well-formed input.
+ switch (wide) {
+ .narrow, .spacer_head, .wide => screen.cursor.x,
+ .spacer_tail => screen.cursor.x -| 1,
+ },
+ screen.cursor.y,
+ };
+
+ self.uniforms.bools.cursor_wide = switch (wide) {
+ .narrow, .spacer_head => false,
+ .wide, .spacer_tail => true,
+ };
+
+ const uniform_color = if (self.cursor_invert) blk: {
+ // Use the background color from the cell under the cursor, if any.
+ const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
+ break :blk if (sty.flags.inverse)
+ // If the cell is reversed, use foreground color instead.
+ (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color)
+ else
+ (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color);
+ } else if (self.config.cursor_text) |txt|
+ txt
+ else
+ self.background_color orelse self.default_background_color;
+
+ self.uniforms.cursor_color = .{
+ uniform_color.r,
+ uniform_color.g,
+ uniform_color.b,
+ 255,
+ };
+ }
+ }
+
+ // Setup our preedit text.
+ if (preedit) |preedit_v| {
+ const range = preedit_range.?;
+ var x = range.x[0];
+ for (preedit_v.codepoints[range.cp_offset..]) |cp| {
+ self.addPreeditCell(cp, .{ .x = x, .y = range.y }) catch |err| {
+ log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{
+ x,
+ range.y,
+ err,
+ });
+ };
+
+ x += if (cp.wide) 2 else 1;
+ }
+ }
+
+ // Update that our cells rebuilt
+ self.cells_rebuilt = true;
+
+ // Log some things
+ // log.debug("rebuildCells complete cached_runs={}", .{
+ // self.font_shaper_cache.count(),
+ // });
+ }
+
+ /// Add an underline decoration to the specified cell
+ fn addUnderline(
+ self: *Self,
+ x: terminal.size.CellCountInt,
+ y: terminal.size.CellCountInt,
+ style: terminal.Attribute.Underline,
+ color: terminal.color.RGB,
+ alpha: u8,
+ ) !void {
+ const sprite: font.Sprite = switch (style) {
+ .none => unreachable,
+ .single => .underline,
+ .double => .underline_double,
+ .dotted => .underline_dotted,
+ .dashed => .underline_dashed,
+ .curly => .underline_curly,
+ };
+
+ const render = try self.font_grid.renderGlyph(
+ self.alloc,
+ font.sprite_index,
+ @intFromEnum(sprite),
+ .{
+ .cell_width = 1,
+ .grid_metrics = self.grid_metrics,
+ },
+ );
+
+ try self.cells.add(self.alloc, .underline, .{
+ .mode = .fg,
+ .grid_pos = .{ @intCast(x), @intCast(y) },
+ .constraint_width = 1,
+ .color = .{ color.r, color.g, color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ });
+ }
+
+ /// Add a overline decoration to the specified cell
+ fn addOverline(
+ self: *Self,
+ x: terminal.size.CellCountInt,
+ y: terminal.size.CellCountInt,
+ color: terminal.color.RGB,
+ alpha: u8,
+ ) !void {
+ const render = try self.font_grid.renderGlyph(
+ self.alloc,
+ font.sprite_index,
+ @intFromEnum(font.Sprite.overline),
+ .{
+ .cell_width = 1,
+ .grid_metrics = self.grid_metrics,
+ },
+ );
+
+ try self.cells.add(self.alloc, .overline, .{
+ .mode = .fg,
+ .grid_pos = .{ @intCast(x), @intCast(y) },
+ .constraint_width = 1,
+ .color = .{ color.r, color.g, color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ });
+ }
+
+ /// Add a strikethrough decoration to the specified cell
+ fn addStrikethrough(
+ self: *Self,
+ x: terminal.size.CellCountInt,
+ y: terminal.size.CellCountInt,
+ color: terminal.color.RGB,
+ alpha: u8,
+ ) !void {
+ const render = try self.font_grid.renderGlyph(
+ self.alloc,
+ font.sprite_index,
+ @intFromEnum(font.Sprite.strikethrough),
+ .{
+ .cell_width = 1,
+ .grid_metrics = self.grid_metrics,
+ },
+ );
+
+ try self.cells.add(self.alloc, .strikethrough, .{
+ .mode = .fg,
+ .grid_pos = .{ @intCast(x), @intCast(y) },
+ .constraint_width = 1,
+ .color = .{ color.r, color.g, color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ });
+ }
+
+ // Add a glyph to the specified cell.
+ fn addGlyph(
+ self: *Self,
+ x: terminal.size.CellCountInt,
+ y: terminal.size.CellCountInt,
+ cell_pin: terminal.Pin,
+ shaper_cell: font.shape.Cell,
+ shaper_run: font.shape.TextRun,
+ color: terminal.color.RGB,
+ alpha: u8,
+ ) !void {
+ const rac = cell_pin.rowAndCell();
+ const cell = rac.cell;
+
+ // Render
+ const render = try self.font_grid.renderGlyph(
+ self.alloc,
+ shaper_run.font_index,
+ shaper_cell.glyph_index,
+ .{
+ .grid_metrics = self.grid_metrics,
+ .thicken = self.config.font_thicken,
+ .thicken_strength = self.config.font_thicken_strength,
+ },
+ );
+
+ // If the glyph is 0 width or height, it will be invisible
+ // when drawn, so don't bother adding it to the buffer.
+ if (render.glyph.width == 0 or render.glyph.height == 0) {
+ return;
+ }
+
+ const mode: shaderpkg.CellText.Mode = switch (try fgMode(
+ render.presentation,
+ cell_pin,
+ )) {
+ .normal => .fg,
+ .color => .fg_color,
+ .constrained => .fg_constrained,
+ .powerline => .fg_powerline,
+ };
+
+ try self.cells.add(self.alloc, .text, .{
+ .mode = mode,
+ .grid_pos = .{ @intCast(x), @intCast(y) },
+ .constraint_width = cell.gridWidth(),
+ .color = .{ color.r, color.g, color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x + shaper_cell.x_offset),
+ @intCast(render.glyph.offset_y + shaper_cell.y_offset),
+ },
+ });
+ }
+
+ fn addCursor(
+ self: *Self,
+ screen: *terminal.Screen,
+ cursor_style: renderer.CursorStyle,
+ cursor_color: terminal.color.RGB,
+ ) void {
+ // Add the cursor. We render the cursor over the wide character if
+ // we're on the wide character tail.
+ const wide, const x = cell: {
+ // The cursor goes over the screen cursor position.
+ const cell = screen.cursor.page_cell;
+ if (cell.wide != .spacer_tail or screen.cursor.x == 0)
+ break :cell .{ cell.wide == .wide, screen.cursor.x };
+
+ // If we're part of a wide character, we move the cursor back to
+ // the actual character.
+ const prev_cell = screen.cursorCellLeft(1);
+ break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 };
+ };
+
+ const alpha: u8 = if (!self.focused) 255 else alpha: {
+ const alpha = 255 * self.config.cursor_opacity;
+ break :alpha @intFromFloat(@ceil(alpha));
+ };
+
+ const render = switch (cursor_style) {
+ .block,
+ .block_hollow,
+ .bar,
+ .underline,
+ => render: {
+ const sprite: font.Sprite = switch (cursor_style) {
+ .block => .cursor_rect,
+ .block_hollow => .cursor_hollow_rect,
+ .bar => .cursor_bar,
+ .underline => .underline,
+ .lock => unreachable,
+ };
+
+ break :render self.font_grid.renderGlyph(
+ self.alloc,
+ font.sprite_index,
+ @intFromEnum(sprite),
+ .{
+ .cell_width = if (wide) 2 else 1,
+ .grid_metrics = self.grid_metrics,
+ },
+ ) catch |err| {
+ log.warn("error rendering cursor glyph err={}", .{err});
+ return;
+ };
+ },
+
+ .lock => self.font_grid.renderCodepoint(
+ self.alloc,
+ 0xF023, // lock symbol
+ .regular,
+ .text,
+ .{
+ .cell_width = if (wide) 2 else 1,
+ .grid_metrics = self.grid_metrics,
+ },
+ ) catch |err| {
+ log.warn("error rendering cursor glyph err={}", .{err});
+ return;
+ } orelse {
+ // This should never happen because we embed nerd
+ // fonts so we just log and return instead of fallback.
+ log.warn("failed to find lock symbol for cursor codepoint=0xF023", .{});
+ return;
+ },
+ };
+
+ self.cells.setCursor(.{
+ .mode = .cursor,
+ .grid_pos = .{ x, screen.cursor.y },
+ .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ });
+ }
+
+ fn addPreeditCell(
+ self: *Self,
+ cp: renderer.State.Preedit.Codepoint,
+ coord: terminal.Coordinate,
+ ) !void {
+ // Preedit is rendered inverted
+ const bg = self.foreground_color orelse self.default_foreground_color;
+ const fg = self.background_color orelse self.default_background_color;
+
+ // Render the glyph for our preedit text
+ const render_ = self.font_grid.renderCodepoint(
+ self.alloc,
+ @intCast(cp.codepoint),
+ .regular,
+ .text,
+ .{ .grid_metrics = self.grid_metrics },
+ ) catch |err| {
+ log.warn("error rendering preedit glyph err={}", .{err});
+ return;
+ };
+ const render = render_ orelse {
+ log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint});
+ return;
+ };
+
+ // Add our opaque background cell
+ self.cells.bgCell(coord.y, coord.x).* = .{
+ bg.r, bg.g, bg.b, 255,
+ };
+ if (cp.wide and coord.x < self.cells.size.columns - 1) {
+ self.cells.bgCell(coord.y, coord.x + 1).* = .{
+ bg.r, bg.g, bg.b, 255,
+ };
+ }
+
+ // Add our text
+ try self.cells.add(self.alloc, .text, .{
+ .mode = .fg,
+ .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
+ .color = .{ fg.r, fg.g, fg.b, 255 },
+ .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
+ .glyph_size = .{ render.glyph.width, render.glyph.height },
+ .bearings = .{
+ @intCast(render.glyph.offset_x),
+ @intCast(render.glyph.offset_y),
+ },
+ });
+ }
+
+ /// Sync the atlas data to the given texture. This copies the bytes
+ /// associated with the atlas to the given texture. If the atlas no
+ /// longer fits into the texture, the texture will be resized.
+ fn syncAtlasTexture(
+ self: *const Self,
+ atlas: *const font.Atlas,
+ texture: *Texture,
+ ) !void {
+ if (atlas.size > texture.width) {
+ // Free our old texture
+ texture.*.deinit();
+
+ // Reallocate
+ texture.* = try self.api.initAtlasTexture(atlas);
+ }
+
+ try texture.replaceRegion(0, 0, atlas.size, atlas.size, atlas.data);
+ }
+ };
+}
diff --git a/src/renderer/metal/Frame.zig b/src/renderer/metal/Frame.zig
new file mode 100644
index 000000000..81b38e7b6
--- /dev/null
+++ b/src/renderer/metal/Frame.zig
@@ -0,0 +1,137 @@
+//! Wrapper for handling render passes.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const builtin = @import("builtin");
+const objc = @import("objc");
+
+const mtl = @import("api.zig");
+const Renderer = @import("../generic.zig").Renderer(Metal);
+const Metal = @import("../Metal.zig");
+const Target = @import("Target.zig");
+const Pipeline = @import("Pipeline.zig");
+const RenderPass = @import("RenderPass.zig");
+const Buffer = @import("buffer.zig").Buffer;
+
+const Health = @import("../../renderer.zig").Health;
+
+const log = std.log.scoped(.metal);
+
+/// Options for beginning a frame.
+pub const Options = struct {
+ /// MTLCommandQueue
+ queue: objc.Object,
+};
+
+/// MTLCommandBuffer
+buffer: objc.Object,
+
+block: CompletionBlock,
+
+/// Begin encoding a frame.
+pub fn begin(
+ opts: Options,
+ /// Once the frame has been completed, the `frameCompleted` method
+ /// on the renderer is called with the health status of the frame.
+ renderer: *Renderer,
+ /// The target is presented via the provided renderer's API when completed.
+ target: *Target,
+) !Self {
+ const buffer = opts.queue.msgSend(
+ objc.Object,
+ objc.sel("commandBuffer"),
+ .{},
+ );
+
+ // Create our block to register for completion updates.
+ // The block is deallocated by the objC runtime on success.
+ const block = try CompletionBlock.init(
+ .{
+ .renderer = renderer,
+ .target = target,
+ .sync = false,
+ },
+ &bufferCompleted,
+ );
+ errdefer block.deinit();
+
+ return .{ .buffer = buffer, .block = block };
+}
+
+/// This is the block type used for the addCompletedHandler callback.
+const CompletionBlock = objc.Block(struct {
+ renderer: *Renderer,
+ target: *Target,
+ sync: bool,
+}, .{
+ objc.c.id, // MTLCommandBuffer
+}, void);
+
+fn bufferCompleted(
+ block: *const CompletionBlock.Context,
+ buffer_id: objc.c.id,
+) callconv(.c) void {
+ const buffer = objc.Object.fromId(buffer_id);
+
+ // Get our command buffer status to pass back to the generic renderer.
+ const status = buffer.getProperty(mtl.MTLCommandBufferStatus, "status");
+ const health: Health = switch (status) {
+ .@"error" => .unhealthy,
+ else => .healthy,
+ };
+
+ // If the frame is healthy, present it.
+ if (health == .healthy) {
+ block.renderer.api.present(
+ block.target.*,
+ block.sync,
+ ) catch |err| {
+ log.err("Failed to present render target: err={}", .{err});
+ };
+ }
+
+ block.renderer.frameCompleted(health);
+}
+
+/// Add a render pass to this frame with the provided attachments.
+/// Returns a RenderPass which allows render steps to be added.
+pub inline fn renderPass(
+ self: *const Self,
+ attachments: []const RenderPass.Options.Attachment,
+) RenderPass {
+ return RenderPass.begin(.{
+ .attachments = attachments,
+ .command_buffer = self.buffer,
+ });
+}
+
+/// Complete this frame and present the target.
+///
+/// If `sync` is true, this will block until the frame is presented.
+pub inline fn complete(self: *Self, sync: bool) void {
+ // If we don't need to complete synchronously,
+ // we add our block as a completion handler.
+ //
+ // It will be deallocated by the objc runtime on success.
+ if (!sync) {
+ self.buffer.msgSend(
+ void,
+ objc.sel("addCompletedHandler:"),
+ .{self.block.context},
+ );
+ }
+
+ self.buffer.msgSend(void, objc.sel("commit"), .{});
+
+ // If we need to complete synchronously, we wait until
+ // the buffer is completed and call the callback directly,
+ // deiniting the block after we're done.
+ if (sync) {
+ self.buffer.msgSend(void, "waitUntilCompleted", .{});
+ self.block.context.sync = true;
+ bufferCompleted(self.block.context, self.buffer.value);
+ self.block.deinit();
+ }
+}
diff --git a/src/renderer/metal/IOSurfaceLayer.zig b/src/renderer/metal/IOSurfaceLayer.zig
new file mode 100644
index 000000000..9212bd5e1
--- /dev/null
+++ b/src/renderer/metal/IOSurfaceLayer.zig
@@ -0,0 +1,190 @@
+//! A wrapper around a CALayer with a utility method
+//! for settings its `contents` to an IOSurface.
+const IOSurfaceLayer = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const builtin = @import("builtin");
+const objc = @import("objc");
+const macos = @import("macos");
+
+const IOSurface = macos.iosurface.IOSurface;
+
+const log = std.log.scoped(.IOSurfaceLayer);
+
+/// We subclass CALayer with a custom display handler, we only need
+/// to make the subclass once, and then we can use it as a singleton.
+var Subclass: ?objc.Class = null;
+
+/// The underlying CALayer
+layer: objc.Object,
+
+pub fn init() !IOSurfaceLayer {
+ // The layer returned by `[CALayer layer]` is autoreleased, which means
+ // that at the end of the current autorelease pool it will be deallocated
+ // if it isn't retained, so we retain it here manually an extra time.
+ const layer = (try getSubclass()).msgSend(
+ objc.Object,
+ objc.sel("layer"),
+ .{},
+ ).retain();
+ errdefer layer.release();
+
+ // The layer gravity is set to top-left so that the contents aren't
+ // stretched during resize operations before a new frame has been drawn.
+ layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft);
+
+ layer.setInstanceVariable("display_cb", .{ .value = null });
+ layer.setInstanceVariable("display_ctx", .{ .value = null });
+
+ return .{ .layer = layer };
+}
+
+pub fn release(self: *IOSurfaceLayer) void {
+ self.layer.release();
+}
+
+/// Sets the layer's `contents` to the provided IOSurface.
+///
+/// Makes sure to do so on the main thread to avoid visual artifacts.
+pub inline fn setSurface(self: *IOSurfaceLayer, surface: *IOSurface) !void {
+ // We retain the surface to make sure it's not GC'd
+ // before we can set it as the contents of the layer.
+ //
+ // We release in the callback after setting the contents.
+ surface.retain();
+ // We also need to retain the layer itself to make sure it
+ // isn't destroyed before the callback completes, since if
+ // that happens it will try to interact with a deallocated
+ // object.
+ _ = self.layer.retain();
+
+ var block = try SetSurfaceBlock.init(.{
+ .layer = self.layer.value,
+ .surface = surface,
+ }, &setSurfaceCallback);
+
+ // We check if we're on the main thread and run the block directly if so.
+ const NSThread = objc.getClass("NSThread").?;
+ if (NSThread.msgSend(bool, "isMainThread", .{})) {
+ setSurfaceCallback(block.context);
+ block.deinit();
+ } else {
+ // NOTE: The block will automatically be deallocated by the objc
+ // runtime once it's executed, so there's no need to deinit it.
+
+ macos.dispatch.dispatch_async(
+ @ptrCast(macos.dispatch.queue.getMain()),
+ @ptrCast(block.context),
+ );
+ }
+}
+
+/// Sets the layer's `contents` to the provided IOSurface.
+///
+/// Does not ensure this happens on the main thread.
+pub inline fn setSurfaceSync(self: *IOSurfaceLayer, surface: *IOSurface) void {
+ self.layer.setProperty("contents", surface);
+}
+
+const SetSurfaceBlock = objc.Block(struct {
+ layer: objc.c.id,
+ surface: *IOSurface,
+}, .{}, void);
+
+fn setSurfaceCallback(
+ block: *const SetSurfaceBlock.Context,
+) callconv(.c) void {
+ const layer = objc.Object.fromId(block.layer);
+ const surface: *IOSurface = block.surface;
+
+ // See explanation of why we retain and release in `setSurface`.
+ defer {
+ surface.release();
+ layer.release();
+ }
+
+ // We check to see if the surface is the appropriate size for
+ // the layer, if it's not then we discard it. This is because
+ // asynchronously drawn frames can sometimes finish just after
+ // a synchronously drawn frame during a resize, and if we don't
+ // discard the improperly sized surface it creates jank.
+ const bounds = layer.getProperty(macos.graphics.Rect, "bounds");
+ const scale = layer.getProperty(f64, "contentsScale");
+ const width: usize = @intFromFloat(bounds.size.width * scale);
+ const height: usize = @intFromFloat(bounds.size.height * scale);
+ if (width != surface.getWidth() or height != surface.getHeight()) {
+ log.debug(
+ "setSurfaceCallback(): surface is wrong size for layer, discarding. surface = {d}x{d}, layer = {d}x{d}",
+ .{ surface.getWidth(), surface.getHeight(), width, height },
+ );
+ return;
+ }
+
+ layer.setProperty("contents", surface);
+}
+
+pub const DisplayCallback = ?*align(8) const fn (?*anyopaque) void;
+
+pub fn setDisplayCallback(
+ self: *IOSurfaceLayer,
+ display_cb: DisplayCallback,
+ display_ctx: ?*anyopaque,
+) void {
+ self.layer.setInstanceVariable(
+ "display_cb",
+ objc.Object.fromId(@constCast(display_cb)),
+ );
+ self.layer.setInstanceVariable(
+ "display_ctx",
+ objc.Object.fromId(display_ctx),
+ );
+}
+
+fn getSubclass() error{ObjCFailed}!objc.Class {
+ if (Subclass) |c| return c;
+
+ const CALayer =
+ objc.getClass("CALayer") orelse return error.ObjCFailed;
+
+ var subclass =
+ objc.allocateClassPair(CALayer, "IOSurfaceLayer") orelse return error.ObjCFailed;
+ errdefer objc.disposeClassPair(subclass);
+
+ if (!subclass.addIvar("display_cb")) return error.ObjCFailed;
+ if (!subclass.addIvar("display_ctx")) return error.ObjCFailed;
+
+ subclass.replaceMethod("display", struct {
+ fn display(target: objc.c.id, sel: objc.c.SEL) callconv(.c) void {
+ _ = sel;
+ const self = objc.Object.fromId(target);
+ const display_cb: DisplayCallback = @ptrFromInt(@intFromPtr(
+ self.getInstanceVariable("display_cb").value,
+ ));
+ if (display_cb) |cb| cb(
+ @ptrCast(self.getInstanceVariable("display_ctx").value),
+ );
+ }
+ }.display);
+
+ // Disable all animations for this layer by returning null for all actions.
+ subclass.replaceMethod("actionForKey:", struct {
+ fn actionForKey(
+ target: objc.c.id,
+ sel: objc.c.SEL,
+ key: objc.c.id,
+ ) callconv(.c) objc.c.id {
+ _ = target;
+ _ = sel;
+ _ = key;
+ return objc.getClass("NSNull").?.msgSend(objc.c.id, "null", .{});
+ }
+ }.actionForKey);
+
+ objc.registerClassPair(subclass);
+
+ Subclass = subclass;
+
+ return subclass;
+}
diff --git a/src/renderer/metal/Pipeline.zig b/src/renderer/metal/Pipeline.zig
new file mode 100644
index 000000000..f72aeb2e1
--- /dev/null
+++ b/src/renderer/metal/Pipeline.zig
@@ -0,0 +1,203 @@
+//! Wrapper for handling render pipelines.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const builtin = @import("builtin");
+const macos = @import("macos");
+const objc = @import("objc");
+
+const mtl = @import("api.zig");
+const Texture = @import("Texture.zig");
+const Metal = @import("../Metal.zig");
+
+const log = std.log.scoped(.metal);
+
+/// Options for initializing a render pipeline.
+pub const Options = struct {
+ /// MTLDevice
+ device: objc.Object,
+
+ /// Name of the vertex function
+ vertex_fn: []const u8,
+ /// Name of the fragment function
+ fragment_fn: []const u8,
+
+ /// MTLLibrary to get the vertex function from
+ vertex_library: objc.Object,
+ /// MTLLibrary to get the fragment function from
+ fragment_library: objc.Object,
+
+ /// Vertex step function
+ step_fn: mtl.MTLVertexStepFunction = .per_vertex,
+
+ /// Info about the color attachments used by this render pipeline.
+ attachments: []const Attachment,
+
+ /// Describes a color attachment.
+ pub const Attachment = struct {
+ pixel_format: mtl.MTLPixelFormat,
+ blending_enabled: bool = true,
+ };
+};
+
+/// MTLRenderPipelineState
+state: objc.Object,
+
+pub fn init(comptime VertexAttributes: ?type, opts: Options) !Self {
+ // Create our descriptor
+ const desc = init: {
+ const Class = objc.getClass("MTLRenderPipelineDescriptor").?;
+ const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
+ const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
+ break :init id_init;
+ };
+ defer desc.msgSend(void, objc.sel("release"), .{});
+
+ // Get our vertex and fragment functions and add them to the descriptor.
+ {
+ const str = try macos.foundation.String.createWithBytes(
+ opts.vertex_fn,
+ .utf8,
+ false,
+ );
+ defer str.release();
+
+ const ptr = opts.vertex_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
+ const func_vert = objc.Object.fromId(ptr.?);
+ defer func_vert.msgSend(void, objc.sel("release"), .{});
+
+ desc.setProperty("vertexFunction", func_vert);
+ }
+ {
+ const str = try macos.foundation.String.createWithBytes(
+ opts.fragment_fn,
+ .utf8,
+ false,
+ );
+ defer str.release();
+
+ const ptr = opts.fragment_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
+ const func_frag = objc.Object.fromId(ptr.?);
+ defer func_frag.msgSend(void, objc.sel("release"), .{});
+
+ desc.setProperty("fragmentFunction", func_frag);
+ }
+
+ // If we have vertex attributes, create and add a vertex descriptor.
+ if (VertexAttributes) |V| {
+ const vertex_desc = init: {
+ const Class = objc.getClass("MTLVertexDescriptor").?;
+ const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
+ const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
+ break :init id_init;
+ };
+ defer vertex_desc.msgSend(void, objc.sel("release"), .{});
+
+ // Our attributes are the fields of the input
+ const attrs = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "attributes"));
+ autoAttribute(V, attrs);
+
+ // The layout describes how and when we fetch the next vertex input.
+ const layouts = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "layouts"));
+ {
+ const layout = layouts.msgSend(
+ objc.Object,
+ objc.sel("objectAtIndexedSubscript:"),
+ .{@as(c_ulong, 0)},
+ );
+
+ layout.setProperty("stepFunction", @intFromEnum(opts.step_fn));
+ layout.setProperty("stride", @as(c_ulong, @sizeOf(V)));
+ }
+
+ desc.setProperty("vertexDescriptor", vertex_desc);
+ }
+
+ // Set our color attachment
+ const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
+ for (opts.attachments, 0..) |at, i| {
+ const attachment = attachments.msgSend(
+ objc.Object,
+ objc.sel("objectAtIndexedSubscript:"),
+ .{@as(c_ulong, i)},
+ );
+
+ attachment.setProperty("pixelFormat", @intFromEnum(at.pixel_format));
+
+ attachment.setProperty("blendingEnabled", at.blending_enabled);
+ // We always use premultiplied alpha blending for now.
+ if (at.blending_enabled) {
+ attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
+ attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
+ attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
+ attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
+ attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
+ attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
+ }
+ }
+
+ // Make our state
+ var err: ?*anyopaque = null;
+ const pipeline_state = opts.device.msgSend(
+ objc.Object,
+ objc.sel("newRenderPipelineStateWithDescriptor:error:"),
+ .{ desc, &err },
+ );
+ try checkError(err);
+ errdefer pipeline_state.release();
+
+ return .{ .state = pipeline_state };
+}
+
+pub fn deinit(self: *const Self) void {
+ self.state.release();
+}
+
+fn autoAttribute(T: type, attrs: objc.Object) void {
+ inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
+ const offset = @offsetOf(T, field.name);
+
+ const FT = switch (@typeInfo(field.type)) {
+ .@"enum" => |e| e.tag_type,
+ else => field.type,
+ };
+
+ // Very incomplete list, expand as necessary.
+ const format = switch (FT) {
+ [4]u8 => mtl.MTLVertexFormat.uchar4,
+ [2]u16 => mtl.MTLVertexFormat.ushort2,
+ [2]i16 => mtl.MTLVertexFormat.short2,
+ [2]f32 => mtl.MTLVertexFormat.float2,
+ [4]f32 => mtl.MTLVertexFormat.float4,
+ [2]i32 => mtl.MTLVertexFormat.int2,
+ u32 => mtl.MTLVertexFormat.uint,
+ [2]u32 => mtl.MTLVertexFormat.uint2,
+ [4]u32 => mtl.MTLVertexFormat.uint4,
+ u8 => mtl.MTLVertexFormat.uchar,
+ else => comptime unreachable,
+ };
+
+ const attr = attrs.msgSend(
+ objc.Object,
+ objc.sel("objectAtIndexedSubscript:"),
+ .{@as(c_ulong, i)},
+ );
+
+ attr.setProperty("format", @intFromEnum(format));
+ attr.setProperty("offset", @as(c_ulong, offset));
+ attr.setProperty("bufferIndex", @as(c_ulong, 0));
+ }
+}
+
+fn checkError(err_: ?*anyopaque) !void {
+ const nserr = objc.Object.fromId(err_ orelse return);
+ const str = @as(
+ *macos.foundation.String,
+ @ptrCast(nserr.getProperty(?*anyopaque, "localizedDescription").?),
+ );
+
+ log.err("metal error={s}", .{str.cstringPtr(.ascii).?});
+ return error.MetalFailed;
+}
diff --git a/src/renderer/metal/RenderPass.zig b/src/renderer/metal/RenderPass.zig
new file mode 100644
index 000000000..e48bc4c00
--- /dev/null
+++ b/src/renderer/metal/RenderPass.zig
@@ -0,0 +1,220 @@
+//! Wrapper for handling render passes.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const builtin = @import("builtin");
+const objc = @import("objc");
+
+const mtl = @import("api.zig");
+const Pipeline = @import("Pipeline.zig");
+const Texture = @import("Texture.zig");
+const Target = @import("Target.zig");
+const Metal = @import("../Metal.zig");
+const Buffer = @import("buffer.zig").Buffer;
+
+const log = std.log.scoped(.metal);
+
+/// Options for beginning a render pass.
+pub const Options = struct {
+ /// MTLCommandBuffer
+ command_buffer: objc.Object,
+ /// Color attachments for this render pass.
+ attachments: []const Attachment,
+
+ /// Describes a color attachment.
+ pub const Attachment = struct {
+ target: union(enum) {
+ texture: Texture,
+ target: Target,
+ },
+ clear_color: ?[4]f64 = null,
+ };
+};
+
+/// Describes a step in a render pass.
+pub const Step = struct {
+ pipeline: Pipeline,
+ /// MTLBuffer
+ uniforms: ?objc.Object = null,
+ /// MTLBuffer
+ buffers: []const ?objc.Object = &.{},
+ textures: []const ?Texture = &.{},
+ draw: Draw,
+
+ /// Describes the draw call for this step.
+ pub const Draw = struct {
+ type: mtl.MTLPrimitiveType,
+ vertex_count: usize,
+ instance_count: usize = 1,
+ };
+};
+
+/// MTLRenderCommandEncoder
+encoder: objc.Object,
+
+/// Begin a render pass.
+pub fn begin(
+ opts: Options,
+) Self {
+ // Create a pass descriptor
+ const desc = desc: {
+ const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?;
+ const desc = MTLRenderPassDescriptor.msgSend(
+ objc.Object,
+ objc.sel("renderPassDescriptor"),
+ .{},
+ );
+
+ // Set our color attachment to be our drawable surface.
+ const attachments = objc.Object.fromId(
+ desc.getProperty(?*anyopaque, "colorAttachments"),
+ );
+ for (opts.attachments, 0..) |at, i| {
+ const attachment = attachments.msgSend(
+ objc.Object,
+ objc.sel("objectAtIndexedSubscript:"),
+ .{@as(c_ulong, i)},
+ );
+
+ attachment.setProperty(
+ "loadAction",
+ @intFromEnum(@as(
+ mtl.MTLLoadAction,
+ if (at.clear_color != null)
+ .clear
+ else
+ .load,
+ )),
+ );
+ attachment.setProperty(
+ "storeAction",
+ @intFromEnum(mtl.MTLStoreAction.store),
+ );
+ attachment.setProperty("texture", switch (at.target) {
+ .texture => |t| t.texture.value,
+ .target => |t| t.texture.value,
+ });
+ if (at.clear_color) |c| attachment.setProperty(
+ "clearColor",
+ mtl.MTLClearColor{
+ .red = c[0],
+ .green = c[1],
+ .blue = c[2],
+ .alpha = c[3],
+ },
+ );
+ }
+
+ break :desc desc;
+ };
+
+ // MTLRenderCommandEncoder
+ const encoder = opts.command_buffer.msgSend(
+ objc.Object,
+ objc.sel("renderCommandEncoderWithDescriptor:"),
+ .{desc.value},
+ );
+
+ return .{ .encoder = encoder };
+}
+
+/// Add a step to this render pass.
+pub fn step(self: *const Self, s: Step) void {
+ if (s.draw.instance_count == 0) return;
+
+ // Set pipeline state
+ self.encoder.msgSend(
+ void,
+ objc.sel("setRenderPipelineState:"),
+ .{s.pipeline.state.value},
+ );
+
+ if (s.buffers.len > 0) {
+ // We reserve index 0 for the vertex buffer, this isn't very
+ // flexible but it lines up with the API we have for OpenGL.
+ if (s.buffers[0]) |buf| {
+ self.encoder.msgSend(
+ void,
+ objc.sel("setVertexBuffer:offset:atIndex:"),
+ .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) },
+ );
+ self.encoder.msgSend(
+ void,
+ objc.sel("setFragmentBuffer:offset:atIndex:"),
+ .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) },
+ );
+ }
+
+ // Set the rest of the buffers starting at index 2, this is
+ // so that we can use index 1 for the uniforms if present.
+ //
+ // Also, we set buffers (and textures) for both stages.
+ //
+ // Again, not very flexible, but it's consistent and predictable,
+ // and we need to treat the uniforms as special because of OpenGL.
+ //
+ // TODO: Maybe in the future add info to the pipeline struct which
+ // allows it to define a mapping between provided buffers and
+ // what index they get set at for the vertex / fragment stage.
+ for (s.buffers[1..], 2..) |b, i| if (b) |buf| {
+ self.encoder.msgSend(
+ void,
+ objc.sel("setVertexBuffer:offset:atIndex:"),
+ .{ buf.value, @as(c_ulong, 0), @as(c_ulong, i) },
+ );
+ self.encoder.msgSend(
+ void,
+ objc.sel("setFragmentBuffer:offset:atIndex:"),
+ .{ buf.value, @as(c_ulong, 0), @as(c_ulong, i) },
+ );
+ };
+ }
+
+ // Set the uniforms as buffer index 1 if present.
+ if (s.uniforms) |buf| {
+ self.encoder.msgSend(
+ void,
+ objc.sel("setVertexBuffer:offset:atIndex:"),
+ .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 1) },
+ );
+ self.encoder.msgSend(
+ void,
+ objc.sel("setFragmentBuffer:offset:atIndex:"),
+ .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 1) },
+ );
+ }
+
+ // Set textures.
+ for (s.textures, 0..) |t, i| if (t) |tex| {
+ self.encoder.msgSend(
+ void,
+ objc.sel("setVertexTexture:atIndex:"),
+ .{ tex.texture.value, @as(c_ulong, i) },
+ );
+ self.encoder.msgSend(
+ void,
+ objc.sel("setFragmentTexture:atIndex:"),
+ .{ tex.texture.value, @as(c_ulong, i) },
+ );
+ };
+
+ // Draw!
+ self.encoder.msgSend(
+ void,
+ objc.sel("drawPrimitives:vertexStart:vertexCount:instanceCount:"),
+ .{
+ @intFromEnum(s.draw.type),
+ @as(c_ulong, 0),
+ @as(c_ulong, s.draw.vertex_count),
+ @as(c_ulong, s.draw.instance_count),
+ },
+ );
+}
+
+/// Complete this render pass.
+/// This struct can no longer be used after calling this.
+pub fn complete(self: *const Self) void {
+ self.encoder.msgSend(void, objc.sel("endEncoding"), .{});
+}
diff --git a/src/renderer/metal/Target.zig b/src/renderer/metal/Target.zig
new file mode 100644
index 000000000..fa62d3014
--- /dev/null
+++ b/src/renderer/metal/Target.zig
@@ -0,0 +1,110 @@
+//! Represents a render target.
+//!
+//! In this case, an IOSurface-backed MTLTexture.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const builtin = @import("builtin");
+const objc = @import("objc");
+const macos = @import("macos");
+const graphics = macos.graphics;
+const IOSurface = macos.iosurface.IOSurface;
+
+const mtl = @import("api.zig");
+
+const log = std.log.scoped(.metal);
+
+/// Options for initializing a Target
+pub const Options = struct {
+ /// MTLDevice
+ device: objc.Object,
+
+ /// Desired width
+ width: usize,
+ /// Desired height
+ height: usize,
+
+ /// Pixel format for the MTLTexture
+ pixel_format: mtl.MTLPixelFormat,
+ /// Storage mode for the MTLTexture
+ storage_mode: mtl.MTLResourceOptions.StorageMode,
+};
+
+/// The underlying IOSurface.
+surface: *IOSurface,
+
+/// The underlying MTLTexture.
+texture: objc.Object,
+
+/// Current width of this target.
+width: usize,
+/// Current height of this target.
+height: usize,
+
+pub fn init(opts: Options) !Self {
+ // We set our surface's color space to Display P3.
+ // This allows us to have "Apple-style" alpha blending,
+ // since it seems to be the case that Apple apps like
+ // Terminal and TextEdit render text in the display's
+ // color space using converted colors, which reduces,
+ // but does not fully eliminate blending artifacts.
+ const colorspace = try graphics.ColorSpace.createNamed(.displayP3);
+ defer colorspace.release();
+
+ const surface = try IOSurface.init(.{
+ .width = @intCast(opts.width),
+ .height = @intCast(opts.height),
+ .pixel_format = .@"32BGRA",
+ .bytes_per_element = 4,
+ .colorspace = colorspace,
+ });
+
+ // Create our descriptor
+ const desc = init: {
+ const Class = objc.getClass("MTLTextureDescriptor").?;
+ const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
+ const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
+ break :init id_init;
+ };
+ errdefer desc.msgSend(void, objc.sel("release"), .{});
+
+ // Set our properties
+ desc.setProperty("width", @as(c_ulong, @intCast(opts.width)));
+ desc.setProperty("height", @as(c_ulong, @intCast(opts.height)));
+ desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format));
+ desc.setProperty("usage", mtl.MTLTextureUsage{ .render_target = true });
+ desc.setProperty(
+ "resourceOptions",
+ mtl.MTLResourceOptions{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = opts.storage_mode,
+ },
+ );
+
+ const id = opts.device.msgSend(
+ ?*anyopaque,
+ objc.sel("newTextureWithDescriptor:iosurface:plane:"),
+ .{
+ desc,
+ surface,
+ @as(c_ulong, 0),
+ },
+ ) orelse return error.MetalFailed;
+
+ const texture = objc.Object.fromId(id);
+
+ return .{
+ .surface = surface,
+ .texture = texture,
+ .width = opts.width,
+ .height = opts.height,
+ };
+}
+
+pub fn deinit(self: *Self) void {
+ self.surface.deinit();
+ self.texture.release();
+}
diff --git a/src/renderer/metal/Texture.zig b/src/renderer/metal/Texture.zig
new file mode 100644
index 000000000..32820f8fc
--- /dev/null
+++ b/src/renderer/metal/Texture.zig
@@ -0,0 +1,201 @@
+//! Wrapper for handling textures.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const builtin = @import("builtin");
+const objc = @import("objc");
+
+const mtl = @import("api.zig");
+const Metal = @import("../Metal.zig");
+
+const log = std.log.scoped(.metal);
+
+/// Options for initializing a texture.
+pub const Options = struct {
+ /// MTLDevice
+ device: objc.Object,
+ pixel_format: mtl.MTLPixelFormat,
+ resource_options: mtl.MTLResourceOptions,
+};
+
+/// The underlying MTLTexture Object.
+texture: objc.Object,
+
+/// The width of this texture.
+width: usize,
+/// The height of this texture.
+height: usize,
+
+/// Bytes per pixel for this texture.
+bpp: usize,
+
+pub const Error = error{
+ /// A Metal API call failed.
+ MetalFailed,
+};
+
+/// Initialize a texture
+pub fn init(
+ opts: Options,
+ width: usize,
+ height: usize,
+ data: ?[]const u8,
+) Error!Self {
+ // Create our descriptor
+ const desc = init: {
+ const Class = objc.getClass("MTLTextureDescriptor").?;
+ const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
+ const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
+ break :init id_init;
+ };
+ errdefer desc.msgSend(void, objc.sel("release"), .{});
+
+ // Set our properties
+ desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format));
+ desc.setProperty("width", @as(c_ulong, width));
+ desc.setProperty("height", @as(c_ulong, height));
+ desc.setProperty("resourceOptions", opts.resource_options);
+
+ // Initialize
+ const id = opts.device.msgSend(
+ ?*anyopaque,
+ objc.sel("newTextureWithDescriptor:"),
+ .{desc},
+ ) orelse return error.MetalFailed;
+
+ const self: Self = .{
+ .texture = objc.Object.fromId(id),
+ .width = width,
+ .height = height,
+ .bpp = bppOf(opts.pixel_format),
+ };
+
+ // If we have data, we set it here.
+ if (data) |d| {
+ assert(d.len == width * height * self.bpp);
+ try self.replaceRegion(0, 0, width, height, d);
+ }
+
+ return self;
+}
+
+pub fn deinit(self: Self) void {
+ self.texture.release();
+}
+
+/// Replace a region of the texture with the provided data.
+///
+/// Does NOT check the dimensions of the data to ensure correctness.
+pub fn replaceRegion(
+ self: Self,
+ x: usize,
+ y: usize,
+ width: usize,
+ height: usize,
+ data: []const u8,
+) error{}!void {
+ self.texture.msgSend(
+ void,
+ objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"),
+ .{
+ mtl.MTLRegion{
+ .origin = .{ .x = x, .y = y, .z = 0 },
+ .size = .{
+ .width = @intCast(width),
+ .height = @intCast(height),
+ .depth = 1,
+ },
+ },
+ @as(c_ulong, 0),
+ @as(*const anyopaque, data.ptr),
+ @as(c_ulong, self.bpp * width),
+ },
+ );
+}
+
+/// Returns the bytes per pixel for the provided pixel format
+fn bppOf(pixel_format: mtl.MTLPixelFormat) usize {
+ return switch (pixel_format) {
+ // Invalid
+ .invalid => @panic("invalid pixel format"),
+
+ // Weird formats I was too lazy to get the sizes of
+ else => @panic("pixel format size unknown (unlikely that this format was actually used, could be memory corruption)"),
+
+ // 8-bit pixel formats
+ .a8unorm,
+ .r8unorm,
+ .r8unorm_srgb,
+ .r8snorm,
+ .r8uint,
+ .r8sint,
+ .rg8unorm,
+ .rg8unorm_srgb,
+ .rg8snorm,
+ .rg8uint,
+ .rg8sint,
+ .stencil8,
+ => 1,
+
+ // 16-bit pixel formats
+ .r16unorm,
+ .r16snorm,
+ .r16uint,
+ .r16sint,
+ .r16float,
+ .rg16unorm,
+ .rg16snorm,
+ .rg16uint,
+ .rg16sint,
+ .rg16float,
+ .b5g6r5unorm,
+ .a1bgr5unorm,
+ .abgr4unorm,
+ .bgr5a1unorm,
+ .depth16unorm,
+ => 2,
+
+ // 32-bit pixel formats
+ .rgba8unorm,
+ .rgba8unorm_srgb,
+ .rgba8snorm,
+ .rgba8uint,
+ .rgba8sint,
+ .bgra8unorm,
+ .bgra8unorm_srgb,
+ .rgb10a2unorm,
+ .rgb10a2uint,
+ .rg11b10float,
+ .rgb9e5float,
+ .bgr10a2unorm,
+ .bgr10_xr,
+ .bgr10_xr_srgb,
+ .r32uint,
+ .r32sint,
+ .r32float,
+ .depth32float,
+ .depth24unorm_stencil8,
+ => 4,
+
+ // 64-bit pixel formats
+ .rg32uint,
+ .rg32sint,
+ .rg32float,
+ .rgba16unorm,
+ .rgba16snorm,
+ .rgba16uint,
+ .rgba16sint,
+ .rgba16float,
+ .bgra10_xr,
+ .bgra10_xr_srgb,
+ => 8,
+
+ // 128-bit pixel formats,
+ .rgba32uint,
+ .rgba32sint,
+ .rgba32float,
+ => 128,
+ };
+}
diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig
index 46cb4f6bc..e1daa6848 100644
--- a/src/renderer/metal/api.zig
+++ b/src/renderer/metal/api.zig
@@ -1,4 +1,10 @@
//! This file contains the definitions of the Metal API that we use.
+//!
+//! Because the online Apple developer docs have recently (as of January 2025)
+//! been changed to hide enum values, `Metal-cpp` has been used as a reference
+//! source instead.
+//!
+//! Ref: https://developer.apple.com/metal/cpp/
/// https://developer.apple.com/documentation/metal/mtlcommandbufferstatus?language=objc
pub const MTLCommandBufferStatus = enum(c_ulong) {
@@ -22,6 +28,10 @@ pub const MTLLoadAction = enum(c_ulong) {
pub const MTLStoreAction = enum(c_ulong) {
dont_care = 0,
store = 1,
+ multisample_resolve = 2,
+ store_and_multisample_resolve = 3,
+ unknown = 4,
+ custom_sample_depth_store = 5,
};
/// https://developer.apple.com/documentation/metal/mtlresourceoptions?language=objc
@@ -73,16 +83,60 @@ pub const MTLIndexType = enum(c_ulong) {
/// https://developer.apple.com/documentation/metal/mtlvertexformat?language=objc
pub const MTLVertexFormat = enum(c_ulong) {
+ invalid = 0,
+ uchar2 = 1,
+ uchar3 = 2,
uchar4 = 3,
+ char2 = 4,
+ char3 = 5,
+ char4 = 6,
+ uchar2normalized = 7,
+ uchar3normalized = 8,
+ uchar4normalized = 9,
+ char2normalized = 10,
+ char3normalized = 11,
+ char4normalized = 12,
ushort2 = 13,
+ ushort3 = 14,
+ ushort4 = 15,
short2 = 16,
+ short3 = 17,
+ short4 = 18,
+ ushort2normalized = 19,
+ ushort3normalized = 20,
+ ushort4normalized = 21,
+ short2normalized = 22,
+ short3normalized = 23,
+ short4normalized = 24,
+ half2 = 25,
+ half3 = 26,
+ half4 = 27,
+ float = 28,
float2 = 29,
+ float3 = 30,
float4 = 31,
+ int = 32,
int2 = 33,
+ int3 = 34,
+ int4 = 35,
uint = 36,
uint2 = 37,
+ uint3 = 38,
uint4 = 39,
+ int1010102normalized = 40,
+ uint1010102normalized = 41,
+ uchar4normalized_bgra = 42,
uchar = 45,
+ char = 46,
+ ucharnormalized = 47,
+ charnormalized = 48,
+ ushort = 49,
+ short = 50,
+ ushortnormalized = 51,
+ shortnormalized = 52,
+ half = 53,
+ floatrg11b10 = 54,
+ floatrgb9e5 = 55,
};
/// https://developer.apple.com/documentation/metal/mtlvertexstepfunction?language=objc
@@ -90,20 +144,158 @@ pub const MTLVertexStepFunction = enum(c_ulong) {
constant = 0,
per_vertex = 1,
per_instance = 2,
+ per_patch = 3,
+ per_patch_control_point = 4,
};
/// https://developer.apple.com/documentation/metal/mtlpixelformat?language=objc
pub const MTLPixelFormat = enum(c_ulong) {
+ invalid = 0,
+ a8unorm = 1,
r8unorm = 10,
+ r8unorm_srgb = 11,
+ r8snorm = 12,
+ r8uint = 13,
+ r8sint = 14,
+ r16unorm = 20,
+ r16snorm = 22,
+ r16uint = 23,
+ r16sint = 24,
+ r16float = 25,
+ rg8unorm = 30,
+ rg8unorm_srgb = 31,
+ rg8snorm = 32,
+ rg8uint = 33,
+ rg8sint = 34,
+ b5g6r5unorm = 40,
+ a1bgr5unorm = 41,
+ abgr4unorm = 42,
+ bgr5a1unorm = 43,
+ r32uint = 53,
+ r32sint = 54,
+ r32float = 55,
+ rg16unorm = 60,
+ rg16snorm = 62,
+ rg16uint = 63,
+ rg16sint = 64,
+ rg16float = 65,
rgba8unorm = 70,
rgba8unorm_srgb = 71,
+ rgba8snorm = 72,
rgba8uint = 73,
+ rgba8sint = 74,
bgra8unorm = 80,
bgra8unorm_srgb = 81,
+ rgb10a2unorm = 90,
+ rgb10a2uint = 91,
+ rg11b10float = 92,
+ rgb9e5float = 93,
+ bgr10a2unorm = 94,
+ bgr10_xr = 554,
+ bgr10_xr_srgb = 555,
+ rg32uint = 103,
+ rg32sint = 104,
+ rg32float = 105,
+ rgba16unorm = 110,
+ rgba16snorm = 112,
+ rgba16uint = 113,
+ rgba16sint = 114,
+ rgba16float = 115,
+ bgra10_xr = 552,
+ bgra10_xr_srgb = 553,
+ rgba32uint = 123,
+ rgba32sint = 124,
+ rgba32float = 125,
+ bc1_rgba = 130,
+ bc1_rgba_srgb = 131,
+ bc2_rgba = 132,
+ bc2_rgba_srgb = 133,
+ bc3_rgba = 134,
+ bc3_rgba_srgb = 135,
+ bc4_runorm = 140,
+ bc4_rsnorm = 141,
+ bc5_rgunorm = 142,
+ bc5_rgsnorm = 143,
+ bc6h_rgbfloat = 150,
+ bc6h_rgbufloat = 151,
+ bc7_rgbaunorm = 152,
+ bc7_rgbaunorm_srgb = 153,
+ pvrtc_rgb_2bpp = 160,
+ pvrtc_rgb_2bpp_srgb = 161,
+ pvrtc_rgb_4bpp = 162,
+ pvrtc_rgb_4bpp_srgb = 163,
+ pvrtc_rgba_2bpp = 164,
+ pvrtc_rgba_2bpp_srgb = 165,
+ pvrtc_rgba_4bpp = 166,
+ pvrtc_rgba_4bpp_srgb = 167,
+ eac_r11unorm = 170,
+ eac_r11snorm = 172,
+ eac_rg11unorm = 174,
+ eac_rg11snorm = 176,
+ eac_rgba8 = 178,
+ eac_rgba8_srgb = 179,
+ etc2_rgb8 = 180,
+ etc2_rgb8_srgb = 181,
+ etc2_rgb8a1 = 182,
+ etc2_rgb8a1_srgb = 183,
+ astc_4x4_srgb = 186,
+ astc_5x4_srgb = 187,
+ astc_5x5_srgb = 188,
+ astc_6x5_srgb = 189,
+ astc_6x6_srgb = 190,
+ astc_8x5_srgb = 192,
+ astc_8x6_srgb = 193,
+ astc_8x8_srgb = 194,
+ astc_10x5_srgb = 195,
+ astc_10x6_srgb = 196,
+ astc_10x8_srgb = 197,
+ astc_10x10_srgb = 198,
+ astc_12x10_srgb = 199,
+ astc_12x12_srgb = 200,
+ astc_4x4_ldr = 204,
+ astc_5x4_ldr = 205,
+ astc_5x5_ldr = 206,
+ astc_6x5_ldr = 207,
+ astc_6x6_ldr = 208,
+ astc_8x5_ldr = 210,
+ astc_8x6_ldr = 211,
+ astc_8x8_ldr = 212,
+ astc_10x5_ldr = 213,
+ astc_10x6_ldr = 214,
+ astc_10x8_ldr = 215,
+ astc_10x10_ldr = 216,
+ astc_12x10_ldr = 217,
+ astc_12x12_ldr = 218,
+ astc_4x4_hdr = 222,
+ astc_5x4_hdr = 223,
+ astc_5x5_hdr = 224,
+ astc_6x5_hdr = 225,
+ astc_6x6_hdr = 226,
+ astc_8x5_hdr = 228,
+ astc_8x6_hdr = 229,
+ astc_8x8_hdr = 230,
+ astc_10x5_hdr = 231,
+ astc_10x6_hdr = 232,
+ astc_10x8_hdr = 233,
+ astc_10x10_hdr = 234,
+ astc_12x10_hdr = 235,
+ astc_12x12_hdr = 236,
+ gbgr422 = 240,
+ bgrg422 = 241,
+ depth16unorm = 250,
+ depth32float = 252,
+ stencil8 = 253,
+ depth24unorm_stencil8 = 255,
+ depth32float_stencil8 = 260,
+ x32_stencil8 = 261,
+ x24_stencil8 = 262,
};
/// https://developer.apple.com/documentation/metal/mtlpurgeablestate?language=objc
pub const MTLPurgeableState = enum(c_ulong) {
+ keep_current = 1,
+ non_volatile = 2,
+ @"volatile" = 3,
empty = 4,
};
@@ -155,13 +347,48 @@ pub const MTLBlendOperation = enum(c_ulong) {
max = 4,
};
-/// https://developer.apple.com/documentation/metal/mtltextureusage?language=objc
-pub const MTLTextureUsage = enum(c_ulong) {
- unknown = 0,
- shader_read = 1,
- shader_write = 2,
- render_target = 4,
- pixel_format_view = 8,
+/// https://developer.apple.com/documentation/metal/mtltextureusage?language=objc
+pub const MTLTextureUsage = packed struct(c_ulong) {
+ /// https://developer.apple.com/documentation/metal/mtltextureusage/shaderread?language=objc
+ shader_read: bool = false, // TextureUsageShaderRead = 1,
+
+ /// https://developer.apple.com/documentation/metal/mtltextureusage/shaderwrite?language=objc
+ shader_write: bool = false, // TextureUsageShaderWrite = 2,
+
+ /// https://developer.apple.com/documentation/metal/mtltextureusage/rendertarget?language=objc
+ render_target: bool = false, // TextureUsageRenderTarget = 4,
+
+ _reserved: u1 = 0, // The enum skips from 4 to 16, 8 has no documented use.
+
+ /// https://developer.apple.com/documentation/metal/mtltextureusage/pixelformatview?language=objc
+ pixel_format_view: bool = false, // TextureUsagePixelFormatView = 16,
+
+ /// https://developer.apple.com/documentation/metal/mtltextureusage/shaderatomic?language=objc
+ shader_atomic: bool = false, // TextureUsageShaderAtomic = 32,
+
+ __reserved: @Type(.{ .int = .{
+ .signedness = .unsigned,
+ .bits = @bitSizeOf(c_ulong) - 6,
+ } }) = 0,
+
+ /// https://developer.apple.com/documentation/metal/mtltextureusage/unknown?language=objc
+ const unknown: MTLTextureUsage = @bitCast(0); // TextureUsageUnknown = 0,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlbarrierscope?language=objc
+pub const MTLBarrierScope = enum(c_ulong) {
+ buffers = 1,
+ textures = 2,
+ render_targets = 4,
+};
+
+/// https://developer.apple.com/documentation/metal/mtlrenderstages?language=objc
+pub const MTLRenderStage = enum(c_ulong) {
+ vertex = 1,
+ fragment = 2,
+ tile = 4,
+ object = 8,
+ mesh = 16,
};
pub const MTLClearColor = extern struct {
diff --git a/src/renderer/metal/buffer.zig b/src/renderer/metal/buffer.zig
index 4128e297b..43320a60b 100644
--- a/src/renderer/metal/buffer.zig
+++ b/src/renderer/metal/buffer.zig
@@ -5,9 +5,17 @@ const objc = @import("objc");
const macos = @import("macos");
const mtl = @import("api.zig");
+const Metal = @import("../Metal.zig");
const log = std.log.scoped(.metal);
+/// Options for initializing a buffer.
+pub const Options = struct {
+ /// MTLDevice
+ device: objc.Object,
+ resource_options: mtl.MTLResourceOptions,
+};
+
/// Metal data storage for a certain set of equal types. This is usually
/// used for vertex buffers, etc. This helpful wrapper makes it easy to
/// prealloc, shrink, grow, sync, buffers with Metal.
@@ -15,74 +23,57 @@ pub fn Buffer(comptime T: type) type {
return struct {
const Self = @This();
- /// The resource options for this buffer.
- options: mtl.MTLResourceOptions,
+ /// The options this buffer was initialized with.
+ opts: Options,
- buffer: objc.Object, // MTLBuffer
+ /// The underlying MTLBuffer object.
+ buffer: objc.Object,
+
+ /// The allocated length of the buffer.
+ /// Note that this is the number
+ /// of `T`s not the size in bytes.
+ len: usize,
/// Initialize a buffer with the given length pre-allocated.
- pub fn init(
- device: objc.Object,
- len: usize,
- options: mtl.MTLResourceOptions,
- ) !Self {
- const buffer = device.msgSend(
+ pub fn init(opts: Options, len: usize) !Self {
+ const buffer = opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(len * @sizeOf(T))),
- options,
+ opts.resource_options,
},
);
- return .{ .buffer = buffer, .options = options };
+ return .{ .buffer = buffer, .opts = opts, .len = len };
}
/// Init the buffer filled with the given data.
- pub fn initFill(
- device: objc.Object,
- data: []const T,
- options: mtl.MTLResourceOptions,
- ) !Self {
- const buffer = device.msgSend(
+ pub fn initFill(opts: Options, data: []const T) !Self {
+ const buffer = opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithBytes:length:options:"),
.{
@as(*const anyopaque, @ptrCast(data.ptr)),
@as(c_ulong, @intCast(data.len * @sizeOf(T))),
- options,
+ opts.resource_options,
},
);
- return .{ .buffer = buffer, .options = options };
+ return .{ .buffer = buffer, .opts = opts, .len = data.len };
}
- pub fn deinit(self: *Self) void {
+ pub fn deinit(self: *const Self) void {
self.buffer.msgSend(void, objc.sel("release"), .{});
}
- /// Get the buffer contents as a slice of T. The contents are
- /// mutable. The contents may or may not be automatically synced
- /// depending on the buffer storage mode. See the Metal docs.
- pub fn contents(self: *Self) ![]T {
- const len_bytes = self.buffer.getProperty(c_ulong, "length");
- assert(@mod(len_bytes, @sizeOf(T)) == 0);
- const len = @divExact(len_bytes, @sizeOf(T));
- const ptr = self.buffer.msgSend(
- ?[*]T,
- objc.sel("contents"),
- .{},
- ).?;
- return ptr[0..len];
- }
-
/// Sync new contents to the buffer. The data is expected to be the
/// complete contents of the buffer. If the amount of data is larger
/// than the buffer length, the buffer will be reallocated.
///
/// If the amount of data is smaller than the buffer length, the
/// remaining data in the buffer is left untouched.
- pub fn sync(self: *Self, device: objc.Object, data: []const T) !void {
+ pub fn sync(self: *Self, data: []const T) !void {
// If we need more bytes than our buffer has, we need to reallocate.
const req_bytes = data.len * @sizeOf(T);
const avail_bytes = self.buffer.getProperty(c_ulong, "length");
@@ -92,12 +83,12 @@ pub fn Buffer(comptime T: type) type {
// Allocate a new buffer with enough to hold double what we require.
const size = req_bytes * 2;
- self.buffer = device.msgSend(
+ self.buffer = self.opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(size * @sizeOf(T))),
- self.options,
+ self.opts.resource_options,
},
);
}
@@ -123,7 +114,7 @@ pub fn Buffer(comptime T: type) type {
// we need to signal Metal to synchronize the buffer data.
//
// Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc
- if (self.options.storage_mode == .managed) {
+ if (self.opts.resource_options.storage_mode == .managed) {
self.buffer.msgSend(
void,
"didModifyRange:",
@@ -134,7 +125,7 @@ pub fn Buffer(comptime T: type) type {
/// Like Buffer.sync but takes data from an array of ArrayLists,
/// rather than a single array. Returns the number of items synced.
- pub fn syncFromArrayLists(self: *Self, device: objc.Object, lists: []std.ArrayListUnmanaged(T)) !usize {
+ pub fn syncFromArrayLists(self: *Self, lists: []const std.ArrayListUnmanaged(T)) !usize {
var total_len: usize = 0;
for (lists) |list| {
total_len += list.items.len;
@@ -149,12 +140,12 @@ pub fn Buffer(comptime T: type) type {
// Allocate a new buffer with enough to hold double what we require.
const size = req_bytes * 2;
- self.buffer = device.msgSend(
+ self.buffer = self.opts.device.msgSend(
objc.Object,
objc.sel("newBufferWithLength:options:"),
.{
@as(c_ulong, @intCast(size * @sizeOf(T))),
- self.options,
+ self.opts.resource_options,
},
);
}
@@ -181,7 +172,7 @@ pub fn Buffer(comptime T: type) type {
// we need to signal Metal to synchronize the buffer data.
//
// Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc
- if (self.options.storage_mode == .managed) {
+ if (self.opts.resource_options.storage_mode == .managed) {
self.buffer.msgSend(
void,
"didModifyRange:",
diff --git a/src/renderer/metal/image.zig b/src/renderer/metal/image.zig
index 7d2599308..1bfa3c621 100644
--- a/src/renderer/metal/image.zig
+++ b/src/renderer/metal/image.zig
@@ -4,6 +4,9 @@ const assert = std.debug.assert;
const objc = @import("objc");
const wuffs = @import("wuffs");
+const Metal = @import("../Metal.zig");
+const Texture = Metal.Texture;
+
const mtl = @import("api.zig");
/// Represents a single image placement on the grid. A placement is a
@@ -61,15 +64,15 @@ pub const Image = union(enum) {
replace_rgba: Replace,
/// The image is uploaded and ready to be used.
- ready: objc.Object, // MTLTexture
+ ready: Texture,
/// The image is uploaded but is scheduled to be unloaded.
unload_pending: []u8,
- unload_ready: objc.Object, // MTLTexture
- unload_replace: struct { []u8, objc.Object },
+ unload_ready: Texture,
+ unload_replace: struct { []u8, Texture },
pub const Replace = struct {
- texture: objc.Object,
+ texture: Texture,
pending: Pending,
};
@@ -101,32 +104,32 @@ pub const Image = union(enum) {
.replace_gray => |r| {
alloc.free(r.pending.dataSlice(1));
- r.texture.msgSend(void, objc.sel("release"), .{});
+ r.texture.deinit();
},
.replace_gray_alpha => |r| {
alloc.free(r.pending.dataSlice(2));
- r.texture.msgSend(void, objc.sel("release"), .{});
+ r.texture.deinit();
},
.replace_rgb => |r| {
alloc.free(r.pending.dataSlice(3));
- r.texture.msgSend(void, objc.sel("release"), .{});
+ r.texture.deinit();
},
.replace_rgba => |r| {
alloc.free(r.pending.dataSlice(4));
- r.texture.msgSend(void, objc.sel("release"), .{});
+ r.texture.deinit();
},
.unload_replace => |r| {
alloc.free(r[0]);
- r[1].msgSend(void, objc.sel("release"), .{});
+ r[1].deinit();
},
.ready,
.unload_ready,
- => |obj| obj.msgSend(void, objc.sel("release"), .{}),
+ => |t| t.deinit(),
}
}
@@ -170,7 +173,7 @@ pub const Image = union(enum) {
// Get our existing texture. This switch statement will also handle
// scenarios where there is no existing texture and we can modify
// the self pointer directly.
- const existing: objc.Object = switch (self.*) {
+ const existing: Texture = switch (self.*) {
// For pending, we can free the old data and become pending
// ourselves.
.pending_gray => |p| {
@@ -357,10 +360,11 @@ pub const Image = union(enum) {
pub fn upload(
self: *Image,
alloc: Allocator,
- device: objc.Object,
- /// Storage mode for the MTLTexture object
- storage_mode: mtl.MTLResourceOptions.StorageMode,
+ metal: *const Metal,
) !void {
+ const device = metal.device;
+ const storage_mode = metal.default_storage_mode;
+
// Convert our data if we have to
try self.convert(alloc);
@@ -368,27 +372,19 @@ pub const Image = union(enum) {
const p = self.pending().?;
// Create our texture
- const texture = try initTexture(p, device, storage_mode);
- errdefer texture.msgSend(void, objc.sel("release"), .{});
-
- // Upload our data
- const d = self.depth();
- texture.msgSend(
- void,
- objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"),
+ const texture = try Texture.init(
.{
- mtl.MTLRegion{
- .origin = .{ .x = 0, .y = 0, .z = 0 },
- .size = .{
- .width = @intCast(p.width),
- .height = @intCast(p.height),
- .depth = 1,
- },
+ .device = device,
+ .pixel_format = .rgba8unorm_srgb,
+ .resource_options = .{
+ // Indicate that the CPU writes to this resource but never reads it.
+ .cpu_cache_mode = .write_combined,
+ .storage_mode = storage_mode,
},
- @as(c_ulong, 0),
- @as(*const anyopaque, p.data),
- @as(c_ulong, d * p.width),
},
+ @intCast(p.width),
+ @intCast(p.height),
+ p.data[0 .. p.width * p.height * self.depth()],
);
// Uploaded. We can now clear our data and change our state.
@@ -425,42 +421,4 @@ pub const Image = union(enum) {
else => null,
};
}
-
- fn initTexture(
- p: Pending,
- device: objc.Object,
- /// Storage mode for the MTLTexture object
- storage_mode: mtl.MTLResourceOptions.StorageMode,
- ) !objc.Object {
- // Create our descriptor
- const desc = init: {
- const Class = objc.getClass("MTLTextureDescriptor").?;
- const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
- const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
- break :init id_init;
- };
-
- // Set our properties
- desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.rgba8unorm_srgb));
- desc.setProperty("width", @as(c_ulong, @intCast(p.width)));
- desc.setProperty("height", @as(c_ulong, @intCast(p.height)));
-
- desc.setProperty(
- "resourceOptions",
- mtl.MTLResourceOptions{
- // Indicate that the CPU writes to this resource but never reads it.
- .cpu_cache_mode = .write_combined,
- .storage_mode = storage_mode,
- },
- );
-
- // Initialize
- const id = device.msgSend(
- ?*anyopaque,
- objc.sel("newTextureWithDescriptor:"),
- .{desc},
- ) orelse return error.MetalFailed;
-
- return objc.Object.fromId(id);
- }
};
diff --git a/src/renderer/metal/sampler.zig b/src/renderer/metal/sampler.zig
deleted file mode 100644
index c7a04df3a..000000000
--- a/src/renderer/metal/sampler.zig
+++ /dev/null
@@ -1,38 +0,0 @@
-const std = @import("std");
-const Allocator = std.mem.Allocator;
-const assert = std.debug.assert;
-const objc = @import("objc");
-
-const mtl = @import("api.zig");
-
-pub const Sampler = struct {
- sampler: objc.Object,
-
- pub fn init(device: objc.Object) !Sampler {
- const desc = init: {
- const Class = objc.getClass("MTLSamplerDescriptor").?;
- const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
- const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
- break :init id_init;
- };
- defer desc.msgSend(void, objc.sel("release"), .{});
- desc.setProperty("rAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge));
- desc.setProperty("sAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge));
- desc.setProperty("tAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge));
- desc.setProperty("minFilter", @intFromEnum(mtl.MTLSamplerMinMagFilter.linear));
- desc.setProperty("magFilter", @intFromEnum(mtl.MTLSamplerMinMagFilter.linear));
-
- const sampler = device.msgSend(
- objc.Object,
- objc.sel("newSamplerStateWithDescriptor:"),
- .{desc},
- );
- errdefer sampler.msgSend(void, objc.sel("release"), .{});
-
- return .{ .sampler = sampler };
- }
-
- pub fn deinit(self: *Sampler) void {
- self.sampler.msgSend(void, objc.sel("release"), .{});
- }
-};
diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig
index 8fa170bf2..dc5d1122c 100644
--- a/src/renderer/metal/shaders.zig
+++ b/src/renderer/metal/shaders.zig
@@ -6,6 +6,7 @@ const objc = @import("objc");
const math = @import("../../math.zig");
const mtl = @import("api.zig");
+const Pipeline = @import("Pipeline.zig");
const log = std.log.scoped(.metal);
@@ -14,20 +15,24 @@ pub const Shaders = struct {
library: objc.Object,
/// Renders cell foreground elements (text, decorations).
- cell_text_pipeline: objc.Object,
+ cell_text_pipeline: Pipeline,
/// The cell background shader is the shader used to render the
/// background of terminal cells.
- cell_bg_pipeline: objc.Object,
+ cell_bg_pipeline: Pipeline,
/// The image shader is the shader used to render images for things
/// like the Kitty image protocol.
- image_pipeline: objc.Object,
+ image_pipeline: Pipeline,
/// Custom shaders to run against the final drawable texture. This
/// can be used to apply a lot of effects. Each shader is run in sequence
/// against the output of the previous shader.
- post_pipelines: []const objc.Object,
+ post_pipelines: []const Pipeline,
+
+ /// Set to true when deinited, if you try to deinit a defunct set
+ /// of shaders it will just be ignored, to prevent double-free.
+ defunct: bool = false,
/// Initialize our shader set.
///
@@ -44,15 +49,15 @@ pub const Shaders = struct {
errdefer library.msgSend(void, objc.sel("release"), .{});
const cell_text_pipeline = try initCellTextPipeline(device, library, pixel_format);
- errdefer cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
+ errdefer cell_text_pipeline.deinit();
const cell_bg_pipeline = try initCellBgPipeline(device, library, pixel_format);
- errdefer cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
+ errdefer cell_bg_pipeline.deinit();
const image_pipeline = try initImagePipeline(device, library, pixel_format);
- errdefer image_pipeline.msgSend(void, objc.sel("release"), .{});
+ errdefer image_pipeline.deinit();
- const post_pipelines: []const objc.Object = initPostPipelines(
+ const post_pipelines: []const Pipeline = initPostPipelines(
alloc,
device,
library,
@@ -66,7 +71,7 @@ pub const Shaders = struct {
break :err &.{};
};
errdefer if (post_pipelines.len > 0) {
- for (post_pipelines) |pipeline| pipeline.msgSend(void, objc.sel("release"), .{});
+ for (post_pipelines) |pipeline| pipeline.deinit();
alloc.free(post_pipelines);
};
@@ -80,16 +85,19 @@ pub const Shaders = struct {
}
pub fn deinit(self: *Shaders, alloc: Allocator) void {
+ if (self.defunct) return;
+ self.defunct = true;
+
// Release our primary shaders
- self.cell_text_pipeline.msgSend(void, objc.sel("release"), .{});
- self.cell_bg_pipeline.msgSend(void, objc.sel("release"), .{});
- self.image_pipeline.msgSend(void, objc.sel("release"), .{});
+ self.cell_text_pipeline.deinit();
+ self.cell_bg_pipeline.deinit();
+ self.image_pipeline.deinit();
self.library.msgSend(void, objc.sel("release"), .{});
// Release our postprocess shaders
if (self.post_pipelines.len > 0) {
for (self.post_pipelines) |pipeline| {
- pipeline.msgSend(void, objc.sel("release"), .{});
+ pipeline.deinit();
}
alloc.free(self.post_pipelines);
}
@@ -106,7 +114,7 @@ pub const Image = extern struct {
/// The uniforms that are passed to the terminal cell shader.
pub const Uniforms = extern struct {
- // Note: all of the explicit aligmnments are copied from the
+ // Note: all of the explicit alignments are copied from the
// MSL developer reference just so that we can be sure that we got
// it all exactly right.
@@ -140,25 +148,30 @@ pub const Uniforms = extern struct {
/// The background color for the whole surface.
bg_color: [4]u8 align(4),
- /// Whether the cursor is 2 cells wide.
- cursor_wide: bool align(1),
+ /// Various booleans.
+ ///
+ /// TODO: Maybe put these in a packed struct, like for OpenGL.
+ bools: extern struct {
+ /// Whether the cursor is 2 cells wide.
+ cursor_wide: bool align(1),
- /// Indicates that colors provided to the shader are already in
- /// the P3 color space, so they don't need to be converted from
- /// sRGB.
- use_display_p3: bool align(1),
+ /// Indicates that colors provided to the shader are already in
+ /// the P3 color space, so they don't need to be converted from
+ /// sRGB.
+ use_display_p3: bool align(1),
- /// Indicates that the color attachments for the shaders have
- /// an `*_srgb` pixel format, which means the shaders need to
- /// output linear RGB colors rather than gamma encoded colors,
- /// since blending will be performed in linear space and then
- /// Metal itself will re-encode the colors for storage.
- use_linear_blending: bool align(1),
+ /// Indicates that the color attachments for the shaders have
+ /// an `*_srgb` pixel format, which means the shaders need to
+ /// output linear RGB colors rather than gamma encoded colors,
+ /// since blending will be performed in linear space and then
+ /// Metal itself will re-encode the colors for storage.
+ use_linear_blending: bool align(1),
- /// Enables a weight correction step that makes text rendered
- /// with linear alpha blending have a similar apparent weight
- /// (thickness) to gamma-incorrect blending.
- use_linear_correction: bool align(1) = false,
+ /// Enables a weight correction step that makes text rendered
+ /// with linear alpha blending have a similar apparent weight
+ /// (thickness) to gamma-incorrect blending.
+ use_linear_correction: bool align(1) = false,
+ },
const PaddingExtend = packed struct(u8) {
left: bool = false,
@@ -169,23 +182,6 @@ pub const Uniforms = extern struct {
};
};
-/// The uniforms used for custom postprocess shaders.
-pub const PostUniforms = extern struct {
- // Note: all of the explicit aligmnments are copied from the
- // MSL developer reference just so that we can be sure that we got
- // it all exactly right.
- resolution: [3]f32 align(16),
- time: f32 align(4),
- time_delta: f32 align(4),
- frame_rate: f32 align(4),
- frame: i32 align(4),
- channel_time: [4][4]f32 align(16),
- channel_resolution: [4][4]f32 align(16),
- mouse: [4]f32 align(16),
- date: [4]f32 align(16),
- sample_rate: f32 align(4),
-};
-
/// Initialize the MTLLibrary. A MTLLibrary is a collection of shaders.
fn initLibrary(device: objc.Object) !objc.Object {
const start = try std.time.Instant.now();
@@ -214,15 +210,16 @@ fn initLibrary(device: objc.Object) !objc.Object {
return library;
}
-/// Initialize our custom shader pipelines. The shaders argument is a
-/// set of shader source code, not file paths.
+/// Initialize our custom shader pipelines.
+///
+/// The shaders argument is a set of shader source code, not file paths.
fn initPostPipelines(
alloc: Allocator,
device: objc.Object,
library: objc.Object,
shaders: []const [:0]const u8,
pixel_format: mtl.MTLPixelFormat,
-) ![]const objc.Object {
+) ![]const Pipeline {
// If we have no shaders, do nothing.
if (shaders.len == 0) return &.{};
@@ -230,10 +227,10 @@ fn initPostPipelines(
var i: usize = 0;
// Initialize our result set. If any error happens, we undo everything.
- var pipelines = try alloc.alloc(objc.Object, shaders.len);
+ var pipelines = try alloc.alloc(Pipeline, shaders.len);
errdefer {
for (pipelines[0..i]) |pipeline| {
- pipeline.msgSend(void, objc.sel("release"), .{});
+ pipeline.deinit();
}
alloc.free(pipelines);
}
@@ -259,7 +256,7 @@ fn initPostPipeline(
library: objc.Object,
data: [:0]const u8,
pixel_format: mtl.MTLPixelFormat,
-) !objc.Object {
+) !Pipeline {
// Create our library which has the shader source
const post_library = library: {
const source = try macos.foundation.String.createWithBytes(
@@ -282,65 +279,19 @@ fn initPostPipeline(
};
defer post_library.msgSend(void, objc.sel("release"), .{});
- // Get our vertex and fragment functions
- const func_vert = func_vert: {
- const str = try macos.foundation.String.createWithBytes(
- "full_screen_vertex",
- .utf8,
- false,
- );
- defer str.release();
-
- const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
- break :func_vert objc.Object.fromId(ptr.?);
- };
- const func_frag = func_frag: {
- const str = try macos.foundation.String.createWithBytes(
- "main0",
- .utf8,
- false,
- );
- defer str.release();
-
- const ptr = post_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
- break :func_frag objc.Object.fromId(ptr.?);
- };
- defer func_vert.msgSend(void, objc.sel("release"), .{});
- defer func_frag.msgSend(void, objc.sel("release"), .{});
-
- // Create our descriptor
- const desc = init: {
- const Class = objc.getClass("MTLRenderPipelineDescriptor").?;
- const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
- const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
- break :init id_init;
- };
- defer desc.msgSend(void, objc.sel("release"), .{});
- desc.setProperty("vertexFunction", func_vert);
- desc.setProperty("fragmentFunction", func_frag);
-
- // Set our color attachment
- const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
- {
- const attachment = attachments.msgSend(
- objc.Object,
- objc.sel("objectAtIndexedSubscript:"),
- .{@as(c_ulong, 0)},
- );
-
- attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
- }
-
- // Make our state
- var err: ?*anyopaque = null;
- const pipeline_state = device.msgSend(
- objc.Object,
- objc.sel("newRenderPipelineStateWithDescriptor:error:"),
- .{ desc, &err },
- );
- try checkError(err);
-
- return pipeline_state;
+ return try Pipeline.init(null, .{
+ .device = device,
+ .vertex_fn = "full_screen_vertex",
+ .fragment_fn = "main0",
+ .vertex_library = library,
+ .fragment_library = post_library,
+ .attachments = &.{
+ .{
+ .pixel_format = pixel_format,
+ .blending_enabled = false,
+ },
+ },
+ });
}
/// This is a single parameter for the terminal cell shader.
@@ -373,114 +324,21 @@ fn initCellTextPipeline(
device: objc.Object,
library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
-) !objc.Object {
- // Get our vertex and fragment functions
- const func_vert = func_vert: {
- const str = try macos.foundation.String.createWithBytes(
- "cell_text_vertex",
- .utf8,
- false,
- );
- defer str.release();
-
- const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
- break :func_vert objc.Object.fromId(ptr.?);
- };
- const func_frag = func_frag: {
- const str = try macos.foundation.String.createWithBytes(
- "cell_text_fragment",
- .utf8,
- false,
- );
- defer str.release();
-
- const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
- break :func_frag objc.Object.fromId(ptr.?);
- };
- defer func_vert.msgSend(void, objc.sel("release"), .{});
- defer func_frag.msgSend(void, objc.sel("release"), .{});
-
- // Create the vertex descriptor. The vertex descriptor describes the
- // data layout of the vertex inputs. We use indexed (or "instanced")
- // rendering, so this makes it so that each instance gets a single
- // Cell as input.
- const vertex_desc = vertex_desc: {
- const desc = init: {
- const Class = objc.getClass("MTLVertexDescriptor").?;
- const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
- const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
- break :init id_init;
- };
-
- // Our attributes are the fields of the input
- const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes"));
- autoAttribute(CellText, attrs);
-
- // The layout describes how and when we fetch the next vertex input.
- const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts"));
- {
- const layout = layouts.msgSend(
- objc.Object,
- objc.sel("objectAtIndexedSubscript:"),
- .{@as(c_ulong, 0)},
- );
-
- // Access each Cell per instance, not per vertex.
- layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance));
- layout.setProperty("stride", @as(c_ulong, @sizeOf(CellText)));
- }
-
- break :vertex_desc desc;
- };
- defer vertex_desc.msgSend(void, objc.sel("release"), .{});
-
- // Create our descriptor
- const desc = init: {
- const Class = objc.getClass("MTLRenderPipelineDescriptor").?;
- const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
- const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
- break :init id_init;
- };
- defer desc.msgSend(void, objc.sel("release"), .{});
-
- // Set our properties
- desc.setProperty("vertexFunction", func_vert);
- desc.setProperty("fragmentFunction", func_frag);
- desc.setProperty("vertexDescriptor", vertex_desc);
-
- // Set our color attachment
- const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
- {
- const attachment = attachments.msgSend(
- objc.Object,
- objc.sel("objectAtIndexedSubscript:"),
- .{@as(c_ulong, 0)},
- );
-
- attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
-
- // Blending. This is required so that our text we render on top
- // of our drawable properly blends into the bg.
- attachment.setProperty("blendingEnabled", true);
- attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
- attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
- attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
- attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
- attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
- attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
- }
-
- // Make our state
- var err: ?*anyopaque = null;
- const pipeline_state = device.msgSend(
- objc.Object,
- objc.sel("newRenderPipelineStateWithDescriptor:error:"),
- .{ desc, &err },
- );
- try checkError(err);
- errdefer pipeline_state.msgSend(void, objc.sel("release"), .{});
-
- return pipeline_state;
+) !Pipeline {
+ return try Pipeline.init(CellText, .{
+ .device = device,
+ .vertex_fn = "cell_text_vertex",
+ .fragment_fn = "cell_text_fragment",
+ .vertex_library = library,
+ .fragment_library = library,
+ .step_fn = .per_instance,
+ .attachments = &.{
+ .{
+ .pixel_format = pixel_format,
+ .blending_enabled = true,
+ },
+ },
+ });
}
/// This is a single parameter for the cell bg shader.
@@ -491,79 +349,20 @@ fn initCellBgPipeline(
device: objc.Object,
library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
-) !objc.Object {
- // Get our vertex and fragment functions
- const func_vert = func_vert: {
- const str = try macos.foundation.String.createWithBytes(
- "cell_bg_vertex",
- .utf8,
- false,
- );
- defer str.release();
-
- const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
- break :func_vert objc.Object.fromId(ptr.?);
- };
- defer func_vert.msgSend(void, objc.sel("release"), .{});
- const func_frag = func_frag: {
- const str = try macos.foundation.String.createWithBytes(
- "cell_bg_fragment",
- .utf8,
- false,
- );
- defer str.release();
-
- const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
- break :func_frag objc.Object.fromId(ptr.?);
- };
- defer func_frag.msgSend(void, objc.sel("release"), .{});
-
- // Create our descriptor
- const desc = init: {
- const Class = objc.getClass("MTLRenderPipelineDescriptor").?;
- const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
- const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
- break :init id_init;
- };
- defer desc.msgSend(void, objc.sel("release"), .{});
-
- // Set our properties
- desc.setProperty("vertexFunction", func_vert);
- desc.setProperty("fragmentFunction", func_frag);
-
- // Set our color attachment
- const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
- {
- const attachment = attachments.msgSend(
- objc.Object,
- objc.sel("objectAtIndexedSubscript:"),
- .{@as(c_ulong, 0)},
- );
-
- attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
-
- // Blending. This is required so that our text we render on top
- // of our drawable properly blends into the bg.
- attachment.setProperty("blendingEnabled", true);
- attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
- attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
- attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
- attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
- attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
- attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
- }
-
- // Make our state
- var err: ?*anyopaque = null;
- const pipeline_state = device.msgSend(
- objc.Object,
- objc.sel("newRenderPipelineStateWithDescriptor:error:"),
- .{ desc, &err },
- );
- try checkError(err);
- errdefer pipeline_state.msgSend(void, objc.sel("release"), .{});
-
- return pipeline_state;
+) !Pipeline {
+ return try Pipeline.init(null, .{
+ .device = device,
+ .vertex_fn = "cell_bg_vertex",
+ .fragment_fn = "cell_bg_fragment",
+ .vertex_library = library,
+ .fragment_library = library,
+ .attachments = &.{
+ .{
+ .pixel_format = pixel_format,
+ .blending_enabled = false,
+ },
+ },
+ });
}
/// Initialize the image render pipeline for our shader library.
@@ -571,148 +370,21 @@ fn initImagePipeline(
device: objc.Object,
library: objc.Object,
pixel_format: mtl.MTLPixelFormat,
-) !objc.Object {
- // Get our vertex and fragment functions
- const func_vert = func_vert: {
- const str = try macos.foundation.String.createWithBytes(
- "image_vertex",
- .utf8,
- false,
- );
- defer str.release();
-
- const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
- break :func_vert objc.Object.fromId(ptr.?);
- };
- const func_frag = func_frag: {
- const str = try macos.foundation.String.createWithBytes(
- "image_fragment",
- .utf8,
- false,
- );
- defer str.release();
-
- const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str});
- break :func_frag objc.Object.fromId(ptr.?);
- };
- defer func_vert.msgSend(void, objc.sel("release"), .{});
- defer func_frag.msgSend(void, objc.sel("release"), .{});
-
- // Create the vertex descriptor. The vertex descriptor describes the
- // data layout of the vertex inputs. We use indexed (or "instanced")
- // rendering, so this makes it so that each instance gets a single
- // Image as input.
- const vertex_desc = vertex_desc: {
- const desc = init: {
- const Class = objc.getClass("MTLVertexDescriptor").?;
- const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
- const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
- break :init id_init;
- };
-
- // Our attributes are the fields of the input
- const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes"));
- autoAttribute(Image, attrs);
-
- // The layout describes how and when we fetch the next vertex input.
- const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts"));
- {
- const layout = layouts.msgSend(
- objc.Object,
- objc.sel("objectAtIndexedSubscript:"),
- .{@as(c_ulong, 0)},
- );
-
- // Access each Image per instance, not per vertex.
- layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance));
- layout.setProperty("stride", @as(c_ulong, @sizeOf(Image)));
- }
-
- break :vertex_desc desc;
- };
- defer vertex_desc.msgSend(void, objc.sel("release"), .{});
-
- // Create our descriptor
- const desc = init: {
- const Class = objc.getClass("MTLRenderPipelineDescriptor").?;
- const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{});
- const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{});
- break :init id_init;
- };
- defer desc.msgSend(void, objc.sel("release"), .{});
-
- // Set our properties
- desc.setProperty("vertexFunction", func_vert);
- desc.setProperty("fragmentFunction", func_frag);
- desc.setProperty("vertexDescriptor", vertex_desc);
-
- // Set our color attachment
- const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments"));
- {
- const attachment = attachments.msgSend(
- objc.Object,
- objc.sel("objectAtIndexedSubscript:"),
- .{@as(c_ulong, 0)},
- );
-
- attachment.setProperty("pixelFormat", @intFromEnum(pixel_format));
-
- // Blending. This is required so that our text we render on top
- // of our drawable properly blends into the bg.
- attachment.setProperty("blendingEnabled", true);
- attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
- attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add));
- attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
- attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one));
- attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
- attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha));
- }
-
- // Make our state
- var err: ?*anyopaque = null;
- const pipeline_state = device.msgSend(
- objc.Object,
- objc.sel("newRenderPipelineStateWithDescriptor:error:"),
- .{ desc, &err },
- );
- try checkError(err);
-
- return pipeline_state;
-}
-
-fn autoAttribute(T: type, attrs: objc.Object) void {
- inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
- const offset = @offsetOf(T, field.name);
-
- const FT = switch (@typeInfo(field.type)) {
- .@"enum" => |e| e.tag_type,
- else => field.type,
- };
-
- const format = switch (FT) {
- [4]u8 => mtl.MTLVertexFormat.uchar4,
- [2]u16 => mtl.MTLVertexFormat.ushort2,
- [2]i16 => mtl.MTLVertexFormat.short2,
- [2]f32 => mtl.MTLVertexFormat.float2,
- [4]f32 => mtl.MTLVertexFormat.float4,
- [2]i32 => mtl.MTLVertexFormat.int2,
- u32 => mtl.MTLVertexFormat.uint,
- [2]u32 => mtl.MTLVertexFormat.uint2,
- [4]u32 => mtl.MTLVertexFormat.uint4,
- u8 => mtl.MTLVertexFormat.uchar,
- else => comptime unreachable,
- };
-
- const attr = attrs.msgSend(
- objc.Object,
- objc.sel("objectAtIndexedSubscript:"),
- .{@as(c_ulong, i)},
- );
-
- attr.setProperty("format", @intFromEnum(format));
- attr.setProperty("offset", @as(c_ulong, offset));
- attr.setProperty("bufferIndex", @as(c_ulong, 0));
- }
+) !Pipeline {
+ return try Pipeline.init(Image, .{
+ .device = device,
+ .vertex_fn = "image_vertex",
+ .fragment_fn = "image_fragment",
+ .vertex_library = library,
+ .fragment_library = library,
+ .step_fn = .per_instance,
+ .attachments = &.{
+ .{
+ .pixel_format = pixel_format,
+ .blending_enabled = true,
+ },
+ },
+ });
}
fn checkError(err_: ?*anyopaque) !void {
diff --git a/src/renderer/opengl/CellProgram.zig b/src/renderer/opengl/CellProgram.zig
deleted file mode 100644
index c4da8e233..000000000
--- a/src/renderer/opengl/CellProgram.zig
+++ /dev/null
@@ -1,196 +0,0 @@
-/// The OpenGL program for rendering terminal cells.
-const CellProgram = @This();
-
-const std = @import("std");
-const gl = @import("opengl");
-
-program: gl.Program,
-vao: gl.VertexArray,
-ebo: gl.Buffer,
-vbo: gl.Buffer,
-
-/// The raw structure that maps directly to the buffer sent to the vertex shader.
-/// This must be "extern" so that the field order is not reordered by the
-/// Zig compiler.
-pub const Cell = extern struct {
- /// vec2 grid_coord
- grid_col: u16,
- grid_row: u16,
-
- /// vec2 glyph_pos
- glyph_x: u32 = 0,
- glyph_y: u32 = 0,
-
- /// vec2 glyph_size
- glyph_width: u32 = 0,
- glyph_height: u32 = 0,
-
- /// vec2 glyph_offset
- glyph_offset_x: i32 = 0,
- glyph_offset_y: i32 = 0,
-
- /// vec4 color_in
- r: u8,
- g: u8,
- b: u8,
- a: u8,
-
- /// vec4 bg_color_in
- bg_r: u8,
- bg_g: u8,
- bg_b: u8,
- bg_a: u8,
-
- /// uint mode
- mode: CellMode,
-
- /// The width in grid cells that a rendering takes.
- grid_width: u8,
-};
-
-pub const CellMode = enum(u8) {
- bg = 1,
- fg = 2,
- fg_constrained = 3,
- fg_color = 7,
- fg_powerline = 15,
-
- // Non-exhaustive because masks change it
- _,
-
- /// Apply a mask to the mode.
- pub fn mask(self: CellMode, m: CellMode) CellMode {
- return @enumFromInt(@intFromEnum(self) | @intFromEnum(m));
- }
-
- pub fn isFg(self: CellMode) bool {
- // Since we use bit tricks below, we want to ensure the enum
- // doesn't change without us looking at this logic again.
- comptime {
- const info = @typeInfo(CellMode).@"enum";
- std.debug.assert(info.fields.len == 5);
- }
-
- return @intFromEnum(self) & @intFromEnum(@as(CellMode, .fg)) != 0;
- }
-};
-
-pub fn init() !CellProgram {
- // Load and compile our shaders.
- const program = try gl.Program.createVF(
- @embedFile("../shaders/cell.v.glsl"),
- @embedFile("../shaders/cell.f.glsl"),
- );
- errdefer program.destroy();
-
- // Set our cell dimensions
- const pbind = try program.use();
- defer pbind.unbind();
-
- // Set all of our texture indexes
- try program.setUniform("text", 0);
- try program.setUniform("text_color", 1);
-
- // Setup our VAO
- const vao = try gl.VertexArray.create();
- errdefer vao.destroy();
- const vaobind = try vao.bind();
- defer vaobind.unbind();
-
- // Element buffer (EBO)
- const ebo = try gl.Buffer.create();
- errdefer ebo.destroy();
- var ebobind = try ebo.bind(.element_array);
- defer ebobind.unbind();
- try ebobind.setData([6]u8{
- 0, 1, 3, // Top-left triangle
- 1, 2, 3, // Bottom-right triangle
- }, .static_draw);
-
- // Vertex buffer (VBO)
- const vbo = try gl.Buffer.create();
- errdefer vbo.destroy();
- var vbobind = try vbo.bind(.array);
- defer vbobind.unbind();
- var offset: usize = 0;
- try vbobind.attributeAdvanced(0, 2, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(Cell), offset);
- offset += 2 * @sizeOf(u16);
- try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Cell), offset);
- offset += 2 * @sizeOf(u32);
- try vbobind.attributeAdvanced(2, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Cell), offset);
- offset += 2 * @sizeOf(u32);
- try vbobind.attributeAdvanced(3, 2, gl.c.GL_INT, false, @sizeOf(Cell), offset);
- offset += 2 * @sizeOf(i32);
- try vbobind.attributeAdvanced(4, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Cell), offset);
- offset += 4 * @sizeOf(u8);
- try vbobind.attributeAdvanced(5, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Cell), offset);
- offset += 4 * @sizeOf(u8);
- try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset);
- offset += 1 * @sizeOf(u8);
- try vbobind.attributeIAdvanced(7, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset);
- try vbobind.enableAttribArray(0);
- try vbobind.enableAttribArray(1);
- try vbobind.enableAttribArray(2);
- try vbobind.enableAttribArray(3);
- try vbobind.enableAttribArray(4);
- try vbobind.enableAttribArray(5);
- try vbobind.enableAttribArray(6);
- try vbobind.enableAttribArray(7);
- try vbobind.attributeDivisor(0, 1);
- try vbobind.attributeDivisor(1, 1);
- try vbobind.attributeDivisor(2, 1);
- try vbobind.attributeDivisor(3, 1);
- try vbobind.attributeDivisor(4, 1);
- try vbobind.attributeDivisor(5, 1);
- try vbobind.attributeDivisor(6, 1);
- try vbobind.attributeDivisor(7, 1);
-
- return .{
- .program = program,
- .vao = vao,
- .ebo = ebo,
- .vbo = vbo,
- };
-}
-
-pub fn bind(self: CellProgram) !Binding {
- const program = try self.program.use();
- errdefer program.unbind();
-
- const vao = try self.vao.bind();
- errdefer vao.unbind();
-
- const ebo = try self.ebo.bind(.element_array);
- errdefer ebo.unbind();
-
- const vbo = try self.vbo.bind(.array);
- errdefer vbo.unbind();
-
- return .{
- .program = program,
- .vao = vao,
- .ebo = ebo,
- .vbo = vbo,
- };
-}
-
-pub fn deinit(self: CellProgram) void {
- self.vbo.destroy();
- self.ebo.destroy();
- self.vao.destroy();
- self.program.destroy();
-}
-
-pub const Binding = struct {
- program: gl.Program.Binding,
- vao: gl.VertexArray.Binding,
- ebo: gl.Buffer.Binding,
- vbo: gl.Buffer.Binding,
-
- pub fn unbind(self: Binding) void {
- self.vbo.unbind();
- self.ebo.unbind();
- self.vao.unbind();
- self.program.unbind();
- }
-};
diff --git a/src/renderer/opengl/Frame.zig b/src/renderer/opengl/Frame.zig
new file mode 100644
index 000000000..4c23fe106
--- /dev/null
+++ b/src/renderer/opengl/Frame.zig
@@ -0,0 +1,75 @@
+//! Wrapper for handling render passes.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const builtin = @import("builtin");
+const gl = @import("opengl");
+
+const Renderer = @import("../generic.zig").Renderer(OpenGL);
+const OpenGL = @import("../OpenGL.zig");
+const Target = @import("Target.zig");
+const Pipeline = @import("Pipeline.zig");
+const RenderPass = @import("RenderPass.zig");
+const Buffer = @import("buffer.zig").Buffer;
+
+const Health = @import("../../renderer.zig").Health;
+
+const log = std.log.scoped(.opengl);
+
+/// Options for beginning a frame.
+pub const Options = struct {};
+
+renderer: *Renderer,
+target: *Target,
+
+/// Begin encoding a frame.
+pub fn begin(
+ opts: Options,
+ /// Once the frame has been completed, the `frameCompleted` method
+ /// on the renderer is called with the health status of the frame.
+ renderer: *Renderer,
+ /// The target is presented via the provided renderer's API when completed.
+ target: *Target,
+) !Self {
+ _ = opts;
+
+ return .{
+ .renderer = renderer,
+ .target = target,
+ };
+}
+
+/// Add a render pass to this frame with the provided attachments.
+/// Returns a RenderPass which allows render steps to be added.
+pub inline fn renderPass(
+ self: *const Self,
+ attachments: []const RenderPass.Options.Attachment,
+) RenderPass {
+ _ = self;
+ return RenderPass.begin(.{ .attachments = attachments });
+}
+
+/// Complete this frame and present the target.
+///
+/// If `sync` is true, this will block until the frame is presented.
+///
+/// NOTE: For OpenGL, `sync` is ignored and we always block.
+pub fn complete(self: *const Self, sync: bool) void {
+ _ = sync;
+ gl.finish();
+
+ // If there are any GL errors, consider the frame unhealthy.
+ const health: Health = if (gl.errors.getError()) .healthy else |_| .unhealthy;
+
+ // If the frame is healthy, present it.
+ if (health == .healthy) {
+ self.renderer.api.present(self.target.*) catch |err| {
+ log.err("Failed to present render target: err={}", .{err});
+ };
+ }
+
+ // Report the health to the renderer.
+ self.renderer.frameCompleted(health);
+}
diff --git a/src/renderer/opengl/ImageProgram.zig b/src/renderer/opengl/ImageProgram.zig
deleted file mode 100644
index ff6794085..000000000
--- a/src/renderer/opengl/ImageProgram.zig
+++ /dev/null
@@ -1,134 +0,0 @@
-/// The OpenGL program for rendering terminal cells.
-const ImageProgram = @This();
-
-const std = @import("std");
-const gl = @import("opengl");
-
-program: gl.Program,
-vao: gl.VertexArray,
-ebo: gl.Buffer,
-vbo: gl.Buffer,
-
-pub const Input = extern struct {
- /// vec2 grid_coord
- grid_col: i32,
- grid_row: i32,
-
- /// vec2 cell_offset
- cell_offset_x: u32 = 0,
- cell_offset_y: u32 = 0,
-
- /// vec4 source_rect
- source_x: u32 = 0,
- source_y: u32 = 0,
- source_width: u32 = 0,
- source_height: u32 = 0,
-
- /// vec2 dest_size
- dest_width: u32 = 0,
- dest_height: u32 = 0,
-};
-
-pub fn init() !ImageProgram {
- // Load and compile our shaders.
- const program = try gl.Program.createVF(
- @embedFile("../shaders/image.v.glsl"),
- @embedFile("../shaders/image.f.glsl"),
- );
- errdefer program.destroy();
-
- // Set our program uniforms
- const pbind = try program.use();
- defer pbind.unbind();
-
- // Set all of our texture indexes
- try program.setUniform("image", 0);
-
- // Setup our VAO
- const vao = try gl.VertexArray.create();
- errdefer vao.destroy();
- const vaobind = try vao.bind();
- defer vaobind.unbind();
-
- // Element buffer (EBO)
- const ebo = try gl.Buffer.create();
- errdefer ebo.destroy();
- var ebobind = try ebo.bind(.element_array);
- defer ebobind.unbind();
- try ebobind.setData([6]u8{
- 0, 1, 3, // Top-left triangle
- 1, 2, 3, // Bottom-right triangle
- }, .static_draw);
-
- // Vertex buffer (VBO)
- const vbo = try gl.Buffer.create();
- errdefer vbo.destroy();
- var vbobind = try vbo.bind(.array);
- defer vbobind.unbind();
- var offset: usize = 0;
- try vbobind.attributeAdvanced(0, 2, gl.c.GL_INT, false, @sizeOf(Input), offset);
- offset += 2 * @sizeOf(i32);
- try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset);
- offset += 2 * @sizeOf(u32);
- try vbobind.attributeAdvanced(2, 4, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset);
- offset += 4 * @sizeOf(u32);
- try vbobind.attributeAdvanced(3, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset);
- offset += 2 * @sizeOf(u32);
- try vbobind.enableAttribArray(0);
- try vbobind.enableAttribArray(1);
- try vbobind.enableAttribArray(2);
- try vbobind.enableAttribArray(3);
- try vbobind.attributeDivisor(0, 1);
- try vbobind.attributeDivisor(1, 1);
- try vbobind.attributeDivisor(2, 1);
- try vbobind.attributeDivisor(3, 1);
-
- return .{
- .program = program,
- .vao = vao,
- .ebo = ebo,
- .vbo = vbo,
- };
-}
-
-pub fn bind(self: ImageProgram) !Binding {
- const program = try self.program.use();
- errdefer program.unbind();
-
- const vao = try self.vao.bind();
- errdefer vao.unbind();
-
- const ebo = try self.ebo.bind(.element_array);
- errdefer ebo.unbind();
-
- const vbo = try self.vbo.bind(.array);
- errdefer vbo.unbind();
-
- return .{
- .program = program,
- .vao = vao,
- .ebo = ebo,
- .vbo = vbo,
- };
-}
-
-pub fn deinit(self: ImageProgram) void {
- self.vbo.destroy();
- self.ebo.destroy();
- self.vao.destroy();
- self.program.destroy();
-}
-
-pub const Binding = struct {
- program: gl.Program.Binding,
- vao: gl.VertexArray.Binding,
- ebo: gl.Buffer.Binding,
- vbo: gl.Buffer.Binding,
-
- pub fn unbind(self: Binding) void {
- self.vbo.unbind();
- self.ebo.unbind();
- self.vao.unbind();
- self.program.unbind();
- }
-};
diff --git a/src/renderer/opengl/Pipeline.zig b/src/renderer/opengl/Pipeline.zig
new file mode 100644
index 000000000..501e6124c
--- /dev/null
+++ b/src/renderer/opengl/Pipeline.zig
@@ -0,0 +1,169 @@
+//! Wrapper for handling render pipelines.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const builtin = @import("builtin");
+const gl = @import("opengl");
+
+const OpenGL = @import("../OpenGL.zig");
+const Texture = @import("Texture.zig");
+const Buffer = @import("buffer.zig").Buffer;
+
+const log = std.log.scoped(.opengl);
+
+/// Options for initializing a render pipeline.
+pub const Options = struct {
+ /// GLSL source of the vertex function
+ vertex_fn: [:0]const u8,
+ /// GLSL source of the fragment function
+ fragment_fn: [:0]const u8,
+
+ /// Vertex step function
+ step_fn: StepFunction = .per_vertex,
+
+ /// Whether to enable blending.
+ blending_enabled: bool = true,
+
+ pub const StepFunction = enum {
+ constant,
+ per_vertex,
+ per_instance,
+ };
+};
+
+program: gl.Program,
+
+fbo: gl.Framebuffer,
+
+vao: gl.VertexArray,
+
+stride: usize,
+
+blending_enabled: bool,
+
+pub fn init(comptime VertexAttributes: ?type, opts: Options) !Self {
+ // Load and compile our shaders.
+ const program = try gl.Program.createVF(
+ opts.vertex_fn,
+ opts.fragment_fn,
+ );
+ errdefer program.destroy();
+
+ const pbind = try program.use();
+ defer pbind.unbind();
+
+ const fbo = try gl.Framebuffer.create();
+ errdefer fbo.destroy();
+ const fbobind = try fbo.bind(.framebuffer);
+ defer fbobind.unbind();
+
+ const vao = try gl.VertexArray.create();
+ errdefer vao.destroy();
+ const vaobind = try vao.bind();
+ defer vaobind.unbind();
+
+ if (VertexAttributes) |VA| try autoAttribute(VA, vaobind, opts.step_fn);
+
+ return .{
+ .program = program,
+ .fbo = fbo,
+ .vao = vao,
+ .stride = if (VertexAttributes) |VA| @sizeOf(VA) else 0,
+ .blending_enabled = opts.blending_enabled,
+ };
+}
+
+pub fn deinit(self: *const Self) void {
+ self.program.destroy();
+}
+
+fn autoAttribute(
+ T: type,
+ vaobind: gl.VertexArray.Binding,
+ step_fn: Options.StepFunction,
+) !void {
+ const divisor: gl.c.GLuint = switch (step_fn) {
+ .per_vertex => 0,
+ .per_instance => 1,
+ .constant => std.math.maxInt(gl.c.GLuint),
+ };
+
+ inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| {
+ try vaobind.enableAttribArray(i);
+ try vaobind.attributeBinding(i, 0);
+ try vaobind.bindingDivisor(i, divisor);
+
+ const offset = @offsetOf(T, field.name);
+
+ const FT = switch (@typeInfo(field.type)) {
+ .@"enum" => |e| e.tag_type,
+ else => field.type,
+ };
+
+ const size, const IT = switch (@typeInfo(FT)) {
+ .array => |a| .{ a.len, a.child },
+ else => .{ 1, FT },
+ };
+
+ try switch (IT) {
+ u8 => vaobind.attributeIFormat(
+ i,
+ size,
+ gl.c.GL_UNSIGNED_BYTE,
+ offset,
+ ),
+ u16 => vaobind.attributeIFormat(
+ i,
+ size,
+ gl.c.GL_UNSIGNED_SHORT,
+ offset,
+ ),
+ u32 => vaobind.attributeIFormat(
+ i,
+ size,
+ gl.c.GL_UNSIGNED_INT,
+ offset,
+ ),
+ i8 => vaobind.attributeIFormat(
+ i,
+ size,
+ gl.c.GL_BYTE,
+ offset,
+ ),
+ i16 => vaobind.attributeIFormat(
+ i,
+ size,
+ gl.c.GL_SHORT,
+ offset,
+ ),
+ i32 => vaobind.attributeIFormat(
+ i,
+ size,
+ gl.c.GL_INT,
+ offset,
+ ),
+ f16 => vaobind.attributeFormat(
+ i,
+ size,
+ gl.c.GL_HALF_FLOAT,
+ false,
+ offset,
+ ),
+ f32 => vaobind.attributeFormat(
+ i,
+ size,
+ gl.c.GL_FLOAT,
+ false,
+ offset,
+ ),
+ f64 => vaobind.attributeLFormat(
+ i,
+ size,
+ offset,
+ ),
+ else => unreachable,
+ };
+ }
+}
diff --git a/src/renderer/opengl/RenderPass.zig b/src/renderer/opengl/RenderPass.zig
new file mode 100644
index 000000000..0f5bd89e7
--- /dev/null
+++ b/src/renderer/opengl/RenderPass.zig
@@ -0,0 +1,141 @@
+//! Wrapper for handling render passes.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const builtin = @import("builtin");
+const gl = @import("opengl");
+
+const OpenGL = @import("../OpenGL.zig");
+const Target = @import("Target.zig");
+const Texture = @import("Texture.zig");
+const Pipeline = @import("Pipeline.zig");
+const RenderPass = @import("RenderPass.zig");
+const Buffer = @import("buffer.zig").Buffer;
+
+/// Options for beginning a render pass.
+pub const Options = struct {
+ /// Color attachments for this render pass.
+ attachments: []const Attachment,
+
+ /// Describes a color attachment.
+ pub const Attachment = struct {
+ target: union(enum) {
+ texture: Texture,
+ target: Target,
+ },
+ clear_color: ?[4]f32 = null,
+ };
+};
+
+/// Describes a step in a render pass.
+pub const Step = struct {
+ pipeline: Pipeline,
+ uniforms: ?gl.Buffer = null,
+ buffers: []const ?gl.Buffer = &.{},
+ textures: []const ?Texture = &.{},
+ draw: Draw,
+
+ /// Describes the draw call for this step.
+ pub const Draw = struct {
+ type: gl.Primitive,
+ vertex_count: usize,
+ instance_count: usize = 1,
+ };
+};
+
+attachments: []const Options.Attachment,
+
+step_number: usize = 0,
+
+/// Begin a render pass.
+pub fn begin(
+ opts: Options,
+) Self {
+ return .{
+ .attachments = opts.attachments,
+ };
+}
+
+/// Add a step to this render pass.
+///
+/// TODO: Errors are silently ignored in this function, maybe they shouldn't be?
+pub fn step(self: *Self, s: Step) void {
+ if (s.draw.instance_count == 0) return;
+
+ const pbind = s.pipeline.program.use() catch return;
+ defer pbind.unbind();
+
+ const vaobind = s.pipeline.vao.bind() catch return;
+ defer vaobind.unbind();
+
+ const fbobind = switch (self.attachments[0].target) {
+ .target => |t| t.framebuffer.bind(.framebuffer) catch return,
+ .texture => |t| bind: {
+ const fbobind = s.pipeline.fbo.bind(.framebuffer) catch return;
+ fbobind.texture2D(.color0, t.target, t.texture, 0) catch {
+ fbobind.unbind();
+ return;
+ };
+ break :bind fbobind;
+ },
+ };
+ defer fbobind.unbind();
+
+ defer self.step_number += 1;
+
+ // If we have a clear color and this is the
+ // first step in the pass, go ahead and clear.
+ if (self.step_number == 0) if (self.attachments[0].clear_color) |c| {
+ gl.clearColor(c[0], c[1], c[2], c[3]);
+ gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
+ };
+
+ // Bind the uniform buffer we bind at index 1 to align with Metal.
+ if (s.uniforms) |ubo| {
+ _ = ubo.bindBase(.uniform, 1) catch return;
+ }
+
+ // Bind relevant texture units.
+ for (s.textures, 0..) |t, i| if (t) |tex| {
+ gl.Texture.active(@intCast(i)) catch return;
+ _ = tex.texture.bind(tex.target) catch return;
+ };
+
+ // Bind 0th buffer as the vertex buffer,
+ // and bind the rest as storage buffers.
+ if (s.buffers.len > 0) {
+ if (s.buffers[0]) |vbo| vaobind.bindVertexBuffer(
+ 0,
+ vbo.id,
+ 0,
+ @intCast(s.pipeline.stride),
+ ) catch return;
+
+ for (s.buffers[1..], 1..) |b, i| if (b) |buf| {
+ _ = buf.bindBase(.storage, @intCast(i)) catch return;
+ };
+ }
+
+ if (s.pipeline.blending_enabled) {
+ gl.enable(gl.c.GL_BLEND) catch return;
+ gl.blendFunc(gl.c.GL_ONE, gl.c.GL_ONE_MINUS_SRC_ALPHA) catch return;
+ } else {
+ gl.disable(gl.c.GL_BLEND) catch return;
+ }
+
+ gl.drawArraysInstanced(
+ s.draw.type,
+ 0,
+ @intCast(s.draw.vertex_count),
+ @intCast(s.draw.instance_count),
+ ) catch return;
+}
+
+/// Complete this render pass.
+/// This struct can no longer be used after calling this.
+pub fn complete(self: *const Self) void {
+ _ = self;
+ gl.flush();
+}
diff --git a/src/renderer/opengl/Target.zig b/src/renderer/opengl/Target.zig
new file mode 100644
index 000000000..1b3a13ed0
--- /dev/null
+++ b/src/renderer/opengl/Target.zig
@@ -0,0 +1,62 @@
+//! Represents a render target.
+//!
+//! In this case, an OpenGL renderbuffer-backed framebuffer.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const builtin = @import("builtin");
+const gl = @import("opengl");
+
+const log = std.log.scoped(.opengl);
+
+/// Options for initializing a Target
+pub const Options = struct {
+ /// Desired width
+ width: usize,
+ /// Desired height
+ height: usize,
+
+ /// Internal format for the renderbuffer.
+ internal_format: gl.Texture.InternalFormat,
+};
+
+/// The underlying `gl.Framebuffer` instance.
+framebuffer: gl.Framebuffer,
+
+/// The underlying `gl.Renderbuffer` instance.
+renderbuffer: gl.Renderbuffer,
+
+/// Current width of this target.
+width: usize,
+/// Current height of this target.
+height: usize,
+
+pub fn init(opts: Options) !Self {
+ const rbo = try gl.Renderbuffer.create();
+ const bound_rbo = try rbo.bind();
+ defer bound_rbo.unbind();
+ try bound_rbo.storage(
+ opts.internal_format,
+ @intCast(opts.width),
+ @intCast(opts.height),
+ );
+
+ const fbo = try gl.Framebuffer.create();
+ const bound_fbo = try fbo.bind(.framebuffer);
+ defer bound_fbo.unbind();
+ try bound_fbo.renderbuffer(.color0, rbo);
+
+ return .{
+ .framebuffer = fbo,
+ .renderbuffer = rbo,
+ .width = opts.width,
+ .height = opts.height,
+ };
+}
+
+pub fn deinit(self: *Self) void {
+ self.framebuffer.destroy();
+ self.renderbuffer.destroy();
+}
diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig
new file mode 100644
index 000000000..07123922f
--- /dev/null
+++ b/src/renderer/opengl/Texture.zig
@@ -0,0 +1,103 @@
+//! Wrapper for handling textures.
+const Self = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const builtin = @import("builtin");
+const gl = @import("opengl");
+
+const OpenGL = @import("../OpenGL.zig");
+
+const log = std.log.scoped(.opengl);
+
+/// Options for initializing a texture.
+pub const Options = struct {
+ format: gl.Texture.Format,
+ internal_format: gl.Texture.InternalFormat,
+ target: gl.Texture.Target,
+};
+
+texture: gl.Texture,
+
+/// The width of this texture.
+width: usize,
+/// The height of this texture.
+height: usize,
+
+/// Format for this texture.
+format: gl.Texture.Format,
+
+/// Target for this texture.
+target: gl.Texture.Target,
+
+pub const Error = error{
+ /// An OpenGL API call failed.
+ OpenGLFailed,
+};
+
+/// Initialize a texture
+pub fn init(
+ opts: Options,
+ width: usize,
+ height: usize,
+ data: ?[]const u8,
+) Error!Self {
+ const tex = gl.Texture.create() catch return error.OpenGLFailed;
+ errdefer tex.destroy();
+ {
+ const texbind = tex.bind(opts.target) catch return error.OpenGLFailed;
+ defer texbind.unbind();
+ texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed;
+ texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed;
+ texbind.parameter(.MinFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed;
+ texbind.parameter(.MagFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed;
+ texbind.image2D(
+ 0,
+ opts.internal_format,
+ @intCast(width),
+ @intCast(height),
+ 0,
+ opts.format,
+ .UnsignedByte,
+ if (data) |d| @ptrCast(d.ptr) else null,
+ ) catch return error.OpenGLFailed;
+ }
+
+ return .{
+ .texture = tex,
+ .width = width,
+ .height = height,
+ .format = opts.format,
+ .target = opts.target,
+ };
+}
+
+pub fn deinit(self: Self) void {
+ self.texture.destroy();
+}
+
+/// Replace a region of the texture with the provided data.
+///
+/// Does NOT check the dimensions of the data to ensure correctness.
+pub fn replaceRegion(
+ self: Self,
+ x: usize,
+ y: usize,
+ width: usize,
+ height: usize,
+ data: []const u8,
+) Error!void {
+ const texbind = self.texture.bind(self.target) catch return error.OpenGLFailed;
+ defer texbind.unbind();
+ texbind.subImage2D(
+ 0,
+ @intCast(x),
+ @intCast(y),
+ @intCast(width),
+ @intCast(height),
+ self.format,
+ .UnsignedByte,
+ data.ptr,
+ ) catch return error.OpenGLFailed;
+}
diff --git a/src/renderer/opengl/buffer.zig b/src/renderer/opengl/buffer.zig
new file mode 100644
index 000000000..48b6f410e
--- /dev/null
+++ b/src/renderer/opengl/buffer.zig
@@ -0,0 +1,127 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const gl = @import("opengl");
+
+const OpenGL = @import("../OpenGL.zig");
+
+const log = std.log.scoped(.opengl);
+
+/// Options for initializing a buffer.
+pub const Options = struct {
+ target: gl.Buffer.Target = .array,
+ usage: gl.Buffer.Usage = .dynamic_draw,
+};
+
+/// OpenGL data storage for a certain set of equal types. This is usually
+/// used for vertex buffers, etc. This helpful wrapper makes it easy to
+/// prealloc, shrink, grow, sync, buffers with OpenGL.
+pub fn Buffer(comptime T: type) type {
+ return struct {
+ const Self = @This();
+
+ /// Underlying `gl.Buffer` instance.
+ buffer: gl.Buffer,
+
+ /// Options this buffer was allocated with.
+ opts: Options,
+
+ /// Current allocated length of the data store.
+ /// Note this is the number of `T`s, not the size in bytes.
+ len: usize,
+
+ /// Initialize a buffer with the given length pre-allocated.
+ pub fn init(opts: Options, len: usize) !Self {
+ const buffer = try gl.Buffer.create();
+ errdefer buffer.destroy();
+
+ const binding = try buffer.bind(opts.target);
+ defer binding.unbind();
+
+ try binding.setDataNullManual(len * @sizeOf(T), opts.usage);
+
+ return .{
+ .buffer = buffer,
+ .opts = opts,
+ .len = len,
+ };
+ }
+
+ /// Init the buffer filled with the given data.
+ pub fn initFill(opts: Options, data: []const T) !Self {
+ const buffer = try gl.Buffer.create();
+ errdefer buffer.destroy();
+
+ const binding = try buffer.bind(opts.target);
+ defer binding.unbind();
+
+ try binding.setData(data, opts.usage);
+
+ return .{
+ .buffer = buffer,
+ .opts = opts,
+ .len = data.len * @sizeOf(T),
+ };
+ }
+
+ pub fn deinit(self: Self) void {
+ self.buffer.destroy();
+ }
+
+ /// Sync new contents to the buffer. The data is expected to be the
+ /// complete contents of the buffer. If the amount of data is larger
+ /// than the buffer length, the buffer will be reallocated.
+ ///
+ /// If the amount of data is smaller than the buffer length, the
+ /// remaining data in the buffer is left untouched.
+ pub fn sync(self: *Self, data: []const T) !void {
+ const binding = try self.buffer.bind(self.opts.target);
+ defer binding.unbind();
+
+ // If we need more space than our buffer has, we need to reallocate.
+ if (data.len > self.len) {
+ // Reallocate the buffer to hold double what we require.
+ self.len = data.len * 2;
+ try binding.setDataNullManual(
+ self.len * @sizeOf(T),
+ self.opts.usage,
+ );
+ }
+
+ // We can fit within the buffer so we can just replace bytes.
+ try binding.setSubData(0, data);
+ }
+
+ /// Like Buffer.sync but takes data from an array of ArrayLists,
+ /// rather than a single array. Returns the number of items synced.
+ pub fn syncFromArrayLists(self: *Self, lists: []const std.ArrayListUnmanaged(T)) !usize {
+ const binding = try self.buffer.bind(self.opts.target);
+ defer binding.unbind();
+
+ var total_len: usize = 0;
+ for (lists) |list| {
+ total_len += list.items.len;
+ }
+
+ // If we need more space than our buffer has, we need to reallocate.
+ if (total_len > self.len) {
+ // Reallocate the buffer to hold double what we require.
+ self.len = total_len * 2;
+ try binding.setDataNullManual(
+ self.len * @sizeOf(T),
+ self.opts.usage,
+ );
+ }
+
+ // We can fit within the buffer so we can just replace bytes.
+ var i: usize = 0;
+
+ for (lists) |list| {
+ try binding.setSubData(i, list.items);
+ i += list.items.len * @sizeOf(T);
+ }
+
+ return total_len;
+ }
+ };
+}
diff --git a/src/renderer/opengl/cell.zig b/src/renderer/opengl/cell.zig
new file mode 100644
index 000000000..abdbaa0e8
--- /dev/null
+++ b/src/renderer/opengl/cell.zig
@@ -0,0 +1,220 @@
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+
+const renderer = @import("../../renderer.zig");
+const terminal = @import("../../terminal/main.zig");
+const shaderpkg = @import("shaders.zig");
+
+/// The possible cell content keys that exist.
+pub const Key = enum {
+ bg,
+ text,
+ underline,
+ strikethrough,
+ overline,
+
+ /// Returns the GPU vertex type for this key.
+ pub fn CellType(self: Key) type {
+ return switch (self) {
+ .bg => shaderpkg.CellBg,
+
+ .text,
+ .underline,
+ .strikethrough,
+ .overline,
+ => shaderpkg.CellText,
+ };
+ }
+};
+
+/// A pool of ArrayLists with methods for bulk operations.
+fn ArrayListPool(comptime T: type) type {
+ return struct {
+ const Self = ArrayListPool(T);
+ const ArrayListT = std.ArrayListUnmanaged(T);
+
+ // An array containing the lists that belong to this pool.
+ lists: []ArrayListT = &[_]ArrayListT{},
+
+ // The pool will be initialized with empty ArrayLists.
+ pub fn init(alloc: Allocator, list_count: usize, initial_capacity: usize) !Self {
+ const self: Self = .{
+ .lists = try alloc.alloc(ArrayListT, list_count),
+ };
+
+ for (self.lists) |*list| {
+ list.* = try ArrayListT.initCapacity(alloc, initial_capacity);
+ }
+
+ return self;
+ }
+
+ pub fn deinit(self: *Self, alloc: Allocator) void {
+ for (self.lists) |*list| {
+ list.deinit(alloc);
+ }
+ alloc.free(self.lists);
+ }
+
+ /// Clear all lists in the pool.
+ pub fn reset(self: *Self) void {
+ for (self.lists) |*list| {
+ list.clearRetainingCapacity();
+ }
+ }
+ };
+}
+
+/// The contents of all the cells in the terminal.
+///
+/// The goal of this data structure is to allow for efficient row-wise
+/// clearing of data from the GPU buffers, to allow for row-wise dirty
+/// tracking to eliminate the overhead of rebuilding the GPU buffers
+/// each frame.
+///
+/// Must be initialized by resizing before calling any operations.
+pub const Contents = struct {
+ size: renderer.GridSize = .{ .rows = 0, .columns = 0 },
+
+ /// Flat array containing cell background colors for the terminal grid.
+ ///
+ /// Indexed as `bg_cells[row * size.columns + col]`.
+ ///
+ /// Prefer accessing with `Contents.bgCell(row, col).*` instead
+ /// of directly indexing in order to avoid integer size bugs.
+ bg_cells: []shaderpkg.CellBg = undefined,
+
+ /// The ArrayListPool which holds all of the foreground cells. When sized
+ /// with Contents.resize the individual ArrayLists are given enough room
+ /// that they can hold a single row with #cols glyphs, underlines, and
+ /// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
+ /// it is possible to exceed this with combining glyphs that add a glyph
+ /// but take up no column since they combine with the previous one, as
+ /// well as with fonts that perform multi-substitutions for glyphs, which
+ /// can result in a similar situation where multiple glyphs reside in the
+ /// same column.
+ ///
+ /// Allocations should nevertheless be exceedingly rare since hitting the
+ /// initial capacity of a list would require a row filled with underlined
+ /// struck through characters, at least one of which is a multi-glyph
+ /// composite.
+ ///
+ /// Rows are indexed as Contents.fg_rows[y + 1], because the first list in
+ /// the pool is reserved for the cursor, which must be the first item in
+ /// the buffer.
+ ///
+ /// Must be initialized by calling resize on the Contents struct before
+ /// calling any operations.
+ fg_rows: ArrayListPool(shaderpkg.CellText) = .{},
+
+ pub fn deinit(self: *Contents, alloc: Allocator) void {
+ alloc.free(self.bg_cells);
+ self.fg_rows.deinit(alloc);
+ }
+
+ /// Resize the cell contents for the given grid size. This will
+ /// always invalidate the entire cell contents.
+ pub fn resize(
+ self: *Contents,
+ alloc: Allocator,
+ size: renderer.GridSize,
+ ) !void {
+ self.size = size;
+
+ const cell_count = @as(usize, size.columns) * @as(usize, size.rows);
+
+ const bg_cells = try alloc.alloc(shaderpkg.CellBg, cell_count);
+ errdefer alloc.free(bg_cells);
+
+ @memset(bg_cells, .{ 0, 0, 0, 0 });
+
+ // The foreground lists can hold 3 types of items:
+ // - Glyphs
+ // - Underlines
+ // - Strikethroughs
+ // So we give them an initial capacity of size.columns * 3, which will
+ // avoid any further allocations in the vast majority of cases. Sadly
+ // we can not assume capacity though, since with combining glyphs that
+ // form a single grapheme, and multi-substitutions in fonts, the number
+ // of glyphs in a row is theoretically unlimited.
+ //
+ // We have size.rows + 1 lists because index 0 is used for a special
+ // list containing the cursor cell which needs to be first in the buffer.
+ var fg_rows = try ArrayListPool(shaderpkg.CellText).init(alloc, size.rows + 1, size.columns * 3);
+ errdefer fg_rows.deinit(alloc);
+
+ alloc.free(self.bg_cells);
+ self.fg_rows.deinit(alloc);
+
+ self.bg_cells = bg_cells;
+ self.fg_rows = fg_rows;
+
+ // We don't need 3*cols worth of cells for the cursor list, so we can
+ // replace it with a smaller list. This is technically a tiny bit of
+ // extra work but resize is not a hot function so it's worth it to not
+ // waste the memory.
+ self.fg_rows.lists[0].deinit(alloc);
+ self.fg_rows.lists[0] = try std.ArrayListUnmanaged(shaderpkg.CellText).initCapacity(alloc, 1);
+ }
+
+ /// Reset the cell contents to an empty state without resizing.
+ pub fn reset(self: *Contents) void {
+ @memset(self.bg_cells, .{ 0, 0, 0, 0 });
+ self.fg_rows.reset();
+ }
+
+ /// Set the cursor value. If the value is null then the cursor is hidden.
+ pub fn setCursor(self: *Contents, v: ?shaderpkg.CellText) void {
+ self.fg_rows.lists[0].clearRetainingCapacity();
+
+ if (v) |cell| {
+ self.fg_rows.lists[0].appendAssumeCapacity(cell);
+ }
+ }
+
+ /// Access a background cell. Prefer this function over direct indexing
+ /// of `bg_cells` in order to avoid integer size bugs causing overflows.
+ pub inline fn bgCell(self: *Contents, row: usize, col: usize) *shaderpkg.CellBg {
+ return &self.bg_cells[row * self.size.columns + col];
+ }
+
+ /// Add a cell to the appropriate list. Adding the same cell twice will
+ /// result in duplication in the vertex buffer. The caller should clear
+ /// the corresponding row with Contents.clear to remove old cells first.
+ pub fn add(
+ self: *Contents,
+ alloc: Allocator,
+ comptime key: Key,
+ cell: key.CellType(),
+ ) !void {
+ const y = cell.grid_pos[1];
+
+ assert(y < self.size.rows);
+
+ switch (key) {
+ .bg => comptime unreachable,
+
+ .text,
+ .underline,
+ .strikethrough,
+ .overline,
+ // We have a special list containing the cursor cell at the start
+ // of our fg row pool, so we need to add 1 to the y to get the
+ // correct index.
+ => try self.fg_rows.lists[y + 1].append(alloc, cell),
+ }
+ }
+
+ /// Clear all of the cell contents for a given row.
+ pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
+ assert(y < self.size.rows);
+
+ @memset(self.bg_cells[@as(usize, y) * self.size.columns ..][0..self.size.columns], .{ 0, 0, 0, 0 });
+
+ // We have a special list containing the cursor cell at the start
+ // of our fg row pool, so we need to add 1 to the y to get the
+ // correct index.
+ self.fg_rows.lists[y + 1].clearRetainingCapacity();
+ }
+};
diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig
deleted file mode 100644
index 859277ce5..000000000
--- a/src/renderer/opengl/custom.zig
+++ /dev/null
@@ -1,310 +0,0 @@
-const std = @import("std");
-const Allocator = std.mem.Allocator;
-const gl = @import("opengl");
-const Size = @import("../size.zig").Size;
-
-const log = std.log.scoped(.opengl_custom);
-
-/// The "INDEX" is the index into the global GL state and the
-/// "BINDING" is the binding location in the shader.
-const UNIFORM_INDEX: gl.c.GLuint = 0;
-const UNIFORM_BINDING: gl.c.GLuint = 0;
-
-/// Global uniforms for custom shaders.
-pub const Uniforms = extern struct {
- resolution: [3]f32 align(16) = .{ 0, 0, 0 },
- time: f32 align(4) = 1,
- time_delta: f32 align(4) = 1,
- frame_rate: f32 align(4) = 1,
- frame: i32 align(4) = 1,
- channel_time: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
- channel_resolution: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4,
- mouse: [4]f32 align(16) = .{ 0, 0, 0, 0 },
- date: [4]f32 align(16) = .{ 0, 0, 0, 0 },
- sample_rate: f32 align(4) = 1,
-};
-
-/// The state associated with custom shaders. This should only be initialized
-/// if there is at least one custom shader.
-///
-/// To use this, the main terminal shader should render to the framebuffer
-/// specified by "fbo". The resulting "fb_texture" will contain the color
-/// attachment. This is then used as the iChannel0 input to the custom
-/// shader.
-pub const State = struct {
- /// The uniform data
- uniforms: Uniforms,
-
- /// The OpenGL buffers
- fbo: gl.Framebuffer,
- ubo: gl.Buffer,
- vao: gl.VertexArray,
- ebo: gl.Buffer,
- fb_texture: gl.Texture,
-
- /// The set of programs for the custom shaders.
- programs: []const Program,
-
- /// The first time a frame was drawn. This is used to update
- /// the time uniform.
- first_frame_time: std.time.Instant,
-
- /// The last time a frame was drawn. This is used to update
- /// the time uniform.
- last_frame_time: std.time.Instant,
-
- pub fn init(
- alloc: Allocator,
- srcs: []const [:0]const u8,
- ) !State {
- if (srcs.len == 0) return error.OneCustomShaderRequired;
-
- // Create our programs
- var programs = std.ArrayList(Program).init(alloc);
- defer programs.deinit();
- errdefer for (programs.items) |p| p.deinit();
- for (srcs) |src| {
- try programs.append(try Program.init(src));
- }
-
- // Create the texture for the framebuffer
- const fb_tex = try gl.Texture.create();
- errdefer fb_tex.destroy();
- {
- const texbind = try fb_tex.bind(.@"2D");
- try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
- try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
- try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
- try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
- try texbind.image2D(
- 0,
- .rgb,
- 1,
- 1,
- 0,
- .rgb,
- .UnsignedByte,
- null,
- );
- }
-
- // Create our framebuffer for rendering off screen.
- // The shader prior to custom shaders should use this
- // framebuffer.
- const fbo = try gl.Framebuffer.create();
- errdefer fbo.destroy();
- const fbbind = try fbo.bind(.framebuffer);
- defer fbbind.unbind();
- try fbbind.texture2D(.color0, .@"2D", fb_tex, 0);
- const fbstatus = fbbind.checkStatus();
- if (fbstatus != .complete) {
- log.warn(
- "framebuffer is not complete state={}",
- .{fbstatus},
- );
- return error.InvalidFramebuffer;
- }
-
- // Create our uniform buffer that is shared across all
- // custom shaders
- const ubo = try gl.Buffer.create();
- errdefer ubo.destroy();
- {
- var ubobind = try ubo.bind(.uniform);
- defer ubobind.unbind();
- try ubobind.setDataNull(Uniforms, .static_draw);
- }
-
- // Setup our VAO for the custom shader.
- const vao = try gl.VertexArray.create();
- errdefer vao.destroy();
- const vaobind = try vao.bind();
- defer vaobind.unbind();
-
- // Element buffer (EBO)
- const ebo = try gl.Buffer.create();
- errdefer ebo.destroy();
- var ebobind = try ebo.bind(.element_array);
- defer ebobind.unbind();
- try ebobind.setData([6]u8{
- 0, 1, 3, // Top-left triangle
- 1, 2, 3, // Bottom-right triangle
- }, .static_draw);
-
- return .{
- .programs = try programs.toOwnedSlice(),
- .uniforms = .{},
- .fbo = fbo,
- .ubo = ubo,
- .vao = vao,
- .ebo = ebo,
- .fb_texture = fb_tex,
- .first_frame_time = try std.time.Instant.now(),
- .last_frame_time = try std.time.Instant.now(),
- };
- }
-
- pub fn deinit(self: *const State, alloc: Allocator) void {
- for (self.programs) |p| p.deinit();
- alloc.free(self.programs);
- self.ubo.destroy();
- self.ebo.destroy();
- self.vao.destroy();
- self.fb_texture.destroy();
- self.fbo.destroy();
- }
-
- pub fn setScreenSize(self: *State, size: Size) !void {
- // Update our uniforms
- self.uniforms.resolution = .{
- @floatFromInt(size.screen.width),
- @floatFromInt(size.screen.height),
- 1,
- };
- try self.syncUniforms();
-
- // Update our texture
- const texbind = try self.fb_texture.bind(.@"2D");
- try texbind.image2D(
- 0,
- .rgb,
- @intCast(size.screen.width),
- @intCast(size.screen.height),
- 0,
- .rgb,
- .UnsignedByte,
- null,
- );
- }
-
- /// Call this prior to drawing a frame to update the time
- /// and synchronize the uniforms. This synchronizes uniforms
- /// so you should make changes to uniforms prior to calling
- /// this.
- pub fn newFrame(self: *State) !void {
- // Update our frame time
- const now = std.time.Instant.now() catch self.first_frame_time;
- const since_ns: f32 = @floatFromInt(now.since(self.first_frame_time));
- const delta_ns: f32 = @floatFromInt(now.since(self.last_frame_time));
- self.uniforms.time = since_ns / std.time.ns_per_s;
- self.uniforms.time_delta = delta_ns / std.time.ns_per_s;
- self.last_frame_time = now;
-
- // Sync our uniform changes
- try self.syncUniforms();
- }
-
- fn syncUniforms(self: *State) !void {
- var ubobind = try self.ubo.bind(.uniform);
- defer ubobind.unbind();
- try ubobind.setData(self.uniforms, .static_draw);
- }
-
- /// Call this to bind all the necessary OpenGL resources for
- /// all custom shaders. Each individual shader needs to be bound
- /// one at a time too.
- pub fn bind(self: *const State) !Binding {
- // Move our uniform buffer into proper global index. Note that
- // in theory we can do this globally once and never worry about
- // it again. I don't think we're high-performance enough at all
- // to worry about that and this makes it so you can just move
- // around CustomProgram usage without worrying about clobbering
- // the global state.
- try self.ubo.bindBase(.uniform, UNIFORM_INDEX);
-
- // Bind our texture that is shared amongst all
- try gl.Texture.active(gl.c.GL_TEXTURE0);
- var texbind = try self.fb_texture.bind(.@"2D");
- errdefer texbind.unbind();
-
- const vao = try self.vao.bind();
- errdefer vao.unbind();
-
- const ebo = try self.ebo.bind(.element_array);
- errdefer ebo.unbind();
-
- return .{
- .vao = vao,
- .ebo = ebo,
- .fb_texture = texbind,
- };
- }
-
- /// Copy the fbo's attached texture to the backbuffer.
- pub fn copyFramebuffer(self: *State) !void {
- const texbind = try self.fb_texture.bind(.@"2D");
- errdefer texbind.unbind();
- try texbind.copySubImage2D(
- 0,
- 0,
- 0,
- 0,
- 0,
- @intFromFloat(self.uniforms.resolution[0]),
- @intFromFloat(self.uniforms.resolution[1]),
- );
- }
-
- pub const Binding = struct {
- vao: gl.VertexArray.Binding,
- ebo: gl.Buffer.Binding,
- fb_texture: gl.Texture.Binding,
-
- pub fn unbind(self: Binding) void {
- self.ebo.unbind();
- self.vao.unbind();
- self.fb_texture.unbind();
- }
- };
-};
-
-/// A single OpenGL program (combined shaders) for custom shaders.
-pub const Program = struct {
- program: gl.Program,
-
- pub fn init(src: [:0]const u8) !Program {
- const program = try gl.Program.createVF(
- @embedFile("../shaders/custom.v.glsl"),
- src,
- );
- errdefer program.destroy();
-
- // Map our uniform buffer to the global GL state
- try program.uniformBlockBinding(UNIFORM_INDEX, UNIFORM_BINDING);
-
- return .{ .program = program };
- }
-
- pub fn deinit(self: *const Program) void {
- self.program.destroy();
- }
-
- /// Bind the program for use. This should be called so that draw can
- /// be called.
- pub fn bind(self: *const Program) !Binding {
- const program = try self.program.use();
- errdefer program.unbind();
-
- return .{
- .program = program,
- };
- }
-
- pub const Binding = struct {
- program: gl.Program.Binding,
-
- pub fn unbind(self: Binding) void {
- self.program.unbind();
- }
-
- pub fn draw(self: Binding) !void {
- _ = self;
- try gl.drawElementsInstanced(
- gl.c.GL_TRIANGLES,
- 6,
- gl.c.GL_UNSIGNED_BYTE,
- 1,
- );
- }
- };
-};
diff --git a/src/renderer/opengl/image.zig b/src/renderer/opengl/image.zig
index 26cd90736..77779fb8a 100644
--- a/src/renderer/opengl/image.zig
+++ b/src/renderer/opengl/image.zig
@@ -3,6 +3,8 @@ const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const gl = @import("opengl");
const wuffs = @import("wuffs");
+const OpenGL = @import("../OpenGL.zig");
+const Texture = OpenGL.Texture;
/// Represents a single image placement on the grid. A placement is a
/// request to render an instance of an image.
@@ -59,15 +61,15 @@ pub const Image = union(enum) {
replace_rgba: Replace,
/// The image is uploaded and ready to be used.
- ready: gl.Texture,
+ ready: Texture,
/// The image is uploaded but is scheduled to be unloaded.
unload_pending: []u8,
- unload_ready: gl.Texture,
- unload_replace: struct { []u8, gl.Texture },
+ unload_ready: Texture,
+ unload_replace: struct { []u8, Texture },
pub const Replace = struct {
- texture: gl.Texture,
+ texture: Texture,
pending: Pending,
};
@@ -99,32 +101,32 @@ pub const Image = union(enum) {
.replace_gray => |r| {
alloc.free(r.pending.dataSlice(1));
- r.texture.destroy();
+ r.texture.deinit();
},
.replace_gray_alpha => |r| {
alloc.free(r.pending.dataSlice(2));
- r.texture.destroy();
+ r.texture.deinit();
},
.replace_rgb => |r| {
alloc.free(r.pending.dataSlice(3));
- r.texture.destroy();
+ r.texture.deinit();
},
.replace_rgba => |r| {
alloc.free(r.pending.dataSlice(4));
- r.texture.destroy();
+ r.texture.deinit();
},
.unload_replace => |r| {
alloc.free(r[0]);
- r[1].destroy();
+ r[1].deinit();
},
.ready,
.unload_ready,
- => |tex| tex.destroy(),
+ => |tex| tex.deinit(),
}
}
@@ -168,7 +170,7 @@ pub const Image = union(enum) {
// Get our existing texture. This switch statement will also handle
// scenarios where there is no existing texture and we can modify
// the self pointer directly.
- const existing: gl.Texture = switch (self.*) {
+ const existing: Texture = switch (self.*) {
// For pending, we can free the old data and become pending ourselves.
.pending_gray => |p| {
alloc.free(p.dataSlice(1));
@@ -356,7 +358,10 @@ pub const Image = union(enum) {
pub fn upload(
self: *Image,
alloc: Allocator,
+ opengl: *const OpenGL,
) !void {
+ _ = opengl;
+
// Convert our data if we have to
try self.convert(alloc);
@@ -374,23 +379,15 @@ pub const Image = union(enum) {
};
// Create our texture
- const tex = try gl.Texture.create();
- errdefer tex.destroy();
-
- const texbind = try tex.bind(.@"2D");
- try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE);
- try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE);
- try texbind.parameter(.MinFilter, gl.c.GL_LINEAR);
- try texbind.parameter(.MagFilter, gl.c.GL_LINEAR);
- try texbind.image2D(
- 0,
- formats.internal,
+ const tex = try Texture.init(
+ .{
+ .format = formats.format,
+ .internal_format = formats.internal,
+ .target = .Rectangle,
+ },
@intCast(p.width),
@intCast(p.height),
- 0,
- formats.format,
- .UnsignedByte,
- p.data,
+ p.data[0 .. p.width * p.height * self.depth()],
);
// Uploaded. We can now clear our data and change our state.
diff --git a/src/renderer/opengl/shaders.zig b/src/renderer/opengl/shaders.zig
new file mode 100644
index 000000000..7e54fd37b
--- /dev/null
+++ b/src/renderer/opengl/shaders.zig
@@ -0,0 +1,296 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const math = @import("../../math.zig");
+
+const Pipeline = @import("Pipeline.zig");
+
+const log = std.log.scoped(.opengl);
+
+/// This contains the state for the shaders used by the Metal renderer.
+pub const Shaders = struct {
+ /// Renders cell foreground elements (text, decorations).
+ cell_text_pipeline: Pipeline,
+
+ /// The cell background shader is the shader used to render the
+ /// background of terminal cells.
+ cell_bg_pipeline: Pipeline,
+
+ /// The image shader is the shader used to render images for things
+ /// like the Kitty image protocol.
+ image_pipeline: Pipeline,
+
+ /// Custom shaders to run against the final drawable texture. This
+ /// can be used to apply a lot of effects. Each shader is run in sequence
+ /// against the output of the previous shader.
+ post_pipelines: []const Pipeline,
+
+ /// Set to true when deinited, if you try to deinit a defunct set
+ /// of shaders it will just be ignored, to prevent double-free.
+ defunct: bool = false,
+
+ /// Initialize our shader set.
+ ///
+ /// "post_shaders" is an optional list of postprocess shaders to run
+ /// against the final drawable texture. This is an array of shader source
+ /// code, not file paths.
+ pub fn init(
+ alloc: Allocator,
+ post_shaders: []const [:0]const u8,
+ ) !Shaders {
+ const cell_text_pipeline = try initCellTextPipeline();
+ errdefer cell_text_pipeline.deinit();
+
+ const cell_bg_pipeline = try initCellBgPipeline();
+ errdefer cell_bg_pipeline.deinit();
+
+ const image_pipeline = try initImagePipeline();
+ errdefer image_pipeline.deinit();
+
+ const post_pipelines: []const Pipeline = initPostPipelines(
+ alloc,
+ post_shaders,
+ ) catch |err| err: {
+ // If an error happens while building postprocess shaders we
+ // want to just not use any postprocess shaders since we don't
+ // want to block Ghostty from working.
+ log.warn("error initializing postprocess shaders err={}", .{err});
+ break :err &.{};
+ };
+ errdefer if (post_pipelines.len > 0) {
+ for (post_pipelines) |pipeline| pipeline.deinit();
+ alloc.free(post_pipelines);
+ };
+
+ return .{
+ .cell_text_pipeline = cell_text_pipeline,
+ .cell_bg_pipeline = cell_bg_pipeline,
+ .image_pipeline = image_pipeline,
+ .post_pipelines = post_pipelines,
+ };
+ }
+
+ pub fn deinit(self: *Shaders, alloc: Allocator) void {
+ if (self.defunct) return;
+ self.defunct = true;
+
+ // Release our primary shaders
+ self.cell_text_pipeline.deinit();
+ self.cell_bg_pipeline.deinit();
+ self.image_pipeline.deinit();
+
+ // Release our postprocess shaders
+ if (self.post_pipelines.len > 0) {
+ for (self.post_pipelines) |pipeline| {
+ pipeline.deinit();
+ }
+ alloc.free(self.post_pipelines);
+ }
+ }
+};
+
+/// Single parameter for the image shader. See shader for field details.
+pub const Image = extern struct {
+ grid_pos: [2]f32 align(8),
+ cell_offset: [2]f32 align(8),
+ source_rect: [4]f32 align(16),
+ dest_size: [2]f32 align(8),
+};
+
+/// The uniforms that are passed to the terminal cell shader.
+pub const Uniforms = extern struct {
+ /// The projection matrix for turning world coordinates to normalized.
+ /// This is calculated based on the size of the screen.
+ projection_matrix: math.Mat align(16),
+
+ /// Size of a single cell in pixels, unscaled.
+ cell_size: [2]f32 align(8),
+
+ /// Size of the grid in columns and rows.
+ grid_size: [2]u16 align(4),
+
+ /// The padding around the terminal grid in pixels. In order:
+ /// top, right, bottom, left.
+ grid_padding: [4]f32 align(16),
+
+ /// Bit mask defining which directions to
+ /// extend cell colors in to the padding.
+ /// Order, LSB first: left, right, up, down
+ padding_extend: PaddingExtend align(4),
+
+ /// The minimum contrast ratio for text. The contrast ratio is calculated
+ /// according to the WCAG 2.0 spec.
+ min_contrast: f32 align(4),
+
+ /// The cursor position and color.
+ cursor_pos: [2]u16 align(4),
+ cursor_color: [4]u8 align(4),
+
+ /// The background color for the whole surface.
+ bg_color: [4]u8 align(4),
+
+ /// Various booleans, in a packed struct for space efficiency.
+ bools: Bools align(4),
+
+ const Bools = packed struct(u32) {
+ /// Whether the cursor is 2 cells wide.
+ cursor_wide: bool,
+
+ /// Indicates that colors provided to the shader are already in
+ /// the P3 color space, so they don't need to be converted from
+ /// sRGB.
+ use_display_p3: bool,
+
+ /// Indicates that the color attachments for the shaders have
+ /// an `*_srgb` pixel format, which means the shaders need to
+ /// output linear RGB colors rather than gamma encoded colors,
+ /// since blending will be performed in linear space and then
+ /// Metal itself will re-encode the colors for storage.
+ use_linear_blending: bool,
+
+ /// Enables a weight correction step that makes text rendered
+ /// with linear alpha blending have a similar apparent weight
+ /// (thickness) to gamma-incorrect blending.
+ use_linear_correction: bool = false,
+
+ _padding: u28 = 0,
+ };
+
+ const PaddingExtend = packed struct(u32) {
+ left: bool = false,
+ right: bool = false,
+ up: bool = false,
+ down: bool = false,
+ _padding: u28 = 0,
+ };
+};
+
+/// Initialize our custom shader pipelines. The shaders argument is a
+/// set of shader source code, not file paths.
+fn initPostPipelines(
+ alloc: Allocator,
+ shaders: []const [:0]const u8,
+) ![]const Pipeline {
+ // If we have no shaders, do nothing.
+ if (shaders.len == 0) return &.{};
+
+ // Keeps track of how many shaders we successfully wrote.
+ var i: usize = 0;
+
+ // Initialize our result set. If any error happens, we undo everything.
+ var pipelines = try alloc.alloc(Pipeline, shaders.len);
+ errdefer {
+ for (pipelines[0..i]) |pipeline| {
+ pipeline.deinit();
+ }
+ alloc.free(pipelines);
+ }
+
+ // Build each shader. Note we don't use "0.." to build our index
+ // because we need to keep track of our length to clean up above.
+ for (shaders) |source| {
+ pipelines[i] = try initPostPipeline(source);
+ i += 1;
+ }
+
+ return pipelines;
+}
+
+/// Initialize a single custom shader pipeline from shader source.
+fn initPostPipeline(data: [:0]const u8) !Pipeline {
+ return try Pipeline.init(null, .{
+ .vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
+ .fragment_fn = data,
+ });
+}
+
+/// This is a single parameter for the terminal cell shader.
+pub const CellText = extern struct {
+ glyph_pos: [2]u32 align(8) = .{ 0, 0 },
+ glyph_size: [2]u32 align(8) = .{ 0, 0 },
+ bearings: [2]i16 align(4) = .{ 0, 0 },
+ grid_pos: [2]u16 align(4),
+ color: [4]u8 align(4),
+ mode: Mode align(4),
+ constraint_width: u32 align(4) = 0,
+
+ pub const Mode = enum(u32) {
+ fg = 1,
+ fg_constrained = 2,
+ fg_color = 3,
+ cursor = 4,
+ fg_powerline = 5,
+ };
+
+ // test {
+ // // Minimizing the size of this struct is important,
+ // // so we test it in order to be aware of any changes.
+ // try std.testing.expectEqual(32, @sizeOf(CellText));
+ // }
+};
+
+/// Initialize the cell render pipeline.
+fn initCellTextPipeline() !Pipeline {
+ return try Pipeline.init(CellText, .{
+ .vertex_fn = loadShaderCode("../shaders/glsl/cell_text.v.glsl"),
+ .fragment_fn = loadShaderCode("../shaders/glsl/cell_text.f.glsl"),
+ .step_fn = .per_instance,
+ });
+}
+
+/// This is a single parameter for the cell bg shader.
+pub const CellBg = [4]u8;
+
+/// Initialize the cell background render pipeline.
+fn initCellBgPipeline() !Pipeline {
+ return try Pipeline.init(null, .{
+ .vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"),
+ .fragment_fn = loadShaderCode("../shaders/glsl/cell_bg.f.glsl"),
+ });
+}
+
+/// Initialize the image render pipeline.
+fn initImagePipeline() !Pipeline {
+ return try Pipeline.init(Image, .{
+ .vertex_fn = loadShaderCode("../shaders/glsl/image.v.glsl"),
+ .fragment_fn = loadShaderCode("../shaders/glsl/image.f.glsl"),
+ .step_fn = .per_instance,
+ });
+}
+
+/// Load shader code from the target path, processing `#include` directives.
+///
+/// Comptime only for now, this code is really sloppy and makes a bunch of
+/// assumptions about things being well formed and file names not containing
+/// quote marks. If we ever want to process `#include`s for custom shaders
+/// then we need to write something better than this for it.
+fn loadShaderCode(comptime path: []const u8) [:0]const u8 {
+ return comptime processIncludes(@embedFile(path), std.fs.path.dirname(path).?);
+}
+
+/// Used by loadShaderCode
+fn processIncludes(contents: [:0]const u8, basedir: []const u8) [:0]const u8 {
+ @setEvalBranchQuota(100_000);
+ var i: usize = 0;
+ while (i < contents.len) {
+ if (std.mem.startsWith(u8, contents[i..], "#include")) {
+ assert(std.mem.startsWith(u8, contents[i..], "#include \""));
+ const start = i + "#include \"".len;
+ const end = std.mem.indexOfScalarPos(u8, contents, start, '"').?;
+ return std.fmt.comptimePrint(
+ "{s}{s}{s}",
+ .{
+ contents[0..i],
+ @embedFile(basedir ++ "/" ++ contents[start..end]),
+ processIncludes(contents[end + 1 ..], basedir),
+ },
+ );
+ }
+ if (std.mem.indexOfPos(u8, contents, i, "\n#")) |j| {
+ i = (j + 1);
+ } else {
+ break;
+ }
+ }
+ return contents;
+}
diff --git a/src/renderer/shaders/cell.f.glsl b/src/renderer/shaders/cell.f.glsl
deleted file mode 100644
index f9c1ce2b1..000000000
--- a/src/renderer/shaders/cell.f.glsl
+++ /dev/null
@@ -1,53 +0,0 @@
-#version 330 core
-
-in vec2 glyph_tex_coords;
-flat in uint mode;
-
-// The color for this cell. If this is a background pass this is the
-// background color. Otherwise, this is the foreground color.
-flat in vec4 color;
-
-// The position of the cells top-left corner.
-flat in vec2 screen_cell_pos;
-
-// Position the fragment coordinate to the upper left
-layout(origin_upper_left) in vec4 gl_FragCoord;
-
-// Must declare this output for some versions of OpenGL.
-layout(location = 0) out vec4 out_FragColor;
-
-// Font texture
-uniform sampler2D text;
-uniform sampler2D text_color;
-
-// Dimensions of the cell
-uniform vec2 cell_size;
-
-// See vertex shader
-const uint MODE_BG = 1u;
-const uint MODE_FG = 2u;
-const uint MODE_FG_CONSTRAINED = 3u;
-const uint MODE_FG_COLOR = 7u;
-const uint MODE_FG_POWERLINE = 15u;
-
-void main() {
- float a;
-
- switch (mode) {
- case MODE_BG:
- out_FragColor = color;
- break;
-
- case MODE_FG:
- case MODE_FG_CONSTRAINED:
- case MODE_FG_POWERLINE:
- a = texture(text, glyph_tex_coords).r;
- vec3 premult = color.rgb * color.a;
- out_FragColor = vec4(premult.rgb*a, a);
- break;
-
- case MODE_FG_COLOR:
- out_FragColor = texture(text_color, glyph_tex_coords);
- break;
- }
-}
diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal
index 5b3875221..039c600ed 100644
--- a/src/renderer/shaders/cell.metal
+++ b/src/renderer/shaders/cell.metal
@@ -249,20 +249,12 @@ vertex CellBgVertexOut cell_bg_vertex(
fragment float4 cell_bg_fragment(
CellBgVertexOut in [[stage_in]],
- constant uchar4 *cells [[buffer(0)]],
- constant Uniforms& uniforms [[buffer(1)]]
+ constant Uniforms& uniforms [[buffer(1)]],
+ constant uchar4 *cells [[buffer(2)]]
) {
int2 grid_pos = int2(floor((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size));
- float4 bg = float4(0.0);
- // If we have any background transparency then we render bg-colored cells as
- // fully transparent, since the background is handled by the layer bg color
- // and we don't want to double up our bg color, but if our bg color is fully
- // opaque then our layer is opaque and can't handle transparency, so we need
- // to return the bg color directly instead.
- if (uniforms.bg_color.a == 255) {
- bg = in.bg_color;
- }
+ float4 bg = in.bg_color;
// Clamp x position, extends edge bg colors in to padding on sides.
if (grid_pos.x < 0) {
@@ -374,19 +366,23 @@ vertex CellTextVertexOut cell_text_vertex(
// Convert the grid x, y into world space x, y by accounting for cell size
float2 cell_pos = uniforms.cell_size * float2(in.grid_pos);
- // Turn the cell position into a vertex point depending on the
- // vertex ID. Since we use instanced drawing, we have 4 vertices
- // for each corner of the cell. We can use vertex ID to determine
- // which one we're looking at. Using this, we can use 1 or 0 to keep
- // or discard the value for the vertex.
+ // We use a triangle strip with 4 vertices to render quads,
+ // so we determine which corner of the cell this vertex is in
+ // based on the vertex ID.
//
- // 0 = top-right
- // 1 = bot-right
- // 2 = bot-left
- // 3 = top-left
+ // 0 --> 1
+ // | .'|
+ // | / |
+ // | L |
+ // 2 --> 3
+ //
+ // 0 = top-left (0, 0)
+ // 1 = top-right (1, 0)
+ // 2 = bot-left (0, 1)
+ // 3 = bot-right (1, 1)
float2 corner;
- corner.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
- corner.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
+ corner.x = float(vid == 1 || vid == 3);
+ corner.y = float(vid == 2 || vid == 3);
CellTextVertexOut out;
out.mode = in.mode;
@@ -502,7 +498,7 @@ fragment float4 cell_text_fragment(
CellTextVertexOut in [[stage_in]],
texture2d textureGrayscale [[texture(0)]],
texture2d textureColor [[texture(1)]],
- constant Uniforms& uniforms [[buffer(2)]]
+ constant Uniforms& uniforms [[buffer(1)]]
) {
constexpr sampler textureSampler(
coord::pixel,
@@ -621,19 +617,23 @@ vertex ImageVertexOut image_vertex(
texture2d image [[texture(0)]],
constant Uniforms& uniforms [[buffer(1)]]
) {
- // Turn the image position into a vertex point depending on the
- // vertex ID. Since we use instanced drawing, we have 4 vertices
- // for each corner of the cell. We can use vertex ID to determine
- // which one we're looking at. Using this, we can use 1 or 0 to keep
- // or discard the value for the vertex.
+ // We use a triangle strip with 4 vertices to render quads,
+ // so we determine which corner of the cell this vertex is in
+ // based on the vertex ID.
//
- // 0 = top-right
- // 1 = bot-right
- // 2 = bot-left
- // 3 = top-left
+ // 0 --> 1
+ // | .'|
+ // | / |
+ // | L |
+ // 2 --> 3
+ //
+ // 0 = top-left (0, 0)
+ // 1 = top-right (1, 0)
+ // 2 = bot-left (0, 1)
+ // 3 = bot-right (1, 1)
float2 corner;
- corner.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
- corner.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
+ corner.x = float(vid == 1 || vid == 3);
+ corner.y = float(vid == 2 || vid == 3);
// The texture coordinates start at our source x/y
// and add the width/height depending on the corner.
diff --git a/src/renderer/shaders/cell.v.glsl b/src/renderer/shaders/cell.v.glsl
deleted file mode 100644
index f37e69adc..000000000
--- a/src/renderer/shaders/cell.v.glsl
+++ /dev/null
@@ -1,258 +0,0 @@
-#version 330 core
-
-// These are the possible modes that "mode" can be set to. This is
-// used to multiplex multiple render modes into a single shader.
-//
-// NOTE: this must be kept in sync with the fragment shader
-const uint MODE_BG = 1u;
-const uint MODE_FG = 2u;
-const uint MODE_FG_CONSTRAINED = 3u;
-const uint MODE_FG_COLOR = 7u;
-const uint MODE_FG_POWERLINE = 15u;
-
-// The grid coordinates (x, y) where x < columns and y < rows
-layout (location = 0) in vec2 grid_coord;
-
-// Position of the glyph in the texture.
-layout (location = 1) in vec2 glyph_pos;
-
-// Width/height of the glyph
-layout (location = 2) in vec2 glyph_size;
-
-// Offset of the top-left corner of the glyph when rendered in a rect.
-layout (location = 3) in vec2 glyph_offset;
-
-// The color for this cell in RGBA (0 to 1.0). Background or foreground
-// depends on mode.
-layout (location = 4) in vec4 color_in;
-
-// Only set for MODE_FG, this is the background color of the FG text.
-// This is used to detect minimal contrast for the text.
-layout (location = 5) in vec4 bg_color_in;
-
-// The mode of this shader. The mode determines what fields are used,
-// what the output will be, etc. This shader is capable of executing in
-// multiple "modes" so that we can share some logic and so that we can draw
-// the entire terminal grid in a single GPU pass.
-layout (location = 6) in uint mode_in;
-
-// The width in cells of this item.
-layout (location = 7) in uint grid_width;
-
-// The background or foreground color for the fragment, depending on
-// whether this is a background or foreground pass.
-flat out vec4 color;
-
-// The x/y coordinate for the glyph representing the font.
-out vec2 glyph_tex_coords;
-
-// The position of the cell top-left corner in screen cords. z and w
-// are width and height.
-flat out vec2 screen_cell_pos;
-
-// Pass the mode forward to the fragment shader.
-flat out uint mode;
-
-uniform sampler2D text;
-uniform sampler2D text_color;
-uniform vec2 cell_size;
-uniform vec2 grid_size;
-uniform vec4 grid_padding;
-uniform bool padding_vertical_top;
-uniform bool padding_vertical_bottom;
-uniform mat4 projection;
-uniform float min_contrast;
-
-/********************************************************************
- * Modes
- *
- *-------------------------------------------------------------------
- * MODE_BG
- *
- * In MODE_BG, this shader renders only the background color for the
- * cell. This is a simple mode where we generate a simple rectangle
- * made up of 4 vertices and then it is filled. In this mode, the output
- * "color" is the fill color for the bg.
- *
- *-------------------------------------------------------------------
- * MODE_FG
- *
- * In MODE_FG, the shader renders the glyph onto this cell and utilizes
- * the glyph texture "text". In this mode, the output "color" is the
- * fg color to use for the glyph.
- *
- */
-
-//-------------------------------------------------------------------
-// Color Functions
-//-------------------------------------------------------------------
-
-// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
-float luminance_component(float c) {
- if (c <= 0.03928) {
- return c / 12.92;
- } else {
- return pow((c + 0.055) / 1.055, 2.4);
- }
-}
-
-float relative_luminance(vec3 color) {
- vec3 color_adjusted = vec3(
- luminance_component(color.r),
- luminance_component(color.g),
- luminance_component(color.b)
- );
-
- vec3 weights = vec3(0.2126, 0.7152, 0.0722);
- return dot(color_adjusted, weights);
-}
-
-// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
-float contrast_ratio(vec3 color1, vec3 color2) {
- float luminance1 = relative_luminance(color1) + 0.05;
- float luminance2 = relative_luminance(color2) + 0.05;
- return max(luminance1, luminance2) / min(luminance1, luminance2);
-}
-
-// Return the fg if the contrast ratio is greater than min, otherwise
-// return a color that satisfies the contrast ratio. Currently, the color
-// is always white or black, whichever has the highest contrast ratio.
-vec4 contrasted_color(float min_ratio, vec4 fg, vec4 bg) {
- vec3 fg_premult = fg.rgb * fg.a;
- vec3 bg_premult = bg.rgb * bg.a;
- float ratio = contrast_ratio(fg_premult, bg_premult);
- if (ratio < min_ratio) {
- float white_ratio = contrast_ratio(vec3(1.0, 1.0, 1.0), bg_premult);
- float black_ratio = contrast_ratio(vec3(0.0, 0.0, 0.0), bg_premult);
- if (white_ratio > black_ratio) {
- return vec4(1.0, 1.0, 1.0, fg.a);
- } else {
- return vec4(0.0, 0.0, 0.0, fg.a);
- }
- }
-
- return fg;
-}
-
-//-------------------------------------------------------------------
-// Main
-//-------------------------------------------------------------------
-
-void main() {
- // We always forward our mode unmasked because the fragment
- // shader doesn't use any of the masks.
- mode = mode_in;
-
- // Top-left cell coordinates converted to world space
- // Example: (1,0) with a 30 wide cell is converted to (30,0)
- vec2 cell_pos = cell_size * grid_coord;
-
- // Our Z value. For now we just use grid_z directly but we pull it
- // out here so the variable name is more uniform to our cell_pos and
- // in case we want to do any other math later.
- float cell_z = 0.0;
-
- // Turn the cell position into a vertex point depending on the
- // gl_VertexID. Since we use instanced drawing, we have 4 vertices
- // for each corner of the cell. We can use gl_VertexID to determine
- // which one we're looking at. Using this, we can use 1 or 0 to keep
- // or discard the value for the vertex.
- //
- // 0 = top-right
- // 1 = bot-right
- // 2 = bot-left
- // 3 = top-left
- vec2 position;
- position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.;
- position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.;
-
- // Scaled for wide chars
- vec2 cell_size_scaled = cell_size;
- cell_size_scaled.x = cell_size_scaled.x * grid_width;
-
- switch (mode) {
- case MODE_BG:
- // If we're at the edge of the grid, we add our padding to the background
- // to extend it. Note: grid_padding is top/right/bottom/left.
- if (grid_coord.y == 0 && padding_vertical_top) {
- cell_pos.y -= grid_padding.r;
- cell_size_scaled.y += grid_padding.r;
- } else if (grid_coord.y == grid_size.y - 1 && padding_vertical_bottom) {
- cell_size_scaled.y += grid_padding.b;
- }
- if (grid_coord.x == 0) {
- cell_pos.x -= grid_padding.a;
- cell_size_scaled.x += grid_padding.a;
- } else if (grid_coord.x == grid_size.x - 1) {
- cell_size_scaled.x += grid_padding.g;
- }
-
- // Calculate the final position of our cell in world space.
- // We have to add our cell size since our vertices are offset
- // one cell up and to the left. (Do the math to verify yourself)
- cell_pos = cell_pos + cell_size_scaled * position;
-
- gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
- color = color_in / 255.0;
- break;
-
- case MODE_FG:
- case MODE_FG_CONSTRAINED:
- case MODE_FG_COLOR:
- case MODE_FG_POWERLINE:
- vec2 glyph_offset_calc = glyph_offset;
-
- // The glyph_offset.y is the y bearing, a y value that when added
- // to the baseline is the offset (+y is up). Our grid goes down.
- // So we flip it with `cell_size.y - glyph_offset.y`.
- glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y;
-
- // If this is a constrained mode, we need to constrain it!
- vec2 glyph_size_calc = glyph_size;
- if (mode == MODE_FG_CONSTRAINED) {
- if (glyph_size.x > cell_size_scaled.x) {
- float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x);
- glyph_offset_calc.y = glyph_offset_calc.y + ((glyph_size.y - new_y) / 2);
- glyph_size_calc.y = new_y;
- glyph_size_calc.x = cell_size_scaled.x;
- }
- }
-
- // Calculate the final position of the cell.
- cell_pos = cell_pos + (glyph_size_calc * position) + glyph_offset_calc;
- gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
-
- // We need to convert our texture position and size to normalized
- // device coordinates (0 to 1.0) by dividing by the size of the texture.
- ivec2 text_size;
- switch(mode) {
- case MODE_FG_CONSTRAINED:
- case MODE_FG_POWERLINE:
- case MODE_FG:
- text_size = textureSize(text, 0);
- break;
-
- case MODE_FG_COLOR:
- text_size = textureSize(text_color, 0);
- break;
- }
- vec2 glyph_tex_pos = glyph_pos / text_size;
- vec2 glyph_tex_size = glyph_size / text_size;
- glyph_tex_coords = glyph_tex_pos + glyph_tex_size * position;
-
- // If we have a minimum contrast, we need to check if we need to
- // change the color of the text to ensure it has enough contrast
- // with the background.
- // We only apply this adjustment to "normal" text with MODE_FG,
- // since we want color glyphs to appear in their original color
- // and Powerline glyphs to be unaffected (else parts of the line would
- // have different colors as some parts are displayed via background colors).
- vec4 color_final = color_in / 255.0;
- if (min_contrast > 1.0 && mode == MODE_FG) {
- vec4 bg_color = bg_color_in / 255.0;
- color_final = contrasted_color(min_contrast, color_final, bg_color);
- }
- color = color_final;
- break;
- }
-}
diff --git a/src/renderer/shaders/custom.v.glsl b/src/renderer/shaders/custom.v.glsl
deleted file mode 100644
index 653e1800e..000000000
--- a/src/renderer/shaders/custom.v.glsl
+++ /dev/null
@@ -1,8 +0,0 @@
-#version 330 core
-
-void main(){
- vec2 position;
- position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? -1. : 1.;
- position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 1. : -1.;
- gl_Position = vec4(position.xy, 0.0f, 1.0f);
-}
diff --git a/src/renderer/shaders/glsl/cell_bg.f.glsl b/src/renderer/shaders/glsl/cell_bg.f.glsl
new file mode 100644
index 000000000..cfd598f95
--- /dev/null
+++ b/src/renderer/shaders/glsl/cell_bg.f.glsl
@@ -0,0 +1,61 @@
+#include "common.glsl"
+
+// Position the origin to the upper left
+layout(origin_upper_left, pixel_center_integer) in vec4 gl_FragCoord;
+
+// Must declare this output for some versions of OpenGL.
+layout(location = 0) out vec4 out_FragColor;
+
+layout(binding = 1, std430) readonly buffer bg_cells {
+ uint cells[];
+};
+
+vec4 cell_bg() {
+ uvec2 grid_size = unpack2u16(grid_size_packed_2u16);
+ ivec2 grid_pos = ivec2(floor((gl_FragCoord.xy - grid_padding.wx) / cell_size));
+ bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
+
+ vec4 bg = load_color(unpack4u8(bg_color_packed_4u8), use_linear_blending);
+
+ // Clamp x position, extends edge bg colors in to padding on sides.
+ if (grid_pos.x < 0) {
+ if ((padding_extend & EXTEND_LEFT) != 0) {
+ grid_pos.x = 0;
+ } else {
+ return bg;
+ }
+ } else if (grid_pos.x > grid_size.x - 1) {
+ if ((padding_extend & EXTEND_RIGHT) != 0) {
+ grid_pos.x = int(grid_size.x) - 1;
+ } else {
+ return bg;
+ }
+ }
+
+ // Clamp y position if we should extend, otherwise discard if out of bounds.
+ if (grid_pos.y < 0) {
+ if ((padding_extend & EXTEND_UP) != 0) {
+ grid_pos.y = 0;
+ } else {
+ return bg;
+ }
+ } else if (grid_pos.y > grid_size.y - 1) {
+ if ((padding_extend & EXTEND_DOWN) != 0) {
+ grid_pos.y = int(grid_size.y) - 1;
+ } else {
+ return bg;
+ }
+ }
+
+ // Load the color for the cell.
+ vec4 cell_color = load_color(
+ unpack4u8(cells[grid_pos.y * grid_size.x + grid_pos.x]),
+ use_linear_blending
+ );
+
+ return cell_color;
+}
+
+void main() {
+ out_FragColor = cell_bg();
+}
diff --git a/src/renderer/shaders/glsl/cell_text.f.glsl b/src/renderer/shaders/glsl/cell_text.f.glsl
new file mode 100644
index 000000000..fda552424
--- /dev/null
+++ b/src/renderer/shaders/glsl/cell_text.f.glsl
@@ -0,0 +1,109 @@
+#include "common.glsl"
+
+layout(binding = 0) uniform sampler2DRect atlas_grayscale;
+layout(binding = 1) uniform sampler2DRect atlas_color;
+
+in CellTextVertexOut {
+ flat uint mode;
+ flat vec4 color;
+ flat vec4 bg_color;
+ vec2 tex_coord;
+} in_data;
+
+// These are the possible modes that "mode" can be set to. This is
+// used to multiplex multiple render modes into a single shader.
+//
+// NOTE: this must be kept in sync with the fragment shader
+const uint MODE_TEXT = 1u;
+const uint MODE_TEXT_CONSTRAINED = 2u;
+const uint MODE_TEXT_COLOR = 3u;
+const uint MODE_TEXT_CURSOR = 4u;
+const uint MODE_TEXT_POWERLINE = 5u;
+
+// Must declare this output for some versions of OpenGL.
+layout(location = 0) out vec4 out_FragColor;
+
+void main() {
+ bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
+ bool use_linear_correction = (bools & USE_LINEAR_CORRECTION) != 0;
+
+ switch (in_data.mode) {
+ default:
+ case MODE_TEXT_CURSOR:
+ case MODE_TEXT_CONSTRAINED:
+ case MODE_TEXT_POWERLINE:
+ case MODE_TEXT:
+ {
+ // Our input color is always linear.
+ vec4 color = in_data.color;
+
+ // If we're not doing linear blending, then we need to
+ // re-apply the gamma encoding to our color manually.
+ //
+ // Since the alpha is premultiplied, we need to divide
+ // it out before unlinearizing and re-multiply it after.
+ if (!use_linear_blending) {
+ color.rgb /= vec3(color.a);
+ color = unlinearize(color);
+ color.rgb *= vec3(color.a);
+ }
+
+ // Fetch our alpha mask for this pixel.
+ float a = texture(atlas_grayscale, in_data.tex_coord).r;
+
+ // Linear blending weight correction corrects the alpha value to
+ // produce blending results which match gamma-incorrect blending.
+ if (use_linear_correction) {
+ // Short explanation of how this works:
+ //
+ // We get the luminances of the foreground and background colors,
+ // and then unlinearize them and perform blending on them. This
+ // gives us our desired luminance, which we derive our new alpha
+ // value from by mapping the range [bg_l, fg_l] to [0, 1], since
+ // our final blend will be a linear interpolation from bg to fg.
+ //
+ // This yields virtually identical results for grayscale blending,
+ // and very similar but non-identical results for color blending.
+ vec4 bg = in_data.bg_color;
+ float fg_l = luminance(color.rgb);
+ float bg_l = luminance(bg.rgb);
+ // To avoid numbers going haywire, we don't apply correction
+ // when the bg and fg luminances are within 0.001 of each other.
+ if (abs(fg_l - bg_l) > 0.001) {
+ float blend_l = linearize(unlinearize(fg_l) * a + unlinearize(bg_l) * (1.0 - a));
+ a = clamp((blend_l - bg_l) / (fg_l - bg_l), 0.0, 1.0);
+ }
+ }
+
+ // Multiply our whole color by the alpha mask.
+ // Since we use premultiplied alpha, this is
+ // the correct way to apply the mask.
+ color *= a;
+
+ out_FragColor = color;
+ return;
+ }
+
+ case MODE_TEXT_COLOR:
+ {
+ // For now, we assume that color glyphs
+ // are already premultiplied sRGB colors.
+ vec4 color = texture(atlas_color, in_data.tex_coord);
+
+ // If we aren't doing linear blending, we can return this right away.
+ if (!use_linear_blending) {
+ out_FragColor = color;
+ return;
+ }
+
+ // Otherwise we need to linearize the color. Since the alpha is
+ // premultiplied, we need to divide it out before linearizing.
+ color.rgb /= vec3(color.a);
+ color = linearize(color);
+ color.rgb *= vec3(color.a);
+
+ out_FragColor = color;
+ return;
+ }
+ }
+}
diff --git a/src/renderer/shaders/glsl/cell_text.v.glsl b/src/renderer/shaders/glsl/cell_text.v.glsl
new file mode 100644
index 000000000..76ede1082
--- /dev/null
+++ b/src/renderer/shaders/glsl/cell_text.v.glsl
@@ -0,0 +1,162 @@
+#include "common.glsl"
+
+// The position of the glyph in the texture (x, y)
+layout(location = 0) in uvec2 glyph_pos;
+
+// The size of the glyph in the texture (w, h)
+layout(location = 1) in uvec2 glyph_size;
+
+// The left and top bearings for the glyph (x, y)
+layout(location = 2) in ivec2 bearings;
+
+// The grid coordinates (x, y) where x < columns and y < rows
+layout(location = 3) in uvec2 grid_pos;
+
+// The color of the rendered text glyph.
+layout(location = 4) in uvec4 color;
+
+// The mode for this cell.
+layout(location = 5) in uint mode;
+
+// The width to constrain the glyph to, in cells, or 0 for no constraint.
+layout(location = 6) in uint constraint_width;
+
+// These are the possible modes that "mode" can be set to. This is
+// used to multiplex multiple render modes into a single shader.
+const uint MODE_TEXT = 1u;
+const uint MODE_TEXT_CONSTRAINED = 2u;
+const uint MODE_TEXT_COLOR = 3u;
+const uint MODE_TEXT_CURSOR = 4u;
+const uint MODE_TEXT_POWERLINE = 5u;
+
+out CellTextVertexOut {
+ flat uint mode;
+ flat vec4 color;
+ flat vec4 bg_color;
+ vec2 tex_coord;
+} out_data;
+
+layout(binding = 1, std430) readonly buffer bg_cells {
+ uint bg_colors[];
+};
+
+void main() {
+ uvec2 grid_size = unpack2u16(grid_size_packed_2u16);
+ uvec2 cursor_pos = unpack2u16(cursor_pos_packed_2u16);
+ bool cursor_wide = (bools & CURSOR_WIDE) != 0;
+ bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
+
+ // Convert the grid x, y into world space x, y by accounting for cell size
+ vec2 cell_pos = cell_size * vec2(grid_pos);
+
+ int vid = gl_VertexID;
+
+ // We use a triangle strip with 4 vertices to render quads,
+ // so we determine which corner of the cell this vertex is in
+ // based on the vertex ID.
+ //
+ // 0 --> 1
+ // | .'|
+ // | / |
+ // | L |
+ // 2 --> 3
+ //
+ // 0 = top-left (0, 0)
+ // 1 = top-right (1, 0)
+ // 2 = bot-left (0, 1)
+ // 3 = bot-right (1, 1)
+ vec2 corner;
+ corner.x = float(vid == 1 || vid == 3);
+ corner.y = float(vid == 2 || vid == 3);
+
+ out_data.mode = mode;
+
+ // === Grid Cell ===
+ // +X
+ // 0,0--...->
+ // |
+ // . offset.x = bearings.x
+ // +Y. .|.
+ // . | |
+ // | cell_pos -> +-------+ _.
+ // v ._| |_. _|- offset.y = cell_size.y - bearings.y
+ // | | .###. | |
+ // | | #...# | |
+ // glyph_size.y -+ | ##### | |
+ // | | #.... | +- bearings.y
+ // |_| .#### | |
+ // | |_|
+ // +-------+
+ // |_._|
+ // |
+ // glyph_size.x
+ //
+ // In order to get the top left of the glyph, we compute an offset based on
+ // the bearings. The Y bearing is the distance from the bottom of the cell
+ // to the top of the glyph, so we subtract it from the cell height to get
+ // the y offset. The X bearing is the distance from the left of the cell
+ // to the left of the glyph, so it works as the x offset directly.
+
+ vec2 size = vec2(glyph_size);
+ vec2 offset = vec2(bearings);
+
+ offset.y = cell_size.y - offset.y;
+
+ // If we're constrained then we need to scale the glyph.
+ if (mode == MODE_TEXT_CONSTRAINED) {
+ float max_width = cell_size.x * constraint_width;
+ // If this glyph is wider than the constraint width,
+ // fit it to the width and remove its horizontal offset.
+ if (size.x > max_width) {
+ float new_y = size.y * (max_width / size.x);
+ offset.y += (size.y - new_y) / 2.0;
+ offset.x = 0.0;
+ size.y = new_y;
+ size.x = max_width;
+ } else if (max_width - size.x > offset.x) {
+ // However, if it does fit in the constraint width, make
+ // sure the offset is small enough to not push it over the
+ // right edge of the constraint width.
+ offset.x = max_width - size.x;
+ }
+ }
+
+ // Calculate the final position of the cell which uses our glyph size
+ // and glyph offset to create the correct bounding box for the glyph.
+ cell_pos = cell_pos + size * corner + offset;
+ gl_Position = projection_matrix * vec4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
+
+ // Calculate the texture coordinate in pixels. This is NOT normalized
+ // (between 0.0 and 1.0), and does not need to be, since the texture will
+ // be sampled with pixel coordinate mode.
+ out_data.tex_coord = vec2(glyph_pos) + vec2(glyph_size) * corner;
+
+ // Get our color. We always fetch a linearized version to
+ // make it easier to handle minimum contrast calculations.
+ out_data.color = load_color(color, true);
+ // Get the BG color
+ out_data.bg_color = load_color(
+ unpack4u8(bg_colors[grid_pos.y * grid_size.x + grid_pos.x]),
+ true
+ );
+
+ // If we have a minimum contrast, we need to check if we need to
+ // change the color of the text to ensure it has enough contrast
+ // with the background.
+ // We only apply this adjustment to "normal" text with MODE_TEXT,
+ // since we want color glyphs to appear in their original color
+ // and Powerline glyphs to be unaffected (else parts of the line would
+ // have different colors as some parts are displayed via background colors).
+ if (min_contrast > 1.0f && mode == MODE_TEXT) {
+ // Ensure our minimum contrast
+ out_data.color = contrasted_color(min_contrast, out_data.color, out_data.bg_color);
+ }
+
+ // Check if current position is under cursor (including wide cursor)
+ bool is_cursor_pos = ((grid_pos.x == cursor_pos.x) || (cursor_wide && (grid_pos.x == (cursor_pos.x + 1)))) && (grid_pos.y == cursor_pos.y);
+
+ // If this cell is the cursor cell, then we need to change the color.
+ if (mode != MODE_TEXT_CURSOR && is_cursor_pos) {
+ out_data.color = load_color(unpack4u8(cursor_color_packed_4u8), use_linear_blending);
+ }
+}
diff --git a/src/renderer/shaders/glsl/common.glsl b/src/renderer/shaders/glsl/common.glsl
new file mode 100644
index 000000000..0450d0c06
--- /dev/null
+++ b/src/renderer/shaders/glsl/common.glsl
@@ -0,0 +1,155 @@
+#version 430 core
+
+// These are common definitions to be shared across shaders, the first
+// line of any shader that needs these should be `#include "common.glsl"`.
+//
+// Included in this file are:
+// - The interface block for the global uniforms.
+// - Functions for unpacking values.
+// - Functions for working with colors.
+
+//----------------------------------------------------------------------------//
+// Global Uniforms
+//----------------------------------------------------------------------------//
+layout(binding = 1, std140) uniform Globals {
+ uniform mat4 projection_matrix;
+ uniform vec2 cell_size;
+ uniform uint grid_size_packed_2u16;
+ uniform vec4 grid_padding;
+ uniform uint padding_extend;
+ uniform float min_contrast;
+ uniform uint cursor_pos_packed_2u16;
+ uniform uint cursor_color_packed_4u8;
+ uniform uint bg_color_packed_4u8;
+ uniform uint bools;
+};
+
+// Bools
+const uint CURSOR_WIDE = 1u;
+const uint USE_DISPLAY_P3 = 2u;
+const uint USE_LINEAR_BLENDING = 4u;
+const uint USE_LINEAR_CORRECTION = 8u;
+
+// Padding extend enum
+const uint EXTEND_LEFT = 1u;
+const uint EXTEND_RIGHT = 2u;
+const uint EXTEND_UP = 4u;
+const uint EXTEND_DOWN = 8u;
+
+//----------------------------------------------------------------------------//
+// Functions for Unpacking Values
+//----------------------------------------------------------------------------//
+// NOTE: These unpack functions assume little-endian.
+// If this ever becomes a problem... oh dear!
+
+uvec4 unpack4u8(uint packed_value) {
+ return uvec4(
+ uint(packed_value >> 0) & uint(0xFF),
+ uint(packed_value >> 8) & uint(0xFF),
+ uint(packed_value >> 16) & uint(0xFF),
+ uint(packed_value >> 24) & uint(0xFF)
+ );
+}
+
+uvec2 unpack2u16(uint packed_value) {
+ return uvec2(
+ uint(packed_value >> 0) & uint(0xFFFF),
+ uint(packed_value >> 16) & uint(0xFFFF)
+ );
+}
+
+ivec2 unpack2i16(int packed_value) {
+ return ivec2(
+ (packed_value << 16) >> 16,
+ (packed_value << 0) >> 16
+ );
+}
+
+//----------------------------------------------------------------------------//
+// Color Functions
+//----------------------------------------------------------------------------//
+
+// Compute the luminance of the provided color.
+//
+// Takes colors in linear RGB space. If your colors are gamma
+// encoded, linearize them before using them with this function.
+float luminance(vec3 color) {
+ return dot(color, vec3(0.2126f, 0.7152f, 0.0722f));
+}
+
+// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
+//
+// Takes colors in linear RGB space. If your colors are gamma
+// encoded, linearize them before using them with this function.
+float contrast_ratio(vec3 color1, vec3 color2) {
+ float luminance1 = luminance(color1) + 0.05;
+ float luminance2 = luminance(color2) + 0.05;
+ return max(luminance1, luminance2) / min(luminance1, luminance2);
+}
+
+// Return the fg if the contrast ratio is greater than min, otherwise
+// return a color that satisfies the contrast ratio. Currently, the color
+// is always white or black, whichever has the highest contrast ratio.
+//
+// Takes colors in linear RGB space. If your colors are gamma
+// encoded, linearize them before using them with this function.
+vec4 contrasted_color(float min_ratio, vec4 fg, vec4 bg) {
+ float ratio = contrast_ratio(fg.rgb, bg.rgb);
+ if (ratio < min_ratio) {
+ float white_ratio = contrast_ratio(vec3(1.0, 1.0, 1.0), bg.rgb);
+ float black_ratio = contrast_ratio(vec3(0.0, 0.0, 0.0), bg.rgb);
+ if (white_ratio > black_ratio) {
+ return vec4(1.0);
+ } else {
+ return vec4(0.0);
+ }
+ }
+
+ return fg;
+}
+
+// Converts a color from sRGB gamma encoding to linear.
+vec4 linearize(vec4 srgb) {
+ bvec3 cutoff = lessThanEqual(srgb.rgb, vec3(0.04045));
+ vec3 higher = pow((srgb.rgb + vec3(0.055)) / vec3(1.055), vec3(2.4));
+ vec3 lower = srgb.rgb / vec3(12.92);
+
+ return vec4(mix(higher, lower, cutoff), srgb.a);
+}
+float linearize(float v) {
+ return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4);
+}
+
+// Converts a color from linear to sRGB gamma encoding.
+vec4 unlinearize(vec4 linear) {
+ bvec3 cutoff = lessThanEqual(linear.rgb, vec3(0.0031308));
+ vec3 higher = pow(linear.rgb, vec3(1.0 / 2.4)) * vec3(1.055) - vec3(0.055);
+ vec3 lower = linear.rgb * vec3(12.92);
+
+ return vec4(mix(higher, lower, cutoff), linear.a);
+}
+float unlinearize(float v) {
+ return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055;
+}
+
+// Load a 4 byte RGBA non-premultiplied color and linearize
+// and convert it as necessary depending on the provided info.
+//
+// `linear` controls whether the returned color is linear or gamma encoded.
+vec4 load_color(
+ uvec4 in_color,
+ bool linear
+) {
+ // 0 .. 255 -> 0.0 .. 1.0
+ vec4 color = vec4(in_color) / vec4(255.0f);
+
+ // Linearize if necessary.
+ if (linear) color = linearize(color);
+
+ // Premultiply our color by its alpha.
+ color.rgb *= color.a;
+
+ return color;
+}
+
+//----------------------------------------------------------------------------//
diff --git a/src/renderer/shaders/glsl/full_screen.v.glsl b/src/renderer/shaders/glsl/full_screen.v.glsl
new file mode 100644
index 000000000..b89cedfa5
--- /dev/null
+++ b/src/renderer/shaders/glsl/full_screen.v.glsl
@@ -0,0 +1,24 @@
+#version 330 core
+
+void main() {
+ vec4 position;
+ position.x = (gl_VertexID == 2) ? 3.0 : -1.0;
+ position.y = (gl_VertexID == 0) ? -3.0 : 1.0;
+ position.z = 1.0;
+ position.w = 1.0;
+
+ // Single triangle is clipped to viewport.
+ //
+ // X <- vid == 0: (-1, -3)
+ // |\
+ // | \
+ // | \
+ // |###\
+ // |#+# \ `+` is (0, 0). `#`s are viewport area.
+ // |### \
+ // X------X <- vid == 2: (3, 1)
+ // ^
+ // vid == 1: (-1, 1)
+
+ gl_Position = position;
+}
diff --git a/src/renderer/shaders/glsl/image.f.glsl b/src/renderer/shaders/glsl/image.f.glsl
new file mode 100644
index 000000000..cd93cf666
--- /dev/null
+++ b/src/renderer/shaders/glsl/image.f.glsl
@@ -0,0 +1,21 @@
+#include "common.glsl"
+
+layout(binding = 0) uniform sampler2DRect image;
+
+in vec2 tex_coord;
+
+layout(location = 0) out vec4 out_FragColor;
+
+void main() {
+ bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0;
+
+ vec4 rgba = texture(image, tex_coord);
+
+ if (!use_linear_blending) {
+ rgba = unlinearize(rgba);
+ }
+
+ rgba.rgb *= vec3(rgba.a);
+
+ out_FragColor = rgba;
+}
diff --git a/src/renderer/shaders/glsl/image.v.glsl b/src/renderer/shaders/glsl/image.v.glsl
new file mode 100644
index 000000000..55b12ed68
--- /dev/null
+++ b/src/renderer/shaders/glsl/image.v.glsl
@@ -0,0 +1,46 @@
+#include "common.glsl"
+
+layout(binding = 0) uniform sampler2DRect image;
+
+layout(location = 0) in vec2 grid_pos;
+layout(location = 1) in vec2 cell_offset;
+layout(location = 2) in vec4 source_rect;
+layout(location = 3) in vec2 dest_size;
+
+out vec2 tex_coord;
+
+void main() {
+ int vid = gl_VertexID;
+
+ // We use a triangle strip with 4 vertices to render quads,
+ // so we determine which corner of the cell this vertex is in
+ // based on the vertex ID.
+ //
+ // 0 --> 1
+ // | .'|
+ // | / |
+ // | L |
+ // 2 --> 3
+ //
+ // 0 = top-left (0, 0)
+ // 1 = top-right (1, 0)
+ // 2 = bot-left (0, 1)
+ // 3 = bot-right (1, 1)
+ vec2 corner;
+ corner.x = float(vid == 1 || vid == 3);
+ corner.y = float(vid == 2 || vid == 3);
+
+ // The texture coordinates start at our source x/y
+ // and add the width/height depending on the corner.
+ //
+ // We don't need to normalize because we use pixel addressing for our sampler.
+ tex_coord = source_rect.xy;
+ tex_coord += source_rect.zw * corner;
+
+ // The position of our image starts at the top-left of the grid cell and
+ // adds the source rect width/height components.
+ vec2 image_pos = (cell_size * grid_pos) + cell_offset;
+ image_pos += dest_size * corner;
+
+ gl_Position = projection_matrix * vec4(image_pos.xy, 1.0, 1.0);
+}
diff --git a/src/renderer/shaders/image.f.glsl b/src/renderer/shaders/image.f.glsl
deleted file mode 100644
index e4aa9ef8e..000000000
--- a/src/renderer/shaders/image.f.glsl
+++ /dev/null
@@ -1,29 +0,0 @@
-#version 330 core
-
-in vec2 tex_coord;
-
-layout(location = 0) out vec4 out_FragColor;
-
-uniform sampler2D image;
-
-// Converts a color from linear to sRGB gamma encoding.
-vec4 unlinearize(vec4 linear) {
- bvec3 cutoff = lessThan(linear.rgb, vec3(0.0031308));
- vec3 higher = pow(linear.rgb, vec3(1.0/2.4)) * vec3(1.055) - vec3(0.055);
- vec3 lower = linear.rgb * vec3(12.92);
-
- return vec4(mix(higher, lower, cutoff), linear.a);
-}
-
-void main() {
- vec4 color = texture(image, tex_coord);
-
- // Our texture is stored with an sRGB internal format,
- // which means that the values are linearized when we
- // sample the texture, but for now we actually want to
- // output the color with gamma compression, so we do
- // that.
- color = unlinearize(color);
-
- out_FragColor = vec4(color.rgb * color.a, color.a);
-}
diff --git a/src/renderer/shaders/image.v.glsl b/src/renderer/shaders/image.v.glsl
deleted file mode 100644
index e3d07ca9e..000000000
--- a/src/renderer/shaders/image.v.glsl
+++ /dev/null
@@ -1,44 +0,0 @@
-#version 330 core
-
-layout (location = 0) in vec2 grid_pos;
-layout (location = 1) in vec2 cell_offset;
-layout (location = 2) in vec4 source_rect;
-layout (location = 3) in vec2 dest_size;
-
-out vec2 tex_coord;
-
-uniform sampler2D image;
-uniform vec2 cell_size;
-uniform mat4 projection;
-
-void main() {
- // The size of the image in pixels
- vec2 image_size = textureSize(image, 0);
-
- // Turn the cell position into a vertex point depending on the
- // gl_VertexID. Since we use instanced drawing, we have 4 vertices
- // for each corner of the cell. We can use gl_VertexID to determine
- // which one we're looking at. Using this, we can use 1 or 0 to keep
- // or discard the value for the vertex.
- //
- // 0 = top-right
- // 1 = bot-right
- // 2 = bot-left
- // 3 = top-left
- vec2 position;
- position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.;
- position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.;
-
- // The texture coordinates start at our source x/y, then add the width/height
- // as enabled by our instance id, then normalize to [0, 1]
- tex_coord = source_rect.xy;
- tex_coord += source_rect.zw * position;
- tex_coord /= image_size;
-
- // The position of our image starts at the top-left of the grid cell and
- // adds the source rect width/height components.
- vec2 image_pos = (cell_size * grid_pos) + cell_offset;
- image_pos += dest_size * position;
-
- gl_Position = projection * vec4(image_pos.xy, 0, 1.0);
-}
diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl
index a1a220bd4..6d9cf0f68 100644
--- a/src/renderer/shaders/shadertoy_prefix.glsl
+++ b/src/renderer/shaders/shadertoy_prefix.glsl
@@ -1,24 +1,29 @@
#version 430 core
-layout(binding = 0) uniform Globals {
- uniform vec3 iResolution;
- uniform float iTime;
- uniform float iTimeDelta;
- uniform float iFrameRate;
- uniform int iFrame;
- uniform float iChannelTime[4];
- uniform vec3 iChannelResolution[4];
- uniform vec4 iMouse;
- uniform vec4 iDate;
- uniform float iSampleRate;
+layout(binding = 1, std140) uniform Globals {
+ uniform vec3 iResolution;
+ uniform float iTime;
+ uniform float iTimeDelta;
+ uniform float iFrameRate;
+ uniform int iFrame;
+ uniform float iChannelTime[4];
+ uniform vec3 iChannelResolution[4];
+ uniform vec4 iMouse;
+ uniform vec4 iDate;
+ uniform float iSampleRate;
+ uniform vec4 iCurrentCursor;
+ uniform vec4 iPreviousCursor;
+ uniform vec4 iCurrentCursorColor;
+ uniform vec4 iPreviousCursorColor;
+ uniform float iTimeCursorChange;
};
-layout(binding = 0) uniform sampler2D iChannel0;
+layout(binding = 0) uniform sampler2D iChannel0;
// These are unused currently by Ghostty:
-// layout(binding = 1) uniform sampler2D iChannel1;
-// layout(binding = 2) uniform sampler2D iChannel2;
-// layout(binding = 3) uniform sampler2D iChannel3;
+// layout(binding = 1) uniform sampler2D iChannel1;
+// layout(binding = 2) uniform sampler2D iChannel2;
+// layout(binding = 3) uniform sampler2D iChannel3;
layout(location = 0) in vec4 gl_FragCoord;
layout(location = 0) out vec4 _fragColor;
diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig
index 45d86cbfe..576237587 100644
--- a/src/renderer/shadertoy.zig
+++ b/src/renderer/shadertoy.zig
@@ -9,6 +9,25 @@ const configpkg = @import("../config.zig");
const log = std.log.scoped(.shadertoy);
+/// The uniform struct used for shadertoy shaders.
+pub const Uniforms = extern struct {
+ resolution: [3]f32 align(16),
+ time: f32 align(4),
+ time_delta: f32 align(4),
+ frame_rate: f32 align(4),
+ frame: i32 align(4),
+ channel_time: [4][4]f32 align(16),
+ channel_resolution: [4][4]f32 align(16),
+ mouse: [4]f32 align(16),
+ date: [4]f32 align(16),
+ sample_rate: f32 align(4),
+ current_cursor: [4]f32 align(16),
+ previous_cursor: [4]f32 align(16),
+ current_cursor_color: [4]f32 align(16),
+ previous_cursor_color: [4]f32 align(16),
+ cursor_change_time: f32 align(4),
+};
+
/// The target to load shaders for.
pub const Target = enum { glsl, msl };
@@ -205,18 +224,25 @@ pub const SpirvLog = struct {
/// Convert SPIR-V binary to MSL.
pub fn mslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 {
- return try spvCross(alloc, spvcross.c.SPVC_BACKEND_MSL, spv, null);
+ const c = spvcross.c;
+ return try spvCross(alloc, spvcross.c.SPVC_BACKEND_MSL, spv, (struct {
+ fn setOptions(options: c.spvc_compiler_options) error{SpvcFailed}!void {
+ // We enable decoration binding, because we need this
+ // to properly locate the uniform block to index 1.
+ if (c.spvc_compiler_options_set_bool(
+ options,
+ c.SPVC_COMPILER_OPTION_MSL_ENABLE_DECORATION_BINDING,
+ c.SPVC_TRUE,
+ ) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+ }
+ }).setOptions);
}
-/// Convert SPIR-V binary to GLSL..
+/// Convert SPIR-V binary to GLSL.
pub fn glslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 {
- // Our minimum version for shadertoy shaders is OpenGL 4.2 because
- // Spirv-Cross generates binding locations for uniforms which is
- // only supported in OpenGL 4.2 and above.
- //
- // If we can figure out a way to NOT do this then we can lower this
- // version.
- const GLSL_VERSION = 420;
+ const GLSL_VERSION = 430;
const c = spvcross.c;
return try spvCross(alloc, c.SPVC_BACKEND_GLSL, spv, (struct {
diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig
index 317ad13b4..aed7cefb6 100644
--- a/src/termio/Exec.zig
+++ b/src/termio/Exec.zig
@@ -1364,6 +1364,13 @@ pub const ReadThread = struct {
// Always close our end of the pipe when we exit.
defer posix.close(quit);
+ // Right now, on Darwin, `std.Thread.setName` can only name the current
+ // thread, and we have no way to get the current thread from within it,
+ // so instead we use this code to name the thread instead.
+ if (builtin.os.tag.isDarwin()) {
+ internal_os.macos.pthread_setname_np(&"io-reader".*);
+ }
+
// Setup our crash metadata
crash.sentry.thread_state = .{
.type = .io,
diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig
index ecfb9951e..c474d55bb 100644
--- a/src/termio/Termio.zig
+++ b/src/termio/Termio.zig
@@ -70,6 +70,89 @@ terminal_stream: terminalpkg.Stream(StreamHandler),
/// flooding with cursor resets.
last_cursor_reset: ?std.time.Instant = null,
+/// State we have for thread enter. This may be null if we don't need
+/// to keep track of any state or if its already been freed.
+thread_enter_state: ?*ThreadEnterState = null,
+
+/// The state we need to keep around only until we enter the IO
+/// thread. Then we can throw it all away.
+const ThreadEnterState = struct {
+ arena: ArenaAllocator,
+
+ /// Initial input to send to the subprocess after starting. This
+ /// memory is freed once the subprocess start is attempted, even
+ /// if it fails, because Exec only starts once.
+ input: configpkg.io.RepeatableReadableIO,
+
+ pub fn create(
+ alloc: Allocator,
+ config: *const configpkg.Config,
+ ) !?*ThreadEnterState {
+ // If we have no input then we have no thread enter state
+ if (config.input.list.items.len == 0) return null;
+
+ // Create our arena allocator
+ var arena = ArenaAllocator.init(alloc);
+ errdefer arena.deinit();
+ const arena_alloc = arena.allocator();
+
+ // Allocate our ThreadEnterState
+ const ptr = try arena_alloc.create(ThreadEnterState);
+
+ // Copy the input from the config
+ const input = try config.input.cloneParsed(arena_alloc);
+
+ // Return the initialized state
+ ptr.* = .{
+ .arena = arena,
+ .input = input,
+ };
+ return ptr;
+ }
+
+ pub fn destroy(self: *ThreadEnterState) void {
+ self.arena.deinit();
+ }
+
+ /// Prepare the inputs for use. Allocations happen on the arena.
+ pub fn prepareInput(
+ self: *ThreadEnterState,
+ ) (Allocator.Error || error{InputNotFound})![]const Input {
+ const alloc = self.arena.allocator();
+
+ var input = try alloc.alloc(
+ Input,
+ self.input.list.items.len,
+ );
+ for (self.input.list.items, 0..) |item, i| {
+ input[i] = switch (item) {
+ .raw => |v| .{ .string = try alloc.dupe(u8, v) },
+ .path => |path| file: {
+ const f = std.fs.cwd().openFile(
+ path,
+ .{},
+ ) catch |err| {
+ log.warn("failed to open input file={s} err={}", .{
+ path,
+ err,
+ });
+ return error.InputNotFound;
+ };
+
+ break :file .{ .file = f };
+ },
+ };
+ }
+
+ return input;
+ }
+
+ const Input = union(enum) {
+ string: []const u8,
+ file: std.fs.File,
+ };
+};
+
/// The configuration for this IO that is derived from the main
/// configuration. This must be exported so that we don't need to
/// pass around Config pointers which makes memory management a pain.
@@ -211,6 +294,11 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
};
};
+ const thread_enter_state = try ThreadEnterState.create(
+ alloc,
+ opts.full_config,
+ );
+
self.* = .{
.alloc = alloc,
.terminal = term,
@@ -232,6 +320,7 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
},
},
},
+ .thread_enter_state = thread_enter_state,
};
}
@@ -244,9 +333,30 @@ pub fn deinit(self: *Termio) void {
// Clear any StreamHandler state
self.terminal_stream.handler.deinit();
self.terminal_stream.deinit();
+
+ // Clear any initial state if we have it
+ if (self.thread_enter_state) |v| v.destroy();
}
-pub fn threadEnter(self: *Termio, thread: *termio.Thread, data: *ThreadData) !void {
+pub fn threadEnter(
+ self: *Termio,
+ thread: *termio.Thread,
+ data: *ThreadData,
+) !void {
+ // Always free our thread enter state when we're done.
+ defer if (self.thread_enter_state) |v| {
+ v.destroy();
+ self.thread_enter_state = null;
+ };
+
+ // If we have thread enter state then we're going to validate
+ // and set that all up now so that we can error before we actually
+ // start the command and pty.
+ const inputs: ?[]const ThreadEnterState.Input = if (self.thread_enter_state) |v|
+ try v.prepareInput()
+ else
+ null;
+
data.* = .{
.alloc = self.alloc,
.loop = &thread.loop,
@@ -258,6 +368,29 @@ pub fn threadEnter(self: *Termio, thread: *termio.Thread, data: *ThreadData) !vo
// Setup our backend
try self.backend.threadEnter(self.alloc, self, data);
+ errdefer self.backend.threadExit(data);
+
+ // If we have inputs, then queue them all up.
+ for (inputs orelse &.{}) |input| switch (input) {
+ .string => |v| self.queueWrite(data, v, false) catch |err| {
+ log.warn("failed to queue input string err={}", .{err});
+ return error.InputFailed;
+ },
+ .file => |f| self.queueWrite(
+ data,
+ f.readToEndAlloc(
+ self.alloc,
+ 10 * 1024 * 1024, // 10 MiB max
+ ) catch |err| {
+ log.warn("failed to read input file err={}", .{err});
+ return error.InputFailed;
+ },
+ false,
+ ) catch |err| {
+ log.warn("failed to queue input file err={}", .{err});
+ return error.InputFailed;
+ },
+ };
}
pub fn threadExit(self: *Termio, data: *ThreadData) void {
diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig
index d8018341d..58a04f5a7 100644
--- a/src/termio/Thread.zig
+++ b/src/termio/Thread.zig
@@ -16,6 +16,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const builtin = @import("builtin");
const xev = @import("../global.zig").xev;
const crash = @import("../crash/main.zig");
+const internal_os = @import("../os/main.zig");
const termio = @import("../termio.zig");
const renderer = @import("../renderer.zig");
const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue;
@@ -145,6 +146,8 @@ pub fn threadMain(self: *Thread, io: *termio.Termio) void {
// have "OpenptyFailed".
const Err = @TypeOf(err) || error{
OpenptyFailed,
+ InputNotFound,
+ InputFailed,
};
switch (@as(Err, @errorCast(err))) {
@@ -164,6 +167,24 @@ pub fn threadMain(self: *Thread, io: *termio.Termio) void {
t.printString(str) catch {};
},
+ error.InputNotFound,
+ error.InputFailed,
+ => {
+ const str =
+ \\A configured `input` path was not found, was not readable,
+ \\was too large, or the underlying pty failed to accept
+ \\the write.
+ \\
+ \\Ghostty can't continue since it can't guarantee that
+ \\initial terminal state will be as desired. Please review
+ \\the value of `input` in your configuration file and
+ \\ensure that all the path values exist and are readable.
+ ;
+
+ t.eraseDisplay(.complete, false);
+ t.printString(str) catch {};
+ },
+
else => {
const str = std.fmt.allocPrint(
alloc,
@@ -202,6 +223,13 @@ pub fn threadMain(self: *Thread, io: *termio.Termio) void {
fn threadMain_(self: *Thread, io: *termio.Termio) !void {
defer log.debug("IO thread exited", .{});
+ // Right now, on Darwin, `std.Thread.setName` can only name the current
+ // thread, and we have no way to get the current thread from within it,
+ // so instead we use this code to name the thread instead.
+ if (builtin.os.tag.isDarwin()) {
+ internal_os.macos.pthread_setname_np(&"io".*);
+ }
+
// Setup our crash metadata
crash.sentry.thread_state = .{
.type = .io,
diff --git a/vendor/glad/include/glad/gl.h b/vendor/glad/include/glad/gl.h
index 2f71276dc..b9b398187 100644
--- a/vendor/glad/include/glad/gl.h
+++ b/vendor/glad/include/glad/gl.h
@@ -1,5 +1,5 @@
/**
- * Loader generated by glad 2.0.0 on Mon Oct 24 00:13:28 2022
+ * Loader generated by glad 2.0.8 on Mon May 19 01:37:34 2025
*
* SPDX-License-Identifier: (WTFPL OR CC0-1.0) AND Apache-2.0
*
@@ -8,7 +8,7 @@
* Extensions: 0
*
* APIs:
- * - gl:core=3.3
+ * - gl:core=4.3
*
* Options:
* - ALIAS = False
@@ -19,10 +19,10 @@
* - ON_DEMAND = False
*
* Commandline:
- * --api='gl:core=3.3' --extensions='' c --loader --mx
+ * --api='gl:core=4.3' --extensions='' c --loader --mx
*
* Online:
- * http://glad.sh/#api=gl%3Acore%3D3.3&extensions=&generator=c&options=LOADER%2CMX
+ * http://glad.sh/#api=gl%3Acore%3D4.3&extensions=&generator=c&options=LOADER%2CMX
*
*/
@@ -165,7 +165,7 @@ extern "C" {
#define GLAD_VERSION_MAJOR(version) (version / 10000)
#define GLAD_VERSION_MINOR(version) (version % 10000)
-#define GLAD_GENERATOR_VERSION "2.0.0"
+#define GLAD_GENERATOR_VERSION "2.0.8"
typedef void (*GLADapiproc)(void);
@@ -177,14 +177,25 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#endif /* GLAD_PLATFORM_H_ */
+#define GL_ACTIVE_ATOMIC_COUNTER_BUFFERS 0x92D9
#define GL_ACTIVE_ATTRIBUTES 0x8B89
#define GL_ACTIVE_ATTRIBUTE_MAX_LENGTH 0x8B8A
+#define GL_ACTIVE_PROGRAM 0x8259
+#define GL_ACTIVE_RESOURCES 0x92F5
+#define GL_ACTIVE_SUBROUTINES 0x8DE5
+#define GL_ACTIVE_SUBROUTINE_MAX_LENGTH 0x8E48
+#define GL_ACTIVE_SUBROUTINE_UNIFORMS 0x8DE6
+#define GL_ACTIVE_SUBROUTINE_UNIFORM_LOCATIONS 0x8E47
+#define GL_ACTIVE_SUBROUTINE_UNIFORM_MAX_LENGTH 0x8E49
#define GL_ACTIVE_TEXTURE 0x84E0
#define GL_ACTIVE_UNIFORMS 0x8B86
#define GL_ACTIVE_UNIFORM_BLOCKS 0x8A36
#define GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH 0x8A35
#define GL_ACTIVE_UNIFORM_MAX_LENGTH 0x8B87
+#define GL_ACTIVE_VARIABLES 0x9305
#define GL_ALIASED_LINE_WIDTH_RANGE 0x846E
+#define GL_ALL_BARRIER_BITS 0xFFFFFFFF
+#define GL_ALL_SHADER_BITS 0xFFFFFFFF
#define GL_ALPHA 0x1906
#define GL_ALREADY_SIGNALED 0x911A
#define GL_ALWAYS 0x0207
@@ -192,9 +203,28 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_AND_INVERTED 0x1504
#define GL_AND_REVERSE 0x1502
#define GL_ANY_SAMPLES_PASSED 0x8C2F
+#define GL_ANY_SAMPLES_PASSED_CONSERVATIVE 0x8D6A
#define GL_ARRAY_BUFFER 0x8892
#define GL_ARRAY_BUFFER_BINDING 0x8894
+#define GL_ARRAY_SIZE 0x92FB
+#define GL_ARRAY_STRIDE 0x92FE
+#define GL_ATOMIC_COUNTER_BARRIER_BIT 0x00001000
+#define GL_ATOMIC_COUNTER_BUFFER 0x92C0
+#define GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS 0x92C5
+#define GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES 0x92C6
+#define GL_ATOMIC_COUNTER_BUFFER_BINDING 0x92C1
+#define GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE 0x92C4
+#define GL_ATOMIC_COUNTER_BUFFER_INDEX 0x9301
+#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_COMPUTE_SHADER 0x90ED
+#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER 0x92CB
+#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_GEOMETRY_SHADER 0x92CA
+#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_CONTROL_SHADER 0x92C8
+#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_EVALUATION_SHADER 0x92C9
+#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER 0x92C7
+#define GL_ATOMIC_COUNTER_BUFFER_SIZE 0x92C3
+#define GL_ATOMIC_COUNTER_BUFFER_START 0x92C2
#define GL_ATTACHED_SHADERS 0x8B85
+#define GL_AUTO_GENERATE_MIPMAP 0x8295
#define GL_BACK 0x0405
#define GL_BACK_LEFT 0x0402
#define GL_BACK_RIGHT 0x0403
@@ -213,26 +243,34 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_BLEND_SRC 0x0BE1
#define GL_BLEND_SRC_ALPHA 0x80CB
#define GL_BLEND_SRC_RGB 0x80C9
+#define GL_BLOCK_INDEX 0x92FD
#define GL_BLUE 0x1905
#define GL_BLUE_INTEGER 0x8D96
#define GL_BOOL 0x8B56
#define GL_BOOL_VEC2 0x8B57
#define GL_BOOL_VEC3 0x8B58
#define GL_BOOL_VEC4 0x8B59
+#define GL_BUFFER 0x82E0
#define GL_BUFFER_ACCESS 0x88BB
#define GL_BUFFER_ACCESS_FLAGS 0x911F
+#define GL_BUFFER_BINDING 0x9302
+#define GL_BUFFER_DATA_SIZE 0x9303
#define GL_BUFFER_MAPPED 0x88BC
#define GL_BUFFER_MAP_LENGTH 0x9120
#define GL_BUFFER_MAP_OFFSET 0x9121
#define GL_BUFFER_MAP_POINTER 0x88BD
#define GL_BUFFER_SIZE 0x8764
+#define GL_BUFFER_UPDATE_BARRIER_BIT 0x00000200
#define GL_BUFFER_USAGE 0x8765
+#define GL_BUFFER_VARIABLE 0x92E5
#define GL_BYTE 0x1400
+#define GL_CAVEAT_SUPPORT 0x82B8
#define GL_CCW 0x0901
#define GL_CLAMP_READ_COLOR 0x891C
#define GL_CLAMP_TO_BORDER 0x812D
#define GL_CLAMP_TO_EDGE 0x812F
#define GL_CLEAR 0x1500
+#define GL_CLEAR_BUFFER 0x82B4
#define GL_CLIP_DISTANCE0 0x3000
#define GL_CLIP_DISTANCE1 0x3001
#define GL_CLIP_DISTANCE2 0x3002
@@ -276,39 +314,93 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_COLOR_ATTACHMENT9 0x8CE9
#define GL_COLOR_BUFFER_BIT 0x00004000
#define GL_COLOR_CLEAR_VALUE 0x0C22
+#define GL_COLOR_COMPONENTS 0x8283
+#define GL_COLOR_ENCODING 0x8296
#define GL_COLOR_LOGIC_OP 0x0BF2
+#define GL_COLOR_RENDERABLE 0x8286
#define GL_COLOR_WRITEMASK 0x0C23
+#define GL_COMMAND_BARRIER_BIT 0x00000040
#define GL_COMPARE_REF_TO_TEXTURE 0x884E
+#define GL_COMPATIBLE_SUBROUTINES 0x8E4B
#define GL_COMPILE_STATUS 0x8B81
+#define GL_COMPRESSED_R11_EAC 0x9270
#define GL_COMPRESSED_RED 0x8225
#define GL_COMPRESSED_RED_RGTC1 0x8DBB
#define GL_COMPRESSED_RG 0x8226
+#define GL_COMPRESSED_RG11_EAC 0x9272
#define GL_COMPRESSED_RGB 0x84ED
+#define GL_COMPRESSED_RGB8_ETC2 0x9274
+#define GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9276
#define GL_COMPRESSED_RGBA 0x84EE
+#define GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278
+#define GL_COMPRESSED_RGBA_BPTC_UNORM 0x8E8C
+#define GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT 0x8E8E
+#define GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT 0x8E8F
#define GL_COMPRESSED_RG_RGTC2 0x8DBD
+#define GL_COMPRESSED_SIGNED_R11_EAC 0x9271
#define GL_COMPRESSED_SIGNED_RED_RGTC1 0x8DBC
+#define GL_COMPRESSED_SIGNED_RG11_EAC 0x9273
#define GL_COMPRESSED_SIGNED_RG_RGTC2 0x8DBE
#define GL_COMPRESSED_SRGB 0x8C48
+#define GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC 0x9279
+#define GL_COMPRESSED_SRGB8_ETC2 0x9275
+#define GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9277
#define GL_COMPRESSED_SRGB_ALPHA 0x8C49
+#define GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM 0x8E8D
#define GL_COMPRESSED_TEXTURE_FORMATS 0x86A3
+#define GL_COMPUTE_SHADER 0x91B9
+#define GL_COMPUTE_SHADER_BIT 0x00000020
+#define GL_COMPUTE_SUBROUTINE 0x92ED
+#define GL_COMPUTE_SUBROUTINE_UNIFORM 0x92F3
+#define GL_COMPUTE_TEXTURE 0x82A0
+#define GL_COMPUTE_WORK_GROUP_SIZE 0x8267
#define GL_CONDITION_SATISFIED 0x911C
#define GL_CONSTANT_ALPHA 0x8003
#define GL_CONSTANT_COLOR 0x8001
#define GL_CONTEXT_COMPATIBILITY_PROFILE_BIT 0x00000002
#define GL_CONTEXT_CORE_PROFILE_BIT 0x00000001
#define GL_CONTEXT_FLAGS 0x821E
+#define GL_CONTEXT_FLAG_DEBUG_BIT 0x00000002
#define GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT 0x00000001
#define GL_CONTEXT_PROFILE_MASK 0x9126
#define GL_COPY 0x1503
#define GL_COPY_INVERTED 0x150C
#define GL_COPY_READ_BUFFER 0x8F36
+#define GL_COPY_READ_BUFFER_BINDING 0x8F36
#define GL_COPY_WRITE_BUFFER 0x8F37
+#define GL_COPY_WRITE_BUFFER_BINDING 0x8F37
#define GL_CULL_FACE 0x0B44
#define GL_CULL_FACE_MODE 0x0B45
#define GL_CURRENT_PROGRAM 0x8B8D
#define GL_CURRENT_QUERY 0x8865
#define GL_CURRENT_VERTEX_ATTRIB 0x8626
#define GL_CW 0x0900
+#define GL_DEBUG_CALLBACK_FUNCTION 0x8244
+#define GL_DEBUG_CALLBACK_USER_PARAM 0x8245
+#define GL_DEBUG_GROUP_STACK_DEPTH 0x826D
+#define GL_DEBUG_LOGGED_MESSAGES 0x9145
+#define GL_DEBUG_NEXT_LOGGED_MESSAGE_LENGTH 0x8243
+#define GL_DEBUG_OUTPUT 0x92E0
+#define GL_DEBUG_OUTPUT_SYNCHRONOUS 0x8242
+#define GL_DEBUG_SEVERITY_HIGH 0x9146
+#define GL_DEBUG_SEVERITY_LOW 0x9148
+#define GL_DEBUG_SEVERITY_MEDIUM 0x9147
+#define GL_DEBUG_SEVERITY_NOTIFICATION 0x826B
+#define GL_DEBUG_SOURCE_API 0x8246
+#define GL_DEBUG_SOURCE_APPLICATION 0x824A
+#define GL_DEBUG_SOURCE_OTHER 0x824B
+#define GL_DEBUG_SOURCE_SHADER_COMPILER 0x8248
+#define GL_DEBUG_SOURCE_THIRD_PARTY 0x8249
+#define GL_DEBUG_SOURCE_WINDOW_SYSTEM 0x8247
+#define GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR 0x824D
+#define GL_DEBUG_TYPE_ERROR 0x824C
+#define GL_DEBUG_TYPE_MARKER 0x8268
+#define GL_DEBUG_TYPE_OTHER 0x8251
+#define GL_DEBUG_TYPE_PERFORMANCE 0x8250
+#define GL_DEBUG_TYPE_POP_GROUP 0x826A
+#define GL_DEBUG_TYPE_PORTABILITY 0x824F
+#define GL_DEBUG_TYPE_PUSH_GROUP 0x8269
+#define GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR 0x824E
#define GL_DECR 0x1E03
#define GL_DECR_WRAP 0x8508
#define GL_DELETE_STATUS 0x8B80
@@ -324,16 +416,33 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_DEPTH_COMPONENT24 0x81A6
#define GL_DEPTH_COMPONENT32 0x81A7
#define GL_DEPTH_COMPONENT32F 0x8CAC
+#define GL_DEPTH_COMPONENTS 0x8284
#define GL_DEPTH_FUNC 0x0B74
#define GL_DEPTH_RANGE 0x0B70
+#define GL_DEPTH_RENDERABLE 0x8287
#define GL_DEPTH_STENCIL 0x84F9
#define GL_DEPTH_STENCIL_ATTACHMENT 0x821A
+#define GL_DEPTH_STENCIL_TEXTURE_MODE 0x90EA
#define GL_DEPTH_TEST 0x0B71
#define GL_DEPTH_WRITEMASK 0x0B72
+#define GL_DISPATCH_INDIRECT_BUFFER 0x90EE
+#define GL_DISPATCH_INDIRECT_BUFFER_BINDING 0x90EF
#define GL_DITHER 0x0BD0
#define GL_DONT_CARE 0x1100
#define GL_DOUBLE 0x140A
#define GL_DOUBLEBUFFER 0x0C32
+#define GL_DOUBLE_MAT2 0x8F46
+#define GL_DOUBLE_MAT2x3 0x8F49
+#define GL_DOUBLE_MAT2x4 0x8F4A
+#define GL_DOUBLE_MAT3 0x8F47
+#define GL_DOUBLE_MAT3x2 0x8F4B
+#define GL_DOUBLE_MAT3x4 0x8F4C
+#define GL_DOUBLE_MAT4 0x8F48
+#define GL_DOUBLE_MAT4x2 0x8F4D
+#define GL_DOUBLE_MAT4x3 0x8F4E
+#define GL_DOUBLE_VEC2 0x8FFC
+#define GL_DOUBLE_VEC3 0x8FFD
+#define GL_DOUBLE_VEC4 0x8FFE
#define GL_DRAW_BUFFER 0x0C01
#define GL_DRAW_BUFFER0 0x8825
#define GL_DRAW_BUFFER1 0x8826
@@ -353,11 +462,14 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_DRAW_BUFFER9 0x882E
#define GL_DRAW_FRAMEBUFFER 0x8CA9
#define GL_DRAW_FRAMEBUFFER_BINDING 0x8CA6
+#define GL_DRAW_INDIRECT_BUFFER 0x8F3F
+#define GL_DRAW_INDIRECT_BUFFER_BINDING 0x8F43
#define GL_DST_ALPHA 0x0304
#define GL_DST_COLOR 0x0306
#define GL_DYNAMIC_COPY 0x88EA
#define GL_DYNAMIC_DRAW 0x88E8
#define GL_DYNAMIC_READ 0x88E9
+#define GL_ELEMENT_ARRAY_BARRIER_BIT 0x00000002
#define GL_ELEMENT_ARRAY_BUFFER 0x8893
#define GL_ELEMENT_ARRAY_BUFFER_BINDING 0x8895
#define GL_EQUAL 0x0202
@@ -366,7 +478,9 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_FALSE 0
#define GL_FASTEST 0x1101
#define GL_FILL 0x1B02
+#define GL_FILTER 0x829A
#define GL_FIRST_VERTEX_CONVENTION 0x8E4D
+#define GL_FIXED 0x140C
#define GL_FIXED_ONLY 0x891D
#define GL_FLOAT 0x1406
#define GL_FLOAT_32_UNSIGNED_INT_24_8_REV 0x8DAD
@@ -382,8 +496,15 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_FLOAT_VEC2 0x8B50
#define GL_FLOAT_VEC3 0x8B51
#define GL_FLOAT_VEC4 0x8B52
+#define GL_FRACTIONAL_EVEN 0x8E7C
+#define GL_FRACTIONAL_ODD 0x8E7B
+#define GL_FRAGMENT_INTERPOLATION_OFFSET_BITS 0x8E5D
#define GL_FRAGMENT_SHADER 0x8B30
+#define GL_FRAGMENT_SHADER_BIT 0x00000002
#define GL_FRAGMENT_SHADER_DERIVATIVE_HINT 0x8B8B
+#define GL_FRAGMENT_SUBROUTINE 0x92EC
+#define GL_FRAGMENT_SUBROUTINE_UNIFORM 0x92F2
+#define GL_FRAGMENT_TEXTURE 0x829F
#define GL_FRAMEBUFFER 0x8D40
#define GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE 0x8215
#define GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE 0x8214
@@ -399,15 +520,24 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE 0x8CD3
#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER 0x8CD4
#define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL 0x8CD2
+#define GL_FRAMEBUFFER_BARRIER_BIT 0x00000400
#define GL_FRAMEBUFFER_BINDING 0x8CA6
+#define GL_FRAMEBUFFER_BLEND 0x828B
#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
#define GL_FRAMEBUFFER_DEFAULT 0x8218
+#define GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS 0x9314
+#define GL_FRAMEBUFFER_DEFAULT_HEIGHT 0x9311
+#define GL_FRAMEBUFFER_DEFAULT_LAYERS 0x9312
+#define GL_FRAMEBUFFER_DEFAULT_SAMPLES 0x9313
+#define GL_FRAMEBUFFER_DEFAULT_WIDTH 0x9310
#define GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT 0x8CD6
#define GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER 0x8CDB
#define GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS 0x8DA8
#define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT 0x8CD7
#define GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE 0x8D56
#define GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER 0x8CDC
+#define GL_FRAMEBUFFER_RENDERABLE 0x8289
+#define GL_FRAMEBUFFER_RENDERABLE_LAYERED 0x828A
#define GL_FRAMEBUFFER_SRGB 0x8DB9
#define GL_FRAMEBUFFER_UNDEFINED 0x8219
#define GL_FRAMEBUFFER_UNSUPPORTED 0x8CDD
@@ -416,24 +546,97 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_FRONT_FACE 0x0B46
#define GL_FRONT_LEFT 0x0400
#define GL_FRONT_RIGHT 0x0401
+#define GL_FULL_SUPPORT 0x82B7
#define GL_FUNC_ADD 0x8006
#define GL_FUNC_REVERSE_SUBTRACT 0x800B
#define GL_FUNC_SUBTRACT 0x800A
#define GL_GEOMETRY_INPUT_TYPE 0x8917
#define GL_GEOMETRY_OUTPUT_TYPE 0x8918
#define GL_GEOMETRY_SHADER 0x8DD9
+#define GL_GEOMETRY_SHADER_BIT 0x00000004
+#define GL_GEOMETRY_SHADER_INVOCATIONS 0x887F
+#define GL_GEOMETRY_SUBROUTINE 0x92EB
+#define GL_GEOMETRY_SUBROUTINE_UNIFORM 0x92F1
+#define GL_GEOMETRY_TEXTURE 0x829E
#define GL_GEOMETRY_VERTICES_OUT 0x8916
#define GL_GEQUAL 0x0206
+#define GL_GET_TEXTURE_IMAGE_FORMAT 0x8291
+#define GL_GET_TEXTURE_IMAGE_TYPE 0x8292
#define GL_GREATER 0x0204
#define GL_GREEN 0x1904
#define GL_GREEN_INTEGER 0x8D95
#define GL_HALF_FLOAT 0x140B
+#define GL_HIGH_FLOAT 0x8DF2
+#define GL_HIGH_INT 0x8DF5
+#define GL_IMAGE_1D 0x904C
+#define GL_IMAGE_1D_ARRAY 0x9052
+#define GL_IMAGE_2D 0x904D
+#define GL_IMAGE_2D_ARRAY 0x9053
+#define GL_IMAGE_2D_MULTISAMPLE 0x9055
+#define GL_IMAGE_2D_MULTISAMPLE_ARRAY 0x9056
+#define GL_IMAGE_2D_RECT 0x904F
+#define GL_IMAGE_3D 0x904E
+#define GL_IMAGE_BINDING_ACCESS 0x8F3E
+#define GL_IMAGE_BINDING_FORMAT 0x906E
+#define GL_IMAGE_BINDING_LAYER 0x8F3D
+#define GL_IMAGE_BINDING_LAYERED 0x8F3C
+#define GL_IMAGE_BINDING_LEVEL 0x8F3B
+#define GL_IMAGE_BINDING_NAME 0x8F3A
+#define GL_IMAGE_BUFFER 0x9051
+#define GL_IMAGE_CLASS_10_10_10_2 0x82C3
+#define GL_IMAGE_CLASS_11_11_10 0x82C2
+#define GL_IMAGE_CLASS_1_X_16 0x82BE
+#define GL_IMAGE_CLASS_1_X_32 0x82BB
+#define GL_IMAGE_CLASS_1_X_8 0x82C1
+#define GL_IMAGE_CLASS_2_X_16 0x82BD
+#define GL_IMAGE_CLASS_2_X_32 0x82BA
+#define GL_IMAGE_CLASS_2_X_8 0x82C0
+#define GL_IMAGE_CLASS_4_X_16 0x82BC
+#define GL_IMAGE_CLASS_4_X_32 0x82B9
+#define GL_IMAGE_CLASS_4_X_8 0x82BF
+#define GL_IMAGE_COMPATIBILITY_CLASS 0x82A8
+#define GL_IMAGE_CUBE 0x9050
+#define GL_IMAGE_CUBE_MAP_ARRAY 0x9054
+#define GL_IMAGE_FORMAT_COMPATIBILITY_BY_CLASS 0x90C9
+#define GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE 0x90C8
+#define GL_IMAGE_FORMAT_COMPATIBILITY_TYPE 0x90C7
+#define GL_IMAGE_PIXEL_FORMAT 0x82A9
+#define GL_IMAGE_PIXEL_TYPE 0x82AA
+#define GL_IMAGE_TEXEL_SIZE 0x82A7
+#define GL_IMPLEMENTATION_COLOR_READ_FORMAT 0x8B9B
+#define GL_IMPLEMENTATION_COLOR_READ_TYPE 0x8B9A
#define GL_INCR 0x1E02
#define GL_INCR_WRAP 0x8507
#define GL_INFO_LOG_LENGTH 0x8B84
#define GL_INT 0x1404
#define GL_INTERLEAVED_ATTRIBS 0x8C8C
+#define GL_INTERNALFORMAT_ALPHA_SIZE 0x8274
+#define GL_INTERNALFORMAT_ALPHA_TYPE 0x827B
+#define GL_INTERNALFORMAT_BLUE_SIZE 0x8273
+#define GL_INTERNALFORMAT_BLUE_TYPE 0x827A
+#define GL_INTERNALFORMAT_DEPTH_SIZE 0x8275
+#define GL_INTERNALFORMAT_DEPTH_TYPE 0x827C
+#define GL_INTERNALFORMAT_GREEN_SIZE 0x8272
+#define GL_INTERNALFORMAT_GREEN_TYPE 0x8279
+#define GL_INTERNALFORMAT_PREFERRED 0x8270
+#define GL_INTERNALFORMAT_RED_SIZE 0x8271
+#define GL_INTERNALFORMAT_RED_TYPE 0x8278
+#define GL_INTERNALFORMAT_SHARED_SIZE 0x8277
+#define GL_INTERNALFORMAT_STENCIL_SIZE 0x8276
+#define GL_INTERNALFORMAT_STENCIL_TYPE 0x827D
+#define GL_INTERNALFORMAT_SUPPORTED 0x826F
#define GL_INT_2_10_10_10_REV 0x8D9F
+#define GL_INT_IMAGE_1D 0x9057
+#define GL_INT_IMAGE_1D_ARRAY 0x905D
+#define GL_INT_IMAGE_2D 0x9058
+#define GL_INT_IMAGE_2D_ARRAY 0x905E
+#define GL_INT_IMAGE_2D_MULTISAMPLE 0x9060
+#define GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x9061
+#define GL_INT_IMAGE_2D_RECT 0x905A
+#define GL_INT_IMAGE_3D 0x9059
+#define GL_INT_IMAGE_BUFFER 0x905C
+#define GL_INT_IMAGE_CUBE 0x905B
+#define GL_INT_IMAGE_CUBE_MAP_ARRAY 0x905F
#define GL_INT_SAMPLER_1D 0x8DC9
#define GL_INT_SAMPLER_1D_ARRAY 0x8DCE
#define GL_INT_SAMPLER_2D 0x8DCA
@@ -444,6 +647,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_INT_SAMPLER_3D 0x8DCB
#define GL_INT_SAMPLER_BUFFER 0x8DD0
#define GL_INT_SAMPLER_CUBE 0x8DCC
+#define GL_INT_SAMPLER_CUBE_MAP_ARRAY 0x900E
#define GL_INT_VEC2 0x8B53
#define GL_INT_VEC3 0x8B54
#define GL_INT_VEC4 0x8B55
@@ -453,8 +657,12 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_INVALID_OPERATION 0x0502
#define GL_INVALID_VALUE 0x0501
#define GL_INVERT 0x150A
+#define GL_ISOLINES 0x8E7A
+#define GL_IS_PER_PATCH 0x92E7
+#define GL_IS_ROW_MAJOR 0x9300
#define GL_KEEP 0x1E00
#define GL_LAST_VERTEX_CONVENTION 0x8E4E
+#define GL_LAYER_PROVOKING_VERTEX 0x825E
#define GL_LEFT 0x0406
#define GL_LEQUAL 0x0203
#define GL_LESS 0x0201
@@ -473,71 +681,176 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_LINE_WIDTH_GRANULARITY 0x0B23
#define GL_LINE_WIDTH_RANGE 0x0B22
#define GL_LINK_STATUS 0x8B82
+#define GL_LOCATION 0x930E
+#define GL_LOCATION_INDEX 0x930F
#define GL_LOGIC_OP_MODE 0x0BF0
#define GL_LOWER_LEFT 0x8CA1
+#define GL_LOW_FLOAT 0x8DF0
+#define GL_LOW_INT 0x8DF3
#define GL_MAJOR_VERSION 0x821B
+#define GL_MANUAL_GENERATE_MIPMAP 0x8294
#define GL_MAP_FLUSH_EXPLICIT_BIT 0x0010
#define GL_MAP_INVALIDATE_BUFFER_BIT 0x0008
#define GL_MAP_INVALIDATE_RANGE_BIT 0x0004
#define GL_MAP_READ_BIT 0x0001
#define GL_MAP_UNSYNCHRONIZED_BIT 0x0020
#define GL_MAP_WRITE_BIT 0x0002
+#define GL_MATRIX_STRIDE 0x92FF
#define GL_MAX 0x8008
#define GL_MAX_3D_TEXTURE_SIZE 0x8073
#define GL_MAX_ARRAY_TEXTURE_LAYERS 0x88FF
+#define GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS 0x92DC
+#define GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE 0x92D8
#define GL_MAX_CLIP_DISTANCES 0x0D32
#define GL_MAX_COLOR_ATTACHMENTS 0x8CDF
#define GL_MAX_COLOR_TEXTURE_SAMPLES 0x910E
+#define GL_MAX_COMBINED_ATOMIC_COUNTERS 0x92D7
+#define GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS 0x92D1
+#define GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS 0x8266
+#define GL_MAX_COMBINED_DIMENSIONS 0x8282
#define GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS 0x8A33
#define GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS 0x8A32
+#define GL_MAX_COMBINED_IMAGE_UNIFORMS 0x90CF
+#define GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS 0x8F39
+#define GL_MAX_COMBINED_SHADER_OUTPUT_RESOURCES 0x8F39
+#define GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS 0x90DC
+#define GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS 0x8E1E
+#define GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS 0x8E1F
#define GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS 0x8B4D
#define GL_MAX_COMBINED_UNIFORM_BLOCKS 0x8A2E
#define GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS 0x8A31
+#define GL_MAX_COMPUTE_ATOMIC_COUNTERS 0x8265
+#define GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS 0x8264
+#define GL_MAX_COMPUTE_IMAGE_UNIFORMS 0x91BD
+#define GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS 0x90DB
+#define GL_MAX_COMPUTE_SHARED_MEMORY_SIZE 0x8262
+#define GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS 0x91BC
+#define GL_MAX_COMPUTE_UNIFORM_BLOCKS 0x91BB
+#define GL_MAX_COMPUTE_UNIFORM_COMPONENTS 0x8263
+#define GL_MAX_COMPUTE_WORK_GROUP_COUNT 0x91BE
+#define GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS 0x90EB
+#define GL_MAX_COMPUTE_WORK_GROUP_SIZE 0x91BF
#define GL_MAX_CUBE_MAP_TEXTURE_SIZE 0x851C
+#define GL_MAX_DEBUG_GROUP_STACK_DEPTH 0x826C
+#define GL_MAX_DEBUG_LOGGED_MESSAGES 0x9144
+#define GL_MAX_DEBUG_MESSAGE_LENGTH 0x9143
+#define GL_MAX_DEPTH 0x8280
#define GL_MAX_DEPTH_TEXTURE_SAMPLES 0x910F
#define GL_MAX_DRAW_BUFFERS 0x8824
#define GL_MAX_DUAL_SOURCE_DRAW_BUFFERS 0x88FC
#define GL_MAX_ELEMENTS_INDICES 0x80E9
#define GL_MAX_ELEMENTS_VERTICES 0x80E8
+#define GL_MAX_ELEMENT_INDEX 0x8D6B
+#define GL_MAX_FRAGMENT_ATOMIC_COUNTERS 0x92D6
+#define GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS 0x92D0
+#define GL_MAX_FRAGMENT_IMAGE_UNIFORMS 0x90CE
#define GL_MAX_FRAGMENT_INPUT_COMPONENTS 0x9125
+#define GL_MAX_FRAGMENT_INTERPOLATION_OFFSET 0x8E5C
+#define GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS 0x90DA
#define GL_MAX_FRAGMENT_UNIFORM_BLOCKS 0x8A2D
#define GL_MAX_FRAGMENT_UNIFORM_COMPONENTS 0x8B49
+#define GL_MAX_FRAGMENT_UNIFORM_VECTORS 0x8DFD
+#define GL_MAX_FRAMEBUFFER_HEIGHT 0x9316
+#define GL_MAX_FRAMEBUFFER_LAYERS 0x9317
+#define GL_MAX_FRAMEBUFFER_SAMPLES 0x9318
+#define GL_MAX_FRAMEBUFFER_WIDTH 0x9315
+#define GL_MAX_GEOMETRY_ATOMIC_COUNTERS 0x92D5
+#define GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS 0x92CF
+#define GL_MAX_GEOMETRY_IMAGE_UNIFORMS 0x90CD
#define GL_MAX_GEOMETRY_INPUT_COMPONENTS 0x9123
#define GL_MAX_GEOMETRY_OUTPUT_COMPONENTS 0x9124
#define GL_MAX_GEOMETRY_OUTPUT_VERTICES 0x8DE0
+#define GL_MAX_GEOMETRY_SHADER_INVOCATIONS 0x8E5A
+#define GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS 0x90D7
#define GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS 0x8C29
#define GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS 0x8DE1
#define GL_MAX_GEOMETRY_UNIFORM_BLOCKS 0x8A2C
#define GL_MAX_GEOMETRY_UNIFORM_COMPONENTS 0x8DDF
+#define GL_MAX_HEIGHT 0x827F
+#define GL_MAX_IMAGE_SAMPLES 0x906D
+#define GL_MAX_IMAGE_UNITS 0x8F38
#define GL_MAX_INTEGER_SAMPLES 0x9110
+#define GL_MAX_LABEL_LENGTH 0x82E8
+#define GL_MAX_LAYERS 0x8281
+#define GL_MAX_NAME_LENGTH 0x92F6
+#define GL_MAX_NUM_ACTIVE_VARIABLES 0x92F7
+#define GL_MAX_NUM_COMPATIBLE_SUBROUTINES 0x92F8
+#define GL_MAX_PATCH_VERTICES 0x8E7D
#define GL_MAX_PROGRAM_TEXEL_OFFSET 0x8905
+#define GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET 0x8E5F
#define GL_MAX_RECTANGLE_TEXTURE_SIZE 0x84F8
#define GL_MAX_RENDERBUFFER_SIZE 0x84E8
#define GL_MAX_SAMPLES 0x8D57
#define GL_MAX_SAMPLE_MASK_WORDS 0x8E59
#define GL_MAX_SERVER_WAIT_TIMEOUT 0x9111
+#define GL_MAX_SHADER_STORAGE_BLOCK_SIZE 0x90DE
+#define GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS 0x90DD
+#define GL_MAX_SUBROUTINES 0x8DE7
+#define GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS 0x8DE8
+#define GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS 0x92D3
+#define GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS 0x92CD
+#define GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS 0x90CB
+#define GL_MAX_TESS_CONTROL_INPUT_COMPONENTS 0x886C
+#define GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS 0x8E83
+#define GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS 0x90D8
+#define GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS 0x8E81
+#define GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS 0x8E85
+#define GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS 0x8E89
+#define GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS 0x8E7F
+#define GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS 0x92D4
+#define GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS 0x92CE
+#define GL_MAX_TESS_EVALUATION_IMAGE_UNIFORMS 0x90CC
+#define GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS 0x886D
+#define GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS 0x8E86
+#define GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS 0x90D9
+#define GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS 0x8E82
+#define GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS 0x8E8A
+#define GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS 0x8E80
+#define GL_MAX_TESS_GEN_LEVEL 0x8E7E
+#define GL_MAX_TESS_PATCH_COMPONENTS 0x8E84
#define GL_MAX_TEXTURE_BUFFER_SIZE 0x8C2B
#define GL_MAX_TEXTURE_IMAGE_UNITS 0x8872
#define GL_MAX_TEXTURE_LOD_BIAS 0x84FD
#define GL_MAX_TEXTURE_SIZE 0x0D33
+#define GL_MAX_TRANSFORM_FEEDBACK_BUFFERS 0x8E70
#define GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS 0x8C8A
#define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS 0x8C8B
#define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS 0x8C80
#define GL_MAX_UNIFORM_BLOCK_SIZE 0x8A30
#define GL_MAX_UNIFORM_BUFFER_BINDINGS 0x8A2F
+#define GL_MAX_UNIFORM_LOCATIONS 0x826E
#define GL_MAX_VARYING_COMPONENTS 0x8B4B
#define GL_MAX_VARYING_FLOATS 0x8B4B
+#define GL_MAX_VARYING_VECTORS 0x8DFC
+#define GL_MAX_VERTEX_ATOMIC_COUNTERS 0x92D2
+#define GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS 0x92CC
#define GL_MAX_VERTEX_ATTRIBS 0x8869
+#define GL_MAX_VERTEX_ATTRIB_BINDINGS 0x82DA
+#define GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET 0x82D9
+#define GL_MAX_VERTEX_IMAGE_UNIFORMS 0x90CA
#define GL_MAX_VERTEX_OUTPUT_COMPONENTS 0x9122
+#define GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS 0x90D6
+#define GL_MAX_VERTEX_STREAMS 0x8E71
#define GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS 0x8B4C
#define GL_MAX_VERTEX_UNIFORM_BLOCKS 0x8A2B
#define GL_MAX_VERTEX_UNIFORM_COMPONENTS 0x8B4A
+#define GL_MAX_VERTEX_UNIFORM_VECTORS 0x8DFB
+#define GL_MAX_VIEWPORTS 0x825B
#define GL_MAX_VIEWPORT_DIMS 0x0D3A
+#define GL_MAX_WIDTH 0x827E
+#define GL_MEDIUM_FLOAT 0x8DF1
+#define GL_MEDIUM_INT 0x8DF4
#define GL_MIN 0x8007
#define GL_MINOR_VERSION 0x821C
+#define GL_MIN_FRAGMENT_INTERPOLATION_OFFSET 0x8E5B
+#define GL_MIN_MAP_BUFFER_ALIGNMENT 0x90BC
#define GL_MIN_PROGRAM_TEXEL_OFFSET 0x8904
+#define GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET 0x8E5E
+#define GL_MIN_SAMPLE_SHADING_VALUE 0x8C37
+#define GL_MIPMAP 0x8293
#define GL_MIRRORED_REPEAT 0x8370
#define GL_MULTISAMPLE 0x809D
+#define GL_NAME_LENGTH 0x92F9
#define GL_NAND 0x150E
#define GL_NEAREST 0x2600
#define GL_NEAREST_MIPMAP_LINEAR 0x2702
@@ -549,9 +862,16 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_NOR 0x1508
#define GL_NOTEQUAL 0x0205
#define GL_NO_ERROR 0
+#define GL_NUM_ACTIVE_VARIABLES 0x9304
+#define GL_NUM_COMPATIBLE_SUBROUTINES 0x8E4A
#define GL_NUM_COMPRESSED_TEXTURE_FORMATS 0x86A2
#define GL_NUM_EXTENSIONS 0x821D
+#define GL_NUM_PROGRAM_BINARY_FORMATS 0x87FE
+#define GL_NUM_SAMPLE_COUNTS 0x9380
+#define GL_NUM_SHADER_BINARY_FORMATS 0x8DF9
+#define GL_NUM_SHADING_LANGUAGE_VERSIONS 0x82E9
#define GL_OBJECT_TYPE 0x9112
+#define GL_OFFSET 0x92FC
#define GL_ONE 1
#define GL_ONE_MINUS_CONSTANT_ALPHA 0x8004
#define GL_ONE_MINUS_CONSTANT_COLOR 0x8002
@@ -566,6 +886,10 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_OR_REVERSE 0x150B
#define GL_OUT_OF_MEMORY 0x0505
#define GL_PACK_ALIGNMENT 0x0D05
+#define GL_PACK_COMPRESSED_BLOCK_DEPTH 0x912D
+#define GL_PACK_COMPRESSED_BLOCK_HEIGHT 0x912C
+#define GL_PACK_COMPRESSED_BLOCK_SIZE 0x912E
+#define GL_PACK_COMPRESSED_BLOCK_WIDTH 0x912B
#define GL_PACK_IMAGE_HEIGHT 0x806C
#define GL_PACK_LSB_FIRST 0x0D01
#define GL_PACK_ROW_LENGTH 0x0D02
@@ -573,6 +897,11 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_PACK_SKIP_PIXELS 0x0D04
#define GL_PACK_SKIP_ROWS 0x0D03
#define GL_PACK_SWAP_BYTES 0x0D00
+#define GL_PATCHES 0x000E
+#define GL_PATCH_DEFAULT_INNER_LEVEL 0x8E73
+#define GL_PATCH_DEFAULT_OUTER_LEVEL 0x8E74
+#define GL_PATCH_VERTICES 0x8E72
+#define GL_PIXEL_BUFFER_BARRIER_BIT 0x00000080
#define GL_PIXEL_PACK_BUFFER 0x88EB
#define GL_PIXEL_PACK_BUFFER_BINDING 0x88ED
#define GL_PIXEL_UNPACK_BUFFER 0x88EC
@@ -594,8 +923,18 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_POLYGON_SMOOTH_HINT 0x0C53
#define GL_PRIMITIVES_GENERATED 0x8C87
#define GL_PRIMITIVE_RESTART 0x8F9D
+#define GL_PRIMITIVE_RESTART_FIXED_INDEX 0x8D69
#define GL_PRIMITIVE_RESTART_INDEX 0x8F9E
+#define GL_PROGRAM 0x82E2
+#define GL_PROGRAM_BINARY_FORMATS 0x87FF
+#define GL_PROGRAM_BINARY_LENGTH 0x8741
+#define GL_PROGRAM_BINARY_RETRIEVABLE_HINT 0x8257
+#define GL_PROGRAM_INPUT 0x92E3
+#define GL_PROGRAM_OUTPUT 0x92E4
+#define GL_PROGRAM_PIPELINE 0x82E4
+#define GL_PROGRAM_PIPELINE_BINDING 0x825A
#define GL_PROGRAM_POINT_SIZE 0x8642
+#define GL_PROGRAM_SEPARABLE 0x8258
#define GL_PROVOKING_VERTEX 0x8E4F
#define GL_PROXY_TEXTURE_1D 0x8063
#define GL_PROXY_TEXTURE_1D_ARRAY 0x8C19
@@ -605,8 +944,11 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_PROXY_TEXTURE_2D_MULTISAMPLE_ARRAY 0x9103
#define GL_PROXY_TEXTURE_3D 0x8070
#define GL_PROXY_TEXTURE_CUBE_MAP 0x851B
+#define GL_PROXY_TEXTURE_CUBE_MAP_ARRAY 0x900B
#define GL_PROXY_TEXTURE_RECTANGLE 0x84F7
+#define GL_QUADS 0x0007
#define GL_QUADS_FOLLOW_PROVOKING_VERTEX_CONVENTION 0x8E4C
+#define GL_QUERY 0x82E3
#define GL_QUERY_BY_REGION_NO_WAIT 0x8E16
#define GL_QUERY_BY_REGION_WAIT 0x8E15
#define GL_QUERY_COUNTER_BITS 0x8864
@@ -633,9 +975,18 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_READ_FRAMEBUFFER 0x8CA8
#define GL_READ_FRAMEBUFFER_BINDING 0x8CAA
#define GL_READ_ONLY 0x88B8
+#define GL_READ_PIXELS 0x828C
+#define GL_READ_PIXELS_FORMAT 0x828D
+#define GL_READ_PIXELS_TYPE 0x828E
#define GL_READ_WRITE 0x88BA
#define GL_RED 0x1903
#define GL_RED_INTEGER 0x8D94
+#define GL_REFERENCED_BY_COMPUTE_SHADER 0x930B
+#define GL_REFERENCED_BY_FRAGMENT_SHADER 0x930A
+#define GL_REFERENCED_BY_GEOMETRY_SHADER 0x9309
+#define GL_REFERENCED_BY_TESS_CONTROL_SHADER 0x9307
+#define GL_REFERENCED_BY_TESS_EVALUATION_SHADER 0x9308
+#define GL_REFERENCED_BY_VERTEX_SHADER 0x9306
#define GL_RENDERBUFFER 0x8D41
#define GL_RENDERBUFFER_ALPHA_SIZE 0x8D53
#define GL_RENDERBUFFER_BINDING 0x8CA7
@@ -679,6 +1030,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_RGB32UI 0x8D71
#define GL_RGB4 0x804F
#define GL_RGB5 0x8050
+#define GL_RGB565 0x8D62
#define GL_RGB5_A1 0x8057
#define GL_RGB8 0x8051
#define GL_RGB8I 0x8D8F
@@ -705,6 +1057,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_RGB_INTEGER 0x8D98
#define GL_RG_INTEGER 0x8228
#define GL_RIGHT 0x0407
+#define GL_SAMPLER 0x82E6
#define GL_SAMPLER_1D 0x8B5D
#define GL_SAMPLER_1D_ARRAY 0x8DC0
#define GL_SAMPLER_1D_ARRAY_SHADOW 0x8DC3
@@ -721,6 +1074,8 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_SAMPLER_BINDING 0x8919
#define GL_SAMPLER_BUFFER 0x8DC2
#define GL_SAMPLER_CUBE 0x8B60
+#define GL_SAMPLER_CUBE_MAP_ARRAY 0x900C
+#define GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW 0x900D
#define GL_SAMPLER_CUBE_SHADOW 0x8DC5
#define GL_SAMPLES 0x80A9
#define GL_SAMPLES_PASSED 0x8914
@@ -733,16 +1088,35 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_SAMPLE_MASK 0x8E51
#define GL_SAMPLE_MASK_VALUE 0x8E52
#define GL_SAMPLE_POSITION 0x8E50
+#define GL_SAMPLE_SHADING 0x8C36
#define GL_SCISSOR_BOX 0x0C10
#define GL_SCISSOR_TEST 0x0C11
#define GL_SEPARATE_ATTRIBS 0x8C8D
#define GL_SET 0x150F
+#define GL_SHADER 0x82E1
+#define GL_SHADER_BINARY_FORMATS 0x8DF8
+#define GL_SHADER_COMPILER 0x8DFA
+#define GL_SHADER_IMAGE_ACCESS_BARRIER_BIT 0x00000020
+#define GL_SHADER_IMAGE_ATOMIC 0x82A6
+#define GL_SHADER_IMAGE_LOAD 0x82A4
+#define GL_SHADER_IMAGE_STORE 0x82A5
#define GL_SHADER_SOURCE_LENGTH 0x8B88
+#define GL_SHADER_STORAGE_BARRIER_BIT 0x00002000
+#define GL_SHADER_STORAGE_BLOCK 0x92E6
+#define GL_SHADER_STORAGE_BUFFER 0x90D2
+#define GL_SHADER_STORAGE_BUFFER_BINDING 0x90D3
+#define GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT 0x90DF
+#define GL_SHADER_STORAGE_BUFFER_SIZE 0x90D5
+#define GL_SHADER_STORAGE_BUFFER_START 0x90D4
#define GL_SHADER_TYPE 0x8B4F
#define GL_SHADING_LANGUAGE_VERSION 0x8B8C
#define GL_SHORT 0x1402
#define GL_SIGNALED 0x9119
#define GL_SIGNED_NORMALIZED 0x8F9C
+#define GL_SIMULTANEOUS_TEXTURE_AND_DEPTH_TEST 0x82AC
+#define GL_SIMULTANEOUS_TEXTURE_AND_DEPTH_WRITE 0x82AE
+#define GL_SIMULTANEOUS_TEXTURE_AND_STENCIL_TEST 0x82AD
+#define GL_SIMULTANEOUS_TEXTURE_AND_STENCIL_WRITE 0x82AF
#define GL_SMOOTH_LINE_WIDTH_GRANULARITY 0x0B23
#define GL_SMOOTH_LINE_WIDTH_RANGE 0x0B22
#define GL_SMOOTH_POINT_SIZE_GRANULARITY 0x0B13
@@ -756,6 +1130,10 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_SRGB8 0x8C41
#define GL_SRGB8_ALPHA8 0x8C43
#define GL_SRGB_ALPHA 0x8C42
+#define GL_SRGB_READ 0x8297
+#define GL_SRGB_WRITE 0x8298
+#define GL_STACK_OVERFLOW 0x0503
+#define GL_STACK_UNDERFLOW 0x0504
#define GL_STATIC_COPY 0x88E6
#define GL_STATIC_DRAW 0x88E4
#define GL_STATIC_READ 0x88E5
@@ -770,6 +1148,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_STENCIL_BACK_WRITEMASK 0x8CA5
#define GL_STENCIL_BUFFER_BIT 0x00000400
#define GL_STENCIL_CLEAR_VALUE 0x0B91
+#define GL_STENCIL_COMPONENTS 0x8285
#define GL_STENCIL_FAIL 0x0B94
#define GL_STENCIL_FUNC 0x0B92
#define GL_STENCIL_INDEX 0x1901
@@ -780,6 +1159,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_STENCIL_PASS_DEPTH_FAIL 0x0B95
#define GL_STENCIL_PASS_DEPTH_PASS 0x0B96
#define GL_STENCIL_REF 0x0B97
+#define GL_STENCIL_RENDERABLE 0x8288
#define GL_STENCIL_TEST 0x0B90
#define GL_STENCIL_VALUE_MASK 0x0B93
#define GL_STENCIL_WRITEMASK 0x0B98
@@ -794,6 +1174,21 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
#define GL_SYNC_STATUS 0x9114
+#define GL_TESS_CONTROL_OUTPUT_VERTICES 0x8E75
+#define GL_TESS_CONTROL_SHADER 0x8E88
+#define GL_TESS_CONTROL_SHADER_BIT 0x00000008
+#define GL_TESS_CONTROL_SUBROUTINE 0x92E9
+#define GL_TESS_CONTROL_SUBROUTINE_UNIFORM 0x92EF
+#define GL_TESS_CONTROL_TEXTURE 0x829C
+#define GL_TESS_EVALUATION_SHADER 0x8E87
+#define GL_TESS_EVALUATION_SHADER_BIT 0x00000010
+#define GL_TESS_EVALUATION_SUBROUTINE 0x92EA
+#define GL_TESS_EVALUATION_SUBROUTINE_UNIFORM 0x92F0
+#define GL_TESS_EVALUATION_TEXTURE 0x829D
+#define GL_TESS_GEN_MODE 0x8E76
+#define GL_TESS_GEN_POINT_MODE 0x8E79
+#define GL_TESS_GEN_SPACING 0x8E77
+#define GL_TESS_GEN_VERTEX_ORDER 0x8E78
#define GL_TEXTURE 0x1702
#define GL_TEXTURE0 0x84C0
#define GL_TEXTURE1 0x84C1
@@ -846,18 +1241,26 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_TEXTURE_BINDING_3D 0x806A
#define GL_TEXTURE_BINDING_BUFFER 0x8C2C
#define GL_TEXTURE_BINDING_CUBE_MAP 0x8514
+#define GL_TEXTURE_BINDING_CUBE_MAP_ARRAY 0x900A
#define GL_TEXTURE_BINDING_RECTANGLE 0x84F6
#define GL_TEXTURE_BLUE_SIZE 0x805E
#define GL_TEXTURE_BLUE_TYPE 0x8C12
#define GL_TEXTURE_BORDER_COLOR 0x1004
#define GL_TEXTURE_BUFFER 0x8C2A
#define GL_TEXTURE_BUFFER_DATA_STORE_BINDING 0x8C2D
+#define GL_TEXTURE_BUFFER_OFFSET 0x919D
+#define GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT 0x919F
+#define GL_TEXTURE_BUFFER_SIZE 0x919E
#define GL_TEXTURE_COMPARE_FUNC 0x884D
#define GL_TEXTURE_COMPARE_MODE 0x884C
#define GL_TEXTURE_COMPRESSED 0x86A1
+#define GL_TEXTURE_COMPRESSED_BLOCK_HEIGHT 0x82B2
+#define GL_TEXTURE_COMPRESSED_BLOCK_SIZE 0x82B3
+#define GL_TEXTURE_COMPRESSED_BLOCK_WIDTH 0x82B1
#define GL_TEXTURE_COMPRESSED_IMAGE_SIZE 0x86A0
#define GL_TEXTURE_COMPRESSION_HINT 0x84EF
#define GL_TEXTURE_CUBE_MAP 0x8513
+#define GL_TEXTURE_CUBE_MAP_ARRAY 0x9009
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x8516
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x8518
#define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x851A
@@ -868,10 +1271,17 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_TEXTURE_DEPTH 0x8071
#define GL_TEXTURE_DEPTH_SIZE 0x884A
#define GL_TEXTURE_DEPTH_TYPE 0x8C16
+#define GL_TEXTURE_FETCH_BARRIER_BIT 0x00000008
#define GL_TEXTURE_FIXED_SAMPLE_LOCATIONS 0x9107
+#define GL_TEXTURE_GATHER 0x82A2
+#define GL_TEXTURE_GATHER_SHADOW 0x82A3
#define GL_TEXTURE_GREEN_SIZE 0x805D
#define GL_TEXTURE_GREEN_TYPE 0x8C11
#define GL_TEXTURE_HEIGHT 0x1001
+#define GL_TEXTURE_IMAGE_FORMAT 0x828F
+#define GL_TEXTURE_IMAGE_TYPE 0x8290
+#define GL_TEXTURE_IMMUTABLE_FORMAT 0x912F
+#define GL_TEXTURE_IMMUTABLE_LEVELS 0x82DF
#define GL_TEXTURE_INTERNAL_FORMAT 0x1003
#define GL_TEXTURE_LOD_BIAS 0x8501
#define GL_TEXTURE_MAG_FILTER 0x2800
@@ -883,6 +1293,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_TEXTURE_RED_SIZE 0x805C
#define GL_TEXTURE_RED_TYPE 0x8C10
#define GL_TEXTURE_SAMPLES 0x9106
+#define GL_TEXTURE_SHADOW 0x82A1
#define GL_TEXTURE_SHARED_SIZE 0x8C3F
#define GL_TEXTURE_STENCIL_SIZE 0x88F1
#define GL_TEXTURE_SWIZZLE_A 0x8E45
@@ -890,6 +1301,12 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_TEXTURE_SWIZZLE_G 0x8E43
#define GL_TEXTURE_SWIZZLE_R 0x8E42
#define GL_TEXTURE_SWIZZLE_RGBA 0x8E46
+#define GL_TEXTURE_UPDATE_BARRIER_BIT 0x00000100
+#define GL_TEXTURE_VIEW 0x82B5
+#define GL_TEXTURE_VIEW_MIN_LAYER 0x82DD
+#define GL_TEXTURE_VIEW_MIN_LEVEL 0x82DB
+#define GL_TEXTURE_VIEW_NUM_LAYERS 0x82DE
+#define GL_TEXTURE_VIEW_NUM_LEVELS 0x82DC
#define GL_TEXTURE_WIDTH 0x1000
#define GL_TEXTURE_WRAP_R 0x8072
#define GL_TEXTURE_WRAP_S 0x2802
@@ -898,12 +1315,22 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_TIMEOUT_IGNORED 0xFFFFFFFFFFFFFFFF
#define GL_TIMESTAMP 0x8E28
#define GL_TIME_ELAPSED 0x88BF
+#define GL_TOP_LEVEL_ARRAY_SIZE 0x930C
+#define GL_TOP_LEVEL_ARRAY_STRIDE 0x930D
+#define GL_TRANSFORM_FEEDBACK 0x8E22
+#define GL_TRANSFORM_FEEDBACK_ACTIVE 0x8E24
+#define GL_TRANSFORM_FEEDBACK_BARRIER_BIT 0x00000800
+#define GL_TRANSFORM_FEEDBACK_BINDING 0x8E25
#define GL_TRANSFORM_FEEDBACK_BUFFER 0x8C8E
+#define GL_TRANSFORM_FEEDBACK_BUFFER_ACTIVE 0x8E24
#define GL_TRANSFORM_FEEDBACK_BUFFER_BINDING 0x8C8F
#define GL_TRANSFORM_FEEDBACK_BUFFER_MODE 0x8C7F
+#define GL_TRANSFORM_FEEDBACK_BUFFER_PAUSED 0x8E23
#define GL_TRANSFORM_FEEDBACK_BUFFER_SIZE 0x8C85
#define GL_TRANSFORM_FEEDBACK_BUFFER_START 0x8C84
+#define GL_TRANSFORM_FEEDBACK_PAUSED 0x8E23
#define GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN 0x8C88
+#define GL_TRANSFORM_FEEDBACK_VARYING 0x92F4
#define GL_TRANSFORM_FEEDBACK_VARYINGS 0x8C83
#define GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH 0x8C76
#define GL_TRIANGLES 0x0004
@@ -912,15 +1339,24 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_TRIANGLE_STRIP 0x0005
#define GL_TRIANGLE_STRIP_ADJACENCY 0x000D
#define GL_TRUE 1
+#define GL_TYPE 0x92FA
+#define GL_UNDEFINED_VERTEX 0x8260
+#define GL_UNIFORM 0x92E1
#define GL_UNIFORM_ARRAY_STRIDE 0x8A3C
+#define GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX 0x92DA
+#define GL_UNIFORM_BARRIER_BIT 0x00000004
+#define GL_UNIFORM_BLOCK 0x92E2
#define GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS 0x8A42
#define GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES 0x8A43
#define GL_UNIFORM_BLOCK_BINDING 0x8A3F
#define GL_UNIFORM_BLOCK_DATA_SIZE 0x8A40
#define GL_UNIFORM_BLOCK_INDEX 0x8A3A
#define GL_UNIFORM_BLOCK_NAME_LENGTH 0x8A41
+#define GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER 0x90EC
#define GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER 0x8A46
#define GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER 0x8A45
+#define GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER 0x84F0
+#define GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER 0x84F1
#define GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER 0x8A44
#define GL_UNIFORM_BUFFER 0x8A11
#define GL_UNIFORM_BUFFER_BINDING 0x8A28
@@ -934,6 +1370,10 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_UNIFORM_SIZE 0x8A38
#define GL_UNIFORM_TYPE 0x8A37
#define GL_UNPACK_ALIGNMENT 0x0CF5
+#define GL_UNPACK_COMPRESSED_BLOCK_DEPTH 0x9129
+#define GL_UNPACK_COMPRESSED_BLOCK_HEIGHT 0x9128
+#define GL_UNPACK_COMPRESSED_BLOCK_SIZE 0x912A
+#define GL_UNPACK_COMPRESSED_BLOCK_WIDTH 0x9127
#define GL_UNPACK_IMAGE_HEIGHT 0x806E
#define GL_UNPACK_LSB_FIRST 0x0CF1
#define GL_UNPACK_ROW_LENGTH 0x0CF2
@@ -953,6 +1393,18 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_UNSIGNED_INT_5_9_9_9_REV 0x8C3E
#define GL_UNSIGNED_INT_8_8_8_8 0x8035
#define GL_UNSIGNED_INT_8_8_8_8_REV 0x8367
+#define GL_UNSIGNED_INT_ATOMIC_COUNTER 0x92DB
+#define GL_UNSIGNED_INT_IMAGE_1D 0x9062
+#define GL_UNSIGNED_INT_IMAGE_1D_ARRAY 0x9068
+#define GL_UNSIGNED_INT_IMAGE_2D 0x9063
+#define GL_UNSIGNED_INT_IMAGE_2D_ARRAY 0x9069
+#define GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE 0x906B
+#define GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x906C
+#define GL_UNSIGNED_INT_IMAGE_2D_RECT 0x9065
+#define GL_UNSIGNED_INT_IMAGE_3D 0x9064
+#define GL_UNSIGNED_INT_IMAGE_BUFFER 0x9067
+#define GL_UNSIGNED_INT_IMAGE_CUBE 0x9066
+#define GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY 0x906A
#define GL_UNSIGNED_INT_SAMPLER_1D 0x8DD1
#define GL_UNSIGNED_INT_SAMPLER_1D_ARRAY 0x8DD6
#define GL_UNSIGNED_INT_SAMPLER_2D 0x8DD2
@@ -963,6 +1415,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_UNSIGNED_INT_SAMPLER_3D 0x8DD3
#define GL_UNSIGNED_INT_SAMPLER_BUFFER 0x8DD8
#define GL_UNSIGNED_INT_SAMPLER_CUBE 0x8DD4
+#define GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY 0x900F
#define GL_UNSIGNED_INT_VEC2 0x8DC6
#define GL_UNSIGNED_INT_VEC3 0x8DC7
#define GL_UNSIGNED_INT_VEC4 0x8DC8
@@ -978,19 +1431,52 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro
#define GL_VALIDATE_STATUS 0x8B83
#define GL_VENDOR 0x1F00
#define GL_VERSION 0x1F02
+#define GL_VERTEX_ARRAY 0x8074
#define GL_VERTEX_ARRAY_BINDING 0x85B5
+#define GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT 0x00000001
#define GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING 0x889F
#define GL_VERTEX_ATTRIB_ARRAY_DIVISOR 0x88FE
#define GL_VERTEX_ATTRIB_ARRAY_ENABLED 0x8622
#define GL_VERTEX_ATTRIB_ARRAY_INTEGER 0x88FD
+#define GL_VERTEX_ATTRIB_ARRAY_LONG 0x874E
#define GL_VERTEX_ATTRIB_ARRAY_NORMALIZED 0x886A
#define GL_VERTEX_ATTRIB_ARRAY_POINTER 0x8645
#define GL_VERTEX_ATTRIB_ARRAY_SIZE 0x8623
#define GL_VERTEX_ATTRIB_ARRAY_STRIDE 0x8624
#define GL_VERTEX_ATTRIB_ARRAY_TYPE 0x8625
+#define GL_VERTEX_ATTRIB_BINDING 0x82D4
+#define GL_VERTEX_ATTRIB_RELATIVE_OFFSET 0x82D5
+#define GL_VERTEX_BINDING_BUFFER 0x8F4F
+#define GL_VERTEX_BINDING_DIVISOR 0x82D6
+#define GL_VERTEX_BINDING_OFFSET 0x82D7
+#define GL_VERTEX_BINDING_STRIDE 0x82D8
#define GL_VERTEX_PROGRAM_POINT_SIZE 0x8642
#define GL_VERTEX_SHADER 0x8B31
+#define GL_VERTEX_SHADER_BIT 0x00000001
+#define GL_VERTEX_SUBROUTINE 0x92E8
+#define GL_VERTEX_SUBROUTINE_UNIFORM 0x92EE
+#define GL_VERTEX_TEXTURE 0x829B
#define GL_VIEWPORT 0x0BA2
+#define GL_VIEWPORT_BOUNDS_RANGE 0x825D
+#define GL_VIEWPORT_INDEX_PROVOKING_VERTEX 0x825F
+#define GL_VIEWPORT_SUBPIXEL_BITS 0x825C
+#define GL_VIEW_CLASS_128_BITS 0x82C4
+#define GL_VIEW_CLASS_16_BITS 0x82CA
+#define GL_VIEW_CLASS_24_BITS 0x82C9
+#define GL_VIEW_CLASS_32_BITS 0x82C8
+#define GL_VIEW_CLASS_48_BITS 0x82C7
+#define GL_VIEW_CLASS_64_BITS 0x82C6
+#define GL_VIEW_CLASS_8_BITS 0x82CB
+#define GL_VIEW_CLASS_96_BITS 0x82C5
+#define GL_VIEW_CLASS_BPTC_FLOAT 0x82D3
+#define GL_VIEW_CLASS_BPTC_UNORM 0x82D2
+#define GL_VIEW_CLASS_RGTC1_RED 0x82D0
+#define GL_VIEW_CLASS_RGTC2_RG 0x82D1
+#define GL_VIEW_CLASS_S3TC_DXT1_RGB 0x82CC
+#define GL_VIEW_CLASS_S3TC_DXT1_RGBA 0x82CD
+#define GL_VIEW_CLASS_S3TC_DXT3_RGBA 0x82CE
+#define GL_VIEW_CLASS_S3TC_DXT5_RGBA 0x82CF
+#define GL_VIEW_COMPATIBILITY_CLASS 0x82B6
#define GL_WAIT_FAILED 0x911D
#define GL_WRITE_ONLY 0x88B9
#define GL_XOR 0x1506
@@ -1074,12 +1560,18 @@ typedef void (GLAD_API_PTR *GLVULKANPROCNV)(void);
#define GL_VERSION_3_1 1
#define GL_VERSION_3_2 1
#define GL_VERSION_3_3 1
+#define GL_VERSION_4_0 1
+#define GL_VERSION_4_1 1
+#define GL_VERSION_4_2 1
+#define GL_VERSION_4_3 1
+typedef void (GLAD_API_PTR *PFNGLACTIVESHADERPROGRAMPROC)(GLuint pipeline, GLuint program);
typedef void (GLAD_API_PTR *PFNGLACTIVETEXTUREPROC)(GLenum texture);
typedef void (GLAD_API_PTR *PFNGLATTACHSHADERPROC)(GLuint program, GLuint shader);
typedef void (GLAD_API_PTR *PFNGLBEGINCONDITIONALRENDERPROC)(GLuint id, GLenum mode);
typedef void (GLAD_API_PTR *PFNGLBEGINQUERYPROC)(GLenum target, GLuint id);
+typedef void (GLAD_API_PTR *PFNGLBEGINQUERYINDEXEDPROC)(GLenum target, GLuint index, GLuint id);
typedef void (GLAD_API_PTR *PFNGLBEGINTRANSFORMFEEDBACKPROC)(GLenum primitiveMode);
typedef void (GLAD_API_PTR *PFNGLBINDATTRIBLOCATIONPROC)(GLuint program, GLuint index, const GLchar * name);
typedef void (GLAD_API_PTR *PFNGLBINDBUFFERPROC)(GLenum target, GLuint buffer);
@@ -1088,27 +1580,38 @@ typedef void (GLAD_API_PTR *PFNGLBINDBUFFERRANGEPROC)(GLenum target, GLuint inde
typedef void (GLAD_API_PTR *PFNGLBINDFRAGDATALOCATIONPROC)(GLuint program, GLuint color, const GLchar * name);
typedef void (GLAD_API_PTR *PFNGLBINDFRAGDATALOCATIONINDEXEDPROC)(GLuint program, GLuint colorNumber, GLuint index, const GLchar * name);
typedef void (GLAD_API_PTR *PFNGLBINDFRAMEBUFFERPROC)(GLenum target, GLuint framebuffer);
+typedef void (GLAD_API_PTR *PFNGLBINDIMAGETEXTUREPROC)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format);
+typedef void (GLAD_API_PTR *PFNGLBINDPROGRAMPIPELINEPROC)(GLuint pipeline);
typedef void (GLAD_API_PTR *PFNGLBINDRENDERBUFFERPROC)(GLenum target, GLuint renderbuffer);
typedef void (GLAD_API_PTR *PFNGLBINDSAMPLERPROC)(GLuint unit, GLuint sampler);
typedef void (GLAD_API_PTR *PFNGLBINDTEXTUREPROC)(GLenum target, GLuint texture);
+typedef void (GLAD_API_PTR *PFNGLBINDTRANSFORMFEEDBACKPROC)(GLenum target, GLuint id);
typedef void (GLAD_API_PTR *PFNGLBINDVERTEXARRAYPROC)(GLuint array);
+typedef void (GLAD_API_PTR *PFNGLBINDVERTEXBUFFERPROC)(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride);
typedef void (GLAD_API_PTR *PFNGLBLENDCOLORPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
typedef void (GLAD_API_PTR *PFNGLBLENDEQUATIONPROC)(GLenum mode);
typedef void (GLAD_API_PTR *PFNGLBLENDEQUATIONSEPARATEPROC)(GLenum modeRGB, GLenum modeAlpha);
+typedef void (GLAD_API_PTR *PFNGLBLENDEQUATIONSEPARATEIPROC)(GLuint buf, GLenum modeRGB, GLenum modeAlpha);
+typedef void (GLAD_API_PTR *PFNGLBLENDEQUATIONIPROC)(GLuint buf, GLenum mode);
typedef void (GLAD_API_PTR *PFNGLBLENDFUNCPROC)(GLenum sfactor, GLenum dfactor);
typedef void (GLAD_API_PTR *PFNGLBLENDFUNCSEPARATEPROC)(GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha);
+typedef void (GLAD_API_PTR *PFNGLBLENDFUNCSEPARATEIPROC)(GLuint buf, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha);
+typedef void (GLAD_API_PTR *PFNGLBLENDFUNCIPROC)(GLuint buf, GLenum src, GLenum dst);
typedef void (GLAD_API_PTR *PFNGLBLITFRAMEBUFFERPROC)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
typedef void (GLAD_API_PTR *PFNGLBUFFERDATAPROC)(GLenum target, GLsizeiptr size, const void * data, GLenum usage);
typedef void (GLAD_API_PTR *PFNGLBUFFERSUBDATAPROC)(GLenum target, GLintptr offset, GLsizeiptr size, const void * data);
typedef GLenum (GLAD_API_PTR *PFNGLCHECKFRAMEBUFFERSTATUSPROC)(GLenum target);
typedef void (GLAD_API_PTR *PFNGLCLAMPCOLORPROC)(GLenum target, GLenum clamp);
typedef void (GLAD_API_PTR *PFNGLCLEARPROC)(GLbitfield mask);
+typedef void (GLAD_API_PTR *PFNGLCLEARBUFFERDATAPROC)(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void * data);
+typedef void (GLAD_API_PTR *PFNGLCLEARBUFFERSUBDATAPROC)(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void * data);
typedef void (GLAD_API_PTR *PFNGLCLEARBUFFERFIPROC)(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
typedef void (GLAD_API_PTR *PFNGLCLEARBUFFERFVPROC)(GLenum buffer, GLint drawbuffer, const GLfloat * value);
typedef void (GLAD_API_PTR *PFNGLCLEARBUFFERIVPROC)(GLenum buffer, GLint drawbuffer, const GLint * value);
typedef void (GLAD_API_PTR *PFNGLCLEARBUFFERUIVPROC)(GLenum buffer, GLint drawbuffer, const GLuint * value);
typedef void (GLAD_API_PTR *PFNGLCLEARCOLORPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha);
typedef void (GLAD_API_PTR *PFNGLCLEARDEPTHPROC)(GLdouble depth);
+typedef void (GLAD_API_PTR *PFNGLCLEARDEPTHFPROC)(GLfloat d);
typedef void (GLAD_API_PTR *PFNGLCLEARSTENCILPROC)(GLint s);
typedef GLenum (GLAD_API_PTR *PFNGLCLIENTWAITSYNCPROC)(GLsync sync, GLbitfield flags, GLuint64 timeout);
typedef void (GLAD_API_PTR *PFNGLCOLORMASKPROC)(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha);
@@ -1121,6 +1624,7 @@ typedef void (GLAD_API_PTR *PFNGLCOMPRESSEDTEXSUBIMAGE1DPROC)(GLenum target, GLi
typedef void (GLAD_API_PTR *PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void * data);
typedef void (GLAD_API_PTR *PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void * data);
typedef void (GLAD_API_PTR *PFNGLCOPYBUFFERSUBDATAPROC)(GLenum readTarget, GLenum writeTarget, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size);
+typedef void (GLAD_API_PTR *PFNGLCOPYIMAGESUBDATAPROC)(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
typedef void (GLAD_API_PTR *PFNGLCOPYTEXIMAGE1DPROC)(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLint border);
typedef void (GLAD_API_PTR *PFNGLCOPYTEXIMAGE2DPROC)(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border);
typedef void (GLAD_API_PTR *PFNGLCOPYTEXSUBIMAGE1DPROC)(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
@@ -1128,44 +1632,66 @@ typedef void (GLAD_API_PTR *PFNGLCOPYTEXSUBIMAGE2DPROC)(GLenum target, GLint lev
typedef void (GLAD_API_PTR *PFNGLCOPYTEXSUBIMAGE3DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height);
typedef GLuint (GLAD_API_PTR *PFNGLCREATEPROGRAMPROC)(void);
typedef GLuint (GLAD_API_PTR *PFNGLCREATESHADERPROC)(GLenum type);
+typedef GLuint (GLAD_API_PTR *PFNGLCREATESHADERPROGRAMVPROC)(GLenum type, GLsizei count, const GLchar *const* strings);
typedef void (GLAD_API_PTR *PFNGLCULLFACEPROC)(GLenum mode);
+typedef void (GLAD_API_PTR *PFNGLDEBUGMESSAGECALLBACKPROC)(GLDEBUGPROC callback, const void * userParam);
+typedef void (GLAD_API_PTR *PFNGLDEBUGMESSAGECONTROLPROC)(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint * ids, GLboolean enabled);
+typedef void (GLAD_API_PTR *PFNGLDEBUGMESSAGEINSERTPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar * buf);
typedef void (GLAD_API_PTR *PFNGLDELETEBUFFERSPROC)(GLsizei n, const GLuint * buffers);
typedef void (GLAD_API_PTR *PFNGLDELETEFRAMEBUFFERSPROC)(GLsizei n, const GLuint * framebuffers);
typedef void (GLAD_API_PTR *PFNGLDELETEPROGRAMPROC)(GLuint program);
+typedef void (GLAD_API_PTR *PFNGLDELETEPROGRAMPIPELINESPROC)(GLsizei n, const GLuint * pipelines);
typedef void (GLAD_API_PTR *PFNGLDELETEQUERIESPROC)(GLsizei n, const GLuint * ids);
typedef void (GLAD_API_PTR *PFNGLDELETERENDERBUFFERSPROC)(GLsizei n, const GLuint * renderbuffers);
typedef void (GLAD_API_PTR *PFNGLDELETESAMPLERSPROC)(GLsizei count, const GLuint * samplers);
typedef void (GLAD_API_PTR *PFNGLDELETESHADERPROC)(GLuint shader);
typedef void (GLAD_API_PTR *PFNGLDELETESYNCPROC)(GLsync sync);
typedef void (GLAD_API_PTR *PFNGLDELETETEXTURESPROC)(GLsizei n, const GLuint * textures);
+typedef void (GLAD_API_PTR *PFNGLDELETETRANSFORMFEEDBACKSPROC)(GLsizei n, const GLuint * ids);
typedef void (GLAD_API_PTR *PFNGLDELETEVERTEXARRAYSPROC)(GLsizei n, const GLuint * arrays);
typedef void (GLAD_API_PTR *PFNGLDEPTHFUNCPROC)(GLenum func);
typedef void (GLAD_API_PTR *PFNGLDEPTHMASKPROC)(GLboolean flag);
typedef void (GLAD_API_PTR *PFNGLDEPTHRANGEPROC)(GLdouble n, GLdouble f);
+typedef void (GLAD_API_PTR *PFNGLDEPTHRANGEARRAYVPROC)(GLuint first, GLsizei count, const GLdouble * v);
+typedef void (GLAD_API_PTR *PFNGLDEPTHRANGEINDEXEDPROC)(GLuint index, GLdouble n, GLdouble f);
+typedef void (GLAD_API_PTR *PFNGLDEPTHRANGEFPROC)(GLfloat n, GLfloat f);
typedef void (GLAD_API_PTR *PFNGLDETACHSHADERPROC)(GLuint program, GLuint shader);
typedef void (GLAD_API_PTR *PFNGLDISABLEPROC)(GLenum cap);
typedef void (GLAD_API_PTR *PFNGLDISABLEVERTEXATTRIBARRAYPROC)(GLuint index);
typedef void (GLAD_API_PTR *PFNGLDISABLEIPROC)(GLenum target, GLuint index);
+typedef void (GLAD_API_PTR *PFNGLDISPATCHCOMPUTEPROC)(GLuint num_groups_x, GLuint num_groups_y, GLuint num_groups_z);
+typedef void (GLAD_API_PTR *PFNGLDISPATCHCOMPUTEINDIRECTPROC)(GLintptr indirect);
typedef void (GLAD_API_PTR *PFNGLDRAWARRAYSPROC)(GLenum mode, GLint first, GLsizei count);
+typedef void (GLAD_API_PTR *PFNGLDRAWARRAYSINDIRECTPROC)(GLenum mode, const void * indirect);
typedef void (GLAD_API_PTR *PFNGLDRAWARRAYSINSTANCEDPROC)(GLenum mode, GLint first, GLsizei count, GLsizei instancecount);
+typedef void (GLAD_API_PTR *PFNGLDRAWARRAYSINSTANCEDBASEINSTANCEPROC)(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance);
typedef void (GLAD_API_PTR *PFNGLDRAWBUFFERPROC)(GLenum buf);
typedef void (GLAD_API_PTR *PFNGLDRAWBUFFERSPROC)(GLsizei n, const GLenum * bufs);
typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSPROC)(GLenum mode, GLsizei count, GLenum type, const void * indices);
typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSBASEVERTEXPROC)(GLenum mode, GLsizei count, GLenum type, const void * indices, GLint basevertex);
+typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSINDIRECTPROC)(GLenum mode, GLenum type, const void * indirect);
typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDPROC)(GLenum mode, GLsizei count, GLenum type, const void * indices, GLsizei instancecount);
+typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDBASEINSTANCEPROC)(GLenum mode, GLsizei count, GLenum type, const void * indices, GLsizei instancecount, GLuint baseinstance);
typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXPROC)(GLenum mode, GLsizei count, GLenum type, const void * indices, GLsizei instancecount, GLint basevertex);
+typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXBASEINSTANCEPROC)(GLenum mode, GLsizei count, GLenum type, const void * indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance);
typedef void (GLAD_API_PTR *PFNGLDRAWRANGEELEMENTSPROC)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void * indices);
typedef void (GLAD_API_PTR *PFNGLDRAWRANGEELEMENTSBASEVERTEXPROC)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void * indices, GLint basevertex);
+typedef void (GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKPROC)(GLenum mode, GLuint id);
+typedef void (GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKINSTANCEDPROC)(GLenum mode, GLuint id, GLsizei instancecount);
+typedef void (GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKSTREAMPROC)(GLenum mode, GLuint id, GLuint stream);
+typedef void (GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKSTREAMINSTANCEDPROC)(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount);
typedef void (GLAD_API_PTR *PFNGLENABLEPROC)(GLenum cap);
typedef void (GLAD_API_PTR *PFNGLENABLEVERTEXATTRIBARRAYPROC)(GLuint index);
typedef void (GLAD_API_PTR *PFNGLENABLEIPROC)(GLenum target, GLuint index);
typedef void (GLAD_API_PTR *PFNGLENDCONDITIONALRENDERPROC)(void);
typedef void (GLAD_API_PTR *PFNGLENDQUERYPROC)(GLenum target);
+typedef void (GLAD_API_PTR *PFNGLENDQUERYINDEXEDPROC)(GLenum target, GLuint index);
typedef void (GLAD_API_PTR *PFNGLENDTRANSFORMFEEDBACKPROC)(void);
typedef GLsync (GLAD_API_PTR *PFNGLFENCESYNCPROC)(GLenum condition, GLbitfield flags);
typedef void (GLAD_API_PTR *PFNGLFINISHPROC)(void);
typedef void (GLAD_API_PTR *PFNGLFLUSHPROC)(void);
typedef void (GLAD_API_PTR *PFNGLFLUSHMAPPEDBUFFERRANGEPROC)(GLenum target, GLintptr offset, GLsizeiptr length);
+typedef void (GLAD_API_PTR *PFNGLFRAMEBUFFERPARAMETERIPROC)(GLenum target, GLenum pname, GLint param);
typedef void (GLAD_API_PTR *PFNGLFRAMEBUFFERRENDERBUFFERPROC)(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);
typedef void (GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTUREPROC)(GLenum target, GLenum attachment, GLuint texture, GLint level);
typedef void (GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTURE1DPROC)(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level);
@@ -1175,13 +1701,19 @@ typedef void (GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTURELAYERPROC)(GLenum target, GLe
typedef void (GLAD_API_PTR *PFNGLFRONTFACEPROC)(GLenum mode);
typedef void (GLAD_API_PTR *PFNGLGENBUFFERSPROC)(GLsizei n, GLuint * buffers);
typedef void (GLAD_API_PTR *PFNGLGENFRAMEBUFFERSPROC)(GLsizei n, GLuint * framebuffers);
+typedef void (GLAD_API_PTR *PFNGLGENPROGRAMPIPELINESPROC)(GLsizei n, GLuint * pipelines);
typedef void (GLAD_API_PTR *PFNGLGENQUERIESPROC)(GLsizei n, GLuint * ids);
typedef void (GLAD_API_PTR *PFNGLGENRENDERBUFFERSPROC)(GLsizei n, GLuint * renderbuffers);
typedef void (GLAD_API_PTR *PFNGLGENSAMPLERSPROC)(GLsizei count, GLuint * samplers);
typedef void (GLAD_API_PTR *PFNGLGENTEXTURESPROC)(GLsizei n, GLuint * textures);
+typedef void (GLAD_API_PTR *PFNGLGENTRANSFORMFEEDBACKSPROC)(GLsizei n, GLuint * ids);
typedef void (GLAD_API_PTR *PFNGLGENVERTEXARRAYSPROC)(GLsizei n, GLuint * arrays);
typedef void (GLAD_API_PTR *PFNGLGENERATEMIPMAPPROC)(GLenum target);
+typedef void (GLAD_API_PTR *PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC)(GLuint program, GLuint bufferIndex, GLenum pname, GLint * params);
typedef void (GLAD_API_PTR *PFNGLGETACTIVEATTRIBPROC)(GLuint program, GLuint index, GLsizei bufSize, GLsizei * length, GLint * size, GLenum * type, GLchar * name);
+typedef void (GLAD_API_PTR *PFNGLGETACTIVESUBROUTINENAMEPROC)(GLuint program, GLenum shadertype, GLuint index, GLsizei bufSize, GLsizei * length, GLchar * name);
+typedef void (GLAD_API_PTR *PFNGLGETACTIVESUBROUTINEUNIFORMNAMEPROC)(GLuint program, GLenum shadertype, GLuint index, GLsizei bufSize, GLsizei * length, GLchar * name);
+typedef void (GLAD_API_PTR *PFNGLGETACTIVESUBROUTINEUNIFORMIVPROC)(GLuint program, GLenum shadertype, GLuint index, GLenum pname, GLint * values);
typedef void (GLAD_API_PTR *PFNGLGETACTIVEUNIFORMPROC)(GLuint program, GLuint index, GLsizei bufSize, GLsizei * length, GLint * size, GLenum * type, GLchar * name);
typedef void (GLAD_API_PTR *PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC)(GLuint program, GLuint uniformBlockIndex, GLsizei bufSize, GLsizei * length, GLchar * uniformBlockName);
typedef void (GLAD_API_PTR *PFNGLGETACTIVEUNIFORMBLOCKIVPROC)(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint * params);
@@ -1196,19 +1728,39 @@ typedef void (GLAD_API_PTR *PFNGLGETBUFFERPARAMETERIVPROC)(GLenum target, GLenum
typedef void (GLAD_API_PTR *PFNGLGETBUFFERPOINTERVPROC)(GLenum target, GLenum pname, void ** params);
typedef void (GLAD_API_PTR *PFNGLGETBUFFERSUBDATAPROC)(GLenum target, GLintptr offset, GLsizeiptr size, void * data);
typedef void (GLAD_API_PTR *PFNGLGETCOMPRESSEDTEXIMAGEPROC)(GLenum target, GLint level, void * img);
+typedef GLuint (GLAD_API_PTR *PFNGLGETDEBUGMESSAGELOGPROC)(GLuint count, GLsizei bufSize, GLenum * sources, GLenum * types, GLuint * ids, GLenum * severities, GLsizei * lengths, GLchar * messageLog);
+typedef void (GLAD_API_PTR *PFNGLGETDOUBLEI_VPROC)(GLenum target, GLuint index, GLdouble * data);
typedef void (GLAD_API_PTR *PFNGLGETDOUBLEVPROC)(GLenum pname, GLdouble * data);
typedef GLenum (GLAD_API_PTR *PFNGLGETERRORPROC)(void);
+typedef void (GLAD_API_PTR *PFNGLGETFLOATI_VPROC)(GLenum target, GLuint index, GLfloat * data);
typedef void (GLAD_API_PTR *PFNGLGETFLOATVPROC)(GLenum pname, GLfloat * data);
typedef GLint (GLAD_API_PTR *PFNGLGETFRAGDATAINDEXPROC)(GLuint program, const GLchar * name);
typedef GLint (GLAD_API_PTR *PFNGLGETFRAGDATALOCATIONPROC)(GLuint program, const GLchar * name);
typedef void (GLAD_API_PTR *PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC)(GLenum target, GLenum attachment, GLenum pname, GLint * params);
+typedef void (GLAD_API_PTR *PFNGLGETFRAMEBUFFERPARAMETERIVPROC)(GLenum target, GLenum pname, GLint * params);
typedef void (GLAD_API_PTR *PFNGLGETINTEGER64I_VPROC)(GLenum target, GLuint index, GLint64 * data);
typedef void (GLAD_API_PTR *PFNGLGETINTEGER64VPROC)(GLenum pname, GLint64 * data);
typedef void (GLAD_API_PTR *PFNGLGETINTEGERI_VPROC)(GLenum target, GLuint index, GLint * data);
typedef void (GLAD_API_PTR *PFNGLGETINTEGERVPROC)(GLenum pname, GLint * data);
+typedef void (GLAD_API_PTR *PFNGLGETINTERNALFORMATI64VPROC)(GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64 * params);
+typedef void (GLAD_API_PTR *PFNGLGETINTERNALFORMATIVPROC)(GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint * params);
typedef void (GLAD_API_PTR *PFNGLGETMULTISAMPLEFVPROC)(GLenum pname, GLuint index, GLfloat * val);
+typedef void (GLAD_API_PTR *PFNGLGETOBJECTLABELPROC)(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei * length, GLchar * label);
+typedef void (GLAD_API_PTR *PFNGLGETOBJECTPTRLABELPROC)(const void * ptr, GLsizei bufSize, GLsizei * length, GLchar * label);
+typedef void (GLAD_API_PTR *PFNGLGETPOINTERVPROC)(GLenum pname, void ** params);
+typedef void (GLAD_API_PTR *PFNGLGETPROGRAMBINARYPROC)(GLuint program, GLsizei bufSize, GLsizei * length, GLenum * binaryFormat, void * binary);
typedef void (GLAD_API_PTR *PFNGLGETPROGRAMINFOLOGPROC)(GLuint program, GLsizei bufSize, GLsizei * length, GLchar * infoLog);
+typedef void (GLAD_API_PTR *PFNGLGETPROGRAMINTERFACEIVPROC)(GLuint program, GLenum programInterface, GLenum pname, GLint * params);
+typedef void (GLAD_API_PTR *PFNGLGETPROGRAMPIPELINEINFOLOGPROC)(GLuint pipeline, GLsizei bufSize, GLsizei * length, GLchar * infoLog);
+typedef void (GLAD_API_PTR *PFNGLGETPROGRAMPIPELINEIVPROC)(GLuint pipeline, GLenum pname, GLint * params);
+typedef GLuint (GLAD_API_PTR *PFNGLGETPROGRAMRESOURCEINDEXPROC)(GLuint program, GLenum programInterface, const GLchar * name);
+typedef GLint (GLAD_API_PTR *PFNGLGETPROGRAMRESOURCELOCATIONPROC)(GLuint program, GLenum programInterface, const GLchar * name);
+typedef GLint (GLAD_API_PTR *PFNGLGETPROGRAMRESOURCELOCATIONINDEXPROC)(GLuint program, GLenum programInterface, const GLchar * name);
+typedef void (GLAD_API_PTR *PFNGLGETPROGRAMRESOURCENAMEPROC)(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei * length, GLchar * name);
+typedef void (GLAD_API_PTR *PFNGLGETPROGRAMRESOURCEIVPROC)(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount, const GLenum * props, GLsizei count, GLsizei * length, GLint * params);
+typedef void (GLAD_API_PTR *PFNGLGETPROGRAMSTAGEIVPROC)(GLuint program, GLenum shadertype, GLenum pname, GLint * values);
typedef void (GLAD_API_PTR *PFNGLGETPROGRAMIVPROC)(GLuint program, GLenum pname, GLint * params);
+typedef void (GLAD_API_PTR *PFNGLGETQUERYINDEXEDIVPROC)(GLenum target, GLuint index, GLenum pname, GLint * params);
typedef void (GLAD_API_PTR *PFNGLGETQUERYOBJECTI64VPROC)(GLuint id, GLenum pname, GLint64 * params);
typedef void (GLAD_API_PTR *PFNGLGETQUERYOBJECTIVPROC)(GLuint id, GLenum pname, GLint * params);
typedef void (GLAD_API_PTR *PFNGLGETQUERYOBJECTUI64VPROC)(GLuint id, GLenum pname, GLuint64 * params);
@@ -1220,10 +1772,13 @@ typedef void (GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERIUIVPROC)(GLuint sampler, GL
typedef void (GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERFVPROC)(GLuint sampler, GLenum pname, GLfloat * params);
typedef void (GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERIVPROC)(GLuint sampler, GLenum pname, GLint * params);
typedef void (GLAD_API_PTR *PFNGLGETSHADERINFOLOGPROC)(GLuint shader, GLsizei bufSize, GLsizei * length, GLchar * infoLog);
+typedef void (GLAD_API_PTR *PFNGLGETSHADERPRECISIONFORMATPROC)(GLenum shadertype, GLenum precisiontype, GLint * range, GLint * precision);
typedef void (GLAD_API_PTR *PFNGLGETSHADERSOURCEPROC)(GLuint shader, GLsizei bufSize, GLsizei * length, GLchar * source);
typedef void (GLAD_API_PTR *PFNGLGETSHADERIVPROC)(GLuint shader, GLenum pname, GLint * params);
typedef const GLubyte * (GLAD_API_PTR *PFNGLGETSTRINGPROC)(GLenum name);
typedef const GLubyte * (GLAD_API_PTR *PFNGLGETSTRINGIPROC)(GLenum name, GLuint index);
+typedef GLuint (GLAD_API_PTR *PFNGLGETSUBROUTINEINDEXPROC)(GLuint program, GLenum shadertype, const GLchar * name);
+typedef GLint (GLAD_API_PTR *PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC)(GLuint program, GLenum shadertype, const GLchar * name);
typedef void (GLAD_API_PTR *PFNGLGETSYNCIVPROC)(GLsync sync, GLenum pname, GLsizei count, GLsizei * length, GLint * values);
typedef void (GLAD_API_PTR *PFNGLGETTEXIMAGEPROC)(GLenum target, GLint level, GLenum format, GLenum type, void * pixels);
typedef void (GLAD_API_PTR *PFNGLGETTEXLEVELPARAMETERFVPROC)(GLenum target, GLint level, GLenum pname, GLfloat * params);
@@ -1236,36 +1791,56 @@ typedef void (GLAD_API_PTR *PFNGLGETTRANSFORMFEEDBACKVARYINGPROC)(GLuint program
typedef GLuint (GLAD_API_PTR *PFNGLGETUNIFORMBLOCKINDEXPROC)(GLuint program, const GLchar * uniformBlockName);
typedef void (GLAD_API_PTR *PFNGLGETUNIFORMINDICESPROC)(GLuint program, GLsizei uniformCount, const GLchar *const* uniformNames, GLuint * uniformIndices);
typedef GLint (GLAD_API_PTR *PFNGLGETUNIFORMLOCATIONPROC)(GLuint program, const GLchar * name);
+typedef void (GLAD_API_PTR *PFNGLGETUNIFORMSUBROUTINEUIVPROC)(GLenum shadertype, GLint location, GLuint * params);
+typedef void (GLAD_API_PTR *PFNGLGETUNIFORMDVPROC)(GLuint program, GLint location, GLdouble * params);
typedef void (GLAD_API_PTR *PFNGLGETUNIFORMFVPROC)(GLuint program, GLint location, GLfloat * params);
typedef void (GLAD_API_PTR *PFNGLGETUNIFORMIVPROC)(GLuint program, GLint location, GLint * params);
typedef void (GLAD_API_PTR *PFNGLGETUNIFORMUIVPROC)(GLuint program, GLint location, GLuint * params);
typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBIIVPROC)(GLuint index, GLenum pname, GLint * params);
typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBIUIVPROC)(GLuint index, GLenum pname, GLuint * params);
+typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBLDVPROC)(GLuint index, GLenum pname, GLdouble * params);
typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBPOINTERVPROC)(GLuint index, GLenum pname, void ** pointer);
typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBDVPROC)(GLuint index, GLenum pname, GLdouble * params);
typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBFVPROC)(GLuint index, GLenum pname, GLfloat * params);
typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBIVPROC)(GLuint index, GLenum pname, GLint * params);
typedef void (GLAD_API_PTR *PFNGLHINTPROC)(GLenum target, GLenum mode);
+typedef void (GLAD_API_PTR *PFNGLINVALIDATEBUFFERDATAPROC)(GLuint buffer);
+typedef void (GLAD_API_PTR *PFNGLINVALIDATEBUFFERSUBDATAPROC)(GLuint buffer, GLintptr offset, GLsizeiptr length);
+typedef void (GLAD_API_PTR *PFNGLINVALIDATEFRAMEBUFFERPROC)(GLenum target, GLsizei numAttachments, const GLenum * attachments);
+typedef void (GLAD_API_PTR *PFNGLINVALIDATESUBFRAMEBUFFERPROC)(GLenum target, GLsizei numAttachments, const GLenum * attachments, GLint x, GLint y, GLsizei width, GLsizei height);
+typedef void (GLAD_API_PTR *PFNGLINVALIDATETEXIMAGEPROC)(GLuint texture, GLint level);
+typedef void (GLAD_API_PTR *PFNGLINVALIDATETEXSUBIMAGEPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth);
typedef GLboolean (GLAD_API_PTR *PFNGLISBUFFERPROC)(GLuint buffer);
typedef GLboolean (GLAD_API_PTR *PFNGLISENABLEDPROC)(GLenum cap);
typedef GLboolean (GLAD_API_PTR *PFNGLISENABLEDIPROC)(GLenum target, GLuint index);
typedef GLboolean (GLAD_API_PTR *PFNGLISFRAMEBUFFERPROC)(GLuint framebuffer);
typedef GLboolean (GLAD_API_PTR *PFNGLISPROGRAMPROC)(GLuint program);
+typedef GLboolean (GLAD_API_PTR *PFNGLISPROGRAMPIPELINEPROC)(GLuint pipeline);
typedef GLboolean (GLAD_API_PTR *PFNGLISQUERYPROC)(GLuint id);
typedef GLboolean (GLAD_API_PTR *PFNGLISRENDERBUFFERPROC)(GLuint renderbuffer);
typedef GLboolean (GLAD_API_PTR *PFNGLISSAMPLERPROC)(GLuint sampler);
typedef GLboolean (GLAD_API_PTR *PFNGLISSHADERPROC)(GLuint shader);
typedef GLboolean (GLAD_API_PTR *PFNGLISSYNCPROC)(GLsync sync);
typedef GLboolean (GLAD_API_PTR *PFNGLISTEXTUREPROC)(GLuint texture);
+typedef GLboolean (GLAD_API_PTR *PFNGLISTRANSFORMFEEDBACKPROC)(GLuint id);
typedef GLboolean (GLAD_API_PTR *PFNGLISVERTEXARRAYPROC)(GLuint array);
typedef void (GLAD_API_PTR *PFNGLLINEWIDTHPROC)(GLfloat width);
typedef void (GLAD_API_PTR *PFNGLLINKPROGRAMPROC)(GLuint program);
typedef void (GLAD_API_PTR *PFNGLLOGICOPPROC)(GLenum opcode);
typedef void * (GLAD_API_PTR *PFNGLMAPBUFFERPROC)(GLenum target, GLenum access);
typedef void * (GLAD_API_PTR *PFNGLMAPBUFFERRANGEPROC)(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access);
+typedef void (GLAD_API_PTR *PFNGLMEMORYBARRIERPROC)(GLbitfield barriers);
+typedef void (GLAD_API_PTR *PFNGLMINSAMPLESHADINGPROC)(GLfloat value);
typedef void (GLAD_API_PTR *PFNGLMULTIDRAWARRAYSPROC)(GLenum mode, const GLint * first, const GLsizei * count, GLsizei drawcount);
+typedef void (GLAD_API_PTR *PFNGLMULTIDRAWARRAYSINDIRECTPROC)(GLenum mode, const void * indirect, GLsizei drawcount, GLsizei stride);
typedef void (GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSPROC)(GLenum mode, const GLsizei * count, GLenum type, const void *const* indices, GLsizei drawcount);
typedef void (GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSBASEVERTEXPROC)(GLenum mode, const GLsizei * count, GLenum type, const void *const* indices, GLsizei drawcount, const GLint * basevertex);
+typedef void (GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSINDIRECTPROC)(GLenum mode, GLenum type, const void * indirect, GLsizei drawcount, GLsizei stride);
+typedef void (GLAD_API_PTR *PFNGLOBJECTLABELPROC)(GLenum identifier, GLuint name, GLsizei length, const GLchar * label);
+typedef void (GLAD_API_PTR *PFNGLOBJECTPTRLABELPROC)(const void * ptr, GLsizei length, const GLchar * label);
+typedef void (GLAD_API_PTR *PFNGLPATCHPARAMETERFVPROC)(GLenum pname, const GLfloat * values);
+typedef void (GLAD_API_PTR *PFNGLPATCHPARAMETERIPROC)(GLenum pname, GLint value);
+typedef void (GLAD_API_PTR *PFNGLPAUSETRANSFORMFEEDBACKPROC)(void);
typedef void (GLAD_API_PTR *PFNGLPIXELSTOREFPROC)(GLenum pname, GLfloat param);
typedef void (GLAD_API_PTR *PFNGLPIXELSTOREIPROC)(GLenum pname, GLint param);
typedef void (GLAD_API_PTR *PFNGLPOINTPARAMETERFPROC)(GLenum pname, GLfloat param);
@@ -1275,13 +1850,69 @@ typedef void (GLAD_API_PTR *PFNGLPOINTPARAMETERIVPROC)(GLenum pname, const GLint
typedef void (GLAD_API_PTR *PFNGLPOINTSIZEPROC)(GLfloat size);
typedef void (GLAD_API_PTR *PFNGLPOLYGONMODEPROC)(GLenum face, GLenum mode);
typedef void (GLAD_API_PTR *PFNGLPOLYGONOFFSETPROC)(GLfloat factor, GLfloat units);
+typedef void (GLAD_API_PTR *PFNGLPOPDEBUGGROUPPROC)(void);
typedef void (GLAD_API_PTR *PFNGLPRIMITIVERESTARTINDEXPROC)(GLuint index);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMBINARYPROC)(GLuint program, GLenum binaryFormat, const void * binary, GLsizei length);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMPARAMETERIPROC)(GLuint program, GLenum pname, GLint value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1DPROC)(GLuint program, GLint location, GLdouble v0);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1FPROC)(GLuint program, GLint location, GLfloat v0);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1IPROC)(GLuint program, GLint location, GLint v0);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1IVPROC)(GLuint program, GLint location, GLsizei count, const GLint * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1UIPROC)(GLuint program, GLint location, GLuint v0);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2DPROC)(GLuint program, GLint location, GLdouble v0, GLdouble v1);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2FPROC)(GLuint program, GLint location, GLfloat v0, GLfloat v1);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2IPROC)(GLuint program, GLint location, GLint v0, GLint v1);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2IVPROC)(GLuint program, GLint location, GLsizei count, const GLint * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2UIPROC)(GLuint program, GLint location, GLuint v0, GLuint v1);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3DPROC)(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3FPROC)(GLuint program, GLint location, GLfloat v0, GLfloat v1, GLfloat v2);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3IPROC)(GLuint program, GLint location, GLint v0, GLint v1, GLint v2);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3IVPROC)(GLuint program, GLint location, GLsizei count, const GLint * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3UIPROC)(GLuint program, GLint location, GLuint v0, GLuint v1, GLuint v2);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4DPROC)(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4FPROC)(GLuint program, GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4IPROC)(GLuint program, GLint location, GLint v0, GLint v1, GLint v2, GLint v3);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4IVPROC)(GLuint program, GLint location, GLsizei count, const GLint * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4UIPROC)(GLuint program, GLint location, GLuint v0, GLuint v1, GLuint v2, GLuint v3);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X3DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X3FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X4DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X4FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X2DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X2FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X4DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X4FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X2DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X2FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X3DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
+typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X3FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
typedef void (GLAD_API_PTR *PFNGLPROVOKINGVERTEXPROC)(GLenum mode);
+typedef void (GLAD_API_PTR *PFNGLPUSHDEBUGGROUPPROC)(GLenum source, GLuint id, GLsizei length, const GLchar * message);
typedef void (GLAD_API_PTR *PFNGLQUERYCOUNTERPROC)(GLuint id, GLenum target);
typedef void (GLAD_API_PTR *PFNGLREADBUFFERPROC)(GLenum src);
typedef void (GLAD_API_PTR *PFNGLREADPIXELSPROC)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void * pixels);
+typedef void (GLAD_API_PTR *PFNGLRELEASESHADERCOMPILERPROC)(void);
typedef void (GLAD_API_PTR *PFNGLRENDERBUFFERSTORAGEPROC)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
typedef void (GLAD_API_PTR *PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height);
+typedef void (GLAD_API_PTR *PFNGLRESUMETRANSFORMFEEDBACKPROC)(void);
typedef void (GLAD_API_PTR *PFNGLSAMPLECOVERAGEPROC)(GLfloat value, GLboolean invert);
typedef void (GLAD_API_PTR *PFNGLSAMPLEMASKIPROC)(GLuint maskNumber, GLbitfield mask);
typedef void (GLAD_API_PTR *PFNGLSAMPLERPARAMETERIIVPROC)(GLuint sampler, GLenum pname, const GLint * param);
@@ -1291,7 +1922,12 @@ typedef void (GLAD_API_PTR *PFNGLSAMPLERPARAMETERFVPROC)(GLuint sampler, GLenum
typedef void (GLAD_API_PTR *PFNGLSAMPLERPARAMETERIPROC)(GLuint sampler, GLenum pname, GLint param);
typedef void (GLAD_API_PTR *PFNGLSAMPLERPARAMETERIVPROC)(GLuint sampler, GLenum pname, const GLint * param);
typedef void (GLAD_API_PTR *PFNGLSCISSORPROC)(GLint x, GLint y, GLsizei width, GLsizei height);
+typedef void (GLAD_API_PTR *PFNGLSCISSORARRAYVPROC)(GLuint first, GLsizei count, const GLint * v);
+typedef void (GLAD_API_PTR *PFNGLSCISSORINDEXEDPROC)(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height);
+typedef void (GLAD_API_PTR *PFNGLSCISSORINDEXEDVPROC)(GLuint index, const GLint * v);
+typedef void (GLAD_API_PTR *PFNGLSHADERBINARYPROC)(GLsizei count, const GLuint * shaders, GLenum binaryFormat, const void * binary, GLsizei length);
typedef void (GLAD_API_PTR *PFNGLSHADERSOURCEPROC)(GLuint shader, GLsizei count, const GLchar *const* string, const GLint * length);
+typedef void (GLAD_API_PTR *PFNGLSHADERSTORAGEBLOCKBINDINGPROC)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
typedef void (GLAD_API_PTR *PFNGLSTENCILFUNCPROC)(GLenum func, GLint ref, GLuint mask);
typedef void (GLAD_API_PTR *PFNGLSTENCILFUNCSEPARATEPROC)(GLenum face, GLenum func, GLint ref, GLuint mask);
typedef void (GLAD_API_PTR *PFNGLSTENCILMASKPROC)(GLuint mask);
@@ -1299,6 +1935,7 @@ typedef void (GLAD_API_PTR *PFNGLSTENCILMASKSEPARATEPROC)(GLenum face, GLuint ma
typedef void (GLAD_API_PTR *PFNGLSTENCILOPPROC)(GLenum fail, GLenum zfail, GLenum zpass);
typedef void (GLAD_API_PTR *PFNGLSTENCILOPSEPARATEPROC)(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass);
typedef void (GLAD_API_PTR *PFNGLTEXBUFFERPROC)(GLenum target, GLenum internalformat, GLuint buffer);
+typedef void (GLAD_API_PTR *PFNGLTEXBUFFERRANGEPROC)(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size);
typedef void (GLAD_API_PTR *PFNGLTEXIMAGE1DPROC)(GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, const void * pixels);
typedef void (GLAD_API_PTR *PFNGLTEXIMAGE2DPROC)(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void * pixels);
typedef void (GLAD_API_PTR *PFNGLTEXIMAGE2DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations);
@@ -1310,28 +1947,42 @@ typedef void (GLAD_API_PTR *PFNGLTEXPARAMETERFPROC)(GLenum target, GLenum pname,
typedef void (GLAD_API_PTR *PFNGLTEXPARAMETERFVPROC)(GLenum target, GLenum pname, const GLfloat * params);
typedef void (GLAD_API_PTR *PFNGLTEXPARAMETERIPROC)(GLenum target, GLenum pname, GLint param);
typedef void (GLAD_API_PTR *PFNGLTEXPARAMETERIVPROC)(GLenum target, GLenum pname, const GLint * params);
+typedef void (GLAD_API_PTR *PFNGLTEXSTORAGE1DPROC)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width);
+typedef void (GLAD_API_PTR *PFNGLTEXSTORAGE2DPROC)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height);
+typedef void (GLAD_API_PTR *PFNGLTEXSTORAGE2DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations);
+typedef void (GLAD_API_PTR *PFNGLTEXSTORAGE3DPROC)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth);
+typedef void (GLAD_API_PTR *PFNGLTEXSTORAGE3DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations);
typedef void (GLAD_API_PTR *PFNGLTEXSUBIMAGE1DPROC)(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void * pixels);
typedef void (GLAD_API_PTR *PFNGLTEXSUBIMAGE2DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void * pixels);
typedef void (GLAD_API_PTR *PFNGLTEXSUBIMAGE3DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void * pixels);
+typedef void (GLAD_API_PTR *PFNGLTEXTUREVIEWPROC)(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers);
typedef void (GLAD_API_PTR *PFNGLTRANSFORMFEEDBACKVARYINGSPROC)(GLuint program, GLsizei count, const GLchar *const* varyings, GLenum bufferMode);
+typedef void (GLAD_API_PTR *PFNGLUNIFORM1DPROC)(GLint location, GLdouble x);
+typedef void (GLAD_API_PTR *PFNGLUNIFORM1DVPROC)(GLint location, GLsizei count, const GLdouble * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORM1FPROC)(GLint location, GLfloat v0);
typedef void (GLAD_API_PTR *PFNGLUNIFORM1FVPROC)(GLint location, GLsizei count, const GLfloat * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORM1IPROC)(GLint location, GLint v0);
typedef void (GLAD_API_PTR *PFNGLUNIFORM1IVPROC)(GLint location, GLsizei count, const GLint * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORM1UIPROC)(GLint location, GLuint v0);
typedef void (GLAD_API_PTR *PFNGLUNIFORM1UIVPROC)(GLint location, GLsizei count, const GLuint * value);
+typedef void (GLAD_API_PTR *PFNGLUNIFORM2DPROC)(GLint location, GLdouble x, GLdouble y);
+typedef void (GLAD_API_PTR *PFNGLUNIFORM2DVPROC)(GLint location, GLsizei count, const GLdouble * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORM2FPROC)(GLint location, GLfloat v0, GLfloat v1);
typedef void (GLAD_API_PTR *PFNGLUNIFORM2FVPROC)(GLint location, GLsizei count, const GLfloat * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORM2IPROC)(GLint location, GLint v0, GLint v1);
typedef void (GLAD_API_PTR *PFNGLUNIFORM2IVPROC)(GLint location, GLsizei count, const GLint * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORM2UIPROC)(GLint location, GLuint v0, GLuint v1);
typedef void (GLAD_API_PTR *PFNGLUNIFORM2UIVPROC)(GLint location, GLsizei count, const GLuint * value);
+typedef void (GLAD_API_PTR *PFNGLUNIFORM3DPROC)(GLint location, GLdouble x, GLdouble y, GLdouble z);
+typedef void (GLAD_API_PTR *PFNGLUNIFORM3DVPROC)(GLint location, GLsizei count, const GLdouble * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORM3FPROC)(GLint location, GLfloat v0, GLfloat v1, GLfloat v2);
typedef void (GLAD_API_PTR *PFNGLUNIFORM3FVPROC)(GLint location, GLsizei count, const GLfloat * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORM3IPROC)(GLint location, GLint v0, GLint v1, GLint v2);
typedef void (GLAD_API_PTR *PFNGLUNIFORM3IVPROC)(GLint location, GLsizei count, const GLint * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORM3UIPROC)(GLint location, GLuint v0, GLuint v1, GLuint v2);
typedef void (GLAD_API_PTR *PFNGLUNIFORM3UIVPROC)(GLint location, GLsizei count, const GLuint * value);
+typedef void (GLAD_API_PTR *PFNGLUNIFORM4DPROC)(GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w);
+typedef void (GLAD_API_PTR *PFNGLUNIFORM4DVPROC)(GLint location, GLsizei count, const GLdouble * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORM4FPROC)(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3);
typedef void (GLAD_API_PTR *PFNGLUNIFORM4FVPROC)(GLint location, GLsizei count, const GLfloat * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORM4IPROC)(GLint location, GLint v0, GLint v1, GLint v2, GLint v3);
@@ -1339,18 +1990,30 @@ typedef void (GLAD_API_PTR *PFNGLUNIFORM4IVPROC)(GLint location, GLsizei count,
typedef void (GLAD_API_PTR *PFNGLUNIFORM4UIPROC)(GLint location, GLuint v0, GLuint v1, GLuint v2, GLuint v3);
typedef void (GLAD_API_PTR *PFNGLUNIFORM4UIVPROC)(GLint location, GLsizei count, const GLuint * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORMBLOCKBINDINGPROC)(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding);
+typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX2DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX2FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX2X3DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX2X3FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX2X4DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX2X4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX3DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX3FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX3X2DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX3X2FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX3X4DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX3X4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX4DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX4X2DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX4X2FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX4X3DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value);
typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX4X3FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);
+typedef void (GLAD_API_PTR *PFNGLUNIFORMSUBROUTINESUIVPROC)(GLenum shadertype, GLsizei count, const GLuint * indices);
typedef GLboolean (GLAD_API_PTR *PFNGLUNMAPBUFFERPROC)(GLenum target);
typedef void (GLAD_API_PTR *PFNGLUSEPROGRAMPROC)(GLuint program);
+typedef void (GLAD_API_PTR *PFNGLUSEPROGRAMSTAGESPROC)(GLuint pipeline, GLbitfield stages, GLuint program);
typedef void (GLAD_API_PTR *PFNGLVALIDATEPROGRAMPROC)(GLuint program);
+typedef void (GLAD_API_PTR *PFNGLVALIDATEPROGRAMPIPELINEPROC)(GLuint pipeline);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB1DPROC)(GLuint index, GLdouble x);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB1DVPROC)(GLuint index, const GLdouble * v);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB1FPROC)(GLuint index, GLfloat x);
@@ -1387,7 +2050,9 @@ typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB4SVPROC)(GLuint index, const GLshor
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB4UBVPROC)(GLuint index, const GLubyte * v);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB4UIVPROC)(GLuint index, const GLuint * v);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB4USVPROC)(GLuint index, const GLushort * v);
+typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBBINDINGPROC)(GLuint attribindex, GLuint bindingindex);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBDIVISORPROC)(GLuint index, GLuint divisor);
+typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBFORMATPROC)(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI1IPROC)(GLuint index, GLint x);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI1IVPROC)(GLuint index, const GLint * v);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI1UIPROC)(GLuint index, GLuint x);
@@ -1408,7 +2073,18 @@ typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI4UBVPROC)(GLuint index, const GLub
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI4UIPROC)(GLuint index, GLuint x, GLuint y, GLuint z, GLuint w);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI4UIVPROC)(GLuint index, const GLuint * v);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI4USVPROC)(GLuint index, const GLushort * v);
+typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBIFORMATPROC)(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBIPOINTERPROC)(GLuint index, GLint size, GLenum type, GLsizei stride, const void * pointer);
+typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL1DPROC)(GLuint index, GLdouble x);
+typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL1DVPROC)(GLuint index, const GLdouble * v);
+typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL2DPROC)(GLuint index, GLdouble x, GLdouble y);
+typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL2DVPROC)(GLuint index, const GLdouble * v);
+typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL3DPROC)(GLuint index, GLdouble x, GLdouble y, GLdouble z);
+typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL3DVPROC)(GLuint index, const GLdouble * v);
+typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL4DPROC)(GLuint index, GLdouble x, GLdouble y, GLdouble z, GLdouble w);
+typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL4DVPROC)(GLuint index, const GLdouble * v);
+typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBLFORMATPROC)(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset);
+typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBLPOINTERPROC)(GLuint index, GLint size, GLenum type, GLsizei stride, const void * pointer);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBP1UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBP1UIVPROC)(GLuint index, GLenum type, GLboolean normalized, const GLuint * value);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBP2UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value);
@@ -1418,7 +2094,11 @@ typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBP3UIVPROC)(GLuint index, GLenum typ
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBP4UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBP4UIVPROC)(GLuint index, GLenum type, GLboolean normalized, const GLuint * value);
typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBPOINTERPROC)(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void * pointer);
+typedef void (GLAD_API_PTR *PFNGLVERTEXBINDINGDIVISORPROC)(GLuint bindingindex, GLuint divisor);
typedef void (GLAD_API_PTR *PFNGLVIEWPORTPROC)(GLint x, GLint y, GLsizei width, GLsizei height);
+typedef void (GLAD_API_PTR *PFNGLVIEWPORTARRAYVPROC)(GLuint first, GLsizei count, const GLfloat * v);
+typedef void (GLAD_API_PTR *PFNGLVIEWPORTINDEXEDFPROC)(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h);
+typedef void (GLAD_API_PTR *PFNGLVIEWPORTINDEXEDFVPROC)(GLuint index, const GLfloat * v);
typedef void (GLAD_API_PTR *PFNGLWAITSYNCPROC)(GLsync sync, GLbitfield flags, GLuint64 timeout);
typedef struct GladGLContext {
@@ -1436,11 +2116,17 @@ typedef struct GladGLContext {
int VERSION_3_1;
int VERSION_3_2;
int VERSION_3_3;
+ int VERSION_4_0;
+ int VERSION_4_1;
+ int VERSION_4_2;
+ int VERSION_4_3;
+ PFNGLACTIVESHADERPROGRAMPROC ActiveShaderProgram;
PFNGLACTIVETEXTUREPROC ActiveTexture;
PFNGLATTACHSHADERPROC AttachShader;
PFNGLBEGINCONDITIONALRENDERPROC BeginConditionalRender;
PFNGLBEGINQUERYPROC BeginQuery;
+ PFNGLBEGINQUERYINDEXEDPROC BeginQueryIndexed;
PFNGLBEGINTRANSFORMFEEDBACKPROC BeginTransformFeedback;
PFNGLBINDATTRIBLOCATIONPROC BindAttribLocation;
PFNGLBINDBUFFERPROC BindBuffer;
@@ -1449,27 +2135,38 @@ typedef struct GladGLContext {
PFNGLBINDFRAGDATALOCATIONPROC BindFragDataLocation;
PFNGLBINDFRAGDATALOCATIONINDEXEDPROC BindFragDataLocationIndexed;
PFNGLBINDFRAMEBUFFERPROC BindFramebuffer;
+ PFNGLBINDIMAGETEXTUREPROC BindImageTexture;
+ PFNGLBINDPROGRAMPIPELINEPROC BindProgramPipeline;
PFNGLBINDRENDERBUFFERPROC BindRenderbuffer;
PFNGLBINDSAMPLERPROC BindSampler;
PFNGLBINDTEXTUREPROC BindTexture;
+ PFNGLBINDTRANSFORMFEEDBACKPROC BindTransformFeedback;
PFNGLBINDVERTEXARRAYPROC BindVertexArray;
+ PFNGLBINDVERTEXBUFFERPROC BindVertexBuffer;
PFNGLBLENDCOLORPROC BlendColor;
PFNGLBLENDEQUATIONPROC BlendEquation;
PFNGLBLENDEQUATIONSEPARATEPROC BlendEquationSeparate;
+ PFNGLBLENDEQUATIONSEPARATEIPROC BlendEquationSeparatei;
+ PFNGLBLENDEQUATIONIPROC BlendEquationi;
PFNGLBLENDFUNCPROC BlendFunc;
PFNGLBLENDFUNCSEPARATEPROC BlendFuncSeparate;
+ PFNGLBLENDFUNCSEPARATEIPROC BlendFuncSeparatei;
+ PFNGLBLENDFUNCIPROC BlendFunci;
PFNGLBLITFRAMEBUFFERPROC BlitFramebuffer;
PFNGLBUFFERDATAPROC BufferData;
PFNGLBUFFERSUBDATAPROC BufferSubData;
PFNGLCHECKFRAMEBUFFERSTATUSPROC CheckFramebufferStatus;
PFNGLCLAMPCOLORPROC ClampColor;
PFNGLCLEARPROC Clear;
+ PFNGLCLEARBUFFERDATAPROC ClearBufferData;
+ PFNGLCLEARBUFFERSUBDATAPROC ClearBufferSubData;
PFNGLCLEARBUFFERFIPROC ClearBufferfi;
PFNGLCLEARBUFFERFVPROC ClearBufferfv;
PFNGLCLEARBUFFERIVPROC ClearBufferiv;
PFNGLCLEARBUFFERUIVPROC ClearBufferuiv;
PFNGLCLEARCOLORPROC ClearColor;
PFNGLCLEARDEPTHPROC ClearDepth;
+ PFNGLCLEARDEPTHFPROC ClearDepthf;
PFNGLCLEARSTENCILPROC ClearStencil;
PFNGLCLIENTWAITSYNCPROC ClientWaitSync;
PFNGLCOLORMASKPROC ColorMask;
@@ -1482,6 +2179,7 @@ typedef struct GladGLContext {
PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC CompressedTexSubImage2D;
PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC CompressedTexSubImage3D;
PFNGLCOPYBUFFERSUBDATAPROC CopyBufferSubData;
+ PFNGLCOPYIMAGESUBDATAPROC CopyImageSubData;
PFNGLCOPYTEXIMAGE1DPROC CopyTexImage1D;
PFNGLCOPYTEXIMAGE2DPROC CopyTexImage2D;
PFNGLCOPYTEXSUBIMAGE1DPROC CopyTexSubImage1D;
@@ -1489,44 +2187,66 @@ typedef struct GladGLContext {
PFNGLCOPYTEXSUBIMAGE3DPROC CopyTexSubImage3D;
PFNGLCREATEPROGRAMPROC CreateProgram;
PFNGLCREATESHADERPROC CreateShader;
+ PFNGLCREATESHADERPROGRAMVPROC CreateShaderProgramv;
PFNGLCULLFACEPROC CullFace;
+ PFNGLDEBUGMESSAGECALLBACKPROC DebugMessageCallback;
+ PFNGLDEBUGMESSAGECONTROLPROC DebugMessageControl;
+ PFNGLDEBUGMESSAGEINSERTPROC DebugMessageInsert;
PFNGLDELETEBUFFERSPROC DeleteBuffers;
PFNGLDELETEFRAMEBUFFERSPROC DeleteFramebuffers;
PFNGLDELETEPROGRAMPROC DeleteProgram;
+ PFNGLDELETEPROGRAMPIPELINESPROC DeleteProgramPipelines;
PFNGLDELETEQUERIESPROC DeleteQueries;
PFNGLDELETERENDERBUFFERSPROC DeleteRenderbuffers;
PFNGLDELETESAMPLERSPROC DeleteSamplers;
PFNGLDELETESHADERPROC DeleteShader;
PFNGLDELETESYNCPROC DeleteSync;
PFNGLDELETETEXTURESPROC DeleteTextures;
+ PFNGLDELETETRANSFORMFEEDBACKSPROC DeleteTransformFeedbacks;
PFNGLDELETEVERTEXARRAYSPROC DeleteVertexArrays;
PFNGLDEPTHFUNCPROC DepthFunc;
PFNGLDEPTHMASKPROC DepthMask;
PFNGLDEPTHRANGEPROC DepthRange;
+ PFNGLDEPTHRANGEARRAYVPROC DepthRangeArrayv;
+ PFNGLDEPTHRANGEINDEXEDPROC DepthRangeIndexed;
+ PFNGLDEPTHRANGEFPROC DepthRangef;
PFNGLDETACHSHADERPROC DetachShader;
PFNGLDISABLEPROC Disable;
PFNGLDISABLEVERTEXATTRIBARRAYPROC DisableVertexAttribArray;
PFNGLDISABLEIPROC Disablei;
+ PFNGLDISPATCHCOMPUTEPROC DispatchCompute;
+ PFNGLDISPATCHCOMPUTEINDIRECTPROC DispatchComputeIndirect;
PFNGLDRAWARRAYSPROC DrawArrays;
+ PFNGLDRAWARRAYSINDIRECTPROC DrawArraysIndirect;
PFNGLDRAWARRAYSINSTANCEDPROC DrawArraysInstanced;
+ PFNGLDRAWARRAYSINSTANCEDBASEINSTANCEPROC DrawArraysInstancedBaseInstance;
PFNGLDRAWBUFFERPROC DrawBuffer;
PFNGLDRAWBUFFERSPROC DrawBuffers;
PFNGLDRAWELEMENTSPROC DrawElements;
PFNGLDRAWELEMENTSBASEVERTEXPROC DrawElementsBaseVertex;
+ PFNGLDRAWELEMENTSINDIRECTPROC DrawElementsIndirect;
PFNGLDRAWELEMENTSINSTANCEDPROC DrawElementsInstanced;
+ PFNGLDRAWELEMENTSINSTANCEDBASEINSTANCEPROC DrawElementsInstancedBaseInstance;
PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXPROC DrawElementsInstancedBaseVertex;
+ PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXBASEINSTANCEPROC DrawElementsInstancedBaseVertexBaseInstance;
PFNGLDRAWRANGEELEMENTSPROC DrawRangeElements;
PFNGLDRAWRANGEELEMENTSBASEVERTEXPROC DrawRangeElementsBaseVertex;
+ PFNGLDRAWTRANSFORMFEEDBACKPROC DrawTransformFeedback;
+ PFNGLDRAWTRANSFORMFEEDBACKINSTANCEDPROC DrawTransformFeedbackInstanced;
+ PFNGLDRAWTRANSFORMFEEDBACKSTREAMPROC DrawTransformFeedbackStream;
+ PFNGLDRAWTRANSFORMFEEDBACKSTREAMINSTANCEDPROC DrawTransformFeedbackStreamInstanced;
PFNGLENABLEPROC Enable;
PFNGLENABLEVERTEXATTRIBARRAYPROC EnableVertexAttribArray;
PFNGLENABLEIPROC Enablei;
PFNGLENDCONDITIONALRENDERPROC EndConditionalRender;
PFNGLENDQUERYPROC EndQuery;
+ PFNGLENDQUERYINDEXEDPROC EndQueryIndexed;
PFNGLENDTRANSFORMFEEDBACKPROC EndTransformFeedback;
PFNGLFENCESYNCPROC FenceSync;
PFNGLFINISHPROC Finish;
PFNGLFLUSHPROC Flush;
PFNGLFLUSHMAPPEDBUFFERRANGEPROC FlushMappedBufferRange;
+ PFNGLFRAMEBUFFERPARAMETERIPROC FramebufferParameteri;
PFNGLFRAMEBUFFERRENDERBUFFERPROC FramebufferRenderbuffer;
PFNGLFRAMEBUFFERTEXTUREPROC FramebufferTexture;
PFNGLFRAMEBUFFERTEXTURE1DPROC FramebufferTexture1D;
@@ -1536,13 +2256,19 @@ typedef struct GladGLContext {
PFNGLFRONTFACEPROC FrontFace;
PFNGLGENBUFFERSPROC GenBuffers;
PFNGLGENFRAMEBUFFERSPROC GenFramebuffers;
+ PFNGLGENPROGRAMPIPELINESPROC GenProgramPipelines;
PFNGLGENQUERIESPROC GenQueries;
PFNGLGENRENDERBUFFERSPROC GenRenderbuffers;
PFNGLGENSAMPLERSPROC GenSamplers;
PFNGLGENTEXTURESPROC GenTextures;
+ PFNGLGENTRANSFORMFEEDBACKSPROC GenTransformFeedbacks;
PFNGLGENVERTEXARRAYSPROC GenVertexArrays;
PFNGLGENERATEMIPMAPPROC GenerateMipmap;
+ PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC GetActiveAtomicCounterBufferiv;
PFNGLGETACTIVEATTRIBPROC GetActiveAttrib;
+ PFNGLGETACTIVESUBROUTINENAMEPROC GetActiveSubroutineName;
+ PFNGLGETACTIVESUBROUTINEUNIFORMNAMEPROC GetActiveSubroutineUniformName;
+ PFNGLGETACTIVESUBROUTINEUNIFORMIVPROC GetActiveSubroutineUniformiv;
PFNGLGETACTIVEUNIFORMPROC GetActiveUniform;
PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC GetActiveUniformBlockName;
PFNGLGETACTIVEUNIFORMBLOCKIVPROC GetActiveUniformBlockiv;
@@ -1557,19 +2283,39 @@ typedef struct GladGLContext {
PFNGLGETBUFFERPOINTERVPROC GetBufferPointerv;
PFNGLGETBUFFERSUBDATAPROC GetBufferSubData;
PFNGLGETCOMPRESSEDTEXIMAGEPROC GetCompressedTexImage;
+ PFNGLGETDEBUGMESSAGELOGPROC GetDebugMessageLog;
+ PFNGLGETDOUBLEI_VPROC GetDoublei_v;
PFNGLGETDOUBLEVPROC GetDoublev;
PFNGLGETERRORPROC GetError;
+ PFNGLGETFLOATI_VPROC GetFloati_v;
PFNGLGETFLOATVPROC GetFloatv;
PFNGLGETFRAGDATAINDEXPROC GetFragDataIndex;
PFNGLGETFRAGDATALOCATIONPROC GetFragDataLocation;
PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC GetFramebufferAttachmentParameteriv;
+ PFNGLGETFRAMEBUFFERPARAMETERIVPROC GetFramebufferParameteriv;
PFNGLGETINTEGER64I_VPROC GetInteger64i_v;
PFNGLGETINTEGER64VPROC GetInteger64v;
PFNGLGETINTEGERI_VPROC GetIntegeri_v;
PFNGLGETINTEGERVPROC GetIntegerv;
+ PFNGLGETINTERNALFORMATI64VPROC GetInternalformati64v;
+ PFNGLGETINTERNALFORMATIVPROC GetInternalformativ;
PFNGLGETMULTISAMPLEFVPROC GetMultisamplefv;
+ PFNGLGETOBJECTLABELPROC GetObjectLabel;
+ PFNGLGETOBJECTPTRLABELPROC GetObjectPtrLabel;
+ PFNGLGETPOINTERVPROC GetPointerv;
+ PFNGLGETPROGRAMBINARYPROC GetProgramBinary;
PFNGLGETPROGRAMINFOLOGPROC GetProgramInfoLog;
+ PFNGLGETPROGRAMINTERFACEIVPROC GetProgramInterfaceiv;
+ PFNGLGETPROGRAMPIPELINEINFOLOGPROC GetProgramPipelineInfoLog;
+ PFNGLGETPROGRAMPIPELINEIVPROC GetProgramPipelineiv;
+ PFNGLGETPROGRAMRESOURCEINDEXPROC GetProgramResourceIndex;
+ PFNGLGETPROGRAMRESOURCELOCATIONPROC GetProgramResourceLocation;
+ PFNGLGETPROGRAMRESOURCELOCATIONINDEXPROC GetProgramResourceLocationIndex;
+ PFNGLGETPROGRAMRESOURCENAMEPROC GetProgramResourceName;
+ PFNGLGETPROGRAMRESOURCEIVPROC GetProgramResourceiv;
+ PFNGLGETPROGRAMSTAGEIVPROC GetProgramStageiv;
PFNGLGETPROGRAMIVPROC GetProgramiv;
+ PFNGLGETQUERYINDEXEDIVPROC GetQueryIndexediv;
PFNGLGETQUERYOBJECTI64VPROC GetQueryObjecti64v;
PFNGLGETQUERYOBJECTIVPROC GetQueryObjectiv;
PFNGLGETQUERYOBJECTUI64VPROC GetQueryObjectui64v;
@@ -1581,10 +2327,13 @@ typedef struct GladGLContext {
PFNGLGETSAMPLERPARAMETERFVPROC GetSamplerParameterfv;
PFNGLGETSAMPLERPARAMETERIVPROC GetSamplerParameteriv;
PFNGLGETSHADERINFOLOGPROC GetShaderInfoLog;
+ PFNGLGETSHADERPRECISIONFORMATPROC GetShaderPrecisionFormat;
PFNGLGETSHADERSOURCEPROC GetShaderSource;
PFNGLGETSHADERIVPROC GetShaderiv;
PFNGLGETSTRINGPROC GetString;
PFNGLGETSTRINGIPROC GetStringi;
+ PFNGLGETSUBROUTINEINDEXPROC GetSubroutineIndex;
+ PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC GetSubroutineUniformLocation;
PFNGLGETSYNCIVPROC GetSynciv;
PFNGLGETTEXIMAGEPROC GetTexImage;
PFNGLGETTEXLEVELPARAMETERFVPROC GetTexLevelParameterfv;
@@ -1597,36 +2346,56 @@ typedef struct GladGLContext {
PFNGLGETUNIFORMBLOCKINDEXPROC GetUniformBlockIndex;
PFNGLGETUNIFORMINDICESPROC GetUniformIndices;
PFNGLGETUNIFORMLOCATIONPROC GetUniformLocation;
+ PFNGLGETUNIFORMSUBROUTINEUIVPROC GetUniformSubroutineuiv;
+ PFNGLGETUNIFORMDVPROC GetUniformdv;
PFNGLGETUNIFORMFVPROC GetUniformfv;
PFNGLGETUNIFORMIVPROC GetUniformiv;
PFNGLGETUNIFORMUIVPROC GetUniformuiv;
PFNGLGETVERTEXATTRIBIIVPROC GetVertexAttribIiv;
PFNGLGETVERTEXATTRIBIUIVPROC GetVertexAttribIuiv;
+ PFNGLGETVERTEXATTRIBLDVPROC GetVertexAttribLdv;
PFNGLGETVERTEXATTRIBPOINTERVPROC GetVertexAttribPointerv;
PFNGLGETVERTEXATTRIBDVPROC GetVertexAttribdv;
PFNGLGETVERTEXATTRIBFVPROC GetVertexAttribfv;
PFNGLGETVERTEXATTRIBIVPROC GetVertexAttribiv;
PFNGLHINTPROC Hint;
+ PFNGLINVALIDATEBUFFERDATAPROC InvalidateBufferData;
+ PFNGLINVALIDATEBUFFERSUBDATAPROC InvalidateBufferSubData;
+ PFNGLINVALIDATEFRAMEBUFFERPROC InvalidateFramebuffer;
+ PFNGLINVALIDATESUBFRAMEBUFFERPROC InvalidateSubFramebuffer;
+ PFNGLINVALIDATETEXIMAGEPROC InvalidateTexImage;
+ PFNGLINVALIDATETEXSUBIMAGEPROC InvalidateTexSubImage;
PFNGLISBUFFERPROC IsBuffer;
PFNGLISENABLEDPROC IsEnabled;
PFNGLISENABLEDIPROC IsEnabledi;
PFNGLISFRAMEBUFFERPROC IsFramebuffer;
PFNGLISPROGRAMPROC IsProgram;
+ PFNGLISPROGRAMPIPELINEPROC IsProgramPipeline;
PFNGLISQUERYPROC IsQuery;
PFNGLISRENDERBUFFERPROC IsRenderbuffer;
PFNGLISSAMPLERPROC IsSampler;
PFNGLISSHADERPROC IsShader;
PFNGLISSYNCPROC IsSync;
PFNGLISTEXTUREPROC IsTexture;
+ PFNGLISTRANSFORMFEEDBACKPROC IsTransformFeedback;
PFNGLISVERTEXARRAYPROC IsVertexArray;
PFNGLLINEWIDTHPROC LineWidth;
PFNGLLINKPROGRAMPROC LinkProgram;
PFNGLLOGICOPPROC LogicOp;
PFNGLMAPBUFFERPROC MapBuffer;
PFNGLMAPBUFFERRANGEPROC MapBufferRange;
+ PFNGLMEMORYBARRIERPROC MemoryBarrier;
+ PFNGLMINSAMPLESHADINGPROC MinSampleShading;
PFNGLMULTIDRAWARRAYSPROC MultiDrawArrays;
+ PFNGLMULTIDRAWARRAYSINDIRECTPROC MultiDrawArraysIndirect;
PFNGLMULTIDRAWELEMENTSPROC MultiDrawElements;
PFNGLMULTIDRAWELEMENTSBASEVERTEXPROC MultiDrawElementsBaseVertex;
+ PFNGLMULTIDRAWELEMENTSINDIRECTPROC MultiDrawElementsIndirect;
+ PFNGLOBJECTLABELPROC ObjectLabel;
+ PFNGLOBJECTPTRLABELPROC ObjectPtrLabel;
+ PFNGLPATCHPARAMETERFVPROC PatchParameterfv;
+ PFNGLPATCHPARAMETERIPROC PatchParameteri;
+ PFNGLPAUSETRANSFORMFEEDBACKPROC PauseTransformFeedback;
PFNGLPIXELSTOREFPROC PixelStoref;
PFNGLPIXELSTOREIPROC PixelStorei;
PFNGLPOINTPARAMETERFPROC PointParameterf;
@@ -1636,13 +2405,69 @@ typedef struct GladGLContext {
PFNGLPOINTSIZEPROC PointSize;
PFNGLPOLYGONMODEPROC PolygonMode;
PFNGLPOLYGONOFFSETPROC PolygonOffset;
+ PFNGLPOPDEBUGGROUPPROC PopDebugGroup;
PFNGLPRIMITIVERESTARTINDEXPROC PrimitiveRestartIndex;
+ PFNGLPROGRAMBINARYPROC ProgramBinary;
+ PFNGLPROGRAMPARAMETERIPROC ProgramParameteri;
+ PFNGLPROGRAMUNIFORM1DPROC ProgramUniform1d;
+ PFNGLPROGRAMUNIFORM1DVPROC ProgramUniform1dv;
+ PFNGLPROGRAMUNIFORM1FPROC ProgramUniform1f;
+ PFNGLPROGRAMUNIFORM1FVPROC ProgramUniform1fv;
+ PFNGLPROGRAMUNIFORM1IPROC ProgramUniform1i;
+ PFNGLPROGRAMUNIFORM1IVPROC ProgramUniform1iv;
+ PFNGLPROGRAMUNIFORM1UIPROC ProgramUniform1ui;
+ PFNGLPROGRAMUNIFORM1UIVPROC ProgramUniform1uiv;
+ PFNGLPROGRAMUNIFORM2DPROC ProgramUniform2d;
+ PFNGLPROGRAMUNIFORM2DVPROC ProgramUniform2dv;
+ PFNGLPROGRAMUNIFORM2FPROC ProgramUniform2f;
+ PFNGLPROGRAMUNIFORM2FVPROC ProgramUniform2fv;
+ PFNGLPROGRAMUNIFORM2IPROC ProgramUniform2i;
+ PFNGLPROGRAMUNIFORM2IVPROC ProgramUniform2iv;
+ PFNGLPROGRAMUNIFORM2UIPROC ProgramUniform2ui;
+ PFNGLPROGRAMUNIFORM2UIVPROC ProgramUniform2uiv;
+ PFNGLPROGRAMUNIFORM3DPROC ProgramUniform3d;
+ PFNGLPROGRAMUNIFORM3DVPROC ProgramUniform3dv;
+ PFNGLPROGRAMUNIFORM3FPROC ProgramUniform3f;
+ PFNGLPROGRAMUNIFORM3FVPROC ProgramUniform3fv;
+ PFNGLPROGRAMUNIFORM3IPROC ProgramUniform3i;
+ PFNGLPROGRAMUNIFORM3IVPROC ProgramUniform3iv;
+ PFNGLPROGRAMUNIFORM3UIPROC ProgramUniform3ui;
+ PFNGLPROGRAMUNIFORM3UIVPROC ProgramUniform3uiv;
+ PFNGLPROGRAMUNIFORM4DPROC ProgramUniform4d;
+ PFNGLPROGRAMUNIFORM4DVPROC ProgramUniform4dv;
+ PFNGLPROGRAMUNIFORM4FPROC ProgramUniform4f;
+ PFNGLPROGRAMUNIFORM4FVPROC ProgramUniform4fv;
+ PFNGLPROGRAMUNIFORM4IPROC ProgramUniform4i;
+ PFNGLPROGRAMUNIFORM4IVPROC ProgramUniform4iv;
+ PFNGLPROGRAMUNIFORM4UIPROC ProgramUniform4ui;
+ PFNGLPROGRAMUNIFORM4UIVPROC ProgramUniform4uiv;
+ PFNGLPROGRAMUNIFORMMATRIX2DVPROC ProgramUniformMatrix2dv;
+ PFNGLPROGRAMUNIFORMMATRIX2FVPROC ProgramUniformMatrix2fv;
+ PFNGLPROGRAMUNIFORMMATRIX2X3DVPROC ProgramUniformMatrix2x3dv;
+ PFNGLPROGRAMUNIFORMMATRIX2X3FVPROC ProgramUniformMatrix2x3fv;
+ PFNGLPROGRAMUNIFORMMATRIX2X4DVPROC ProgramUniformMatrix2x4dv;
+ PFNGLPROGRAMUNIFORMMATRIX2X4FVPROC ProgramUniformMatrix2x4fv;
+ PFNGLPROGRAMUNIFORMMATRIX3DVPROC ProgramUniformMatrix3dv;
+ PFNGLPROGRAMUNIFORMMATRIX3FVPROC ProgramUniformMatrix3fv;
+ PFNGLPROGRAMUNIFORMMATRIX3X2DVPROC ProgramUniformMatrix3x2dv;
+ PFNGLPROGRAMUNIFORMMATRIX3X2FVPROC ProgramUniformMatrix3x2fv;
+ PFNGLPROGRAMUNIFORMMATRIX3X4DVPROC ProgramUniformMatrix3x4dv;
+ PFNGLPROGRAMUNIFORMMATRIX3X4FVPROC ProgramUniformMatrix3x4fv;
+ PFNGLPROGRAMUNIFORMMATRIX4DVPROC ProgramUniformMatrix4dv;
+ PFNGLPROGRAMUNIFORMMATRIX4FVPROC ProgramUniformMatrix4fv;
+ PFNGLPROGRAMUNIFORMMATRIX4X2DVPROC ProgramUniformMatrix4x2dv;
+ PFNGLPROGRAMUNIFORMMATRIX4X2FVPROC ProgramUniformMatrix4x2fv;
+ PFNGLPROGRAMUNIFORMMATRIX4X3DVPROC ProgramUniformMatrix4x3dv;
+ PFNGLPROGRAMUNIFORMMATRIX4X3FVPROC ProgramUniformMatrix4x3fv;
PFNGLPROVOKINGVERTEXPROC ProvokingVertex;
+ PFNGLPUSHDEBUGGROUPPROC PushDebugGroup;
PFNGLQUERYCOUNTERPROC QueryCounter;
PFNGLREADBUFFERPROC ReadBuffer;
PFNGLREADPIXELSPROC ReadPixels;
+ PFNGLRELEASESHADERCOMPILERPROC ReleaseShaderCompiler;
PFNGLRENDERBUFFERSTORAGEPROC RenderbufferStorage;
PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC RenderbufferStorageMultisample;
+ PFNGLRESUMETRANSFORMFEEDBACKPROC ResumeTransformFeedback;
PFNGLSAMPLECOVERAGEPROC SampleCoverage;
PFNGLSAMPLEMASKIPROC SampleMaski;
PFNGLSAMPLERPARAMETERIIVPROC SamplerParameterIiv;
@@ -1652,7 +2477,12 @@ typedef struct GladGLContext {
PFNGLSAMPLERPARAMETERIPROC SamplerParameteri;
PFNGLSAMPLERPARAMETERIVPROC SamplerParameteriv;
PFNGLSCISSORPROC Scissor;
+ PFNGLSCISSORARRAYVPROC ScissorArrayv;
+ PFNGLSCISSORINDEXEDPROC ScissorIndexed;
+ PFNGLSCISSORINDEXEDVPROC ScissorIndexedv;
+ PFNGLSHADERBINARYPROC ShaderBinary;
PFNGLSHADERSOURCEPROC ShaderSource;
+ PFNGLSHADERSTORAGEBLOCKBINDINGPROC ShaderStorageBlockBinding;
PFNGLSTENCILFUNCPROC StencilFunc;
PFNGLSTENCILFUNCSEPARATEPROC StencilFuncSeparate;
PFNGLSTENCILMASKPROC StencilMask;
@@ -1660,6 +2490,7 @@ typedef struct GladGLContext {
PFNGLSTENCILOPPROC StencilOp;
PFNGLSTENCILOPSEPARATEPROC StencilOpSeparate;
PFNGLTEXBUFFERPROC TexBuffer;
+ PFNGLTEXBUFFERRANGEPROC TexBufferRange;
PFNGLTEXIMAGE1DPROC TexImage1D;
PFNGLTEXIMAGE2DPROC TexImage2D;
PFNGLTEXIMAGE2DMULTISAMPLEPROC TexImage2DMultisample;
@@ -1671,28 +2502,42 @@ typedef struct GladGLContext {
PFNGLTEXPARAMETERFVPROC TexParameterfv;
PFNGLTEXPARAMETERIPROC TexParameteri;
PFNGLTEXPARAMETERIVPROC TexParameteriv;
+ PFNGLTEXSTORAGE1DPROC TexStorage1D;
+ PFNGLTEXSTORAGE2DPROC TexStorage2D;
+ PFNGLTEXSTORAGE2DMULTISAMPLEPROC TexStorage2DMultisample;
+ PFNGLTEXSTORAGE3DPROC TexStorage3D;
+ PFNGLTEXSTORAGE3DMULTISAMPLEPROC TexStorage3DMultisample;
PFNGLTEXSUBIMAGE1DPROC TexSubImage1D;
PFNGLTEXSUBIMAGE2DPROC TexSubImage2D;
PFNGLTEXSUBIMAGE3DPROC TexSubImage3D;
+ PFNGLTEXTUREVIEWPROC TextureView;
PFNGLTRANSFORMFEEDBACKVARYINGSPROC TransformFeedbackVaryings;
+ PFNGLUNIFORM1DPROC Uniform1d;
+ PFNGLUNIFORM1DVPROC Uniform1dv;
PFNGLUNIFORM1FPROC Uniform1f;
PFNGLUNIFORM1FVPROC Uniform1fv;
PFNGLUNIFORM1IPROC Uniform1i;
PFNGLUNIFORM1IVPROC Uniform1iv;
PFNGLUNIFORM1UIPROC Uniform1ui;
PFNGLUNIFORM1UIVPROC Uniform1uiv;
+ PFNGLUNIFORM2DPROC Uniform2d;
+ PFNGLUNIFORM2DVPROC Uniform2dv;
PFNGLUNIFORM2FPROC Uniform2f;
PFNGLUNIFORM2FVPROC Uniform2fv;
PFNGLUNIFORM2IPROC Uniform2i;
PFNGLUNIFORM2IVPROC Uniform2iv;
PFNGLUNIFORM2UIPROC Uniform2ui;
PFNGLUNIFORM2UIVPROC Uniform2uiv;
+ PFNGLUNIFORM3DPROC Uniform3d;
+ PFNGLUNIFORM3DVPROC Uniform3dv;
PFNGLUNIFORM3FPROC Uniform3f;
PFNGLUNIFORM3FVPROC Uniform3fv;
PFNGLUNIFORM3IPROC Uniform3i;
PFNGLUNIFORM3IVPROC Uniform3iv;
PFNGLUNIFORM3UIPROC Uniform3ui;
PFNGLUNIFORM3UIVPROC Uniform3uiv;
+ PFNGLUNIFORM4DPROC Uniform4d;
+ PFNGLUNIFORM4DVPROC Uniform4dv;
PFNGLUNIFORM4FPROC Uniform4f;
PFNGLUNIFORM4FVPROC Uniform4fv;
PFNGLUNIFORM4IPROC Uniform4i;
@@ -1700,18 +2545,30 @@ typedef struct GladGLContext {
PFNGLUNIFORM4UIPROC Uniform4ui;
PFNGLUNIFORM4UIVPROC Uniform4uiv;
PFNGLUNIFORMBLOCKBINDINGPROC UniformBlockBinding;
+ PFNGLUNIFORMMATRIX2DVPROC UniformMatrix2dv;
PFNGLUNIFORMMATRIX2FVPROC UniformMatrix2fv;
+ PFNGLUNIFORMMATRIX2X3DVPROC UniformMatrix2x3dv;
PFNGLUNIFORMMATRIX2X3FVPROC UniformMatrix2x3fv;
+ PFNGLUNIFORMMATRIX2X4DVPROC UniformMatrix2x4dv;
PFNGLUNIFORMMATRIX2X4FVPROC UniformMatrix2x4fv;
+ PFNGLUNIFORMMATRIX3DVPROC UniformMatrix3dv;
PFNGLUNIFORMMATRIX3FVPROC UniformMatrix3fv;
+ PFNGLUNIFORMMATRIX3X2DVPROC UniformMatrix3x2dv;
PFNGLUNIFORMMATRIX3X2FVPROC UniformMatrix3x2fv;
+ PFNGLUNIFORMMATRIX3X4DVPROC UniformMatrix3x4dv;
PFNGLUNIFORMMATRIX3X4FVPROC UniformMatrix3x4fv;
+ PFNGLUNIFORMMATRIX4DVPROC UniformMatrix4dv;
PFNGLUNIFORMMATRIX4FVPROC UniformMatrix4fv;
+ PFNGLUNIFORMMATRIX4X2DVPROC UniformMatrix4x2dv;
PFNGLUNIFORMMATRIX4X2FVPROC UniformMatrix4x2fv;
+ PFNGLUNIFORMMATRIX4X3DVPROC UniformMatrix4x3dv;
PFNGLUNIFORMMATRIX4X3FVPROC UniformMatrix4x3fv;
+ PFNGLUNIFORMSUBROUTINESUIVPROC UniformSubroutinesuiv;
PFNGLUNMAPBUFFERPROC UnmapBuffer;
PFNGLUSEPROGRAMPROC UseProgram;
+ PFNGLUSEPROGRAMSTAGESPROC UseProgramStages;
PFNGLVALIDATEPROGRAMPROC ValidateProgram;
+ PFNGLVALIDATEPROGRAMPIPELINEPROC ValidateProgramPipeline;
PFNGLVERTEXATTRIB1DPROC VertexAttrib1d;
PFNGLVERTEXATTRIB1DVPROC VertexAttrib1dv;
PFNGLVERTEXATTRIB1FPROC VertexAttrib1f;
@@ -1748,7 +2605,9 @@ typedef struct GladGLContext {
PFNGLVERTEXATTRIB4UBVPROC VertexAttrib4ubv;
PFNGLVERTEXATTRIB4UIVPROC VertexAttrib4uiv;
PFNGLVERTEXATTRIB4USVPROC VertexAttrib4usv;
+ PFNGLVERTEXATTRIBBINDINGPROC VertexAttribBinding;
PFNGLVERTEXATTRIBDIVISORPROC VertexAttribDivisor;
+ PFNGLVERTEXATTRIBFORMATPROC VertexAttribFormat;
PFNGLVERTEXATTRIBI1IPROC VertexAttribI1i;
PFNGLVERTEXATTRIBI1IVPROC VertexAttribI1iv;
PFNGLVERTEXATTRIBI1UIPROC VertexAttribI1ui;
@@ -1769,7 +2628,18 @@ typedef struct GladGLContext {
PFNGLVERTEXATTRIBI4UIPROC VertexAttribI4ui;
PFNGLVERTEXATTRIBI4UIVPROC VertexAttribI4uiv;
PFNGLVERTEXATTRIBI4USVPROC VertexAttribI4usv;
+ PFNGLVERTEXATTRIBIFORMATPROC VertexAttribIFormat;
PFNGLVERTEXATTRIBIPOINTERPROC VertexAttribIPointer;
+ PFNGLVERTEXATTRIBL1DPROC VertexAttribL1d;
+ PFNGLVERTEXATTRIBL1DVPROC VertexAttribL1dv;
+ PFNGLVERTEXATTRIBL2DPROC VertexAttribL2d;
+ PFNGLVERTEXATTRIBL2DVPROC VertexAttribL2dv;
+ PFNGLVERTEXATTRIBL3DPROC VertexAttribL3d;
+ PFNGLVERTEXATTRIBL3DVPROC VertexAttribL3dv;
+ PFNGLVERTEXATTRIBL4DPROC VertexAttribL4d;
+ PFNGLVERTEXATTRIBL4DVPROC VertexAttribL4dv;
+ PFNGLVERTEXATTRIBLFORMATPROC VertexAttribLFormat;
+ PFNGLVERTEXATTRIBLPOINTERPROC VertexAttribLPointer;
PFNGLVERTEXATTRIBP1UIPROC VertexAttribP1ui;
PFNGLVERTEXATTRIBP1UIVPROC VertexAttribP1uiv;
PFNGLVERTEXATTRIBP2UIPROC VertexAttribP2ui;
@@ -1779,7 +2649,11 @@ typedef struct GladGLContext {
PFNGLVERTEXATTRIBP4UIPROC VertexAttribP4ui;
PFNGLVERTEXATTRIBP4UIVPROC VertexAttribP4uiv;
PFNGLVERTEXATTRIBPOINTERPROC VertexAttribPointer;
+ PFNGLVERTEXBINDINGDIVISORPROC VertexBindingDivisor;
PFNGLVIEWPORTPROC Viewport;
+ PFNGLVIEWPORTARRAYVPROC ViewportArrayv;
+ PFNGLVIEWPORTINDEXEDFPROC ViewportIndexedf;
+ PFNGLVIEWPORTINDEXEDFVPROC ViewportIndexedfv;
PFNGLWAITSYNCPROC WaitSync;
void* glad_loader_handle;
diff --git a/vendor/glad/include/glad/glad.h b/vendor/glad/include/glad/glad.h
deleted file mode 100644
index f70d5b73f..000000000
--- a/vendor/glad/include/glad/glad.h
+++ /dev/null
@@ -1 +0,0 @@
-#include
diff --git a/vendor/glad/src/gl.c b/vendor/glad/src/gl.c
index ad49f387a..3eaf35450 100644
--- a/vendor/glad/src/gl.c
+++ b/vendor/glad/src/gl.c
@@ -90,6 +90,7 @@ static void glad_gl_load_GL_VERSION_1_1(GladGLContext *context, GLADuserptrloadf
context->DrawArrays = (PFNGLDRAWARRAYSPROC) load(userptr, "glDrawArrays");
context->DrawElements = (PFNGLDRAWELEMENTSPROC) load(userptr, "glDrawElements");
context->GenTextures = (PFNGLGENTEXTURESPROC) load(userptr, "glGenTextures");
+ context->GetPointerv = (PFNGLGETPOINTERVPROC) load(userptr, "glGetPointerv");
context->IsTexture = (PFNGLISTEXTUREPROC) load(userptr, "glIsTexture");
context->PolygonOffset = (PFNGLPOLYGONOFFSETPROC) load(userptr, "glPolygonOffset");
context->TexSubImage1D = (PFNGLTEXSUBIMAGE1DPROC) load(userptr, "glTexSubImage1D");
@@ -411,39 +412,229 @@ static void glad_gl_load_GL_VERSION_3_3(GladGLContext *context, GLADuserptrloadf
context->VertexAttribP4ui = (PFNGLVERTEXATTRIBP4UIPROC) load(userptr, "glVertexAttribP4ui");
context->VertexAttribP4uiv = (PFNGLVERTEXATTRIBP4UIVPROC) load(userptr, "glVertexAttribP4uiv");
}
+static void glad_gl_load_GL_VERSION_4_0(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {
+ if(!context->VERSION_4_0) return;
+ context->BeginQueryIndexed = (PFNGLBEGINQUERYINDEXEDPROC) load(userptr, "glBeginQueryIndexed");
+ context->BindTransformFeedback = (PFNGLBINDTRANSFORMFEEDBACKPROC) load(userptr, "glBindTransformFeedback");
+ context->BlendEquationSeparatei = (PFNGLBLENDEQUATIONSEPARATEIPROC) load(userptr, "glBlendEquationSeparatei");
+ context->BlendEquationi = (PFNGLBLENDEQUATIONIPROC) load(userptr, "glBlendEquationi");
+ context->BlendFuncSeparatei = (PFNGLBLENDFUNCSEPARATEIPROC) load(userptr, "glBlendFuncSeparatei");
+ context->BlendFunci = (PFNGLBLENDFUNCIPROC) load(userptr, "glBlendFunci");
+ context->DeleteTransformFeedbacks = (PFNGLDELETETRANSFORMFEEDBACKSPROC) load(userptr, "glDeleteTransformFeedbacks");
+ context->DrawArraysIndirect = (PFNGLDRAWARRAYSINDIRECTPROC) load(userptr, "glDrawArraysIndirect");
+ context->DrawElementsIndirect = (PFNGLDRAWELEMENTSINDIRECTPROC) load(userptr, "glDrawElementsIndirect");
+ context->DrawTransformFeedback = (PFNGLDRAWTRANSFORMFEEDBACKPROC) load(userptr, "glDrawTransformFeedback");
+ context->DrawTransformFeedbackStream = (PFNGLDRAWTRANSFORMFEEDBACKSTREAMPROC) load(userptr, "glDrawTransformFeedbackStream");
+ context->EndQueryIndexed = (PFNGLENDQUERYINDEXEDPROC) load(userptr, "glEndQueryIndexed");
+ context->GenTransformFeedbacks = (PFNGLGENTRANSFORMFEEDBACKSPROC) load(userptr, "glGenTransformFeedbacks");
+ context->GetActiveSubroutineName = (PFNGLGETACTIVESUBROUTINENAMEPROC) load(userptr, "glGetActiveSubroutineName");
+ context->GetActiveSubroutineUniformName = (PFNGLGETACTIVESUBROUTINEUNIFORMNAMEPROC) load(userptr, "glGetActiveSubroutineUniformName");
+ context->GetActiveSubroutineUniformiv = (PFNGLGETACTIVESUBROUTINEUNIFORMIVPROC) load(userptr, "glGetActiveSubroutineUniformiv");
+ context->GetProgramStageiv = (PFNGLGETPROGRAMSTAGEIVPROC) load(userptr, "glGetProgramStageiv");
+ context->GetQueryIndexediv = (PFNGLGETQUERYINDEXEDIVPROC) load(userptr, "glGetQueryIndexediv");
+ context->GetSubroutineIndex = (PFNGLGETSUBROUTINEINDEXPROC) load(userptr, "glGetSubroutineIndex");
+ context->GetSubroutineUniformLocation = (PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC) load(userptr, "glGetSubroutineUniformLocation");
+ context->GetUniformSubroutineuiv = (PFNGLGETUNIFORMSUBROUTINEUIVPROC) load(userptr, "glGetUniformSubroutineuiv");
+ context->GetUniformdv = (PFNGLGETUNIFORMDVPROC) load(userptr, "glGetUniformdv");
+ context->IsTransformFeedback = (PFNGLISTRANSFORMFEEDBACKPROC) load(userptr, "glIsTransformFeedback");
+ context->MinSampleShading = (PFNGLMINSAMPLESHADINGPROC) load(userptr, "glMinSampleShading");
+ context->PatchParameterfv = (PFNGLPATCHPARAMETERFVPROC) load(userptr, "glPatchParameterfv");
+ context->PatchParameteri = (PFNGLPATCHPARAMETERIPROC) load(userptr, "glPatchParameteri");
+ context->PauseTransformFeedback = (PFNGLPAUSETRANSFORMFEEDBACKPROC) load(userptr, "glPauseTransformFeedback");
+ context->ResumeTransformFeedback = (PFNGLRESUMETRANSFORMFEEDBACKPROC) load(userptr, "glResumeTransformFeedback");
+ context->Uniform1d = (PFNGLUNIFORM1DPROC) load(userptr, "glUniform1d");
+ context->Uniform1dv = (PFNGLUNIFORM1DVPROC) load(userptr, "glUniform1dv");
+ context->Uniform2d = (PFNGLUNIFORM2DPROC) load(userptr, "glUniform2d");
+ context->Uniform2dv = (PFNGLUNIFORM2DVPROC) load(userptr, "glUniform2dv");
+ context->Uniform3d = (PFNGLUNIFORM3DPROC) load(userptr, "glUniform3d");
+ context->Uniform3dv = (PFNGLUNIFORM3DVPROC) load(userptr, "glUniform3dv");
+ context->Uniform4d = (PFNGLUNIFORM4DPROC) load(userptr, "glUniform4d");
+ context->Uniform4dv = (PFNGLUNIFORM4DVPROC) load(userptr, "glUniform4dv");
+ context->UniformMatrix2dv = (PFNGLUNIFORMMATRIX2DVPROC) load(userptr, "glUniformMatrix2dv");
+ context->UniformMatrix2x3dv = (PFNGLUNIFORMMATRIX2X3DVPROC) load(userptr, "glUniformMatrix2x3dv");
+ context->UniformMatrix2x4dv = (PFNGLUNIFORMMATRIX2X4DVPROC) load(userptr, "glUniformMatrix2x4dv");
+ context->UniformMatrix3dv = (PFNGLUNIFORMMATRIX3DVPROC) load(userptr, "glUniformMatrix3dv");
+ context->UniformMatrix3x2dv = (PFNGLUNIFORMMATRIX3X2DVPROC) load(userptr, "glUniformMatrix3x2dv");
+ context->UniformMatrix3x4dv = (PFNGLUNIFORMMATRIX3X4DVPROC) load(userptr, "glUniformMatrix3x4dv");
+ context->UniformMatrix4dv = (PFNGLUNIFORMMATRIX4DVPROC) load(userptr, "glUniformMatrix4dv");
+ context->UniformMatrix4x2dv = (PFNGLUNIFORMMATRIX4X2DVPROC) load(userptr, "glUniformMatrix4x2dv");
+ context->UniformMatrix4x3dv = (PFNGLUNIFORMMATRIX4X3DVPROC) load(userptr, "glUniformMatrix4x3dv");
+ context->UniformSubroutinesuiv = (PFNGLUNIFORMSUBROUTINESUIVPROC) load(userptr, "glUniformSubroutinesuiv");
+}
+static void glad_gl_load_GL_VERSION_4_1(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {
+ if(!context->VERSION_4_1) return;
+ context->ActiveShaderProgram = (PFNGLACTIVESHADERPROGRAMPROC) load(userptr, "glActiveShaderProgram");
+ context->BindProgramPipeline = (PFNGLBINDPROGRAMPIPELINEPROC) load(userptr, "glBindProgramPipeline");
+ context->ClearDepthf = (PFNGLCLEARDEPTHFPROC) load(userptr, "glClearDepthf");
+ context->CreateShaderProgramv = (PFNGLCREATESHADERPROGRAMVPROC) load(userptr, "glCreateShaderProgramv");
+ context->DeleteProgramPipelines = (PFNGLDELETEPROGRAMPIPELINESPROC) load(userptr, "glDeleteProgramPipelines");
+ context->DepthRangeArrayv = (PFNGLDEPTHRANGEARRAYVPROC) load(userptr, "glDepthRangeArrayv");
+ context->DepthRangeIndexed = (PFNGLDEPTHRANGEINDEXEDPROC) load(userptr, "glDepthRangeIndexed");
+ context->DepthRangef = (PFNGLDEPTHRANGEFPROC) load(userptr, "glDepthRangef");
+ context->GenProgramPipelines = (PFNGLGENPROGRAMPIPELINESPROC) load(userptr, "glGenProgramPipelines");
+ context->GetDoublei_v = (PFNGLGETDOUBLEI_VPROC) load(userptr, "glGetDoublei_v");
+ context->GetFloati_v = (PFNGLGETFLOATI_VPROC) load(userptr, "glGetFloati_v");
+ context->GetProgramBinary = (PFNGLGETPROGRAMBINARYPROC) load(userptr, "glGetProgramBinary");
+ context->GetProgramPipelineInfoLog = (PFNGLGETPROGRAMPIPELINEINFOLOGPROC) load(userptr, "glGetProgramPipelineInfoLog");
+ context->GetProgramPipelineiv = (PFNGLGETPROGRAMPIPELINEIVPROC) load(userptr, "glGetProgramPipelineiv");
+ context->GetShaderPrecisionFormat = (PFNGLGETSHADERPRECISIONFORMATPROC) load(userptr, "glGetShaderPrecisionFormat");
+ context->GetVertexAttribLdv = (PFNGLGETVERTEXATTRIBLDVPROC) load(userptr, "glGetVertexAttribLdv");
+ context->IsProgramPipeline = (PFNGLISPROGRAMPIPELINEPROC) load(userptr, "glIsProgramPipeline");
+ context->ProgramBinary = (PFNGLPROGRAMBINARYPROC) load(userptr, "glProgramBinary");
+ context->ProgramParameteri = (PFNGLPROGRAMPARAMETERIPROC) load(userptr, "glProgramParameteri");
+ context->ProgramUniform1d = (PFNGLPROGRAMUNIFORM1DPROC) load(userptr, "glProgramUniform1d");
+ context->ProgramUniform1dv = (PFNGLPROGRAMUNIFORM1DVPROC) load(userptr, "glProgramUniform1dv");
+ context->ProgramUniform1f = (PFNGLPROGRAMUNIFORM1FPROC) load(userptr, "glProgramUniform1f");
+ context->ProgramUniform1fv = (PFNGLPROGRAMUNIFORM1FVPROC) load(userptr, "glProgramUniform1fv");
+ context->ProgramUniform1i = (PFNGLPROGRAMUNIFORM1IPROC) load(userptr, "glProgramUniform1i");
+ context->ProgramUniform1iv = (PFNGLPROGRAMUNIFORM1IVPROC) load(userptr, "glProgramUniform1iv");
+ context->ProgramUniform1ui = (PFNGLPROGRAMUNIFORM1UIPROC) load(userptr, "glProgramUniform1ui");
+ context->ProgramUniform1uiv = (PFNGLPROGRAMUNIFORM1UIVPROC) load(userptr, "glProgramUniform1uiv");
+ context->ProgramUniform2d = (PFNGLPROGRAMUNIFORM2DPROC) load(userptr, "glProgramUniform2d");
+ context->ProgramUniform2dv = (PFNGLPROGRAMUNIFORM2DVPROC) load(userptr, "glProgramUniform2dv");
+ context->ProgramUniform2f = (PFNGLPROGRAMUNIFORM2FPROC) load(userptr, "glProgramUniform2f");
+ context->ProgramUniform2fv = (PFNGLPROGRAMUNIFORM2FVPROC) load(userptr, "glProgramUniform2fv");
+ context->ProgramUniform2i = (PFNGLPROGRAMUNIFORM2IPROC) load(userptr, "glProgramUniform2i");
+ context->ProgramUniform2iv = (PFNGLPROGRAMUNIFORM2IVPROC) load(userptr, "glProgramUniform2iv");
+ context->ProgramUniform2ui = (PFNGLPROGRAMUNIFORM2UIPROC) load(userptr, "glProgramUniform2ui");
+ context->ProgramUniform2uiv = (PFNGLPROGRAMUNIFORM2UIVPROC) load(userptr, "glProgramUniform2uiv");
+ context->ProgramUniform3d = (PFNGLPROGRAMUNIFORM3DPROC) load(userptr, "glProgramUniform3d");
+ context->ProgramUniform3dv = (PFNGLPROGRAMUNIFORM3DVPROC) load(userptr, "glProgramUniform3dv");
+ context->ProgramUniform3f = (PFNGLPROGRAMUNIFORM3FPROC) load(userptr, "glProgramUniform3f");
+ context->ProgramUniform3fv = (PFNGLPROGRAMUNIFORM3FVPROC) load(userptr, "glProgramUniform3fv");
+ context->ProgramUniform3i = (PFNGLPROGRAMUNIFORM3IPROC) load(userptr, "glProgramUniform3i");
+ context->ProgramUniform3iv = (PFNGLPROGRAMUNIFORM3IVPROC) load(userptr, "glProgramUniform3iv");
+ context->ProgramUniform3ui = (PFNGLPROGRAMUNIFORM3UIPROC) load(userptr, "glProgramUniform3ui");
+ context->ProgramUniform3uiv = (PFNGLPROGRAMUNIFORM3UIVPROC) load(userptr, "glProgramUniform3uiv");
+ context->ProgramUniform4d = (PFNGLPROGRAMUNIFORM4DPROC) load(userptr, "glProgramUniform4d");
+ context->ProgramUniform4dv = (PFNGLPROGRAMUNIFORM4DVPROC) load(userptr, "glProgramUniform4dv");
+ context->ProgramUniform4f = (PFNGLPROGRAMUNIFORM4FPROC) load(userptr, "glProgramUniform4f");
+ context->ProgramUniform4fv = (PFNGLPROGRAMUNIFORM4FVPROC) load(userptr, "glProgramUniform4fv");
+ context->ProgramUniform4i = (PFNGLPROGRAMUNIFORM4IPROC) load(userptr, "glProgramUniform4i");
+ context->ProgramUniform4iv = (PFNGLPROGRAMUNIFORM4IVPROC) load(userptr, "glProgramUniform4iv");
+ context->ProgramUniform4ui = (PFNGLPROGRAMUNIFORM4UIPROC) load(userptr, "glProgramUniform4ui");
+ context->ProgramUniform4uiv = (PFNGLPROGRAMUNIFORM4UIVPROC) load(userptr, "glProgramUniform4uiv");
+ context->ProgramUniformMatrix2dv = (PFNGLPROGRAMUNIFORMMATRIX2DVPROC) load(userptr, "glProgramUniformMatrix2dv");
+ context->ProgramUniformMatrix2fv = (PFNGLPROGRAMUNIFORMMATRIX2FVPROC) load(userptr, "glProgramUniformMatrix2fv");
+ context->ProgramUniformMatrix2x3dv = (PFNGLPROGRAMUNIFORMMATRIX2X3DVPROC) load(userptr, "glProgramUniformMatrix2x3dv");
+ context->ProgramUniformMatrix2x3fv = (PFNGLPROGRAMUNIFORMMATRIX2X3FVPROC) load(userptr, "glProgramUniformMatrix2x3fv");
+ context->ProgramUniformMatrix2x4dv = (PFNGLPROGRAMUNIFORMMATRIX2X4DVPROC) load(userptr, "glProgramUniformMatrix2x4dv");
+ context->ProgramUniformMatrix2x4fv = (PFNGLPROGRAMUNIFORMMATRIX2X4FVPROC) load(userptr, "glProgramUniformMatrix2x4fv");
+ context->ProgramUniformMatrix3dv = (PFNGLPROGRAMUNIFORMMATRIX3DVPROC) load(userptr, "glProgramUniformMatrix3dv");
+ context->ProgramUniformMatrix3fv = (PFNGLPROGRAMUNIFORMMATRIX3FVPROC) load(userptr, "glProgramUniformMatrix3fv");
+ context->ProgramUniformMatrix3x2dv = (PFNGLPROGRAMUNIFORMMATRIX3X2DVPROC) load(userptr, "glProgramUniformMatrix3x2dv");
+ context->ProgramUniformMatrix3x2fv = (PFNGLPROGRAMUNIFORMMATRIX3X2FVPROC) load(userptr, "glProgramUniformMatrix3x2fv");
+ context->ProgramUniformMatrix3x4dv = (PFNGLPROGRAMUNIFORMMATRIX3X4DVPROC) load(userptr, "glProgramUniformMatrix3x4dv");
+ context->ProgramUniformMatrix3x4fv = (PFNGLPROGRAMUNIFORMMATRIX3X4FVPROC) load(userptr, "glProgramUniformMatrix3x4fv");
+ context->ProgramUniformMatrix4dv = (PFNGLPROGRAMUNIFORMMATRIX4DVPROC) load(userptr, "glProgramUniformMatrix4dv");
+ context->ProgramUniformMatrix4fv = (PFNGLPROGRAMUNIFORMMATRIX4FVPROC) load(userptr, "glProgramUniformMatrix4fv");
+ context->ProgramUniformMatrix4x2dv = (PFNGLPROGRAMUNIFORMMATRIX4X2DVPROC) load(userptr, "glProgramUniformMatrix4x2dv");
+ context->ProgramUniformMatrix4x2fv = (PFNGLPROGRAMUNIFORMMATRIX4X2FVPROC) load(userptr, "glProgramUniformMatrix4x2fv");
+ context->ProgramUniformMatrix4x3dv = (PFNGLPROGRAMUNIFORMMATRIX4X3DVPROC) load(userptr, "glProgramUniformMatrix4x3dv");
+ context->ProgramUniformMatrix4x3fv = (PFNGLPROGRAMUNIFORMMATRIX4X3FVPROC) load(userptr, "glProgramUniformMatrix4x3fv");
+ context->ReleaseShaderCompiler = (PFNGLRELEASESHADERCOMPILERPROC) load(userptr, "glReleaseShaderCompiler");
+ context->ScissorArrayv = (PFNGLSCISSORARRAYVPROC) load(userptr, "glScissorArrayv");
+ context->ScissorIndexed = (PFNGLSCISSORINDEXEDPROC) load(userptr, "glScissorIndexed");
+ context->ScissorIndexedv = (PFNGLSCISSORINDEXEDVPROC) load(userptr, "glScissorIndexedv");
+ context->ShaderBinary = (PFNGLSHADERBINARYPROC) load(userptr, "glShaderBinary");
+ context->UseProgramStages = (PFNGLUSEPROGRAMSTAGESPROC) load(userptr, "glUseProgramStages");
+ context->ValidateProgramPipeline = (PFNGLVALIDATEPROGRAMPIPELINEPROC) load(userptr, "glValidateProgramPipeline");
+ context->VertexAttribL1d = (PFNGLVERTEXATTRIBL1DPROC) load(userptr, "glVertexAttribL1d");
+ context->VertexAttribL1dv = (PFNGLVERTEXATTRIBL1DVPROC) load(userptr, "glVertexAttribL1dv");
+ context->VertexAttribL2d = (PFNGLVERTEXATTRIBL2DPROC) load(userptr, "glVertexAttribL2d");
+ context->VertexAttribL2dv = (PFNGLVERTEXATTRIBL2DVPROC) load(userptr, "glVertexAttribL2dv");
+ context->VertexAttribL3d = (PFNGLVERTEXATTRIBL3DPROC) load(userptr, "glVertexAttribL3d");
+ context->VertexAttribL3dv = (PFNGLVERTEXATTRIBL3DVPROC) load(userptr, "glVertexAttribL3dv");
+ context->VertexAttribL4d = (PFNGLVERTEXATTRIBL4DPROC) load(userptr, "glVertexAttribL4d");
+ context->VertexAttribL4dv = (PFNGLVERTEXATTRIBL4DVPROC) load(userptr, "glVertexAttribL4dv");
+ context->VertexAttribLPointer = (PFNGLVERTEXATTRIBLPOINTERPROC) load(userptr, "glVertexAttribLPointer");
+ context->ViewportArrayv = (PFNGLVIEWPORTARRAYVPROC) load(userptr, "glViewportArrayv");
+ context->ViewportIndexedf = (PFNGLVIEWPORTINDEXEDFPROC) load(userptr, "glViewportIndexedf");
+ context->ViewportIndexedfv = (PFNGLVIEWPORTINDEXEDFVPROC) load(userptr, "glViewportIndexedfv");
+}
+static void glad_gl_load_GL_VERSION_4_2(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {
+ if(!context->VERSION_4_2) return;
+ context->BindImageTexture = (PFNGLBINDIMAGETEXTUREPROC) load(userptr, "glBindImageTexture");
+ context->DrawArraysInstancedBaseInstance = (PFNGLDRAWARRAYSINSTANCEDBASEINSTANCEPROC) load(userptr, "glDrawArraysInstancedBaseInstance");
+ context->DrawElementsInstancedBaseInstance = (PFNGLDRAWELEMENTSINSTANCEDBASEINSTANCEPROC) load(userptr, "glDrawElementsInstancedBaseInstance");
+ context->DrawElementsInstancedBaseVertexBaseInstance = (PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXBASEINSTANCEPROC) load(userptr, "glDrawElementsInstancedBaseVertexBaseInstance");
+ context->DrawTransformFeedbackInstanced = (PFNGLDRAWTRANSFORMFEEDBACKINSTANCEDPROC) load(userptr, "glDrawTransformFeedbackInstanced");
+ context->DrawTransformFeedbackStreamInstanced = (PFNGLDRAWTRANSFORMFEEDBACKSTREAMINSTANCEDPROC) load(userptr, "glDrawTransformFeedbackStreamInstanced");
+ context->GetActiveAtomicCounterBufferiv = (PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC) load(userptr, "glGetActiveAtomicCounterBufferiv");
+ context->GetInternalformativ = (PFNGLGETINTERNALFORMATIVPROC) load(userptr, "glGetInternalformativ");
+ context->MemoryBarrier = (PFNGLMEMORYBARRIERPROC) load(userptr, "glMemoryBarrier");
+ context->TexStorage1D = (PFNGLTEXSTORAGE1DPROC) load(userptr, "glTexStorage1D");
+ context->TexStorage2D = (PFNGLTEXSTORAGE2DPROC) load(userptr, "glTexStorage2D");
+ context->TexStorage3D = (PFNGLTEXSTORAGE3DPROC) load(userptr, "glTexStorage3D");
+}
+static void glad_gl_load_GL_VERSION_4_3(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) {
+ if(!context->VERSION_4_3) return;
+ context->BindVertexBuffer = (PFNGLBINDVERTEXBUFFERPROC) load(userptr, "glBindVertexBuffer");
+ context->ClearBufferData = (PFNGLCLEARBUFFERDATAPROC) load(userptr, "glClearBufferData");
+ context->ClearBufferSubData = (PFNGLCLEARBUFFERSUBDATAPROC) load(userptr, "glClearBufferSubData");
+ context->CopyImageSubData = (PFNGLCOPYIMAGESUBDATAPROC) load(userptr, "glCopyImageSubData");
+ context->DebugMessageCallback = (PFNGLDEBUGMESSAGECALLBACKPROC) load(userptr, "glDebugMessageCallback");
+ context->DebugMessageControl = (PFNGLDEBUGMESSAGECONTROLPROC) load(userptr, "glDebugMessageControl");
+ context->DebugMessageInsert = (PFNGLDEBUGMESSAGEINSERTPROC) load(userptr, "glDebugMessageInsert");
+ context->DispatchCompute = (PFNGLDISPATCHCOMPUTEPROC) load(userptr, "glDispatchCompute");
+ context->DispatchComputeIndirect = (PFNGLDISPATCHCOMPUTEINDIRECTPROC) load(userptr, "glDispatchComputeIndirect");
+ context->FramebufferParameteri = (PFNGLFRAMEBUFFERPARAMETERIPROC) load(userptr, "glFramebufferParameteri");
+ context->GetDebugMessageLog = (PFNGLGETDEBUGMESSAGELOGPROC) load(userptr, "glGetDebugMessageLog");
+ context->GetFramebufferParameteriv = (PFNGLGETFRAMEBUFFERPARAMETERIVPROC) load(userptr, "glGetFramebufferParameteriv");
+ context->GetInternalformati64v = (PFNGLGETINTERNALFORMATI64VPROC) load(userptr, "glGetInternalformati64v");
+ context->GetObjectLabel = (PFNGLGETOBJECTLABELPROC) load(userptr, "glGetObjectLabel");
+ context->GetObjectPtrLabel = (PFNGLGETOBJECTPTRLABELPROC) load(userptr, "glGetObjectPtrLabel");
+ context->GetPointerv = (PFNGLGETPOINTERVPROC) load(userptr, "glGetPointerv");
+ context->GetProgramInterfaceiv = (PFNGLGETPROGRAMINTERFACEIVPROC) load(userptr, "glGetProgramInterfaceiv");
+ context->GetProgramResourceIndex = (PFNGLGETPROGRAMRESOURCEINDEXPROC) load(userptr, "glGetProgramResourceIndex");
+ context->GetProgramResourceLocation = (PFNGLGETPROGRAMRESOURCELOCATIONPROC) load(userptr, "glGetProgramResourceLocation");
+ context->GetProgramResourceLocationIndex = (PFNGLGETPROGRAMRESOURCELOCATIONINDEXPROC) load(userptr, "glGetProgramResourceLocationIndex");
+ context->GetProgramResourceName = (PFNGLGETPROGRAMRESOURCENAMEPROC) load(userptr, "glGetProgramResourceName");
+ context->GetProgramResourceiv = (PFNGLGETPROGRAMRESOURCEIVPROC) load(userptr, "glGetProgramResourceiv");
+ context->InvalidateBufferData = (PFNGLINVALIDATEBUFFERDATAPROC) load(userptr, "glInvalidateBufferData");
+ context->InvalidateBufferSubData = (PFNGLINVALIDATEBUFFERSUBDATAPROC) load(userptr, "glInvalidateBufferSubData");
+ context->InvalidateFramebuffer = (PFNGLINVALIDATEFRAMEBUFFERPROC) load(userptr, "glInvalidateFramebuffer");
+ context->InvalidateSubFramebuffer = (PFNGLINVALIDATESUBFRAMEBUFFERPROC) load(userptr, "glInvalidateSubFramebuffer");
+ context->InvalidateTexImage = (PFNGLINVALIDATETEXIMAGEPROC) load(userptr, "glInvalidateTexImage");
+ context->InvalidateTexSubImage = (PFNGLINVALIDATETEXSUBIMAGEPROC) load(userptr, "glInvalidateTexSubImage");
+ context->MultiDrawArraysIndirect = (PFNGLMULTIDRAWARRAYSINDIRECTPROC) load(userptr, "glMultiDrawArraysIndirect");
+ context->MultiDrawElementsIndirect = (PFNGLMULTIDRAWELEMENTSINDIRECTPROC) load(userptr, "glMultiDrawElementsIndirect");
+ context->ObjectLabel = (PFNGLOBJECTLABELPROC) load(userptr, "glObjectLabel");
+ context->ObjectPtrLabel = (PFNGLOBJECTPTRLABELPROC) load(userptr, "glObjectPtrLabel");
+ context->PopDebugGroup = (PFNGLPOPDEBUGGROUPPROC) load(userptr, "glPopDebugGroup");
+ context->PushDebugGroup = (PFNGLPUSHDEBUGGROUPPROC) load(userptr, "glPushDebugGroup");
+ context->ShaderStorageBlockBinding = (PFNGLSHADERSTORAGEBLOCKBINDINGPROC) load(userptr, "glShaderStorageBlockBinding");
+ context->TexBufferRange = (PFNGLTEXBUFFERRANGEPROC) load(userptr, "glTexBufferRange");
+ context->TexStorage2DMultisample = (PFNGLTEXSTORAGE2DMULTISAMPLEPROC) load(userptr, "glTexStorage2DMultisample");
+ context->TexStorage3DMultisample = (PFNGLTEXSTORAGE3DMULTISAMPLEPROC) load(userptr, "glTexStorage3DMultisample");
+ context->TextureView = (PFNGLTEXTUREVIEWPROC) load(userptr, "glTextureView");
+ context->VertexAttribBinding = (PFNGLVERTEXATTRIBBINDINGPROC) load(userptr, "glVertexAttribBinding");
+ context->VertexAttribFormat = (PFNGLVERTEXATTRIBFORMATPROC) load(userptr, "glVertexAttribFormat");
+ context->VertexAttribIFormat = (PFNGLVERTEXATTRIBIFORMATPROC) load(userptr, "glVertexAttribIFormat");
+ context->VertexAttribLFormat = (PFNGLVERTEXATTRIBLFORMATPROC) load(userptr, "glVertexAttribLFormat");
+ context->VertexBindingDivisor = (PFNGLVERTEXBINDINGDIVISORPROC) load(userptr, "glVertexBindingDivisor");
+}
-#if defined(GL_ES_VERSION_3_0) || defined(GL_VERSION_3_0)
-#define GLAD_GL_IS_SOME_NEW_VERSION 1
-#else
-#define GLAD_GL_IS_SOME_NEW_VERSION 0
-#endif
-
-static int glad_gl_get_extensions(GladGLContext *context, int version, const char **out_exts, unsigned int *out_num_exts_i, char ***out_exts_i) {
-#if GLAD_GL_IS_SOME_NEW_VERSION
- if(GLAD_VERSION_MAJOR(version) < 3) {
-#else
- GLAD_UNUSED(version);
- GLAD_UNUSED(out_num_exts_i);
- GLAD_UNUSED(out_exts_i);
-#endif
- if (context->GetString == NULL) {
- return 0;
+static void glad_gl_free_extensions(char **exts_i) {
+ if (exts_i != NULL) {
+ unsigned int index;
+ for(index = 0; exts_i[index]; index++) {
+ free((void *) (exts_i[index]));
}
- *out_exts = (const char *)context->GetString(GL_EXTENSIONS);
-#if GLAD_GL_IS_SOME_NEW_VERSION
- } else {
+ free((void *)exts_i);
+ exts_i = NULL;
+ }
+}
+static int glad_gl_get_extensions(GladGLContext *context, const char **out_exts, char ***out_exts_i) {
+#if defined(GL_ES_VERSION_3_0) || defined(GL_VERSION_3_0)
+ if (context->GetStringi != NULL && context->GetIntegerv != NULL) {
unsigned int index = 0;
unsigned int num_exts_i = 0;
char **exts_i = NULL;
- if (context->GetStringi == NULL || context->GetIntegerv == NULL) {
- return 0;
- }
context->GetIntegerv(GL_NUM_EXTENSIONS, (int*) &num_exts_i);
- if (num_exts_i > 0) {
- exts_i = (char **) malloc(num_exts_i * (sizeof *exts_i));
- }
+ exts_i = (char **) malloc((num_exts_i + 1) * (sizeof *exts_i));
if (exts_i == NULL) {
return 0;
}
@@ -452,31 +643,40 @@ static int glad_gl_get_extensions(GladGLContext *context, int version, const cha
size_t len = strlen(gl_str_tmp) + 1;
char *local_str = (char*) malloc(len * sizeof(char));
- if(local_str != NULL) {
- memcpy(local_str, gl_str_tmp, len * sizeof(char));
+ if(local_str == NULL) {
+ exts_i[index] = NULL;
+ glad_gl_free_extensions(exts_i);
+ return 0;
}
+ memcpy(local_str, gl_str_tmp, len * sizeof(char));
exts_i[index] = local_str;
}
+ exts_i[index] = NULL;
- *out_num_exts_i = num_exts_i;
*out_exts_i = exts_i;
+
+ return 1;
}
+#else
+ GLAD_UNUSED(out_exts_i);
#endif
+ if (context->GetString == NULL) {
+ return 0;
+ }
+ *out_exts = (const char *)context->GetString(GL_EXTENSIONS);
return 1;
}
-static void glad_gl_free_extensions(char **exts_i, unsigned int num_exts_i) {
- if (exts_i != NULL) {
+static int glad_gl_has_extension(const char *exts, char **exts_i, const char *ext) {
+ if(exts_i) {
unsigned int index;
- for(index = 0; index < num_exts_i; index++) {
- free((void *) (exts_i[index]));
+ for(index = 0; exts_i[index]; index++) {
+ const char *e = exts_i[index];
+ if(strcmp(e, ext) == 0) {
+ return 1;
+ }
}
- free((void *)exts_i);
- exts_i = NULL;
- }
-}
-static int glad_gl_has_extension(int version, const char *exts, unsigned int num_exts_i, char **exts_i, const char *ext) {
- if(GLAD_VERSION_MAJOR(version) < 3 || !GLAD_GL_IS_SOME_NEW_VERSION) {
+ } else {
const char *extensions;
const char *loc;
const char *terminator;
@@ -496,14 +696,6 @@ static int glad_gl_has_extension(int version, const char *exts, unsigned int num
}
extensions = terminator;
}
- } else {
- unsigned int index;
- for(index = 0; index < num_exts_i; index++) {
- const char *e = exts_i[index];
- if(strcmp(e, ext) == 0) {
- return 1;
- }
- }
}
return 0;
}
@@ -512,15 +704,14 @@ static GLADapiproc glad_gl_get_proc_from_userptr(void *userptr, const char* name
return (GLAD_GNUC_EXTENSION (GLADapiproc (*)(const char *name)) userptr)(name);
}
-static int glad_gl_find_extensions_gl(GladGLContext *context, int version) {
+static int glad_gl_find_extensions_gl(GladGLContext *context) {
const char *exts = NULL;
- unsigned int num_exts_i = 0;
char **exts_i = NULL;
- if (!glad_gl_get_extensions(context, version, &exts, &num_exts_i, &exts_i)) return 0;
+ if (!glad_gl_get_extensions(context, &exts, &exts_i)) return 0;
- GLAD_UNUSED(glad_gl_has_extension);
+ GLAD_UNUSED(&glad_gl_has_extension);
- glad_gl_free_extensions(exts_i, num_exts_i);
+ glad_gl_free_extensions(exts_i);
return 1;
}
@@ -561,6 +752,10 @@ static int glad_gl_find_core_gl(GladGLContext *context) {
context->VERSION_3_1 = (major == 3 && minor >= 1) || major > 3;
context->VERSION_3_2 = (major == 3 && minor >= 2) || major > 3;
context->VERSION_3_3 = (major == 3 && minor >= 3) || major > 3;
+ context->VERSION_4_0 = (major == 4 && minor >= 0) || major > 4;
+ context->VERSION_4_1 = (major == 4 && minor >= 1) || major > 4;
+ context->VERSION_4_2 = (major == 4 && minor >= 2) || major > 4;
+ context->VERSION_4_3 = (major == 4 && minor >= 3) || major > 4;
return GLAD_MAKE_VERSION(major, minor);
}
@@ -570,7 +765,6 @@ int gladLoadGLContextUserPtr(GladGLContext *context, GLADuserptrloadfunc load, v
context->GetString = (PFNGLGETSTRINGPROC) load(userptr, "glGetString");
if(context->GetString == NULL) return 0;
- if(context->GetString(GL_VERSION) == NULL) return 0;
version = glad_gl_find_core_gl(context);
glad_gl_load_GL_VERSION_1_0(context, load, userptr);
@@ -585,8 +779,12 @@ int gladLoadGLContextUserPtr(GladGLContext *context, GLADuserptrloadfunc load, v
glad_gl_load_GL_VERSION_3_1(context, load, userptr);
glad_gl_load_GL_VERSION_3_2(context, load, userptr);
glad_gl_load_GL_VERSION_3_3(context, load, userptr);
+ glad_gl_load_GL_VERSION_4_0(context, load, userptr);
+ glad_gl_load_GL_VERSION_4_1(context, load, userptr);
+ glad_gl_load_GL_VERSION_4_2(context, load, userptr);
+ glad_gl_load_GL_VERSION_4_3(context, load, userptr);
- if (!glad_gl_find_extensions_gl(context, version)) return 0;
+ if (!glad_gl_find_extensions_gl(context)) return 0;