跪拜 Guibai
← All articles
Frontend

GoGUI: A Pure-Go, Zero-CGO GUI That Draws Web-Style UI Directly on OpenGL

By 陌离 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

A pure-Go, zero-cgo desktop GUI means no toolchain lock-in and no runtime dependency; on Windows it yields a single small executable. It also reopens the trade-off between WebView-based apps and hand-drawn UI: you get Web aesthetics without paying for a browser kernel, but you own every missing widget and edge case yourself.

Summary

GoGUI replaces the browser layer with its own component tree, layout engine, font atlas, and SDF shader. It draws everything with OpenGL 3.3 through purego dynamic loading of Win32/WGL, so there is no cgo, no WebView2, and no VC++ runtime. A minimal app is one .exe around 1.5 MB with UPX, down from 6.45 MB with debug info.

The framework reimplements enough of a browser to be useful: Flutter-style constraints layout, SDF-based rounded rects with stroke and shadow in one draw call, a multiline text pipeline with gamma-corrected glyph coverage for Chinese, atlas growth from 256² to 2048² with dirty-rect uploads, and a full IME input path. It also ships a Markdown renderer, charts, tables, video via spawning ffmpeg as a subprocess, and a WebView fallback.

The editor's own benchmarks show idle scenes produce zero draw frames, memory for 13 font instances is 3.2 MB (vs 208 MB with fixed 2048² atlases), and scroll dragging no longer restarts ffmpeg on every mouse move. The framework is Windows-only for now; macOS, Linux, Android, and WASM backends are planned but not shipped.

Takeaways
— GoGUI is a pure-Go GUI framework with zero cgo, using purego to dynamically load Win32/WGL and OpenGL 3.3 for rendering.
— Final executable size: 6.45 MB (default go build) → 4.54 MB (with -trimpath, -s -w, -buildvcs=false) → 1.52 MB (with UPX -9).
— Each rounded rectangle uses 6 vertices and 22 floats (528 bytes); batching reduces draw calls to single digits per screen.
— Layout uses Flutter-style constraints with two-pass measure/arrange; flex requires re-measuring only children without explicit main-axis size.
— Text pipeline uses x/image/font/opentype, with gamma correction (1/1.8 ≈ 0.55) for bright text to match browser sharpness.
— Font atlases start at 256² (256 KB) and scale up to 2048² (16 MB) only when supersampling needs it; LRU eviction caps at 24 font instances.
— Dirty-rectangle glTexSubImage2D uploads replace full-atlas reuploads when new glyphs appear.
— Supersampling runs the frame at 2× (or configurable) into an FBO, downsamples with 4×4 box filter, and applies a subtle unsharp (default 0.4) to preserve 1px detail.
— Video playback spawns ffmpeg as a subprocess for both video (rawvideo → pipe → ring buffer) and audio (s16le → WinMM), using audio as the master clock; scrubbing delays ffmpeg restarts until mouse-up.
— Media Foundation fallback requires all IMFSourceReader calls serialized on one thread via win32.MFRun, otherwise access violations occur.
— Known limits: rich text, vertical text, and virtual lists not implemented; video decoding depends on external ffmpeg; DComp composition for WebView2 is experimental; production-ready platform is Windows only.
Conclusions

Spawning ffmpeg as a subprocess avoids cgo and keeps the binary small, but it adds process spawn and pipe overhead; using audio as master clock is a pragmatic fix for A/V drift.

Reimplementing browser features (Markdown parser, syntax highlighter, flex layout, IME) is a large surface; the author admits it's heavy but keeps control. The trade-off is real: teams with front-end familiarity might prefer WebView for speed of iteration.

The gamma correction finding (0.55 for bright text) shows that naive sRGB blending isn't enough for CJK small fonts; browsers apply coverage gamma, and self-drawn UIs must replicate it to avoid blur.

Supersampling doesn't automatically fix text: the author found that gamma compensation should NOT be scaled down with SSAA because the box filter linearly averages sub-samples, so a uniform gamma works across all scales. This is a non-obvious detail for anyone doing SSAA.

Batch rendering with SDF for all shape effects (fill, stroke, shadow) is a clever way to cut draw calls, but it couples effects into a single shader; adding a new effect requires shader changes, not just a new draw path.

Concepts & terms
purego
A library that allows calling C functions from Go by dynamically loading shared libraries without cgo; it registers function pointers from DLLs and wraps them as Go functions.
SDF (Signed Distance Field)
A function that returns the signed distance from a point to a shape's boundary; used here to render rounded rectangles with smooth edges in a fragment shader.
SSAA (Supersampling Anti-Aliasing)
Rendering the scene at a higher resolution and downsampling to the target resolution to smooth edges and reduce aliasing.
LRU atlas
A cache of glyph textures that evicts least-recently-used fonts to control memory usage.
MediaClock
A playback clock driven by the audio device's played samples rather than wall-clock time, to keep video in sync with audio.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗