nitai/projects

Noxstrap

Every other Roblox bootstrapper is a fork of Bloxstrap: C#, .NET, WPF. This one starts at nothing. There is no egui, no winit, no skia, no windows-rs — the dependency list of every crate here is empty. Shapes are filled by a hand-written anti-aliased rasterizer, text comes from a TrueType parser and glyph rasterizer written against the spec, and the window is raw win32 through hand-declared bindings. The operating system is asked for exactly three things: a window, input events, and ‘put these pixels on screen’. What is done is the hard part — the graphics stack, the font stack, the window, and an interface shell that runs on them. What is not done is the Roblox side: this build draws the launcher, it does not install or start the game yet.

Rust2026
0dependencies, in every crate
326 MBof Roblox installed by its own code
12tweaks, expanding into engine flags
81tests across the workspace
69source files
27426lines of code

Download

Screenshots

The launcher, with its own title bar — the system caption is removed and the window controls are drawn by the same renderer as everything else
The launcher, with its own title bar — the system caption is removed and the window controls are drawn by the same renderer as everything else
FastFlags: renderer, quality, anti-aliasing and texture pickers, then toggles grouped by performance, graphics, window and privacy
FastFlags: renderer, quality, anti-aliasing and texture pickers, then toggles grouped by performance, graphics, window and privacy
File overrides: anything under the Mods folder is copied over the client, mirroring its own layout
File overrides: anything under the Mods folder is copied over the client, mirroring its own layout
A real install in flight: live package name, byte counts and throughput, from this project's own download and inflate code
A real install in flight: live package name, byte counts and throughput, from this project's own download and inflate code
Settings is launcher behaviour only — launching, installing, channel, paths
Settings is launcher behaviour only — launching, installing, channel, paths
Installed versions with their real on-disk sizes
Installed versions with their real on-disk sizes
The font engine — 8 to 30px, kerning on and off, five faces, accented composites, all parsed and rasterised here
The font engine — 8 to 30px, kerning on and off, five faces, accented composites, all parsed and rasterised here
The 2D engine exercised deliberately: gradients, radial glows, frosted glass, soft shadows, a rounded-corner clip and stroked curves
The 2D engine exercised deliberately: gradients, radial glows, frosted glass, soft shadows, a rounded-corner clip and stroked curves

Why from scratch

Bloxstrap, Voidstrap, Fishstrap and the rest are all the same program with different themes — a C# WPF app that needs the .NET runtime. The interesting version of this project is not another fork with a new colour scheme, it is finding out what a bootstrapper looks like when nothing is borrowed. So the rule is strict: no GUI framework, no graphics library, not even a PNG encoder or a hash function pulled in from elsewhere. Everything below the operating system call boundary is written here.

How it looks, and why

The first interface shell looked like every dark dashboard a language model produces: a violet accent with a violet-to-cyan gradient, rounded cards with soft shadows floating on a slightly different dark background, a sidebar with five items and a title-and-subtitle header, and filler copy where information should be. That is a real failure mode and worth naming, because the shape is so default that it reads as generic on sight. Three alternatives were built and rendered side by side — a near-monochrome graphite version, a dense monospace instrument version, and a marquee version driven by display type and one saturated colour — and graphite won. So: no shadows, no gradients, no coloured accent, corners at three pixels only on buttons. Structure comes from hairlines, spacing and type weight. The primary action is a solid white rectangle on near-black, because contrast is a stronger signal than colour. Numbers, versions and file names are set in monospace. When a download is running the layout gives way to a package table with live byte counts, since at that moment the data is the interface.

How fast can it start a game

Every bootstrapper, and Roblox's own app, makes you wait before the game even begins loading. That wait has parts, and only some of them belong to the launcher. Measured here: the version check against Roblox's client-settings service costs 61 to 366 milliseconds depending on the network, and it happens before anything else. So it was moved off the launch path — with instant launch on, an installed client starts immediately and the check runs afterwards, surfacing as a quiet banner if a newer build exists. What remains on the launch path is scanning the installed versions and building the command line: 0.05 milliseconds together. When the browser hands over a roblox-player link, no window is created at all — the process starts the client and exits. There is also a preload option that reads the client's largest files once at startup so the operating system has them cached before the executable is actually run, which is a first-launch win and a no-op afterwards. What cannot be removed is the client's own initialisation, its authentication ticket, the PlaceLauncher handshake and the server allocation. Those are inside Roblox's binary and on Roblox's servers, and anything claiming to skip them is not telling the truth. The honest summary is that the launcher's share of the wait is now effectively zero, and the rest is not ours to cut.

The renderer

nox-gfx is a 2D engine of about 2,000 lines. Filling a shape works by accumulating signed area into a coverage buffer and running a prefix sum along each row, which gives exact analytic anti-aliasing in one pass with no supersampling. Strokes are emitted as a union of per-segment quadrilaterals and joint discs straight into that buffer — because every contour shares a winding direction, overlaps merge for free, so self-intersecting strokes and rings work without a real stroking algorithm. On top of that: linear and radial gradients, blur, frosted-glass backdrops, drop shadows, rounded-rectangle geometry, affine transforms and rectangle clipping. Pixels are premultiplied BGRA in a u32, which is exactly the layout of a Win32 DIB section, so putting a frame on screen is a single blit with no conversion.

Its own title bar

The system caption is gone. The window intercepts WM_NCCALCSIZE to make the client area cover the whole window, which removes the frame, and answers WM_NCHITTEST itself: within six pixels of an edge it reports the matching resize corner or side, inside the top strip it reports caption so the window drags, and over the three controls it reports client so they behave as buttons. The application hands the window the rectangles of its own controls each frame, which is what keeps drag and click from fighting each other. The frame is extended one pixel into the client area through DWM so the drop shadow and snap behaviour survive, and a maximised window is inset by the frame size or its edges spill onto the next monitor. The minimise, maximise and close glyphs are drawn as paths by the same rasterizer as the rest of the interface, so they stay monochrome and line up exactly.

Reading what the client is doing

Roblox exposes no way to ask which game you are in, so every launcher of this kind reads it out of the client's own log — and this one does too. The log lives in %localappdata%\\Roblox\\logs, one file per session; the watcher picks the newest file whose name contains Player, tails it by byte offset, holds back any partial trailing line until the rest arrives, and switches files when a new session starts. Four markers carry the whole join sequence: an Output line announcing the job id, place id and machine address; a GameJoinLoadTime report carrying the universe and user ids; a Network line whose serverId confirms which machine was actually reached; and a disconnect line when the session ends. The markers were established by reading Bloxstrap's watcher, which is MIT licensed and no longer maintained; the parser, the tailing and the state machine here are written from scratch, and the state machine is tested against a synthetic log that grows a line at a time, including a line split across two polls.

Learned from the others, rebuilt

Reading how Bloxstrap, Fishstrap and Voidstrap approach the problem was worth more than guessing. From that: engine flags are far more useful presented as named outcomes than as raw flag names, so the tweaks here are grouped into performance, graphics, window and privacy, and the renderer, quality level, anti-aliasing and texture detail are pickers rather than free text. Version pinning is worth having, because an update can regress a game you play, so any installed version can be pinned and the launcher will use it instead of the newest. Multi-instance matters to people running alt accounts, and it works by holding the client's own singleton mutex for the launcher's lifetime so a second client never sees it free. None of their code was copied — the flag names and log markers are facts about Roblox, and everything around them is written here.

Making it fast

The first working version drew a full scene in 44.9 ms. It now does the same scene in 14.0 ms on a loaded two-core VM, which is a pessimistic floor rather than a target machine. The wins, in order of payoff: blurring at reduced resolution and upsampling, since blurred output cannot resolve detail the blur is about to destroy — 25x on glass backdrops; an SSE2 compositor handling four pixels per instruction group; SWAR blending for the scalar path, doing all four colour channels in one 32-bit operation; per-row span bounds in the rasterizer; fixed-point incremental bilinear sampling in the shadow compositor; and occlusion skipping, which drops the shadow pixels that the opaque shape casting them is about to cover anyway.

The font engine

nox-text parses TrueType directly — head, hhea, maxp, hmtx, loca, glyf, cmap, kern, and collections — and feeds glyph outlines through the same rasterizer the shapes use, so text and graphics share one anti-aliasing path. It handles cmap formats 0, 4, 6 and 12, simple glyphs with their flag run-length and delta coordinate encoding, composite glyphs with nested transforms, and reconstructs the implied on-curve midpoints that TrueType leaves out of quadratic contours. Glyphs are cached per face, size and subpixel phase, with four horizontal phases and a gamma curve for stem weight, and rendering costs about 160 nanoseconds a glyph with a warm cache. Every read is bounds-checked, so a malformed font returns an error instead of crashing.

Proving the parser is right

The danger with a hand-written font parser is that it produces plausible garbage: text that looks like text but has quietly wrong metrics. So correctness is measured against a second, independent implementation — a TrueType reader written separately in Python from the specification, using nothing but struct. The two are diffed across all 294 fonts installed on the build machine, and must agree on units per em, glyph counts, vertical metrics, 7,350 character-to-glyph lookups, 924 exact decoded point bounding boxes, 420 composite contour counts and 1,538 kerning lookups. On top of that, 2,112 truncated and byte-corrupted font files have to be rejected or survive without panicking.

Bugs the tests caught that the eye did not

Four so far, and none were visible by looking. The box-blur sizing formula was missing its pass-count term, so every blur in the engine was about 30 percent too wide. The SSE2 and scalar compositors disagreed by one part in 255 on some pixels, because a ‘coverage is full, skip the blend’ shortcut used a threshold that did not match what coverage actually quantises to — which made the result depend on how pixels happened to group into vectors. The glyph cache was keyed on glyph, size and subpixel position but not on the face, so one font’s glyph was served for another’s, showing up as random bold letters in the middle of a word. And a kerning test passed while testing nothing at all, because its pair list referenced characters that were not in the probe set, so every lookup silently resolved to ‘skip’ — a reminder that a test going green on the first run deserves suspicion, not celebration.

The window

nox-win is raw win32 through hand-written extern declarations: RegisterClassExW, CreateWindowExW, a window procedure, a message pump, and StretchDIBits with a negative bitmap height to present the top-down frame buffer. It carries mouse movement, buttons, wheel, leave events, keys and character input with surrogate pairs joined correctly, plus DPI awareness and a minimum window size. Because there is no console under the windows subsystem, a panic hook turns a crash into a message box instead of a silent disappearance. There is also an offscreen backend used for development: on a machine with no display it runs the real drawing code for a set number of frames, accepts scripted pointer input so hover and click states are reachable, and writes the result to a PNG. Every screenshot on this page was produced that way.

What actually works today

It installs Roblox. Given a click it resolves the live client version, fetches the package manifest, downloads all twenty-one packages, checks each against the MD5 in the manifest, inflates them and lays them out in the directory structure the client expects, then writes AppSettings.xml and the engine-flag file. On the build machine that is 326 MB on disk in about twenty-five seconds. The download runs on its own thread and reports package name, byte counts and throughput back to the interface, so the window never freezes. Beyond that: file overrides, where anything under the Mods folder is copied over the client mirroring its layout, applied automatically after an install or on demand; a set of named tweaks that expand into engine flags, covering frame rate, telemetry, shadows, post-processing, lighting technology, terrain grass and the renderer backend, with a quality-level pin; installed versions listed with real sizes and removable; and settings that persist. Still missing: joining a specific game with an account, and a text field for typing custom flags by hand.

A caveat worth stating plainly

The Windows executable is cross-compiled from Linux with MinGW and has still never been run on Windows. It links cleanly, it is a valid 64-bit Windows GUI binary, and every import resolves against USER32, GDI32, ADVAPI32, KERNEL32 and WINHTTP — but that is compile-verification, not testing. The parts that are platform-independent are the ones that are heavily tested: the renderer, the font engine, the zip inflater, JSON, MD5 and the whole deploy pipeline, which really did download and extract a full client on the build machine. The parts taken on faith are the window itself, WinHTTP, and the registry writes. If it fails on first run it should fail with a message box rather than disappearing.

Known gaps

GPOS kerning is not implemented, only the older kern table — of the 294 fonts on the build machine 16 have kern while 243 have GPOS, so most modern faces currently render unkerned. There is no line breaking, no bidirectional text and no complex script shaping. Clipping is rectangular and snapped to whole pixels, so rounded clipping would need a mask layer. And the biggest remaining performance win is not in the renderer at all: redrawing only the region that changed, which belongs in the widget layer and will matter more than everything already done, because a real interface changes a small fraction of the screen each frame.

Everything under it is also from scratch

Extracting Roblox means reading zip archives, which means a DEFLATE decompressor, so there is one — a bit reader, canonical Huffman decoding with a fast lookup table in front of it, and back-reference copying, checked against python's zlib at every compression level. Checking a download against the manifest means MD5, so there is one, verified against python's hashlib across two hundred input lengths. Reading the client-settings response means JSON, so there is a parser and a writer. Talking to the servers means HTTPS, and that is the one place the rule bends: implementing TLS is not a reasonable thing to ship, so on Windows this calls WinHTTP, which is a system library in the same sense as USER32. Off Windows there is a development backend that drives curl, used only so the pipeline can be run and tested on the build machine.

The bug a synthetic test could not have found

The zip extractor refused every real Roblox package. Its zip-slip guard — the check that stops an archive writing outside the destination — normalised backslashes to forward slashes and then rejected anything starting with a slash as an absolute path. Roblox's packages contain an entry named exactly one backslash, the archive root marker, which normalised to a bare slash and tripped it. Every test written by hand had passed, because the hostile paths were invented rather than observed. The fix separates the three cases that were being conflated: a path that escapes the destination is refused, a leading slash is stripped rather than refused since it cannot escape anyway, and an empty result is a directory marker to skip. It only surfaced by pointing the thing at the real servers.

Source

Browse the whole tree in the reader below, or take the archive above.

Open source browser