CHAOS /DOCS/DESIGN-MAP

DESIGN MAP

Design map

The why and the status. this is the file I keep next to the code, shown here as it is, nothing rewritten. the how is in the flow doc and the code.

A design map for the engine, I guess I have to make this (if the project grew and I can't maintian it).

the how, flow by flow, is in "Data and logic flow" next to this file. this one is the why and the status.

Refernces are from the web version CHOAS at https://chaos.tetstack.dev/ and source code at https://github.com/PicasoTheDeal/CHAOS (the reference files were removed from /CHAOS on 2026-11-05, that folder is the registry site now. the reference lives on github and in the port)

Language: C++

Reasons I didn't went with Rust: I don't want to rewrite idioms that are already there.

Why not zig... well best of two but too young for this project. And I barely know it.

01Implemtnatations:

The rng(splitmx64 -> xoshiro256**, no idea about the name but anyways because we want 256 bit state, check one part 3)

Seed encoding generator (new scheme, not the current based seed + visitor count (obviosly))

Every shape generator (41 of them, 40 real ones + compound that fuses 2 to 4 of the others. the "~100" I wrote before was a guess, I counted)

Every shader (ported, not copied, and why the hell you ask cuz glsl version changes)

Post processing stack

Palette / mood / mutation systems

New stuffs: rarity, modes, certs, weekly allowance, UI, accounts, might be more but yeah you will find the new things once down the list.

02core stack

Language: C++ (same reason as above and also closest to the reference js in addition to mature libs, and no runtime gc)

Build: CMake 3.20+ (I have to use it for c++ graphics)

Window + Context: GLFW 3.4 (Does one thing and has x11 and wayland escape hatches)

Graphics API: OpenGL 4.3 core (shader port is nearly mechanical from three.js)

Gl loader: glad2 (generated per project and no runtime cost)

Math: GLM (headers only, standard for c++ graphics)

UI: Dear ImGui (docked) (embedded in the render loop. no ipc, no second toolkit)

JSON: nlohmann/json (header only and no perf)

Logging: spdlog (small, fast, file and console sinks)

Tests: Catch2 (header only, tags, good matchers)

Image io: stb_image, stb_image_write (png for preview and wallet export)

Crypto sign: libsodium (ed25519) (easy, safe and well audited(unlike my code, lol))

Crypto hash: blake3 (vendored c) (256 bit and one small .c file)

Wallet crypto: libsecp256k1 (same lib bitcoin core uses, only wallet lib that matters)

Keccak: openssl libcrypto (ethereum address and sig hash)

Symmetric enc: openssl libcrypto (aes-256-gcm) (vault encryption)

Password hash: libargon2 (derive encryption key from google id)

Http: libcurl (google oauth, drive, ipfs, rpc, all in one)

Keyring: libsecret (storage google refresh token in desktop keyring)

Fonts: FreeType + stb_truetype (UI rendering and embeded fonts)

Packaging: appimage, tar.gz, .deb,aur, flatpak (cover most distro's and windows yeah nah not gonna do it, until my next adhd hits or smthing)

"oh no but why not this and this " well first because I said so and second,"

not choosen cuz:

vulkan: forces full pipline rewrite and overkill for one scene

sdl3: more features than we need, glfw is easier to patch for layer shell (I don't want to panic every time a new CVE comes out)

qt/gtk: adds 100mb+ dependencies and forces two toolkits with imgui

electron: the whole point of the recode is escape electron

03seed engine

state space

entropy source os.urandom(32) at generation time

state space 2^256, about 1.15 * 10^77 possible dimensions (1.159 undecillion where the web is only about 18 quintillion). all 32 bytes drive the picture. (I shortened it to 16 for a day so the seed was pasteable, then put it back: the identity and the picture should be the same number, and that number is this one)

hash blake3 of "CHAOS-SEED-v2" || ticket_id(16) || client_random(32) || instance_id(16) (the ticket is what the server gives you for one roll, see 11. no ticket = preview roll)

stream xoshiro256** seeded from the 32 byte hash

sub streams independent xoshiro instances, one per system: rarity, shape, mutation, skin, palette, lights, motion, satellites, twin, planes, debris, particles, rings, background, mood, mode, runtime. each is blake3(seed_full || label) so adding a system later does not disturb existing seeds (done, v0.2)

shareable forms

seed_full: 32 bytes, 64 hex. the registry identity, what the .chaos file and the website key on. collision proof, nobody types this.

base58: the whole 32 bytes, 43 or 44 chars, bitcoin alphabet. the display in the corner and what goes in seed_display in the file. chaos --seed <it> replays. (done, v0.4)

seed_short: first 8 bytes as 16 hex. a display prefix and the vault file name, like a git short hash. does not replay anything on its own

words: bip39, 24 words (256 bits + 8 bit sha256 checksum, the standard). chaos --seed "word word ..." replays. checked against the trezor vectors. for humans saying it out loud (done, v0.4)

.chaos: the 512 byte file. not a seed, a claim ticket. see 11.

url: chaos://<base58> later maybe, so a link opens the app. (not planned, if adhd allows)

rng api (done)

uniform() [0,1), range(lo,hi), rangeInt(lo,hi) inclusive like the js, chance(p), jump() 2^128 steps

xoshiro all-zero state is the one state it cannot leave, so a zero seed gets kicked to a constant. (yes someone will try)

04shapes (done, v0.2)

41 generators, ported from the web buildShape. 0..39 real, 40 compound (2 to 4 others fused, each thrown into its own orientation and scale)

torus knot, supershape, wobble torus, icosahedron, octahedron, dodecahedron, box, sphere, cylinder, cone, ring, plane, lathe, spiral shell, saddle sheet, corkscrew, fluted torus, ribbed cylinder, rippled sphere, sinc ripple, torus arc, spline tube, lathe pod, lissajous knot, harmonic blob, klein bottle, mobius band, berry cluster, fractal flake, gyroid sheet, conch, superellipsoid, braid, enneper, dini, roman surface, voxel colony, clelia spiral, urchin, rose lathe, compound

the three.js geometry zoo had to be rewritten (there is no three.js in c++, obviously): box, plane, cylinder/cone, ring, torus, torus knot, lathe, tube (catmull-rom centripetal + parallel transport frames), polyhedron with subdivision (icosa/octa/tetra/dodeca), parametric. plus merge, transform, weld (mergeVertices). all in client/src/shape/primitives.cpp, about 350 lines

weld before mutate: compound and merged shapes come back with seams and the vertex shader pushes along the normal, so without the weld every face tears apart. same trick as the web

mutation lineage: gen 0 is twist, maybe warp (60%), ripple. then 1 to 4 more gens picked from twist, warp, ripple, noise, swirl, bulge/pinch, shear, fold. each gen works on the output of the last and is a bit stronger (0.6 + 0.3 * gen). the app prints the lineage like the web console did

family ids for the .chaos file (0..15): the 41 generators bucketed into 16 families for rarity and value. knots, superformula, tori, polyhedra, primitives, lathes, shells, sheets, ribbed, tubes, minimal, clusters, colonies, compound, 14 reserved for mythic, 15 reserved. table in client/src/seed/rarity.cpp, mirrored as a comment in shared/chaos_format.h (done, v0.4). the generator id itself is not in the file, the seed replays it

not ported: nothing. everything in buildShape is in. the vertex counts are a bit different because the parametric grids are mine not three's, nobody will notice

05skin + shaders (done, v0.2)

berry skin: ported 1:1 from BERRY_VERT / BERRY_FRAG. glsl 430 core, in/out instead of varying, fragColor instead of gl_FragColor, uProj/uModelView instead of the three.js built-ins. the common block (hash, noise, fbm, voronoi) is a #include <berry_common.glsl> my shader loader expands

lights are fed in model space like the web (feedBerry: cam and 3 light dirs multiplied by the inverse model matrix per mesh)

hyperspace backdrop: same shader, drawn on the inside of a radius 90 sphere parked on the camera, no culling, depth off. (first time it rendered black cuz I never cleared depth. don't ask)

satellites: 2 to 14, 55% wear the berry skin with their own smaller params, the rest use the glitch shader (5 modes, same as web)

fold planes, debris: MeshPhongMaterial does not exist here so phong.frag is a stand in: lambert / pi, blinn-phong normalised, 3 directional + ambient + emissive + linear fog 60..240. close enough, the duotone eats the hue anyway

rings: MeshBasicMaterial, half of them wireframe (glPolygonMode line)

particles: 400 to 900 points, additive, size attenuated, drifting and slowly rotating like the web

wire ghost: the main shape again at 1.01 scale, white lines, alpha 0.04..0.16

shadows: not ported. the web had a 2048 shadow map on the key light. after the duotone you can barely tell. maybe v1.3 polish, maybe never

msaa: 4x on the scene target, resolved before post. the web had antialias:true

06post stack (done, v0.2)

render (hdr, rgba16f) -> bloom -> duotone -> film -> afterimage -> screen. same order as the web composer

bloom: UnrealBloomPass rebuilt. luminosity high pass (threshold 0.65) at half res, 5 mips of separable gaussian (radii 3 5 7 9 11), composite with the 1.0 .8 .6 .4 .2 weights and the radius lerp. strength is driven per frame: 0.18 + pulse stuff, mood scaled, + kick * 0.6

duotone: the whole frame is luminance mapped onto 4 stops built from A and B only (A*0.05, A, B, B lifted to white 35%). aces fit, 1/1.8 gamma, contrast, split point, dither. the kick flashes B over the frame when a palette lands. verbatim port

film: three's FilmShader r163, grayscale off, intensity from mood (0.03 calm .. 0.14 wild)

afterimage: ping pong, damp from mood (0.80 .. 0.94)

everything is a fullscreen triangle from gl_VertexID, no quad buffers

shaders live in client/shaders/, loaded at runtime from next to the exe or the source tree. edit, relaunch. hot reload is a v0.6 nicety

07palette / mood / mutation (done, v0.2)

palette: exactly two colours, A deep, B bright. makePair ported with its 5 modes (ink+neon, dusk, near-mono, bleached, acid). jumps on a seeded schedule from the runtime stream: calm never hard cuts, wild does 30% of the time, calm jumps half as often. each jump kicks bloom and scale and nudges mutation

mood: one scalar 0 calm .. 1 wild, pow(u, 1.6) so most seeds land calm/soft. every eye strain effect reads ml(calm, wild). M key cycles CALM SOFT LIVELY WILD, eased over a second. --mood 0..1 pins it

mutation clock: dv ~ rate * (0.25 + v), saturates at 1.5. drives uMutate in the berry vert: displacement deepens, a finer second layer grows, drupelets divide finer. calm slows it, wild speeds it

08rarity (done, v0.4)

the web had none. this is the new thing and the whole reason for the website. rules:

tier is rolled on its own sub stream ("rarity"), fixed odds: common 60%, uncommon 25%, rare 10%, epic 4%, legendary 0.9%, mythic 0.1%. deterministic from seed_short, the server recomputes it, the client cannot lie

tier feeds the generation, not the other way around. I am not scoring "how pretty is this" (impossible, and people would game it). the tier unlocks stuff:

common: what you get now

uncommon: +1 extra mutation gen, satellites lean berry (75%)

rare: a second berry orbiting (its own skin), palette gets a third stop

epic: compound forced (2 to 4 shapes), 2 extra mutation gens, longer afterimage

legendary: native mode bleed (see 9), a unique palette family (acid only), rings doubled

mythic: all of the above + a shape family that only mythic can roll (not written yet, something nobody has seen) + the seed display turns gold lol

rarity_score (float, in the db) is tier + small deterministic bonuses: lineage length, satellite count, mood extremes. only for sorting the market, never for the tier itself

why fixed odds and not a lottery on visuals: the server can enforce it from the seed alone, and a mythic is a mythic no matter whose screen it's on

how it landed: "rarity" sub stream rolled first, the tier feeds everything after it. the unlocks above are all in except the mythic-only family (reserved family 14, still the open question). the second berry is a different generator at 0.35 scale with its own skin on the "twin" stream, orbiting wide. the third stop makes the duotone a five stop ramp (A*0.05, A, C, B, B lifted). the 16 family buckets are in client/src/seed/rarity.cpp and as a comment table in shared/chaos_format.h, the server mirrors that table. the app prints tier, family and score at launch

09modes (done, v0.3)

zen, calm, chaos, bleed. 0..3 in the .chaos file. every seed has a native mode (rolled, weights 20 / 35 / 35 / 10). the user can override the display mode locally, the native one is what gets valued

zen: mood pinned 0, no palette jumps ever, mutation clock off, particles only. for people who actually work on their desktop

calm: mood capped at 0.35, no hard cuts

chaos: the default. mood as rolled, full range

bleed: mood pinned wild, hard cuts 60%, afterimage 0.96, plus a smear pass (chromatic split along the kick) that only exists in bleed. this is the one that hurts

the mode is a post/mood preset, not a different scene. same seed, same shape, same everything, just how much it moves

how it landed: native mode on its own "mode" sub stream. --mode zen|calm|chaos|bleed overrides the display, N cycles, 1-4 set it. the app prints "BLEED native, showing ZEN" so you always know what the seed really is. zen also hides the swarm (satellites, planes, debris, rings) and leaves the berry, the backdrop and the particles. a manual M press still wins over the preset, same as the web

the smear: chromatic split along a slowly turning direction (never fully closed, bleed always bleeds a bit, 0.0015 at rest) that tears open with the kick (up to 0.0135), plus 7 taps dragged along the same line. sits between the duotone and the film grain. only built when the mode asks for it, zero cost otherwise

10weekly allowance (v0.8, server side)

claimable rolls are tickets. the server gives you 1 per week (2026-11-06, was 1 per day banked to 14: ROLL_PERIOD_DAYS / ROLLS_PER_PERIOD / BANK_MAX in config), and only to a signed-in account; the app will not generate anything, previews included, without a session. a preview roll is free and unlimited but can never be claimed (no ticket in the seed)

allowance lives on the website account, not in the app. the app just asks. offline = preview only

numbers are tunable, these are v1 floors

11ownership layer (.chaos file, website registry, value)

the seed alone is not the asset. the asset is a .chaos file (seed + certificate) that only counts once the website accepts it.

source of truth: the website. the app renders anything offline, but owning, valuing and transferring needs a google login on the site. blockchain is pushed to v2.0 as an optional mirror, not the registry.

identity: google account on the website. the per-install key is a subkey, not the identity.

.chaos file (512 bytes, fixed layout, binary)

magic "CHAS", version, flags

seed_full (32 byte blake3), seed_short (first 16, the shareable form), family, tier, mode, mutation_seed

issued_at (unix ms), instance_id (16 random bytes, generated once per install)

cert_hmac (hmac-sha256, key k1, over bytes 0..87), payload_hmac (hmac-sha256, key k2, over bytes 0..119)

nonce (16 bytes, the "current valid copy" marker, rotated on every transfer)

seed_display (base58 of seed_short) + display_len

240 bytes reserved padding, then preview_hash (blake3 of the 1920x1080 preview png)

why fixed binary: trivially parseable, opaque to a text editor, reserved room so version stays at 1 for a while

preview goes as a hash only, not embedded (keeps the file tiny; the site stores the png)

keys k1 / k2: derived at runtime from constexpr fragments, not literal strings. this is obfuscation, not cryptography. anyone who disassembles the client gets them. accepted: "until someone figures it out". the website is what gives value, so a forged file alone is worth nothing.

value model

rarity = property of the seed. deterministic, computed on both sides, never changes. common / uncommon / rare / epic / legendary / mythic.

value = property of the market. set by the website at claim time, starts at a per-tier floor (v1: 0.10 / 0.50 / 5 / 25 / 200 / 2000), then moves.

value = floor(tier) * family_mult * mode_mult * age_bonus * provenance_bonus * supply_discount. multipliers live in the site db, the app never knows them.

claim flow

app rolls seed, writes .chaos to the vault folder

user logs in on site, uploads the file

site verifies magic / version / both hmacs, rejects if seed_full already claimed or instance_id blacklisted, computes value, inserts seed_full -> owner, returns token_id + value

app marks the vault entry claimed. from here the local file is a receipt.

transfer / gift

owner lists it on the site (price or 0 for gift). other user acquires.

site swaps owner_id, issues a NEW .chaos with a fresh nonce, revokes the old one.

old file uploaded again -> rejected (stale nonce). copied file uploaded by anyone else -> rejected (already claimed).

this is the "even if they share the files they have to confirm on the website" rule. the file is a bearer ticket the site can revoke. only one claim is ever honored, first upload wins by server timestamp. admin can reassign on bugs, no appeals.

sync: app sends claimed seed_fulls to /api/vault, site returns state per seed, app updates local vault. runs on start, on login, and on a manual button. a seed transferred away on another device shows "transferred out" locally.

website (decided 2026-11-05, built v0.8)

cloudflare. one sveltekit app, CHAOS/ (the CHAOS folder IS the site now: the old web toy's files are gone, its git history stays, so the site pushes to the same repo), at chaos-eng.tetstack.dev, adapter-cloudflare, pages + workers, D1 (sqlite) for the books, the signing key as a worker secret (never the dev key), google tokens checked with jose against google's jwks (no node-only libs, no Buffer, runs on workers). wrangler.toml in CHAOS/. npm run deploy. the api is +server.ts routes under /api, the pages are svelte 5, the registry logic sits in src/lib/server which sveltekit refuses to bundle for the browser. that is the security argument: one framework, one place the private code can live, no second backend to keep honest. (I had started a separate node/hono service, got stopped, fair)

D1: users, tickets, seeds, files (the current signed bytes, so a buyer gets a real file), transfers (CHAOS/migrations/0001_init.sql, npm run db:migrate). no D1 binding = memory store, dev only, nothing persists

auth: google identity services button on /login, id token to POST /api/auth/google, httpOnly cookie session (jwt, 30 days). the app will use the same jwt as a bearer token (v1.0). ALLOW_FAKE_LOGIN=1 for dev

endpoints: POST /api/auth/google, GET /api/me, POST /api/ticket {commit}, POST /api/check (public: is this file real, what does the registry know), POST /api/claim, POST /api/list, DELETE /api/revoke/:seed, POST /api/acquire, GET /api/vault, POST /api/sync, GET /api/market, GET /api/seed/:seed, GET /api/pubkey. files travel as base64 in json, 512 bytes is nothing

pages (v0.9): / (the pitch, how a roll cannot be rigged in four cards, the odds), /check (drop any .chaos: valid or not, tier, value, is this copy current. no sign in. this is what the user asked the web for), /login (google button, or the dev login when no client id), /vault (allowance, the four-step ticket dance, cards per seed with download / copy / list / unlist), /claim (drop the rolled file, get it back signed), /market (cards, tier filter, acquire), /seed/[hash], an error page. one layout built from the reference page's own stylesheet: --bg #0b0c0b, --silver #ecebe2, hairline --line, flat hairline .key buttons (fill silver on hover), the .chip lcd counters with dseg14, unbounded 800 for the big words, instrument serif italic for the subtitle line, jetbrains mono for everything else, all five fonts served from CHAOS/static/fonts (pulled back out of git history). no gradients behind content, no neon, no purple. gold is legendary/mythic, red is an error, nothing else has colour. GET /api/file/:seed downloads the current signed bytes of a seed you own (the app's sync will use it)

the registry logic is a port of client/src/chaos_file/format.cpp + client/tools/registry_dev.cpp. client/build/chaos-vectors dumps 400 seeds + a dev-key signed ticket/rolled/claimed set into CHAOS/test/vectors, and vitest proves the ts derives the same tier/family/mode/seed and verifies the same bytes. regenerate the vectors whenever the c++ roll order changes

value lives in CHAOS/src/lib/server/value.ts, the numbers live in config: floor per tier (0.10 / 0.50 / 5 / 25 / 200 / 2000) * family * mode * age * supply. the client never sees those numbers

config (v1.3): nothing is inline any more. CHAOS/src/lib/server/config.ts reads every knob from the environment with a default: site name / url / source, session cookie + days, rolls per day, bank max, starting bank, floors, family and mode multipliers, age bonus, supply discount, preview cap, market page size, featured count. locally they come from .dev.vars (gitignored, .dev.vars.example lists all of them), on cloudflare the plain ones sit in wrangler.toml [vars] and the three secrets (signing key, session secret, google client id) go through wrangler pages secret put. the user moves them to cloudflare one by one, the defaults keep working meanwhile. what is NOT config, on purpose: the tier odds, the unlock table, the family buckets, the seed derivation. those are protocol, the c++ and the ts must agree on them, the vectors prove they do

previews (v1.3): the claim can carry a png. the app shrinks the vault png to 640 wide (box filter, client/src/util/png.cpp shrinkPng) and sends it base64 with the claim; the registry checks it is a png under PREVIEW_MAX_BYTES and keeps it in r2 (binding PREVIEWS, bucket chaos-previews, local dev gets a fake one from wrangler). GET /api/preview/:seed serves it, the market, vault and seed pages show it, featured on the market is the highest tiers listed. search by seed prefix or owner, sort newest / cheapest / priciest / rarest, tier filter, all as url params so a filtered market is a link

payments: still recorded, not collected. that is v2.0 territory now, it needs a provider account

acquire hands the buyer a re-signed file with a new nonce. the registry keeps the claimed bytes (files table), so the buyer always gets a real file with the ticket intact, no receipt stubs

payments: recorded, not collected. v1.3

chaos.tetstack.dev stays the old web toy. previews (png) stay on the client until the market wants thumbnails (s3/r2, v1.3)

repo additions (as it ended up): client/src/chaos_file/ (format, verify, sign), client/src/sync/ (api client, session flows), CHAOS/ (the registry site), server/dev/ (the dev key), shared/chaos_format.h, shared/server_pubkey.h, docs/chaos-file-format.md, docs/ownership-model.md, docs/deploy.md

roadmap shift: v0.5 = .chaos format, v0.8 = site backend, v0.9 = site frontend, v1.0 = sync, v1.1 = release. blockchain mint = v2.0 optional.

cert decision (2026-11-04): server-signed, nothing secret in the client. amended 2026-11-05: commit-reveal, see below

server holds one ed25519 keypair. only the public key ships in the client binary.

roll (online): app asks POST /api/ticket while logged in. the weekly allowance is enforced HERE. server returns ticket = ticket_id(16) || user_id(8) || issued_ms(8) || ed25519 sig(64).

seed_full was blake3(tag || ticket_id || client_random || instance_id). that still had a hole, see "the grind hole" below. now (v3): seed_full = blake3("CHAOS-SEED-v3" || ticket_id || server_random || client_random)

roll (offline): no ticket, state = preview. renders fine, never claimable. replayable but worthless.

claim: upload .chaos. server checks ticket sig, ticket unused, ticket user == uploader, recomputes seed_full, derives family/tier/mode itself, computes value, then fills owner_id, claimed_at, nonce and cert_sig = ed25519("CHAOS-CERT-v2" || bytes 8..223). app verifies cert with the embedded pubkey, offline.

transfer: server rotates nonce, re-signs, old nonce revoked in db. same as before.

dropped: hmac k1/k2, key obfuscation, client-side cert. forging now needs the server private key. no "until someone figures it out".

cost: a claimable roll needs the network. preview rolls stay offline.

the layout lives in shared/chaos_format.h (512 bytes, version 2). docs/chaos-file-format.md explains it.

how it landed (v0.5)

client/src/chaos_file/format.cpp: read, write, deriveSeed, verify (layout, seed recompute, ticket sig, cert sig, by state), and the signing half for the server. libsodium ed25519, blake3 for the seed. the registry public key is shared/server_pubkey.h

the ticket on the wire is 96 bytes: id(16) user(8) issued_ms(8) sig(64). the app takes it with --ticket t.ticket, derives the seed from it, and the kept file is state rolled

verify rules: preview = layout only and no ticket allowed. rolled = layout + ticket sig + seed recompute. claimed = all that + cert sig + owner != 0. offline verify answers "was this ever real", only the registry answers "is this copy current" (nonce)

chaos-registry-dev: the server's half in c++ until the website exists. keygen, ticket, claim, transfer, verify, one flat text db. it never trusts the client: on claim it recomputes the seed and re-derives tier/family/mode with deriveProps (the same function rollDimension uses, test enforced). the ts server is a port of this file, not a redesign

dev key: server/dev/server.key is committed on purpose (tests + tool). NOT the prod key. chaos-registry-dev keygen on the real box, paste the pubkey into shared/server_pubkey.h, rebuild, done

app side: --verify f.chaos, --import f.chaos (verify then store, how a claimed or transferred file comes back into the vault), --vault now shows valid/INVALID per file

tested end to end through the binaries: ticket -> roll -> claim -> second claim refused (ticket used) -> transfer -> old copy STALE, new copy CURRENT -> transfer from the stale copy refused -> preview claim refused

the grind hole (found 2026-11-05, fixed, format v3)

with the ticket fixed and client_random free, the client could loop client_random a million times a second, derive the tier locally (3 blake3 + a few rng steps), and only submit the mythic one. the server would accept it: the ticket really was used. exactly "reproduce the rare one again and again and sell it"

fix, the provably fair way games do it: commit-reveal. client draws client_random, sends commit = blake3("CHAOS-COMMIT-v3" || client_random) with the ticket request. the ticket the registry signs carries that commit and its own server_random (drawn after it saw only the commit). seed = blake3(tag || ticket_id || server_random || client_random). the client cannot change its random after seeing server_random (the commit is under the signature), the registry cannot pick server_random for an outcome (it does not know client_random). instance_id left the hash: anything the client can vary freely must stay out of the seed

verify now checks: ticket sig, commit matches client_random, seed derives, cert sig. the app: chaos --commit out.reveal prints the commit, the ticket request carries it, chaos --ticket t --reveal out.reveal rolls. the panel's roll tab has both fields. preview rolls are unchanged, they never mattered

layout v3: ticket grew to 160 bytes on the wire (id, user, issued, commit, server_random, sig), cert now covers bytes 8..288. shared/chaos_format.h has the map. nothing had shipped so no v2 compatibility

what the previous draft got wrong (kept for the record)

a. rarity was a local lottery: seed from os.urandom, loop until mythic, claim. fixed by the ticket in the seed hash.

b. "non-bruteforceable" cert whose keys shipped in the binary. fixed by moving the signature to the server.

open questions still unanswered: hosting (fly/railway/hetzner), two-step vs direct-to-email transfer (two-step is what shipped).

12control panel + ui (done, v0.6)

dear imgui, docked, inside the render loop. no second toolkit

screens: vault (your .chaos files, claimed / rolled / preview, thumbnails), roll (preview or claim, allowance left), view (seed, lineage, mood, mode, palette, copy buttons), settings (monitor, fps cap, mode override, autostart), account (google login, sync)

boot poster: the web's loading screen, two colour field, build stages as headlines. the app is fast enough that it might flash for 200ms, keep it anyway, it's the brand

fonts: the web ones vendored (unbounded 800 for headlines, jetbrains mono for seeds, dseg14 for the counters)

hotkeys: M mood, R reroll preview, space pause, F fullscreen, esc panel. all rebindable later or never

how it landed: no docking branch in the vendored imgui, so it is a fixed 460px panel on the right with tabs, esc toggles it. vault tab lists every .chaos with its png thumbnail (stb_image -> gl texture), state, tier, family, mode, valid/INVALID, a load button. roll tab: preview roll, claimable roll from a .ticket path, load any seed form. view tab: hex / base58 / words with copy buttons (glfw clipboard), tier (gold text when gold), lineage, mood slider, mode combo, palette swatches, mutation bar. settings: vsync, fps cap, pause, monitor, fullscreen, poster toggle. account: greyed google + sync buttons (v1.0), instance id, registry key fingerprint, vault path

the panel only sets requests (reroll, load, keep, fullscreen, quit), the main loop acts on them next frame. imgui's glfw backend chains to my key callback, and hotkeys are ignored while a text field has focus

poster: drawn in imgui's foreground list, A field with soft B bands, the 7 real build stages as headlines with their real timestamps, unbounded 800 for the headlines, jetbrains mono for the detail, dseg14 for the clock. holds 0.9s after the first real frame then fades over 0.7s. the woff2 fonts from the web got converted to ttf (fontTools) and live in client/assets/fonts. the band field is a crude stand in for the web's shader poster, polish later

reroll (R) rebuilds the dimension and the renderer in place, pause freezes the sim clock so nothing jumps, fullscreen remembers the windowed geometry, fps cap sleeps the rest of the frame. --ui-shot out.png renders the panel + poster into a hidden window for screenshots without a display

13wallpaper mode (x11 done v0.7, wayland done v1.2)

x11: _NET_WM_WINDOW_TYPE_DESKTOP, below everything, one window per monitor (xrandr), no decorations, no input. works on every x11 wm and xwayland for free

wayland: wlr-layer-shell background layer. sway, hyprland, river, all the wlroots ones. gnome and kde do not do layer shell for third parties, so there it is a plain window you maximise behind stuff (or xwayland). I'm not writing a gnome extension

glfw gets patched for the layer shell surface, that is the whole reason glfw not sdl

how it landed (v1.2): no glfw patch after all. the wayland wallpaper is its own backend, client/src/platform/wl_layer.cpp: wl_display, registry, one wl_surface + zwlr_layer_surface_v1 per wl_output (layer background, anchored to all four edges, exclusive zone -1, keyboard interactivity none, size 0x0 so the compositor sizes it), one EGL gl 4.3 core context, a wl_egl_window per output, glad loaded through eglGetProcAddress, swap interval 0 and our own pacing. buffer scale honoured. the two protocol xmls (wlr-layer-shell, xdg-shell for the popup type it references) are vendored in client/third_party/wayland-protocols and wayland-scanner generates the glue at build time. --backend auto|wayland|x11, auto = layer shell when the compositor offers it, else the x11 path. tested on kwin from the build container: 1920x1080 background layer, 150 frames, clean exit

no fullscreen-app pause on wayland: compositors do not tell clients what has focus. battery pause works. the x11 path keeps both

kde, found on the real desktop (2026-11-06): kwin puts a layer-shell background surface in the same layer as plasma's own desktop window and stacks the newer one on top, so the engine covered the wallpaper AND the icons and widgets, and it is not a "wallpaper option" the way the user wanted. the only thing that is, on plasma, is a wallpaper plugin. so: client/plasma/, a qt6 qml module (org.chaos.wallpaper) with one item, ChaosView, a QQuickFramebufferObject that rolls the dimension and runs our Renderer inside plasmashell's gl context (glad loaded from that context), paced by a QTimer at the configured fps, battery pause, the registry probe on a worker thread so plasmashell never blocks. the package (metadata.json, main.qml, config.qml with seed / mode / fps / registry / battery pause, kcfg) installs to share/plasma/wallpapers/org.chaos.wallpaper and shows up as wallpaper type "CHAOS". the gl renderer moved into its own PIC library (chaos_render) so the plugin and the app share it, and the resource lookup learned the install data dir because inside plasmashell "next to the exe" is plasmashell. built with -DCHAOS_PLASMA=ON, needs qt6-base + qt6-declarative. written blind: my build container has no qt and lost sudo, so the user builds it on arch and reports. the layer-shell backend stays for sway / hyprland / river where it is the right thing

how it landed (2026-11-06, second try): first install crashed plasmashell (a worker thread poked the view after it died: now a QFutureWatcher member, status handed over inside synchronize). second install was a black desktop with no crash and no message. cause, found with nm: pickWallpaperSeed and power::onBattery were compiled into the chaos binary, not into a library, so libchaosqml.so shipped with two undefined symbols; a shared lib links fine like that, plasmashell refuses to dlopen it, the qml import fails, and plasma paints its fallback. nobody says a word. so:

the seed rule moved to client/src/wallpaper_seed.cpp and the battery check to client/src/platform/power.cpp, both in chaos_core, which the app and the plugin both link. the plugin links with -Wl,--no-undefined so a hole is a link error, never a black desktop again

the view never probed the registry on its own: setRegistry() with the same default is a no-op, so it sat offline forever. now componentComplete() probes, then again every 30 s offline / 10 min online, and a pick made while offline is retried once the registry answers

the lobby: when nothing may play (offline, nothing activated, bad seed, registry unset) the plugin draws one fixed public seed, blake3("CHAOS-LOBBY-v1"), instead of #0b0c0b. a constant is not generation, so the offline rule stands: nothing new is rolled, nothing claimable appears

a status bus: every line the wallpaper reports (showing x / tier, lobby (why), registry offline, paused, render error) goes to ~/.cache/chaos/plasma.log and the settings page shows the last one live, so "it is black" always has a sentence next to it

third install: module loaded, but the journal said "Root item of wallpaper not a WallpaperItem instance" and "wallpaper is not defined". plasma 6 wants the package root to be a WallpaperItem (import org.kde.plasma.plasmoid) and hands the settings over as root.configuration; the plasma 5 wallpaper context id is gone. main.qml rewritten that way

then it loaded. next ask: "make it smooth" and "people who won't want any extra satellite than the main one". so, plugin only (the chaos itself is untouched: value, lineage, cert, all the same):

pacing: at 50 fps and above the item asks for a frame on the window's frameSwapped, so vsync paces it and no timer beats against the refresh; below that a Qt::PreciseTimer paces it. the 30 fps default stays the light option

render size: the fbo may be smaller than the item (full / 3/4 / half); qt stretches the texture, the post stack pays for fewer pixels. the easy win on 4k

draw: Layers on the Renderer (satellites / swarm = rings + debris + planes / particles). the settings page offers everything, no satellites, main shape alone. a per-screen draw choice, never a property of the seed, so nothing about rarity or value moves

and the real reason the user's own chaos showed the lobby: the registry is not deployed yet, so nothing can ever be activated, and the rule said activated-only offline. relaxed (2026-11-06): offline, anything kept in this install's vault plays too. showing again what this machine already rolled and showed is not generation; the thing the rule protects, claiming, still needs the server. the empty-seed order offline is now activated, then newest kept, then the lobby

the panel restyled (2026-11-06, "it kinda is ugly"): the imgui panel and the boot poster now sit on the site's tokens, copied from CHAOS/src/routes/+layout.svelte into client/src/ui/panel.cpp (tk::). silver on #0b0c0b, hairlines, tracked uppercase mono labels drawn glyph by glyph (imgui has no letter-spacing), flat hairline keys that fill silver on hover, bordered tags for tier / online / fps (gold, red the only accents), the status line in instrument serif italic, the uptime counter on an lcd plate in dseg14. no green, no rounding, no gradients. the tabs are a row of keys, the lit one is the page

registry toolchain (2026-11-06, "there are some vulnerabilities"): npm audit on the kit 2 / vite 5 / wrangler 3 / vitest 2 set showed 19 findings, all in dev tooling (old esbuild, undici, sharp via the old adapter, cookie via kit 2); the runtime deps (jose, noble, scure) were clean. moved to sveltekit 3 / vite 8 / vitest 5 / wrangler 4 / adapter-cloudflare 8 / typescript 6. kit 3 changes that touched us: no svelte.config.js (adapter + preprocess go to the sveltekit() plugin in vite.config.ts), $lib became #lib (package.json imports + tsconfig paths), $app/stores became $app/state, goto lost keepFocus, Handle moved to @sveltejs/kit/hooks, tsconfig extends $app/tsconfig, and env vars must be declared: src/env.ts lists all 21 registry keys as optional strings, config.ts and context.ts read $app/env/private. adapter 8 no longer fills event.platform under vite dev; bindings come from import { env } from "cloudflare:workers" (wrangler's local proxy in dev, the real env in prod), so context.ts reads DB / PREVIEWS there with platform.env as the fallback. compatibility_date bumped to 2025-10-01 for that import. result: 4 low findings left, all deepmerge inside vite-plugin-svelte's dev inspector, nothing that runs in prod. build, 7 tests, svelte-check zero errors, and vite dev on the local D1 all verified

the lag (2026-11-06, intel hd 630): the stack ran 4x msaa at full window size with 16f hdr targets and five bloom mips, and with the vault tab open the panel verified every file's two ed25519 signatures and re-read activated.json per entry per frame. now: Quality on the Renderer (render scale 0.25..1 and msaa 0/2/4, Post allocates without the multisample target at 0 and the final blit stretches to the window), default msaa 2, the display section in the settings tab (draw / render size / anti-aliasing) persisted in settings.json and read by the app, wallpaper mode and the plasma plugin (Samples config) alike, and the vault rows verified once per refresh. the user's answer to "lagging?" is half size + 2x

the mark + the window icon: a 16x16 pixel orb cut once, the lower half slipped two pixels right, in mint. one bitmap, client/src/ui/mark.h, drawn as rects by the panel, set as the glfw window icon (x11), and emitted as rects into CHAOS/static/favicon.svg and packaging/chaos.svg. the window sets app id / x11 class "chaos" so wayland compositors pick the packaged icon up through chaos.desktop

the direction (2026-11-06, three references: a crt pixel pub site, a light grotesk agency page, an industrial dark site with bracketed mono nav and corner clocks): deep green-black #0c1411, text #e7eee9, mint #9be3c0 as the live accent, red #ff4d5e for bad, gold kept for the top tiers; Inter 400 for headlines in sentence case and the one sentence voice, JetBrains Mono for everything else, labels tracked uppercase, nav as [bracketed] mono links, a /PATH crumb and a ticking clock in the corners, a footer with menu columns, the big mark and a dark gradient. unbounded and instrument serif are gone from the site. applied to the app panel + poster and the whole site through +layout.svelte tokens; page headlines recased

"too rectangular and dull" (same day): hairlines on flat dark read as a spreadsheet. so, both sides: pill keys and tags, panels with 16px corners, the hud a floating pill, the footer a rounded card on a warm-to-green gradient, a faint dot field behind everything, tiers coloured (mint / gold / red). the app's panel is a floating card with the dimension showing through (78% bg) and its accent is the current seed's palette b lifted to read on dark, so every roll recolours the ui; the poster draws the mark big in the two palette colours with the lower half slipping on a step clock. the site's hero does the same orb in divs, and shows the highest listed seeds as live preview cards

third pass, "remove the AI looking round extras, add a white mode, make the animation smooth, architecture drawing kind of look, flat, the line that gets out a bit": the drafted look. no rounding anywhere; every box is four hairlines that run 6 px past their corners (css: ::before carries top+bottom lines 6 px wider, ::after carries left+right lines 6 px taller; imgui: archRect); grid paper behind everything (24 px); the mark's cut row drawn as a dimension line with end ticks; the orb's lower half drifts on a sine, drawn per frame (requestAnimationFrame on the site, the frame clock in the poster), never stepped; sections rise in on load. light mode: a second token set under :root[data-theme=light] (paper #f4f4f0, ink #121412, deeper mint / gold / red so they read on paper), chosen by the [light] / [dark] key in the hud, remembered in localStorage, defaulting to the os preference via a two-line script in app.html so there is no flash. the app has the same: Theme DARK / LIGHT selected by settings.json "theme", and the palette accent is darkened to ink on paper

the mark, fourth try (2026-11-06): the cut orb was "not correct"; the ask was "something that represents chaos, drawn as small boxes, the grid cut should affect it". twelve candidates on contact sheets, the user picked entropy: 16x16, solid order in columns 1..4, cells scattered by a fixed noise to the right. every cell is a small box inside its grid cell (10% inset), so the grid shows between them. loose(x >= 5) cells drift on their own slow sines where it animates (poster, site hero); the ordered side never moves. client/src/ui/mark.h is the source; the svgs and the hero rows are generated from the same rows

the second overlay (2026-11-06): the app's old "autostart" checkbox had written ~/.config/autostart/chaos.desktop, so chaos --wallpaper (the layer-shell window) started at login underneath the plasma plugin and showed through on desktop switches. the entry is removed, the app removes it again at start if it finds one, and the wallpaper section left the app's settings: on kde the plugin's own page holds draw / size / fps / battery, the app keeps only its window, display and registry settings

the user's asks, same message: the vault folder is a setting now (account tab: choose a folder via kdialog / zenity, or type a path; settings.json vault_dir; vault::setDir; the plugin reads the same key) so "where are the files" has one answer the user chose. the app asks the registry every 30 s on a worker and on request (account tab: check now), so online is live; the default registry is http://localhost:5173 until launch (CHAOS_REGISTRY_URL cmake option, the plugin's main.xml) and then the domain. a setup tab, first and default: four steps with one primary key each (sign in, roll, keep then claim, set as desktop wallpaper), the last one a plasmashell script over qdbus6 / qdbus / gdbus that switches every desktop to the plugin with this seed. keys grew (38 px, 11 px text) and got a primary role filled in the palette accent. fonts come from the desktop: kdeglobals font / fixed through fc-match (fallback: fontconfig sans / monospace, then our shipped inter / jetbrains); the site uses system-ui and ui-monospace stacks first for the same reason, with a four-step setup section on the front page

sanitization + honeypot (2026-11-06, "do a full code clean and sanitization plus honeypot"): every api body goes through CHAOS/src/lib/server/input.ts (size cap, json object only, seeds as exact lowercase hex, files as base64 of exactly 512 bytes, previews bounded and png-signature checked, ints bounded, enums closed, search trimmed of control characters, lists capped at 500); a failed check is a 400 with the reason, never an exception. path params checked the same. the honeypot (honeypot.ts): paths nobody honest asks for (/api/admin, /api/ticket/free, /api/internal, /.env, /wp-login.php ...) answer a random decoy that looks like a win and flag the caller's address (D1 table flags, migration 0002, 30 days); robots.txt "disallows" exactly those paths, which is where a scraper looks first. a flagged address still reads but gets no ticket, claim, listing, acquisition, revoke or sign-in (403 "no"). the login form carries a hidden website field; filled = bot = flagged. plus nosniff / deny-frame / same-origin referrer headers on every response, the session cookie secure on https, tokens over 4 kb ignored. wrangler 4 defaults d1 migrations to local, so db:migrate passes --remote; r2 has to be enabled once in the dashboard before the bucket exists

storage (2026-11-06): D1 only. the user chose not to activate R2 (it wants a billing method), so previews live in a D1 table (migration 0003, 900 KB cap per row against D1's 1 MB row limit); the R2 code and binding are gone, not optional. D1 free tier is 5 GB for everything; "grow the D1 slowly later after I get money" is the plan, and R2 is a future move if the market ever fills

"set as desktop wallpaper" failed on the real desktop (2026-11-06): the plasmashell script carried single-quoted js strings inside a single-quoted shell argument, so every tool got a broken command. now the js uses double quotes only, busctl (systemd, always there) is tried before qdbus6 / qdbus / gdbus, and the tool's own stderr lands in the status line. the plugin's vault picker lists everything kept (claimed first, then rolled, then previews; the user wanted previews back, and anything kept may play since the offline rule was relaxed), and its settings page is a left-aligned column instead of plasma's centred form

config, final form (2026-11-06): the cloudflare dashboard is the only place plain values live. wrangler.toml carries no [vars] (a block there would overwrite the dashboard on every deploy), .dev.vars and its example are gone, local dev runs on the defaults in config.ts plus ALLOW_FAKE_LOGIN=1 on the command line. docs/deploy.md keeps the full list with the production values. the roll tab had two visible keys named "claimable roll" (imgui id clash, a red box over the panel); the by-hand one is "roll with this ticket" now

performance pass (2026-11-07, "so it runs smoothly for low end pcs too"): measured first. chaos --bench N renders offscreen at 1080p and prints ms per frame and resident memory; CHAOS_PROFILE=1 adds gpu timer queries per pass. on the intel hd 630 the full stack was 35 ms a frame: the scene 12 (the backdrop dome shaded over the whole screen, the berry skin with ten noise taps per pixel, thirteen satellites), the post stack 13 across eight full-screen passes. what changed: bloom composite + duotone are one pass and the 16-bit bloom frame is gone; the film grain lives inside the afterimage pass; the final draw is a framebuffer copy; the smear target exists only once bleed has shown; the backdrop is drawn after the opaque berries with the depth test on, so pixels under a berry are never shaded; a lite detail level (settings "detail", plugin "lite") drops the background to three octaves, the bloom chain to quarter res and the berry's pore gradient and wax; thumbnails are box-filtered to 320 px before upload (8 MB of texture each before); the view tab no longer re-encodes the seed's three spellings every frame; minimised windows render nothing; on battery the app caps itself at 30 fps. presets in the display section: low-end (60% size, no msaa, lite, no satellites, 30 fps) and full. resident memory was never the issue (86 MB); the gpu was

measured on the real desktop (2026-11-07, intel_gpu_top + top): the wallpaper at full size and 2x msaa took 58% of the gpu at 543 MHz, 5.5 W, 9% of a core, 243 MB of gpu memory inside plasmashell (576 MB resident in all; plasmashell itself is 150 to 250). so the plugin's defaults are 3/4 size and no msaa now (16 ms a frame instead of 28 on the bench, nothing a wallpaper shows), and the hdr targets are R11F_G11F_B10F instead of RGBA16F (half the bytes; alpha was never read). lite on top lands at 12 ms. the user's own machine keeps full in the app

pause when gaming (2026-11-07): the plasma plugin reads what is over the desktop through plasma's own TasksModel (Coverage.qml behind a Loader, so a plasma without the taskmanager module just leaves it at 0): a fullscreen window on this screen and desktop stops the frames entirely, a maximised one drops it to 4 fps, the desktop showing again restores the pace. a checkbox on the wallpaper page, off by default (the user: an option for those who want it, disabled unless asked). the app's battery cap and the x11 fullscreen pause are options now too, both off; the app's pause is a big key on the prefs tab. the x11 wallpaper mode had its own fullscreen check since v0.7

.cheng (2026-11-07, the chat's vote): your own model on the stage, offline, a display choice. client/src/model/cheng.cpp imports obj / stl / gltf / glb itself and blend / fbx / dae / step / usd / x3d / abc through blender when it is installed (a generated python script, glb out, read back), then welds, decimates by grid clustering until it fits 20k vertices, centres, sizes to radius 4.5 and writes the .cheng: magic, counts, bounds, name, positions quantized to 16 bits inside the bounds, normals as 16-bit octahedral pairs, u32 indices. about 300 KB for 20k vertices, loads in a blink. applyModel() swaps the dimension's main mesh, drops the swarm, keeps palette / mood / mode / backdrop / particles; skins: berry (the seed's skin on the model), mono (phong, one colour), wire; random picks per start. the app's model tab (file dialog or path, skin, colour, random, on / off), chaos --cheng and --model, settings.json model_*, wallpaper mode, and the plasma plugin (Model* config, carried by "set as desktop wallpaper"). never a seed, never claimable, never valued: the registry never hears of it

the docs page (2026-11-07): /docs on the site, one page, the project's voice: what this is, the app, the four steps, tabs, keys, every file (.chaos states, .cheng, .ticket / .reveal, settings.json, session, instance, activated.json, plasma.log), the registry pages, value and odds, the wallpaper, offline, low-end, what gets stored, the command line. linked from the hud and the footer. the repo had nothing of any assistant's name in it to scrub

the palette, again (2026-11-07, "the dark theme color looks ugly"): the green-black and mint are gone. dark is slate #141416 with bone #ece9e2 and amber #f0b43c as the live accent; light is paper #f3f1ec with ink #17171a and a darker amber #a86d12; red #e5484d / #c8323a for bad, gold stays for the top tiers. same tokens in CHAOS/src/routes/+layout.svelte (--accent replaces --mint everywhere) and tk:: in the panel; the mark, the favicon and the window icon turned amber. the light theme's list problem was two things: the panel was 86% opaque, mud on a bright scene (paper is 96% now), and the seed-palette accent went grey on paper whenever the palette was near white (the accent falls back to the theme's amber when the palette has no hue, and is held at ink strength otherwise)

docs, second cut (2026-11-07, "a tab each thing alone, with code parts"): /docs is a layout with a side rail of tabs and one page per topic (overview, the app, generation, .chaos, the roll, .cheng, the registry, the wallpaper, files and settings, command line). each page quotes the real code that does the thing, the chunk and not the file: the 512-byte struct and its header comment, commitOf / deriveSeed / verify, subStream / rollTier / unlocksFor / deriveProps and the first half of rollDimension, the app's rollClaimable and the registry's issueTicket / claim / allowance, prepareModel / writeCheng / readCheng / applyModel, valueCents, the input validators and the trap list, pickWallpaperSeed / lobbySeed and the plugin's pick, the mark's rows. the chunks are sliced out of the source files by a generator script at writing time, so they are the code as of 2026-11-07; when the code moves, regenerate or edit the page

"when loading or rolling it kind of not appearing" (2026-11-07): the boot poster replayed on every roll and load, a full opaque sheet for close to two seconds, and in slate that read as nothing having happened. the poster plays once now (App::bootShown); after that a roll or a load cuts straight to the frame. CHAOS_UI_KEY=R / L with --ui-shot renders the window right after a roll or a load, for exactly this kind of check

the sandbox: gl version checked (needs 4.3, refuses gles with a hint), fbo clamped to 8192, every qt callback wrapped (render, status, vault, power), render exceptions back off 3 s and give up after 3 in a row until the settings change, the destructor no longer waits on the probe (that stalled plasmashell for the curl timeout on every wallpaper swap)

gnome still does not offer the layer shell, so on gnome wayland it falls back to the xwayland desktop window, which gnome stacks wrong. that stays the open question in 17. wayland-client, wayland-egl, egl are new deps (arch: wayland, libglvnd. debian: libwayland-dev, libegl-dev)

gotcha found the hard way: glfw on wayland pulls libdecor, and libdecor's gtk plugin can block forever in glfwInit (it did, in my build container, the day after a reboot). --no-libdecor (GLFW_WAYLAND_DISABLE_LIBDECOR init hint) or LIBDECOR_PLUGIN_DIR=/nonexistent. --x11 is the other escape hatch. the wallpaper mode never hits it because it forces x11

fps cap in wallpaper mode (30 default), pause when a fullscreen app has focus, pause on battery. a wallpaper that eats the gpu is a wallpaper that gets uninstalled

multi monitor: one dimension per monitor or the same one mirrored, user picks

how it landed (v0.7): chaos --wallpaper. glfw is forced onto x11 (GLFW_PLATFORM_X11), so a wayland session goes through xwayland without asking. one undecorated, unfocusable window per glfw monitor at the monitor's position and size, created hidden, then via glfw3native + xlib: _NET_WM_WINDOW_TYPE_DESKTOP, _NET_WM_STATE below + sticky + skip taskbar + skip pager, _NET_WM_DESKTOP all, WM_HINTS input False, empty XShape input region. then shown. verified with xprop on kwin's xwayland: desktop type, below, no focus, 1920x1080+0+0, bottom of _NET_CLIENT_LIST_STACKING

each monitor has its own gl context and its own Renderer. "each" derives the extra seeds as blake3(seed || "monitor" || i), so one seed still describes the whole setup. vsync is off per window and one sleep paces the whole loop at wallpaper_fps (30 default), because vsync on N windows stacks

pause: every 0.5s it checks _NET_ACTIVE_WINDOW for _NET_WM_STATE_FULLSCREEN and /sys/class/power_supply/*/status for Discharging. paused = sleep 250ms, no frames. (the fullscreen check is written against the ewmh spec, not yet watched with a real game on top. --no-pause for testing)

settings.json (~/.config/chaos/): seed, mode, monitors, wallpaper_fps, pause_on_battery, pause_on_fullscreen, vsync, fps_cap, poster. the panel's settings tab edits it ("use this seed as the wallpaper", save). flags override the file

autostart: the panel toggles ~/.config/autostart/chaos.desktop with Exec=<this binary> --wallpaper. xdg autostart, every desktop reads it. no systemd unit, maybe later

no panel and no keys in wallpaper mode on purpose: it has no input region. you drive it from the normal chaos window (settings tab) and restart it. a control socket is a v1.3 nicety

gnome/kde under wayland: still the open question in 17. kwin honours the x11 desktop type through xwayland (tested), gnome does not stack xwayland desktop windows under the shell. that one needs the layer shell or a gnome extension, and I said no extension

14vault + accounts + sync (vault done v0.4, sync v1.0)

vault: ~/.local/share/chaos/vault/<seed_short>.chaos plus a preview png next to it. plain files, the user can back them up with rsync like an adult. (done, v0.4: --keep with --shot, or K in the window. --vault lists it. the preview png is blake3'd into preview_hash)

the offline rule (decided 2026-11-05, done): "once loaded online, usable offline. never generated offline." the app pings GET /api/health at start (2.5 s, and the registry's pubkey must be ours). online: everything, and loading a vault file also asks POST /api/check and records the answer in ~/.local/share/chaos/activated.json (that is the activation; --import does it too). offline: no previews, no rerolls, no tickets, no --seed of anything not activated here. allowed: loading an activated vault file, wallpaper mode on an activated seed. the panel shows ONLINE / OFFLINE, the roll tab says why. --dev-offline lifts it for development. honest note: this is client code, so it is policy for honest users, not a wall. the walls are the registry's: rationed tickets, commit-reveal, one claim per seed, server-side rarity. the full flows are in "Data and logic flow"

the file is a receipt, the website is the truth. the app never decides ownership

google login in the app: pkce flow, system browser, refresh token in libsecret. openid email profile only, no drive scope (the old plan used drive, dropped, the website is the sync)

sync on start, on login, on a button: send the seed_fulls you hold, get state per seed back, mark transferred / revoked locally.

how it landed (v1.0): no oauth inside the app. the site's /login?app=1 signs you in with google and shows the session jwt; the app takes it (--login-token, or paste in the account tab, "open the sign-in page" does xdg-open) and keeps it in ~/.config/chaos/session (0600). libsecret later, maybe. the token rides as a bearer header on /api/me, /api/ticket, /api/claim, /api/vault, /api/sync, /api/file/:seed (client/src/sync/api.cpp, libcurl + nlohmann). --login-dev NAME for registries with ALLOW_FAKE_LOGIN

--roll: commit, ticket, seed in one go (client/src/sync/session_flow.cpp rollClaimable). the app checks the ticket's signature and that it carries the commit it sent before trusting it. panel: one button when connected

--claim f.chaos / "claim this roll": POST, verify what comes back against our pubkey and the seed, store it over the rolled file, activate it. the by-hand --commit / --ticket / --reveal path still exists for people without a session

--sync / "sync vault": every non-preview seed you hold -> /api/sync -> activation updated (current or transferred away). then /api/vault: anything the registry says is yours that you do not hold (acquired on the web) is fetched from /api/file, verified, stored, activated. tested: a seed sold on the web turned STALE on the seller's app and appeared in the buyer's vault on their next sync

instance_id: 16 random bytes made once, ~/.config/chaos/instance. goes in every .chaos so the server can ban one install if it leaks tickets (done, v0.4)

15packaging (v1.1)

appimage first (one file, every distro), then aur (-bin and -git), .deb, flatpak last cuz flatpak and wallpapers and layer shell is a fight

tar.gz for the people who read

windows: no. mac: no. (until the next adhd hit, as stated)

how it landed (v1.1): cmake --install puts chaos + chaos-registry-dev in bin/, shaders and assets in share/chaos/, the .desktop and the icon where they go, the two format docs in share/doc. the binary looks for its resources next to itself, then at ../share/chaos, then at the build-time path, so the same build runs from the source tree, a prefix, a .deb or an appimage. chaos --version

.deb + .tar.gz: cpack from client/build (CPACK_GENERATOR DEB;TGZ, deps on libglfw3, libsodium23, libssl3, libcurl4, libx11-6, libxext6, libgl1). built and inspected here (the installed-prefix run works, the .deb's contents and depends are right; dpkg -i itself could not be tried, the container's sudo broke after the host reboot)

appimage: packaging/build-appimage.sh. installs into an AppDir, linuxdeploy bundles glfw/sodium/curl/ssl/x11 next to the binary, leaves mesa to the host like every appimage does, appimagetool wraps it. the tools run extracted, no fuse needed in a container. NOT run here: the script downloads and executes those two tools from github and my sandbox refused that. run packaging/build-appimage.sh yourself once, it should just work

aur: packaging/PKGBUILD (chaos-engine-git). arch deps are the pkg names in client/README.md. not pushed, that needs the aur account

flatpak: not done. the sandbox and a wallpaper (desktop window type, xwayland, autostart) is the fight I said it would be. v1.3 or never

the registry: docs/deploy.md, nine steps, all of them need the user's cloudflare and google accounts and a prod key that is never the dev one. the app refuses any registry whose /api/health pubkey is not the one it ships

16roadmap + where it is (2026-11-04)

v0.1 window + one shape done. glfw, glad, berry skin, backdrop, offscreen --shot png

v0.2 full scene done. 41 generators, lineage, swarm, particles, rings, post stack, mood, palette jumps, msaa

v0.3 modes done. native mode per seed, display override, zen hides the swarm, bleed smear pass

v0.4 seed engine done. base58, bip39 24 words, rarity tiers + unlocks, 16 family buckets, vault (preview files), instance_id

v0.5 .chaos format done. read/write/verify, ticket roll, ed25519 cert, chaos-registry-dev (server half, reference for the ts port)

v0.6 control panel done. imgui panel (vault, roll, view, settings, account), boot poster, hotkeys, pause, fullscreen, fps cap

v0.7 x11 wallpaper done. desktop-layer window per monitor via xwayland/x11, mirror or each, fps cap, pause rules, settings.json, autostart

v0.8 website backend done. sveltekit on cloudflare (pages + D1), api under /api, google auth, allowance, commit-reveal tickets, claim, list, acquire, sync, public check, value, vectors from the c++

v0.9 website frontend done. landing (how a roll cannot be rigged, odds), /check with drag-drop, vault with lcd counters + download + list/unlist, market with tier keys, seed page, error page. the look is the reference's: its stylesheet tokens, its fonts served locally (unbounded 800, instrument serif italic, jetbrains mono, dseg14), hairline borders, lcd chips, silver on #0b0c0b, gold only for legendary/mythic, red only for errors. buttons are flat hairline boxes that fill silver on hover (the chrome keys were the old page's, user wanted new ones). its own mark: two squares, one solid one dashed, joined by a cut (static/favicon.svg). no tetstack name anywhere on it. first pass had gradients and neon, got called ai slop, rightly, redone. thumbnails still v1.3 (need r2)

v1.0 sync done. session token from /login?app=1, --roll in one go, --claim, --sync with fetch of web-acquired seeds, panel buttons for all of it

v1.1 release done, the build half. install rules, .deb + tgz via cpack, appimage script, PKGBUILD, desktop + icon, --version, docs/deploy.md. the deploy half (cloudflare, d1, secrets, google client id, prod key) needs the user's accounts

v1.2 wayland layer shell done. native backend, wl_layer.cpp, tested on kwin. + a plasma wallpaper plugin (client/plasma) for kde, untested here (no qt in the container)

v1.3 marketplace polish done. market search / sort / tier filter / featured, previews (app shrinks the png to 640 wide and sends it with the claim, registry keeps it in r2, /api/preview/:seed), every registry number moved to config + .dev.vars + wrangler [vars], client constants named

v2.0 blockchain mirror (optional) maybe never. before it: the cleanup pass (done 2026-11-05, section 18)

v3.0 rarity 2.0, value from real market later

build: client/README.md. debian/ubuntu and arch package lines are in there

tests: catch2, 19 cases (activation list, settings.json round trip + autostart, ticket/claim/transfer with tampering at every signed byte, deriveProps == rollDimension, vault rolled round trip, format offsets, splitmix vectors, xoshiro determinism, every byte matters, superellipsoid, hex, mode ids vs the file enum, mode odds, mode parsing, preset pins/caps, base58, bip39 trezor vectors, tier odds + ids, family coverage + unlocks, vault round trip)

18the cleanup pass (2026-11-05, before v2.0)

done: the stale adapter-node build dir and the two vendored-source clone dirs gone. every inline registry number in config. client timeouts and the preview cap named constants, user agent carries the real version, registry url a cmake option (CHAOS_REGISTRY_URL) instead of a string in the code. the stb_image implementation lives in one place (util/png.cpp), thumbs.cpp just uses it. svelte-check clean, zero warnings. the middle-dot character gone everywhere. main.cpp split: the cli commands (login, roll, claim, sync, verify, import, vault, commit, wallpaper hand-off) live in client/src/cli.cpp, main.cpp is the window loop

second pass (the full one): a clean rebuild with -Wall -Wextra -Wshadow -Wmisleading-indentation -Wpedantic found five warnings in our code, all fixed, and the shadow + indentation flags are now permanent in CMakeLists. the registry type-checked with noUnusedLocals / noUnusedParameters: clean. fourteen registry symbols were exported for no one, now internal. two dead c++ functions removed (Window::time, the screenshot() helper main stopped using). the Motion fields nothing reads any more are annotated: they are still drawn so the stream stays in step, removing a draw would move every seed. a root README that says what each folder is. the stale vite cache at the root gone. stale lines in this map fixed (repo layout, hosting). nineteen c++ test cases and seven registry tests pass after all of it, the cloudflare bundle builds, the offscreen, headless-ui and windowed paths run

not done, on purpose: the by-hand --commit / --ticket / --reveal path stays (people without a session, the dev tool, the tests). libsecret for the session file stays "later". shadows stay unported

17open questions (answer when you feel like it)

hosting: cloudflare pages + workers + d1 + r2, decided. npm run deploy from CHAOS/. docs/deploy.md has the nine steps

transfer: two step (list then acquire) or direct to email. two step is what 11 describes

mythic shape family: what is it. I have ideas, you have taste

gnome/kde wallpaper: plain window behind, or skip them in v1