Skip to content

Embed native OpenGL surfaces on Linux and Windows - #110

Open
lawrencecchen wants to merge 12 commits into
mainfrom
feat/electron-embedded-cross-platform
Open

Embed native OpenGL surfaces on Linux and Windows#110
lawrencecchen wants to merge 12 commits into
mainfrom
feat/electron-embedded-cross-platform

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jul 13, 2026

Copy link
Copy Markdown

Adds an embedder-owned OpenGL surface ABI for libghostty, Linux shared-library packaging, and a native HWND/WGL Electron example. The X11/GLX Electron host lives in the private manaflow-ai/electron-ghostty-demo repository.

The renderer updates the GL viewport from the live embedded surface size before each frame. GLAD thread-local contexts are zero-initialized before reload, preventing invalid dlclose during rapid teardown. Renderer and IO stop watchers are armed before embedded surface creation returns.

Validation against Ghostty commit 186ce2b:

  • Linux: exact merge built with Zig 0.15.2, then 5 cold Electron processes created 625 surfaces, performed 20,000 resizes and 125 immediate teardowns, and survived 25 deliberate Chromium renderer crashes. Zero unexpected crashes, hangs, or renderer deaths. Peak post-warmup growth: 4.99 MB.
  • Windows: exact merge built with Zig 0.15.2 and MSVC. Five fresh Electron 43.1.0 processes created 375 real libghostty surfaces, performed 12,500 resizes and 125 immediate teardowns, and survived 25 deliberate renderer crashes. Zero unexpected renderer deaths, unresponsive windows, native failures, or leftover processes.
  • Final Windows DLL: 28,307,456 bytes, SHA-256 5a9c6e1ff3ae39ead368ecf023a170105979dc1f3c23495f92f34ec763f1f2ab.
  • Computer Use on macOS and Windows covered typing, selection, scrolling, clipboard, right-click menus, and Chromium controls.

Windows cloud validation used Mesa llvmpipe because the RDP adapter exposed OpenGL 1.1. Hardware WGL remains a required follow-up.

Summary by CodeRabbit

  • New Features

    • Added OpenGL support for embedded integrations, including rendering, resizing, and buffer presentation.
    • Added a Windows Electron embedding example with native terminal controls, diagnostics, software-rendering support, and stress-test workflows.
    • Added an option to suppress terminal protocol responses for mirrored or externally managed terminals.
  • Bug Fixes

    • Improved Windows pseudo-console cleanup and input/output handling.
    • Reduced startup and teardown races during embedded surface creation and destruction.
  • Build Improvements

    • Improved cross-platform library packaging and installation of supporting resources.

@socket-security

socket-security Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednode-gyp@​13.0.19910010093100
Addedelectron@​43.1.09910010098100

View full report

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds embedded OpenGL platform APIs and rendering support, terminal response suppression, safer Windows ConPTY I/O teardown, standardized internal library artifacts, and a Windows Electron demo with native WGL/N-API integration, diagnostics, software-renderer deployment, and stress testing.

Changes

OpenGL embedding

Layer / File(s) Summary
Embedding contracts and terminal response control
include/ghostty.h, src/apprt/embedded.zig, src/termio/*, pkg/opengl/glad.zig
Adds OpenGL embedding callbacks and platform representations, propagates terminal response suppression, and supports an additional GLAD callback form.
Embedded OpenGL rendering and startup synchronization
src/renderer/OpenGL.zig, src/renderer/Thread.zig, src/termio/Thread.zig, src/Surface.zig, src/build/SharedDeps.zig
Initializes embedder-owned contexts, updates viewport and surface size, swaps buffers, enables GLAD for embedded libraries, and waits for renderer and IO startup readiness.
Library build and installation outputs
src/build/GhosttyLib.zig, build.zig
Tracks Windows import libraries, standardizes internal library names, and installs internal libraries and runtime resources.
Windows ConPTY teardown and overlapped I/O
src/pty.zig, src/termio/Exec.zig, src/os/windows.zig
Separates ConPTY pipes, makes handle release idempotent, adds Windows API bindings, and uses cancellable overlapped reads.
Windows Electron native embedder and validation tooling
example/electron-embed-windows/*, .gitattributes
Adds the N-API HWND/WGL addon, Electron host and renderer, Windows build scripts, Mesa deployment, package configuration, documentation, CRLF handling, and stress-cycle reporting.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: mitchellh, rockorager

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding native OpenGL surface embedding support for Linux and Windows.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/electron-embedded-cross-platform

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds native OpenGL surface embedding for libghostty. The main changes are:

  • A public embedder-owned OpenGL callback ABI.
  • Native HWND/WGL Electron integration on Windows.
  • Linux shared-library packaging and resource installation.
  • Live viewport updates for embedded surfaces.
  • Safer GLAD reload and thread-startup teardown ordering.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.
  • Surface teardown joins renderer work before the host destroys WGL resources.
  • Startup barriers prevent immediate destruction from losing stop notifications.
  • GLAD initialization prevents an untouched loader handle from reaching the unload path.

Important Files Changed

Filename Overview
include/ghostty.h Adds the public embedder-owned OpenGL callback ABI.
src/apprt/embedded.zig Connects the OpenGL callbacks to embedded surface creation and lifecycle management.
src/Surface.zig Waits for renderer and IO stop watchers before embedded surface initialization completes.
src/renderer/OpenGL.zig Manages embedded contexts and updates the viewport from the live surface size.
src/renderer/Thread.zig Arms renderer shutdown before signaling that thread startup is complete.
src/termio/Thread.zig Applies the startup barrier to the terminal IO thread.
pkg/opengl/glad.zig Zero-initializes each thread-local GLAD context before loading function pointers.
example/electron-embed-windows/src/libghostty_embed_win.cc Implements the HWND/WGL host, callback bridge, input handling, and native teardown.
example/electron-embed-windows/main.js Adds the Electron host lifecycle, diagnostics, resizing, and stress workflow.
build.zig Updates shared-library naming and installs resources for library builds.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Host as Native Embedder
    participant ABI as libghostty ABI
    participant Surface as Embedded Surface
    participant Renderer as Renderer Thread
    participant GL as Host OpenGL Context

    Host->>ABI: Create surface with OpenGL callbacks
    ABI->>Surface: Initialize surface
    Surface->>Renderer: Start renderer
    Renderer->>Renderer: Arm stop watcher
    Renderer-->>Surface: Signal started
    Renderer->>GL: Make context current
    loop Each frame
        Renderer->>Surface: Read live dimensions
        Renderer->>GL: Update viewport and render
        Renderer->>GL: Swap buffers
    end
    Host->>ABI: Free surface
    ABI->>Renderer: Notify stop and join
    Renderer->>GL: Unload GLAD and clear context
    ABI-->>Host: Surface teardown complete
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Host as Native Embedder
    participant ABI as libghostty ABI
    participant Surface as Embedded Surface
    participant Renderer as Renderer Thread
    participant GL as Host OpenGL Context

    Host->>ABI: Create surface with OpenGL callbacks
    ABI->>Surface: Initialize surface
    Surface->>Renderer: Start renderer
    Renderer->>Renderer: Arm stop watcher
    Renderer-->>Surface: Signal started
    Renderer->>GL: Make context current
    loop Each frame
        Renderer->>Surface: Read live dimensions
        Renderer->>GL: Update viewport and render
        Renderer->>GL: Swap buffers
    end
    Host->>ABI: Free surface
    ABI->>Renderer: Notify stop and join
    Renderer->>GL: Unload GLAD and clear context
    ABI-->>Host: Surface teardown complete
Loading

Reviews (1): Last reviewed commit: "fix: harden embedded OpenGL resize and t..." | Re-trigger Greptile

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 437064019e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +350 to +351
host->wgl_choose_pixel_format = reinterpret_cast<WglChoosePixelFormatFn>(
load("wglChoosePixelFormat"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the GDI pixel-format entry points

When this runs against the normal Windows OpenGL stack, these lookups leave wgl_choose_pixel_format/wgl_set_pixel_format/wgl_swap_buffers null because the Win32 pixel-format APIs are exposed as ChoosePixelFormat, SetPixelFormat, and SwapBuffers from wingdi/GDI, not as wgl* exports. Since LoadWglApi treats any missing pointer as fatal, create() throws before the Electron example can create any terminal surface.

Useful? React with 👍 / 👎.

Comment thread src/renderer/OpenGL.zig
Comment on lines +209 to +210
errdefer leaveEmbedded();
try prepareContext(&embeddedGetProcAddress);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Balance make_current when surface creation unwinds

This errdefer is scoped only to surfaceInit, but Surface.init keeps the embedded context current until finalizeSurfaceInit after several later fallible steps such as font-grid setup and renderer construction. If one of those steps fails, ghostty_surface_new unwinds without calling finalizeSurfaceInit, so embedders see make_current succeed without a matching clear_current, which can leave their context lock/current context stuck after a failed surface creation.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@example/electron-embed-windows/scripts/start-software-renderer.cmd`:
- Around line 1-18: Convert start-software-renderer.cmd from LF-only to CRLF
line endings, preserving its existing commands and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ba76671e-01ad-4189-9db8-8cd47a0d90a9

📥 Commits

Reviewing files that changed from the base of the PR and between 81a6daa and 4370640.

⛔ Files ignored due to path filters (1)
  • example/electron-embed-windows/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (21)
  • build.zig
  • example/electron-embed-windows/.gitignore
  • example/electron-embed-windows/README.md
  • example/electron-embed-windows/binding.gyp
  • example/electron-embed-windows/main.js
  • example/electron-embed-windows/package.json
  • example/electron-embed-windows/renderer.html
  • example/electron-embed-windows/scripts/build-ghostty.ps1
  • example/electron-embed-windows/scripts/deploy-mesa.ps1
  • example/electron-embed-windows/scripts/start-software-renderer.cmd
  • example/electron-embed-windows/scripts/stress-cycles.mjs
  • example/electron-embed-windows/src/libghostty_embed_win.cc
  • include/ghostty.h
  • pkg/opengl/glad.zig
  • src/Surface.zig
  • src/apprt/embedded.zig
  • src/build/GhosttyLib.zig
  • src/build/SharedDeps.zig
  • src/renderer/OpenGL.zig
  • src/renderer/Thread.zig
  • src/termio/Thread.zig

Comment thread example/electron-embed-windows/scripts/start-software-renderer.cmd

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 186ce2b0b3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +256 to +257
ghostty_surface_mouse_pos(host->surface, GET_X_LPARAM(lparam),
GET_Y_LPARAM(lparam), CurrentModifiers(host));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid double-scaling Win32 mouse coordinates

On high-DPI displays, the mouse lparam coordinates for this child HWND are already client/framebuffer pixels, while ghostty_surface_mouse_pos goes through embedded.Surface.cursorPosCallback, which multiplies the supplied coordinates by the surface content scale. Because UpdateSurfaceMetrics also reports the scaled framebuffer size and content scale, passing GET_X/Y_LPARAM here makes clicks and selections land too far down/right at scales like 150%; convert back to DIPs before calling the C API.

Useful? React with 👍 / 👎.

Comment on lines +805 to +806
if (IsTextProducingKey(wparam) && !HasCommandModifier(host))
return 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve physical key events for printable keys

When no Ctrl/Alt/Super modifier is held, this returns before SendKey for all printable keys, including the numpad, and the later WM_CHAR path calls ghostty_surface_text_input, whose implementation explicitly bypasses keyboard protocol encoding. That means terminal modes that depend on physical key identity, such as application keypad or enhanced keyboard protocols, receive plain text instead of a key event; for example numpad 1 cannot be encoded as a keypad key.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/termio/stream_handler.zig (1)

19-61: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clarify the side-effecting nature of the suppression function.

Because this function acts as a destructor for .write_alloc messages by freeing req.data, its predicate-style name (suppressTerminalResponse) obscures the fact that it consumes the message.

Consider renaming it to dropTerminalResponse or consumeSuppressedResponse to clearly signal its side effects, protecting against accidental use-after-free or memory leaks if misused in future conditionals or logging.

♻️ Proposed refactor
-fn suppressTerminalResponse(enabled: bool, msg: termio.Message) bool {
+fn dropTerminalResponse(enabled: bool, msg: termio.Message) bool {
     if (!enabled) return false;
     switch (msg) {
         .write_small,
         .write_stable,
         .color_scheme_report,
         .size_report,
         .focused,
         => return true,
         .write_alloc => |req| {
             req.alloc.free(req.data);
             return true;
         },
         else => return false,
     }
 }

-test "terminal response suppression only drops parser writes" {
+test "dropTerminalResponse only drops parser writes" {
     const testing = std.testing;

     const small_bytes: []const u8 = "reply";
     const small = try termio.Message.writeReq(testing.allocator, small_bytes);
-    try testing.expect(suppressTerminalResponse(true, small));
+    try testing.expect(dropTerminalResponse(true, small));

     const large_bytes = [_]u8{'x'} ** 80;
     const large_slice: []const u8 = &large_bytes;
     const large = try termio.Message.writeReq(testing.allocator, large_slice);
-    try testing.expect(suppressTerminalResponse(true, large));
+    try testing.expect(dropTerminalResponse(true, large));

-    try testing.expect(!suppressTerminalResponse(
+    try testing.expect(!dropTerminalResponse(
         false,
         .{ .write_stable = "reply" },
     ));
-    try testing.expect(!suppressTerminalResponse(
+    try testing.expect(!dropTerminalResponse(
         true,
         .{ .linefeed_mode = true },
     ));
-    try testing.expect(suppressTerminalResponse(
+    try testing.expect(dropTerminalResponse(
         true,
         .{ .size_report = .csi_18_t },
     ));
 }

Apply the rename at the call site on line 187 as well:

-        if (suppressTerminalResponse(self.suppress_terminal_responses, msg))
+        if (dropTerminalResponse(self.suppress_terminal_responses, msg))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/termio/stream_handler.zig` around lines 19 - 61, Rename the
side-effecting suppressTerminalResponse function to dropTerminalResponse, or an
equivalent consume-prefixed name, so callers understand that .write_alloc
messages are freed. Update the call site around the indicated handler logic and
adjust the related test name/references while preserving the existing
suppression behavior.
src/pty.zig (1)

391-451: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting a helper for the pipe-pair creation.

in_pipe/in_pipe_pty and out_pipe/out_pipe_pty creation are near-duplicates (CreateNamedPipeW + CreateFileW, differing only by path/access direction). A small helper parameterized on direction would reduce the chance of the two paths drifting apart on future edits.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pty.zig` around lines 391 - 451, Extract the duplicated
CreateNamedPipeW/CreateFileW logic from the Windows PTY setup into a helper that
accepts the pipe path and direction-specific access flags, then use it for both
in_pipe/in_pipe_pty and out_pipe/out_pipe_pty. Preserve the existing overlapped
app-side handles, synchronous ConPTY client handles, security attributes, buffer
settings, and error propagation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/pty.zig`:
- Around line 391-451: Extract the duplicated CreateNamedPipeW/CreateFileW logic
from the Windows PTY setup into a helper that accepts the pipe path and
direction-specific access flags, then use it for both in_pipe/in_pipe_pty and
out_pipe/out_pipe_pty. Preserve the existing overlapped app-side handles,
synchronous ConPTY client handles, security attributes, buffer settings, and
error propagation.

In `@src/termio/stream_handler.zig`:
- Around line 19-61: Rename the side-effecting suppressTerminalResponse function
to dropTerminalResponse, or an equivalent consume-prefixed name, so callers
understand that .write_alloc messages are freed. Update the call site around the
indicated handler logic and adjust the related test name/references while
preserving the existing suppression behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3127f4ae-c394-430b-8e37-a7e5cbc44105

📥 Commits

Reviewing files that changed from the base of the PR and between 186ce2b and 350c96b.

📒 Files selected for processing (12)
  • example/electron-embed-windows/README.md
  • example/electron-embed-windows/package.json
  • example/electron-embed-windows/src/libghostty_embed_win.cc
  • include/ghostty.h
  • src/Surface.zig
  • src/apprt/embedded.zig
  • src/os/windows.zig
  • src/pty.zig
  • src/termio/Exec.zig
  • src/termio/Options.zig
  • src/termio/Termio.zig
  • src/termio/stream_handler.zig
🚧 Files skipped from review as they are similar to previous changes (3)
  • example/electron-embed-windows/package.json
  • include/ghostty.h
  • example/electron-embed-windows/src/libghostty_embed_win.cc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant