Skip to content

MilkDrop: preset thumbnails in a grid (#575), with engine buffer and light sampler fixes - #608

Merged
CaYatur merged 4 commits into
mainfrom
milkdrop/thumbnails-575
Sep 22, 2026
Merged

CaYatur merged 4 commits into
mainfrom
milkdrop/thumbnails-575

Conversation

@CaYatur

@CaYatur CaYatur commented Sep 22, 2026

Copy link
Copy Markdown
Owner

#575: preset thumbnails. Two engine fixes found along the way go beyond it and are listed separately below.

Thumbnails

The MilkDrop panel's list gets a Layout choice, List or Grid. In the grid, every preset shows a thumbnail. Each thumbnail is drawn once in a hidden window and kept in userData/milkdrop-thumbs/.

  • Recipe. Measured on a seeded 200-preset sample of the corpus.
    • Drawing starts from black buffers and runs 60 frames of the demo sound at 1/30 s. It is drawn at 640x360 and scaled to a 256x144 WebP of about 3.6 KB.
    • I tried a fixed seed image instead: a builtin preset for 45 frames, then a hard cut. It cut black thumbnails only from 20 to 18, made each job 45% longer, and painted its own colours into presets that draw nothing like it.
    • More frames did not help: 90 frames gave 20 black and 150 gave 22.
    • Drawing at 640x360 instead of 320x180 barely changed the time (250 ms vs 259 ms median).
  • Same bytes every time.
    • Each thumbnail uses a new engine instance, whose context is released on dispose.
    • Math.random and the preset seed are derived from the key.
    • Requested textures are waited for. A thumbnail whose texture does not arrive, or whose frames were not all drawn on a live context, is not written.
    • A crashed drawing process is replaced.
    • Checked in the real page:
      • 7 of 7 presets gave the same bytes after two different busy predecessors: the five busiest of 40, a textured one, and one using sampler_randNN.
      • Under load, one preset drawn 48 times gave the same bytes every time.
  • Key. A hash of the recipe (the bytes of every file the page loads, plus the sizes and frame count), the preset source, and a signature of the textures it uses.
    • An engine change retires every thumbnail by itself.
    • An edited preset or a changed texture gets a new thumbnail.
    • Files that no current preset produces are deleted once per session.
  • Panel.
    • The panel requests the visible cells. Cached thumbnails come back at once; the rest arrive as they are drawn, newest request first.
    • A waiting cell is striped and a failed one has a dashed frame, so neither looks like a thumbnail that is really black.
    • Images come through an sv-thumb: protocol that accepts only a key.
    • The hidden window closes when idle and when the panel closes.
  • Measured.
    • First thumbnail after 1.0 s; the 20 visible cells were full in 10–17 s.
    • With a visualizer window drawing MilkDrop at 75 Hz: frame-time p99 was 13.5 ms idle vs 13.6 ms while drawing thumbnails. The longest frame was 17.2 ms vs 26.7 ms, and no frame took over 33 ms.
    • Asking for 40 cached thumbnails took 2.9–4.2 ms.
  • Smoke. The smoke test draws a builtin's thumbnail through the whole chain into a temporary folder: the recipe files read from the package, the hidden window, the protocol and the panel's CSP.

Found on the way

  • A picture from another window in a new buffer (engine).
    • Render targets were allocated empty and cleared with gl.clear.
    • With another MilkDrop context drawing on the same GPU and the CPU busy, a freshly allocated feedback buffer could still hold that context's picture, and feedback grew it. A preset whose thumbnail is black came out as another window's starburst in 13 of 24 draws.
    • Neither condition alone caused it: 18 of 18 were clean with each one.
    • Targets are now allocated from a shared zero buffer that is never written. The same conditions gave 0 of 48.
    • Drawing alone is unchanged: 200 presets gave identical results before and after.
    • Cost: only when buffers are allocated. A full rebuild at 1080p takes 8.6 ms instead of 3.3 ms.
    • Live windows (a new window, a resize, a restored context) and the video export were exposed too.
  • Light colours from MilkDrop (MilkDrop: drive the lights from the colours MilkDrop draws #589).
    • The sampler read its canvas with getImageData about 30 times a second, and Chromium warned about it. The smoke test caught the warning because lights in the current settings take MilkDrop's colours.
    • The downscale now stays on the GPU; only the 4 KB result is read back, from a canvas made for reading.
  • Automation never drives physical lights.
    • The smoke test and the screenshot tool run on the user's own profile. With Dynamic Lighting on, they drove the lights at start-up. The smoke test also turns OpenRGB on in the panel.
    • As with the camera, in these runs:
      • every Dynamic Lighting setting is sent switched off;
      • the device scan is skipped;
      • OpenRGB and Art-Net are never started.
    • --diag still drives the lights.
  • Smaller fixes.

Not done

  • Thumbnails in the web remote and in the list view.
  • Drawing every thumbnail ahead of time.
  • Thumbnails show the first two seconds and use the engine defaults.

Checks

  • npm test: 2108/2108.
  • New tests pass with process.platform set to Linux.
  • npm run smoke: PASS. User-data checksums were unchanged.
  • 57 of 58 mutations are caught. The survivor is an equivalent check.

Refs #575, #589, #560

The MilkDrop panel's list gets a Layout choice, List or Grid. In the grid
every preset shows a thumbnail, drawn once in a hidden window and kept in
the user data.

- Recipe measured on a seeded 200-preset sample of the corpus: a black
  start, 60 frames of the demo sound at 1/30 s, drawn at 640x360 and
  scaled to a 256x144 WebP. A fixed seed image cut black thumbnails only
  from 20 to 18, took 45% longer and painted its colours into presets
  that draw nothing like it; more frames did not help.
- Each thumbnail is drawn by a new engine instance from black buffers,
  with Math.random and the preset seed taken from the key, and waits for
  the textures it asks for; the same preset gives the same bytes whatever
  was drawn before it. A texture that does not arrive is not written.
- The key hashes the files the thumbnail page loads, the preset source
  and the textures it uses, so an engine change, an edited preset or a
  changed texture gets a new thumbnail. Unused files are deleted once per
  session.
- The panel asks for the visible cells; cached thumbnails come back at
  once, the rest as they are drawn, newest request first. Waiting and
  failed cells look different from a thumbnail that is really black.
  Images come through an sv-thumb: protocol that accepts only a key.
- The hidden window closes when idle and when the panel closes.
- The smoke test draws a builtin preset's thumbnail through the whole
  chain into a temporary folder.

Found on the way:
- sampler_randNN picked from the texture list in file-system order,
  which differs between Windows and Linux; the list is now sorted.
- The importer did not strip filter/wrap prefixes from sampler names, so
  a preset asking for sampler_fw_worms did not bring worms.jpg along.
- The builtin presets module now gives its list to Node too.

Refs #575, #560
…ll canvas

Render targets were allocated empty and cleared with gl.clear. With
another MilkDrop context drawing on the same GPU and the CPU busy, a
freshly allocated feedback buffer could still hold that context's
picture, and the feedback loop grew it: a preset whose first two seconds
are black came out as the starburst another window was showing, in 13 of
24 draws. Neither the other window nor the CPU load alone did it. Targets
are now allocated from a shared zero buffer that is never written (8 MB
at 1080p) and the clear stays; the same conditions gave 0 of 48, and 200
presets drawn alone gave identical results before and after. Any window
whose buffers are allocated again (a new window, a resize, a restored
context) and the video export were exposed too.

The light colour sampler (#589) read its 64x16 canvas with getImageData
about 30 times a second; Chromium warned that the canvas should be made
for frequent reading, and the smoke test counts that warning as an
error. The downscale stays on the GPU canvas and only the small result
is copied into a second canvas made for reading, so 4 KB come back
instead of the whole frame.

Refs #575, #589, #560
- A thumbnail is written only if every frame was drawn on a live context
  (the engine's frame counter matches the recipe, no loss or recovery).
  With no context the engine draws a placeholder and after a loss it
  keeps the last frame; either would be kept for good under a content
  key.
- If the hidden drawing process crashes, its window is destroyed and the
  job fails at once instead of every later cell waiting for the 30 s
  limit.
- The panel tests poll for their condition instead of racing the 80 ms
  batching timer, and a panel's leftover timer no longer leaks into the
  next test.
- Docs: the order check with busy presets (7 of 7, textured and
  sampler_randNN included), the load check, the IPC cost, and that
  thumbnails use the engine defaults.

Refs #575, #560
The smoke test and the screenshot tool run on the user's own profile.
With Dynamic Lighting on in the settings, the app drove the lights at
start-up in those runs too, and the smoke test itself turns OpenRGB on in
the panel, which would reach a running OpenRGB server; Art-Net would send
to the network. As with the camera, which automation never opens, the
physical outputs are now off in these runs: every Dynamic Lighting
setting goes through one function that sends it switched off, the device
scan is skipped, and OpenRGB and Art-Net are never started. Diagnostics
(--diag) still drives the lights.

Docs: the light sampler fix, this guard, the cost of zero-filled targets
(a full rebuild at 1080p 8.6 ms instead of 3.3 ms) and the test totals.

Refs #575, #560
@CaYatur
CaYatur merged commit f91068d into main Sep 22, 2026
5 checks passed
@CaYatur
CaYatur deleted the milkdrop/thumbnails-575 branch September 30, 2026 19:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant