Skip to content

Render Pipeline ​

The render pipeline handles the full process of transforming an .osr replay file into a rendered .mp4 video. It supports two execution paths: Modal GPU (production) and Local danser (development).

Execution Paths ​

Pipeline Stages ​

Both paths share the same core pipeline (src/core/render_pipeline.py):

1. Directory Setup ​

Creates the osu! directory structure and symlinks required by danser-go:

~/.osu/Songs → /mnt/osu_data/Songs (or /tmp/osu_data/Songs)
~/.osu/Skins → /mnt/osu_data/Skins

2. Replay Download ​

Downloads the .osr file from S3/R2 to a temporary directory.

3. Beatmap Resolution ​

Beatmap mirror fallback order:

  1. https://api.nerinyan.moe/d/{set_id}
  2. https://osu.direct/api/d/{set_id}
  3. https://catboy.best/d/{set_id}

4. Skin Download ​

If a non-default skin is specified, it's downloaded from S3 and extracted with zip bomb protection (max 1 GB uncompressed, path traversal checks).

5. Danser Rendering ​

The core rendering uses xvfb-run (virtual framebuffer) with danser-go:

bash
xvfb-run -a -s "-screen 0 1920x1080x24 +extension GLX +render -noreset" \
  danser-cli \
  -replay=replay.osr \
  -skin=Default \
  -sPatch=settings.json \
  -out=render_jobid \
  -record

6. Settings Patch ​

danser-go is configured via a JSON settings patch generated from the job config:

json
{
  "Graphics": { "Width": 1920, "Height": 1080 },
  "Gameplay": {
    "HitErrorMeter": { "Show": true },
    "KeyOverlay": { "Show": true }
  },
  "Skin": {
    "CurrentSkin": "Default",
    "UseBeatmapColors": false,
    "Cursor": { "UseSkinCursor": true, "Scale": 0.6 }
  },
  "Playfield": {
    "Background": {
      "Dim": { "Normal": 0.95 },
      "LoadStoryboards": true,
      "LoadVideos": false
    }
  },
  "Recording": {
    "MotionBlur": { "Enabled": true },
    "Encoder": "libx264"
  }
}

7. Log Streaming ​

During the render, logs are uploaded to S3 every 3 seconds so clients can monitor progress in real-time via /v1/artifacts/logs/{job_id}.log.

8. Post-Processing ​

  • Thumbnail generation: ffmpeg extracts a frame at 00:00:15
  • PP parsing: Regex extraction from danser's output table
  • Video upload: Final .mp4 uploaded to S3

9. Artifact Upload ​

All artifacts are uploaded with appropriate content types:

ArtifactS3 KeyContent-Type
Videovideos/{job_id}.mp4video/mp4
Thumbnailthumbnails/{job_id}.jpgimage/jpeg
Logslogs/{job_id}.logtext/plain

Worker Idempotency ​

The Celery worker uses an atomic status transition to prevent duplicate execution:

python
update_stmt = (
    update(Job)
    .where(Job.id == job_id, Job.status == JobStatus.QUEUED)
    .values(status=JobStatus.DOWNLOADING)
)
res = await db.execute(update_stmt)
if res.rowcount == 0:
    return "aborted"  # Another worker already claimed this

Timeouts ​

TimeoutDurationAction
Danser render600s (10 min)Process killed, job marked failed
Modal function660s (11 min)Container terminated
Celery task660s hard, 600s softTask revoked
Thumbnail ffmpeg30sProcess killed

Built with VitePress