A native Windows chrome for Beacon, written in C#/WPF over beacon-ffi.
This is the third shell against the same C ABI, alongside the GTK one (crates/beacon-gtk)
and the macOS one (swift/). The browser itself - tabs, navigation, history, downloads,
rendering - lives in Rust and is shared; everything here is chrome.
Working:
-
page rendering, mouse and scroll input, zoom
-
tab strip with favicons, close buttons, pinning, drag-to-reorder and a context menu
-
menu bar: File, Edit, View, History, Bookmarks, Help
-
bookmarks bar, bookmark manager, and the Bookmarks menu, all read from the engine
-
history window with live search
-
page context menu driven by
beacon_hit_test(open/copy link, open/copy image) -
forward-branch menu (right-click Forward) when the history has forked
-
session restore on a bare start, matching the GTK shell
-
view-source, reopen-closed-tab, crash reporting in the status line
-
Engine Room (F12): network, console and timing
-
settings editor over the engine's settings store
-
downloads: save dialog, progress, open when finished
-
the About dialog, sharing the macOS shell's artwork and credits crossfade
This shell binds 133 of the C ABI's 168 functions. Not yet bound: the form-control picker
(beacon_picker_*, so date, time and color inputs open nothing), the network panel's
per-request detail (cache, auth, upgrades, request changes, retries, preflights, TLS, and
the failure fix and spec links), beacon_question_*, beacon_clear_history,
beacon_clear_cookies, beacon_certificate_trust, beacon_identity_make,
beacon_identity_forget and beacon_download_url.
F12, or Tools -> Engine Room. Three tabs, over the developer-panel families in
beacon.h:
- Network - status, method, type, URL, size, time, phase and initiator, with request and response headers, the captured body, the redirect chain, and a Diagnosis tab that collects the engine's own failure and phase hints. Scoped to the current tab by default.
- Console - engine log records with level filtering. What gets captured follows
BEACON_LOG/RUST_LOG, which default to warnings only, so an empty console usually means the level rather than a broken panel. The panel says so rather than looking empty. - Timing - the engine's timing table, slowest namespace first, with count/total/avg and
p50/p95/p99. Empty when the engine was built without its
timingfeature, which compiles the subsystem out; that is nothing to show rather than an error.
Body capture and sensitive headers (Cookie, Authorization) are off by default and follow
the window's visibility: closing the panel stops capturing them. What was already captured
stays in the request log until it is evicted or cleared.
Fields the engine never reported show as - rather than 0: a request on a pooled
connection resolves nothing, which is not the same as resolving instantly.
Set BEACON_DEVTOOLS=1 to open the panel at launch, or BEACON_DEVTOOLS=all to raise the
settings and downloads windows with it. Both exist because over ssh a GUI lands in session 0,
where windows can be created but not seen - opening them at launch is the only way to put
their marshalling through its paces without a desktop.
Help -> About. A port of the macOS dialog rather than a re-imagining: the same two pages, the same 250 ms crossfade between artwork and credits, the same column of names over the right half of the picture.
The artwork is not copied into this project. The csproj references
swift/Sources/BeaconMac/Resources/ directly and packs it with Link=, so there is one copy
in the repository and the two shells cannot drift apart.
Text drawn over the picture uses explicit colours rather than theme ones, since a label that turned white on a dark theme would vanish into the picture's pale left half, and carries a drop shadow to stay legible where the lighthouse beam passes behind it.
Tools -> Advanced Settings. The ABI hands over rows - key, description, type, value, default, constraint - and this shell decides what each looks like: a checkbox for a bool, a popup where the schema restricts the value to literal choices, a text field otherwise with the accepted range beside it. Modified keys are bold and get a working Reset.
A write can be refused for an unknown key or a value outside its constraint, so every commit
checks the return and reverts the editor on false. Settings under net.* are read once when
the engine starts, so changes take effect next launch; the window says so.
Ctrl+J, or Tools -> Downloads. A BEACON_DOWNLOAD_OFFERED event raises a Windows save
dialog; the offer id keeps the answer attached to the right offer if a second download
arrives while the dialog is open.
The dialog is raised through the dispatcher rather than from inside the event pump, because a
modal dialog runs its own message loop and would tick CompositionTarget.Rendering,
re-entering the pump.
Progress of -1 means the server sent no length, so those rows show an indeterminate bar and the bytes received.
windows\package.ps1This is what the Windows CI workflow runs, and the zip it uploads is the one to test and
ship: target\windows\GosubBeacon-<version>-<build>-<commit>-engine-<engine>-win-x64.zip. It builds beacon.dll in release,
publishes the shell self-contained, copies the VC++ runtime DLLs beacon.dll imports next to
it, and zips the lot. Nothing has to be installed to run it: no .NET runtime, no VC++
redistributable. Not +crt-static: skia-binaries publishes no static-CRT build of the
engine's Skia features, and building Skia from source needs LLVM and a long build.
It needs the Rust toolchain in scripts/release-toolchain, the .NET SDK (10.0+), the
Visual Studio C++ build tools (for the linker and the runtime DLLs), Perl and NASM (vendored
OpenSSL). The icon is rendered from the lighthouse SVG by beacon-icon as
part of the build, so cargo must be on the PATH for dotnet build too.
The zip is not signed. SmartScreen stops the first start with "Windows protected your PC";
"More info", then "Run anyway". README-zip.txt goes into the zip as README.txt and says
so.
Broken at the moment: vendored OpenSSL's Configure refuses a Linux perl for MSVC, so
build on Windows with package.ps1 (above). Once that is fixed, both halves cross-compile
from Linux and only running needs Windows:
./windows/build.sh # debug
./windows/build.sh releasePrerequisites, none of which need root or Visual Studio:
cargo install cargo-xwin # fetches the MS CRT + Windows SDK itself
rustup toolchain install "$(cat scripts/release-toolchain)" --target x86_64-pc-windows-msvc
ln -s "$(command -v clang)" ~/bin/clang-cl # clang picks its MSVC mode from argv[0]
# lld-link ships with lld, usually already at /usr/lib/llvm-*/bin/lld-linkplus the .NET SDK (10.0+). EnableWindowsTargeting in Directory.Build.props is what lets
the SDK build a WPF project on a non-Windows host.
Two reasons, both hard requirements:
- It is the ABI .NET's P/Invoke expects.
skia-safepublishes prebuilt binaries forx86_64-pc-windows-msvcand not forx86_64-pc-windows-gnu. Without a prebuilt, skia-bindings compiles Skia from source, which assertswin_vc != ""and requires a Visual Studio install.
The prebuilt is keyed on target and features. Enabling a Linux-only skia feature
(x11, wayland, egl) produces a key that was never published; the download 404s and the
source build begins, so the win_vc error is three steps downstream of the real cause. Keep
the Windows feature set to svg + gl.
Unzip the release zip and run GosubBeacon.exe. A plain dotnet build is
framework-dependent instead, so the machine running it needs the .NET 10 Desktop Runtime:
windows/src/BeaconWindows/bin/Debug/net10.0-windows/win-x64/
beacon.dll is copied next to the exe by the build and must stay there: the debug one for a
Debug build, the release one for Release.
URLs on the command line become the startup tabs.
The engine's log, and the shell's own lines, go to beacon.log in the profile folder
(%APPDATA%\gosub-beacon\profiles\...); the previous run's is kept as beacon.log.1. A
WinExe has no console, so stderr goes nowhere.
The page is drawn with beacon_acquire_frame, which hands over finished BGRA pixels that
PageView blits into a WriteableBitmap. Beacon's frames are premultiplied BGRA, which is
exactly PixelFormats.Pbgra32, so nothing is converted.
There is no GPU path on Windows today. beacon_attach_view would take an HWND and have Vello
draw into it with no copy, but beacon-ffi compiles that path on macOS only and returns false
here. It was dropped because it needs a real graphics adapter: on a machine whose display is
a basic driver (a VM), there is no hardware D3D12, wgpu falls back to WARP, and Vello's
compute shaders crash inside d3d10warp.dll with an access violation. Bringing it back means
a runtime backend choice in beacon-ffi first, then PageView.cs.
windows/
Directory.Build.props EnableWindowsTargeting and shared settings
build.sh builds beacon.dll and the exe, from Linux
package.ps1 the release zip, on Windows (what CI runs)
README-zip.txt the README.txt that goes into the zip
src/BeaconWindows/
Interop/BeaconNative.cs raw P/Invoke, one-to-one with beacon.h
Interop/BeaconBrowser.cs safe wrapper: owns the handle, copies event strings
PageView.cs the page: frame blitting, input, hit-test requests
TabStrip.cs favicons, close, pin, drag-reorder, context menu
BookmarksBar.cs the bar, plus a clipboard helper that cannot throw
BookmarksWindow.cs bookmark manager
HistoryWindow.cs visited pages with live search
DeveloperPanel.cs the Engine Room: network, console and timing
SettingsWindow.cs the engine's settings store, typed per row
DownloadsWindow.cs progress, and open when finished
AboutWindow.cs the artwork, and the credits crossfade
BuildInfo.cs version and product name ("Gosub Beacon Preview")
Log.cs the shell's own lines, into beacon.log
MainWindow.xaml[.cs] menu bar, toolbar, the event pump, context menus
App.xaml[.cs] startup and the "beacon.dll is missing" message
The C ABI has 168 functions and this shell binds 133 of them; see the list near the top for what is still missing.
- The shell keeps no state. The tab strip asks the engine for the list every time rather than tracking one.
- Events are pulled, not pushed.
CompositionTarget.Renderingdrainsbeacon_poll_eventsonce a frame. - One thread, the UI thread. Every call in this shell is made from it.
Two more that are specific to C#: a char* returned to you must be freed with
beacon_string_free, so those functions return IntPtr and are read through
BeaconStrings.Take - marshalling them as string would free them with CoTaskMemFree.
And C's bool is one byte, so every bool is [MarshalAs(UnmanagedType.U1)]; the .NET
default is the four-byte Win32 BOOL.
| Ctrl+T | new tab |
| Ctrl+Shift+T | reopen closed tab |
| Ctrl+W | close tab |
| Ctrl+L | focus address bar |
| Ctrl+D | bookmark this page |
| Ctrl+H | history window |
| Ctrl+Shift+O | bookmark manager |
| Ctrl+U | view source |
| F12 | Engine Room |
| Ctrl+J | downloads |
| F5 / Ctrl+F5 | reload / reload ignoring cache |
| Esc | stop |
| Ctrl++ / Ctrl+- / Ctrl+0 | zoom in, out, actual size |
| Alt+Left / Alt+Right | back / forward |
| Middle-click a tab | close it |
| Middle-click a bookmark | open in a new tab |
| Right-click Forward | forward branches, when the history forked |