From 6d283c012e19f7b09eb2a07b19133f093b5270a0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 17 Jun 2025 13:34:18 -0700 Subject: [PATCH 01/89] ci: build macOS releases with Xcode 26 Resolves #7591 This moves our CI to build macOS on Sequoia (macOS 15) with Xcode 26, including the new macOS 26 beta SDK. Importantly, this will make our builds on macOS 26 use the new styling. I've added a new job that ensures we can continue to build with Xcode 16 and the macOS 15 SDK, as well, although I think that might come to an end when we switch over to an IconComposer-based icon. I'll verify then. For now, we continue to support both. I've also removed our `hasLiquidGlass` check, since this will now always be true for macOS 26 builds. --- .github/workflows/release-pr.yml | 4 +- .github/workflows/release-tip.yml | 6 +- .github/workflows/test.yml | 56 ++++++++++++++----- .../Terminal/TerminalController.swift | 2 +- .../Window Styles/TerminalWindow.swift | 2 +- .../TransparentTitlebarTerminalWindow.swift | 10 ++-- macos/Sources/Helpers/AppInfo.swift | 34 ----------- 7 files changed, 56 insertions(+), 58 deletions(-) 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..2eca0a41e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,7 @@ jobs: - build-nix - build-snap - build-macos + - build-macos-sequoia-stable - build-macos-tahoe - build-macos-matrix - build-windows @@ -270,6 +271,46 @@ jobs: ghostty-source.tar.gz build-macos: + runs-on: namespace-profile-ghostty-macos-sequoia + needs: test + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # Install Nix and use that to run our tests so our environment matches exactly. + - uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@v16 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Xcode Select + run: sudo xcode-select -s /Applications/Xcode_26.0.app + + - name: get the Zig deps + id: deps + run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT + + # GhosttyKit is the framework that is built from Zig for our native + # Mac app to access. + - name: Build GhosttyKit + run: nix develop -c zig build --system ${{ steps.deps.outputs.deps }} + + # The native app is built with native Xcode tooling. This also does + # codesigning. IMPORTANT: this must NOT run in a Nix environment. + # Nix breaks xcodebuild so this has to be run outside. + - name: Build Ghostty.app + run: cd macos && xcodebuild -target Ghostty + + # Build the iOS target without code signing just to verify it works. + - name: Build Ghostty iOS + run: | + cd macos + xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" + + build-macos-sequoia-stable: runs-on: namespace-profile-ghostty-macos-sequoia needs: test steps: @@ -328,17 +369,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 +407,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 +725,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/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 49b3fea34..03a4e548e 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -16,7 +16,7 @@ class TerminalController: BaseTerminalController { case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" case "tabs": - if #available(macOS 26.0, *), hasLiquidGlass() { + if #available(macOS 26.0, *) { "TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index e24323113..f9dfb9591 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -447,7 +447,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/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 -} From e6c77789d341742aa80d7387cfba67bba9843b75 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 17 Jun 2025 15:02:59 -0700 Subject: [PATCH 02/89] macOS: Confirm close on window close Fixes #7615 We were incorrectly closing the window without confirmation when there were no tabs. --- .../Features/Terminal/TerminalController.swift | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 03a4e548e..2e4fb7363 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -1001,20 +1001,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 } From 51b9fa751a13fab2884e37292159379372f4da91 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 17 Jun 2025 16:13:23 -0700 Subject: [PATCH 03/89] macos: disambiguate close tab vs close window for confirmation This fixes an issue where pressing the red close button in a window or the "x" button on a tab couldn't differentiate and would always close the tab or close the window (depending on tab counts). It seems like in both cases, AppKit triggers the `windowShouldClose` delegate method on the controller, but for the close window case it triggers this on ALL the windows in the group, not just the one that was clicked. I implemented a kind of silly coordinator that debounces `windowShouldClose` calls over 100ms and uses that to differentiate between the two cases. --- macos/Ghostty.xcodeproj/project.pbxproj | 6 +- .../Terminal/TerminalController.swift | 21 +-- .../Extensions/NSWindow+Extension.swift | 6 + .../Helpers/TabGroupCloseCoordinator.swift | 124 ++++++++++++++++++ 4 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 macos/Sources/Helpers/TabGroupCloseCoordinator.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index a5663202b..5c584709e 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -119,6 +119,7 @@ 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 */; }; 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 */; }; @@ -238,6 +239,7 @@ 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 = ""; }; 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 = ""; }; @@ -320,12 +322,13 @@ 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 */, + A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */, A5CA378D2D31D6C100931030 /* Weak.swift */, C1F26EE72B76CBFC00404083 /* VibrantLayer.h */, C1F26EE82B76CBFC00404083 /* VibrantLayer.m */, @@ -792,6 +795,7 @@ A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */, A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */, A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */, + A5E4082A2E022E9E0035FEAC /* TabGroupCloseCoordinator.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, A53A29882DB69D2F00B6E02C /* TerminalCommandPalette.swift in Sources */, A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 03a4e548e..01ed25e63 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -5,7 +5,7 @@ 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" @@ -882,14 +882,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 @@ -1270,4 +1276,3 @@ extension TerminalController: NSMenuItemValidation { } } } - 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/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 = [:] + } +} From 559fd922959905e12dabfdecf8b2a78db8ecda22 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 17 Jun 2025 16:23:15 -0700 Subject: [PATCH 04/89] build: use `xcrun --sdk metal` for metal paths This wasn't working before but it just requires a restart of the machine for the changes to take effect. The namespace runners have this prebuilt so this should work now. The other workaround was flaky for unknown reasons so I'd prefer to go back to this. --- src/build/MetallibStep.zig | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/src/build/MetallibStep.zig b/src/build/MetallibStep.zig index bac3a72c5..b7405c496 100644 --- a/src/build/MetallibStep.zig +++ b/src/build/MetallibStep.zig @@ -22,10 +22,11 @@ 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 => "iphoneos", + else => return null, + }; const self = b.allocator.create(MetallibStep) catch @panic("OOM"); @@ -37,31 +38,11 @@ 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); @@ -81,7 +62,7 @@ pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { 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); From 7d2da23021921551e977413c87ddc675a1a7beb9 Mon Sep 17 00:00:00 2001 From: Ken VanDine Date: Wed, 18 Jun 2025 10:06:35 -0400 Subject: [PATCH 05/89] snap: vendor libgtk4-layer-shell.so --- snap/snapcraft.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 From b89cb59d792ac0a6b6eec52ae517b3ae3311e66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Victor=20Ribeiro=20Silva?= Date: Fri, 20 Jun 2025 10:23:10 -0300 Subject: [PATCH 06/89] translation(pt_BR): add missing translation --- po/pt_BR.UTF-8.po | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index d2ba0e693..c7bdf4df7 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -9,15 +9,16 @@ 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" -"Language-Team: Brazilian Portuguese \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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 3.6\n" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" @@ -174,8 +175,8 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Uma aplicação está tentando ler da área de transferência. O conteúdo atual " -"da área de transferência está sendo exibido abaixo." +"Uma aplicação está tentando ler da área de transferência. O conteúdo atual da " +"área de transferência está sendo exibido abaixo." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -189,8 +190,8 @@ msgstr "Permitir" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." +"An application is attempting to write to the clipboard. The current clipboard " +"contents are shown below." msgstr "" "Uma aplicação está tentando escrever na área de transferência. O conteúdo " "atual da área de transferência está aparecendo abaixo." @@ -217,11 +218,10 @@ 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 "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgid "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "" "⚠️ Você está rodando uma build de debug do Ghostty! O desempenho será afetado." From fda08a699987c2caf8585e66d4f2111c369f855b Mon Sep 17 00:00:00 2001 From: Zhaofeng Li Date: Fri, 20 Jun 2025 14:02:07 -0600 Subject: [PATCH 07/89] build: Use correct SDK for iOS Simulator shader build --- src/build/MetallibStep.zig | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/build/MetallibStep.zig b/src/build/MetallibStep.zig index b7405c496..1067e519c 100644 --- a/src/build/MetallibStep.zig +++ b/src/build/MetallibStep.zig @@ -24,9 +24,20 @@ output: LazyPath, pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { const sdk = switch (opts.target.result.os.tag) { .macos => "macosx", - .ios => "iphoneos", + .ios => switch (opts.target.result.abi) { + .simulator => "iphonesimulator", + 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"); @@ -46,16 +57,11 @@ pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { 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( From f40cd3cae3469da4c5a70e637c3243d7a4c2f804 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 19 Jan 2025 16:47:08 -0500 Subject: [PATCH 08/89] chore: improve Metal API definitions a bit --- src/renderer/Metal.zig | 16 ++- src/renderer/metal/api.zig | 225 +++++++++++++++++++++++++++++++++++-- 2 files changed, 228 insertions(+), 13 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 99dbc838e..639ef354b 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -2337,9 +2337,11 @@ pub fn setScreenSize( 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), + mtl.MTLTextureUsage{ + .render_target = true, + .shader_read = true, + .shader_write = true, + }, ); // If we fail to create the texture, then we just don't have a screen @@ -2377,9 +2379,11 @@ pub fn setScreenSize( 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), + mtl.MTLTextureUsage{ + .render_target = true, + .shader_read = true, + .shader_write = true, + }, ); // If we fail to create the texture, then we just don't have a screen diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index 46cb4f6bc..90a1a65ab 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,32 @@ 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, }; pub const MTLClearColor = extern struct { From 77c050c156945a8bc7f1e46f732e33e842a58fe0 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 19 Jan 2025 18:28:36 -0500 Subject: [PATCH 09/89] refactor(Metal): make pipeline handling DRYer --- src/renderer/metal/shaders.zig | 474 +++++++++++---------------------- 1 file changed, 151 insertions(+), 323 deletions(-) diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 8fa170bf2..ff5f1e6bd 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -106,7 +106,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. @@ -171,7 +171,7 @@ 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 + // 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. resolution: [3]f32 align(16), @@ -282,65 +282,16 @@ 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 }, + return (Pipeline{ + .vertex_fn = "full_screen_vertex", + .fragment_fn = "main0", + .blending_enabled = false, + }).init( + device, + library, + post_library, + pixel_format, ); - try checkError(err); - - return pipeline_state; } /// This is a single parameter for the terminal cell shader. @@ -374,113 +325,18 @@ fn initCellTextPipeline( 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 }, + return (Pipeline{ + .vertex_fn = "cell_text_vertex", + .fragment_fn = "cell_text_fragment", + .Vertex = CellText, + .step_fn = .per_instance, + .blending_enabled = true, + }).init( + device, + library, + library, + pixel_format, ); - try checkError(err); - errdefer pipeline_state.msgSend(void, objc.sel("release"), .{}); - - return pipeline_state; } /// This is a single parameter for the cell bg shader. @@ -492,78 +348,16 @@ fn initCellBgPipeline( 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 }, + return (Pipeline{ + .vertex_fn = "cell_bg_vertex", + .fragment_fn = "cell_bg_fragment", + .blending_enabled = false, + }).init( + device, + library, + library, + pixel_format, ); - try checkError(err); - errdefer pipeline_state.msgSend(void, objc.sel("release"), .{}); - - return pipeline_state; } /// Initialize the image render pipeline for our shader library. @@ -572,113 +366,147 @@ fn initImagePipeline( 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(); + return (Pipeline{ + .vertex_fn = "image_vertex", + .fragment_fn = "image_fragment", + .Vertex = Image, + .step_fn = .per_instance, + .blending_enabled = true, + }).init( + device, + library, + library, + pixel_format, + ); +} - 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(); +/// A struct with all the necessary info to initialize a pipeline. +const Pipeline = struct { + /// Name of the vertex function + vertex_fn: []const u8, + /// Name of the fragment function + fragment_fn: []const u8, - 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"), .{}); + /// Vertex attribute struct + Vertex: ?type = null, + /// Vertex step function + step_fn: mtl.MTLVertexStepFunction = .per_vertex, - // 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: { + /// Whether blending is enabled for the color attachment + blending_enabled: bool = true, + + fn init( + self: *const Pipeline, + device: objc.Object, + vertex_library: objc.Object, + fragment_library: objc.Object, + pixel_format: mtl.MTLPixelFormat, + ) !objc.Object { + // Create our descriptor const desc = init: { - const Class = objc.getClass("MTLVertexDescriptor").?; + 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"), .{}); - // 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")); + // Get our vertex and fragment functions and add them to the descriptor. { - const layout = layouts.msgSend( + const str = try macos.foundation.String.createWithBytes( + self.vertex_fn, + .utf8, + false, + ); + defer str.release(); + + const ptr = 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( + self.fragment_fn, + .utf8, + false, + ); + defer str.release(); + + const ptr = 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 (self.Vertex) |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(self.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")); + { + const attachment = attachments.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))); + attachment.setProperty("pixelFormat", @intFromEnum(pixel_format)); + + attachment.setProperty("blendingEnabled", self.blending_enabled); + // We always use premultiplied alpha blending for now. + if (self.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)); + } } - 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( + // Make our state + var err: ?*anyopaque = null; + const pipeline_state = device.msgSend( objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, + objc.sel("newRenderPipelineStateWithDescriptor:error:"), + .{ desc, &err }, ); + try checkError(err); + errdefer pipeline_state.msgSend(void, objc.sel("release"), .{}); - 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)); + return pipeline_state; } - - // 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| { From 7cfc906c607d94b19613fff17bf261c36f0fca92 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 9 Apr 2025 15:26:28 -0600 Subject: [PATCH 10/89] debug: properly set thread names on macOS --- src/crash/sentry.zig | 7 +++++++ src/os/cf_release_thread.zig | 8 ++++++++ src/os/macos.zig | 4 ++++ src/renderer/Thread.zig | 7 +++++++ src/termio/Exec.zig | 7 +++++++ src/termio/Thread.zig | 8 ++++++++ 6 files changed, 41 insertions(+) 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/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/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/renderer/Thread.zig b/src/renderer/Thread.zig index 1e9c29b26..52f599549 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -198,6 +198,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, 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/Thread.zig b/src/termio/Thread.zig index d8018341d..35da3c2d2 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; @@ -202,6 +203,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, From 521872442a9029031615b0c672781cc46e7fd106 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 18 May 2025 19:39:17 -0600 Subject: [PATCH 11/89] vendor: update glad to OpenGL 4.3 --- vendor/glad/include/glad/gl.h | 884 +++++++++++++++++++++++++++++++- vendor/glad/include/glad/glad.h | 1 - vendor/glad/src/gl.c | 304 +++++++++-- 3 files changed, 1130 insertions(+), 59 deletions(-) delete mode 100644 vendor/glad/include/glad/glad.h 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; From 371d62a82ce26a6dd7c61d0175759f2d53771065 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 16 Jun 2025 17:44:44 -0600 Subject: [PATCH 12/89] renderer: big rework, graphics API abstraction layers, unified logic This commit is very large, representing about a month of work with many interdependent changes that don't separate cleanly in to atomic commits. The main change here is unifying the renderer logic to a single generic renderer, implemented on top of an abstraction layer over OpenGL/Metal. I'll write a more complete summary of the changes in the description of the PR. --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 9 - pkg/macos/animation.zig | 2 + pkg/macos/build.zig | 2 + pkg/macos/dispatch.zig | 10 + pkg/macos/foundation.zig | 1 + pkg/macos/foundation/type.zig | 1 + pkg/macos/iosurface.zig | 8 + pkg/macos/iosurface/c.zig | 1 + pkg/macos/iosurface/iosurface.zig | 136 + pkg/macos/main.zig | 3 + pkg/macos/video.zig | 2 + pkg/macos/video/pixel_format.zig | 171 + pkg/opengl/Buffer.zig | 7 +- pkg/opengl/Framebuffer.zig | 24 + pkg/opengl/Renderbuffer.zig | 56 + pkg/opengl/Texture.zig | 29 +- pkg/opengl/VertexArray.zig | 84 + pkg/opengl/draw.zig | 39 +- pkg/opengl/main.zig | 9 + pkg/opengl/primitives.zig | 18 + src/Surface.zig | 5 +- src/apprt/gtk/App.zig | 5 + src/apprt/gtk/Surface.zig | 20 +- src/config/Config.zig | 10 +- src/renderer.zig | 5 +- src/renderer/Metal.zig | 3441 ++--------------- src/renderer/OpenGL.zig | 2799 ++------------ src/renderer/Options.zig | 3 + src/renderer/Thread.zig | 32 +- src/renderer/generic.zig | 2866 ++++++++++++++ src/renderer/metal/Frame.zig | 137 + src/renderer/metal/IOSurfaceLayer.zig | 187 + src/renderer/metal/Pipeline.zig | 203 + src/renderer/metal/RenderPass.zig | 220 ++ src/renderer/metal/Target.zig | 110 + src/renderer/metal/Texture.zig | 196 + src/renderer/metal/api.zig | 18 +- src/renderer/metal/buffer.zig | 77 +- src/renderer/metal/image.zig | 98 +- src/renderer/metal/sampler.zig | 38 - src/renderer/metal/shaders.zig | 331 +- src/renderer/opengl/CellProgram.zig | 196 - src/renderer/opengl/Frame.zig | 75 + src/renderer/opengl/ImageProgram.zig | 134 - src/renderer/opengl/Pipeline.zig | 170 + src/renderer/opengl/RenderPass.zig | 141 + src/renderer/opengl/Target.zig | 62 + src/renderer/opengl/Texture.zig | 99 + src/renderer/opengl/buffer.zig | 127 + src/renderer/opengl/cell.zig | 220 ++ src/renderer/opengl/custom.zig | 310 -- src/renderer/opengl/image.zig | 49 +- src/renderer/opengl/shaders.zig | 310 ++ src/renderer/shaders/cell.f.glsl | 53 - src/renderer/shaders/cell.metal | 68 +- src/renderer/shaders/cell.v.glsl | 258 -- src/renderer/shaders/custom.v.glsl | 8 - src/renderer/shaders/glsl/cell_bg.f.glsl | 61 + src/renderer/shaders/glsl/cell_text.f.glsl | 109 + src/renderer/shaders/glsl/cell_text.v.glsl | 162 + src/renderer/shaders/glsl/common.glsl | 155 + src/renderer/shaders/glsl/full_screen.v.glsl | 24 + src/renderer/shaders/glsl/image.f.glsl | 21 + src/renderer/shaders/glsl/image.v.glsl | 46 + src/renderer/shaders/image.f.glsl | 29 - src/renderer/shaders/image.v.glsl | 44 - src/renderer/shaders/shadertoy_prefix.glsl | 30 +- src/renderer/shadertoy.zig | 25 +- 68 files changed, 7088 insertions(+), 7311 deletions(-) create mode 100644 pkg/macos/iosurface.zig create mode 100644 pkg/macos/iosurface/c.zig create mode 100644 pkg/macos/iosurface/iosurface.zig create mode 100644 pkg/macos/video/pixel_format.zig create mode 100644 pkg/opengl/Renderbuffer.zig create mode 100644 pkg/opengl/primitives.zig create mode 100644 src/renderer/generic.zig create mode 100644 src/renderer/metal/Frame.zig create mode 100644 src/renderer/metal/IOSurfaceLayer.zig create mode 100644 src/renderer/metal/Pipeline.zig create mode 100644 src/renderer/metal/RenderPass.zig create mode 100644 src/renderer/metal/Target.zig create mode 100644 src/renderer/metal/Texture.zig delete mode 100644 src/renderer/metal/sampler.zig delete mode 100644 src/renderer/opengl/CellProgram.zig create mode 100644 src/renderer/opengl/Frame.zig delete mode 100644 src/renderer/opengl/ImageProgram.zig create mode 100644 src/renderer/opengl/Pipeline.zig create mode 100644 src/renderer/opengl/RenderPass.zig create mode 100644 src/renderer/opengl/Target.zig create mode 100644 src/renderer/opengl/Texture.zig create mode 100644 src/renderer/opengl/buffer.zig create mode 100644 src/renderer/opengl/cell.zig delete mode 100644 src/renderer/opengl/custom.zig create mode 100644 src/renderer/opengl/shaders.zig delete mode 100644 src/renderer/shaders/cell.f.glsl delete mode 100644 src/renderer/shaders/cell.v.glsl delete mode 100644 src/renderer/shaders/custom.v.glsl create mode 100644 src/renderer/shaders/glsl/cell_bg.f.glsl create mode 100644 src/renderer/shaders/glsl/cell_text.f.glsl create mode 100644 src/renderer/shaders/glsl/cell_text.v.glsl create mode 100644 src/renderer/shaders/glsl/common.glsl create mode 100644 src/renderer/shaders/glsl/full_screen.v.glsl create mode 100644 src/renderer/shaders/glsl/image.f.glsl create mode 100644 src/renderer/shaders/glsl/image.v.glsl delete mode 100644 src/renderer/shaders/image.f.glsl delete mode 100644 src/renderer/shaders/image.v.glsl diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index a47dbdaca..7be249b1a 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -147,10 +147,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() @@ -703,11 +699,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) 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..30f11881e --- /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..833a9bb4d 100644 --- a/pkg/opengl/Texture.zig +++ b/pkg/opengl/Texture.zig @@ -7,8 +7,8 @@ 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) !void { + glad.context.ActiveTexture.?(index + c.GL_TEXTURE0); try errors.getError(); } @@ -30,7 +30,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 +67,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. _, @@ -116,6 +116,7 @@ pub const Binding = struct { ), else => unreachable, } + try errors.getError(); } pub fn image2D( @@ -140,6 +141,7 @@ pub const Binding = struct { @intFromEnum(typ), data, ); + try errors.getError(); } pub fn subImage2D( @@ -164,6 +166,7 @@ pub const Binding = struct { @intFromEnum(typ), data, ); + try errors.getError(); } pub fn copySubImage2D( @@ -176,6 +179,16 @@ pub const Binding = struct { width: c.GLsizei, height: c.GLsizei, ) !void { - glad.context.CopyTexSubImage2D.?(@intFromEnum(b.target), level, xoffset, yoffset, x, y, width, height); + 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/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/gtk/App.zig b/src/apprt/gtk/App.zig index 099a051a4..0299cc1ff 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, 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/config/Config.zig b/src/config/Config.zig index 2df66ba45..f7a197184 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -266,6 +266,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 +279,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%, 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 639ef354b..766cbefa5 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1,547 +1,80 @@ -//! 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; + +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, - - /// 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 +86,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 +104,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,2772 +114,236 @@ 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(); + self.queue.release(); + self.device.release(); - 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; + // NOTE: We don't release the layer here because that should be taken + // care of automatically when the hosting view is destroyed. } -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; - - // Build our sampler for our texture - var sampler = try mtl_sampler.Sampler.init(self.gpu_state.device); - errdefer sampler.deinit(); - - 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", - mtl.MTLTextureUsage{ - .render_target = true, - .shader_read = true, - .shader_write = true, - }, - ); - - // 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", - mtl.MTLTextureUsage{ - .render_target = true, - .shader_read = true, - .shader_write = true, - }, - ); - - // 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 repeat(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, - 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 +/// Initializes a Texture suitable for the provided font atlas. +pub fn initAtlasTexture(self: *const Metal, atlas: *const font.Atlas) !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 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..c2f8bd652 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1,452 +1,159 @@ -//! 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; + +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{ +pub fn init(alloc: Allocator, opts: rendererpkg.Options) !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, }; } +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(.c) 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 +162,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 +184,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 +213,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 +257,149 @@ 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 { +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, + ); +} + +/// Get the current size of the runtime surface. +pub fn surfaceSize(self: *const OpenGL) !struct { width: u32, height: u32 } { _ = 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); + 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]), }; - 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); +/// 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, + }); +} - // 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; - } +/// 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. - // 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, - ); - } + // 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}); + }; - // Draw background images first - try self.drawImages( - gl_state, - self.image_placements.items[0..self.image_bg_end], + // 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, ); - // Draw our background - try self.drawCells(gl_state, self.cells_bg); + // Keep track of this target in case we need to repeat it. + self.last_target = target; +} - // Then draw images under text - try self.drawImages( - gl_state, - self.image_placements.items[self.image_bg_end..self.image_text_end], - ); +/// Present the last presented target again. +pub fn repeat(self: *OpenGL) !void { + if (self.last_target) |target| try self.present(target); +} - // Drag foreground - try self.drawCells(gl_state, self.cells); +/// Returns the options to use when constructing buffers. +pub inline fn bufferOptions(self: OpenGL) bufferpkg.Options { + _ = self; + return .{ + .target = .array, + .usage = .dynamic_draw, + }; +} - // Draw remaining images - try self.drawImages( - gl_state, - self.image_placements.items[self.image_text_end..], +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 { + _ = 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"), + }; + + return Texture.init( + .{ + .format = format, + .internal_format = internal_format, + .target = .Rectangle, + }, + atlas.size, + atlas.size, + null, ); } -/// 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, - ); - } +/// 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); } - -/// Loads some set of cell data into our buffer and issues a draw call. -/// This expects all the OpenGL state to be setup. -/// -/// 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; - - // Todo: get rid of this completely - self.gl_cells_written = 0; - - // 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, - ); -} - -/// 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, - - 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(); - - // 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; - - break :custom custom.State.init( - alloc, - shaders, - ) catch |err| err: { - log.warn("error initializing custom shaders err={}", .{err}); - break :err null; - }; - }; - - // 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); - - // 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(); - } -}; 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 52f599549..03ca7b5e1 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. @@ -314,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) { @@ -432,7 +452,7 @@ fn drainMailbox(self: *Thread) !void { self.renderer.markDirty(); }, - .resize => |v| try self.renderer.setScreenSize(v), + .resize => {}, //|v| try self.renderer.setScreenSize(v), .change_config => |config| { defer config.alloc.destroy(config.thread); @@ -468,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}); } } diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig new file mode 100644 index 000000000..359f5f1b3 --- /dev/null +++ b/src/renderer/generic.zig @@ -0,0 +1,2866 @@ +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 { + 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); + + return struct { + const Self = @This(); + + /// 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, + + /// 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. + // For comptime, this is useful for debugging. + const buf_count = 3; + + /// `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. + 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 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(api.fgBufferOptions(), 10 * 10); + errdefer cells.deinit(); + var cells_bg = try CellBgBuffer.init(api.bgBufferOptions(), 10 * 10); + errdefer cells_bg.deinit(); + + // Initialize our textures for our font atlas. + const grayscale = try api.initAtlasTexture(&.{ + .data = undefined, + .size = 8, + .format = .grayscale, + }); + errdefer grayscale.deinit(); + const color = try api.initAtlasTexture(&.{ + .data = undefined, + .size = 8, + .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 at 1x1 px, this is slightly + // wasteful but it's only done once so whatever. + 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, + + uniforms: shaderpkg.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(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, + + .uniforms = .{ + .resolution = .{ 0, 0, 1 }, + .time = 1, + .time_delta = 1, + .frame_rate = 1, + .frame = 1, + .channel_time = @splat(@splat(0)), + .channel_resolution = @splat(@splat(0)), + .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(), + }; + } + + 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; + + self.uniforms.resolution = .{ + @floatFromInt(width), + @floatFromInt(height), + 1, + }; + self.uniforms.channel_resolution[0] = .{ + @floatFromInt(width), + @floatFromInt(height), + 1, + 0, + }; + } + }; + + /// 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 = 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", + }, + }, + + // 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, + 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, + + /// 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(); + + // 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(); + + // 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.repeat(); + 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); + }, + } + } + } + + // 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 we have custom shaders, update the animation time. + if (frame.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; + } + + // 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(shaderpkg.PostUniforms); + const uniform_buffer = try PostBuffer.initFill( + self.api.bufferOptions(), + &.{state.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), + }; + } + + /// 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..4c51a55c2 --- /dev/null +++ b/src/renderer/metal/IOSurfaceLayer.zig @@ -0,0 +1,187 @@ +//! 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 { + const layer = (try getSubclass()).msgSend( + objc.Object, + objc.sel("layer"), + .{}, + ); + 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..6e3ae78c7 --- /dev/null +++ b/src/renderer/metal/Texture.zig @@ -0,0 +1,196 @@ +//! 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, + +/// Initialize a texture +pub fn init( + opts: Options, + width: usize, + height: usize, + data: ?[]const u8, +) !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, +) !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 90a1a65ab..e1daa6848 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -366,7 +366,7 @@ pub const MTLTextureUsage = packed struct(c_ulong) { /// https://developer.apple.com/documentation/metal/mtltextureusage/shaderatomic?language=objc shader_atomic: bool = false, // TextureUsageShaderAtomic = 32, - __reserved: @Type(.{ .Int = .{ + __reserved: @Type(.{ .int = .{ .signedness = .unsigned, .bits = @bitSizeOf(c_ulong) - 6, } }) = 0, @@ -375,6 +375,22 @@ pub const MTLTextureUsage = packed struct(c_ulong) { 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 { red: f64, green: f64, 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 ff5f1e6bd..68994882e 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); } @@ -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, @@ -214,15 +227,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 +244,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 +273,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,16 +296,19 @@ fn initPostPipeline( }; defer post_library.msgSend(void, objc.sel("release"), .{}); - return (Pipeline{ + return try Pipeline.init(null, .{ + .device = device, .vertex_fn = "full_screen_vertex", .fragment_fn = "main0", - .blending_enabled = false, - }).init( - device, - library, - post_library, - pixel_format, - ); + .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. @@ -324,19 +341,21 @@ fn initCellTextPipeline( device: objc.Object, library: objc.Object, pixel_format: mtl.MTLPixelFormat, -) !objc.Object { - return (Pipeline{ +) !Pipeline { + return try Pipeline.init(CellText, .{ + .device = device, .vertex_fn = "cell_text_vertex", .fragment_fn = "cell_text_fragment", - .Vertex = CellText, + .vertex_library = library, + .fragment_library = library, .step_fn = .per_instance, - .blending_enabled = true, - }).init( - device, - library, - library, - pixel_format, - ); + .attachments = &.{ + .{ + .pixel_format = pixel_format, + .blending_enabled = true, + }, + }, + }); } /// This is a single parameter for the cell bg shader. @@ -347,17 +366,20 @@ fn initCellBgPipeline( device: objc.Object, library: objc.Object, pixel_format: mtl.MTLPixelFormat, -) !objc.Object { - return (Pipeline{ +) !Pipeline { + return try Pipeline.init(null, .{ + .device = device, .vertex_fn = "cell_bg_vertex", .fragment_fn = "cell_bg_fragment", - .blending_enabled = false, - }).init( - device, - library, - library, - pixel_format, - ); + .vertex_library = library, + .fragment_library = library, + .attachments = &.{ + .{ + .pixel_format = pixel_format, + .blending_enabled = false, + }, + }, + }); } /// Initialize the image render pipeline for our shader library. @@ -365,182 +387,21 @@ fn initImagePipeline( device: objc.Object, library: objc.Object, pixel_format: mtl.MTLPixelFormat, -) !objc.Object { - return (Pipeline{ +) !Pipeline { + return try Pipeline.init(Image, .{ + .device = device, .vertex_fn = "image_vertex", .fragment_fn = "image_fragment", - .Vertex = Image, + .vertex_library = library, + .fragment_library = library, .step_fn = .per_instance, - .blending_enabled = true, - }).init( - device, - library, - library, - pixel_format, - ); -} - -/// A struct with all the necessary info to initialize a pipeline. -const Pipeline = struct { - /// Name of the vertex function - vertex_fn: []const u8, - /// Name of the fragment function - fragment_fn: []const u8, - - /// Vertex attribute struct - Vertex: ?type = null, - /// Vertex step function - step_fn: mtl.MTLVertexStepFunction = .per_vertex, - - /// Whether blending is enabled for the color attachment - blending_enabled: bool = true, - - fn init( - self: *const Pipeline, - device: objc.Object, - vertex_library: objc.Object, - fragment_library: objc.Object, - pixel_format: mtl.MTLPixelFormat, - ) !objc.Object { - // 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( - self.vertex_fn, - .utf8, - false, - ); - defer str.release(); - - const ptr = 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( - self.fragment_fn, - .utf8, - false, - ); - defer str.release(); - - const ptr = 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 (self.Vertex) |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(self.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")); - { - const attachment = attachments.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, - ); - - attachment.setProperty("pixelFormat", @intFromEnum(pixel_format)); - - attachment.setProperty("blendingEnabled", self.blending_enabled); - // We always use premultiplied alpha blending for now. - if (self.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 = device.msgSend( - objc.Object, - objc.sel("newRenderPipelineStateWithDescriptor:error:"), - .{ desc, &err }, - ); - try checkError(err); - errdefer pipeline_state.msgSend(void, objc.sel("release"), .{}); - - 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)); - } + .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..127d689f5 --- /dev/null +++ b/src/renderer/opengl/Pipeline.zig @@ -0,0 +1,170 @@ +//! 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..84a1ae9bc --- /dev/null +++ b/src/renderer/opengl/Texture.zig @@ -0,0 +1,99 @@ +//! 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, + +/// Initialize a texture +pub fn init( + opts: Options, + width: usize, + height: usize, + data: ?[]const u8, +) !Self { + const tex = try gl.Texture.create(); + errdefer tex.destroy(); + { + const texbind = try tex.bind(opts.target); + defer texbind.unbind(); + 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, + opts.internal_format, + @intCast(width), + @intCast(height), + 0, + opts.format, + .UnsignedByte, + if (data) |d| @ptrCast(d.ptr) else null, + ); + } + + 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, +) !void { + const texbind = try self.texture.bind(self.target); + defer texbind.unbind(); + try texbind.subImage2D( + 0, + @intCast(x), + @intCast(y), + @intCast(width), + @intCast(height), + self.format, + .UnsignedByte, + data.ptr, + ); +} + 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..253ae8719 --- /dev/null +++ b/src/renderer/opengl/shaders.zig @@ -0,0 +1,310 @@ +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, + }; +}; + +/// The uniforms used for custom postprocess shaders. +pub const PostUniforms = 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), +}; + +/// 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 ++ .{std.fs.path.sep} ++ 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..5bc25bc03 100644 --- a/src/renderer/shaders/shadertoy_prefix.glsl +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -1,24 +1,24 @@ #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; }; -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..68171a23e 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -205,18 +205,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 { From ac2eef9aeb319b0a1532607c215925bfc198e8c0 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 17 Jun 2025 16:34:05 -0600 Subject: [PATCH 13/89] renderer: disable multi-buffering for OpenGL Frames are sequential for OpenGL since the completion handler always calls `glFinish`, so the extra buffers do nothing but waste memory. --- src/renderer/OpenGL.zig | 4 ++++ src/renderer/generic.zig | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index c2f8bd652..59c2f41b6 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -29,6 +29,10 @@ pub const imagepkg = @import("opengl/image.zig"); pub const custom_shader_target: shadertoy.Target = .glsl; +/// 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 diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 359f5f1b3..52f789c6c 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -204,8 +204,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // 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 buf_count = 3; + const buf_count = count: { + if (@hasDecl(GraphicsAPI, "swap_chain_count")) { + break :count GraphicsAPI.swap_chain_count; + } + + // Default to triple buffering if + // graphics API has no preference. + break :count 3; + }; /// `buf_count` structs that can hold the /// data needed by the GPU to draw a frame. From 6dc5ae7a00999d0366a70cd389421b5a349e0462 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 17 Jun 2025 17:31:22 -0600 Subject: [PATCH 14/89] format (remove empty lines) --- src/renderer/opengl/Pipeline.zig | 1 - src/renderer/opengl/Texture.zig | 1 - 2 files changed, 2 deletions(-) diff --git a/src/renderer/opengl/Pipeline.zig b/src/renderer/opengl/Pipeline.zig index 127d689f5..501e6124c 100644 --- a/src/renderer/opengl/Pipeline.zig +++ b/src/renderer/opengl/Pipeline.zig @@ -31,7 +31,6 @@ pub const Options = struct { per_vertex, per_instance, }; - }; program: gl.Program, diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig index 84a1ae9bc..d5ec816a6 100644 --- a/src/renderer/opengl/Texture.zig +++ b/src/renderer/opengl/Texture.zig @@ -96,4 +96,3 @@ pub fn replaceRegion( data.ptr, ); } - From ea1e507af712b5e843bf43ac08026f4ff4dc0b64 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 17 Jun 2025 17:32:57 -0600 Subject: [PATCH 15/89] unwrap unnecessary @"" identifiers --- pkg/macos/video/pixel_format.zig | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/macos/video/pixel_format.zig b/pkg/macos/video/pixel_format.zig index 30f11881e..78091daa3 100644 --- a/pkg/macos/video/pixel_format.zig +++ b/pkg/macos/video/pixel_format.zig @@ -96,33 +96,33 @@ pub const PixelFormat = enum(c_int) { /// 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, + OneComponent8 = c.kCVPixelFormatType_OneComponent8, /// 8 bit two component, black is zero - @"TwoComponent8" = c.kCVPixelFormatType_TwoComponent8, + 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, + 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, + OneComponent10 = c.kCVPixelFormatType_OneComponent10, /// 12 bit little-endian one component, stored as 12 MSBs of 16 bits, black is zero - @"OneComponent12" = c.kCVPixelFormatType_OneComponent12, + OneComponent12 = c.kCVPixelFormatType_OneComponent12, /// 16 bit little-endian one component, black is zero - @"OneComponent16" = c.kCVPixelFormatType_OneComponent16, + OneComponent16 = c.kCVPixelFormatType_OneComponent16, /// 16 bit little-endian two component, black is zero - @"TwoComponent16" = c.kCVPixelFormatType_TwoComponent16, + TwoComponent16 = c.kCVPixelFormatType_TwoComponent16, /// 16 bit one component IEEE half-precision float, 16-bit little-endian samples - @"OneComponent16Half" = c.kCVPixelFormatType_OneComponent16Half, + OneComponent16Half = c.kCVPixelFormatType_OneComponent16Half, /// 32 bit one component IEEE float, 32-bit little-endian samples - @"OneComponent32Float" = c.kCVPixelFormatType_OneComponent32Float, + OneComponent32Float = c.kCVPixelFormatType_OneComponent32Float, /// 16 bit two component IEEE half-precision float, 16-bit little-endian samples - @"TwoComponent16Half" = c.kCVPixelFormatType_TwoComponent16Half, + TwoComponent16Half = c.kCVPixelFormatType_TwoComponent16Half, /// 32 bit two component IEEE float, 32-bit little-endian samples - @"TwoComponent32Float" = c.kCVPixelFormatType_TwoComponent32Float, + 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 @@ -136,13 +136,13 @@ pub const PixelFormat = enum(c_int) { /// 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, + 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, + DisparityFloat32 = c.kCVPixelFormatType_DisparityFloat32, /// IEEE754-2008 binary16 (half float), describing the depth (distance to an object) in meters - @"DepthFloat16" = c.kCVPixelFormatType_DepthFloat16, + DepthFloat16 = c.kCVPixelFormatType_DepthFloat16, /// IEEE754-2008 binary32 float, describing the depth (distance to an object) in meters - @"DepthFloat32" = c.kCVPixelFormatType_DepthFloat32, + 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]) From 541bb0d4d9ee441b9655bd8edd697a3168587207 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 18 Jun 2025 16:54:50 -0600 Subject: [PATCH 16/89] fix window cross-compilation --- src/renderer/OpenGL.zig | 12 +++++++++++- src/renderer/opengl/shaders.zig | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 59c2f41b6..fe266d2ef 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -70,6 +70,16 @@ pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints { }; } +/// 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, @@ -78,7 +88,7 @@ fn glDebugMessageCallback( len: gl.c.GLsizei, msg: [*c]const gl.c.GLchar, user_param: ?*const anyopaque, -) callconv(.c) void { +) callconv(gl_debug_proc_callconv) void { _ = user_param; const src_str: []const u8 = switch (src) { diff --git a/src/renderer/opengl/shaders.zig b/src/renderer/opengl/shaders.zig index 253ae8719..e509b723a 100644 --- a/src/renderer/opengl/shaders.zig +++ b/src/renderer/opengl/shaders.zig @@ -295,7 +295,7 @@ fn processIncludes(contents: [:0]const u8, basedir: []const u8) [:0]const u8 { "{s}{s}{s}", .{ contents[0..i], - @embedFile(basedir ++ .{std.fs.path.sep} ++ contents[start..end]), + @embedFile(basedir ++ "/" ++ contents[start..end]), processIncludes(contents[end + 1 ..], basedir), }, ); From e8460e80b206ebadd82c15562e46c86396da6564 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 18 Jun 2025 17:01:14 -0600 Subject: [PATCH 17/89] docs: update info about runtime change of `custom-shader` Also removes incorrect information about OpenGL requirement, since the minimum required OpenGL is now unconditionally 4.3 --- src/config/Config.zig | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index f7a197184..bb094bf4d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1967,9 +1967,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. @@ -1983,8 +1980,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 @@ -2002,8 +1998,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 From 8b23e73d203d975811e9a97f33092b9221c86ba5 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 14:28:31 -0600 Subject: [PATCH 18/89] metal: retain IOSurfaceLayer ourselves instead of relying on the view If this was Swift code, we'd be using a strong reference, which would retain the layer for us and release it when the object is deallocated, but this is Zig land so we have to do that manually. NOTE: We don't *have* to do this, but it fits much better with Zig idiom and hopefully avoids potential future footguns. We should do this to any autoreleased objects that we persist a reference to in a Zig struct. --- src/renderer/Metal.zig | 4 +--- src/renderer/metal/IOSurfaceLayer.zig | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 766cbefa5..94c087f6c 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -165,9 +165,7 @@ pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { pub fn deinit(self: *Metal) void { self.queue.release(); self.device.release(); - - // NOTE: We don't release the layer here because that should be taken - // care of automatically when the hosting view is destroyed. + self.layer.release(); } pub fn loopEnter(self: *Metal) void { diff --git a/src/renderer/metal/IOSurfaceLayer.zig b/src/renderer/metal/IOSurfaceLayer.zig index 4c51a55c2..9212bd5e1 100644 --- a/src/renderer/metal/IOSurfaceLayer.zig +++ b/src/renderer/metal/IOSurfaceLayer.zig @@ -21,11 +21,14 @@ var Subclass: ?objc.Class = null; 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 From b9e35c59704e9be16d11595dba372d574d2fe709 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 14:48:30 -0600 Subject: [PATCH 19/89] renderer: uncomment resize message handling We need this to get info about the padding, even if we do derive the grid and screen size separately. In the future this should possibly be changed to a message that only sends the padding info and nothing else. --- src/renderer/Thread.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 03ca7b5e1..c4036415b 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -452,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); From dccbec2283759e126adc387bffd8469cafd061fa Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 14:51:55 -0600 Subject: [PATCH 20/89] style(renderer): capture generic consts as decls in returned struct Out of an abundance of caution, since there have been issues in the past relating to consts outside of the returned struct. --- src/renderer/generic.zig | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 52f789c6c..0308b4c6d 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -68,24 +68,24 @@ const log = std.log.scoped(.generic_renderer); /// [ Texture ] - An abstraction over a GPU texture. /// pub fn Renderer(comptime GraphicsAPI: type) type { - 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); - 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, From 6b7d751007291e9e082d8486171f97749b04b17c Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 14:53:30 -0600 Subject: [PATCH 21/89] renderer: make GraphicsAPI.swap_chain_count required --- src/renderer/Metal.zig | 3 +++ src/renderer/generic.zig | 10 +--------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 94c087f6c..a69d2e3d3 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -34,6 +34,9 @@ pub const imagepkg = @import("metal/image.zig"); pub const custom_shader_target: shadertoy.Target = .msl; +/// 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. diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 0308b4c6d..274f32585 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -204,15 +204,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // 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 = count: { - if (@hasDecl(GraphicsAPI, "swap_chain_count")) { - break :count GraphicsAPI.swap_chain_count; - } - - // Default to triple buffering if - // graphics API has no preference. - break :count 3; - }; + const buf_count = GraphicsAPI.swap_chain_count; /// `buf_count` structs that can hold the /// data needed by the GPU to draw a frame. From 2f10caec8f694ccf135d024264969cfc1b3be823 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 14:56:18 -0600 Subject: [PATCH 22/89] renderer: clarify why SwapChain.defunct is required --- src/renderer/generic.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 274f32585..2467bfb2e 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -217,6 +217,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// 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 { From 9d00018f8b48c5623ed9798f23d3f01f62779ed6 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 15:02:58 -0600 Subject: [PATCH 23/89] renderer: minimize initial size of GPU resources These will all be resized anyway on the first frame, so there's no point in preallocating sizes that will be too small. --- src/renderer/generic.zig | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 2467bfb2e..fce69316e 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -300,23 +300,30 @@ pub fn Renderer(comptime GraphicsAPI: type) type { var uniforms = try UniformBuffer.init(api.uniformBufferOptions(), 1); 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(api.fgBufferOptions(), 10 * 10); + // 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(), 10 * 10); + 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 = 8, + .size = 1, .format = .grayscale, }); errdefer grayscale.deinit(); const color = try api.initAtlasTexture(&.{ .data = undefined, - .size = 8, + .size = 1, .format = .rgba, }); errdefer color.deinit(); @@ -328,8 +335,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { null; errdefer if (custom_shader_state) |*state| state.deinit(); - // Initialize the target at 1x1 px, this is slightly - // wasteful but it's only done once so whatever. + // 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 .{ From ea7a91e2ba77da472de2d95a8aec1f1fb8b24ed1 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 15:16:31 -0600 Subject: [PATCH 24/89] style(renderer): explicit error sets --- pkg/opengl/Texture.zig | 13 +++++++------ src/renderer/Metal.zig | 7 +++++-- src/renderer/OpenGL.zig | 7 +++++-- src/renderer/metal/Texture.zig | 9 +++++++-- src/renderer/opengl/Texture.zig | 31 ++++++++++++++++++------------- 5 files changed, 42 insertions(+), 25 deletions(-) diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig index 833a9bb4d..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(index: c_uint) !void { +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 }; } @@ -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), @@ -129,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, @@ -154,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, @@ -178,7 +179,7 @@ pub const Binding = struct { y: c.GLint, width: c.GLsizei, height: c.GLsizei, - ) !void { + ) errors.Error!void { glad.context.CopyTexSubImage2D.?( @intFromEnum(b.target), level, diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index a69d2e3d3..21a10d45f 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -284,14 +284,17 @@ pub inline fn textureOptions(self: Metal) Texture.Options { } /// Initializes a Texture suitable for the provided font atlas. -pub fn initAtlasTexture(self: *const Metal, atlas: *const font.Atlas) !Texture { +pub fn initAtlasTexture( + self: *const Metal, + atlas: *const font.Atlas, +) Texture.Error!Texture { const pixel_format: mtl.MTLPixelFormat = switch (atlas.format) { .grayscale => .r8unorm, .rgba => .bgra8unorm, else => @panic("unsupported atlas format for Metal texture"), }; - return Texture.init( + return try Texture.init( .{ .device = self.device, .pixel_format = pixel_format, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index fe266d2ef..1d1b41f0e 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -384,7 +384,10 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options { } /// Initializes a Texture suitable for the provided font atlas. -pub fn initAtlasTexture(self: *const OpenGL, atlas: *const font.Atlas) !Texture { +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) { @@ -393,7 +396,7 @@ pub fn initAtlasTexture(self: *const OpenGL, atlas: *const font.Atlas) !Texture else => @panic("unsupported atlas format for OpenGL texture"), }; - return Texture.init( + return try Texture.init( .{ .format = format, .internal_format = internal_format, diff --git a/src/renderer/metal/Texture.zig b/src/renderer/metal/Texture.zig index 6e3ae78c7..32820f8fc 100644 --- a/src/renderer/metal/Texture.zig +++ b/src/renderer/metal/Texture.zig @@ -31,13 +31,18 @@ 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, -) !Self { +) Error!Self { // Create our descriptor const desc = init: { const Class = objc.getClass("MTLTextureDescriptor").?; @@ -90,7 +95,7 @@ pub fn replaceRegion( width: usize, height: usize, data: []const u8, -) !void { +) error{}!void { self.texture.msgSend( void, objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"), diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig index d5ec816a6..07123922f 100644 --- a/src/renderer/opengl/Texture.zig +++ b/src/renderer/opengl/Texture.zig @@ -31,23 +31,28 @@ 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, -) !Self { - const tex = try gl.Texture.create(); +) Error!Self { + const tex = gl.Texture.create() catch return error.OpenGLFailed; errdefer tex.destroy(); { - const texbind = try tex.bind(opts.target); + const texbind = tex.bind(opts.target) catch return error.OpenGLFailed; defer texbind.unbind(); - 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( + 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), @@ -56,7 +61,7 @@ pub fn init( opts.format, .UnsignedByte, if (data) |d| @ptrCast(d.ptr) else null, - ); + ) catch return error.OpenGLFailed; } return .{ @@ -82,10 +87,10 @@ pub fn replaceRegion( width: usize, height: usize, data: []const u8, -) !void { - const texbind = try self.texture.bind(self.target); +) Error!void { + const texbind = self.texture.bind(self.target) catch return error.OpenGLFailed; defer texbind.unbind(); - try texbind.subImage2D( + texbind.subImage2D( 0, @intCast(x), @intCast(y), @@ -94,5 +99,5 @@ pub fn replaceRegion( self.format, .UnsignedByte, data.ptr, - ); + ) catch return error.OpenGLFailed; } From 3e7d64b5ce965070ca361d1f8d94cfb157faddad Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 15:45:43 -0600 Subject: [PATCH 25/89] style(renderer): explicit empty error set for OpenGL init --- src/renderer/OpenGL.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 1d1b41f0e..584d3cf9d 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -47,7 +47,10 @@ blending: configpkg.Config.AlphaBlending, /// The most recently presented target, in case we need to present it again. last_target: ?Target = null, -pub fn init(alloc: Allocator, opts: rendererpkg.Options) !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, .blending = opts.config.blending, From 8b9e6641f22dfefe917a80cc99542990e149cbbc Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 15:48:44 -0600 Subject: [PATCH 26/89] style(renderer): explicit result type In case of future breaking changes to `options` --- src/renderer/generic.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index fce69316e..3462b6fa4 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -633,7 +633,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const font_critical: struct { metrics: font.Metrics, } = font_critical: { - const grid = options.font_grid; + const grid: *font.SharedGrid = options.font_grid; grid.lock.lockShared(); defer grid.lock.unlockShared(); break :font_critical .{ From a8021085587f0dbe8ad9299bbab90d98ef3ff134 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 15:49:53 -0600 Subject: [PATCH 27/89] renderer: remove unused surface parameter from updateFrame --- src/renderer/Thread.zig | 1 - src/renderer/generic.zig | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index c4036415b..b8884f2fb 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -605,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 index 3462b6fa4..d7b4f4226 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1027,12 +1027,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// Update the frame data. pub fn updateFrame( self: *Self, - 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, From ab926fc842da4765148295b7a12f42cb28b86d25 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 15:51:48 -0600 Subject: [PATCH 28/89] naming(GraphicsAPI): repeat -> presentLastTarget --- src/renderer/Metal.zig | 2 +- src/renderer/OpenGL.zig | 2 +- src/renderer/generic.zig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 21a10d45f..4ba477c40 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -242,7 +242,7 @@ pub inline fn present(self: *Metal, target: Target, sync: bool) !void { } /// Present the last presented target again. (noop for Metal) -pub inline fn repeat(self: *Metal) !void { +pub inline fn presentLastTarget(self: *Metal) !void { _ = self; } diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 584d3cf9d..81dbae66e 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -357,7 +357,7 @@ pub fn present(self: *OpenGL, target: Target) !void { } /// Present the last presented target again. -pub fn repeat(self: *OpenGL) !void { +pub fn presentLastTarget(self: *OpenGL) !void { if (self.last_target) |target| try self.present(target); } diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index d7b4f4226..b3e6c4e12 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1274,7 +1274,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // 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.repeat(); + try self.api.presentLastTarget(); return; } self.cells_rebuilt = false; From ddf1a5b23d2447b1d55656f95c2faad12570a84e Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 16:16:17 -0600 Subject: [PATCH 29/89] renderer: move drawFrame AutoreleasePool handling to GraphicsAPI Introduces `drawFrameStart`/`drawFrameEnd` for this purpose. --- src/renderer/Metal.zig | 20 ++++++++++++++++++++ src/renderer/OpenGL.zig | 14 ++++++++++++++ src/renderer/generic.zig | 13 ++++--------- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 4ba477c40..03a568d87 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -61,6 +61,9 @@ blending: configpkg.Config.AlphaBlending, /// the "shared" storage mode, instead we have to use the "managed" mode. default_storage_mode: mtl.MTLResourceOptions.StorageMode, +/// We start an AutoreleasePool before `drawFrame` and end it afterwards. +autorelease_pool: ?*objc.AutoreleasePool = null, + pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { comptime switch (builtin.os.tag) { .macos, .ios => {}, @@ -185,6 +188,23 @@ fn displayCallback(renderer: *Renderer) align(8) void { }; } +/// 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(); +} + +/// 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; +} + pub fn initShaders( self: *const Metal, alloc: Allocator, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 81dbae66e..23496c148 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -289,6 +289,20 @@ pub fn displayRealized(self: *const OpenGL) void { } } +/// Actions taken before doing anything in `drawFrame`. +/// +/// Right now there's nothing we need to do for OpenGL. +pub fn drawFrameStart(self: *OpenGL) void { + _ = self; +} + +/// Actions taken after `drawFrame` is done. +/// +/// Right now there's nothing we need to do for OpenGL. +pub fn drawFrameEnd(self: *OpenGL) void { + _ = self; +} + pub fn initShaders( self: *const OpenGL, alloc: Allocator, diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index b3e6c4e12..f178d7bef 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1241,15 +1241,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.draw_mutex.lock(); defer self.draw_mutex.unlock(); - // 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(); + // 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(); From b249fe0b2c68de13bac80f0262832d469ddd85bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Victor=20Ribeiro=20Silva?= Date: Fri, 20 Jun 2025 21:15:03 -0300 Subject: [PATCH 30/89] fix: undo poedit formatting --- po/pt_BR.UTF-8.po | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index c7bdf4df7..ba13f4460 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -11,14 +11,13 @@ msgstr "" "POT-Creation-Date: 2025-04-22 08:57-0700\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-Team: Brazilian Portuguese \n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -"X-Generator: Poedit 3.6\n" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" @@ -175,8 +174,8 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Uma aplicação está tentando ler da área de transferência. O conteúdo atual da " -"área de transferência está sendo exibido abaixo." +"Uma aplicação está tentando ler da área de transferência. O conteúdo atual " +"da área de transferência está sendo exibido abaixo." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -190,8 +189,8 @@ msgstr "Permitir" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 msgid "" -"An application is attempting to write to the clipboard. The current clipboard " -"contents are shown below." +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." msgstr "" "Uma aplicação está tentando escrever na área de transferência. O conteúdo " "atual da área de transferência está aparecendo abaixo." @@ -221,7 +220,8 @@ msgid "New Split" msgstr "Nova divisão" #: src/apprt/gtk/Window.zig:312 -msgid "⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "" "⚠️ Você está rodando uma build de debug do Ghostty! O desempenho será afetado." From 7ae5018fe8dbee7db7ff9da64a7ae4a11d874444 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 17 Jun 2025 20:59:12 -0700 Subject: [PATCH 31/89] macos: new terminal intent --- macos/Ghostty.xcodeproj/project.pbxproj | 16 ++++ macos/Sources/App/macOS/AppDelegate.swift | 2 +- .../App Intents/GhosttyIntentError.swift | 9 +++ .../App Intents/NewTerminalIntent.swift | 81 +++++++++++++++++++ .../Terminal/TerminalController.swift | 2 +- 5 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 macos/Sources/Features/App Intents/GhosttyIntentError.swift create mode 100644 macos/Sources/Features/App Intents/NewTerminalIntent.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 5c584709e..c1a7bbaef 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -120,6 +120,8 @@ 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 */; }; 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 */; }; @@ -240,6 +242,8 @@ 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 = ""; }; 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 = ""; }; @@ -299,6 +303,7 @@ A56D58872ACDE6BE00508D2C /* Services */, A59630982AEE1C4400D64628 /* Terminal */, A5CBD05A2CA0C5910017A1AE /* QuickTerminal */, + A5E4082C2E0237270035FEAC /* App Intents */, A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */, A57D79252C9C8782001D522E /* Secure Input */, A58636622DEF955100E04A10 /* Splits */, @@ -598,6 +603,15 @@ path = ClipboardConfirmation; sourceTree = ""; }; + A5E4082C2E0237270035FEAC /* App Intents */ = { + isa = PBXGroup; + children = ( + A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, + A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, + ); + path = "App Intents"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -777,8 +791,10 @@ A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */, A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */, + A5E408302E0271320035FEAC /* GhosttyIntentError.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 */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index c56d7c3ac..7336f18d6 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -167,7 +167,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( diff --git a/macos/Sources/Features/App Intents/GhosttyIntentError.swift b/macos/Sources/Features/App Intents/GhosttyIntentError.swift new file mode 100644 index 000000000..a04db1e6f --- /dev/null +++ b/macos/Sources/Features/App Intents/GhosttyIntentError.swift @@ -0,0 +1,9 @@ +enum GhosttyIntentError: Error, CustomLocalizedStringResourceConvertible { + case appUnavailable + + var localizedStringResource: LocalizedStringResource { + switch self { + case .appUnavailable: return "The Ghostty app isn't properly initialized." + } + } +} diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift new file mode 100644 index 000000000..c54d31c09 --- /dev/null +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -0,0 +1,81 @@ +import AppKit +import AppIntents + +/// 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: "Working Directory", + description: "The working directory to open in the terminal.", + supportedContentTypes: [.folder] + ) + var workingDirectory: IntentFile? + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = .foreground(.immediate) + + @available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes") + static var openAppWhenRun = true + + static var parameterSummary: some ParameterSummary { + Summary("New Terminal \(\.$location)") + } + + @MainActor + func perform() async throws -> some IntentResult { + guard let appDelegate = NSApp.delegate as? AppDelegate else { + throw GhosttyIntentError.appUnavailable + } + + var config = Ghostty.SurfaceConfiguration() + + // 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) + } + + switch location { + case .window: + _ = TerminalController.newWindow( + appDelegate.ghostty, + withBaseConfig: config) + + case .tab: + _ = TerminalController.newTab( + appDelegate.ghostty, + from: TerminalController.preferredParent?.window, + withBaseConfig: config) + } + + return .result() + } +} + +// MARK: NewTerminalLocation + +enum NewTerminalLocation: String { + case tab + case window +} + +extension NewTerminalLocation: AppEnum { + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Location") + + static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ + .tab: .init(title: "Tab"), + .window: .init(title: "Window"), + ] +} diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 273744237..a224c9248 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -169,7 +169,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr 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 From 2aa731a64e13856f02f0484dfce403a855abb723 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jun 2025 08:30:41 -0700 Subject: [PATCH 32/89] macos: TerminalEntity --- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++ .../Features/App Intents/TerminalEntity.swift | 67 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 macos/Sources/Features/App Intents/TerminalEntity.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index c1a7bbaef..913ce1995 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -122,6 +122,7 @@ 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 */; }; 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 */; }; @@ -244,6 +245,7 @@ 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 = ""; }; 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 = ""; }; @@ -606,6 +608,7 @@ A5E4082C2E0237270035FEAC /* App Intents */ = { isa = PBXGroup; children = ( + A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */, A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, ); @@ -753,6 +756,7 @@ A5CF66D42D289CEE00139794 /* NSEvent+Extension.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 */, diff --git a/macos/Sources/Features/App Intents/TerminalEntity.swift b/macos/Sources/Features/App Intents/TerminalEntity.swift new file mode 100644 index 000000000..dabfa25ad --- /dev/null +++ b/macos/Sources/Features/App Intents/TerminalEntity.swift @@ -0,0 +1,67 @@ +import AppKit +import AppIntents + +struct TerminalEntity: AppEntity { + let id: UUID + + @Property(title: "Title") + var title: String + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + TypeDisplayRepresentation(name: "Terminal") + } + + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(title)") + } + + static var defaultQuery = TerminalQuery() + + init(_ view: Ghostty.SurfaceView) { + self.id = view.uuid + self.title = view.title + } +} + +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 + private var all: [Ghostty.SurfaceView] { + // Find all of our terminal windows (includes quick terminal) + let controllers = NSApp.windows.compactMap { + $0.windowController as? BaseTerminalController + } + + // Get all our surfaces + return controllers.reduce([]) { result, c in + result + (c.surfaceTree.root?.leaves() ?? []) + } + } +} From 93f0ee2089c4642813d9002faa100a592027643f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jun 2025 10:39:15 -0700 Subject: [PATCH 33/89] macos: GetTerminalDetails intent --- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++ .../GetTerminalDetailsIntent.swift | 65 +++++++++++++++++++ .../Features/App Intents/TerminalEntity.swift | 26 +++++++- .../Sources/Ghostty/SurfaceView_AppKit.swift | 26 +++++++- .../Helpers/Extensions/NSView+Extension.swift | 19 ++++++ 5 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 913ce1995..7bac50670 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -123,6 +123,7 @@ 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 */; }; 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 */; }; @@ -246,6 +247,7 @@ 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 = ""; }; 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 = ""; }; @@ -610,6 +612,7 @@ children = ( A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */, A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, + A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */, A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, ); path = "App Intents"; @@ -754,6 +757,7 @@ 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 */, diff --git a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift new file mode 100644 index 000000000..a57ad3ac4 --- /dev/null +++ b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift @@ -0,0 +1,65 @@ +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 { + switch detail { + case .title: return .result(value: terminal.title) + case .workingDirectory: return .result(value: terminal.workingDirectory) + case .allContents: + guard let view = terminal.surfaceView else { return .result(value: nil) } + return .result(value: view.cachedScreenContents.get()) + case .selectedText: + guard let view = terminal.surfaceView else { return .result(value: nil) } + return .result(value: view.accessibilitySelectedText()) + case .visibleText: + guard let view = terminal.surfaceView else { return .result(value: nil) } + 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/TerminalEntity.swift b/macos/Sources/Features/App Intents/TerminalEntity.swift index dabfa25ad..0d2832bf5 100644 --- a/macos/Sources/Features/App Intents/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/TerminalEntity.swift @@ -1,5 +1,6 @@ import AppKit import AppIntents +import SwiftUI struct TerminalEntity: AppEntity { let id: UUID @@ -7,12 +8,31 @@ struct TerminalEntity: AppEntity { @Property(title: "Title") var title: String + @Property(title: "Working Directory") + var workingDirectory: String? + + var screenshot: Image? + static var typeDisplayRepresentation: TypeDisplayRepresentation { TypeDisplayRepresentation(name: "Terminal") } + @MainActor var displayRepresentation: DisplayRepresentation { - DisplayRepresentation(title: "\(title)") + 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 } } static var defaultQuery = TerminalQuery() @@ -20,6 +40,8 @@ struct TerminalEntity: AppEntity { init(_ view: Ghostty.SurfaceView) { self.id = view.uuid self.title = view.title + self.workingDirectory = view.pwd + self.screenshot = view.screenshot() } } @@ -53,7 +75,7 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery { } @MainActor - private var all: [Ghostty.SurfaceView] { + var all: [Ghostty.SurfaceView] { // Find all of our terminal windows (includes quick terminal) let controllers = NSApp.windows.compactMap { $0.windowController as? BaseTerminalController diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index a47dbdaca..131e39ba7 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -139,7 +139,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 @@ -166,6 +167,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 +195,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 @@ -1979,7 +2001,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/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 { From e51a93ee7cb859cf3c312293282dde31a0cc55d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jun 2025 11:14:47 -0700 Subject: [PATCH 34/89] macos: Terminal entity has screen contents deferred --- .../Features/App Intents/TerminalEntity.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/macos/Sources/Features/App Intents/TerminalEntity.swift b/macos/Sources/Features/App Intents/TerminalEntity.swift index 0d2832bf5..3aea691fe 100644 --- a/macos/Sources/Features/App Intents/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/TerminalEntity.swift @@ -11,6 +11,26 @@ struct TerminalEntity: AppEntity { @Property(title: "Working Directory") var workingDirectory: String? + @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 { From b8d44637547cf8041f490e9fac4931d33ff51900 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jun 2025 11:37:11 -0700 Subject: [PATCH 35/89] macos: terminal not found should be an error --- .../Features/App Intents/GetTerminalDetailsIntent.swift | 6 +++--- macos/Sources/Features/App Intents/GhosttyIntentError.swift | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift index a57ad3ac4..5c41908f4 100644 --- a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift +++ b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift @@ -30,13 +30,13 @@ struct GetTerminalDetailsIntent: AppIntent { case .title: return .result(value: terminal.title) case .workingDirectory: return .result(value: terminal.workingDirectory) case .allContents: - guard let view = terminal.surfaceView else { return .result(value: nil) } + guard let view = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound } return .result(value: view.cachedScreenContents.get()) case .selectedText: - guard let view = terminal.surfaceView else { return .result(value: nil) } + guard let view = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound } return .result(value: view.accessibilitySelectedText()) case .visibleText: - guard let view = terminal.surfaceView else { return .result(value: nil) } + guard let view = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound } return .result(value: view.cachedVisibleContents.get()) } } diff --git a/macos/Sources/Features/App Intents/GhosttyIntentError.swift b/macos/Sources/Features/App Intents/GhosttyIntentError.swift index a04db1e6f..34a0636d9 100644 --- a/macos/Sources/Features/App Intents/GhosttyIntentError.swift +++ b/macos/Sources/Features/App Intents/GhosttyIntentError.swift @@ -1,9 +1,11 @@ enum GhosttyIntentError: Error, CustomLocalizedStringResourceConvertible { case appUnavailable + case surfaceNotFound var localizedStringResource: LocalizedStringResource { switch self { case .appUnavailable: return "The Ghostty app isn't properly initialized." + case .surfaceNotFound: return "The terminal no longer exists." } } } From 683b38f62ca4032bb3c0880c3857b70c6707fd91 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jun 2025 19:37:41 -0700 Subject: [PATCH 36/89] macos: can specify parent terminal for new terminal intent --- .../App Intents/NewTerminalIntent.swift | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index c54d31c09..51b037cca 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -23,16 +23,18 @@ struct NewTerminalIntent: AppIntent { ) var workingDirectory: IntentFile? + @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 - static var parameterSummary: some ParameterSummary { - Summary("New Terminal \(\.$location)") - } - @MainActor func perform() async throws -> some IntentResult { guard let appDelegate = NSApp.delegate as? AppDelegate else { @@ -47,16 +49,31 @@ struct NewTerminalIntent: AppIntent { config.workingDirectory = dir.path(percentEncoded: false) } + // Determine if we have a parent and get it + let parent: Ghostty.SurfaceView? + if let parentParam = self.parent { + guard let view = parentParam.surfaceView else { + throw GhosttyIntentError.surfaceNotFound + } + + parent = view + } else if let preferred = TerminalController.preferredParent { + parent = preferred.focusedSurface ?? preferred.surfaceTree.root?.leftmostLeaf() + } else { + parent = nil + } + switch location { case .window: _ = TerminalController.newWindow( appDelegate.ghostty, - withBaseConfig: config) + withBaseConfig: config, + withParent: parent?.window) case .tab: _ = TerminalController.newTab( appDelegate.ghostty, - from: TerminalController.preferredParent?.window, + from: parent?.window, withBaseConfig: config) } From bbb69c8f27273247cc8e838aef5075cc258575d9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jun 2025 19:50:05 -0700 Subject: [PATCH 37/89] macos: NewTerminalIntent returns Terminal, can split --- .../App Intents/NewTerminalIntent.swift | 58 +++++++++++++++-- .../Terminal/BaseTerminalController.swift | 65 ++++++++++++------- 2 files changed, 93 insertions(+), 30 deletions(-) diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index 51b037cca..55f33bd46 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -1,5 +1,6 @@ import AppKit import AppIntents +import GhosttyKit /// App intent that allows creating a new terminal window or tab. /// @@ -16,6 +17,12 @@ struct NewTerminalIntent: AppIntent { ) var location: NewTerminalLocation + @Parameter( + title: "Command", + description: "Command to execute instead of the default shell." + ) + var command: String? + @Parameter( title: "Working Directory", description: "The working directory to open in the terminal.", @@ -36,12 +43,14 @@ struct NewTerminalIntent: AppIntent { static var openAppWhenRun = true @MainActor - func perform() async throws -> some IntentResult { + func perform() async throws -> some IntentResult & ReturnsValue { guard let appDelegate = NSApp.delegate as? AppDelegate else { throw GhosttyIntentError.appUnavailable } + let ghostty = appDelegate.ghostty var config = Ghostty.SurfaceConfiguration() + config.command = command // If we were given a working directory then open that directory if let url = workingDirectory?.fileURL { @@ -65,19 +74,38 @@ struct NewTerminalIntent: AppIntent { switch location { case .window: - _ = TerminalController.newWindow( - appDelegate.ghostty, + let newController = TerminalController.newWindow( + ghostty, withBaseConfig: config, withParent: parent?.window) + if let view = newController.surfaceTree.root?.leftmostLeaf() { + return .result(value: TerminalEntity(view)) + } case .tab: - _ = TerminalController.newTab( - appDelegate.ghostty, + let newController = TerminalController.newTab( + ghostty, from: parent?.window, withBaseConfig: config) + if let view = newController?.surfaceTree.root?.leftmostLeaf() { + return .result(value: TerminalEntity(view)) + } + + case .splitLeft, .splitRight, .splitUp, .splitDown: + guard let parent, + let controller = parent.window?.windowController as? BaseTerminalController else { + throw GhosttyIntentError.surfaceNotFound + } + + if let view = controller.newSplit( + at: parent, + direction: location.splitDirection! + ) { + return .result(value: TerminalEntity(view)) + } } - return .result() + return .result(value: .none) } } @@ -86,6 +114,20 @@ struct NewTerminalIntent: AppIntent { enum NewTerminalLocation: String { case tab case window + case splitLeft = "split:left" + case splitRight = "split:right" + case splitUp = "split:up" + case splitDown = "split:down" + + var splitDirection: SplitTree.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 { @@ -94,5 +136,9 @@ extension NewTerminalLocation: AppEnum { 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/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index bc91b920e..81b7d32b6 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. @@ -477,30 +517,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) { From 5259d0fa55e8f60db8632cf47dab98744eb730ba Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 07:07:32 -0700 Subject: [PATCH 38/89] macos: starting to work on new libghostty data models --- macos/Ghostty.xcodeproj/project.pbxproj | 16 +++-- .../TerminalCommandPalette.swift | 38 ++++------- macos/Sources/Ghostty/AppError.swift | 3 - macos/Sources/Ghostty/Ghostty.Command.swift | 46 +++++++++++++ macos/Sources/Ghostty/Ghostty.Error.swift | 12 ++++ macos/Sources/Ghostty/Ghostty.Surface.swift | 64 +++++++++++++++++++ macos/Sources/Ghostty/Package.swift | 9 +++ macos/Sources/Ghostty/SurfaceView.swift | 4 +- .../Sources/Ghostty/SurfaceView_AppKit.swift | 21 +++--- src/apprt/embedded.zig | 4 +- 10 files changed, 171 insertions(+), 46 deletions(-) delete mode 100644 macos/Sources/Ghostty/AppError.swift create mode 100644 macos/Sources/Ghostty/Ghostty.Command.swift create mode 100644 macos/Sources/Ghostty/Ghostty.Error.swift create mode 100644 macos/Sources/Ghostty/Ghostty.Surface.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 7bac50670..db2c9d893 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -53,7 +53,6 @@ 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 */; }; 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 */; }; @@ -124,6 +123,9 @@ 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 */; }; 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 */; }; @@ -175,7 +177,6 @@ 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 = ""; }; 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 = ""; }; @@ -248,6 +249,9 @@ 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 = ""; }; 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 = ""; }; @@ -440,12 +444,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 */, ); @@ -766,6 +772,7 @@ A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */, A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */, A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */, + A5E408382E03C7DA0035FEAC /* Ghostty.Surface.swift in Sources */, A5593FE72DF927D200B47B10 /* TransparentTitlebarTerminalWindow.swift in Sources */, A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */, A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */, @@ -800,6 +807,7 @@ A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */, A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */, A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */, + A5E4083A2E0449BD0035FEAC /* Ghostty.Command.swift in Sources */, A5FEB3002ABB69450068369E /* main.swift in Sources */, A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */, A5E4082E2E0237460035FEAC /* NewTerminalIntent.swift in Sources */, @@ -809,13 +817,13 @@ 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 */, 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/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.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.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift new file mode 100644 index 000000000..5560ff3a8 --- /dev/null +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -0,0 +1,64 @@ +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) + } + } + + /// 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/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..371e4ff41 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 diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 131e39ba7..fe0851261 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -115,10 +115,20 @@ extension Ghostty { } } + /// Returns the data model for this surface. + /// + /// Note: eventually, all surface access will be through this, but presently its in a transition + /// state so we're mixing this with direct surface access. + private(set) var surfaceModel: Ghostty.Surface? + + /// Returns the underlying C value for the surface. See "note" on surfaceModel. + var surface: ghostty_surface_t? { + surfaceModel?.unsafeCValue + } + // Notification identifiers associated with this surface var notificationIdentifiers: Set = [] - private(set) var surface: ghostty_surface_t? private var markedText: NSMutableAttributedString private(set) var focused: Bool = true private var prevPressureStage: Int = 0 @@ -282,10 +292,10 @@ extension Ghostty { 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 + 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() @@ -340,11 +350,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) { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index a61c75e96..01e287d16 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1837,12 +1837,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. From 14e46d09791bed1332b93f050a58eb69d9c8350b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 09:43:25 -0700 Subject: [PATCH 39/89] macos: InvokeCommandPaletteIntent and CommandEntity --- macos/Ghostty.xcodeproj/project.pbxproj | 18 ++- .../App Intents/CommandPaletteIntent.swift | 34 +++++ .../App Intents/Entities/CommandEntity.swift | 128 ++++++++++++++++++ .../{ => Entities}/TerminalEntity.swift | 5 + 4 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 macos/Sources/Features/App Intents/CommandPaletteIntent.swift create mode 100644 macos/Sources/Features/App Intents/Entities/CommandEntity.swift rename macos/Sources/Features/App Intents/{ => Entities}/TerminalEntity.swift (96%) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index db2c9d893..990280397 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -126,6 +126,8 @@ 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 */; }; 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 */; }; @@ -252,6 +254,8 @@ 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 = ""; }; 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 = ""; }; @@ -616,14 +620,24 @@ A5E4082C2E0237270035FEAC /* App Intents */ = { isa = PBXGroup; children = ( - A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */, + A5E408412E0453370035FEAC /* Entities */, A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */, + A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */, A5E4082F2E0271320035FEAC /* GhosttyIntentError.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 */ @@ -750,6 +764,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 */, @@ -827,6 +842,7 @@ 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 */, diff --git a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift new file mode 100644 index 000000000..2c1ff3386 --- /dev/null +++ b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift @@ -0,0 +1,34 @@ +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 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/TerminalEntity.swift b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift similarity index 96% rename from macos/Sources/Features/App Intents/TerminalEntity.swift rename to macos/Sources/Features/App Intents/Entities/TerminalEntity.swift index 3aea691fe..750512d02 100644 --- a/macos/Sources/Features/App Intents/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift @@ -55,6 +55,11 @@ struct TerminalEntity: AppEntity { Self.defaultQuery.all.first { $0.uuid == self.id } } + @MainActor + var surfaceModel: Ghostty.Surface? { + surfaceView?.surfaceModel + } + static var defaultQuery = TerminalQuery() init(_ view: Ghostty.SurfaceView) { From c904e86883a8567d96ab6655d28365557d77c57f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 10:47:56 -0700 Subject: [PATCH 40/89] macos: invoke keybind intent --- macos/Ghostty.xcodeproj/project.pbxproj | 4 +++ .../Features/App Intents/KeybindIntent.swift | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 macos/Sources/Features/App Intents/KeybindIntent.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 990280397..a691ce55f 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -128,6 +128,7 @@ 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 */; }; 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 */; }; @@ -256,6 +257,7 @@ 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 = ""; }; 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 = ""; }; @@ -624,6 +626,7 @@ A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */, A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */, + A5E408442E0483F80035FEAC /* KeybindIntent.swift */, A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, ); path = "App Intents"; @@ -823,6 +826,7 @@ 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 */, diff --git a/macos/Sources/Features/App Intents/KeybindIntent.swift b/macos/Sources/Features/App Intents/KeybindIntent.swift new file mode 100644 index 000000000..ddb9c489c --- /dev/null +++ b/macos/Sources/Features/App Intents/KeybindIntent.swift @@ -0,0 +1,32 @@ +import AppKit +import AppIntents + +/// App intent that invokes a command palette entry. +struct KeybindIntent: AppIntent { + static var title: LocalizedStringResource = "Invoke a Keybind Action" + + @Parameter( + title: "Terminal", + description: "The terminal to base available commands from." + ) + 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 let surface = terminal.surfaceModel else { + throw GhosttyIntentError.surfaceNotFound + } + + let performed = surface.perform(action: action) + return .result(value: performed) + } +} From a6074040e7f9268c84ce44bd5bbe0d072548b9ed Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 11:07:46 -0700 Subject: [PATCH 41/89] macos: input intent --- macos/Ghostty.xcodeproj/project.pbxproj | 4 + .../Features/App Intents/InputIntent.swift | 92 +++++++++++++++++++ .../Features/App Intents/KeybindIntent.swift | 3 +- macos/Sources/Ghostty/Ghostty.Input.swift | 11 +++ macos/Sources/Ghostty/Ghostty.Surface.swift | 13 +++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 10 +- 6 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 macos/Sources/Features/App Intents/InputIntent.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index a691ce55f..bbb34820f 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -129,6 +129,7 @@ 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 */; }; @@ -258,6 +259,7 @@ 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 = ""; }; @@ -626,6 +628,7 @@ A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */, A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */, + A5E408462E0485270035FEAC /* InputIntent.swift */, A5E408442E0483F80035FEAC /* KeybindIntent.swift */, A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, ); @@ -819,6 +822,7 @@ 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 */, diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift new file mode 100644 index 000000000..46c849c99 --- /dev/null +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -0,0 +1,92 @@ +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 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: "Text", + description: "The key to send to the terminal." + ) + var key: KeyIntentKey + + @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 let surface = terminal.surfaceModel else { + throw GhosttyIntentError.surfaceNotFound + } + + surface.sendText(text) + return .result() + } +} + +// MARK: TerminalDetail + +enum KeyIntentKey: String { + case title + case workingDirectory + case allContents + case selectedText + case visibleText +} + +extension KeyIntentKey: 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/KeybindIntent.swift b/macos/Sources/Features/App Intents/KeybindIntent.swift index ddb9c489c..adeb64331 100644 --- a/macos/Sources/Features/App Intents/KeybindIntent.swift +++ b/macos/Sources/Features/App Intents/KeybindIntent.swift @@ -1,13 +1,12 @@ import AppKit import AppIntents -/// App intent that invokes a command palette entry. struct KeybindIntent: AppIntent { static var title: LocalizedStringResource = "Invoke a Keybind Action" @Parameter( title: "Terminal", - description: "The terminal to base available commands from." + description: "The terminal to invoke the action on." ) var terminal: TerminalEntity diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 942ca5973..e18203f65 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -208,4 +208,15 @@ extension Ghostty { 0x43: GHOSTTY_KEY_NUMPAD_MULTIPLY, 0x4E: GHOSTTY_KEY_NUMPAD_SUBTRACT, ]; + + /// `ghostty_input_key_e` + enum Key: String { + case undentified + + var cKey: ghostty_input_key_e { + switch self { + case .undentified: GHOSTTY_KEY_UNIDENTIFIED + } + } + } } diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index 5560ff3a8..10e699c1f 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -35,6 +35,19 @@ extension Ghostty { } } + /// 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)) + } + } + /// Perform a keybinding action. /// /// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4` diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index fe0851261..2e7cf499b 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1700,7 +1700,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 = "" @@ -1724,13 +1724,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: From 93619ad42045c42eaf3a1cc1cdb2cd3a32cc2d29 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 11:29:34 -0700 Subject: [PATCH 42/89] macos: Ghostty.Key --- .../Features/App Intents/InputIntent.swift | 25 +- macos/Sources/Ghostty/Ghostty.Input.swift | 905 +++++++++++++++--- macos/Sources/Ghostty/InspectorView.swift | 4 +- 3 files changed, 789 insertions(+), 145 deletions(-) diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index 46c849c99..1b9f88c9f 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -47,7 +47,7 @@ struct KeyEventIntent: AppIntent { title: "Text", description: "The key to send to the terminal." ) - var key: KeyIntentKey + var key: Ghostty.Key @Parameter( title: "Terminal", @@ -64,29 +64,6 @@ struct KeyEventIntent: AppIntent { throw GhosttyIntentError.surfaceNotFound } - surface.sendText(text) return .result() } } - -// MARK: TerminalDetail - -enum KeyIntentKey: String { - case title - case workingDirectory - case allContents - case selectedText - case visibleText -} - -extension KeyIntentKey: 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/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index e18203f65..b3060a44d 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -1,3 +1,4 @@ +import AppIntents import Cocoa import SwiftUI import GhosttyKit @@ -92,131 +93,797 @@ extension Ghostty { 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, - ]; - /// `ghostty_input_key_e` enum Key: String { - case undentified + // 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 { - case .undentified: GHOSTTY_KEY_UNIDENTIFIED + // 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 } } } } + +// MARK: Ghostty.Key AppEnum + +extension Ghostty.Key: AppEnum { + static var typeDisplayRepresentation: TypeDisplayRepresentation = "Key" + + static var caseDisplayRepresentations: [Ghostty.Key : DisplayRepresentation] = [ + // Writing System Keys + .backquote: "Backtick (`)", + .backslash: "Backslash (\\)", + .bracketLeft: "Left Bracket ([)", + .bracketRight: "Right Bracket (])", + .comma: "Comma (,)", + .digit0: "0", + .digit1: "1", + .digit2: "2", + .digit3: "3", + .digit4: "4", + .digit5: "5", + .digit6: "6", + .digit7: "7", + .digit8: "8", + .digit9: "9", + .equal: "Equal (=)", + .intlBackslash: "International Backslash", + .intlRo: "International Ro", + .intlYen: "International Yen", + .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", + .minus: "Minus (-)", + .period: "Period (.)", + .quote: "Quote (')", + .semicolon: "Semicolon (;)", + .slash: "Slash (/)", + + // Functional Keys + .altLeft: "Left Alt", + .altRight: "Right Alt", + .backspace: "Backspace", + .capsLock: "Caps Lock", + .contextMenu: "Context Menu", + .controlLeft: "Left Control", + .controlRight: "Right Control", + .enter: "Enter", + .metaLeft: "Left Command", + .metaRight: "Right Command", + .shiftLeft: "Left Shift", + .shiftRight: "Right Shift", + .space: "Space", + .tab: "Tab", + .convert: "Convert", + .kanaMode: "Kana Mode", + .nonConvert: "Non Convert", + + // Control Pad Section + .delete: "Delete", + .end: "End", + .help: "Help", + .home: "Home", + .insert: "Insert", + .pageDown: "Page Down", + .pageUp: "Page Up", + + // Arrow Pad Section + .arrowDown: "Down Arrow", + .arrowLeft: "Left Arrow", + .arrowRight: "Right Arrow", + .arrowUp: "Up Arrow", + + // Numpad Section + .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 (+)", + .numpadBackspace: "Numpad Backspace", + .numpadClear: "Numpad Clear", + .numpadClearEntry: "Numpad Clear Entry", + .numpadComma: "Numpad Comma", + .numpadDecimal: "Numpad Decimal", + .numpadDivide: "Numpad Divide (÷)", + .numpadEnter: "Numpad Enter", + .numpadEqual: "Numpad Equal", + .numpadMemoryAdd: "Numpad Memory Add", + .numpadMemoryClear: "Numpad Memory Clear", + .numpadMemoryRecall: "Numpad Memory Recall", + .numpadMemoryStore: "Numpad Memory Store", + .numpadMemorySubtract: "Numpad Memory Subtract", + .numpadMultiply: "Numpad Multiply (×)", + .numpadParenLeft: "Numpad Left Parenthesis", + .numpadParenRight: "Numpad Right Parenthesis", + .numpadSubtract: "Numpad Subtract (-)", + .numpadSeparator: "Numpad Separator", + .numpadUp: "Numpad Up", + .numpadDown: "Numpad Down", + .numpadRight: "Numpad Right", + .numpadLeft: "Numpad Left", + .numpadBegin: "Numpad Begin", + .numpadHome: "Numpad Home", + .numpadEnd: "Numpad End", + .numpadInsert: "Numpad Insert", + .numpadDelete: "Numpad Delete", + .numpadPageUp: "Numpad Page Up", + .numpadPageDown: "Numpad Page Down", + + // Function Section + .escape: "Escape", + .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", + .f21: "F21", + .f22: "F22", + .f23: "F23", + .f24: "F24", + .f25: "F25", + .fn: "Fn", + .fnLock: "Fn Lock", + .printScreen: "Print Screen", + .scrollLock: "Scroll Lock", + .pause: "Pause", + + // Media Keys + .browserBack: "Browser Back", + .browserFavorites: "Browser Favorites", + .browserForward: "Browser Forward", + .browserHome: "Browser Home", + .browserRefresh: "Browser Refresh", + .browserSearch: "Browser Search", + .browserStop: "Browser Stop", + .eject: "Eject", + .launchApp1: "Launch App 1", + .launchApp2: "Launch App 2", + .launchMail: "Launch Mail", + .mediaPlayPause: "Media Play/Pause", + .mediaSelect: "Media Select", + .mediaStop: "Media Stop", + .mediaTrackNext: "Media Next Track", + .mediaTrackPrevious: "Media Previous Track", + .power: "Power", + .sleep: "Sleep", + .audioVolumeDown: "Volume Down", + .audioVolumeMute: "Volume Mute", + .audioVolumeUp: "Volume Up", + .wakeUp: "Wake Up", + + // Legacy, Non-standard, and Special Keys + .copy: "Copy", + .cut: "Cut", + .paste: "Paste" + ] +} diff --git a/macos/Sources/Ghostty/InspectorView.swift b/macos/Sources/Ghostty/InspectorView.swift index a6e80bd47..491ec86e1 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.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 From 71b6e223af1ef2e2033326792e86bc9587326e91 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 12:06:27 -0700 Subject: [PATCH 43/89] macos: input keyboard event can send modifiers and actions now --- .../Features/App Intents/InputIntent.swift | 60 ++- macos/Sources/Ghostty/Ghostty.Input.swift | 509 +++++++++++------- macos/Sources/Ghostty/Ghostty.Surface.swift | 14 + macos/Sources/Ghostty/InspectorView.swift | 2 +- 4 files changed, 382 insertions(+), 203 deletions(-) diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index 1b9f88c9f..6d3d60d59 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -44,10 +44,25 @@ struct KeyEventIntent: AppIntent { static var description = IntentDescription("Simulate a keyboard event. This will not handle text encoding; use the 'Input Text' action for that.") @Parameter( - title: "Text", - description: "The key to send to the terminal." + title: "Key", + description: "The key to send to the terminal.", + default: .enter ) - var key: Ghostty.Key + 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", @@ -64,6 +79,45 @@ struct KeyEventIntent: AppIntent { 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: 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/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index b3060a44d..df93017c7 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -4,6 +4,8 @@ import SwiftUI import GhosttyKit extension Ghostty { + struct Input {} + // MARK: Keyboard Shortcuts /// Return the key equivalent for the given trigger. @@ -92,7 +94,175 @@ extension Ghostty { GHOSTTY_KEY_BACKSPACE: .delete, GHOSTTY_KEY_SPACE: .space, ] +} +// 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.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 @@ -146,7 +316,7 @@ extension Ghostty { case quote case semicolon case slash - + // Functional Keys case altLeft case altRight @@ -165,7 +335,7 @@ extension Ghostty { case convert case kanaMode case nonConvert - + // Control Pad Section case delete case end @@ -174,13 +344,13 @@ extension Ghostty { case insert case pageDown case pageUp - + // Arrow Pad Section case arrowDown case arrowLeft case arrowRight case arrowUp - + // Numpad Section case numLock case numpad0 @@ -223,7 +393,7 @@ extension Ghostty { case numpadDelete case numpadPageUp case numpadPageDown - + // Function Section case escape case f1 @@ -256,7 +426,7 @@ extension Ghostty { case printScreen case scrollLock case pause - + // Media Keys case browserBack case browserFavorites @@ -280,7 +450,7 @@ extension Ghostty { case audioVolumeMute case audioVolumeUp case wakeUp - + // Legacy, Non-standard, and Special Keys case copy case cut @@ -349,7 +519,7 @@ extension Ghostty { 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 @@ -368,7 +538,7 @@ extension Ghostty { 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 @@ -377,13 +547,13 @@ extension Ghostty { 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 @@ -426,7 +596,7 @@ extension Ghostty { 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 @@ -459,7 +629,7 @@ extension Ghostty { 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 @@ -483,7 +653,7 @@ extension Ghostty { 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 @@ -545,7 +715,7 @@ extension Ghostty { case .quote: return 0x0027 case .semicolon: return 0x0029 case .slash: return 0x002c - + // Functional Keys case .altLeft: return 0x003a case .altRight: return 0x003d @@ -564,7 +734,7 @@ extension Ghostty { 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 @@ -573,13 +743,13 @@ extension Ghostty { 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 @@ -622,7 +792,7 @@ extension Ghostty { 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 @@ -655,7 +825,7 @@ extension Ghostty { 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 @@ -679,7 +849,7 @@ extension Ghostty { 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 @@ -689,201 +859,142 @@ extension Ghostty { } } -// MARK: Ghostty.Key AppEnum +extension Ghostty.Input.Key: AppEnum { + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Key") -extension Ghostty.Key: AppEnum { - static var typeDisplayRepresentation: TypeDisplayRepresentation = "Key" - - static var caseDisplayRepresentations: [Ghostty.Key : DisplayRepresentation] = [ - // Writing System Keys - .backquote: "Backtick (`)", - .backslash: "Backslash (\\)", - .bracketLeft: "Left Bracket ([)", - .bracketRight: "Right Bracket (])", - .comma: "Comma (,)", - .digit0: "0", - .digit1: "1", - .digit2: "2", - .digit3: "3", - .digit4: "4", - .digit5: "5", - .digit6: "6", - .digit7: "7", - .digit8: "8", - .digit9: "9", - .equal: "Equal (=)", - .intlBackslash: "International Backslash", - .intlRo: "International Ro", - .intlYen: "International Yen", - .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", - .minus: "Minus (-)", - .period: "Period (.)", - .quote: "Quote (')", - .semicolon: "Semicolon (;)", - .slash: "Slash (/)", + // 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", - // Functional Keys - .altLeft: "Left Alt", - .altRight: "Right Alt", - .backspace: "Backspace", - .capsLock: "Caps Lock", - .contextMenu: "Context Menu", - .controlLeft: "Left Control", - .controlRight: "Right Control", - .enter: "Enter", - .metaLeft: "Left Command", - .metaRight: "Right Command", - .shiftLeft: "Left Shift", - .shiftRight: "Right Shift", + // 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", - .convert: "Convert", - .kanaMode: "Kana Mode", - .nonConvert: "Non Convert", - - // Control Pad Section + .backspace: "Backspace", + .escape: "Escape", .delete: "Delete", - .end: "End", - .help: "Help", - .home: "Home", - .insert: "Insert", - .pageDown: "Page Down", - .pageUp: "Page Up", - // Arrow Pad Section + // Arrow Keys + .arrowUp: "Up Arrow", .arrowDown: "Down Arrow", .arrowLeft: "Left Arrow", .arrowRight: "Right Arrow", - .arrowUp: "Up Arrow", - // Numpad Section + // 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", + .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 (+)", - .numpadBackspace: "Numpad Backspace", - .numpadClear: "Numpad Clear", - .numpadClearEntry: "Numpad Clear Entry", - .numpadComma: "Numpad Comma", - .numpadDecimal: "Numpad Decimal", - .numpadDivide: "Numpad Divide (÷)", - .numpadEnter: "Numpad Enter", - .numpadEqual: "Numpad Equal", - .numpadMemoryAdd: "Numpad Memory Add", - .numpadMemoryClear: "Numpad Memory Clear", - .numpadMemoryRecall: "Numpad Memory Recall", - .numpadMemoryStore: "Numpad Memory Store", - .numpadMemorySubtract: "Numpad Memory Subtract", - .numpadMultiply: "Numpad Multiply (×)", - .numpadParenLeft: "Numpad Left Parenthesis", - .numpadParenRight: "Numpad Right Parenthesis", .numpadSubtract: "Numpad Subtract (-)", - .numpadSeparator: "Numpad Separator", - .numpadUp: "Numpad Up", - .numpadDown: "Numpad Down", - .numpadRight: "Numpad Right", - .numpadLeft: "Numpad Left", - .numpadBegin: "Numpad Begin", - .numpadHome: "Numpad Home", - .numpadEnd: "Numpad End", - .numpadInsert: "Numpad Insert", - .numpadDelete: "Numpad Delete", - .numpadPageUp: "Numpad Page Up", - .numpadPageDown: "Numpad Page Down", - - // Function Section - .escape: "Escape", - .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", - .f21: "F21", - .f22: "F22", - .f23: "F23", - .f24: "F24", - .f25: "F25", - .fn: "Fn", - .fnLock: "Fn Lock", - .printScreen: "Print Screen", - .scrollLock: "Scroll Lock", - .pause: "Pause", + .numpadMultiply: "Numpad Multiply (×)", + .numpadDivide: "Numpad Divide (÷)", + .numpadDecimal: "Numpad Decimal", + .numpadEqual: "Numpad Equal", + .numpadEnter: "Numpad Enter", + .numpadComma: "Numpad Comma", // Media Keys - .browserBack: "Browser Back", - .browserFavorites: "Browser Favorites", - .browserForward: "Browser Forward", - .browserHome: "Browser Home", - .browserRefresh: "Browser Refresh", - .browserSearch: "Browser Search", - .browserStop: "Browser Stop", - .eject: "Eject", - .launchApp1: "Launch App 1", - .launchApp2: "Launch App 2", - .launchMail: "Launch Mail", - .mediaPlayPause: "Media Play/Pause", - .mediaSelect: "Media Select", - .mediaStop: "Media Stop", - .mediaTrackNext: "Media Next Track", - .mediaTrackPrevious: "Media Previous Track", - .power: "Power", - .sleep: "Sleep", + .audioVolumeUp: "Volume Up", .audioVolumeDown: "Volume Down", .audioVolumeMute: "Volume Mute", - .audioVolumeUp: "Volume Up", - .wakeUp: "Wake Up", - // Legacy, Non-standard, and Special Keys - .copy: "Copy", - .cut: "Cut", - .paste: "Paste" + // 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 index 10e699c1f..88d3f1d09 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -48,6 +48,20 @@ extension Ghostty { } } + /// 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) + } + } + /// Perform a keybinding action. /// /// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4` diff --git a/macos/Sources/Ghostty/InspectorView.swift b/macos/Sources/Ghostty/InspectorView.swift index 491ec86e1..8008e49c2 100644 --- a/macos/Sources/Ghostty/InspectorView.swift +++ b/macos/Sources/Ghostty/InspectorView.swift @@ -337,7 +337,7 @@ extension Ghostty { private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) { guard let inspector = self.inspector else { return } - guard let key = Ghostty.Key(keyCode: 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.cKey, mods) } From 4445a9c63701c52906be2c7a4f987939e79fb237 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 13:49:36 -0700 Subject: [PATCH 44/89] macos: add mouse button intent --- .../Features/App Intents/InputIntent.swift | 58 +++++++++ macos/Sources/Ghostty/Ghostty.Input.swift | 117 +++++++++++++++++- macos/Sources/Ghostty/Ghostty.Surface.swift | 26 ++++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 15 +-- 4 files changed, 206 insertions(+), 10 deletions(-) diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index 6d3d60d59..56af10ceb 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -95,6 +95,64 @@ struct KeyEventIntent: AppIntent { } } +// 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 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() + } +} + // MARK: Mods enum KeyEventMods: String, AppEnum, CaseIterable { diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index df93017c7..a2d6b104d 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -215,11 +215,126 @@ extension Ghostty.Input.Action: AppEnum { static var caseDisplayRepresentations: [Ghostty.Input.Action : DisplayRepresentation] = [ .release: "Release", - .press: "Press", + .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) + } + } +} + +// 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.Mods extension Ghostty.Input { diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index 88d3f1d09..2cc85f1e4 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -62,6 +62,32 @@ extension Ghostty { } } + /// 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) + } + /// Perform a keybinding action. /// /// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4` diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 2e7cf499b..d987b80be 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1312,8 +1312,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 } @@ -1323,13 +1323,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 From bc134016f7d978036103cd3c4532ab9636e445ad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 14:07:09 -0700 Subject: [PATCH 45/89] macos: move mousePos and mousScroll to Ghostty.Surface --- macos/Sources/Ghostty/Ghostty.Input.swift | 120 ++++++++++++++++++ macos/Sources/Ghostty/Ghostty.Surface.swift | 32 +++++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 70 +++++----- 3 files changed, 183 insertions(+), 39 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index a2d6b104d..bbc83c5e5 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -269,6 +269,40 @@ extension Ghostty.Input { 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 @@ -335,6 +369,92 @@ extension Ghostty.Input.MouseButton: AppEnum { ] } +// 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 + } + } + } +} + +#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 { diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index 2cc85f1e4..c7198e147 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -88,6 +88,38 @@ extension Ghostty { 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` diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index d987b80be..83a8da29c 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -808,19 +808,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 @@ -830,17 +834,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, @@ -866,16 +878,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; @@ -883,29 +892,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) { From 2df301e2fb6abd5d37fef3f7d8808d24977ad89e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 14:14:09 -0700 Subject: [PATCH 46/89] macos: mouse pos and scroll intents --- .../Features/App Intents/InputIntent.swift | 116 ++++++++++++++++++ macos/Sources/Ghostty/Ghostty.Input.swift | 14 +++ 2 files changed, 130 insertions(+) diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index 56af10ceb..b8c248fe3 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -153,6 +153,122 @@ struct MouseButtonIntent: AppIntent { } } +/// 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 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 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 { diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index bbc83c5e5..e05911c06 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -436,6 +436,20 @@ extension Ghostty.Input { } } +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 From 0a27aef508ceb5b3376a77ac9fbb6dc6f30020dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 14:19:37 -0700 Subject: [PATCH 47/89] README: note Xcode 26 requirement --- .github/workflows/test.yml | 41 -------------------------------------- README.md | 22 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2eca0a41e..4d09603f4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,6 @@ jobs: - build-nix - build-snap - build-macos - - build-macos-sequoia-stable - build-macos-tahoe - build-macos-matrix - build-windows @@ -310,46 +309,6 @@ jobs: cd macos xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" - build-macos-sequoia-stable: - runs-on: namespace-profile-ghostty-macos-sequoia - needs: test - steps: - - name: Checkout code - uses: actions/checkout@v4 - - # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 - with: - nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 - with: - name: ghostty - authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_16.4.app - - - name: get the Zig deps - id: deps - run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT - - # GhosttyKit is the framework that is built from Zig for our native - # Mac app to access. - - name: Build GhosttyKit - run: nix develop -c zig build --system ${{ steps.deps.outputs.deps }} - - # The native app is built with native Xcode tooling. This also does - # codesigning. IMPORTANT: this must NOT run in a Nix environment. - # Nix breaks xcodebuild so this has to be run outside. - - name: Build Ghostty.app - run: cd macos && xcodebuild -target Ghostty - - # Build the iOS target without code signing just to verify it works. - - name: Build Ghostty iOS - run: | - cd macos - xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" - build-macos-tahoe: runs-on: namespace-profile-ghostty-macos-tahoe needs: test 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 From f096675eaf389871e53140d0037a6ce208307654 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 20:00:24 -0700 Subject: [PATCH 48/89] macos: Close Terminal Intent --- macos/Ghostty.xcodeproj/project.pbxproj | 4 + .../App Intents/CloseTerminalIntent.swift | 38 ++++++++++ .../QuickTerminalController.swift | 6 +- .../Terminal/BaseTerminalController.swift | 73 +++++++++++-------- .../Terminal/TerminalController.swift | 4 +- 5 files changed, 88 insertions(+), 37 deletions(-) create mode 100644 macos/Sources/Features/App Intents/CloseTerminalIntent.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index bbb34820f..acf4b0e43 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 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 */; }; 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 */; }; @@ -149,6 +150,7 @@ 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 = ""; }; 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 = ""; }; @@ -625,6 +627,7 @@ isa = PBXGroup; children = ( A5E408412E0453370035FEAC /* Entities */, + A511940E2E050590007258CC /* CloseTerminalIntent.swift */, A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */, A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */, @@ -793,6 +796,7 @@ 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 */, diff --git a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift new file mode 100644 index 000000000..18079650b --- /dev/null +++ b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift @@ -0,0 +1,38 @@ +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 + + @Parameter( + title: "Command", + description: "Command to execute instead of the default shell.", + default: true + ) + var confirm: Bool + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = .background + + @MainActor + func perform() async throws -> some IntentResult { + guard let surfaceView = terminal.surfaceView else { + throw GhosttyIntentError.surfaceNotFound + } + + guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { + return .result() + } + + controller.closeSurface(surfaceView, withConfirmation: confirm) + return .result() + } +} diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 28dea9579..80b0c9413 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -218,19 +218,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 81b7d32b6..c93a9450d 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -300,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. @@ -460,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 } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index a224c9248..77eb079ad 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -519,13 +519,13 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } /// 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 } From 2c1e83ba2fd621d532ea0e7e4746f3f7ae19c069 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 07:03:40 -0700 Subject: [PATCH 49/89] macos: intent to open quick terminal --- macos/Ghostty.xcodeproj/project.pbxproj | 4 +++ macos/Sources/App/macOS/AppDelegate.swift | 15 ++++------ .../App Intents/Entities/TerminalEntity.swift | 27 +++++++++++++++++- .../App Intents/QuickTerminalIntent.swift | 28 +++++++++++++++++++ .../QuickTerminalController.swift | 6 +++- 5 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 macos/Sources/Features/App Intents/QuickTerminalIntent.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index acf4b0e43..6b0cfd6f8 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 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 */; }; 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 */; }; @@ -151,6 +152,7 @@ 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 = ""; }; 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 = ""; }; @@ -630,6 +632,7 @@ A511940E2E050590007258CC /* CloseTerminalIntent.swift */, A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */, + A51194102E05A480007258CC /* QuickTerminalIntent.swift */, A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */, A5E408462E0485270035FEAC /* InputIntent.swift */, A5E408442E0483F80035FEAC /* KeybindIntent.swift */, @@ -806,6 +809,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 */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 7336f18d6..4ffb9efa4 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 @@ -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 } @@ -919,14 +922,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/Entities/TerminalEntity.swift b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift index 750512d02..1fb69f1f8 100644 --- a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift @@ -11,6 +11,9 @@ struct TerminalEntity: AppEntity { @Property(title: "Working Directory") var workingDirectory: String? + @Property(title: "Kind") + var kind: Kind + @MainActor @DeferredProperty(title: "Full Contents") @available(macOS 26.0, *) @@ -67,6 +70,27 @@ struct TerminalEntity: AppEntity { 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") + ] } } @@ -101,7 +125,8 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery { @MainActor var all: [Ghostty.SurfaceView] { - // Find all of our terminal windows (includes quick terminal) + // 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 } diff --git a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift new file mode 100644 index 000000000..ee2761217 --- /dev/null +++ b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift @@ -0,0 +1,28 @@ +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 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/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 80b0c9413..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 From e6c24fbf0a634a28d8c079948d982636f00cf197 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 07:22:12 -0700 Subject: [PATCH 50/89] macos: remove confirmation option for close terminal --- .../Features/App Intents/CloseTerminalIntent.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift index 18079650b..4de415494 100644 --- a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift @@ -12,13 +12,6 @@ struct CloseTerminalIntent: AppIntent { ) var terminal: TerminalEntity - @Parameter( - title: "Command", - description: "Command to execute instead of the default shell.", - default: true - ) - var confirm: Bool - @available(macOS 26.0, *) static var supportedModes: IntentModes = .background @@ -32,7 +25,7 @@ struct CloseTerminalIntent: AppIntent { return .result() } - controller.closeSurface(surfaceView, withConfirmation: confirm) + controller.closeSurface(surfaceView, withConfirmation: false) return .result() } } From f8bc9b547c2a7e37de80cad6709936852126c46e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 10:09:01 -0700 Subject: [PATCH 51/89] macos: support env vars for surface config, clean up surface config --- include/ghostty.h | 10 +- macos/Ghostty.xcodeproj/project.pbxproj | 4 + macos/Sources/Ghostty/SurfaceView.swift | 72 ++++++++++++--- .../Sources/Ghostty/SurfaceView_AppKit.swift | 6 +- macos/Sources/Ghostty/SurfaceView_UIKit.swift | 6 +- .../Helpers/Extensions/Array+Extension.swift | 25 +++++ .../Extensions/Optional+Extension.swift | 10 ++ src/apprt/embedded.zig | 92 ++++++++++++------- src/config/Config.zig | 5 + 9 files changed, 180 insertions(+), 50 deletions(-) create mode 100644 macos/Sources/Helpers/Extensions/Optional+Extension.swift diff --git a/include/ghostty.h b/include/ghostty.h index fc2c915cb..0c5a63448 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,8 @@ typedef struct { float font_size; const char* working_directory; const char* command; + ghostty_env_var_s* env_vars; + size_t env_var_count; } ghostty_surface_config_s; typedef struct { @@ -807,7 +814,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/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 6b0cfd6f8..a64e6038e 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 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 */; }; 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 */; }; @@ -153,6 +154,7 @@ 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 = ""; }; 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 = ""; }; @@ -506,6 +508,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 */, @@ -786,6 +789,7 @@ 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 */, diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 371e4ff41..2f0623b79 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -418,18 +418,36 @@ extension Ghostty { /// Explicit command to set var command: String? = nil + + /// Environment variables to set for the terminal + var environmentVariables: [String: String] = [:] 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) @@ -438,7 +456,6 @@ extension Ghostty { nsview: Unmanaged.passUnretained(view).toOpaque() )) config.scale_factor = NSScreen.main!.backingScaleFactor - #elseif os(iOS) config.platform_tag = GHOSTTY_PLATFORM_IOS config.platform = ghostty_platform_u(ios: ghostty_platform_ios_s( @@ -453,15 +470,42 @@ extension Ghostty { #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 + + // 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..(_ 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) + } else { + 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/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/src/apprt/embedded.zig b/src/apprt/embedded.zig index 01e287d16..02f143985 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,11 @@ 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, }; pub fn init(self: *Surface, app: *App, opts: Options) !void { @@ -443,41 +455,59 @@ 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), + ); + } } // Initialize our surface right away. We're given a view that is diff --git a/src/config/Config.zig b/src/config/Config.zig index 2df66ba45..e9370d9b3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3004,6 +3004,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 From 027171bd5db7f1ffd199fffeed4e8ef41f7a30d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 10:40:33 -0700 Subject: [PATCH 52/89] macos: can set env vars on new terminal --- .../Features/App Intents/NewTerminalIntent.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index 55f33bd46..444f3d7c0 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -30,6 +30,13 @@ struct NewTerminalIntent: AppIntent { ) 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." @@ -58,6 +65,15 @@ struct NewTerminalIntent: AppIntent { 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[.. Date: Fri, 20 Jun 2025 11:06:05 -0700 Subject: [PATCH 53/89] macos: intents all ask for permission --- macos/Ghostty.xcodeproj/project.pbxproj | 8 + .../App Intents/CloseTerminalIntent.swift | 4 + .../App Intents/CommandPaletteIntent.swift | 4 + .../GetTerminalDetailsIntent.swift | 4 + .../App Intents/GhosttyIntentError.swift | 2 + .../Features/App Intents/InputIntent.swift | 20 +++ .../App Intents/IntentPermission.swift | 37 ++++ .../Features/App Intents/KeybindIntent.swift | 4 + .../App Intents/NewTerminalIntent.swift | 3 + .../App Intents/QuickTerminalIntent.swift | 4 + macos/Sources/Helpers/PermissionRequest.swift | 162 ++++++++++++++++++ 11 files changed, 252 insertions(+) create mode 100644 macos/Sources/Features/App Intents/IntentPermission.swift create mode 100644 macos/Sources/Helpers/PermissionRequest.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index a64e6038e..a203ad682 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 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 */; }; @@ -155,6 +157,8 @@ 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 = ""; }; @@ -355,6 +359,7 @@ A59630962AEE163600D64628 /* HostingWindow.swift */, A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */, A59FB5D02AE0DEA7009128F3 /* MetalView.swift */, + A51194162E05D95E007258CC /* PermissionRequest.swift */, A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */, A5CA378D2D31D6C100931030 /* Weak.swift */, C1F26EE72B76CBFC00404083 /* VibrantLayer.h */, @@ -640,6 +645,7 @@ A5E408462E0485270035FEAC /* InputIntent.swift */, A5E408442E0483F80035FEAC /* KeybindIntent.swift */, A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, + A51194182E05DFBB007258CC /* IntentPermission.swift */, ); path = "App Intents"; sourceTree = ""; @@ -821,6 +827,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 */, diff --git a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift index 4de415494..923d22c97 100644 --- a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift @@ -17,6 +17,10 @@ struct CloseTerminalIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surfaceView = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift index 2c1ff3386..fa983054b 100644 --- a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift +++ b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift @@ -24,6 +24,10 @@ struct CommandPaletteIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult & ReturnsValue { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift index 5c41908f4..1cbaa9d68 100644 --- a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift +++ b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift @@ -26,6 +26,10 @@ struct GetTerminalDetailsIntent: AppIntent { @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) diff --git a/macos/Sources/Features/App Intents/GhosttyIntentError.swift b/macos/Sources/Features/App Intents/GhosttyIntentError.swift index 34a0636d9..635250f72 100644 --- a/macos/Sources/Features/App Intents/GhosttyIntentError.swift +++ b/macos/Sources/Features/App Intents/GhosttyIntentError.swift @@ -1,11 +1,13 @@ enum GhosttyIntentError: Error, CustomLocalizedStringResourceConvertible { case appUnavailable case surfaceNotFound + case permissionDenied var localizedStringResource: LocalizedStringResource { switch self { case .appUnavailable: return "The Ghostty app isn't properly initialized." case .surfaceNotFound: return "The terminal no longer exists." + case .permissionDenied: return "Ghostty doesn't allow Shortcuts." } } } diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index b8c248fe3..17c97fbbb 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -29,6 +29,10 @@ struct InputTextIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -75,6 +79,10 @@ struct KeyEventIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -133,6 +141,10 @@ struct MouseButtonIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -190,6 +202,10 @@ struct MousePosIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -254,6 +270,10 @@ struct MouseScrollIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/IntentPermission.swift b/macos/Sources/Features/App Intents/IntentPermission.swift new file mode 100644 index 000000000..e02c4591d --- /dev/null +++ b/macos/Sources/Features/App Intents/IntentPermission.swift @@ -0,0 +1,37 @@ +/// 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 + PermissionRequest.show( + "org.mitchellh.ghostty.shortcutsPermission", + message: "Allow Shortcuts to interact with Ghostty for the next 10 minutes?", + allowDuration: .seconds(600), + ) { response in + continuation.resume(returning: response) + } + } + } +} diff --git a/macos/Sources/Features/App Intents/KeybindIntent.swift b/macos/Sources/Features/App Intents/KeybindIntent.swift index adeb64331..b31da4a50 100644 --- a/macos/Sources/Features/App Intents/KeybindIntent.swift +++ b/macos/Sources/Features/App Intents/KeybindIntent.swift @@ -21,6 +21,10 @@ struct KeybindIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult & ReturnsValue { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index 444f3d7c0..3c36bed87 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -51,6 +51,9 @@ struct NewTerminalIntent: AppIntent { @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 } diff --git a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift index ee2761217..2e6c9850c 100644 --- a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift @@ -10,6 +10,10 @@ struct QuickTerminalIntent: AppIntent { @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 } diff --git a/macos/Sources/Helpers/PermissionRequest.swift b/macos/Sources/Helpers/PermissionRequest.swift new file mode 100644 index 000000000..35694081c --- /dev/null +++ b/macos/Sources/Helpers/PermissionRequest.swift @@ -0,0 +1,162 @@ +import AppKit +import Foundation + +/// Displays a permission request dialog with optional caching of user decisions +class PermissionRequest { + /// 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 + /// - 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 user checks "Remember my decision for one day", both allow/deny are cached for 24 hours + /// - 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: Duration? = nil, + 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 + let checkbox = NSButton( + checkboxWithTitle: "Remember my decision for one day", + 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, completion: completion) + } + } else { + let response = alert.runModal() + handleResponse(response, rememberDecision: checkbox.state == .on, key: key, allowDuration: allowDuration, 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 + /// - completion: Completion handler to call with the result + private static func handleResponse( + _ response: NSApplication.ModalResponse, + rememberDecision: Bool, + key: String, + allowDuration: 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 { + storeResult(result, for: key, duration: .seconds(86400)) + } else if result, let allowDuration { + storeResult(result, for: key, duration: allowDuration) + } + + 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) + } + } + + /// 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") + } + } +} From b6559d08994ebb39ef66d210cd5461e03c594637 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 11:54:19 -0700 Subject: [PATCH 54/89] macos: add a macos-shortcut config --- .../App Intents/IntentPermission.swift | 21 ++++++++++++- macos/Sources/Ghostty/Ghostty.Config.swift | 17 ++++++++++ src/config/Config.zig | 31 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/App Intents/IntentPermission.swift b/macos/Sources/Features/App Intents/IntentPermission.swift index e02c4591d..78efb3d5d 100644 --- a/macos/Sources/Features/App Intents/IntentPermission.swift +++ b/macos/Sources/Features/App Intents/IntentPermission.swift @@ -1,5 +1,7 @@ +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 @@ -25,6 +27,23 @@ 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( "org.mitchellh.ghostty.shortcutsPermission", message: "Allow Shortcuts to interact with Ghostty for the next 10 minutes?", 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/src/config/Config.zig b/src/config/Config.zig index e9370d9b3..aee670213 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2355,6 +2355,30 @@ 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 by default +/// cache the user's choice for 10 minutes since we can't determine +/// when a single workflow begins or ends. The user also has an option +/// in the GUI to allow for the remainder of the day. +/// +/// * `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 @@ -5961,6 +5985,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, From e4c13cdba87761dfd8feb77b7e57231f8032415f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 12:09:03 -0700 Subject: [PATCH 55/89] macos: Optional/Array extensions need to build for iOS too --- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index a203ad682..416d8b106 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -58,6 +58,8 @@ 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 */; }; + 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 */; }; 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 */; }; @@ -892,6 +894,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 */, @@ -901,6 +904,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; From 020976bf8859324f2cd653988988b08b60ae300c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 21 Jun 2025 06:42:31 -0700 Subject: [PATCH 56/89] macos: address some feedback --- .../App Intents/Entities/TerminalEntity.swift | 6 +++--- .../Features/App Intents/GhosttyIntentError.swift | 6 +++--- .../Sources/Helpers/Extensions/Array+Extension.swift | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift index 1fb69f1f8..e29fbba3f 100644 --- a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift @@ -130,10 +130,10 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery { let controllers = NSApp.windows.compactMap { $0.windowController as? BaseTerminalController } - + // Get all our surfaces - return controllers.reduce([]) { result, c in - result + (c.surfaceTree.root?.leaves() ?? []) + return controllers.flatMap { + $0.surfaceTree.root?.leaves() ?? [] } } } diff --git a/macos/Sources/Features/App Intents/GhosttyIntentError.swift b/macos/Sources/Features/App Intents/GhosttyIntentError.swift index 635250f72..c52b7a52e 100644 --- a/macos/Sources/Features/App Intents/GhosttyIntentError.swift +++ b/macos/Sources/Features/App Intents/GhosttyIntentError.swift @@ -5,9 +5,9 @@ enum GhosttyIntentError: Error, CustomLocalizedStringResourceConvertible { var localizedStringResource: LocalizedStringResource { switch self { - case .appUnavailable: return "The Ghostty app isn't properly initialized." - case .surfaceNotFound: return "The terminal no longer exists." - case .permissionDenied: return "Ghostty doesn't allow Shortcuts." + 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/Helpers/Extensions/Array+Extension.swift b/macos/Sources/Helpers/Extensions/Array+Extension.swift index fac340472..4e8e39918 100644 --- a/macos/Sources/Helpers/Extensions/Array+Extension.swift +++ b/macos/Sources/Helpers/Extensions/Array+Extension.swift @@ -34,12 +34,12 @@ extension Array where Element == String { func helper(index: Int, accumulated: [UnsafePointer?], body: ([UnsafePointer?]) throws -> T) rethrows -> T { if index == count { return try body(accumulated) - } else { - 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 self[index].withCString { cStr in + var newAccumulated = accumulated + newAccumulated.append(cStr) + return try helper(index: index + 1, accumulated: newAccumulated, body: body) } } From 296f340ff425c9986fedc2dbb4b5faa496dc63e3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 21 Jun 2025 06:46:33 -0700 Subject: [PATCH 57/89] macos: the approval dialog is now forever --- .../App Intents/IntentPermission.swift | 5 +- macos/Sources/Helpers/PermissionRequest.swift | 87 +++++++++++++++---- src/config/Config.zig | 7 +- 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/macos/Sources/Features/App Intents/IntentPermission.swift b/macos/Sources/Features/App Intents/IntentPermission.swift index 78efb3d5d..2ec4f2bd9 100644 --- a/macos/Sources/Features/App Intents/IntentPermission.swift +++ b/macos/Sources/Features/App Intents/IntentPermission.swift @@ -46,8 +46,9 @@ func requestIntentPermission() async -> Bool { PermissionRequest.show( "org.mitchellh.ghostty.shortcutsPermission", - message: "Allow Shortcuts to interact with Ghostty for the next 10 minutes?", - allowDuration: .seconds(600), + message: "Allow Shortcuts to interact with Ghostty?", + allowDuration: .forever, + rememberDuration: nil, ) { response in continuation.resume(returning: response) } diff --git a/macos/Sources/Helpers/PermissionRequest.swift b/macos/Sources/Helpers/PermissionRequest.swift index 35694081c..9c16c7163 100644 --- a/macos/Sources/Helpers/PermissionRequest.swift +++ b/macos/Sources/Helpers/PermissionRequest.swift @@ -3,17 +3,25 @@ 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 user checks "Remember my decision for one day", both allow/deny are cached for 24 hours + /// - 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 @@ -22,7 +30,8 @@ class PermissionRequest { message: String, informative: String = "", allowText: String = "Allow", - allowDuration: Duration? = nil, + allowDuration: AllowDuration = .once, + rememberDuration: Duration? = .seconds(86400), window: NSWindow? = nil, completion: @escaping (Bool) -> Void ) { @@ -41,24 +50,28 @@ class PermissionRequest { alert.addButton(withTitle: allowText) alert.addButton(withTitle: "Don't Allow") - // Create checkbox for remembering - let checkbox = NSButton( - checkboxWithTitle: "Remember my decision for one day", - target: nil, - action: nil) - checkbox.state = .off - - // Set checkbox as accessory view - alert.accessoryView = checkbox + // 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, completion: completion) + 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, completion: completion) + handleResponse(response, rememberDecision: checkbox?.state == .on, key: key, allowDuration: allowDuration, rememberDuration: rememberDuration, completion: completion) } } @@ -68,12 +81,14 @@ class PermissionRequest { /// - 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: Duration?, + allowDuration: AllowDuration, + rememberDuration: Duration?, completion: @escaping (Bool) -> Void) { let result: Bool @@ -87,10 +102,21 @@ class PermissionRequest { } // Store the result if checkbox is checked or if "Allow" was selected and allowDuration is set - if rememberDecision { - storeResult(result, for: key, duration: .seconds(86400)) - } else if result, let allowDuration { - storeResult(result, for: key, duration: allowDuration) + 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) @@ -130,6 +156,31 @@ class PermissionRequest { } } + /// 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) diff --git a/src/config/Config.zig b/src/config/Config.zig index aee670213..aabf4f6ba 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2368,10 +2368,9 @@ keybind: Keybinds = .{}, /// /// Valid values are: /// -/// * `ask` - Ask the user whether for permission. Ghostty will by default -/// cache the user's choice for 10 minutes since we can't determine -/// when a single workflow begins or ends. The user also has an option -/// in the GUI to allow for the remainder of the day. +/// * `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. /// From c1c3f639c5d50e15c8890ecfa56d82f52072deea Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 21:08:06 -0700 Subject: [PATCH 58/89] macos: Ghostty Icon Update for macOS Tahoe This updates the Ghostty icon to be compatible with macOS Tahoe (supports glass effects, light/dark, tinting, etc.). This icon is made in the new Apple Icon Composer as the source format, and all other formats are exported from it. This commit also updates the icon for non-Apple platforms because the icon is fundamentally the same and I don't see any reason to maintain multiple icons of fundamentally the same design and style. This commit also includes updates to the macOS app so that the About Window and so on will use the new icon. --- .prettierignore | 3 + dist/macos/Ghostty.icns | Bin 382978 -> 0 bytes dist/macos/Info.plist | 17 -- images/Ghostty.icon/Assets/Ghostty.png | Bin 0 -> 106126 bytes .../Ghostty.icon/Assets/Inner Bevel 6px.png | Bin 0 -> 435672 bytes images/Ghostty.icon/Assets/Screen Effects.png | Bin 0 -> 92547 bytes images/Ghostty.icon/Assets/Screen.png | Bin 0 -> 143481 bytes images/Ghostty.icon/Assets/gloss.png | Bin 0 -> 3353 bytes images/Ghostty.icon/icon.json | 170 ++++++++++++++++++ images/icons/icon_1024.png | Bin 464853 -> 2365230 bytes images/icons/icon_1024@2x.png | Bin 0 -> 2365230 bytes images/icons/icon_128.png | Bin 15177 -> 15089 bytes images/icons/icon_256.png | Bin 68189 -> 237699 bytes images/icons/icon_256@2x.png | Bin 221047 -> 237699 bytes images/icons/icon_512.png | Bin 221047 -> 667563 bytes images/icons/icon_512@2x.png | Bin 0 -> 667563 bytes .../AppIcon.appiconset/Contents.json | 74 -------- .../macOS-AppIcon-1024px 1.png | Bin 464853 -> 0 bytes .../macOS-AppIcon-1024px.png | Bin 464853 -> 0 bytes .../macOS-AppIcon-128px-128pt@1x.png | Bin 15177 -> 0 bytes .../macOS-AppIcon-16px-16pt@1x.png | Bin 666 -> 0 bytes .../macOS-AppIcon-256px-128pt@2x 1.png | Bin 68177 -> 0 bytes .../macOS-AppIcon-256px-128pt@2x.png | Bin 68177 -> 0 bytes .../macOS-AppIcon-32px-16pt@2x.png | Bin 1562 -> 0 bytes .../macOS-AppIcon-32px-32pt@1x.png | Bin 1564 -> 0 bytes .../macOS-AppIcon-512px-256pt@2x.png | Bin 221047 -> 0 bytes .../macOS-AppIcon-512px.png | Bin 220725 -> 0 bytes .../macOS-AppIcon-64px-32pt@2x.png | Bin 4485 -> 0 bytes macos/Ghostty.xcodeproj/project.pbxproj | 18 +- .../ColorizedGhosttyIconImage.swift | 14 ++ 30 files changed, 199 insertions(+), 97 deletions(-) delete mode 100644 dist/macos/Ghostty.icns delete mode 100644 dist/macos/Info.plist create mode 100644 images/Ghostty.icon/Assets/Ghostty.png create mode 100644 images/Ghostty.icon/Assets/Inner Bevel 6px.png create mode 100644 images/Ghostty.icon/Assets/Screen Effects.png create mode 100644 images/Ghostty.icon/Assets/Screen.png create mode 100644 images/Ghostty.icon/Assets/gloss.png create mode 100644 images/Ghostty.icon/icon.json create mode 100644 images/icons/icon_1024@2x.png create mode 100644 images/icons/icon_512@2x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-1024px 1.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-1024px.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-128px-128pt@1x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-16px-16pt@1x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-256px-128pt@2x 1.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-256px-128pt@2x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-32px-16pt@2x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-32px-32pt@1x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-512px-256pt@2x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-512px.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-64px-32pt@2x.png 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/dist/macos/Ghostty.icns b/dist/macos/Ghostty.icns deleted file mode 100644 index 44a44711aac562d23a82630e3b374ba9c5325b14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 382978 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