#reel-wrap.is-scrolling { height: 60vh; min-height: 380px; } /* Fade masks only make sense when content runs past the edges. */ #reel-wrap.is-scrolling::before, #reel-wrap.is-scrolling::after { content: ""; position: absolute; left: 0; right: 0; height: 56px; pointer-events: none; z-index: 2; } #reel-wrap.is-scrolling::before { top: 0; background: linear-gradient(var(--surface), transparent); } #reel-wrap.is-scrolling::after { bottom: 0; background: linear-gradient(transparent, var(--surface)); } #reel { padding: var(--sp-6) var(--sp-5); } #reel-wrap.is-scrolling #reel { position: absolute; inset: 0; overflow-y: auto; scrollbar-width: thin; } .credit-group { margin-bottom: var(--sp-8); } .credit-group:last-child { margin-bottom: 0; } .credit-group > h3 { text-align: center; color: var(--accent); letter-spacing: 0.02em; margin: 0 0 var(--sp-2); } .credit-group > .credit-note { text-align: center; color: var(--text-mute); font-size: var(--fs-xs); margin: 0 0 var(--sp-5); } /* One card per PERSON, not per repo — someone who worked on four repos used to render as four separate identical entries. */ .credit-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: var(--sp-4); max-width: 760px; margin: 0 auto; } .credit-card { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-4); border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-alt); text-decoration: none; color: inherit; transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; } a.credit-card:hover { text-decoration: none; transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); } .credit-card img, .credit-avatar-fallback { width: 52px; height: 52px; flex: 0 0 52px; border-radius: 50%; border: 1px solid var(--border); } .credit-avatar-fallback { display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 1.3rem; color: var(--accent-ink); background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 55%, #0a7d43)); } .credit-body { min-width: 0; } .credit-name { display: block; font-weight: 700; color: var(--text); overflow-wrap: anywhere; } .credit-meta { display: block; font-size: var(--fs-xs); color: var(--text-mute); margin-top: 1px; } .credit-roles { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; } .credit-toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-top: var(--sp-3); } .credit-hint { color: var(--text-mute); font-size: var(--fs-sm); margin: 0; } #reel-pause { display: none; } #reel-wrap.is-scrolling ~ .credit-toolbar #reel-pause { display: inline-flex; } @media (max-width: 560px) { .credit-grid { grid-template-columns: 1fr; } .credit-toolbar { flex-direction: column; align-items: flex-start; } }
Viridite

Credits

Everyone who's put code, hardware testing, or compatibility reports into Viridite — pulled live from GitHub, same list the launcher's own About screen shows.

Groundwork

Viridite is not built from nothing. These are the projects, specifications and reverse-engineering efforts it stands on — several of them are the reason a given part works at all, and a few are the reason a bug got found.

Prior art — the same idea, done before

  • so_utilTheOfficialFloW (Andy Nguyen). The original ELF-loading technique this whole scene rests on, first used to run Android games natively on PS Vita.
  • Rinnegatamante, Bythos, frangarcj, CBPS and the wider Vita homebrew scene — who extended it across 45+ Android-to-Vita ports.
  • max_nx — the direct Switch precedent, running Max Payne Mobile's real Android binary natively. Two concrete techniques came from reading it: a preflight check that the JIT syscalls exist before attempting a load, and writing a distinctive poison address into unresolved imports so reaching one crashes unmistakably instead of as an ordinary null dereference.
  • ffd_nx — confirmation the approach generalises past a single game.

Switch platform

  • switchbrew wiki — the reverse-engineered documentation of Horizon OS. Syscall semantics, memory-type tables, NRO layout and the application-record model behind forwarders all come from here.
  • libnx — beyond using it, reading it settles questions. Disassembling its exception entry stub is how we learned that svcReturnFromException runs before the user handler, which ended an entire line of investigation into a loading hang.
  • devkitPro / devkitA64 — toolchain and portlibs.
  • Atmosphère — the CFW everything runs under, and the crash-report format used for post-mortems.
  • libtesla and nx-ovlloader — the overlay runtime. Reading libtesla is how we found an overlay can reposition its own layer, which is what lets an install card sit over a HOME-menu tile instead of in a sidebar.

Android

  • AOSP — the binary XML and resources.arsc formats the APK parser implements, and bionic, whose libc surface the shim table is written against.
  • Material Design 3 — colour roles, the tonal-palette model, and the shape and motion scales the launcher is built to.
  • Google Fonts — Roboto Flex and Material Symbols Rounded, bundled under the OFL.

Libraries and tools

  • SDL2, SDL2_ttf, SDL2_image; minizip/zlib; libcurl over the Switch ssl sysmodule; stb.
  • newlib and its dlmalloc-derived allocator — whose chunk layout and sysmalloc behaviour had to be understood in detail to diagnose the Brain It On! fault. The boundary-tag invariant the heap checker validates comes straight from that design.
  • GNU binutils — objdump, readelf and nm. Most of the real diagnosis in this project has been reading AArch64 disassembly, of game binaries and of our own, rather than adding logging.

Loading credits…

Written with Claude

Nearly all of Viridite's code was written by Claude, Anthropic's AI, working against problems found on real hardware — the ELF loader, the JNI/Bionic shim, the audio backend, this website. A human sets direction, runs every build on an actual Switch, and decides what ships. That split is the project, not a footnote to it, so it belongs on this page rather than only in the README.

(function () { // Each source contributes a ROLE to a person, rather than its own section. // The same person working across four repos is one human who did four // things, not four separate credits. var SOURCES = [ { repo: "Viridite", role: "Launcher", cls: "team-cores" }, { repo: "VNX-Translation-Core", role: "Core", cls: "team-cores" }, { repo: "website", role: "Website", cls: "team-website" }, { repo: "compat-reports", role: "Infra", cls: "team-other" }, ]; var TESTER_ROLE = { role: "Testing", cls: "team-testing" }; // Automated accounts (github-actions[bot], dependabot[bot], …) appear in the // contributors API but have no user page, so their avatar 404s. They aren't // people to credit anyway. function isBot(login) { return /\[bot\]$/i.test(login) || login === "github-actions"; } var people = Object.create(null); // GitHub logins are case-insensitive, and the tester name is a free-text // field on the submission form (optional and unverified), so "Aaronateataco" // typed there and "aaronateataco" from the API are one person and must not // become two cards. Key on the lowercased login; let the contributors API, // which returns the canonical spelling, decide how the name is displayed. function record(login, role, cls, commits, authoritative) { if (login == null) return; login = String(login).trim(); if (!login || isBot(login)) return; var key = login.toLowerCase(); var p = people[key] || (people[key] = { login: login, authoritative: false, commits: 0, roles: [], seen: Object.create(null) }); if (authoritative && !p.authoritative) { p.login = login; p.authoritative = true; } p.commits += commits || 0; if (!p.seen[role]) { p.seen[role] = true; p.roles.push({ role: role, cls: cls }); } } function el(tag, cls, text) { var n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; } function cardFor(p) { var a = el("a", "credit-card"); a.href = "https://github.com/" + encodeURIComponent(p.login); a.target = "_blank"; a.rel = "noopener"; var img = document.createElement("img"); img.src = "https://github.com/" + encodeURIComponent(p.login) + ".png?size=104"; img.alt = ""; img.loading = "lazy"; img.decoding = "async"; img.width = 52; img.height = 52; img.onerror = function () { var f = el("span", "credit-avatar-fallback", (p.login[0] || "?").toUpperCase()); f.setAttribute("aria-hidden", "true"); if (img.parentNode) img.parentNode.replaceChild(f, img); }; a.appendChild(img); var body = el("div", "credit-body"); body.appendChild(el("span", "credit-name", p.login)); if (p.commits > 0) { body.appendChild(el("span", "credit-meta", p.commits === 1 ? "1 commit" : p.commits.toLocaleString() + " commits")); } var roles = el("div", "credit-roles"); p.roles.forEach(function (r) { roles.appendChild(el("span", "pill team-pill " + r.cls, r.role)); }); body.appendChild(roles); a.appendChild(body); return a; } function fetchJson(url) { return fetch(url).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); } var jobs = SOURCES.map(function (s) { return fetchJson("https://api.github.com/repos/Viridite/" + s.repo + "/contributors?per_page=100") .then(function (list) { if (!Array.isArray(list)) return false; list.forEach(function (c) { record(c.login, s.role, s.cls, c.contributions, true); }); return true; }); }); jobs.push( fetchJson("https://raw.githubusercontent.com/Viridite/compat-reports/main/data/games.json?v=" + Date.now()) .then(function (games) { if (!Array.isArray(games)) return false; games.forEach(function (g) { record(g.submitted_by, TESTER_ROLE.role, TESTER_ROLE.cls, 0, false); }); return true; }) ); Promise.all(jobs).then(function (results) { var reel = document.getElementById("reel"); var wrap = document.getElementById("reel-wrap"); var hint = document.getElementById("credit-hint"); var list = Object.keys(people).map(function (k) { return people[k]; }); if (!list.length) { // Distinguish "nobody yet" from "GitHub wouldn't answer" — the // unauthenticated API allows 60 requests/hour per IP, so a visitor who // reloads a few times can genuinely hit the ceiling. var anyOk = results.some(Boolean); reel.innerHTML = ""; reel.appendChild(el("p", null, anyOk ? "No contributors to show yet." : "Couldn't reach GitHub just now (its API rate-limits anonymous requests). Try again in a few minutes.")); reel.firstChild.style.textAlign = "center"; reel.firstChild.style.color = "var(--text-mute)"; return; } // Most commits first; testers with no commits sort alphabetically after. list.sort(function (a, b) { if (b.commits !== a.commits) return b.commits - a.commits; return a.login.toLowerCase().localeCompare(b.login.toLowerCase()); }); reel.innerHTML = ""; var group = el("div", "credit-group"); group.appendChild(el("h3", null, list.length === 1 ? "Contributor" : "Contributors")); group.appendChild(el("p", "credit-note", "Counted once each, with every area they've worked on.")); var grid = el("div", "credit-grid"); list.forEach(function (p) { grid.appendChild(cardFor(p)); }); group.appendChild(grid); reel.appendChild(group); hint.textContent = list.length === 1 ? "One person so far — want to be on this list? Contributions and test reports both count." : list.length + " people. Contributions and test reports both count."; // Only turn this into a rolling-credits reel once there's genuinely more // than fits, and never for visitors who've asked for reduced motion. var reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (reduceMotion) return; wrap.classList.add("is-scrolling"); if (reel.scrollHeight <= reel.clientHeight + 4) { wrap.classList.remove("is-scrolling"); // fits — leave it static return; } var paused = false, resumeTimer = null, manuallyPaused = false; function nudge() { if (manuallyPaused) return; paused = true; clearTimeout(resumeTimer); resumeTimer = setTimeout(function () { paused = false; }, 4000); } reel.addEventListener("wheel", nudge, { passive: true }); reel.addEventListener("touchstart", nudge, { passive: true }); reel.addEventListener("mouseenter", function () { if (!manuallyPaused) paused = true; }); reel.addEventListener("mouseleave", function () { if (!manuallyPaused) paused = false; }); // WCAG 2.2.2: motion that runs longer than five seconds needs a way to // stop it that doesn't depend on hovering — keyboard users can't hover. var btn = document.getElementById("reel-pause"); btn.addEventListener("click", function () { manuallyPaused = !manuallyPaused; paused = manuallyPaused; btn.setAttribute("aria-pressed", manuallyPaused ? "true" : "false"); btn.textContent = manuallyPaused ? "Resume scrolling" : "Pause scrolling"; }); setInterval(function () { if (paused) return; var atBottom = reel.scrollTop + reel.clientHeight >= reel.scrollHeight - 1; reel.scrollTop = atBottom ? 0 : reel.scrollTop + 1; }, 40); }); })();