The home page of dark-moon.org has no hero image. Behind every section sits one live WebGL scene: a starfield and the Moon, mapped with NASA data, which the page descends towards as the reader scrolls. This is a retrospective of how that scene was built and rebuilt over October 2026, and of the engineering it took to keep it usable on office laptops with integrated graphics and on ordinary phones. It is written from the repository history, the code as it ships on origin/main at commit 0cd6528 (25 October 2026), and the measurements we actually recorded. It is not a showcase: several of the numbers below are unflattering, and some of the questions it raises are still open.
Scope and evidence
0cd6528, the commit deployed to production on 25 October 2026. Experiments that were abandoned, or that live on other branches, are named as such. Every measurement gives its conditions. We had no physical low-end device: no Intel HD/UHD 620 laptop, no entry-level Android phone. The low-end figures come from one machine (an Intel Core Ultra 7 356H, whose integrated GPU Chrome reports as “Intel(R) Graphics”) with CPU and GPU throttling. That is emulation, and we treat it as such. The site's source repository is private: commit identifiers are given so that each claim can be traced in our own history, not as public links.Update, 25 October 2026 (evening)
0cd6528. A later release the same day (1b4ba45) changed four things it mentions: phones narrower than 768 px now get the “cinematic” view by default (?cine=0 turns it off for the session); the VR headset is built only once the GPU has proven fast, and a slow device reaching that section gets the dashboard directly; in low-GPU mode the glass renderers are stopped for good; and the orbital map boots only near the screen, at a 0.75 pixel ratio and one frame in three. None of the measurements below were taken on 1b4ba45.The constraint: a live 3D backdrop on a page that must load fast
Three requirements pull against each other. The first is a product rule, written in a commit message on 27 August 2026 (b0c1a04): nothing may display “before” the finished site; the very first paint must be the final result. That commit removed the intro loader altogether. On 4 October a loader came back in a different role: it sits in the static HTML and lifts only when the WebGL scene has drawn its first frame (9c2feff), so that what appears after it is the finished scene, never a half-built one. The second is search: search engines are the site's largest source of visitors, so Lighthouse and PageSpeed Insights scores matter. The third is the hardware our audience uses. Darkmoon is a security tool. Its prospects open the site on corporate laptops, often thin machines with integrated Intel graphics, and on phones. The rule we wrote for ourselves on 22 October was blunt: if the scene needs a dedicated graphics card, we got it wrong.
There is a fourth, less obvious, constraint. Lighthouse and PageSpeed Insights run Chrome without a GPU, so WebGL is rasterised on the CPU by SwiftShader. On 4 October, a profile of the mobile audit showed 8,989 ms of main-thread work classed as “Other”, which we traced to WebGL frames drawn in software. A scene that is cheap on a real GPU can sink a score in an auditor that has none.
Art direction as an engineering input
Every visual decision below has a cost on the GPU, and several were changed for that reason. The reasoning is reconstructed from commit messages and our design notes; where we interpret, we say so.
From mountains to the Moon
The October redesign started on plain black: on 1 October the WebGL backdrop was removed altogether (197a065). It came back the next morning (73cae3a) as four parallax mountain ridges in front of a “sunset” halo, recoloured in the official palette (Jordy #87BFFF on the far ridge through Neon #2667FF and Chrysler #3B26CC to a deep indigo in front; the charter values now live in lib/brand-colors.ts). The same day, tone mapping was switched off (79621b2): ACES had shifted every charter colour towards grey, and the brand needs its exact hex values on screen. About ten variants of the ridges followed one another that day (flat gradients, translucent veils, opaque black ridges); the commit log keeps them.
On 22 October the ridges were replaced by a Moon. The first attempt was our own full-screen ray-sphere shader (08a35a0). Our design notes record it as judged slow and not close enough to the reference; it was replaced fifteen minutes later (cd5eb05) by a far more ordinary construction, explicitly modelled on how the site 21 Hours on the Moon builds its own: one SphereGeometry(1, 128, 128), a Phong material with normal and displacement maps, one directional light swung on a container behind the Moon, and a 50° camera. We kept that structure but none of their assets. Our textures were procedural at first; the same afternoon they became NASA's CGI Moon Kit, the LRO colour mosaic and LOLA elevation published by NASA's Scientific Visualization Studio (527ec89), converted by a script in the repository and credited in public/textures/moon/CREDITS.txt.
Our reading of that day, which we did not measure as an A/B: the “clever” solution was the expensive one. A full-screen ray-sphere shader runs for every pixel of the viewport, every frame, including the empty sky. A textured sphere drawn with a standard material shades only the pixels the Moon covers, uses mipmapped textures the GPU is built for, and lets three.js do frustum and state management.
Composition, light and the descent
The hero framing was validated on 22 October and then mirrored on 23 October to put the limb at the bottom right, its curve rising towards the top right, after a reference chosen in design review (e075cd8). The scroll does not trigger cuts: cosmos-moon.ts defines keyframes (camera position, pitch, light angle, detail weight) at given scroll depths in viewport heights, and the page scroll scrubs through them with a damped follower. Each framing leaves a calm black zone for the copy and a band of detailed matter, always back-lit so that the unlit ground falls to the same black as the sky. One black for shadows and sky was an explicit decision (“one black for shadows and sky”, noted in the code on 22 October), and it is also cheap: no fill light, no ambient term to compute on the night side.
Bloom was removed. The UnrealBloomPass blew the centre of the scene into a bright sun on a team member's real GPU while headless captures never showed it; it was isolated live with a ?diag=nobloom switch. The comment left in cosmos-background.tsx records the reason: bloom amplifies the brightest pixels and rasterises differently per GPU and driver. Removing it also removed a full-screen post-processing chain. Film grain is a 256 px tile generated once on a 2D canvas and moved by a CSS animation, under the content: no per-frame GPU pass.
Interpretation. We read the result as a picture of autonomy in motion: a camera that keeps moving without being driven, light that comes from beyond the horizon, a surface revealed progressively. That reading is ours; the commits record framings, light angles and colour values, not a manifesto.
Architecture of the render pipeline
components/blocks/cosmos-mount.tsx, cosmos-background.tsx, cosmos-moon.ts at 0cd6528.Deferred start and a single canvas
The scene is a client component loaded through next/dynamic with server rendering off, so three.js is not in the initial JavaScript bundle. A prefetch hint for its chunk was removed on 4 October because it sat in Lighthouse's simulated LCP graph (cdb389a). Before anything is built, lib/webgl.ts probes for hardware WebGL once per page: a 1×1 context requested with failIfMajorPerformanceCaveat, whose renderer string is then checked for software rasterisers (SwiftShader, llvmpipe), because headless Chrome hands out a SwiftShader context even with that flag. The probe is cached: on Direct3D 12 drivers each test context cost about half a second, and two components were asking (1151033). Without hardware WebGL the page shows a still: a WebP poster exported from the real scene, 1920×1080 and 720×1440 pixels, and no render loop at all (132c010).
With a GPU, one fixed full-viewport canvas renders the whole backdrop. The renderer is created with depth: false (nothing in the scene writes depth, so the multisampled depth buffer was cleared every frame for nothing) and without preserveDrawingBuffer (44e2029). The blog, legal pages, trust and investor pages do not mount the scene at all; unmounting it, rather than hiding it, frees the GPU (ada5168). A lost WebGL context increments a counter that tears down and rebuilds the scene on a fresh canvas.
Passes
- Stars. Up to three
THREE.Pointslayers (4,200 points per layer on desktop, 2,400 on phones, one layer of 1,200 on a software renderer), additive blending, a vertex shader that culls near stars and clamps point size to 2–6 px. - Moon. Its own scene, rendered into a render target sized to the drawing buffer. The material is a
MeshPhongMaterialpatched withonBeforeCompileto add detail octaves whose weight depends on how magnified the texture is at each pixel (fwidthof the texture coordinates), so close-ups stay sharp without minified noise. At the hero distance the detail weight is exactly zero and a uniform branch skips it (992223a). - Sharpening composite. The render target is drawn to the screen through a contrast-adaptive sharpening pass modelled on AMD FidelityFX CAS. It skips empty pixels, whose output is exactly zero (
91f77c7). - Horizon atmosphere. A thin line computed per pixel from the view ray's height above the sphere; fragments below a ten-thousandth of a colour step are skipped (same commit: the atmosphere's GPU time was roughly halved). Its brightness is capped behind light text (see below).
- Mist. One full-screen pass, skipped entirely while its opacity is zero.
- Glass feed. The liquid-glass cards (the MIT-licensed liquidGL library, vendored with our patches) copy the canvas right after its draw, in the same frame, which is what made
preserveDrawingBufferunnecessary.
Three other WebGL contexts exist on the home page: the liquid-glass library's own, the product's orbital infrastructure map (the same three.js engine described in our post on the orbital graph, paused off screen) and, since 25 October, a procedural VR headset that introduces the product dashboard. The headset's module is imported when its section comes within two screens, or earlier, on an idle slot, once the backdrop has proven the GPU fast; it is never built once the page has entered its low-GPU mode (next section).
Textures and GPU memory
The Moon uses a grey 4096×2048 albedo, 4096×2048 normals, a 2048×1024 height map and two tileable 1024×1024 detail maps. Decoding happens off the main thread through createImageBitmap. Where it is worth it, the 8192×4096 albedo and normals replace the 4k maps, and most of the late-October performance work was about doing that without a visible freeze:
- The 8k maps load only on WebGL2, with
MAX_TEXTURE_SIZEat least 8192, when the device reports at least 4 GB of memory (or does not report it), not on iPhone or iPad, and only when a 4k texel would cover more than 1.25 device pixels at the current camera distance. - An 8192×4096 RGBA map is 134 MB of pixels; one
texImage2Dcall was a 200 ms hitch. The image is now fetched, decoded and cut into 32-row strips in a worker, the texture storage is allocated once withtexStorage2Dand a full mip chain, and one strip (about 1 MB, under 3 ms) is uploaded per frame while the page has been still for 0.8 s (ecdf30f,5af11ad). - The albedo is grey, so it lives in a one-channel
R8texture read as.rrr: 180 MB of storage with mips became 45 MB, with a rendered hero 100 % identical to the RGBA8 version (dd88e7d). - The first write into freshly reserved storage makes the driver commit it: for the two 8k maps, about 180 MB, a GPU stall measured at 150–170 ms on the Intel iGPU (the R8 change alone brought the albedo's share from 100–130 ms to 50 ms or less). The storage is now reserved at the very first render, and the loader lifts only once a
fenceSyncplaced after that reservation has signalled, polled each frame with a 1.5 s cap (dd88e7d). The cost moved into the loader, which stays up 0.35–0.45 s longer on eligible devices. - Shaders are compiled one scene per frame (Moon, sharpening, atmosphere, mist) instead of in one block of about a second (
1151033). On ANGLE's Direct3D 12 backend we found that asynchronous compilation did not help the Moon: the completion poll blocked as long as the compile.
One frame: scroll, state, cadence, draw
lib/scroll-state.ts, lib/frame-clock.ts, components/blocks/cosmos-background.tsx at 0cd6528.The scroll position is never read inside the loop. lib/scroll-state.ts keeps it from one passive scroll listener, the viewport size from one resize listener and the document height from a ResizeObserver. Reading window.scrollY in a frame callback forces style and layout when another script has just written to the DOM; on the phone home page that was about 730 ms of forced layout in a seven-second scroll, all attributed to the backdrop loop (ce17438, 5bbe0d1). The page's other scroll-driven effects were moved onto one shared scheduler that runs every read, then every write (lib/frame-rw.ts).
The descent follows the scroll through a damped value (sv += (target − sv) × min(1, dt / 140), with dt in milliseconds), so a flick of the wheel becomes a camera move rather than a jump. The shared frame clock (lib/frame-clock.ts, 11bb5fe) changes nothing on a 60 Hz screen. On a 120–165 Hz screen whose GPU misses more than 15 % of frames over two seconds, the backdrop and the orbital map draw every other frame, a steady 82 Hz on a 164 Hz panel instead of an irregular 60–164, and the decision sticks for the visit.
// lib/frame-clock.ts (excerpt, 0cd6528)
const s = [...iv].sort((a, b) => a - b), base = s[Math.floor(s.length * 0.2)]; // the refresh interval
if (base < 11) {
const missed = iv.filter((x) => x > base * 1.5).length / iv.length;
if (missed > 0.15) div = 2;
}Adaptive quality for integrated GPUs and modest phones
components/blocks/cosmos-background.tsx, cosmos-mount.tsx, vr-showcase/vr-showcase.tsx at 0cd6528.Pixel ratio first
Fragment cost scales with the square of the device pixel ratio, so it is the first lever. The backdrop never renders above 1.5 on desktop or 1.3 on phones, whatever the screen reports. A device that reports hardwareConcurrency of 4 or less, deviceMemory of 4 GB or less, or the Save-Data preference starts at 1. Then the loop measures itself: more than 12 frames over 24 ms step the ratio down by 0.25, to 0.5 at worst; more than 240 frames under 15 ms step it back up by 0.1, never above where it started. A fast machine never moves. These signals are coarse by design: deviceMemory is rounded to a power of two, exposed only in secure contexts and not at all by Safari, which is exactly why iOS needed its own rule (see the bugs below).
Two verdicts
The loop also issues two page-wide verdicts. dm-gpu-ok fires after 90 drawn frames averaging under 20 ms; only then is the heavier extra, the VR headset, prepared ahead of time (otherwise it waits until its section is near). dm-lowgpu fires when the average over a 2.5-second window, starting 1.5 s after the scene is revealed (so that load hitches never decide it), is under 25 fps, or when the scene stays slow at the lowest ratio. A 30 fps cap from a laptop's energy saver is deliberately above that threshold: it is not a weak GPU. In low-GPU mode the backdrop drops to a pixel ratio of 0.5 and redraws at most once a second, the VR headset is not built (or is disposed), and the liquid-glass cards fall back to a plain frosted CSS tint. The page itself keeps scrolling at whatever rate the compositor manages.
The headset has its own lite tier for the same weak-device signals: no antialiasing,MeshStandardMaterial instead of MeshPhysicalMaterial (no clearcoat), no grain pass and a pixel ratio capped at 0.9 instead of 1.25. In every tier its programs are compiled ahead with three.js'scompileAsync, and its own frame-time control steps the ratio down by 0.15 after a run of frames over 22 ms, to 0.6 at worst; a GPU still slow there switches the page to low-GPU mode.
Core Web Vitals with a WebGL backdrop
Because auditors have no GPU, Lighthouse measures the still-poster path, not the live scene. That is not a trick: it is exactly what a GPU-less visitor gets. The measurement taken on 4 October, before and after the software-WebGL commits of that day, is the clearest effect in this whole history.
| Lighthouse 13.4, home page | Before (9c2feff) | After (same day) |
|---|---|---|
| Mobile performance score | 65 | 98 |
| Mobile LCP | 3,265 ms | 2,355 ms |
| Mobile TBT | 1,382 ms | 38 ms |
| Mobile main thread, “Other” | 8,989 ms | 1,103 ms |
| Desktop performance score | 92 | 100 |
| Desktop TBT | 199 ms | 0 ms |
Conditions: local production export, Lighthouse 13.4.0 with simulated throttling, headless Chromium without GPU, one run each, 22 minutes apart. The scene at the time was the mountain ridges, not the Moon. The “after” build was recorded under a commit identifier that no longer resolves (our local history was rewritten later that day to remove stray files), so we can name the commits made between the two runs (among them 4db0137, d8ff66a, cdb389a, 132c010, and 9f4c525 for the orbital map) but not a single SHA for it. With one run per side, the size of the effect is what makes it credible, not its precision.
The intro loader is the page's first large paint, so its word mark is the LCP element. When the loader was redesigned on 25 October it briefly faded in from zero opacity, which moved the LCP to the hero seconds later; the release removed the fade (0cd6528). In the quality gate that preceded that release, the mobile Lighthouse runs (simulated throttling, no GPU, two runs per build) gave an LCP of 3,761 and 3,462 ms for the previous production build (fbc57bd) and 3,460 and 3,461 ms for the release. TBT went the other way, from 121–125 ms to 157–166 ms, and CLS stayed at 0.0001 or less. In other words: the release did not regress LCP, it did not improve it either, and it added about 35 ms of simulated blocking time. web.dev places an LCP between 2.5 and 4 s in its “needs improvement” band, a threshold meant for the 75th percentile of real page loads; a lab value of about 3.5 s is a warning, not a verdict. The 4 October and 25 October series are not directly comparable (different scene, server and harness), but the gap between a mobile LCP of 2.4 s then and about 3.5 s now is large enough to note, and we have not isolated its cause. That is an open issue, not a result. We have no field data (CrUX) for this page in this article.
Layout shift stays near zero because the canvas is fixed, full-viewport and starts transparent; it fades in over the loader's black. For interaction latency, our internal bench report of 23 October measured clicks on the orbital map, the menu and keyboard input at 48 ms or less on the Intel iGPU, which is a lab measurement, not INP from real users.
Reduced motion and legibility
prefers-reduced-motion: reduce is honoured in two tiers. On a phone the backdrop is replaced by the still poster: no WebGL context, no loop. On a larger screen the scene still renders, because it carries the page's colours, but it holds: the star angle and clock are frozen at zero, the Moon takes its keyframe states at once instead of easing, its slow spin stops, the mist does not drift, and the VR headset stage is not built (the dashboard is shown as it is). The 8k upgrade is also skipped under reduced motion, which keeps the page from doing memory work for an effect the reader asked to minimise.
Legibility had its own engineering problem. The Moon's horizon line is the brightest thing in the scene, and white text crosses it as the page scrolls. Commit 5ffad15 (23 October) builds, in idle slices of at most 4 ms, a low-resolution document-space map of where light text sits (8 px cells, feathered), and the atmosphere shader holds the line under the luminance each piece of text needs for a WCAG contrast of 7:1 (4.5:1 for large text), or the best it can reach above a floor for grey text. Measured on rendered pixels across 72 scroll positions of the desktop home page at 1440×900, lines of text failing the AA level because of the horizon went from 148 to 5 (two of which did not reproduce in isolated reruns); at DPR 1.5 from 55 to 0; on a 390×844 phone at DPR 3 from 11 to 0. Away from text the line is unchanged. The canvas itself is aria-hidden: it carries no information that is not in the text.
GPU-specific bugs and how we reproduced them
The hardest bugs of this project only appeared on hardware we did not have in front of us. Each was fixed only after it was reproduced.
- Stars collapsed on Apple GPUs (
27ca6be, 4 October). The star rotation was computed in the vertex shader astime × 0.015withtimein epoch seconds, about 2.7×10⁷ radians. Desktop GPUs reduced the argument well enough; on an iPhone, a diagnostic page that ran the shader and read the result back measuredcos/sinat 0.001 instead of −0.166/−0.986, so the rotation matrix was nearly zero and every star landed on one point (10 lit pixels instead of 509). The angle is now computed on the CPU in double precision and wrapped to 0–2π. Our first hypothesis (the iPhone was getting the still poster) was wrong, and the probe change made on that hypothesis (d007115) was reverted. - Epoch time in float32, again. The first Moon's mist froze into rectangles for the same reason; its shader clock is now wrapped (
performance.now() % 3600, in08a35a0). - A vertical light streak on an HP EliteBook's integrated GPU. Stars very close to the camera had a tiny clip-space
wand a huge point size that this GPU smeared across a column. The vertex shader now hard-culls anything nearer than 560 units and clamps point size to 6 px. The comments incosmos-background.tsxrecord the two rounds this took. - A glow that jumped on a 125 % Windows display (
b0c1a04, August). A resolution uniform was passed in CSS pixels whilegl_FragCoordis in framebuffer pixels, so at DPR 1.25 the glow rendered at 40 %/55 % of the screen instead of the centre. - An iPhone 14 that reloaded three times, then showed “Application error” (
6aed015, 23 October). Safari exposes nonavigator.deviceMemory, so our “at least 4 GB” gate let the 8k maps through: about 225 MB of GPU storage plus two 128 MB decoded bitmaps on top of everything else, past what iOS allows the tab. iPhone and iPad now keep the 4k maps. This crash was reported from a team member's iPhone 14 (an iPhone 16 Pro Max was fine); we did not measure it ourselves. - A 115 ms shader compile at the first pause in the scroll (
fbc57bd, 24 October). The glass library compiled its lens program synchronously the first time the reader stopped scrolling. WithKHR_parallel_shader_compile, the patched library now pollsCOMPLETION_STATUS_KHReach frame and hands over the program when it is linked; without the extension it keeps the old path. The same commit moved the glass canvas into a reserved slot because inserting it among the body's children shifted:nth-childmatches and restyled the page (25–27 ms, now 0.1 ms).
Measuring without low-end hardware
Our bench drives a native Windows Chrome over the DevTools protocol from WSL. On 25 October Chrome was started with --force_low_power_gpu --use-angle=d3d11 so that it rendered on the Intel integrated GPU, which every result line confirms through WEBGL_debug_renderer_info. The page was opened at 1536×864 CSS pixels with a device scale factor of 1.25. After load, six seconds of idle and one wheel step, a script scrolled programmatically, one scrollTo per animation frame, through fixed segments (the hero, the VR stage, the dive into the dashboard, then the whole page at 700 ms per viewport height), recording the interval between animation frames.
Two brakes approximate weaker machines. The CPU is slowed with the DevTools Emulation.setCPUThrottlingRatecommand (×4 or ×6). The GPU cannot be slowed that way, so a second tab on the same GPU runs 24 full-screen passes of an arithmetic-heavy shader every frame. Calibrated with a fixed GPU workload in a third tab, that stress left 40 passes per second out of about 360 alone, roughly 1/9 of the GPU's throughput. We call “low-end emulation” the combination of that stress and CPU ×4. It is not a Pentium, an Iris Xe or an entry-level Android phone: the compositor is not slowed, GPU memory and bandwidth are not reduced, and sharing a GPU with another tab is not the same as having a smaller one.
0cd6528; local builds of fbc57bd and 0cd6528 served without caching), with the protocol described above. Measured values; one run per dot.| Run (25 Oct 2026) | Frames | Mean fps | p50 | p95 | p99 |
|---|---|---|---|---|---|
| dark-moon.org, no throttling, whole page | 1332 | 60.0 | 16.7 ms | 16.9 ms | 17.4 ms |
| dark-moon.org, no throttling, VR stage | 539 | 59.9 | 16.7 ms | 16.9 ms | 17.4 ms |
| dark-moon.org, CPU ×6, whole page | 247 | 11.1 | 66.8 ms | 216.5 ms | 266.5 ms |
| dark-moon.org, low-end emulation, whole page | 3 | 0.0 | 316.6 ms | 156 s | 156 s |
| empty page, low-end emulation | 50 | 10.2 | 100 ms | 116.7 ms | 200 ms |
What we take from these runs, and what we do not:
- This recent, unthrottled integrated GPU holds 60 fps, the panel's refresh rate during the run (median interval 16.7 ms), through the entire page, the VR stage included, with a p99 of 17.4 ms: one frame over 25 ms out of 1,332. One run; it says nothing about an older iGPU.
- With the CPU slowed six times, results are not stable enough to compare builds. Two runs of the same release build gave 48.8 and 12.0 fps; the previous build gave 22.1 and 22.5. We do not claim the release is faster or slower under CPU pressure.
- The low-end emulation cannot discriminate versions in steady state, because the emulation itself is the bottleneck: an empty black page reaches 10.2 fps, and both local builds scroll at 9.0–9.5. What it does expose is the start of a visit. In a per-second trace of the previous production build (
fbc57bd) under the same emulation, long tasks of up to 4.8 s recur around the moment the scene is revealed, about 20 s after navigation. On production, one run of the scripted whole-page scroll, which starts well after load, got three frames and then a stall of about 156 s. We have not reproduced that stall: the local build of the same commit scrolled at 9.0 fps under the same emulation, so we report it as an unexplained single observation, not as the behaviour of the release. - The low-GPU verdict is slow to arrive on such a device. On production under the emulation,
dm-lowgpufired about 34 s after navigation started (one run, polled once a second), after which the idle page ran 0.3 animation frames per second: the low-GPU mode lightened the scene but did not make this emulated machine fluid. On the same machine without throttling it never fired, and the idle page ran at 60.3 fps.
Two days earlier, our bench report of 23 October had compared the fluidity work of 22–23 October against its starting point on the same machine, with the laptop panel at 164 Hz (1707×1067, DPR 1.5, mean of two alternated runs). Frames over 25 ms during ten successive scrolls went from 86 to 3.5 and the p99 from 69.8 to 21.2 ms; in the idle hero, from 27.5 to 3 frames, with backdrop GPU time per frame going from 6.8 to 2.1 ms. The same report is explicit that on a 60 Hz panel both versions already held 60 fps with a p99 around 17 ms: the gains were about high refresh rates and stalls, not about the average.
History and trade-offs: what we kept, what we dropped
git log origin/main of the site repository, commit dates.Decisions that were tried and reversed, with their reasons as recorded:
- Bloom: removed for GPU-dependent results (see above). A nebula plane and a large atmosphere sphere also remain in the code behind diagnostic flags, off by default.
- A depth-precision fix for the ridges (near plane 0.1 → 2, 1,200 segments;
78c28ab) fixed z-fighting artefacts on the crests but was judged a visual regression and reverted (bd60b75); the kept fix was a polygon offset on the reflection bands only (4174139). The cheaper fix was also the one that preserved the look. - The full-screen ray-traced Moon (
08a35a0): replaced the same morning by a textured sphere, for speed and fidelity. - Glass letters for the hero word mark (
dba8816): reverted (8e05e3e). - Antialiasing of the orbital map: SMAA and FXAA were compared against an 8× MSAA reference in our 23 October report; both had larger edge error than 4× MSAA, which was kept.
- Experiments that never reached production: procedural cities and an eroded terrain for the infrastructure view, and several art directions, all kept on archive branches. A phone “cinematic” mode existed in the code at
0cd6528behind a?cine=1flag; the later release1b4ba45made it the default on phones (see the update note at the top).
The trade-off we accepted most consciously is the loader. Reserving the 8k storage under it, and lifting it only when the GPU has committed that memory, makes the loader 0.35–0.45 s longer on eligible devices, in exchange for no 100–280 ms freezes during the visit. That choice is reversible in one commit (dd88e7d).
Limits and what we did not measure
- No physical low-end device: no Intel HD/UHD 620 or Iris Xe laptop, no entry-level Android phone, no measurement on an iPhone by us. All low-end figures are emulation on one recent laptop.
- The low-end emulation saturates at about 10 fps on an empty page, so it cannot tell builds apart in steady state; CPU ×6 runs vary by a factor of four between repeats; the 156 s production stall and the 34 s low-GPU verdict are single runs.
- No field data (Chrome UX Report or real-user monitoring) is used here; Lighthouse runs are lab runs without a GPU, two per build.
- Battery saver modes, thermal throttling over long sessions and Firefox were not measured on the final build. The 164 Hz figures predate the 25 October release.
- GPU memory figures are estimates from texture sizes (our 23 October report estimated about 396 MB of texture memory at rest on the laptop bench); we did not read driver counters.
What would settle the open questions: the same scripted scroll on a real Intel UHD 620 laptop and a real entry-level Android phone, five runs per build alternated with the previous production build; a trace of the first twenty seconds under the low-end emulation to see why the low-GPU verdict takes 34 s; and field LCP from real users.
Further resources
The scene you are reading about runs on the dark-moon.org home page, and the product's three.js orbital map is described in the orbital infrastructure graph post. For research on immersive attack graphs, see our WebXR attack-graph visualisation study.
- ASCIT31/Darkmoon-Benchmarks: our public repository of LLM penetration-testing benchmarks (autonomous agents and models against vulnerable labs). It does not measure WebGL, rendering or web performance, and nothing in it validates the figures of this article.
- ASCIT31/Dark-Moon: the open-source Darkmoon edition (GPL-3.0), the command-line interface and its agents.
References
- NASA Scientific Visualization Studio, CGI Moon Kit (LRO LROC colour mosaic, LOLA elevation), ID 4720. Visualizer Ernie Wright, scientist Noah Petro. svs.gsfc.nasa.gov/4720
- MDN Web Docs, HTMLCanvasElement.getContext(): WebGL context attributes (failIfMajorPerformanceCaveat, powerPreference, preserveDrawingBuffer). developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext
- MDN Web Docs, WEBGL_debug_renderer_info extension. developer.mozilla.org/en-US/docs/Web/API/WEBGL_debug_renderer_info
- MDN Web Docs, Window.devicePixelRatio. developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio
- MDN Web Docs, Navigator.deviceMemory. developer.mozilla.org/en-US/docs/Web/API/Navigator/deviceMemory
- MDN Web Docs, WebGL2RenderingContext.fenceSync(). developer.mozilla.org/en-US/docs/Web/API/WebGL2RenderingContext/fenceSync
- Khronos Group, WebGL extension registry: KHR_parallel_shader_compile. registry.khronos.org/webgl/extensions/KHR_parallel_shader_compile/
- AMD GPUOpen, FidelityFX Contrast Adaptive Sharpening (CAS). gpuopen.com/fidelityfx-cas/
- MDN Web Docs, prefers-reduced-motion media feature. developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion
- web.dev, Largest Contentful Paint (LCP): definition and thresholds. web.dev/articles/lcp
- 21 Hours on the Moon, the site whose Moon construction we followed (structure only; no asset reused). 21hrs.space/
See the proof, not just the write-up: the Pro remediation benchmark (fixes retested against the exploit) · how Darkmoon compares to other AI pentest tools.
← All articles