GPU render worker for SkinHub skins: claims jobs from skinhub api, renders previews/icons/osk, uploads to staging.
  • TypeScript 92.8%
  • Shell 2.3%
  • NSIS 1.9%
  • Dockerfile 1.6%
  • JavaScript 1.2%
  • Other 0.2%
Find a file
2026-08-18 12:21:01 +00:00
.forgejo/workflows fix: self-update within the release channel, never stable to beta 2026-08-18 13:46:25 +02:00
assets feat: vendor the render assets 2026-08-11 17:55:13 +02:00
dist fix: self-update within the release channel, never stable to beta 2026-08-18 13:46:25 +02:00
installer refactor: drop pre-split cap names and pre-embed tray asset fallbacks 2026-08-17 22:53:57 +02:00
scripts feat: embed tray assets in the exe, stamp PE resources natively, hide child consoles 2026-08-11 23:42:20 +02:00
src fix: self-update within the release channel, never stable to beta 2026-08-18 13:46:25 +02:00
tools/oxlint/anti-slop fix: absorb only one transient danser boot failure per deliverable 2026-08-15 12:44:29 +02:00
.gitignore feat: skinhub icon, system tray, site-styled setup page and running-instance handling in the installer 2026-08-11 22:55:26 +02:00
.oxlintrc.json fix: absorb only one transient danser boot failure per deliverable 2026-08-15 12:44:29 +02:00
.releaserc.cjs ci: attach worker binaries to releases via the forgejo api 2026-08-11 21:13:52 +02:00
CHANGELOG.md chore(release): 1.1.0 [skip ci] 2026-08-18 12:21:01 +00:00
Dockerfile fix: absorb only one transient danser boot failure per deliverable 2026-08-15 12:44:29 +02:00
esbuild.mjs fix: absorb only one transient danser boot failure per deliverable 2026-08-15 12:44:29 +02:00
package.json chore(release): 1.1.0 [skip ci] 2026-08-18 12:21:01 +00:00
pnpm-lock.yaml fix: absorb only one transient danser boot failure per deliverable 2026-08-15 12:44:29 +02:00
pnpm-workspace.yaml feat: add the GPU render worker 2026-08-09 15:02:57 +02:00
README.md feat: run several tasks per lane on one machine 2026-08-18 13:24:43 +02:00
tsconfig.json feat: add the GPU render worker 2026-08-09 15:02:57 +02:00

skinhub-render

Render worker for SkinHub. Claims capability-typed tasks from the worker plane (gameplay = the danser clip, screenshots = the danser panel/thumbnail boots, icons = sharp mod-icon sheets, osk = zip packing), renders them, and uploads the results through the API. A job fans out server-side into per-kind chunks of a few skins each, so any number of workers can share one upload; on one machine the supervisor runs a child per capability lane (gameplay / screenshots / cpu work), each in its own toolchain root, so the three proceed at the same time.

Install

Register a worker on the site (/render-workers), have it approved, then:

  • Standalone binary (Linux/Windows): download skinhub-render-worker-<platform> from the latest release and run skinhub-render-worker --token <token>. Everything else (danser, the assets bundle, sharp) downloads on first run into a data folder next to the binary, at the versions the server pins.
  • Docker: git.sulej.net/skinhub/skinhub-render:beta (or :latest for prod), with the toolchain pre-baked. Needs --gpus all, --cap-add SYS_RESOURCE (danser's ffmpeg feed calls F_SETPIPE_SZ) and --add-host osu.ppy.sh:127.0.0.1.
  • From source: pnpm install && node dist/index.mjs (Node 22+).

Thin-worker contract

The worker is deliberately dumb. Everything tunable lives in skinhub-api's /api/render-worker/config: danser version + download URLs, the assets bundle, danser settings overrides, clip/screenshot timings, the icon-sheet layout, the sharp package pins, and the minimum worker version. The config is data only: the worker knows the shape of every invocation itself. The server refuses a worker older than its minimum at claim time (upgrade_required), and the worker idles until updated.

Trust model

The worker parses member-uploaded skin bytes with danser, sharp and ffmpeg, so the platform treats it as hostile and gives it as little authority as possible:

  • No storage credential. Inputs stream through skinhub-api (only oids confirmed to belong to the task); outputs go to the upload route, which derives the staging key server-side for exactly the slots the task announced.
  • No database access, no session secret, no Forgejo token.
  • One task per process. It exits when the task finishes and the orchestrator restarts it, so a compromise cannot persist into another user's render.
  • Untrusted output. skinhub-api re-validates everything the worker uploads (path shape, size cap, magic bytes) and stores it under a key derived from a hash skinhub-api computes itself.

Config

CLI flags win over env vars.

Flag Env Default Purpose
--token RENDER_WORKER_TOKEN - Worker-plane bearer (required)
--api SKINHUB_API_URL https://skins.sulej.net API origin
--caps WORKER_CAPS gameplay,screenshots,icons,osk Task kinds to advertise
--encoder RENDER_ENCODER nvenc cpu / nvenc / qsv
- SKINHUB_DATA_DIR next to binary / data/ danser + assets + sharp cache
- SKINHUB_POLL_SECONDS 5 Idle poll interval
- SKINHUB_LANE_SLOTS server lane_slots Concurrent tasks per lane, e.g. gameplay=1,osk=4
- SKINHUB_TASK_TIMEOUT_MS server params.task_timeout_secs (600s) Hard ceiling on one task; overrides the server value

The gameplay and screenshots capabilities need a GPU (NVIDIA reservation with capabilities: [gpu, compute, video, graphics] in Docker); icons and osk run anywhere. A machine where sharp cannot load automatically drops the screenshots and icons capabilities.

Development

pnpm install
pnpm run check   # typecheck + test + build (dist/ is committed)

semantic-release cuts releases from beta (prereleases) and from main (stable, starting at the prod promotion); CI attaches the SEA binaries to each release and bumps the deploy pin.