# Noxstrap A Roblox bootstrapper written from zero in Rust — no .NET, no Bloxstrap fork, and no GUI framework. The entire interface is drawn by a renderer that lives in this repo. ## Rules - No `egui`, `winit`, `skia`, `wgpu`, or any other GUI/graphics crate. - No .NET, no WPF, no WebView, no HTML/CSS. - Everything down to glyph outlines and anti-aliasing is our code. The operating system is asked for exactly three things: a window, input events, and "put these pixels on screen". ## Crates | crate | what it is | status | |---|---|---| | `nox-gfx` | 2D engine — paths, analytic AA rasterizer, gradients, blur, shadows, compositing | **working** | | `nox-text` | TrueType parser + glyph rasterizer + cache + layout | **working** | | `nox-ui` | layout, input routing, animation, widgets | not started | | `nox-win` | Win32 window, DIB present, input; offscreen backend for headless dev | **working** | | `nox-zip` | DEFLATE decompression + zip reading | **working** | | `nox-json` | JSON reader and writer | **working** | | `nox-net` | HTTPS via WinHTTP (curl backend for headless dev) | **working** | | `nox-roblox` | version resolve, manifest, MD5, download, extract, launch | **working** | | `noxstrap` | the app — installs Roblox; launching into a game still to come | **working** | ## nox-gfx No dependencies, not even for PNG output. - **Rasterizer** — signed-area coverage accumulation (one f32 buffer, per-row prefix sum). Analytic anti-aliasing, no supersampling. `abs().min(1)` approximates nonzero winding. - **Paths** — lines, quadratics, cubics, adaptive flattening; rects, per-corner rounded rects, ellipses, arcs; affine transforms. - **Strokes** — emitted as a union of per-segment quads plus joint discs. All contours share a winding sign, so overlaps union for free and self-intersecting strokes just work. - **Paint** — solid, linear gradient, radial gradient, multi-stop, 256-entry LUTs. - **Blur / glass / shadows** — 3 box passes at reduced resolution (k = σ/3, up to 1/8), bilinearly upsampled. `blurred_layer` gives a backdrop for frosted panels. - **Pixels** — premultiplied BGRA `u32`, which is exactly a Win32 DIB section, so presenting is a single blit with no conversion. Blending is SWAR: four channels per `u32` op. - **Clipping** — rect clips, snapped to integers so the inner loops carry no clip math. ### Dev loop The renderer is developed headless. `examples/preview.rs` draws a mock launcher and writes a PNG; `examples/bench.rs` times each primitive. ```sh cargo run --release -p nox-gfx --example preview -- /tmp/preview.png cargo run --release -p nox-gfx --example bench ``` ### Correctness `cargo test -p nox-gfx` — 31 tests, and they are the point, not decoration: - Every blend function is checked against an `f64` reference compositor, exhaustively over a grid of alpha/colour combinations, to within 1/255. - The rasterizer is checked against a brute-force 16x supersampled reference on rects, circles, rounded rects, triangles and a self-intersecting star. - **The SSE2 paths are checked against their own scalar twins for bit-exact equality** on random rows at every length mod 4. A vector path that disagrees with the scalar path is a bug even when it looks fine. - Plus clipping, stroke geometry, shadow symmetry and falloff, blur energy conservation, transform algebra, curve flattening tolerance, and PNG chunk/CRC structure. Two real bugs came out of writing these, both invisible to the eye: 1. The box-blur size formula was missing its pass-count terms, so **every blur in the engine was about 30% too wide**. 2. The "coverage is full, so skip blending and just store" shortcut used a threshold that didn't match what coverage actually quantizes to, so the scalar and vector paths disagreed by 1/255 on some pixels. Both are now pinned to `simd::ZERO` / `simd::FULL`, derived from the quantizer itself, which makes the decision grouping-independent. ### Performance Measured on a loaded 2-core VM, so treat these as a pessimistic floor — several times slower than the machines this will actually run on. | operation | before | now | |---|---|---| | full 1100x680 scene, 5 soft shadows | 44.9 ms | **14.0 ms** | | fullscreen rounded-rect gradient | 3.97 ms | 1.30 ms | | large soft shadow (sigma 26) | 8.08 ms | 1.42 ms | | frosted-glass backdrop (sigma 18) | 7.13 ms | 0.28 ms | | radial glow, r=320 | 2.43 ms | 0.78 ms | | card fill | 0.85 ms | 0.32 ms | What got it there, in order of payoff: 1. **Blur at reduced resolution** (k = sigma/3, up to 1/8) then bilinear upsample — 25x on backdrops. Blurred output cannot resolve detail the blur is about to destroy. 2. **SSE2 composite** — four pixels per instruction group, for solid fills, both gradient types, and shadow masks. Roughly 3x. SSE2 is baseline on x86_64, so there is no runtime detection and no fallback to maintain (a scalar twin exists purely to test against). 3. **SWAR blending** for the scalar path — all four channels in one `u32` operation. 4. **Per-row span bounds** in the rasterizer instead of one bounding box for the whole shape. 5. **Fixed-point incremental bilinear** in the shadow compositor, replacing per-pixel float floor/convert/clamp. 6. **Occlusion skipping** — `shadow_occluded` drops the shadow pixels that the opaque shape casting it will cover anyway, typically half of a card shadow. The cost is now almost entirely the per-pixel composite: a large shadow spends 0.04 ms building and blurring its mask and 1.4 ms compositing it. Remaining wins, in order: 1. Damage-rect redraw — only repaint what changed. Belongs in `nox-ui` and will dwarf everything else, since a real UI changes a fraction of the screen per frame. 2. AVX2 (8 pixels wide) behind runtime detection. 3. Caching composited shadows for shapes that did not move. ## nox-text Also dependency-free. Parses TrueType directly and feeds glyph outlines through `nox-gfx`'s rasterizer, so text and shapes share one anti-aliasing path. - **Tables**: `head`, `hhea`, `maxp`, `hmtx`, `loca`, `glyf`, `cmap`, `kern`, plus TrueType Collections. Every read is bounds-checked; a malformed file returns an error, never a panic. - **cmap** formats 0, 4, 6 and 12, chosen by a platform/encoding preference score. - **glyf** simple glyphs (flag run-length decoding, short/long coordinate deltas) and composites (offsets, scale, 2x2 transforms, nested up to 6 deep). - **Quadratic outlines** with implied on-curve midpoints reconstructed, emitted straight into a `Path`. - **Glyph cache** keyed by face, glyph, size and horizontal subpixel position, with 4 subpixel phases and a gamma curve for stem weight. Per-glyph alpha bitmaps rather than a packed atlas — an atlas exists to reduce GPU texture binds, which a CPU renderer does not have. - **Layout** with kerning, letter spacing, and measurement without rasterizing. Rendering is ~160 ns per glyph at 14px with a warm cache, so a screenful of UI text costs well under a millisecond. `cargo run -p nox-text --example specimen` writes a proof sheet. ### Correctness The risk with a from-scratch font parser is quietly producing *plausible* garbage, so correctness here is measured against an independent implementation: `tests/gen_fixture.py` is a second TrueType reader, written separately from the spec using only Python's `struct`, that emits expected values for every font on the machine. Against **294 installed fonts**, the Rust parser must agree on: - units per em, glyph count, vertical metrics, font bounding box - **7,350 character-to-glyph lookups** across Latin, accented, Greek, CJK and symbols - **924 exact decoded point bounding boxes** for simple glyphs — this is what proves the flag-repeat and short/long delta decoding is right, since any slip moves a point - **420 composite glyph contour counts**, so nested component transforms are exercised - **1,538 kerning lookups**, 656 of them non-zero, hitting both ends of the binary search Plus 2,112 truncated and byte-corrupted font cases that must be rejected or survive without panicking, and a test that the glyph cache never serves one face's glyph for another's. Bugs this caught: 1. **The glyph cache keyed on (glyph, size, subpixel) but not the face**, so glyph 39 of DejaVu Bold was served for glyph 39 of DejaVu Sans. Visible in the specimen as random bold letters mid-word. Faces now carry a content fingerprint. 2. My first kerning check silently passed while testing nothing: the pair list referenced characters that were not in the probe set, so every lookup resolved to "skip". The fixture now samples pairs out of each font's own kern table. ### Known gap **GPOS kerning is not implemented yet** — only the legacy `kern` table. Of the 294 fonts here, 16 have `kern` and 243 have `GPOS`, so most modern faces currently render unkerned. This matters for whichever UI font gets bundled and is the next thing to build in this crate. ## nox-win Raw win32 through hand-written `extern "system"` declarations — no `windows-rs`. `RegisterClassExW`, a window procedure, a message pump, and `StretchDIBits` with a negative bitmap height to present the top-down frame buffer directly. Mouse move/buttons/wheel/leave, keys, and character input with surrogate pairs joined. DPI awareness, minimum window size, and a panic hook that turns a crash into a message box, since the windows subsystem has no console to print to. It also ships an **offscreen backend** for non-Windows targets. That is not a stub: it runs the real drawing code for a given number of frames, accepts scripted pointer input so hover and click states are reachable, and writes a PNG. Every screenshot of the app was produced on a headless Linux box that way: ```sh NOX_FRAMES=150 NOX_DUMP=out.png NOX_POINTER=342,224 NOX_PRESS=1 cargo run -p noxstrap ``` ## The deploy pipeline `nox-roblox` resolves the live client version, reads `-rbxPkgManifest.txt`, downloads all twenty-one packages, checks each against the MD5 in the manifest, inflates them with `nox-zip` and lays them out in the directory structure the client expects, then writes `AppSettings.xml` and `ClientSettings/ClientAppSettings.json`. On the build machine that is **326 MB on disk in about 25 seconds**, with `RobloxPlayerBeta.exe`, `RobloxCrashHandler.exe` and every content folder in place. The download runs on its own thread and reports package name, byte counts and throughput back to the interface, so the window never blocks. Everything under it is also from scratch: DEFLATE decompression (checked against python's zlib at every level), MD5 (checked against hashlib across 200 input lengths), JSON, and the zip reader. The one place the no-dependencies rule bends is TLS — implementing it is not a reasonable thing to ship, so on Windows this calls **WinHTTP**, 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 tested here. ```sh cargo run -p nox-roblox --example livecheck # resolve, manifest, download, verify, extract ``` ### The bug a synthetic test could not have found The zip extractor refused every real Roblox package. Its zip-slip guard normalised backslashes to forward slashes, then rejected anything starting with a slash as an absolute path — and Roblox's packages contain an entry named exactly `\`, the archive root marker, which normalises to a bare slash. Every hand-written test passed, because the hostile paths were invented rather than observed. The fix separates three cases that were conflated: a path that escapes is refused, a leading slash is stripped (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. ## Published Source, screenshots and the Windows binary: **The .exe has never been run on Windows.** It cross-compiles from Linux with MinGW, links cleanly, is a valid PE32+ GUI binary, and every import resolves against USER32/GDI32/ KERNEL32 — but that is compile-verification, not testing. The graphics and font layers are platform-independent and heavily tested; the win32 layer is the part taken on faith. ## Roadmap 1. **`nox-gfx`** — done for now. 2. **`nox-text`** — done except GPOS kerning, line breaking, and bidi/complex scripts (none of which a launcher needs on day one). 3. **`nox-win`** — done, pending a real run on Windows. Custom title bar still to come, since we want to draw our own chrome. 4. **`nox-ui`** — retained tree, flex-ish layout, hit testing, focus, animation clock, damage tracking; buttons, toggles, lists, text fields, scroll views. 5. **`noxstrap`** — the bootstrapper itself: channel/version resolve, `rbxPkgManifest`, parallel download + extract, mods and FastFlags, protocol registration, launch. Roblox-side details are already known from the previous attempt and should be ported, not rediscovered.