Skip to content

apprt: minimal Win32 skeleton (window + WGL + CoreSurface wiring)#12403

Open
mattn wants to merge 6 commits intoghostty-org:mainfrom
mattn:win32/t2-apprt-skeleton
Open

apprt: minimal Win32 skeleton (window + WGL + CoreSurface wiring)#12403
mattn wants to merge 6 commits intoghostty-org:mainfrom
mattn:win32/t2-apprt-skeleton

Conversation

@mattn
Copy link
Copy Markdown
Contributor

@mattn mattn commented Apr 23, 2026

First Tier 2 PR of the win32-apprt upstreaming series. Adds a .win32 variant to the apprt Runtime enum, defaulting on Windows exe builds, and enough surface/renderer wiring that zig build -Dtarget=native-native-gnu produces a ghostty.exe which opens a window and runs cmd.exe under ConPTY. The result is a working terminal at a basic level -- you can see output and the terminal resizes with the window.

The four commits stack in a straightforward order:

  1. Add minimal Win32 application runtime -- src/apprt/win32/App.zig creates the HWND and runs the message loop, src/apprt/win32/Surface.zig is a stub. Window opens, no rendering yet.
  2. Add WGL OpenGL context and surface interface -- pixel format + WGL context in Surface.zig plus the core Surface interface methods (getContentScale, getSize, etc.) the renderer needs.
  3. Wire up CoreSurface with WGL context threading -- App.initCoreSurface, release/re-acquire GL context between main thread and renderer thread, so the renderer thread can draw. ConPTY successfully launches cmd.exe at this point.
  4. Add SwapBuffers and WM_SIZE -- SwapBuffers after drawFrame so rendered frames actually appear, and WM_SIZE forwards to CoreSurface.sizeCallback so the terminal resizes.

These four commits are bundled into one PR rather than four because each intermediate state would merge a non-functional ghostty.exe into main -- after commit 1 the window is blank, after commit 2 it's still blank, after commit 3 cmd.exe runs but produces no visible output. Only after commit 4 does the binary behave like a terminal. The commit boundaries are preserved inside this PR so the review can be read as a sequence, but the merge itself needs all four.

Intentionally not included (each item is its own PR in the series):

  • Keyboard, mouse, clipboard, IME input
  • DPI awareness, focus events, window title updates
  • Font-family config (discovery works but config wiring is separate)
  • Dialogs, quit-confirm, child-exit bar
  • Splits, tabs, command palette, quick terminal, multi-window
  • Single-instance mutex, IPC window args
  • Taskbar progress, native scrollbars
  • Adjacent conpty.dll load

The tracker (mattn#1) lists all the follow-up items.


AI usage disclosure: developed with Claude Code (Claude Opus 4.7). The skeleton code comes from my existing win32-apprt branch; Claude assisted with cherry-picking onto upstream main, resolving a merge conflict against the already-merged DllMain change from #12373, and validating the build.

Part of the Win32 apprt upstreaming series (see discussion #2563 / mattn#1).

mattn and others added 4 commits April 24, 2026 08:26
Introduce a new `win32` apprt backend that creates a native Win32
window and runs a standard message loop. This enables `zig build` on
Windows (with MinGW/GNU ABI) to produce a working ghostty.exe.

Changes:
- Add `win32` variant to apprt Runtime enum, defaulting on Windows
- Create win32/App.zig with Win32 window creation and event loop
- Create win32/Surface.zig stub
- Link user32, gdi32, opengl32 for win32 runtime in SharedDeps
- Fix DllMain for GNU ABI (provide no-op handler instead of void type)
- Handle win32 case in Config.zig apprt-specific defaults

The window opens but does not yet render terminal content. Next steps
include OpenGL context setup, ConPTY integration, and input handling.

Co-authored-by: Claude <noreply@anthropic.com>
- Create WGL OpenGL context in Surface.zig with pixel format setup
- Add core Surface interface methods (getContentScale, getSize, etc.)
- Add win32 cases to OpenGL.zig renderer (surfaceInit, threadEnter, threadExit)
- Fix Clipboard.getGObjectType for win32 runtime in structs.zig
- Store App pointer in HWND userdata for wndProc callbacks
- Handle WM_SIZE to track window dimensions

Co-authored-by: Claude <noreply@anthropic.com>
- Initialize CoreSurface in App.initCoreSurface() with config and surfaces
- Release WGL context from main thread in finalizeSurfaceInit
- Re-acquire WGL context on renderer thread in threadEnter
- Release context in threadExit
- Add makeContextCurrent/releaseContext to win32 Surface
- Store App pointer in Surface for rtApp() method
- cmd.exe now starts successfully via ConPTY

Co-authored-by: Claude <noreply@anthropic.com>
- Call swapBuffers() after drawFrame on Win32 to present rendered content
- Forward WM_SIZE to CoreSurface.sizeCallback for terminal resize
- Use @hasDecl to conditionally call swapBuffers only on Win32

Co-authored-by: Claude <noreply@anthropic.com>
@mattn mattn requested a review from a team as a code owner April 23, 2026 23:30
@marler8997
Copy link
Copy Markdown
Contributor

Here's my feedback. I'm not a ghostty maintainer but I have alot of experience writing code for Windows.

It looks like there's alot of return values being ignored. IMO you should almost never ignore a return value when calling a win32 function. In addition, when an error is being handled, the error should be retrieved via GetLastError to see what went wrong, probably logged somewhere.

You're also free to leverage "zigwin32" for all these type/function definitions from the win32 API. It's found at https://github.com/marlersoft/zigwin32, I'd add this as a dependency and here's how I'd recommend using it:

const win32 = @import("win32").everything;

const my_hello_string= win32.L("Hello");
const hwnd = win32.CreateWindowExW(...) orelse win32.panicWin32("CreateWindow", win32.GetLastError());

Since the win32 API is in C, every symbol pretty much has to be unique so we can expose it all in a single namespace.

For many functions in win32, if they fail, something has gone horribly wrong. For these kinds of functions you can simply call win32.panicWin32("FunctionName", win32.GetLastError()). I've also included some wrapper functions that will call this for you, giving you convenient builtin error handling with proper error reporting, i.e.

const dpi = win32.dpiFromHwnd(hwnd);
const client_size = win32.getClientSize(hwnd);
_, const ps = win32.beginPaint(hwnd);
defer win32.endPaint(hwnd, &ps);

Add GetLastError-based warning logs for Win32 calls whose failure is
worth surfacing: PostMessageW (wakeup), DestroyWindow, SwapBuffers,
and wglMakeCurrent. Other ignored returns (TranslateMessage,
DispatchMessageW, ShowWindow, UpdateWindow, EndPaint, SetWindowLongPtrW
on first call) do not indicate errors and are intentionally unchecked.

Co-authored-by: Claude <noreply@anthropic.com>
@mattn
Copy link
Copy Markdown
Contributor Author

mattn commented Apr 24, 2026

Thanks! Addressed the ignored-return issue in 289c6a5. Added GetLastError-based warning logs for the Win32 calls where failure is worth surfacing (PostMessageW, DestroyWindow, SwapBuffers, wglMakeCurrent). The remaining unchecked returns (TranslateMessage, DispatchMessageW, ShowWindow, etc.) don't indicate errors per the docs, so I left them as-is.

Re: zigwin32 -- good suggestion; I'll consider it for a follow-up once the maintainer has a chance to weigh in on adding the dependency.

@jcollie
Copy link
Copy Markdown
Member

jcollie commented Apr 24, 2026

Re: zigwin32 - probably a good idea, especially as Zig 0.16 is dropping a lot of stuff from std.os.windows
https://ziglang.org/download/0.16.0/release-notes.html#posix-and-oswindows-removals

Per @marler8997 and @jcollie, use the marlersoft/zigwin32 bindings
instead of hand-rolled extern decls for the Win32 API. Drops the
inline type / constant / function declarations in App.zig and
Surface.zig in favor of `@import("win32").everything`. The binding
is declared lazy in build.zig.zon so it's only fetched when building
the win32 apprt.

Net effect: -240 / +101 lines. Type-safe style flags (packed structs
for `WNDCLASS_STYLES`, `WINDOW_STYLE`, pixel format flags) replace
raw OR'd u32 constants. Also clears a known Zig 0.16 risk since
std.os.windows is shedding symbols the earlier code was relying on
(pointed out in jcollie's review note).

Co-authored-by: Claude <noreply@anthropic.com>
@mattn
Copy link
Copy Markdown
Contributor Author

mattn commented Apr 24, 2026

Ported the apprt to zigwin32 in 2ad00dc. Dropped the hand-rolled extern decls in App.zig and Surface.zig and now go through @import("win32").everything. The binding is lazy-fetched in build.zig.zon so it's only pulled in for win32 apprt builds. Net -240 / +101, and style flags are now type-safe packed structs instead of OR'd u32 constants. Thanks for the push on this -- it also gets us off std.os.windows symbols that Zig 0.16 is removing.

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.

3 participants