Skip to content

Building Griff

Building Griff takes the .NET 10 SDK on Windows.

Terminal window
dotnet build
dotnet test
dotnet run --project src/Griff.Editor

Every project shares Directory.Build.props (nullable on, warnings are errors, the .NET analyzers’ recommended rules), package versions live in Directory.Packages.props, and .editorconfig holds the code style.

GRIFF_MUTE=1 keeps anything Griff runs silent, never opening the sound device, while games run exactly as they would with sound: the editor (for that session, leaving its saved Game → Mute alone), the players it starts, exported games (or --mute) and BunnyMark (which passes it on to its game); BunnyBench never makes a sound. The editor’s tests set it for themselves. Demo checks (toolsdemo.ps1 -Check) are silent; narrated playback speaks, unless the speaker on the demo’s bar mutes the narration (kept for the next demo, in %LOCALAPPDATA%Griffdemo.json, apart from the editor’s settings, which a demo never reads or writes) or -Mute (GRIFF_MUTE_NARRATION=1) plays that one in silence.

BunnyMark measures an exported game: powershell -NoProfile -ExecutionPolicy Bypass -File tools\bunnymark.ps1 builds the editor in Release, exports the BunnyMark template with the standalone player, plays it on screen until it has found its count and held it for ten seconds, and prints the count with the hold’s frame rate, 1% low and garbage collections (-Runs 3 prints the middle of three; -Count 50000 holds that many instead of finding the count; -NoBuild skips the build). Close other programs first. It’s a heuristic, not a test: the count varies by about a sixth from run to run, so look into a drop rather than trust one run. Changes to how the game runs record their count before and after. BunnyBench (dotnet run -c Release tools\bunnybench.cs -- --count 50000) measures the same bunnies with nothing on the screen, in seconds: it times each frame’s steps, building the frame, OpenGL drawing it (into a hidden window) and the GPU’s part, and counts the garbage collections and bytes allocated (--steps 2 takes two steps a frame, as after a missed one; --by-step adds what each of the game’s steps costs; --profile times the frames as the editor’s Profiler does and prints its costs, the frame’s build split by what’s drawn; --no-draw leaves OpenGL out; --timeline keeps rewind snapshots as the editor does while playing and times them as Rewind, with the memory they take and how long copying them out and settling them take; --workers 1 does everything on one thread, to compare; --dark puts the bunnies in the dark among walls of a tilemap without a tileset and crates, lit by a light that casts shadows and one that doesn’t). On a mid-range desktop PC (20 logical processors), 100,000 bunnies take 1.6 ms of steps, 2.9 ms to build and 2.8 ms to draw (7.4 ms of a 16.7 ms frame; 8.9 ms with two steps), with the GPU’s 4.4 ms alongside, and allocate next to nothing a frame (4 bytes at 50,000; 724 before each frame was built into the objects of the one before); before the frame was built and the bunnies’ scripts run across the cores, they took 4.4, 10.7 and 2.7 ms (18 ms; 22 ms with two steps, which is why 100,000 settled at 30 frames a second). 50,000 take 0.8, 1.3 and 1.4 ms (3.6 ms; 9.2 before), and before the BunnyMark work took 4.4, 13.8 and 6.9 ms, when drawing made 14 MB of garbage a frame. Putting drawing behind the renderer interface (the frame worked out once as a DrawList for every renderer) cost nothing: drawing 100,000 took 3.48 to 3.54 ms against 3.63 to 3.71 before (alternating, on a busier machine than the numbers above), 3.5 ms against 5.2 in the dark, with no garbage, and an exported game still holds 100,000 at 60 fps with a 1% low of 60 and no collections in 3 runs out of 3, as before. These are medians of runs alternating before and after, with other programs open: they moved by about 0.1 ms from run to run, the worst frames by a few. It counts the frames the screen shows, as it always did; since the game moves in fixed steps, a frame that falls behind takes two steps the next time, so past the count the frame rate drops off sharply rather than gently, and the count is where the game can no longer catch up after a missed frame: the frame after one takes two steps, and if those and drawing don’t fit in a sixtieth of a second the game settles at 30 frames a second. Fixed steps cost the count about 7% (30,600 before, about 28,500 after); drawing between steps costs next to nothing (with one step a frame and the drawing still blended, it held 30,634).

The website, griff.games, is built from site/ with Astro Starlight (Node 24: npm ci, then npm run dev serves it at http://127.0.0.1:4321/, npm run build builds it and npm run check-links checks the built site has no broken links; the reference pages are written from the code first by npm run reference, which runs tools/docs.cs with the .NET SDK, and npm run build:all does both). The Site workflow (.github/workflows/site.yml) writes the reference on a Windows runner, builds the site, checks its links and deploys it to GitHub Pages on every push to main. Its screenshots are taken from the editor over the MCP (npm run screenshots); site/README.md says how, and what to set up for the domain.

Exported games’ scripts are compiled optimized, like a Release build; the editor compiles them for debugging, with a breakpoint probe on each line (see The script editor), which makes saving a script take a little longer there (about 50 ms for the Arcade template’s five scripts, against 15). Exported games are self-contained: they include the .NET runtime and run on any 64-bit Windows PC without installing anything (expect a zipped download of about 60 MB).

Building the editor also publishes the game players it exports games with (dotnet build src/Griff.Editor/Griff.Editor.csproj), and the tests are dotnet test tests/Griff.Tests and dotnet test tests/Griff.Editor.Tests. CI (.github/workflows/ci.yml) builds the editor and runs both test projects on Windows for every push and pull request, with Mesa’s software OpenGL so the player smoke test can draw. Its test steps pass --blame-hang-timeout 10m, so a test run that stops making progress is killed after ten minutes with the test that was running named, instead of hanging for hours; add the same to a local run (dotnet test tests/Griff.Editor.Tests --blame-hang-timeout 10m) to get that too. When a test fails, tools/test-failures.ps1 reads the run’s .trx files and names each failed test, with the first lines of its message and stack, as an annotation on the run, which the run’s page and GitHub’s public API show without signing in (the log and the kept results need an account); the Site workflow’s game export (tools\web.ps1 -Site) says why it failed the same way. How the repository is laid out is in Architecture.

A release is a version tag: git tag v0.1.0-alpha.1 and git push origin v0.1.0-alpha.1 for a pre-release (versions are MAJOR.MINOR.PATCH, and a pre-release adds a label like -alpha.1), or v0.1.0 for a release. The Release workflow (.github/workflows/release.yml) builds the editor in Release on Windows with that version (it’s in the title bar: “Griff 0.1.0-alpha.1”; a build from source says just “Griff”), runs both test projects, zips the editor with its game players, free fonts, LICENSE and THIRD-PARTY-NOTICES.txt as Griff-0.1.0-alpha.1-win-x64.zip, the portable copy, and packs the same with Velopack as the installer and its updates: GriffEditor-prerelease-Setup.exe (or -stable- for a release), the full package, a delta package from the previous release on its channel, and the feed the installed editor reads (tools elease.ps1 -Version 0.1.0-alpha.1 makes all of it on your machine, in artifacts elease). It publishes them as a GitHub Release, marked as a pre-release for a pre-release tag, with the commits since the previous tag as its notes (tools elease-notes.ps1). The Releases page and the Download button show the newest one, pre-releases included. site/README.md has the maintainer’s steps.

An installed Griff updates itself from those releases: a little after it starts (Help → Check for Updates Automatically turns that off) or when you pick Help → Check for Updates, it looks for a newer version, downloads it and asks whether to restart into it now or the next time it starts. Help → Check for Updates opens the Updates window at once, which says what it’s doing (checking, then downloading with a bar showing how far it’s got) and then what came of it: that this version is up to date, why it couldn’t check, or the question about restarting; it doesn’t hold up the editor, and closing it leaves a download going. The check after starting says nothing unless it finds a newer version. Restarting now asks about unsaved changes first, as closing Griff does (Cancel keeps Griff open, and the update is used the next time it starts), then Griff closes, the updater puts the new version in place, showing its progress, and starts it. A Griff installed from a pre-release takes newer pre-releases and the first release after them; one installed from a release takes releases only. A build from source and the portable zip don’t update themselves. To try an update without publishing it, pack two versions into one folder (tools elease.ps1 -Version 0.1.0-alpha.1 -Out <folder>, then -Version 0.1.0-alpha.2 into the same folder), install the first from its Setup.exe (GriffEditor-prerelease-Setup.exe --silent --installto <folder> installs it somewhere of your own) and start it with GRIFF_UPDATE_SOURCE=<folder> elopack, which it reads instead of GitHub. Remove it with Update.exe uninstall in the folder it was installed to (or from Windows’ Installed apps).