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;