Documentation/Source example
Example source

samples/vis/Shader - MilkDrop ALL.js

// MilkDrop preset browser. Drop a four-digit .js file into the adjacent
// MilkDrop directory, then reload this sample to discover it automatically.
// Each preset is a commented JavaScript factory returning the ordinary
// CreateAsync source/renderer objects. History normally advances on each Draw;
// presets may request a fixed feedbackIntervalMs for cadence-sensitive effects.
// Preset files are executable JavaScript: load only trusted local files.
// Presets use the component's O3 shader setting. These ports also require
// source b0 float #5 = frameStep (new source ABI), so FPS changes can reuse
// the same visualization, including its history and accumulated state.
// Compile time is CreateAsync wall time (including cache/task overhead);
// Draw CPU is the visual.Draw() call duration, not GPU execution time.
window.DrawMode = 1;

// For recieiving all keys
window.DlgCode = 0x0004;

const FPS_OPTIONS = [30, 60, 120];
// 30 Hz is the port reference; repaint requests are capped at 1000 FPS.
const MIN_FPS = 30, MAX_FPS = 1000;
const CUSTOM_FPS_Y = 99;
// D3DCompile optimization bits. O3 remains fixed now that the selector is gone.
const OPTIMIZATION_MASK = 0xC000;
const OPTIMIZATION_LEVEL3 = 0x8000;
const COMPILE_INDICATOR_DELAY_MS = 150;
// InjectFeedback accepts at most 4096x4096 and 4 Mi pixels.
const MAX_CAPTURE_SIDE = 4096;
const MAX_CAPTURE_PIXELS = 4 * 1024 * 1024;
const SIDEBAR_MAX = 228;
const SIDEBAR_MIN = 170;
const ROW_HEIGHT = 38;
const AUTO_SWITCH_INTERVAL_MS = 16000;
const TRACK_NOTICE_DURATION_MS = 2000;
const AUTO_SWITCH_BUTTON_Y = 155;
const PRESET_ORDER_BUTTON_Y = 191;
const AUTO_HIDE_PLAYLIST_BUTTON_Y = 227;
const PRESETS_LABEL_Y = 271;
const LIST_TOP = 295;
const FOOTER_HEIGHT = 182;
const PLAYLIST_ROW_HEIGHT = 32;
const PLAYLIST_MARGIN = 28;
const PLAYLIST_SCROLLBAR_WIDTH = 9;

const C = {
    background: 0xff080b10,
    sidebar:    0xff171c25,
    panel:      0xff222a35,
    hover:      0xff2b3642,
    border:     0xff394451,
    text:       0xffe5edf5,
    muted:      0xff92a3b3,
    faint:      0xff697987,
    accent:     0xffffc66d,
    accentText: 0xff1b1c1e,
    error:      0xffff857d
};

const fontTitle = gdi.Font('Segoe UI', 16);
const fontMain = gdi.Font('Segoe UI', 12);
const fontSmall = gdi.Font('Segoe UI', 10);
const fontRow = gdi.Font('Segoe UI', 12);
const fontCompile = gdi.Font('Segoe UI Semibold', 28);
const fontCompileSmall = gdi.Font('Segoe UI', 13);
const fontPlaylistTitle = gdi.Font('Segoe UI Semibold', 17);
const fontPlaylistHeader = gdi.Font('Segoe UI Semibold', 12);
const fontPlaylistRow = gdi.Font('Segoe UI', 14);
const fontTrackArtist = gdi.Font('Segoe UI Semibold', 34);
const fontTrackTitle = gdi.Font('Segoe UI', 30);

// The absolute fallback also works when a sample is pasted into the script
// editor rather than selected as a file-backed sample.
const scriptPath = window.ScriptInfo && window.ScriptInfo.Path;
const scriptDir = scriptPath
    ? scriptPath.replace(/[^\\/]+$/, '')
    : fb.ComponentPath.replace(/[\\/]+$/, '') + '\\samples\\vis\\';
const presetDir = scriptDir + 'MilkDrop\\';

// include() evaluates a preset in this panel's realm. A global registry makes
// these factories visible without evaluating the HLSL or using eval().
var MilkDropPresets = Object.create(null);

const presets = utils.Glob(presetDir + '*.js')
    .map(path => {
        const match = /(?:^|[\\/])(\d{4})\.js$/i.exec(path);
        return match ? {id: match[1], path} : null;
    })
    .filter(Boolean)
    .sort((a, b) => a.id.localeCompare(b.id));

const oldFps = Number(window.GetProperty('MilkDrop.FPS', 60));
let fps = validFps(oldFps) ? oldFps : 60;
let fpsText = String(fps);
let fpsEditing = false;
let fpsSelectAll = false;
let fpsCaret = fpsText.length;
let fpsInputError = false;
let autoSwitchEnabled = !!window.GetProperty('MilkDrop.AutoSwitch', false);
let randomPresetOrder = !!window.GetProperty('MilkDrop.RandomOrder', false);
let autoHidePlaylist = !!window.GetProperty('MilkDrop.AutoHidePlaylist', true);
const oldId = String(window.GetProperty('MilkDrop.Preset', ''));
let currentIndex = Math.max(0, presets.findIndex(item => item.id === oldId));
let visual = null;
// The visible preset can differ from the selection while CreateAsync runs.
let visualPresetId = null;
// Re-evaluate only the cadence of an opted-in preset when display FPS changes.
let visualFeedbackCadence = null;
let status = 'idle';
let errorText = '';
let requestId = 0;
let repaintTimer = null;
let autoSwitchTimer = null;
let scrollY = 0;
let initialScrollPending = true;
let mouseX = -1;
let mouseY = -1;
let hoverTarget = '';
let draggingThumb = false;
let thumbGrab = 0;
let draggingPlaylistThumb = false;
let playlistThumbGrab = 0;
let shaderCacheSnapshot = null;
let shaderCacheSampleTime = 0;
let compilingLabel = '';
let compileStartedAt = 0;
let compileIndicatorShown = false;
let lastCompileMs = null;
let drawCpuAvgMs = null;
let paintRate = null;
let paintWindowStartedAt = performance.now();
let paintWindowFrames = 0;
let playlistVisible = false;
let playlistIndex = -1;
let playlistName = '';
let playlistRows = [];
let playlistScroll = 0;
let playlistFocusIndex = -1;
let suppressDrawingClickUntil = 0;
let trackNoticeArtist = '';
let trackNoticeTitle = '';
let trackNoticeVisible = false;
let trackNoticeTimer = null;

const playlistArtistFormat = fb.TitleFormat('$if2(%artist%,)');
const playlistTitleFormat = fb.TitleFormat('$if2(%title%,)');
const playlistLengthFormat = fb.TitleFormat('$if2(%length%,)');
const trackArtistFormat = fb.TitleFormat('$if2(%artist%,Unknown artist)');
const trackTitleFormat = fb.TitleFormat('$if2(%title%,Untitled)');

// ShaderFlags is panel-local; preserve unrelated bits and keep this sample at O3.
function setDefaultOptimizationFlags() {
    if (typeof vis.ShaderFlags !== 'number') return;
    try {
        vis.ShaderFlags = (vis.ShaderFlags & ~OPTIMIZATION_MASK) |
            OPTIMIZATION_LEVEL3;
    } catch (e) {
        // Older builds can still use their default compiler flags.
        console.log('MilkDrop optimizer: ' + String(e));
    }
}

function resetFrameMetrics() {
    drawCpuAvgMs = null;
    paintRate = null;
    paintWindowFrames = 0;
    paintWindowStartedAt = performance.now();
}

function recordDrawCpuTime(elapsedMs) {
    // Exponentially smoothed CPU time inside visual.Draw(); NOT GPU frame time.
    drawCpuAvgMs = drawCpuAvgMs === null ? elapsedMs :
        drawCpuAvgMs * 0.9 + elapsedMs * 0.1;
    ++paintWindowFrames;
    const now = performance.now();
    const interval = now - paintWindowStartedAt;
    if (interval >= 1000) {
        // Actual on_paint callback rate; not guaranteed display/GPU FPS.
        paintRate = paintWindowFrames * 1000 / interval;
        paintWindowFrames = 0;
        paintWindowStartedAt = now;
    }
}

function formatTime(ms) {
    return ms < 1000 ? ms.toFixed(1) : ms.toFixed(0);
}

// The native HLSL -> DXBC LRU is process-wide, not local to this panel.
// Its defaults are 128 entries and 16 MiB; inspect without overriding them.
function cacheStats() {
    const now = Date.now();
    if (!shaderCacheSnapshot || now - shaderCacheSampleTime >= 400) {
        shaderCacheSnapshot = vis.ShaderCacheStats;
        shaderCacheSampleTime = now;
    }
    return shaderCacheSnapshot;
}

function clamp(value, lo, hi) {
    return Math.max(lo, Math.min(hi, value));
}

function layout() {
    const w = window.Width, h = window.Height;
    const side = clamp(Math.round(w * 0.31), SIDEBAR_MIN,
        Math.min(SIDEBAR_MAX, Math.max(0, w - 80)));
    const listBottom = Math.max(LIST_TOP, h - FOOTER_HEIGHT);
    return {
        sidebar: side,
        viewX: side + 1,
        viewWidth: Math.max(0, w - side - 1),
        listX: 8,
        listWidth: Math.max(0, side - 24),
        listTop: LIST_TOP,
        listHeight: Math.max(0, listBottom - LIST_TOP),
        trackX: Math.max(0, side - 11),
        trackWidth: 4
    };
}

function sidebarWideButton(r, y) {
    return {x: 10, y, w: Math.max(0, r.sidebar - 20), h: 30};
}

function autoSwitchButton(r) {
    return sidebarWideButton(r, AUTO_SWITCH_BUTTON_Y);
}

function presetOrderButton(r) {
    return sidebarWideButton(r, PRESET_ORDER_BUTTON_Y);
}

function autoHidePlaylistButton(r) {
    return sidebarWideButton(r, AUTO_HIDE_PLAYLIST_BUTTON_Y);
}

function playlistOverlayLayout(x, y, w, h) {
    if (w < 120 || h < 120) return null;
    const margin = Math.max(10, Math.min(PLAYLIST_MARGIN,
        Math.floor(w / 12), Math.floor(h / 12)));
    const boxW = Math.max(1, w - margin * 2);
    const boxH = Math.max(1, h - margin * 2);
    const pad = 16;
    const titleHeight = 43;
    const headerHeight = 28;
    const bodyY = y + margin + titleHeight + headerHeight;
    const bodyHeight = Math.max(0, boxH - titleHeight - headerHeight - 10);
    const visibleRows = Math.max(0, Math.floor(bodyHeight / PLAYLIST_ROW_HEIGHT));
    const contentWidth = Math.max(0, boxW - pad * 2);
    const lengthWidth = Math.min(72, Math.max(48, Math.floor(contentWidth * 0.12)));
    const textWidth = Math.max(0, contentWidth - lengthWidth - 24);
    const artistWidth = Math.floor(textWidth * 0.36);
    return {
        x: x + margin,
        y: y + margin,
        w: boxW,
        h: boxH,
        pad,
        titleHeight,
        headerHeight,
        bodyY,
        bodyHeight,
        visibleRows,
        artistX: x + margin + pad,
        artistWidth,
        titleX: x + margin + pad + artistWidth + 12,
        titleWidth: Math.max(0, textWidth - artistWidth - 12),
        lengthX: x + margin + boxW - pad - lengthWidth,
        lengthWidth
    };
}

function activePlaylistOverlayLayout() {
    const r = layout();
    return playlistOverlayLayout(r.viewX, 0, r.viewWidth, window.Height);
}

function playlistMaxScroll(info = activePlaylistOverlayLayout()) {
    return info ? Math.max(0, playlistRows.length - info.visibleRows) : 0;
}

function playlistScrollbar(info = activePlaylistOverlayLayout()) {
    const scrollRange = playlistMaxScroll(info);
    if (!info || !info.visibleRows || scrollRange <= 0) return null;
    const trackHeight = info.visibleRows * PLAYLIST_ROW_HEIGHT;
    const thumbHeight = Math.min(trackHeight, Math.max(24,
        trackHeight * info.visibleRows / playlistRows.length));
    const range = Math.max(0, trackHeight - thumbHeight);
    const first = clamp(playlistScroll, 0, scrollRange);
    return {
        x: info.x + info.w - PLAYLIST_SCROLLBAR_WIDTH - 5,
        y: info.bodyY,
        w: PLAYLIST_SCROLLBAR_WIDTH,
        h: trackHeight,
        thumbY: info.bodyY + range * first / scrollRange,
        thumbHeight,
        range,
        scrollRange
    };
}

function normalizePlaylistScroll(info = activePlaylistOverlayLayout()) {
    playlistScroll = clamp(Math.floor(playlistScroll), 0, playlistMaxScroll(info));
}

function refreshActivePlaylist(resetScroll = false) {
    const nextIndex = Number(plman.ActivePlaylist);
    playlistIndex = Number.isInteger(nextIndex) && nextIndex >= 0 &&
        nextIndex < plman.PlaylistCount ? nextIndex : -1;
    playlistName = playlistIndex >= 0 ? plman.GetPlaylistName(playlistIndex) : '';
    playlistRows = [];
    playlistFocusIndex = playlistIndex >= 0
        ? plman.GetPlaylistFocusItemIndex(playlistIndex)
        : -1;

    if (playlistIndex >= 0) {
        const handles = plman.GetPlaylistItems(playlistIndex);
        const artists = playlistArtistFormat.EvalWithMetadbs(handles);
        const titles = playlistTitleFormat.EvalWithMetadbs(handles);
        const lengths = playlistLengthFormat.EvalWithMetadbs(handles);
        for (let i = 0; i < handles.Count; ++i) {
            playlistRows.push({
                index: i,
                artist: artists[i] || '(unknown artist)',
                title: titles[i] || '(untitled)',
                length: lengths[i] || '—'
            });
        }
    }

    if (resetScroll) {
        const info = activePlaylistOverlayLayout();
        playlistScroll = playlistFocusIndex >= 0 && info
            ? playlistFocusIndex - Math.floor(info.visibleRows / 2)
            : 0;
    }
    normalizePlaylistScroll();
    if (playlistVisible) window.Repaint();
}

function showActivePlaylist() {
    refreshActivePlaylist(true);
    playlistVisible = true;
    window.Repaint();
}

function playlistRowAt(x, y, info = activePlaylistOverlayLayout()) {
    if (!playlistVisible || !info || x < info.x || x >= info.x + info.w ||
        y < info.bodyY || y >= info.bodyY + info.visibleRows * PLAYLIST_ROW_HEIGHT)
        return -1;
    const visibleIndex = Math.floor((y - info.bodyY) / PLAYLIST_ROW_HEIGHT);
    const index = playlistScroll + visibleIndex;
    return index >= 0 && index < playlistRows.length ? index : -1;
}

function revealPlaylistFocus(index) {
    playlistFocusIndex = Number.isInteger(index) ? index : -1;
    const info = activePlaylistOverlayLayout();
    if (playlistFocusIndex >= 0 && info && info.visibleRows > 0) {
        if (playlistFocusIndex < playlistScroll)
            playlistScroll = playlistFocusIndex;
        else if (playlistFocusIndex >= playlistScroll + info.visibleRows)
            playlistScroll = playlistFocusIndex - info.visibleRows + 1;
    }
    normalizePlaylistScroll(info);
    if (playlistVisible) window.Repaint();
}

function maxScroll(r) {
    return Math.max(0, presets.length * ROW_HEIGHT - r.listHeight);
}

function setScroll(value, r = layout()) {
    scrollY = clamp(value, 0, maxScroll(r));
    window.Repaint();
}

function scrollToSelected(r = layout()) {
    const top = currentIndex * ROW_HEIGHT;
    const bottom = top + ROW_HEIGHT;
    if (top < scrollY) scrollY = top;
    if (bottom > scrollY + r.listHeight) scrollY = bottom - r.listHeight;
    scrollY = clamp(scrollY, 0, maxScroll(r));
}

// At script startup the panel can still have a zero-sized viewport.
// Do not scroll to the saved preset until the list has a real height.
// Later resizes only clamp the existing position, preserving manual scroll.
function normalizeScroll(r = layout()) {
    if (window.Width <= 0 || window.Height <= 0 || r.listHeight <= 0) return;
    if (initialScrollPending) {
        scrollToSelected(r);
        initialScrollPending = false;
    } else {
        scrollY = clamp(scrollY, 0, maxScroll(r));
    }
}

function thumb(r) {
    const content = presets.length * ROW_HEIGHT;
    const max = maxScroll(r);
    if (!r.listHeight || !max) return null;
    const height = Math.max(22, r.listHeight * r.listHeight / content);
    const range = Math.max(0, r.listHeight - height);
    return {
        x: r.trackX,
        y: r.listTop + range * (scrollY / max),
        w: r.trackWidth,
        h: Math.min(height, r.listHeight),
        range
    };
}

function compileMessage() {
    return compilingLabel || 'Compiling preset...';
}

function drawCompileOverlay(gr, x, y, w, h, message = compileMessage()) {
    if (w <= 0 || h <= 0) return;
    const boxW = Math.min(w - 36, Math.max(240, Math.round(w * 0.48)));
    const boxH = 92;
    const boxX = x + Math.max(18, Math.floor((w - boxW) / 2));
    const boxY = y + Math.floor((h - boxH) / 2);
    gr.FillSolidRect(boxX, boxY, boxW, boxH, 0xC0101318);
    gr.DrawRect(boxX + 0.5, boxY + 0.5, boxW - 1, boxH - 1, 1, 0xFF5B6877);
    const titleW = Math.min(boxW - 30, gr.CalcTextWidth(message, fontCompile) + 6);
    const titleX = boxX + Math.floor((boxW - titleW) / 2);
    gr.DrawString(message, fontCompile, 0xFFF2F6FA,
        titleX, boxY + 15, titleW, 34);
    const note = 'First compile may take a moment';
    const noteW = Math.min(boxW - 30, gr.CalcTextWidth(note, fontCompileSmall) + 6);
    const noteX = boxX + Math.floor((boxW - noteW) / 2);
    gr.DrawString(note, fontCompileSmall, 0xFFC2CCD5,
        noteX, boxY + 55, noteW, 20);
}

function fitText(gr, value, font, width) {
    const text = String(value || '');
    if (width <= 0) return '';
    if (gr.CalcTextWidth(text, font) <= width) return text;
    const suffix = '…';
    let lo = 0, hi = text.length;
    while (lo < hi) {
        const mid = Math.ceil((lo + hi) / 2);
        if (gr.CalcTextWidth(text.slice(0, mid) + suffix, font) <= width)
            lo = mid;
        else
            hi = mid - 1;
    }
    return text.slice(0, lo) + suffix;
}

function drawPlaylistOverlay(gr, x, y, w, h, hoveredIndex = -1) {
    const info = playlistOverlayLayout(x, y, w, h);
    if (!info) return;

    gr.FillSolidRect(info.x, info.y, info.w, info.h, 0xC8121720);
    gr.DrawRect(info.x + 0.5, info.y + 0.5, info.w - 1, info.h - 1,
        1, 0xD0758494);
    gr.FillSolidRect(info.x + 1, info.y + 1, info.w - 2,
        info.titleHeight - 1, 0xD51A222C);

    const heading = playlistIndex >= 0 && playlistName
        ? 'Active playlist — ' + playlistName
        : 'Active playlist';
    gr.DrawString(fitText(gr, heading, fontPlaylistTitle, info.w - 32),
        fontPlaylistTitle, C.text, info.x + 16, info.y + 7,
        info.w - 32, 28);

    const headerY = info.y + info.titleHeight;
    gr.FillSolidRect(info.x + 1, headerY, info.w - 2,
        info.headerHeight, 0xB8242D38);
    gr.DrawString('artist', fontPlaylistHeader, C.muted,
        info.artistX, headerY + 3, info.artistWidth, 21);
    gr.DrawString('title', fontPlaylistHeader, C.muted,
        info.titleX, headerY + 3, info.titleWidth, 21);
    gr.DrawString('length', fontPlaylistHeader, C.muted,
        info.lengthX, headerY + 3, info.lengthWidth, 21);

    if (playlistIndex < 0 || !playlistRows.length || !info.visibleRows) {
        const message = playlistIndex < 0
            ? 'No active playlist'
            : 'The active playlist is empty';
        gr.DrawString(message, fontMain, C.muted,
            info.artistX, info.bodyY + 14, info.w - info.pad * 2, 30);
        return;
    }

    const first = clamp(playlistScroll, 0,
        Math.max(0, playlistRows.length - info.visibleRows));
    gr.PushClip(info.x + 1, info.bodyY,
        info.w - 2, info.visibleRows * PLAYLIST_ROW_HEIGHT);
    try {
        const end = Math.min(playlistRows.length, first + info.visibleRows);
        for (let index = first; index < end; ++index) {
            const row = playlistRows[index];
            const rowY = info.bodyY + (index - first) * PLAYLIST_ROW_HEIGHT;
            if (index === playlistFocusIndex) {
                gr.FillSolidRect(info.x + 1, rowY, info.w - 2,
                    PLAYLIST_ROW_HEIGHT, 0xA05E4A2C);
                gr.FillSolidRect(info.x + 1, rowY, 4,
                    PLAYLIST_ROW_HEIGHT, C.accent);
            } else if (index === hoveredIndex) {
                gr.FillSolidRect(info.x + 1, rowY, info.w - 2,
                    PLAYLIST_ROW_HEIGHT, 0x80708090);
            } else if ((index - first) & 1) {
                gr.FillSolidRect(info.x + 1, rowY, info.w - 2,
                    PLAYLIST_ROW_HEIGHT, 0x241D2530);
            }
            gr.DrawString(fitText(gr, row.artist, fontPlaylistRow, info.artistWidth),
                fontPlaylistRow, C.text, info.artistX, rowY + 4,
                info.artistWidth, 24);
            gr.DrawString(fitText(gr, row.title, fontPlaylistRow, info.titleWidth),
                fontPlaylistRow, C.text, info.titleX, rowY + 4,
                info.titleWidth, 24);
            gr.DrawString(fitText(gr, row.length, fontPlaylistRow, info.lengthWidth),
                fontPlaylistRow, C.muted, info.lengthX, rowY + 4,
                info.lengthWidth, 24);
        }
    } finally {
        gr.PopClip();
    }

    const bar = playlistScrollbar(info);
    if (bar) {
        gr.FillSolidRect(bar.x, bar.y, bar.w, bar.h, 0x80323C48);
        gr.FillSolidRect(bar.x, bar.thumbY, bar.w, bar.thumbHeight,
            draggingPlaylistThumb ? C.accent : 0xE0A3B2C0);
    }
}

function drawTrackNotice(gr, x, y, w, h) {
    if (!trackNoticeVisible || w < 100 || h < 150) return;
    const maximumTextWidth = Math.max(1, w - 96);
    const artist = fitText(gr, trackNoticeArtist,
        fontTrackArtist, maximumTextWidth);
    const title = fitText(gr, trackNoticeTitle,
        fontTrackTitle, maximumTextWidth);
    const artistWidth = gr.CalcTextWidth(artist, fontTrackArtist);
    const titleWidth = gr.CalcTextWidth(title, fontTrackTitle);
    const boxWidth = Math.min(w - 48,
        Math.max(320, Math.max(artistWidth, titleWidth) + 64));
    const boxHeight = 126;
    const boxX = x + Math.floor((w - boxWidth) / 2);
    const boxY = y + Math.floor((h - boxHeight) / 2);
    gr.FillSolidRect(boxX, boxY, boxWidth, boxHeight, 0xA010151D);
    gr.DrawRect(boxX + 0.5, boxY + 0.5, boxWidth - 1, boxHeight - 1,
        1, 0xB0808D9B);
    gr.DrawString(artist, fontTrackArtist, C.text,
        boxX + (boxWidth - artistWidth) / 2, boxY + 16,
        artistWidth + 2, 45);
    gr.DrawString(title, fontTrackTitle, C.accent,
        boxX + (boxWidth - titleWidth) / 2, boxY + 69,
        titleWidth + 2, 40);
}

function validatePreset(data, id) {
    if (!data || typeof data !== 'object' || Array.isArray(data))
        throw new Error('Invalid preset object');
    if (data.name !== id) throw new Error('Preset name must match ' + id + '.js');
    if (data.sourceId !== 5)
        throw new Error('This collection expects Vectorscope sourceId 5');
    if (!data.source || typeof data.source !== 'object' || Array.isArray(data.source))
        throw new Error('Missing source settings');
    if (!Number.isInteger(data.updateIntervalMs) || data.updateIntervalMs < 1)
        throw new Error('Invalid updateIntervalMs');
    const variant = data.renderer;
    if (!variant || typeof variant !== 'object' || Array.isArray(variant))
        throw new Error('Missing renderer object');
    if (typeof variant.shader !== 'string' || !variant.shader.trim())
        throw new Error('Missing shader HLSL');
    const isShaderCode = value =>
        (typeof value === 'string' && !!value.trim()) ||
        (value instanceof Uint8Array && value.byteLength > 0) ||
        (value instanceof ArrayBuffer && value.byteLength > 0);
    const isImagePass = value => {
        if (isShaderCode(value)) return true;
        if (!value || typeof value !== 'object' || Array.isArray(value) ||
            !isShaderCode(value.shader)) return false;
        return Array.isArray(value.textures) && value.textures.length >= 1 &&
            value.textures.length <= 3 &&
            value.textures.every(path => typeof path === 'string' && !!path);
    };
    if (variant.feedback !== undefined && !isImagePass(variant.feedback))
        throw new Error('Invalid feedback shader pass');
    if (variant.post !== undefined && !isImagePass(variant.post) &&
        !(Array.isArray(variant.post) && variant.post.length > 0 &&
            variant.post.every(isImagePass)))
        throw new Error('Invalid post shader pass');
    return variant;
}

// Capture the complete presented image only for seeding a newly selected
// preset. Feeding this opaque image back into the same visualization would
// recursively process the post output and disturb the entire history.
function captureOutgoingFrame(message = null) {
    const r = layout();
    const w = r.viewWidth, h = window.Height;
    if (!visual || w < 1 || h < 1) return null;

    // Fit within the GPU texture limit. In the usual case the capture is
    // full-resolution; on very large panels it is scaled down.
    const scale = Math.min(1, MAX_CAPTURE_SIDE / w, MAX_CAPTURE_SIDE / h,
        Math.sqrt(MAX_CAPTURE_PIXELS / (w * h)));
    const sw = Math.max(1, Math.floor(w * scale));
    const sh = Math.max(1, Math.floor(h * scale));
    const bitmap = d2d.CreateImage(sw, sh);
    const bg = bitmap.GetGraphics();
    try {
        bg.FillSolidRect(0, 0, sw, sh, C.background);
        visual.Draw(bg, 0, 0, sw, sh);
        if (message) drawCompileOverlay(bg, 0, 0, sw, sh, message);
    } finally {
        bitmap.ReleaseGraphics(bg);
    }
    return bitmap;
}

// Create a transparent bitmap containing only a UI overlay. InjectFeedback
// composites this bitmap once with SOURCE_OVER, so transparent pixels leave
// the existing history untouched.
function captureOverlayFrame(drawOverlay) {
    const r = layout();
    const w = r.viewWidth, h = window.Height;
    if (!visual || w < 1 || h < 1) return null;

    const scale = Math.min(1, MAX_CAPTURE_SIDE / w, MAX_CAPTURE_SIDE / h,
        Math.sqrt(MAX_CAPTURE_PIXELS / (w * h)));
    const sw = Math.max(1, Math.floor(w * scale));
    const sh = Math.max(1, Math.floor(h * scale));
    const bitmap = d2d.CreateImage(sw, sh); // New images are transparent.
    const bg = bitmap.GetGraphics();
    try {
        drawOverlay(bg, sw, sh);
    } finally {
        bitmap.ReleaseGraphics(bg);
    }
    return bitmap;
}

function capturePlaylistOverlay() {
    const hoveredPlaylistRow = playlistRowAt(mouseX, mouseY);
    return captureOverlayFrame((gr, w, h) =>
        drawPlaylistOverlay(gr, 0, 0, w, h, hoveredPlaylistRow));
}

function captureTrackNoticeOverlay() {
    return captureOverlayFrame((gr, w, h) =>
        drawTrackNotice(gr, 0, 0, w, h));
}

function injectPlaylistOverlay() {
    if (!playlistVisible) return;
    try {
        if (visual) {
            const frame = capturePlaylistOverlay();
            if (frame) visual.InjectFeedback(frame);
        }
    } catch (e) {
        // A source-only renderer may reject feedback injection; hiding the
        // overlay and continuing playback/preset selection is still safe.
        console.log('MilkDrop playlist feedback injection: ' + String(e));
    }
    playlistVisible = false;
    hoverTarget = '';
    window.Repaint();
}

function injectTrackNotice() {
    if (!trackNoticeVisible) return;
    trackNoticeTimer = null;
    try {
        if (visual) {
            const frame = captureTrackNoticeOverlay();
            if (frame) visual.InjectFeedback(frame);
        }
    } catch (e) {
        console.log('MilkDrop track feedback injection: ' + String(e));
    }
    trackNoticeVisible = false;
    window.Repaint();
}

function showTrackNotice(metadb) {
    if (!metadb) return;
    if (trackNoticeTimer !== null) clearTimeout(trackNoticeTimer);
    trackNoticeArtist = String(trackArtistFormat.EvalWithMetadb(metadb) ||
        'Unknown artist');
    trackNoticeTitle = String(trackTitleFormat.EvalWithMetadb(metadb) ||
        'Untitled');
    trackNoticeVisible = true;
    trackNoticeTimer = setTimeout(injectTrackNotice,
        TRACK_NOTICE_DURATION_MS);
    window.Repaint();
}

function activate() {
    const request = ++requestId;
    errorText = '';
    if (!presets.length) {
        visual = null;
        visualPresetId = null;
        visualFeedbackCadence = null;
        compilingLabel = '';
        status = 'empty';
        window.Repaint();
        return;
    }

    const selected = presets[currentIndex];
    const creationFps = fps;
    let data, renderer, feedbackCadence;
    try {
        // include() has a per-realm guard, while the factory itself can be
        // called again when selecting a preset without reloading its file.
        if (typeof MilkDropPresets[selected.id] !== 'function')
            include(selected.path);
        const factory = MilkDropPresets[selected.id];
        if (typeof factory !== 'function')
            throw new Error('Preset did not register ' + selected.id);
        data = factory(fps);
        renderer = validatePreset(data, selected.id);
        feedbackCadence = renderer.feedbackIntervalMs > 0
            ? targetFps => factory(targetFps).renderer.feedbackIntervalMs ?? 0
            : null;
        // Keep the outgoing preset's analysis cadence during compilation.
        if (!visual) vis.UpdateIntervalMs = data.updateIntervalMs;
    } catch (e) {
        status = 'error';
        compilingLabel = '';
        errorText = String(e);
        console.log('MilkDrop ' + selected.id + ': ' + errorText);
        window.Repaint();
        return;
    }

    compilingLabel = 'Compiling ' + selected.id + '...';
    compileStartedAt = performance.now();
    lastCompileMs = null;
    compileIndicatorShown = false;
    status = 'loading';
    window.Repaint();
    try {
        vis.CreateAsync(data.sourceId, data.source, renderer)
            .then(next => {
                if (request !== requestId) return; // Stale async compile.
                // FPS may have changed while CreateAsync was pending. Apply
                // the latest cadence before the first Draw, without recompiling.
                if (feedbackCadence && fps !== creationFps)
                    next.FeedbackIntervalMs = feedbackCadence(fps);
                // Wall time of CreateAsync: compilation, cache lookup, task
                // scheduling and Promise delivery; excludes feedback capture.
                lastCompileMs = performance.now() - compileStartedAt;

                let frame = null;
                // Only a transition to a different preset may seed history
                // with the outgoing screen. FPS changes retain the existing
                // visualization; its post output must not re-enter its history.
                if (visual && visualPresetId !== selected.id) {
                    try {
                        frame = captureOutgoingFrame(
                            compileIndicatorShown ? compilingLabel : null);
                    } catch (e) {
                        // A failed snapshot must not prevent changing presets.
                        console.log('MilkDrop capture: ' + String(e));
                    }
                }
                if (frame) {
                    try {
                        next.InjectFeedback(frame);
                    } catch (e) {
                        // Some presets cannot accept feedback injection.
                        console.log('MilkDrop feedback injection: ' + String(e));
                    }
                }

                vis.UpdateIntervalMs = data.updateIntervalMs;
                visual = next;
                visualPresetId = selected.id;
                visualFeedbackCadence = feedbackCadence;
                resetFrameMetrics();
                status = 'ready';
                compilingLabel = '';
                compileIndicatorShown = false;
                window.Repaint();
            })
            .catch(e => {
                if (request !== requestId) return;
                lastCompileMs = performance.now() - compileStartedAt;
                status = 'error';
                compilingLabel = '';
                errorText = String(e);
                console.log('MilkDrop ' + selected.id + ': ' + errorText);
                window.Repaint();
            });
    } catch (e) {
        if (request !== requestId) return;
        lastCompileMs = performance.now() - compileStartedAt;
        status = 'error';
        compilingLabel = '';
        errorText = String(e);
        console.log('MilkDrop ' + selected.id + ': ' + errorText);
        window.Repaint();
    }
}
function startRepaintTimer() {
    if (repaintTimer !== null) clearInterval(repaintTimer);
    repaintTimer = setInterval(() => window.Repaint(), 1000 / fps);
}

function nextAutomaticPresetIndex() {
    if (presets.length < 2) return currentIndex;
    if (!randomPresetOrder) return (currentIndex + 1) % presets.length;
    // Choose uniformly from every preset except the one already active.
    let index = Math.floor(Math.random() * (presets.length - 1));
    if (index >= currentIndex) ++index;
    return index;
}

function restartAutoSwitchTimer() {
    if (autoSwitchTimer !== null) {
        clearInterval(autoSwitchTimer);
        autoSwitchTimer = null;
    }
    if (autoSwitchEnabled && presets.length > 1) {
        autoSwitchTimer = setInterval(() => {
            selectPreset(nextAutomaticPresetIndex(), true);
        }, AUTO_SWITCH_INTERVAL_MS);
    }
}

function toggleAutoSwitch() {
    autoSwitchEnabled = !autoSwitchEnabled;
    window.SetProperty('MilkDrop.AutoSwitch', autoSwitchEnabled);
    restartAutoSwitchTimer();
    window.Repaint();
}

function togglePresetOrder() {
    if (!autoSwitchEnabled) return;
    randomPresetOrder = !randomPresetOrder;
    window.SetProperty('MilkDrop.RandomOrder', randomPresetOrder);
    window.Repaint();
}

function toggleAutoHidePlaylist() {
    autoHidePlaylist = !autoHidePlaylist;
    window.SetProperty('MilkDrop.AutoHidePlaylist', autoHidePlaylist);
    window.Repaint();
}

function selectFps(next) {
    if (!validFps(next)) return;
    fpsText = String(next);
    fpsCaret = fpsText.length;
    fpsEditing = fpsSelectAll = fpsInputError = false;
    window.Repaint();
    if (next === fps) return;
    fps = next;
    window.SetProperty('MilkDrop.FPS', fps);
    // Per-Draw presets use measured frameStep. Opted-in presets instead keep
    // a fixed step at their requested cadence, changed on the existing object.
    if (visual && visualFeedbackCadence) {
        try {
            visual.FeedbackIntervalMs = visualFeedbackCadence(fps);
        } catch (e) {
            status = 'error';
            errorText = String(e);
            console.log('MilkDrop feedback cadence: ' + errorText);
        }
    }
    // Preserve history, phase state and time origin in both modes.
    startRepaintTimer();
    resetFrameMetrics();
}

function selectPreset(index, automatic = false) {
    if (index < 0 || index >= presets.length || index === currentIndex) return;
    if (playlistVisible && autoHidePlaylist) injectPlaylistOverlay();
    currentIndex = index;
    window.SetProperty('MilkDrop.Preset', presets[index].id);
    scrollToSelected();
    activate();
    if (!automatic && autoSwitchEnabled) restartAutoSwitchTimer();
}

// An inline numeric field keeps fractional rates (for example 59.94).
// Enter or Apply commits; Escape restores the active rate. Preset selection
// and audio analysis retain their existing independent lifetimes/cadences.
function validFps(value) {
    return Number.isFinite(value) && value >= MIN_FPS && value <= MAX_FPS;
}

function fpsField(r) {
    return {x: 10, y: CUSTOM_FPS_Y, w: Math.max(20, r.sidebar - 80), h: 30};
}

function fpsApplyButton(r) {
    return {x: r.sidebar - 64, y: CUSTOM_FPS_Y, w: 54, h: 30};
}

function contains(rc, x, y) {
    return x >= rc.x && x < rc.x + rc.w && y >= rc.y && y < rc.y + rc.h;
}

function commitFpsInput() {
    const text = fpsText.trim().replace(',', '.');
    const next = /^\d+(?:\.\d*)?$/.test(text) ? Number(text) : NaN;
    fpsInputError = !validFps(next);
    if (fpsInputError) {
        fpsEditing = true;
        window.Repaint();
        return false;
    }
    selectFps(next);
    return true;
}

function insertFpsText(text) {
    text = String(text).trim().replace(',', '.');
    if (!/^[0-9.]*$/.test(text)) return;
    const next = fpsSelectAll ? text :
        fpsText.slice(0, fpsCaret) + text + fpsText.slice(fpsCaret);
    if (next.length > 12 || (next.match(/\./g) || []).length > 1) return;
    fpsCaret = fpsSelectAll ? text.length : fpsCaret + text.length;
    fpsText = next;
    fpsSelectAll = fpsInputError = false;
    window.Repaint();
}

function drawFpsInput(gr, r) {
    const rc = fpsField(r);
    gr.FillSolidRect(rc.x, rc.y, rc.w, rc.h, C.background);
    gr.DrawRect(rc.x + .5, rc.y + .5, rc.w - 1, rc.h - 1, 1,
        fpsInputError ? C.error : fpsEditing ? C.accent : C.border);
    gr.PushClip(rc.x + 5, rc.y + 2, rc.w - 10, rc.h - 4);
    try {
        const width = gr.CalcTextWidth(fpsText, fontMain);
        if (fpsEditing && fpsSelectAll)
            gr.FillSolidRect(rc.x + 6, rc.y + 5, Math.max(2, width), 20, C.hover);
        gr.DrawString(fpsText, fontMain, C.text, rc.x + 6, rc.y + 5, width + 5, 21);
        if (fpsEditing && !fpsSelectAll && Math.floor(Date.now() / 500) % 2 === 0) {
            const caretX = rc.x + 6 + gr.CalcTextWidth(fpsText.slice(0, fpsCaret), fontMain);
            gr.FillSolidRect(caretX, rc.y + 6, 1, 18, C.accent);
        }
    } finally { gr.PopClip(); }
    drawSidebarWideButton(gr, fpsApplyButton(r), 'Apply', false);
    gr.DrawString(fpsInputError ? 'Enter 30–1000 FPS' : 'Custom FPS · 30–1000',
        fontSmall, fpsInputError ? C.error : C.muted, 12, CUSTOM_FPS_Y + 32, r.sidebar - 24, 18);
}

function on_key_down(key) {
    if (!fpsEditing) return;
    const ctrl = utils.IsKeyPressed(0x11);
    // This field accepts only numbers. Handle both digit rows directly so it
    // also works when the host does not forward WM_CHAR to the panel. Do not
    // insert again in on_char: hosts that deliver both would duplicate digits.
    if (!ctrl && !utils.IsKeyPressed(0x12) && key >= 0x30 && key <= 0x39)
        insertFpsText(String.fromCharCode(key));
    else if (!ctrl && key >= 0x60 && key <= 0x69)
        insertFpsText(String(key - 0x60));
    else if (!ctrl && (key === 0x6E || key === 0xBE || key === 0xBC))
        insertFpsText('.');
    else if (ctrl && key === 0x41) fpsSelectAll = true;
    else if (ctrl && key === 0x56) insertFpsText(utils.GetClipboardText());
    else if (key === 0x0D || key === 0x09) { commitFpsInput(); return; }
    else if (key === 0x1B) { selectFps(fps); return; }
    else if (key === 0x08 || key === 0x2E) {
        if (fpsSelectAll) { fpsText = ''; fpsCaret = 0; }
        else if (key === 0x08 && fpsCaret > 0) {
            fpsText = fpsText.slice(0, fpsCaret - 1) + fpsText.slice(fpsCaret);
            --fpsCaret;
        } else if (key === 0x2E)
            fpsText = fpsText.slice(0, fpsCaret) + fpsText.slice(fpsCaret + 1);
        fpsSelectAll = fpsInputError = false;
    } else if (key === 0x25 || key === 0x24) {
        fpsCaret = fpsSelectAll || key === 0x24 ? 0 : Math.max(0, fpsCaret - 1);
        fpsSelectAll = false;
    } else if (key === 0x27 || key === 0x23) {
        fpsCaret = fpsSelectAll || key === 0x23 ? fpsText.length : Math.min(fpsText.length, fpsCaret + 1);
        fpsSelectAll = false;
    }
    window.Repaint();
}

function on_focus(focused) {
    if (!focused) { fpsEditing = fpsSelectAll = false; window.Repaint(); }
}

function choiceButton(r, i, y) {
    const margin = 10, gap = 5;
    const buttonWidth = (r.sidebar - margin * 2 - gap * 2) / 3;
    return {x: margin + (buttonWidth + gap) * i, y,
        w: buttonWidth, h: 30};
}

function drawChoiceButton(gr, r, i, y, label, selected) {
    const rc = choiceButton(r, i, y);
    const hover = mouseX >= rc.x && mouseX < rc.x + rc.w &&
                  mouseY >= rc.y && mouseY < rc.y + rc.h;
    gr.FillSolidRect(rc.x, rc.y, rc.w, rc.h,
        selected ? C.accent : (hover ? C.hover : C.panel));
    gr.DrawRect(rc.x + 0.5, rc.y + 0.5, rc.w - 1, rc.h - 1, 1,
        selected ? C.accent : C.border);
    const textWidth = gr.CalcTextWidth(label, fontMain);
    gr.DrawString(label, fontMain, selected ? C.accentText : C.text,
        rc.x + (rc.w - textWidth) / 2, rc.y + 5, textWidth + 2, 19);
}

function drawSidebarWideButton(gr, rc, label, selected, enabled = true) {
    const hover = enabled && mouseX >= rc.x && mouseX < rc.x + rc.w &&
        mouseY >= rc.y && mouseY < rc.y + rc.h;
    gr.FillSolidRect(rc.x, rc.y, rc.w, rc.h,
        enabled && selected ? C.accent : (hover ? C.hover : C.panel));
    gr.DrawRect(rc.x + 0.5, rc.y + 0.5, rc.w - 1, rc.h - 1, 1,
        enabled && selected ? C.accent : (enabled ? C.border : C.faint));
    const textWidth = Math.min(rc.w - 10, gr.CalcTextWidth(label, fontMain));
    gr.DrawString(label, fontMain,
        enabled && selected ? C.accentText : (enabled ? C.text : C.faint),
        rc.x + (rc.w - textWidth) / 2, rc.y + 5, textWidth + 2, 19);
}

function drawSidebar(gr, r) {
    const h = window.Height;
    gr.FillSolidRect(0, 0, r.sidebar, h, C.sidebar);
    gr.FillSolidRect(r.sidebar, 0, 1, h, C.border);
    gr.DrawString('Milkdrop ports', fontTitle, C.text, 12, 12, r.sidebar - 24, 27);
    gr.DrawString('FPS', fontSmall, C.muted, 12, 41, 50, 15);
    for (let i = 0; i < FPS_OPTIONS.length; ++i)
        drawChoiceButton(gr, r, i, 58, String(FPS_OPTIONS[i]), fps === FPS_OPTIONS[i]);
    drawFpsInput(gr, r);
    drawSidebarWideButton(gr, autoSwitchButton(r),
        'Auto presets: ' + (autoSwitchEnabled ? 'On' : 'Off'),
        autoSwitchEnabled);
    drawSidebarWideButton(gr, presetOrderButton(r),
        'Order: ' + (randomPresetOrder ? 'Random' : 'Sequential'),
        randomPresetOrder, autoSwitchEnabled);
    drawSidebarWideButton(gr, autoHidePlaylistButton(r),
        'Auto-hide playlist: ' + (autoHidePlaylist ? 'On' : 'Off'),
        autoHidePlaylist);
    gr.DrawString('PRESETS  ' + presets.length, fontSmall, C.muted,
        12, PRESETS_LABEL_Y, r.sidebar - 24, 18);

    gr.PushClip(0, r.listTop, r.sidebar, r.listHeight);
    try {
        if (!presets.length) {
            gr.DrawString('No .js presets', fontSmall, C.faint,
                12, r.listTop + 8, r.sidebar - 24, 35);
        }
        for (let i = 0; i < presets.length; ++i) {
            const y = r.listTop + i * ROW_HEIGHT - scrollY;
            if (y + ROW_HEIGHT <= r.listTop || y >= r.listTop + r.listHeight)
                continue;
            const selected = i === currentIndex;
            const hovered = mouseX >= r.listX &&
                mouseX < r.listX + r.listWidth &&
                mouseY >= Math.max(r.listTop, y) &&
                mouseY < Math.min(r.listTop + r.listHeight, y + ROW_HEIGHT);
            gr.FillSolidRect(r.listX, y, r.listWidth, ROW_HEIGHT - 2,
                selected ? C.panel : hovered ? C.hover : C.sidebar);
            if (selected) gr.FillSolidRect(r.listX, y, 3, ROW_HEIGHT - 2, C.accent);
            gr.DrawString(presets[i].id, fontRow, selected ? C.accent : C.text,
                r.listX + 13, y + 6, r.listWidth - 24, 24);
        }
        const bar = thumb(r);
        if (bar) {
            gr.FillSolidRect(bar.x, r.listTop, bar.w, r.listHeight, C.panel);
            gr.FillSolidRect(bar.x, bar.y, bar.w, bar.h, C.muted);
        }
    } finally {
        gr.PopClip();
    }

    if (h >= LIST_TOP + FOOTER_HEIGHT) {
        const y = h - FOOTER_HEIGHT;
        const stats = cacheStats();
        gr.FillSolidRect(0, y, r.sidebar, 1, C.border);
        gr.DrawString('PERFORMANCE', fontSmall, C.accent,
            11, y + 7, r.sidebar - 20, 16);
        const compiling = status === 'loading';
        const elapsed = compiling ? performance.now() - compileStartedAt : lastCompileMs;
        gr.DrawString('Compile: ' + (elapsed === null ? '—' : formatTime(elapsed) + ' ms') +
            (compiling ? '...' : ''),
            fontSmall, C.text, 11, y + 26, r.sidebar - 20, 17);
        gr.DrawString('Draw CPU: ' +
            (drawCpuAvgMs === null ? '—' : drawCpuAvgMs.toFixed(2) + ' ms'),
            fontSmall, C.text, 11, y + 44, r.sidebar - 20, 17);
        gr.DrawString('Paint: ' +
            (paintRate === null ? '—' : paintRate.toFixed(1) + ' FPS'),
            fontSmall, C.muted, 11, y + 62, r.sidebar - 20, 17);

        gr.FillSolidRect(0, y + 83, r.sidebar, 1, C.border);
        gr.DrawString('DXBC CACHE', fontSmall, C.accent,
            11, y + 90, r.sidebar - 20, 16);
        gr.DrawString('Entries: ' + stats.Entries + ' / ' + vis.ShaderCacheMaxEntries,
            fontSmall, C.text, 11, y + 109, r.sidebar - 20, 17);
        const mb = 1024 * 1024;
        gr.DrawString('Size: ' + (stats.Bytes / mb).toFixed(2) + ' / ' +
            (vis.ShaderCacheMaxBytes / mb).toFixed(1) + ' MiB',
            fontSmall, C.text, 11, y + 126, r.sidebar - 20, 17);
        gr.DrawString('Hits: ' + stats.Hits + '  Misses: ' + stats.Misses,
            fontSmall, C.muted, 11, y + 143, r.sidebar - 20, 17);
        gr.DrawString('Evictions: ' + stats.Evictions,
            fontSmall, C.muted, 11, y + 160, r.sidebar - 20, 17);
    }
}

function drawMessage(gr, r, message, color = C.muted) {
    if (r.viewWidth <= 0) return;
    const x = r.viewX + 22;
    gr.DrawString(message, fontMain, color, x,
        Math.max(12, window.Height / 2 - 11),
        Math.max(0, r.viewWidth - 44), 75);
}

function on_size() {
    normalizeScroll();
    normalizePlaylistScroll();
}

function on_paint(gr) {
    const w = window.Width, h = window.Height;
    if (w <= 0 || h <= 0) return;
    const r = layout();
    normalizeScroll(r);
    gr.FillSolidRect(0, 0, w, h, C.background);
    if (visual && r.viewWidth > 0) {
        try {
            const drawStartedAt = performance.now();
            visual.Draw(gr, r.viewX, 0, r.viewWidth, h);
            recordDrawCpuTime(performance.now() - drawStartedAt);
        } catch (e) {
            // Report a failing visualization once, not at every paint tick.
            visual = null;
            visualPresetId = null;
            status = 'error';
            errorText = String(e);
            console.log('MilkDrop draw: ' + errorText);
        }
    }
    if (status === 'loading' && r.viewWidth > 0 &&
        performance.now() - compileStartedAt >= COMPILE_INDICATOR_DELAY_MS) {
        drawCompileOverlay(gr, r.viewX, 0, r.viewWidth, h);
        compileIndicatorShown = true;
    }
    if (status === 'empty') drawMessage(gr, r, 'Add a preset to the MilkDrop folder.');
    if (status === 'error') drawMessage(gr, r,
        'Preset error: ' + errorText, C.error);
    if (playlistVisible && r.viewWidth > 0)
        drawPlaylistOverlay(gr, r.viewX, 0, r.viewWidth, h,
            playlistRowAt(mouseX, mouseY));
    if (trackNoticeVisible && r.viewWidth > 0)
        drawTrackNotice(gr, r.viewX, 0, r.viewWidth, h);
    drawSidebar(gr, r);
}

function hoveredItem(x, y, r) {
    if (x < 0 || x >= r.sidebar) return '';
    if (contains(fpsField(r), x, y)) return 'fps-input';
    if (contains(fpsApplyButton(r), x, y)) return 'fps-apply';
    for (let i = 0; i < FPS_OPTIONS.length; ++i) {
        const rc = choiceButton(r, i, 58);
        if (x >= rc.x && x < rc.x + rc.w &&
            y >= rc.y && y < rc.y + rc.h) return 'fps:' + i;
    }
    const autoRc = autoSwitchButton(r);
    if (x >= autoRc.x && x < autoRc.x + autoRc.w &&
        y >= autoRc.y && y < autoRc.y + autoRc.h)
        return 'auto-switch';
    const orderRc = presetOrderButton(r);
    if (autoSwitchEnabled && x >= orderRc.x && x < orderRc.x + orderRc.w &&
        y >= orderRc.y && y < orderRc.y + orderRc.h)
        return 'preset-order';
    const autoHideRc = autoHidePlaylistButton(r);
    if (x >= autoHideRc.x && x < autoHideRc.x + autoHideRc.w &&
        y >= autoHideRc.y && y < autoHideRc.y + autoHideRc.h)
        return 'auto-hide-playlist';
    if (x >= r.listX && x < r.listX + r.listWidth &&
        y >= r.listTop && y < r.listTop + r.listHeight) {
        const i = Math.floor((y - r.listTop + scrollY) / ROW_HEIGHT);
        if (i >= 0 && i < presets.length) return 'preset:' + i;
    }
    return '';
}

function on_mouse_move(x, y) {
    const previousPlaylistRow = playlistRowAt(mouseX, mouseY);
    mouseX = x;
    mouseY = y;
    const r = layout();
    if (draggingPlaylistThumb) {
        const bar = playlistScrollbar();
        if (bar && bar.range > 0) {
            playlistScroll = clamp(Math.round(
                (y - bar.y - playlistThumbGrab) * bar.scrollRange / bar.range),
                0, bar.scrollRange);
            window.Repaint();
        }
    } else if (draggingThumb) {
        const bar = thumb(r);
        if (bar && bar.range > 0)
            setScroll((y - r.listTop - thumbGrab) * maxScroll(r) / bar.range, r);
    } else {
        const next = hoveredItem(x, y, r);
        const nextPlaylistRow = playlistRowAt(x, y);
        if (next !== hoverTarget || nextPlaylistRow !== previousPlaylistRow) {
            hoverTarget = next;
            window.Repaint();
        }
    }
}

function on_mouse_leave() {
    const hadPlaylistHover = playlistRowAt(mouseX, mouseY) >= 0;
    mouseX = mouseY = -1;
    if ((hoverTarget || hadPlaylistHover) &&
        !draggingThumb && !draggingPlaylistThumb) window.Repaint();
    hoverTarget = '';
}

function on_mouse_wheel(step) {
    const r = layout();
    const playlistInfo = activePlaylistOverlayLayout();
    if (playlistVisible && playlistInfo &&
        mouseX >= playlistInfo.x && mouseX < playlistInfo.x + playlistInfo.w &&
        mouseY >= playlistInfo.y && mouseY < playlistInfo.y + playlistInfo.h) {
        playlistScroll = clamp(playlistScroll - step * 3,
            0, playlistMaxScroll(playlistInfo));
        window.Repaint();
        return;
    }
    if (mouseX === -1 || mouseX <= r.sidebar)
        setScroll(scrollY - step * ROW_HEIGHT * 2, r);
}

function on_mouse_lbtn_down(x, y) {
    if (contains(fpsField(layout()), x, y)) {
        fpsSelectAll = !fpsEditing;
        fpsEditing = true;
        fpsCaret = fpsText.length;
        window.Repaint();
        return;
    }
    if (!contains(fpsApplyButton(layout()), x, y)) {
        fpsEditing = fpsSelectAll = false;
        window.Repaint();
    }
    const playlistBar = playlistVisible ? playlistScrollbar() : null;
    if (playlistBar && x >= playlistBar.x - 3 &&
        x < playlistBar.x + playlistBar.w + 3 &&
        y >= playlistBar.y && y < playlistBar.y + playlistBar.h) {
        draggingPlaylistThumb = true;
        playlistThumbGrab = y >= playlistBar.thumbY &&
            y < playlistBar.thumbY + playlistBar.thumbHeight
            ? y - playlistBar.thumbY
            : playlistBar.thumbHeight / 2;
        if (y < playlistBar.thumbY ||
            y >= playlistBar.thumbY + playlistBar.thumbHeight) {
            playlistScroll = clamp(Math.round(
                (y - playlistBar.y - playlistThumbGrab) *
                playlistBar.scrollRange / playlistBar.range),
                0, playlistBar.scrollRange);
        }
        window.Repaint();
        return;
    }
    const r = layout();
    const bar = thumb(r);
    if (!bar || x < r.trackX - 3 || x > r.trackX + r.trackWidth + 3 ||
        y < r.listTop || y >= r.listTop + r.listHeight) return;
    draggingThumb = true;
    thumbGrab = y >= bar.y && y < bar.y + bar.h ? y - bar.y : bar.h / 2;
    if (y < bar.y || y >= bar.y + bar.h)
        setScroll((y - r.listTop - thumbGrab) * maxScroll(r) / bar.range, r);
}

function on_mouse_lbtn_up(x, y) {
    if (contains(fpsField(layout()), x, y)) return;
    if (contains(fpsApplyButton(layout()), x, y)) { commitFpsInput(); return; }
    if (draggingPlaylistThumb) {
        draggingPlaylistThumb = false;
        window.Repaint();
        return;
    }
    if (draggingThumb) {
        draggingThumb = false;
        return;
    }
    const r = layout();
    if (x >= r.viewX && x < r.viewX + r.viewWidth &&
        y >= 0 && y < window.Height) {
        if (!playlistVisible && Date.now() >= suppressDrawingClickUntil)
            showActivePlaylist();
        return;
    }
    if (x < 0 || x >= r.sidebar) return;
    for (let i = 0; i < FPS_OPTIONS.length; ++i) {
        const b = choiceButton(r, i, 58);
        if (x >= b.x && x < b.x + b.w && y >= b.y && y < b.y + b.h) {
            selectFps(FPS_OPTIONS[i]);
            return;
        }
    }
    const autoRc = autoSwitchButton(r);
    if (x >= autoRc.x && x < autoRc.x + autoRc.w &&
        y >= autoRc.y && y < autoRc.y + autoRc.h) {
        toggleAutoSwitch();
        return;
    }
    const orderRc = presetOrderButton(r);
    if (autoSwitchEnabled && x >= orderRc.x && x < orderRc.x + orderRc.w &&
        y >= orderRc.y && y < orderRc.y + orderRc.h) {
        togglePresetOrder();
        return;
    }
    const autoHideRc = autoHidePlaylistButton(r);
    if (x >= autoHideRc.x && x < autoHideRc.x + autoHideRc.w &&
        y >= autoHideRc.y && y < autoHideRc.y + autoHideRc.h) {
        toggleAutoHidePlaylist();
        return;
    }
    if (y >= r.listTop && y < r.listTop + r.listHeight &&
        x >= r.listX && x < r.listX + r.listWidth) {
        selectPreset(Math.floor((y - r.listTop + scrollY) / ROW_HEIGHT));
    }
}

function on_mouse_lbtn_dblclk(x, y) {
    const rowIndex = playlistRowAt(x, y);
    if (rowIndex < 0 || playlistIndex < 0) return;
    const activePlaylist = playlistIndex;
    const itemIndex = playlistRows[rowIndex].index;
    if (autoHidePlaylist) {
        // Windows sends the final button-up after the double-click callback.
        // Prevent that event from immediately reopening the hidden overlay.
        suppressDrawingClickUntil = Date.now() + 250;
        injectPlaylistOverlay();
    }
    if (activePlaylist >= 0 && activePlaylist < plman.PlaylistCount &&
        itemIndex >= 0 && itemIndex < plman.PlaylistItemCount(activePlaylist))
        plman.ExecutePlaylistDefaultAction(activePlaylist, itemIndex);
}

function on_playlist_switch() {
    if (playlistVisible) refreshActivePlaylist(true);
}

function on_playlists_changed() {
    if (playlistVisible) refreshActivePlaylist(true);
}

function on_playlist_items_added(index) {
    if (playlistVisible && index === playlistIndex) refreshActivePlaylist();
}

function on_playlist_items_removed(index) {
    if (playlistVisible && index === playlistIndex) refreshActivePlaylist();
}

function on_playlist_items_reordered(index) {
    if (playlistVisible && index === playlistIndex) refreshActivePlaylist();
}

function on_item_focus_change(index, from, to) {
    if (playlistVisible && index === playlistIndex)
        revealPlaylistFocus(to);
}

function on_playback_new_track(metadb) {
    if (playlistVisible && autoHidePlaylist) injectPlaylistOverlay();
    showTrackNotice(metadb);
}

// Keep the sample on the production O3 setting without exposing compiler UI.
setDefaultOptimizationFlags();
// Initial selection is scrolled into view after the panel receives its size.
startRepaintTimer();
restartAutoSwitchTimer();
activate();