From 2f7fe9a807efbea1035065129f4d9b7cfb637c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Wed, 23 Sep 2026 07:34:25 +0300 Subject: [PATCH 1/7] fix(milkdrop): builtin and generated presets write fWaveAlpha MilkDrop reads the main wave's alpha from the file as `fWaveAlpha` (state.cpp:1383). `wave_a` is the name per-frame code uses, and a header line with it is never read. The five builtin presets and the generator wrote `wave_a`, so in MilkDrop they drew the wave at its default 0.8. All 10,347 corpus files write fWaveAlpha and none writes wave_a in the header. The engine reads both names, so nothing changes on screen here: for the builtins and 199 generated presets the engine's wave_a is the same with either key. The generator's golden hashes change with the text; its version stays 1 because the same code still gives the same picture. Refs #580, #560 --- src/shared/milkdrop-generator.js | 7 ++++++- src/shared/presets-milkdrop.js | 10 +++++----- tests/milkdrop-generator.test.js | 26 ++++++++++++++++---------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/shared/milkdrop-generator.js b/src/shared/milkdrop-generator.js index db5c9b8..7ab8778 100644 --- a/src/shared/milkdrop-generator.js +++ b/src/shared/milkdrop-generator.js @@ -983,7 +983,12 @@ put('wave_r', num(cW[0])); put('wave_g', num(cW[1])); put('wave_b', num(cW[2])); - put('wave_a', num(wA)); + /* MilkDrop başlıkta dalga saydamlığını `fWaveAlpha`dan okuyor; + `wave_a` yalnız denklem adı. Önce `wave_a` yazılıyordu: motor ikisini + de okuduğu için burada fark yoktu, MilkDrop'ta ise değer okunmayıp + varsayılan 0,8'e düşüyordu (#580). Korpusun 10.332 presetinin hepsi + fWaveAlpha yazıyor. */ + put('fWaveAlpha', num(wA)); put('wave_x', '0.500'); put('wave_y', '0.500'); put('fWaveScale', num(wScale)); diff --git a/src/shared/presets-milkdrop.js b/src/shared/presets-milkdrop.js index 49b7f99..4a74bbf 100644 --- a/src/shared/presets-milkdrop.js +++ b/src/shared/presets-milkdrop.js @@ -67,7 +67,7 @@ wave_y=0.500 wave_r=0.800 wave_g=0.500 wave_b=1.000 -wave_a=0.550 +fWaveAlpha=0.550 fWaveScale=1.600 fWaveSmoothing=0.750 fWaveParam=0.300 @@ -229,7 +229,7 @@ wave_y=0.500 wave_r=1.000 wave_g=0.300 wave_b=0.100 -wave_a=0.420 +fWaveAlpha=0.420 fWaveScale=2.200 fWaveSmoothing=0.300 fWaveParam=0.700 @@ -384,7 +384,7 @@ wave_y=0.500 wave_r=0.750 wave_g=0.900 wave_b=1.000 -wave_a=0.420 +fWaveAlpha=0.420 fWaveScale=0.900 fWaveSmoothing=0.900 fWaveParam=0.000 @@ -506,7 +506,7 @@ wave_y=0.500 wave_r=0.300 wave_g=0.900 wave_b=1.000 -wave_a=0.340 +fWaveAlpha=0.340 fWaveScale=1.100 fWaveSmoothing=0.650 fWaveParam=-0.300 @@ -671,7 +671,7 @@ wave_y=0.500 wave_r=1.000 wave_g=1.000 wave_b=1.000 -wave_a=0.520 +fWaveAlpha=0.520 fWaveScale=1.800 fWaveSmoothing=0.250 fWaveParam=0.000 diff --git a/tests/milkdrop-generator.test.js b/tests/milkdrop-generator.test.js index 4ba49e7..ef90521 100644 --- a/tests/milkdrop-generator.test.js +++ b/tests/milkdrop-generator.test.js @@ -202,15 +202,21 @@ test('aynı kod aynı metni veriyor; dil yalnız adı değiştiriyor', () => { test düşerse değişiklik bilerek yapıldıysa VERSION artırılmalı (kimlik de değişir, eski kayıtların üstüne yazılmaz) ve özetler yenilenmeli. Windows ve Ubuntu'da, Node 20 ve 22'de aynı çıkmalı: üretici yazdığı - sayılara sin/pow/exp sokmuyor. */ + sayılara sin/pow/exp sokmuyor. + + Özetler bir kez VERSION artmadan yenilendi (#580): dalga saydamlığının + anahtarı `wave_a` yerine MilkDrop'un okuduğu `fWaveAlpha` oldu. Değer ve + motordaki görüntü aynı, yalnız MilkDrop'ta artık doğru okunuyor; aynı + kodu yeniden kaydetmek eski dosyanın üstüne AYNI görünen, düzeltilmiş + bir dosya yazıyor. Görüntüyü değiştiren bir değişiklik VERSION ister. */ const GOLDEN = { - '0-0-0-0-0': '471e4ed82802f52e', // warp shader'ı, zorlanan tek dalga - '100-100-100-100-zzzzzz': '39d6604bf92852c8', // birleştirme, 4 dalga, 3 şekil - '50-50-50-50-k3x9ab': '63ae6d15b3fcfc68', // iki shader - '72-15-60-88-2n9c': '78b50f1e94a2c875', - '20-80-35-65-1a2b3c': 'c01c340ae467e8ad', - '90-40-90-10-7': 'f2e5665d620a10a7', - '30-60-40-30-5': 'e7ced80c3c4ab9f4', // shader yok (MilkDrop 1 biçimi) + '0-0-0-0-0': '506e5e7950ae5f55', // warp shader'ı, zorlanan tek dalga + '100-100-100-100-zzzzzz': '766aef37f142e51c', // birleştirme, 4 dalga, 3 şekil + '50-50-50-50-k3x9ab': 'd0ce1d37d84b2f25', // iki shader + '72-15-60-88-2n9c': 'eb04c5e58e0fe0f0', + '20-80-35-65-1a2b3c': 'af26a0cbbfbf1642', + '90-40-90-10-7': 'ffa4bebae2715ca8', + '30-60-40-30-5': '3b7e30dc59759bb4', // shader yok (MilkDrop 1 biçimi) }; test('altın özetler: kod -> metin sabit', () => { @@ -318,7 +324,7 @@ const TIMES = [0, 1.7, 13.3, 101.9, 1234.5]; function mainVars(f, t, a) { const v = { time: t, fps: 30, frame: Math.round(t * 30), progress: 0.5, aspectx: 1, aspecty: 16 / 9 }; for (const k of ['bass', 'mid', 'treb', 'bass_att', 'mid_att', 'treb_att']) v[k] = a; - for (const k of ['zoom', 'rot', 'warp', 'cx', 'cy', 'dx', 'dy', 'sx', 'sy', 'wave_r', 'wave_g', 'wave_b', 'wave_a', 'decay']) { + for (const k of ['zoom', 'rot', 'warp', 'cx', 'cy', 'dx', 'dy', 'sx', 'sy', 'wave_r', 'wave_g', 'wave_b', 'decay']) { if (typeof f.params[k] === 'number') v[k] = f.params[k]; } return v; @@ -464,7 +470,7 @@ test('sönme ve gama makul aralıkta; warp shader\'ı sönmeyi kendisi yapıyor' assert.ok(f.params.fgammaadj >= 1 && f.params.fgammaadj <= 1.8, r.code + ' gama ' + f.params.fgammaadj); // Ana dalga sönme yavaşladıkça saydamlaşıyor: 0,97'de tam, 0,98'de üçte iki const room = Math.max(0.5, Math.min(1, (1 - f.params.fdecay) / 0.03)); - assert.ok(f.params.wave_a <= 0.9 * room + 0.001, r.code + ' wave_a ' + f.params.wave_a + ' sönme ' + f.params.fdecay); + assert.ok(f.params.fwavealpha <= 0.9 * room + 0.001, r.code + ' fWaveAlpha ' + f.params.fwavealpha + ' sönme ' + f.params.fdecay); if (f.warpShader) { /* MilkDrop warp shader'ı olan presette decay'i uygulamıyor. q8'i yazmayan bir warp shader'ı görüntüyü hiç söndürmez ve beyaza doyar. */ From 1047915bc273723200c409b5ec8b8280f75d145b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Wed, 23 Sep 2026 07:34:56 +0300 Subject: [PATCH 2/7] fix(milkdrop): MilkDrop 2's stage rule, fixed composite and defaults Checked against Nullsoft's code (jecassis/foo_vis_milk2 5b44cea) and, for the fixed pipeline's blend passes, the D3D9 code it was ported from (BeatDrop 53d83ee). Stages by version. MilkDrop picks the warp and composite shaders by the file's version, not by whether shader text exists. A file with no MILKDROP_PRESET_VERSION, or one below 200, is a MilkDrop 1 preset. 200 uses PSVERSION; 201 and above use PSVERSION_WARP and PSVERSION_COMP, each 2 when missing (state.cpp:1328-1348). A stage with a version and no text gets a shader written at load time with the file's values baked in (plugin.cpp GenWarpPShaderText, GenCompPShaderText). With fidelity on the engine follows this, and switching fidelity rebuilds the running preset's stages. Fixed composite. MilkDrop draws the flags with blend passes of a white quad: brighten is 1-(1-c)^2 and solarize 2c(1-c). The engine used sqrt(c) and 4c(1-c), the formulas of the shader MilkDrop generates. Echo orientation is (int)x % 4 with C's sign rule. Gamma is drawn as additive redraws, so below 1 it is not applied while echo is on. When two presets whose echoes point different ways blend, the echo fades out before the snap point and back in after it. Defaults. Keys a file does not write take MilkDrop's defaults: decay 0.98, gamma 2.0, echo zoom 2.0, wave alpha 0.8, mv_l 0.9, grid 12x9, border sizes 0.01 and inner border colour 0.25. The per-frame reset wrote the pool's 0 back every frame. Init code sees the defaults too, and a centre of 0 stays in the corner. Measured. Every flag, gamma and orientation case rendered through the engine reads back within 2/255 of MilkDrop's formula, with fidelity on and off (66 of 66). The 900-preset sample sorts into the same classes before and after. Of 120 brighten presets 99 changed (median 3.8%); of 83 solarize presets 63 changed, and median brightness went from 0.291 to 0.112. 14 new tests, 5 updated; the docs' test count follows. Refs #580, #560 --- README.md | 4 +- README.tr.md | 4 +- ROADMAP.md | 4 +- src/shared/milkdrop.js | 171 +++++++++++++- src/visualizer/modes/milkdrop.js | 126 ++++++++-- tests/milkdrop-blend.test.js | 11 +- tests/milkdrop-context-loss.test.js | 2 +- tests/milkdrop-fidelity-comp.test.js | 331 +++++++++++++++++++++++++++ tests/milkdrop-frame-reset.test.js | 8 +- tests/milkdrop-prefetch.test.js | 3 +- tests/milkdrop-switch-async.test.js | 3 + 11 files changed, 635 insertions(+), 32 deletions(-) create mode 100644 tests/milkdrop-fidelity-comp.test.js diff --git a/README.md b/README.md index 5f099a4..b7d9c6a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#build--distribution) [![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/) [![Downloads](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=downloads)](https://github.com/CaYatur/SoundVisualizer/releases) -[![Tests](https://img.shields.io/badge/tests-2167%20passing-2ea043.svg)](#tests) +[![Tests](https://img.shields.io/badge/tests-2181%20passing-2ea043.svg)](#tests) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1289,7 +1289,7 @@ npm test npm start -- --smoke ``` -**2167 unit tests, all passing.** They are written to check answers, not to exercise lines: +**2181 unit tests, all passing.** They are written to check answers, not to exercise lines: - **Formulas** are checked against values derived by hand from their definitions — Viviani's curve staying on its sphere, the torus tube radius, Chladni's m↔n antisymmetry, every attractor diff --git a/README.tr.md b/README.tr.md index d8aad28..197cbdd 100644 --- a/README.tr.md +++ b/README.tr.md @@ -12,7 +12,7 @@ [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#paketleme--dağıtım) [![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/) [![İndirme](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=indirme)](https://github.com/CaYatur/SoundVisualizer/releases) -[![Test](https://img.shields.io/badge/test-2167%20geçiyor-2ea043.svg)](#testler) +[![Test](https://img.shields.io/badge/test-2181%20geçiyor-2ea043.svg)](#testler) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1287,7 +1287,7 @@ npm test npm start -- --smoke ``` -**2167 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: +**2181 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: - **Formüller**, tanımlarından elle türetilmiş değerlerle sınanıyor — Viviani eğrisinin küre üzerinde kalması, simidin boru yarıçapı, Chladni'nin m↔n antisimetrisi, her çekicinin sınırlı diff --git a/ROADMAP.md b/ROADMAP.md index 91169ca..7027e8a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,9 +129,9 @@ npm test npm start -- --smoke ``` -- **2167 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; +- **2181 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; 105 came with v3.1.1; 163 came with v3.1.2; 157 came with v3.1.3 — 1128 at - that tag — 469 more with v3.1.4, most of them from the MilkDrop work, and 570 + that tag — 469 more with v3.1.4, most of them from the MilkDrop work, and 584 on `main` since. Formulas are checked against values derived by hand from their definitions — Viviani's curve staying on its sphere, the diff --git a/src/shared/milkdrop.js b/src/shared/milkdrop.js index 9390c77..02ec8d2 100644 --- a/src/shared/milkdrop.js +++ b/src/shared/milkdrop.js @@ -961,6 +961,30 @@ 'b1n', 'b1x', 'b2n', 'b2x', 'b3n', 'b3x', 'b1ed', ]; + /* MILKDROP'UN VARSAYILANLARI, dosyanın YAZMADIĞI yerleşik adlar için + (state.cpp CState::Default ve Import; #580). Havuzun doğal başlangıcı + 0 ve kare başı sıfırlama o 0'ı her kare geri yazıyordu: fDecay yazmayan + bir preset hiç iz bırakmıyor (MilkDrop'ta 0,98), fGammaAdj yazmayan + yarı parlaklıkta çıkıyor (MilkDrop'ta 2,0). Motorun "yazılmadıysa + 0,98" denetimi de işe yaramıyordu: sıfırlama adı havuza her kare + yazdığı için ad hep "var" görünüyordu. + + `mv_a` MilkDrop'ta da sonunda 0: `bMotionVectorsOn` yoksa 0'a + çevriliyor (state.cpp:1402), ancak sonra `mv_a` okunuyor. Uyum açıkken + bu tablo, kapalıyken eski taban geçerli. */ + const MD2_PF_DEFAULTS = { + zoom: 1, zoomexp: 1, rot: 0, warp: 1, cx: 0.5, cy: 0.5, dx: 0, dy: 0, sx: 1, sy: 1, + decay: 0.98, wave_a: 0.8, wave_r: 1, wave_g: 1, wave_b: 1, wave_x: 0.5, wave_y: 0.5, + wave_mystery: 0, wave_mode: 0, + ob_size: 0.01, ob_r: 0, ob_g: 0, ob_b: 0, ob_a: 0, + ib_size: 0.01, ib_r: 0.25, ib_g: 0.25, ib_b: 0.25, ib_a: 0, + mv_x: 12, mv_y: 9, mv_dx: 0, mv_dy: 0, mv_l: 0.9, mv_r: 1, mv_g: 1, mv_b: 1, mv_a: 0, + echo_zoom: 2, echo_alpha: 0, echo_orient: 0, + wave_usedots: 0, wave_thick: 0, wave_additive: 0, wave_brighten: 1, + darken_center: 0, gamma: 2, wrap: 1, invert: 0, brighten: 0, darken: 0, solarize: 0, + b1n: 0, b1x: 1, b2n: 0, b2x: 1, b3n: 0, b3x: 1, b1ed: 0.25, + }; + const NUM_Q = 32; /* t1..t8 — DALGA VE ŞEKİL BLOKLARININ KENDİ ARA DEĞİŞKENLERİ. @@ -1136,6 +1160,18 @@ yazdigi sey ilk karede zaten uzerine yaziliyor. */ this._pfBase = {}; for (const k of PF_RESET) this._pfBase[k] = this.pool.get(k); + /* Uyum açıkken taban: dosyanın yazdığı değer, yazmadığı adda + MilkDrop'un varsayılanı. Dosya bir adı ya kendi adıyla ya da + başlık adıyla (PARAM_ALIAS) yazıyor; `mv_a`yı `bMotionVectorsOn` + de veriyor. */ + const fromFile = new Set(); + for (const k of PF_RESET) if (typeof this.file.params[k] === 'number') fromFile.add(k); + for (const [from, to] of PARAM_ALIAS) if (typeof this.file.params[from] === 'number') fromFile.add(to); + if (typeof this.file.params.bmotionvectorson === 'number') fromFile.add('mv_a'); + this._pfBaseMd2 = {}; + for (const k of PF_RESET) { + this._pfBaseMd2[k] = fromFile.has(k) || !(k in MD2_PF_DEFAULTS) ? this._pfBase[k] : MD2_PF_DEFAULTS[k]; + } this._qInit = null; /* Custom dalgalar ve şekiller. Referans preset paketinde şekillerin @@ -1370,7 +1406,12 @@ frame(inputs) { const P = this.pool; if (inputs) for (const k in inputs) P.set(k, inputs[k]); + const base = this.accurate ? this._pfBaseMd2 : this._pfBase; if (!this.initialised) { + /* MilkDrop init'i koşturmadan önce de yerleşik adları yüklüyor + (state.cpp RecompileExpressions: LoadPerFrameEvallibVars, sonra + init): init MilkDrop'un varsayılanlarını görmeli. */ + if (this.accurate) for (const k of PF_RESET) P.set(k, base[k]); this.cInit.run(P.values); this.initialised = true; /* q'larin "init sonrasi" degeri: her karenin basladigi nokta. @@ -1382,7 +1423,7 @@ /* Yerlesik kare degiskenleri her karede dosyadaki degere donuyor — ilk kare dahil, cunku MilkDrop init'ten sonra da yeniden yukluyor. Ayrintili gerekce PF_RESET'in yaninda. */ - for (const k of PF_RESET) P.set(k, this._pfBase[k]); + for (const k of PF_RESET) P.set(k, base[k]); if (this._qInit) for (let i = 0; i < NUM_Q; i++) P.set('q' + (i + 1), this._qInit[i]); this.cFrame.run(P.values); /* q'ların KARE değeri, per_pixel koşmadan önce. MilkDrop per_frame @@ -1435,13 +1476,19 @@ // per_frame sonrası hareket değişkenlerinin kare genelindeki değerleri captureBase() { const P = this.pool; + /* Merkez 0 GEÇERLİ bir değer: dönmenin ve germenin merkezi köşede. + `|| 0,5` onu ortaya taşıyordu; korpusta 84 preset başlıkta cx ya da + cy 0 yazıyor (#580). Uyum açıkken varsayılan zaten tabandan geliyor + (MD2_PF_DEFAULTS), yani 0 yalnız yazılmış 0. zoom, sx ve sy'de 0 + MilkDrop'ta da sıfıra bölme — onlarda koruma kalıyor. */ + const acc = this.accurate; this._base = { zoom: P.get('zoom') || 1, zoomexp: P.get('zoomexp') || 1, rot: P.get('rot'), warp: P.get('warp'), - cx: P.get('cx') || 0.5, - cy: P.get('cy') || 0.5, + cx: acc ? P.get('cx') : (P.get('cx') || 0.5), + cy: acc ? P.get('cy') : (P.get('cy') || 0.5), dx: P.get('dx'), dy: P.get('dy'), sx: P.get('sx') || 1, @@ -1491,8 +1538,124 @@ return ((Math.trunc(v * 255) & 0xFF)) / 255; } + // ========================================================================== + // MilkDrop 2'nin aşama seçimi ve sabit yol ayrıntıları (#580) + // ========================================================================== + /* Birincil kaynak: jecassis/foo_vis_milk2 5b44cea (Nullsoft'un kodu), + sabit yol için BeatDrop 53d83ee'deki D3D9 hâliyle de karşılaştırıldı. + Buradakiler kaynaktan öğrenilen KURALLAR; kod bizim. + + AŞAMA SÜRÜMDEN SEÇİLİYOR, METİNDEN DEĞİL (state.cpp:1328-1348, + milkdropfs.cpp:921-924). MilkDrop bir aşamanın shader'ını ancak o + aşamanın sürümü sıfırdan büyükse kullanıyor: + MILKDROP_PRESET_VERSION yok ya da 200'den küçük -> ikisi de 0 + tam 200 -> PSVERSION (yoksa 2) ikisine de + 201 ve üstü -> PSVERSION_WARP / PSVERSION_COMP (yoksa 2) + Sürümü 0 olan aşamanın metni varsa bile okunmuyor, sabit yol çiziyor. + Sürümü sıfırdan büyük ama metni olmayan aşama için MilkDrop YÜKLEMEDE + bir shader yazıyor ve dosyadaki değerleri içine GÖMÜYOR: o presette + kare denklemlerinin gama, yankı ve bayraklara yazdıkları yok sayılıyor. + Tam sayılar `sscanf("%d")` ile okunuyor: kesirli bir değer aşağı + kırpılıyor. */ + function md2Versions(params) { + const p = params || {}; + const int = (v, d) => (typeof v === 'number' && isFinite(v) ? Math.trunc(v) : d); + const pv = int(p.milkdrop_preset_version, 100); + if (pv < 200) return { preset: pv, warp: 0, comp: 0 }; + if (pv === 200) { + const v = int(p.psversion, 2); + return { preset: pv, warp: v, comp: v }; + } + return { preset: pv, warp: int(p.psversion_warp, 2), comp: int(p.psversion_comp, 2) }; + } + + /* Aşama başına yol: 'shader' (presetin metni), 'generated' (MilkDrop'un + yüklemede yazdığı, değerleri gömülü shader) ya da 'fixed' (shader'sız + sabit yol). `file` parseMilk çıktısı. */ + function stagePlan(file) { + const f = file || {}; + const v = md2Versions(f.params); + const pick = (ver, text) => (ver > 0 ? (String(text || '').trim() ? 'shader' : 'generated') : 'fixed'); + return { warp: pick(v.warp, f.warpShader), comp: pick(v.comp, f.compShader), versions: v }; + } + + /* Gömülü değerler dosyadan, MilkDrop'un varsayılanlarıyla + (state.cpp CState::Default): decay 0,98, gama 2,0, yankı yakınlaşması + 2,0, yankı saydamlığı 0, yön 0, doku sarma açık. Kayan sayılar 32 bit + okunuyor (`%f` bir float'a); yuvarlamadan önce Math.fround — 0,975 + float'ta 0,97500002 ve "%.2f" onu 0,98 yazıyor, double 0,97. */ + const f32 = (v, d) => Math.fround(typeof v === 'number' && isFinite(v) ? v : d); + const i32 = (v, d) => (typeof v === 'number' && isFinite(v) ? Math.trunc(v) : d); + + // Sürümü olup metni olmayan warp aşaması (plugin.cpp GenWarpPShaderText) + function genWarpText(params) { + const p = params || {}; + const wrap = i32(p.btexwrap, 1) !== 0; + return [ + 'shader_body', + '{', + ' ret = tex2D(' + (wrap ? 'sampler_main' : 'sampler_fc_main') + ', uv).xyz;', + ' ret *= ' + f32(p.fdecay, 0.98).toFixed(2) + ';', + '}', + ].join('\n'); + } + + /* Sürümü olup metni olmayan birleştirme aşaması (plugin.cpp + GenCompPShaderText). Sıra: yankı ya da düz örnek, gama çarpanı, ton, + sonra dört bayrak — shader'daki biçimleriyle: karekök, kare, + 4c(1-c), 1-c. Yön burada `% 4` görmüyor: 5 iki ekseni de çeviriyor. */ + function genCompText(params) { + const p = params || {}; + const alpha = f32(p.fvideoechoalpha, 0); + const zoom = f32(p.fvideoechozoom, 2); + const orient = i32(p.nvideoechoorientation, 0); + const gamma = f32(p.fgammaadj, 2).toFixed(2); + const hue = f32(p.fshader, 0); + const on = (k) => i32(p[k], 0) !== 0; + const out = ['shader_body', '{']; + if (alpha > 0.001) { + const ox = orient % 2 !== 0 ? -1 : 1; + const oy = orient >= 2 ? -1 : 1; + out.push(' float2 uv_echo = (uv - 0.5)*' + Math.fround(1 / zoom).toFixed(3) + '*float2(' + ox + ',' + oy + ') + 0.5;'); + out.push(' ret = lerp(tex2D(sampler_main, uv).xyz, tex2D(sampler_main, uv_echo).xyz, ' + alpha.toFixed(2) + ');'); + } else { + out.push(' ret = tex2D(sampler_main, uv).xyz;'); + } + out.push(' ret *= ' + gamma + ';'); + if (hue >= 1) out.push(' ret *= hue_shader;'); + else if (hue > 0.001) out.push(' ret *= ' + Math.fround(1 - hue).toFixed(2) + ' + ' + hue.toFixed(2) + '*hue_shader;'); + if (on('bbrighten')) out.push(' ret = sqrt(ret);'); + if (on('bdarken')) out.push(' ret *= ret;'); + if (on('bsolarize')) out.push(' ret = ret*(1-ret)*4;'); + if (on('binvert')) out.push(' ret = 1 - ret;'); + out.push('}'); + return out.join('\n'); + } + + /* Sabit yolun yankı yönü (milkdropfs.cpp:3888, BeatDrop 4066): + `(int)echo_orient % 4` — C'de sıfıra doğru kırpılıyor ve kalan + bölünenin işaretini alıyor; x ekseni `% 2` sıfır değilse, y ekseni + değer 2 ya da üstüyse çevriliyor. Yani -1 x'i çeviriyor, 5 de 1 gibi. + Dönüş: 1 = x, 2 = y, 3 = ikisi. */ + function echoFlipBits(v) { + const o = Math.trunc(Number(v) || 0) % 4; + return (o % 2 !== 0 ? 1 : 0) | (o >= 2 ? 2 : 0); + } + + /* Sabit yolun gaması (milkdropfs.cpp:3955-4010): görüntü tam katları + kadar üst üste TOPLANARAK çiziliyor, son kat kesirli. Yankı açıkken + ilk çizim zaten tam; ek çizim ancak gama 1'i aştığında var, yani + 1'in altındaki gama yankıyla birlikte HİÇ uygulanmıyor. Yankı yokken + tek geçiş gamanın kendisiyle çiziliyor. */ + function fixedGammaGain(g, echoOn) { + const gamma = Math.fround(Number(g) || 0); + if (echoOn) return gamma > 0.001 && Math.trunc(gamma - 0.0001) >= 1 ? gamma : 1; + return gamma > 0 ? gamma : 0; + } + const api = { tokenize, parse, compile, Pool, FUNCS, parseMilk, Preset, - clampColor, colorNorm }; + clampColor, colorNorm, md2Versions, stagePlan, genWarpText, genCompText, + echoFlipBits, fixedGammaGain }; if (typeof module !== 'undefined' && module.exports) module.exports = api; if (typeof window !== 'undefined') window.SVMilkdrop = api; })(); diff --git a/src/visualizer/modes/milkdrop.js b/src/visualizer/modes/milkdrop.js index d5f9925..1370dd8 100644 --- a/src/visualizer/modes/milkdrop.js +++ b/src/visualizer/modes/milkdrop.js @@ -259,6 +259,7 @@ uniform float uEchoAlpha; uniform float uEchoZoom; uniform int uEchoOrient; uniform vec4 uFx; // brighten, darken, solarize, invert +uniform float uFxMd2; // 1: MilkDrop 2'nin sabit yolunun biçimleri (#580) uniform vec3 uHue[4]; // dört köşe rengi: üst-sol, üst-sağ, alt-sol, alt-sağ void main(){ vec3 c = texture(uSrc, vUV).rgb; @@ -277,10 +278,19 @@ void main(){ c = clamp(c, 0.0, 1.0); /* MilkDrop'un MD1 donemi sabit efektleri. Bunlar shader'dan onceki surumlerden kalma ama eski presetlerin cogu hala kullaniyor; yoklugunda - o presetler yazarinin istedigi kontrasti hic gostermiyordu. */ - if (uFx.x > 0.5) c = sqrt(c); + o presetler yazarinin istedigi kontrasti hic gostermiyordu. + + MilkDrop 2'NIN SABIT YOLU bunlari beyaz bir dortgenin HARMANLAMA + gecisleriyle yapiyor (milkdropfs.cpp:4010-4080, BeatDrop'un D3D9 + hali 4200-4285 ile ayni): parlatma once ters cevirip kareliyor ve + yine ceviriyor, yani 1-(1-c)^2 — karekok DEGIL, o shader yolunun + bicimi; solarize d(1-d) sonra kendisini ekliyor, yani 2c(1-c) — 4c(1-c) + degil. Kararti c^2 ve ters cevirme 1-c iki yolda da ayni. Sabit yol + shader'i olmayan presetlerin hepsi: korpusta 2.128, parlatma 410'unda, + solarize 83'unde acik. Uyum kapaliyken eski bicim. */ + if (uFx.x > 0.5) c = uFxMd2 > 0.5 ? 1.0 - (1.0 - c) * (1.0 - c) : sqrt(c); if (uFx.y > 0.5) c = c * c; - if (uFx.z > 0.5) c = c * (1.0 - c) * 4.0; + if (uFx.z > 0.5) c = uFxMd2 > 0.5 ? 2.0 * c * (1.0 - c) : c * (1.0 - c) * 4.0; if (uFx.w > 0.5) c = 1.0 - c; outColor = vec4(clamp(c, 0.0, 1.0), vBlend); }`; @@ -1136,6 +1146,7 @@ void main(){ uEchoZoom: gl.getUniformLocation(this.compFixed, 'uEchoZoom'), uEchoOrient: gl.getUniformLocation(this.compFixed, 'uEchoOrient'), uFx: gl.getUniformLocation(this.compFixed, 'uFx'), + uFxMd2: gl.getUniformLocation(this.compFixed, 'uFxMd2'), uHue: gl.getUniformLocation(this.compFixed, 'uHue[0]'), }; this.locBlur = { @@ -2172,10 +2183,13 @@ void main(){ this.compPreset = null; this.shaderNote = ''; this._shadersLost = false; + // Aşamaları seçen kural uyum anahtarına bağlı (#580): değişince yeniden kurulur + this._stagesAcc = job ? job.acc : this._wantAcc !== false; if (!gl || !T || !this.preset) { this._dropJob(job); return; } /* Arka planda hazırlanmış iş varsa o, yoksa şimdi başlatılıp hemen bitiriliyor — eski senkron yol, aynı adımlarla. */ const j = job || this._beginStages(src); + this._stagesAcc = j.acc; const notes = []; this.warpPreset = this._finishStage(j.warp, notes); this.compPreset = this._finishStage(j.comp, notes); @@ -2188,9 +2202,24 @@ void main(){ _beginStages(src) { const T = window.SVMilkdropShader; const M = window.SVMilkdrop; - const job = { warp: null, comp: null }; + /* Uyum açıkken aşamayı MilkDrop'un kuralı seçiyor: SÜRÜM, metin değil + (shared/milkdrop.js `stagePlan`, #580). Sürümü 0 olan aşamanın + metni okunmuyor; sürümü olup metni olmayan aşamaya MilkDrop'un + yüklemede yazdığı, dosyadaki değerleri gömülü shader veriliyor. + Kapalıyken eski kural: metin varsa shader. `acc` işte taşınıyor ki + anahtar değişince aşamalar yeniden kurulsun. */ + const acc = this._wantAcc !== false; + const job = { warp: null, comp: null, acc }; if (!this.gl || !T || !M) return job; - const fl = M.parseMilk(src); + const parsed = M.parseMilk(src); + let fl = parsed; + if (acc && M.stagePlan) { + const plan = M.stagePlan(parsed); + const text = (stage, own) => (plan[stage] === 'shader' ? own + : plan[stage] === 'generated' ? (stage === 'warp' ? M.genWarpText(parsed.params) : M.genCompText(parsed.params)) + : ''); + fl = { warpShader: text('warp', parsed.warpShader), compShader: text('comp', parsed.compShader) }; + } const begin = (text, stage) => { if (!text || !text.trim()) return null; let r; @@ -2275,12 +2304,65 @@ void main(){ const mix = this._cosMix(); const inv = 1 - mix; for (const k of BLEND_LERP) P.set(k, mix * P.get(k) + inv * O.get(k)); + if (mix < this._snapPoint()) for (const k of BLEND_SNAP) P.set(k, O.get(k)); + } + + /* Mantıksal değişkenlerin geçişte atladığı nokta (milkdropfs.cpp + 667-721, `m_fSnapPoint`): normalde 0,5; yalnız bir tarafın + birleştirme shader'ı varsa -0,01 ya da 1,01 — shader'ı olan taraf o + etkileri kendi içinde uyguluyor. */ + _snapPoint() { const newComp = !!this.compPreset; const oldComp = !!this.oldCompPreset; - let snap = 0.5; - if (oldComp && !newComp) snap = -0.01; - else if (!oldComp && newComp) snap = 1.01; - if (mix < snap) for (const k of BLEND_SNAP) P.set(k, O.get(k)); + if (oldComp && !newComp) return -0.01; + if (!oldComp && newComp) return 1.01; + return 0.5; + } + + /* SABİT BİRLEŞTİRMENİN GİRDİLERİ, MilkDrop 2'nin kuralıyla (#580; + milkdropfs.cpp:3885-4010, BeatDrop 4064-4175 aynı): + - gama ve yankı yakınlaşması dosyada yoksa MilkDrop'un varsayılanı + 2,0 (CState::Default); bizde 1'e düşüyordu, yani gamasız bir + preset yarı parlaklıkta çıkıyordu (varsayılan artık kare başı + tabanında, shared/milkdrop.js MD2_PF_DEFAULTS); + - yön `(int)echo_orient % 4` (shared/milkdrop.js `echoFlipBits`); + - gama görüntüyü üst üste toplayarak çiziyor ve yankıyla birlikte + 1'in altında HİÇ uygulanmıyor (`fixedGammaGain`); + - GEÇİŞTE iki presetin de dosyasında yankı varsa (saydamlık 0,01'in + üstünde) ve yönleri farklıysa, yankı atlama noktasına kadar ESKİ + yönle sönüyor, sonra yeni yönle yeniden beliriyor — yön bir anda + dönmüyor. Karşılaştırılan DOSYADAKİ değerler; MilkDrop da onlara + bakıyor, denklemlerin yazdığına değil. */ + _fixedCompInputs(Pp) { + const M = window.SVMilkdrop; + /* Gama ve yakınlaşma havuzdan: dosyanın yazmadığı adda MilkDrop'un + varsayılanı kare başında zaten orada (shared/milkdrop.js + MD2_PF_DEFAULTS), geçişte karışan da o değer. */ + let alpha = Pp.get('echo_alpha') || 0; + const zoom = Pp.get('echo_zoom'); + let orient = M.echoFlipBits(Pp.get('echo_orient')); + const O = this.oldPreset; + if (O && this.blendProg < 1) { + const fp = (P, k, d) => { + const v = P && P.file && P.file.params ? P.file.params[k] : undefined; + return typeof v === 'number' && isFinite(v) ? v : d; + }; + const na = fp(Pp, 'fvideoechoalpha', 0); + const oa = fp(O, 'fvideoechoalpha', 0); + const no = Math.trunc(fp(Pp, 'nvideoechoorientation', 0)); + const oo = Math.trunc(fp(O, 'nvideoechoorientation', 0)); + if (na > 0.01 && oa > 0.01 && no !== oo) { + const ci = this._cosMix(); + if (this.blendProg < this._snapPoint()) { + // Eski yön dosyadaki tam sayı, `% 4` görmeden + orient = (oo % 2 !== 0 ? 1 : 0) | (oo >= 2 ? 2 : 0); + alpha *= 1 - 2 * ci; + } else { + alpha *= 2 * ci - 1; + } + } + } + return { alpha, zoom, orient, gain: M.fixedGammaGain(Pp.get('gamma'), alpha > 0.001) }; } /* MilkDrop'un dönme matrisleri: rot_s/d/f/vf/uf/rand 1..4. @@ -2958,7 +3040,11 @@ void main(){ havuzu, q değişkenleri ve saat kaybın olduğu yerden sürüyor. Preset bu arada zaten değiştiyse `_buildPresetShaders` bayrağı orada düşürüyor ve burada ikinci kez derlenmiyor. */ - if (this._shadersLost && this._presetSrc) this._buildPresetShaders(this._presetSrc); + /* Uyum anahtarı çevrildiyse de: aşamaları hangi kuralın seçtiği ona + bağlı (#580). Preset nesnesi burada da aynı kalıyor. */ + if ((this._shadersLost || this._stagesAcc !== (this._wantAcc !== false)) && this._presetSrc) { + this._buildPresetShaders(this._presetSrc); + } /* Uyum anahtarı alt blokların hangi kare değişkenlerini gördüğünü de seçiyor (shared/milkdrop.js, SHARED_LEGACY). Her kare yazılıyor: anahtar çizim sürerken değişebiliyor ve geçişteki eski preset de @@ -3366,14 +3452,24 @@ void main(){ geliyor; oradaki değerler `_blendScalars` tarafından zaten karıştırılmış ya da atlatılmış durumda. */ const Pp = this.preset; - const gamma = Pp.get('gamma') || 1; - gl.uniform1f(this.locComp.uGamma, gamma > 0 ? gamma : 1); - gl.uniform1f(this.locComp.uEchoAlpha, Pp.get('echo_alpha') || 0); - gl.uniform1f(this.locComp.uEchoZoom, Pp.get('echo_zoom') || 1); - gl.uniform1i(this.locComp.uEchoOrient, Math.round(Pp.get('echo_orient') || 0)); + const acc = this._wantAcc !== false; + if (acc) { + const f = this._fixedCompInputs(Pp); + gl.uniform1f(this.locComp.uGamma, f.gain); + gl.uniform1f(this.locComp.uEchoAlpha, f.alpha); + gl.uniform1f(this.locComp.uEchoZoom, f.zoom); + gl.uniform1i(this.locComp.uEchoOrient, f.orient); + } else { + const gamma = Pp.get('gamma') || 1; + gl.uniform1f(this.locComp.uGamma, gamma > 0 ? gamma : 1); + gl.uniform1f(this.locComp.uEchoAlpha, Pp.get('echo_alpha') || 0); + gl.uniform1f(this.locComp.uEchoZoom, Pp.get('echo_zoom') || 1); + gl.uniform1i(this.locComp.uEchoOrient, Math.round(Pp.get('echo_orient') || 0)); + } gl.uniform4f(this.locComp.uFx, Pp.get('brighten') ? 1 : 0, Pp.get('darken') ? 1 : 0, Pp.get('solarize') ? 1 : 0, Pp.get('invert') ? 1 : 0); + if (this.locComp.uFxMd2) gl.uniform1f(this.locComp.uFxMd2, acc ? 1 : 0); /* Dört köşe rengi burada da: MilkDrop tam ekran dörtgenini bu renklerle çiziyor (milkdropfs.cpp:3940-3946), shader'lı yolla aynı `shade` dizisinden. Korpusta comp shader'ı olmayan 2.129 diff --git a/tests/milkdrop-blend.test.js b/tests/milkdrop-blend.test.js index f8190d7..5bf9aa2 100644 --- a/tests/milkdrop-blend.test.js +++ b/tests/milkdrop-blend.test.js @@ -217,11 +217,14 @@ function snapOf(s) { } test('atlama noktası motorda da aynı üç durumla yazılmış', () => { + /* Nokta ayrı bir yöntemde (#580): sabit birleştirmenin yankı sönümü de + aynı noktayı kullanıyor, iki yerde ayrı ayrı yazılmasın. */ + const snap = /_snapPoint\(\) \{[\s\S]*?\n \}/.exec(BODY)[0]; + assert.match(snap, /if \(oldComp && !newComp\) return -0\.01;/); + assert.match(snap, /if \(!oldComp && newComp\) return 1\.01;/); + assert.match(snap, /return 0\.5;/); const fn = /_blendScalars\(\) \{[\s\S]*?\n \}/.exec(BODY)[0]; - assert.match(fn, /let snap = 0\.5;/); - assert.match(fn, /if \(oldComp && !newComp\) snap = -0\.01;/); - assert.match(fn, /else if \(!oldComp && newComp\) snap = 1\.01;/); - assert.match(fn, /if \(mix < snap\) for \(const k of BLEND_SNAP\) P\.set\(k, O\.get\(k\)\);/); + assert.match(fn, /if \(mix < this\._snapPoint\(\)\) for \(const k of BLEND_SNAP\) P\.set\(k, O\.get\(k\)\);/); }); // --------------------------------------------------- karıştırılan adlar diff --git a/tests/milkdrop-context-loss.test.js b/tests/milkdrop-context-loss.test.js index 74faf08..f2e11e2 100644 --- a/tests/milkdrop-context-loss.test.js +++ b/tests/milkdrop-context-loss.test.js @@ -175,7 +175,7 @@ test('preset ve saat kayıptan sağ çıkıyor, shader yeniden derleniyor', () = assert.strictEqual(m.time, 42, 'saat sıfırlandı'); assert.strictEqual(m._shadersLost, true, 'shader yeniden derlenmeyecek'); /* Çizim, preset değişmemiş olsa bile shader'ları kaynaktan kuruyor. */ - assert.match(BODY, /if \(this\._shadersLost && this\._presetSrc\) this\._buildPresetShaders\(this\._presetSrc\);/); + assert.match(BODY, /if \(\(this\._shadersLost \|\| this\._stagesAcc !== \(this\._wantAcc !== false\)\) && this\._presetSrc\) \{\s*this\._buildPresetShaders\(this\._presetSrc\);/); assert.match(BODY, /this\._presetSrc = src;/); }); diff --git a/tests/milkdrop-fidelity-comp.test.js b/tests/milkdrop-fidelity-comp.test.js new file mode 100644 index 0000000..eaf9974 --- /dev/null +++ b/tests/milkdrop-fidelity-comp.test.js @@ -0,0 +1,331 @@ +'use strict'; +/* MilkDrop 2'nin sabit birleştirmesi, aşama seçimi ve varsayılanları (#580). + * + * Birincil kaynak jecassis/foo_vis_milk2 5b44cea (Nullsoft'un kodu); sabit + * yolun harmanlama geçişleri BeatDrop 53d83ee'deki D3D9 hâliyle de + * karşılaştırıldı. Buradaki beklentiler o koddan okunan KURALLAR: + * - aşama SÜRÜMDEN seçiliyor, metinden değil (state.cpp:1328-1348); + * - sürümü olup metni olmayan aşamaya MilkDrop yüklemede, dosyadaki + * değerleri gömülü bir shader yazıyor (plugin.cpp GenWarpPShaderText, + * GenCompPShaderText); + * - sabit yolda parlatma 1-(1-c)^2, solarize 2c(1-c), yön `(int)x % 4`, + * gama yankıyla birlikte 1'in altında uygulanmıyor; + * - dosyanın yazmadığı yerleşik adlarda MilkDrop'un varsayılanı. */ +const test = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); + +const M = require('../src/shared/milkdrop.js'); +const S = require('../src/shared/milkdrop-shader.js'); + +const ROOT = path.join(__dirname, '..'); +const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf-8'); +const bare = (s) => s.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1 '); + +// ---------------------------------------------------------- aşama seçimi + +test('sürüm kuralı: MILKDROP_PRESET_VERSION yoksa MilkDrop 1', () => { + const v = (text) => M.md2Versions(M.parseMilk(text).params); + assert.deepStrictEqual(v('[preset00]\nPSVERSION_WARP=2\nPSVERSION_COMP=2\n'), { preset: 100, warp: 0, comp: 0 }); + assert.deepStrictEqual(v('MILKDROP_PRESET_VERSION=199\nPSVERSION=2\n'), { preset: 199, warp: 0, comp: 0 }); + assert.deepStrictEqual(v('MILKDROP_PRESET_VERSION=200\n'), { preset: 200, warp: 2, comp: 2 }); + assert.deepStrictEqual(v('MILKDROP_PRESET_VERSION=200\nPSVERSION=3\nPSVERSION_WARP=0\n'), { preset: 200, warp: 3, comp: 3 }); + assert.deepStrictEqual(v('MILKDROP_PRESET_VERSION=201\n'), { preset: 201, warp: 2, comp: 2 }); + assert.deepStrictEqual(v('MILKDROP_PRESET_VERSION=201\nPSVERSION=3\nPSVERSION_WARP=0\n'), { preset: 201, warp: 0, comp: 2 }); + // %d: kesir aşağı kırpılıyor; sayı olmayan değer varsayılana düşüyor + assert.deepStrictEqual(v('MILKDROP_PRESET_VERSION=201.9\nPSVERSION_WARP=2.7\nPSVERSION_COMP=abc\n'), { preset: 201, warp: 2, comp: 2 }); +}); + +test('aşama: metin + sürüm shader, sürüm tek başına üretilen, sürüm 0 sabit', () => { + const plan = (text) => { const p = M.stagePlan(M.parseMilk(text)); return p.warp + '/' + p.comp; }; + const W = 'warp_1=`shader_body { ret = 1; }\n'; + const C = 'comp_1=`shader_body { ret = 1; }\n'; + assert.strictEqual(plan('MILKDROP_PRESET_VERSION=201\n[preset00]\n' + W + C), 'shader/shader'); + assert.strictEqual(plan('MILKDROP_PRESET_VERSION=201\n[preset00]\n'), 'generated/generated'); + assert.strictEqual(plan('MILKDROP_PRESET_VERSION=201\nPSVERSION_WARP=0\nPSVERSION_COMP=0\n[preset00]\n' + W + C), 'fixed/fixed'); + // MilkDrop 1 dosyasındaki shader metni okunmuyor + assert.strictEqual(plan('[preset00]\n' + W + C), 'fixed/fixed'); + assert.strictEqual(plan('MILKDROP_PRESET_VERSION=201\nPSVERSION_COMP=0\n[preset00]\n' + W), 'shader/fixed'); +}); + +// ---------------------------------------------------- üretilen shader'lar + +test('üretilen warp: decay gömülü, float olarak yuvarlanmış; sarma bayrağı örnekleyiciyi seçiyor', () => { + const t = M.genWarpText(M.parseMilk('fDecay=0.975\nbTexWrap=0\n').params); + /* MilkDrop decay'i 32 bit float okuyor: 0,975 orada 0,97500002 ve + "%.2f" 0,98 yazıyor (double 0,97 olurdu). */ + assert.match(t, /ret \*= 0\.98;/); + assert.match(t, /tex2D\(sampler_fc_main, uv\)/); + assert.match(M.genWarpText({}), /tex2D\(sampler_main, uv\)[\s\S]*ret \*= 0\.98;/, 'varsayılanlar: sarma açık, 0,98'); + for (const text of [t, M.genWarpText({})]) { + const r = S.translate(text, { stage: 'warp' }); + assert.deepStrictEqual(r.hard, []); + assert.deepStrictEqual(r.soft, []); + } +}); + +test('üretilen birleştirme: yankı, gama, ton ve bayraklar shader biçimleriyle ve sırasıyla', () => { + const gen = (hdr) => M.genCompText(M.parseMilk(hdr).params); + // Yankı yok, gama varsayılanı 2 + assert.match(gen(''), /ret = tex2D\(sampler_main, uv\)\.xyz;\s*ret \*= 2\.00;/); + // Yankı: yakınlaşmanın tersi %.3f, saydamlık %.2f; yön 5 iki ekseni de çeviriyor (% 4 yok) + const e = gen('fVideoEchoAlpha=0.35\nfVideoEchoZoom=2\nnVideoEchoOrientation=5\nfGammaAdj=1.5\n'); + assert.match(e, /uv_echo = \(uv - 0\.5\)\*0\.500\*float2\(-1,-1\) \+ 0\.5;/); + assert.match(e, /tex2D\(sampler_main, uv_echo\)\.xyz, 0\.35\);\s*ret \*= 1\.50;/); + assert.match(gen('fVideoEchoAlpha=0.5\nnVideoEchoOrientation=-1\n'), /float2\(-1,1\)/); + assert.match(gen('fVideoEchoAlpha=0.5\nnVideoEchoOrientation=2\n'), /float2\(1,-1\)/); + // Ton: 1 ve üstü tam çarpan, arası karışım + assert.match(gen('fShader=1\n'), /ret \*= hue_shader;/); + assert.match(gen('fShader=0.4\n'), /ret \*= 0\.60 \+ 0\.40\*hue_shader;/); + assert.ok(!/hue_shader/.test(gen('fShader=0.0005\n'))); + // Bayraklar SHADER biçimleriyle ve bu sırayla + const f = gen('bBrighten=1\nbDarken=1\nbSolarize=1\nbInvert=1\n'); + const order = ['ret = sqrt(ret);', 'ret *= ret;', 'ret = ret*(1-ret)*4;', 'ret = 1 - ret;'].map((s) => f.indexOf(s)); + assert.ok(order.every((i) => i > 0), f); + assert.deepStrictEqual(order.slice().sort((a, b) => a - b), order, 'sıra değişmiş'); + for (const text of [gen(''), e, f, gen('fShader=0.4\nfVideoEchoAlpha=0.2\n')]) { + const r = S.translate(text, { stage: 'comp' }); + assert.deepStrictEqual(r.hard, [], text); + } +}); + +// -------------------------------------------------- sabit yolun kuralları + +test('sabit yol yankı yönü: (int)x % 4, C işaret kuralıyla', () => { + const cases = [[0, 0], [1, 1], [2, 2], [3, 3], [4, 0], [5, 1], [6, 2], [7, 3], [1.7, 1], [2.9, 2], + [-1, 1], [-2, 0], [-3, 1], [-4, 0], [-5, 1], [-0.5, 0], [NaN, 0], [undefined, 0]]; + for (const [v, want] of cases) assert.strictEqual(M.echoFlipBits(v), want, String(v)); +}); + +test('sabit yol gaması: yankıyla birlikte 1\'in altı uygulanmıyor', () => { + const on = (g) => M.fixedGammaGain(g, true); + const off = (g) => M.fixedGammaGain(g, false); + assert.strictEqual(on(0.5), 1); + assert.strictEqual(on(0), 1); + assert.strictEqual(on(1), 1); + assert.strictEqual(on(1.00005), 1, 'kırpma sınırı 1,0001'); + assert.strictEqual(on(1.5), 1.5); + assert.strictEqual(on(2), 2); + assert.strictEqual(off(0.5), 0.5); + assert.strictEqual(off(0), 0, 'yankısız gama 0 siyah'); + assert.strictEqual(off(2), 2); +}); + +// ------------------------------------------------------- varsayılanlar + +test('dosyanın yazmadığı yerleşik adlarda MilkDrop\'un varsayılanı; uyum kapalıyken eski taban', () => { + const src = 'fWaveScale=1\nper_frame_1=monitor = 1;'; + const acc = new M.Preset(src, { seed: 1 }); + acc.frame({ time: 0, frame: 0 }); + const want = { decay: 0.98, gamma: 2, echo_zoom: 2, wave_a: 0.8, mv_l: 0.9, mv_x: 12, mv_y: 9, + ib_r: 0.25, ob_size: 0.01, warp: 1, mv_a: 0, wave_brighten: 1 }; + for (const k of Object.keys(want)) assert.strictEqual(acc.get(k), want[k], k); + const legacy = new M.Preset(src, { seed: 1, accurate: false }); + legacy.frame({ time: 0, frame: 0 }); + assert.strictEqual(legacy.get('decay'), 0, 'eski taban: 0'); + assert.strictEqual(legacy.get('gamma'), 0); + assert.strictEqual(legacy.get('wave_a'), 1); +}); + +test('dosyanın yazdığı değer varsayılanın önünde; mv_a bMotionVectorsOn\'dan da geliyor', () => { + const p = new M.Preset('fDecay=0.5\nfGammaAdj=1\nfWaveAlpha=0.3\nbMotionVectorsOn=1\nwave_a=0.6\n', { seed: 1 }); + p.frame({ time: 0, frame: 0 }); + assert.strictEqual(p.get('decay'), 0.5); + assert.strictEqual(p.get('gamma'), 1); + assert.strictEqual(p.get('mv_a'), 1); + /* MilkDrop başlıkta `fWaveAlpha`yı okuyor; `wave_a` yalnız denklem adı, + başlıkta okunmuyor (state.cpp). Başlık adı kazanıyor. */ + assert.strictEqual(p.get('wave_a'), 0.3, 'başlık adı fWaveAlpha kazanmalı'); +}); + +test('init de MilkDrop\'un varsayılanlarını görüyor', () => { + // MilkDrop init'ten önce yerleşik adları yüklüyor (LoadPerFrameEvallibVars) + const p = new M.Preset('per_frame_init_1=q1 = decay + gamma;', { seed: 1 }); + p.frame({ time: 0, frame: 0 }); + assert.ok(Math.abs(p.get('q1') - 2.98) < 1e-12, String(p.get('q1'))); +}); + +test('merkez 0 geçerli: cx/cy 0 köşede kalıyor; sx/sy ve zoom 0 korumada', () => { + const p = new M.Preset('cx=0\ncy=0\nsx=0\nzoom=0\n', { seed: 1 }); + p.frame({ time: 0, frame: 0 }); + const b = p.captureBase(); + assert.strictEqual(b.cx, 0); + assert.strictEqual(b.cy, 0); + assert.strictEqual(b.sx, 1, 'sıfıra bölme: koruma kalıyor'); + assert.strictEqual(b.zoom, 1); + const legacy = new M.Preset('cx=0\ncy=0\n', { seed: 1, accurate: false }); + legacy.frame({ time: 0, frame: 0 }); + assert.strictEqual(legacy.captureBase().cx, 0.5, 'uyum kapalıyken eski davranış'); +}); + +/* MilkDrop'un dosyadan OKUDUĞU anahtarlar (state.cpp CState::Import + 1328-1463, CWave::Import 1203-1221, CShape::Import 1243-1274). Okuma + büyük/küçük harfe duyarlı; kod satırları önek + 1'den başlayan satır + numarası ve ilk eksik numarada duruyor (ReadCode, 1073). Listede + olmayan bir satır MilkDrop'ta hiç okunmuyor: yerleşikler ve üretici + dalga saydamlığını `wave_a` diye yazıyordu, MilkDrop'ta o presetler + dalgayı varsayılan 0,8'le çiziyordu. */ +const MD2_KEYS = new Set([ + 'MILKDROP_PRESET_VERSION', 'PSVERSION', 'PSVERSION_WARP', 'PSVERSION_COMP', + 'fRating', 'fDecay', 'fGammaAdj', 'fVideoEchoZoom', 'fVideoEchoAlpha', 'nVideoEchoOrientation', + 'bRedBlueStereo', 'bBrighten', 'bDarken', 'bSolarize', 'bInvert', 'fShader', + 'b1n', 'b2n', 'b3n', 'b1x', 'b2x', 'b3x', 'b1ed', + 'nWaveMode', 'bAdditiveWaves', 'bWaveDots', 'bWaveThick', 'bModWaveAlphaByVolume', + 'bMaximizeWaveColor', 'fWaveAlpha', 'fWaveScale', 'fWaveSmoothing', 'fWaveParam', + 'fModWaveAlphaStart', 'fModWaveAlphaEnd', 'wave_r', 'wave_g', 'wave_b', 'wave_x', 'wave_y', + 'nMotionVectorsX', 'nMotionVectorsY', 'mv_dx', 'mv_dy', 'mv_l', 'mv_r', 'mv_g', 'mv_b', + 'bMotionVectorsOn', 'mv_a', + 'zoom', 'rot', 'cx', 'cy', 'dx', 'dy', 'warp', 'sx', 'sy', 'bTexWrap', 'bDarkenCenter', + 'fWarpAnimSpeed', 'fWarpScale', 'fZoomExponent', + 'ob_size', 'ob_r', 'ob_g', 'ob_b', 'ob_a', 'ib_size', 'ib_r', 'ib_g', 'ib_b', 'ib_a', +]); +const MD2_WAVE_KEYS = ['enabled', 'samples', 'sep', 'bSpectrum', 'bUseDots', 'bDrawThick', + 'bAdditive', 'scaling', 'smoothing', 'r', 'g', 'b', 'a']; +const MD2_SHAPE_KEYS = ['enabled', 'sides', 'additive', 'thickOutline', 'textured', 'num_inst', + 'x', 'y', 'rad', 'ang', 'tex_ang', 'tex_zoom', 'r', 'g', 'b', 'a', 'r2', 'g2', 'b2', 'a2', + 'border_r', 'border_g', 'border_b', 'border_a']; +const MD2_CODE = /^(per_frame_init_|per_frame_|per_pixel_|warp_|comp_|wave_[0-3]_(?:init|per_frame|per_point)|shape_[0-3]_(?:init|per_frame))([1-9]\d*)$/; +function md2Reads(key) { + if (MD2_KEYS.has(key) || MD2_CODE.test(key)) return true; + const w = /^wavecode_[0-3]_(\w+)$/.exec(key); + if (w) return MD2_WAVE_KEYS.includes(w[1]); + const s = /^shapecode_[0-3]_(\w+)$/.exec(key); + return !!s && MD2_SHAPE_KEYS.includes(s[1]); +} + +test('kendi presetlerimiz yalnız MilkDrop\'un okuduğu anahtarları, birer kez ve boşluksuz yazıyor', () => { + const B = require('../src/shared/presets-milkdrop.js'); + const G = require('../src/shared/milkdrop-generator.js'); + const X = require('../src/shared/milkdrop-mashup.js'); + const texts = B.map((p) => [p.id, p.source]); + for (let i = 0; i < 64; i++) { + const r = G.generate({ energy: (i * 37) % 101, warmth: (i * 53) % 101, + density: (i * 71) % 101, motion: (i * 89) % 101, seed: i * 7919 }); + texts.push([r.code, r.source]); + } + // Karışımlar: satırlar vericilerden olduğu gibi, sürüm satırları yeniden + const own = texts.length; + for (let i = 0; i < 30; i++) { + const d = {}; + X.SLOTS.forEach((slot, k) => { d[slot] = texts[(i + k * 7) % own][1]; }); + texts.push(['karışım ' + i, X.compose(d)]); + } + let checked = 0; + for (const [id, text] of texts) { + const seen = new Set(); + const code = new Map(); + for (const line of text.split(/\r?\n/)) { + if (line === '' || line.startsWith('[')) continue; + assert.ok(!/^\s/.test(line), id + ': girintili satır MilkDrop\'ta okunmuyor: ' + line); + const eq = line.indexOf('='); + assert.ok(eq > 0, id + ': anahtarsız satır: ' + line); + const key = line.slice(0, eq); + assert.ok(md2Reads(key), id + ': MilkDrop bu anahtarı okumuyor: ' + key); + assert.ok(!seen.has(key), id + ': anahtar iki kez (MilkDrop ilkini okuyor): ' + key); + seen.add(key); + const c = MD2_CODE.exec(key); + if (c) code.set(c[1], (code.get(c[1]) || []).concat(+c[2])); + checked++; + } + for (const [prefix, nums] of code) { + const sorted = nums.slice().sort((a, b) => a - b); + sorted.forEach((n, k) => assert.strictEqual(n, k + 1, id + ': ' + prefix + ' numarasında boşluk')); + } + } + assert.ok(texts.length === 5 + 64 + 30 && checked > 10000, 'yeterince satır denetlendi: ' + checked); +}); + +// ----------------------------------------------------------------- motor + +function engine() { + const canvas = () => ({ width: 320, height: 240, getContext: (k) => (k === '2d' ? {} : null), addEventListener() {} }); + const ctx = { window: {}, document: { createElement: canvas }, console, performance }; + ctx.window.document = ctx.document; + vm.createContext(ctx); + for (const f of ['src/shared/milkdrop.js', 'src/shared/milkdrop-hlsl.js', 'src/shared/milkdrop-shader.js', 'src/visualizer/modes/milkdrop.js']) { + vm.runInContext(read(f), ctx, { filename: f }); + } + const gl = new Proxy({}, { + get(_, k) { + const name = String(k); + if (name === 'getExtension') return () => null; + if (/^[A-Z_0-9]+$/.test(name)) return name; + if (/^create/.test(name)) return () => ({ id: name }); + if (name === 'getProgramParameter' || name === 'getShaderParameter') return () => true; + if (name === 'getUniformLocation') return (_p, u) => u; + return () => undefined; + }, + }); + const m = new ctx.window.SVModes.milkdrop(canvas()); + m.gl = gl; + return { m, M: ctx.window.SVMilkdrop }; +} + +test('motor: uyum açıkken aşamayı sürüm seçiyor, kapalıyken metin', () => { + const { m } = engine(); + const md1 = '[preset00]\nwarp_1=`shader_body { ret = tex2D(sampler_main, uv).xyz; }\n'; + m._wantAcc = true; + const a = m._beginStages(md1); + assert.strictEqual(a.acc, true); + assert.strictEqual(a.warp, null, 'MilkDrop 1 dosyasının shader metni okunmamalı'); + m._wantAcc = false; + const b = m._beginStages(md1); + assert.strictEqual(b.acc, false); + assert.ok(b.warp && b.warp.text, 'uyum kapalıyken eski kural: metin varsa shader'); + // Sürümü var, metni yok: üretilen shader derleniyor + m._wantAcc = true; + const g = m._beginStages('MILKDROP_PRESET_VERSION=201\n[preset00]\nfDecay=0.9\n'); + assert.ok(g.warp && /ret \*= 0\.90;/.test(g.warp.text), 'üretilen warp'); + assert.ok(g.comp && /ret \*= 2\.00;/.test(g.comp.text), 'üretilen birleştirme'); +}); + +test('motor: geçişte yönü farklı iki yankı sönüp yeniden beliriyor', () => { + const { m, M: MM } = engine(); + m._wantAcc = true; + const mk = (orient, alpha) => { + const p = new MM.Preset('fVideoEchoAlpha=' + alpha + '\nnVideoEchoOrientation=' + orient + '\nfVideoEchoZoom=1\nfGammaAdj=1\n', { seed: 1 }); + p.frame({ time: 0, frame: 0 }); + return p; + }; + m.preset = mk(2, 0.5); + m.oldPreset = mk(1, 0.5); + m.compPreset = null; + m.oldCompPreset = null; + const ci = (p) => 0.5 - 0.5 * Math.cos(Math.PI * p); + m.blendProg = 0.25; + let f = m._fixedCompInputs(m.preset); + assert.strictEqual(f.orient, 1, 'atlama noktasından önce eski yön'); + assert.ok(Math.abs(f.alpha - 0.5 * (1 - 2 * ci(0.25))) < 1e-12, String(f.alpha)); + m.blendProg = 0.75; + f = m._fixedCompInputs(m.preset); + assert.strictEqual(f.orient, 2, 'sonra yeni yön'); + assert.ok(Math.abs(f.alpha - 0.5 * (2 * ci(0.75) - 1)) < 1e-12, String(f.alpha)); + // Yarı yolda yankı tamamen sönmüş: yön bir anda dönmüyor + m.blendProg = 0.5; + assert.ok(Math.abs(m._fixedCompInputs(m.preset).alpha) < 1e-12); + // Bir tarafın dosyasında yankı yoksa sönüm yok + m.oldPreset = mk(1, 0); + m.blendProg = 0.25; + f = m._fixedCompInputs(m.preset); + assert.strictEqual(f.alpha, 0.5); + assert.strictEqual(f.orient, 2); + // Geçiş yokken yön (int) % 4 + m.oldPreset = null; + m.preset = mk(5, 0.5); + assert.strictEqual(m._fixedCompInputs(m.preset).orient, 1); +}); + +test('sabit birleştirme shader\'ı: MilkDrop 2 biçimleri anahtarın arkasında', () => { + const code = bare(read('src/visualizer/modes/milkdrop.js')); + assert.match(code, /if \(uFx\.x > 0\.5\) c = uFxMd2 > 0\.5 \? 1\.0 - \(1\.0 - c\) \* \(1\.0 - c\) : sqrt\(c\);/); + assert.match(code, /if \(uFx\.z > 0\.5\) c = uFxMd2 > 0\.5 \? 2\.0 \* c \* \(1\.0 - c\) : c \* \(1\.0 - c\) \* 4\.0;/); + assert.match(code, /if \(uFx\.y > 0\.5\) c = c \* c;/); + assert.match(code, /if \(uFx\.w > 0\.5\) c = 1\.0 - c;/); + assert.match(code, /gl\.uniform1f\(this\.locComp\.uFxMd2, acc \? 1 : 0\);/); + // Sabit geçiş uyum açıkken girdilerini MilkDrop'un kuralından alıyor + assert.match(code, /const acc = this\._wantAcc !== false;\s*if \(acc\) \{\s*const f = this\._fixedCompInputs\(Pp\);/); + assert.match(code, /gl\.uniform1i\(this\.locComp\.uEchoOrient, f\.orient\);/); + assert.match(code, /gl\.uniform1f\(this\.locComp\.uGamma, f\.gain\);/); +}); diff --git a/tests/milkdrop-frame-reset.test.js b/tests/milkdrop-frame-reset.test.js index abefcbc..56a15f4 100644 --- a/tests/milkdrop-frame-reset.test.js +++ b/tests/milkdrop-frame-reset.test.js @@ -77,9 +77,15 @@ test('per_frame_init yerleşik ada yazarsa değer atılıyor', () => { }); test('dosya değeri yazılmayan yerleşik adlarda da geri geliyor', () => { + /* Dosyanın yazmadığı adda taban MilkDrop'un varsayılanı (#580): dalga + saydamlığı 0,8 (state.cpp CState::Default). Uyum kapalıyken eski + taban, 1. */ const p = mk('fWaveScale=1.5\nper_frame_1=wave_a = wave_a * 0.5;'); run(p, 4); - assert.strictEqual(p.get('wave_a'), 0.5, 'varsayılan 1 üzerinden, birikmeden'); + assert.strictEqual(p.get('wave_a'), 0.4, 'MilkDrop varsayılanı 0,8 üzerinden, birikmeden'); + const legacy = new MD.Preset('fWaveScale=1.5\nper_frame_1=wave_a = wave_a * 0.5;', { seed: 1, accurate: false }); + run(legacy, 4); + assert.strictEqual(legacy.get('wave_a'), 0.5, 'uyum kapalıyken eski varsayılan 1'); }); test('sıfırlama listesi MilkDrop\'un kendi listesi kadar', () => { diff --git a/tests/milkdrop-prefetch.test.js b/tests/milkdrop-prefetch.test.js index 86b1541..610a61d 100644 --- a/tests/milkdrop-prefetch.test.js +++ b/tests/milkdrop-prefetch.test.js @@ -115,7 +115,8 @@ const bare = (s) => s.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\ const shaderPreset = (id, gain) => ({ id, name: id.toUpperCase(), kind: 'milkdrop', - source: ['[preset00]', 'fDecay=0.97', 'PSVERSION=2', 'PSVERSION_WARP=2', 'PSVERSION_COMP=2', + // MILKDROP_PRESET_VERSION'suz dosyanın shader'ları okunmuyor (#580) + source: ['MILKDROP_PRESET_VERSION=201', '[preset00]', 'fDecay=0.97', 'PSVERSION=2', 'PSVERSION_WARP=2', 'PSVERSION_COMP=2', 'warp_1=`shader_body', 'warp_2=`{', 'warp_3=` ret = tex2D(sampler_main, uv).xyz * ' + gain + ';', 'warp_4=`}', 'comp_1=`shader_body', 'comp_2=`{', 'comp_3=` ret = tex2D(sampler_main, uv).xyz;', 'comp_4=`}', ''].join('\n'), }); diff --git a/tests/milkdrop-switch-async.test.js b/tests/milkdrop-switch-async.test.js index b650190..84a6bee 100644 --- a/tests/milkdrop-switch-async.test.js +++ b/tests/milkdrop-switch-async.test.js @@ -24,7 +24,10 @@ const bare = (s) => s.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\ /* Warp ve comp shader'ı olan küçük presetler: değişim gerçekten derleme gerektirsin. */ +/* MILKDROP_PRESET_VERSION şart: yoksa MilkDrop dosyayı MilkDrop 1 sayıyor + ve shader'larını okumuyor; uyum açıkken motor da (#580). */ const shaderPreset = (decay, gain) => [ + 'MILKDROP_PRESET_VERSION=201', '[preset00]', 'fDecay=' + decay, 'PSVERSION=2', 'PSVERSION_WARP=2', 'PSVERSION_COMP=2', From b6cb9ef15d7ab1f216c71851dd8e459a752dd117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Wed, 23 Sep 2026 16:33:28 +0300 Subject: [PATCH 3/7] fix(milkdrop): mash-ups follow MilkDrop's stage rule A preset gives a mash-up its warp or composite part only if MilkDrop would draw that shader: besides a non-empty shader line, the stage's version under MilkDrop's rule must be above 0. A MilkDrop 1 file, or one whose PSVERSION_WARP is 0, no longer gives a part that neither MilkDrop nor the engine draws. The version written for a donated shader is the one its preset has for that stage, by the same rule as the engine's md2Versions. A version value that is not wholly a number counts as missing, as in the parser, and takes the rule's default. The part test is now checked against the engine's stage choice rather than against the presence of shader text, with eight version edge cases added; composing with a warp from a PSVERSION_WARP=0 preset writes version 0 and the engine draws the fixed warp. Refs #579, #580, #560 --- src/shared/milkdrop-mashup.js | 61 ++++++++++++++++++++++++++++------- tests/milkdrop-mashup.test.js | 20 ++++++++++-- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/shared/milkdrop-mashup.js b/src/shared/milkdrop-mashup.js index 4445e17..eff884e 100644 --- a/src/shared/milkdrop-mashup.js +++ b/src/shared/milkdrop-mashup.js @@ -60,6 +60,24 @@ return 'look'; } + /* Sürüm satırının değeri, ayrıştırıcının kuralıyla: değerin TAMAMI + sayıysa sayı, değilse "yok" (NaN; sürüm kuralı onu varsayılana + düşürüyor). Aynı anahtar iki kez geçerse sonuncusu — ayrıştırıcı da + öyle tutuyor. */ + const numVal = (raw) => { + const n = parseFloat(raw); + return isFinite(n) && /^[\s\-+.0-9eE]+$/.test(raw) ? n : NaN; + }; + + // Yalnız sürüm satırları, metni ayrıştırmadan (aday sınaması için) + function versionsOf(text) { + const ver = {}; + const re = /^[ \t]*(milkdrop_preset_version|psversion_warp|psversion_comp|psversion)[ \t]*=(.*)$/gmi; + let m; + while ((m = re.exec(text)) !== null) ver[m[1].toLowerCase()] = numVal(m[2].replace(/\s+$/, '')); + return ver; + } + /* Metni parçalarına ayırır. Eşittirsiz satırlar ve bölüm başlıkları düşüyor (ayrıştırıcı da onları okumuyor). Dönüş: parça -> satırlar ve sürüm satırlarının değerleri. */ @@ -74,8 +92,7 @@ const key = line.slice(0, eq).trim().toLowerCase(); const slot = slotOf(key); if (slot === 'version') { - const v = parseFloat(line.slice(eq + 1)); - if (isFinite(v)) ver[key] = v; + ver[key] = numVal(line.slice(eq + 1)); continue; } /* Satırın iki ucu kırpılmış hâli: ayrıştırıcı da satırı öyle okuyor. @@ -92,8 +109,17 @@ motion — boş olmayan bir denklem satırı (yorum dışında) waves/shapes — açık (enabled sıfırdan farklı) bir blok - warp/comp — boş olmayan bir shader satırı */ + warp/comp — boş olmayan bir shader satırı VE o aşamanın sürümü + sıfırdan büyük: MilkDrop sürümü 0 olan aşamanın metnini okumuyor, + motor da (#580); öyle bir preset o parçayı veremez */ function has(text, slot) { + if (slot === 'warp' || slot === 'comp') { + return hasLine(text, slot) && stageVersion(versionsOf(String(text == null ? '' : text)), slot) > 0; + } + return hasLine(text, slot); + } + + function hasLine(text, slot) { if (slot === 'look') return true; const s = String(text == null ? '' : text); let re; @@ -126,17 +152,30 @@ return false; } - /* Bir parçanın shader sürümü: parçanın kendi satırı, yoksa genel sürüm, - yoksa 2; shader satırı yoksa 0. Yalnız `warp_N`/`comp_N` satırlarına - bakılıyor: birleştirme parçasında bulanıklık aralıkları da var ve - shader'ı olmayan bir presette onları shader sanmak, 308 MilkDrop 1 - presetine olmayan bir shader için sürüm satırı yazıyordu (ölçüm). */ + /* Bir aşamanın MilkDrop'taki sürümü, MilkDrop'un kuralıyla (#580; + motordaki eşi shared/milkdrop.js `md2Versions`, test ikisini + karşılaştırıyor): MILKDROP_PRESET_VERSION yoksa ya da 200'den küçükse + 0, tam 200'de PSVERSION (yoksa 2), üstünde PSVERSION_WARP/_COMP + (yoksa 2). Sürümü 0 olan aşamanın metni MilkDrop'ta da motorda da + okunmuyor. */ + function stageVersion(ver, slot) { + const int = (v, d) => (typeof v === 'number' && isFinite(v) ? Math.trunc(v) : d); + const pv = int(ver.milkdrop_preset_version, 100); + if (pv < 200) return 0; + if (pv === 200) return int(ver.psversion, 2); + return int(ver['psversion_' + slot], 2); + } + + /* Bir parçanın yazılacak shader sürümü: shader satırı yoksa 0, varsa + onu veren presetin o aşamadaki sürümü — MilkDrop'ta okunmayan bir + shader karışımda da okunmayan kalıyor. Yalnız `warp_N`/`comp_N` + satırlarına bakılıyor: birleştirme parçasında bulanıklık aralıkları da + var ve shader'ı olmayan bir presette onları shader sanmak, 308 MilkDrop + 1 presetine olmayan bir shader için sürüm satırı yazıyordu (ölçüm). */ function shaderVersion(parts, slot) { const re = slot === 'warp' ? /^warp_\d+[ \t]*=/i : /^comp_\d+[ \t]*=/i; if (!parts.lines[slot].some((l) => re.test(l) && l.slice(l.indexOf('=') + 1).replace(/^`/, '').trim())) return 0; - const own = parts.ver['psversion_' + slot]; - if (own > 0) return own; - return parts.ver.psversion > 0 ? parts.ver.psversion : 2; + return stageVersion(parts.ver, slot); } /* Karışımı yazar. `donors`: parça -> preset metni. warp ve comp için diff --git a/tests/milkdrop-mashup.test.js b/tests/milkdrop-mashup.test.js index 7290988..68a0f85 100644 --- a/tests/milkdrop-mashup.test.js +++ b/tests/milkdrop-mashup.test.js @@ -150,6 +150,13 @@ test('sürüm satırları shader\'ı veren presetten', () => { koruyor: tek presetten kurulan karışım kendisini birebir vermeli */ const md2Plain = 'MILKDROP_PRESET_VERSION=201\nPSVERSION=2\nPSVERSION_WARP=0\nPSVERSION_COMP=0\n[preset00]\nfDecay=0.95\nper_frame_1=zoom = 1.01;\n'; assert.deepStrictEqual(ver(X.compose({ look: md2Plain, motion: md2Plain, waves: md2Plain, shapes: md2Plain, warp: md2Plain, comp: md2Plain })), [201, 2, 0, 0]); + /* MilkDrop'un okumadığı bir shader karışımda da okunmayan kalıyor + (#580): warp metni olan ama PSVERSION_WARP=0 yazan presetten gelen + warp parçasının sürümü 0 yazılıyor, 2 değil. */ + const inert = 'MILKDROP_PRESET_VERSION=201\nPSVERSION=2\nPSVERSION_WARP=0\nPSVERSION_COMP=0\n[preset00]\nwarp_1=`shader_body { ret = 0.5; }\n'; + const inertOut = M.parseMilk(X.compose({ look: md1, motion: md1, waves: md1, shapes: md1, warp: inert, comp: md2 })); + assert.strictEqual(inertOut.params.psversion_warp, 0); + assert.strictEqual(M.stagePlan(inertOut).warp, 'fixed'); // Yalnız birleştirme: warp sürümü 0 assert.deepStrictEqual(ver(X.compose({ look: md1, motion: md1, waves: md1, shapes: md1, warp: X.NONE, comp: md2 })), [201, 2, 0, 2]); // Presetin kendi sürümü korunuyor (ps_3_0 shader'ı 2 ile okunmaz) @@ -178,8 +185,8 @@ test('"parçası var mı" ayrıştırıcıyla aynı cevabı veriyor', () => { if (slot === 'motion') return !!(f.init.trim() || f.perFrame.trim() || f.perPixel.trim()); if (slot === 'waves') return enabled('wavecode'); if (slot === 'shapes') return enabled('shapecode'); - if (slot === 'warp') return !!f.warpShader.trim(); - return !!f.compShader.trim(); + // Shader parçası: motorun kuralıyla bu presetin shader'ı çalışıyor mu (#580) + return M.stagePlan(f)[slot] === 'shader'; }; const edge = [ '[preset00]\nper_frame_1=// yalnız yorum\n', @@ -196,6 +203,15 @@ test('"parçası var mı" ayrıştırıcıyla aynı cevabı veriyor', () => { '[preset00]\nwarp_1=`\nwarp_2=` \n', '[preset00]\nwarp_1=`shader_body\ncomp_1=\n', '[preset00]\ncomp_1=`ret = 1;\n', + // Sürüm kuralı (#580): MilkDrop sürümü 0 olan aşamanın metnini okumuyor + 'MILKDROP_PRESET_VERSION=201\n[preset00]\nwarp_1=`shader_body\ncomp_1=`ret = 1;\n', + 'MILKDROP_PRESET_VERSION=201\nPSVERSION_WARP=0\nPSVERSION_COMP=3\n[preset00]\nwarp_1=`shader_body\ncomp_1=`ret = 1;\n', + 'MILKDROP_PRESET_VERSION=200\nPSVERSION=0\n[preset00]\nwarp_1=`shader_body\n', + 'MILKDROP_PRESET_VERSION=200\n[preset00]\nwarp_1=`shader_body\n', + 'MILKDROP_PRESET_VERSION=199\nPSVERSION_WARP=2\n[preset00]\nwarp_1=`shader_body\n', + 'MILKDROP_PRESET_VERSION=201\nPSVERSION_WARP=abc\n[preset00]\nwarp_1=`shader_body\n', + 'MILKDROP_PRESET_VERSION=201\nPSVERSION_WARP=2\nPSVERSION_WARP=0\n[preset00]\nwarp_1=`shader_body\n', + 'milkdrop_preset_version=201.7\npsversion_comp=2.9\n[preset00]\ncomp_1=`ret = 1;\n', '', ]; for (const t of edge.concat(TEXTS)) { From d00b48e273c53392fadef177a3f7e4330b3e9d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Wed, 23 Sep 2026 16:34:26 +0300 Subject: [PATCH 4/7] docs: MilkDrop fidelity follow-ups The stage rule, the fixed composite, MilkDrop's defaults and the fWaveAlpha key, with the measurements, in the README feature list and a ROADMAP entry under #580. The entry also records what was found while finishing this and comes next: preset shaders get the full hue colour in MilkDrop whatever fShader says, and a few keys fall back to other values when a file leaves them out. Refs #580 --- README.md | 10 +++++ README.tr.md | 10 +++++ ROADMAP.md | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) diff --git a/README.md b/README.md index b7d9c6a..641d36e 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,16 @@ that asserts the bar profile has no step in it. correcting for a wide screen corrected the wrong axis. The blurred copy is darkened at its edges (`b1ed`, 68.6% ask for it), and a custom wave is drawn at MilkDrop's amplitude and reads the spectrum when it asks for the spectrum (23.2% do). +- **Presets without a shader of their own look as they do in MilkDrop 2.** MilkDrop picks a preset's + warp and composite shaders by the file's version, not by whether shader text is present, and + draws the rest with a fixed pipeline built from blend passes. Checked against MilkDrop 2's source + and the original D3D9 code: brighten there is `1−(1−c)²` and solarize `2c(1−c)`, not the + `sqrt(c)` and `4c(1−c)` the engine used (410 and 83 presets of a 10,332-preset corpus turn them + on); echo orientation is `(int)x % 4`; gamma below 1 is ignored while echo is on; and when two + presets whose echoes point different ways blend, the echo fades out and back in instead of + flipping. Keys a preset does not write take MilkDrop's defaults — decay 0.98, gamma 2.0 — instead + of 0, and a rotation centre of 0 stays in the corner. Rendered through the engine, every case + matches MilkDrop's formula within 2/255. - **Textured shapes, motion vectors, rotation matrices, mesh density, internal resolution scale, mouse input and preset transitions** are all implemented, and each sampler is read with the filtering and wrapping its name asks for (`sampler_pw_main` is point-sampled, `sampler_fc_main` is diff --git a/README.tr.md b/README.tr.md index 197cbdd..b308e22 100644 --- a/README.tr.md +++ b/README.tr.md @@ -238,6 +238,16 @@ profilinde basamak olmadığını doğruluyor. değiştirmişti, yani geniş ekran düzeltmesi yanlış eksene uygulanıyordu. Bulanık kopya kenarında karartılıyor (`b1ed`, %68,6'sı istiyor), özel dalgalar MilkDrop genliğinde çiziliyor ve tayf isteyen dalga tayfı okuyor (%23,2). +- **Kendi shader'ı olmayan presetler MilkDrop 2'deki gibi görünüyor.** MilkDrop bir presetin warp + ve birleştirme shader'ını shader metninin varlığından değil dosyanın sürümünden seçiyor, gerisini + harmanlama geçişlerinden kurulu sabit bir yolla çiziyor. MilkDrop 2'nin kaynağına ve özgün D3D9 + koduna göre denetlendi: orada parlatma `1−(1−c)²`, solarize `2c(1−c)` — motorun kullandığı + `sqrt(c)` ve `4c(1−c)` değil (10.332 presetlik bir korpusta 410 preset parlatmayı, 83'ü + solarize'ı açıyor); yankı yönü `(int)x % 4`; yankı açıkken 1'in altındaki gama uygulanmıyor; + yankıları farklı yöne bakan iki preset geçişte karışırken yankı bir anda dönmüyor, sönüp + yeniden beliriyor. Presetin yazmadığı anahtarlar 0 yerine MilkDrop'un varsayılanlarını alıyor — + sönme 0,98, gama 2,0 — ve dönme merkezi 0 ise köşede kalıyor. Motordan geçirilerek çizildi: + her durum MilkDrop'un formülüyle 2/255 içinde aynı. - **Dokulu şekiller, hareket vektörleri, dönme matrisleri, ağ sıklığı, iç çözünürlük, fare girdisi ve preset geçişleri** uygulandı; her sampler adının istediği süzme ve sarma ile okunuyor (`sampler_pw_main` noktasal, `sampler_fc_main` süzülmüş+kenetli — presetlerin %22,7'si aynı diff --git a/ROADMAP.md b/ROADMAP.md index 7027e8a..95d076b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2028,6 +2028,111 @@ rest after. No version number yet. was equivalent, and the branch it changed was removed. Putting back the old ring nouns, the " · " in mash-up names or removing the module's script tag each fails a test. +- **Fidelity follow-ups (#580)** · the fixed composite, the stage rule and + MilkDrop's defaults are done; the other audits and the file-reading + differences are next. + - **Checked against the source.** Nullsoft's own code + (jecassis/foo_vis_milk2 5b44cea) and, for the fixed pipeline's blend + passes, the D3D9 code the D3D11 fork was ported from (BeatDrop + 53d83ee). Both agree on everything below. + - **Which stage draws.** MilkDrop picks a preset's warp and composite + shaders by version, not by whether shader text exists. A file with no + `MILKDROP_PRESET_VERSION`, or one below 200, is a MilkDrop 1 preset and + any shader text in it is ignored; 200 uses `PSVERSION`, 201 and above + `PSVERSION_WARP` and `PSVERSION_COMP`, each 2 when missing. A stage with + a version but no text gets a shader MilkDrop writes at load time with + the file's decay, gamma, echo, hue and flags baked in, so per-frame + changes to them are ignored there. With fidelity on the engine follows + this, and switching fidelity rebuilds the running preset's stages. In + the corpus it changes one preset: a composite with a version and no + text, whose per-frame code writes gamma and echo that MilkDrop ignores. + The corpus has no MilkDrop 1 file carrying shader text. + - **The fixed composite** — every preset without a composite shader, + 2,128 in the corpus. MilkDrop draws its flags with blend passes of a + white quad: brighten inverts, squares and inverts again, so it is + `1−(1−c)²`, and solarize multiplies by the inverse and then adds the + result to itself, so it is `2c(1−c)`. The engine used `sqrt(c)` and + `4c(1−c)`, which are what MilkDrop writes into a shader it generates, + not what it draws for these presets. 410 presets turn brighten on and 83 + solarize. Echo orientation is `(int)x % 4` with C's sign rule, where the + engine rounded (38 presets write `echo_orient` per frame). Gamma draws + the picture again additively, so below 1 it is not applied while echo is + on (2 presets). When two presets whose echoes point different ways + blend, the echo now fades out before the snap point and back in after + it, instead of flipping. + - **MilkDrop's defaults for keys a file does not write**: decay 0.98, + gamma 2.0, echo zoom 2.0, wave alpha 0.8, motion-vector length 0.9 and + grid 12×9, border size 0.01, inner border colour 0.25. The per-frame + reset wrote the pool's 0 back every frame, so a preset without `fDecay` + left no trail and one without `fGammaAdj` rendered at half brightness, + and the engine's own "decay missing → 0.98" check never fired because + the reset made the name exist. Init code sees the defaults too, as in + MilkDrop. + - **A centre of 0 is a centre.** `cx` and `cy` of 0 put rotation and + stretch in the corner; the engine turned an explicit 0 into 0.5 (84 + presets write it in the header). A zoom or stretch of 0 still falls + back, since it divides by zero in MilkDrop too. + - **Our own presets write MilkDrop's key.** The five builtin MilkDrop + presets and the generator wrote the main wave's alpha as `wave_a`, the + name per-frame code uses. MilkDrop reads `fWaveAlpha` from the file — + every corpus preset writes that and none writes `wave_a` there — so in + MilkDrop these presets drew the wave at its default 0.8. They now write + `fWaveAlpha`. The engine reads both, so nothing changes on screen here: + the same generator code gives the same picture, and its golden hashes + were renewed without a new generator version. + - **Measured.** Rendered through the engine on the GPU: a warp shader + writes a known flat colour — or a UV gradient for the echo — into the + buffer, the composite takes the fixed path, and every flag, gamma and + orientation case read back within 2/255 of MilkDrop's formula, with + fidelity on and with it off (66 of 66). The 900-preset sample with the + texture pack sorts into the same classes before and after (882 clean, + 12 blown, 4 frozen, 2 black). The last ten of 60 frames compared between + `main` and this change: among 120 brighten presets, 99 changed by more + than 1% (median 3.8%, darks darker: median brightness 0.181 → 0.152); + among the 83 solarize presets, 63 changed and the median brightness + roughly halved (0.291 → 0.112), with four more near black; 23 of 49 + presets with a centre of 0 and 7 of 38 that write `echo_orient` changed. + - **Mash-ups follow the same rule.** A preset whose shader MilkDrop + ignores cannot give that part, and the version written for a shader is + the one its preset has for that stage. + - **Found while finishing this, fixed next.** MilkDrop hands every + composite shader the full hue colour ("since we don't know if shader + uses it or not", in both the D3D11 and the D3D9 code); `fShader` scales + it only on the fixed path and in the shader MilkDrop writes for a + preset without one. The engine has applied `fShader` to preset shaders + too since the `hue_shader` change of 16 September, so with fidelity on + the 914 corpus presets that read `hue_shader` and leave `fShader` at 0 + lost their colour, and 36 got part of it. The engine also takes the + amount from per-frame values: 16 presets write `fshader` in code, + which MilkDrop never sees, all of them with a composite shader. Some + keys fall back to something else when a file leaves them out. MilkDrop + reads a missing `wave_r`, `wave_g`, `wave_b`, `wave_x` or `wave_y` as + `rot`'s value at that point, which is 0, where the engine has 1, 1, 1, + 0.5 and 0.5. Missing wave smoothing is 0.75, and the volume fade runs + from 0.75 to 0.95. Custom shapes default to red inside and green + outside. No corpus preset leaves any of these out, and neither do ours. + - **Not done yet:** the fixed warp path, the blur chain, borders and + centre darkening, and the rest of the blend snap points; the + file-reading differences found while building the mash-ups (a + duplicated key, where MilkDrop reads the first occurrence and we keep + the last; numbered code that MilkDrop stops reading at the first + missing number; `\\` comments; integer keys holding fractions; text + after a number; key case; indented lines — about 40 corpus presets in + all); the reference comparison with an external renderer, which needs + one installed and waits for the user's approval. + - **Tests.** 14 new: the version rule and the stage choice, the two + generated shaders (float rounding, samplers, echo, hue, flag order, + and that they translate), echo orientation and gamma cases, the + defaults with fidelity on and off, init seeing them, a header value + winning over them, the centre of 0, the engine choosing stages by + version and by text, the echo fade during a blend, the shader's + formulas behind the switch, and our own presets — the builtins, 64 + generated ones and 30 mash-ups of them — writing only keys MilkDrop + reads, each once, unindented, with code lines numbered from 1 without + a gap. Seven older tests were updated: two used presets with + `PSVERSION` lines but no `MILKDROP_PRESET_VERSION`, which MilkDrop + reads as MilkDrop 1, and the generator's golden hashes and wave-alpha + check follow the new key. 35 of 35 mutations are caught. ## v3.1.6 — Comprehensive video export From 2e23e3cac69774443976c56d4cd650b6b0f7da82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Wed, 23 Sep 2026 16:38:49 +0300 Subject: [PATCH 5/7] fix(milkdrop): mash-up rule version 2 for the stage rule The mash-up id carries the rule version so that a rule change which alters the text of the same recipe never overwrites an earlier save. Following MilkDrop's stage rule is such a change: a warp from a preset with PSVERSION_WARP=0 used to be written as version 2, and the engine drew it; it is now written as 0. A recipe from the history saved again now writes md_mix2_... next to the old md_mix1_... file instead of replacing it. Nothing recognises mash-ups by the prefix, so older saves stay ordinary presets. Refs #579, #580 --- src/shared/milkdrop-mashup.js | 9 ++++++++- tests/milkdrop-mashup-panel.test.js | 2 +- tests/milkdrop-mashup.test.js | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/shared/milkdrop-mashup.js b/src/shared/milkdrop-mashup.js index eff884e..31fe704 100644 --- a/src/shared/milkdrop-mashup.js +++ b/src/shared/milkdrop-mashup.js @@ -33,7 +33,14 @@ bir presetin kare denklemlerinin yazdığı q değişkenlerini okuyor olabilir. Karışımın doğası bu; MilkDrop 2'de de öyle. */ (function () { - const VERSION = 1; + /* Kural sürümü, kimliğin içinde (idOf). Aynı tarifin METNİNİ değiştiren + her kural değişikliği bunu artırıyor ki eski kural altında kaydedilmiş + bir karışımın dosyasının üstüne yazılmasın. + 1: ilk kurallar (#579). + 2: shader parçası MilkDrop'un aşama kuralına uyuyor (#580) — sürümü + MilkDrop'ta 0 okunan bir shader karışımda da 0 yazılıyor; 1'de 2 + yazılıyor ve motor o shader'ı çiziyordu. */ + const VERSION = 2; const SLOTS = ['look', 'motion', 'waves', 'shapes', 'warp', 'comp']; // Shader parçası "yok" olabilir: görünümün sabit yolu (yankı, gama, bayraklar) o zaman çalışıyor const NONE = ''; diff --git a/tests/milkdrop-mashup-panel.test.js b/tests/milkdrop-mashup-panel.test.js index 725f401..0044e42 100644 --- a/tests/milkdrop-mashup-panel.test.js +++ b/tests/milkdrop-mashup-panel.test.js @@ -161,7 +161,7 @@ test('karışımın metni parçalarının presetlerinden, önizleme kaydedilmiyo GP.rollAll(); const { mix, mixLast } = GP.state(); assert.strictEqual(cfg.milkdrop.presetId, mixLast.id); - assert.match(mixLast.id, /^md_mix1_[0-9a-f]{16}$/); + assert.match(mixLast.id, /^md_mix2_[0-9a-f]{16}$/); assert.strictEqual(mixLast.id, X.idOf(mix)); assert.strictEqual(cfg.milkdrop.source, mixLast.source); const f = M0.parseMilk(mixLast.source); diff --git a/tests/milkdrop-mashup.test.js b/tests/milkdrop-mashup.test.js index 68a0f85..3aaad8e 100644 --- a/tests/milkdrop-mashup.test.js +++ b/tests/milkdrop-mashup.test.js @@ -226,7 +226,7 @@ test('"parçası var mı" ayrıştırıcıyla aynı cevabı veriyor', () => { test('kimlik tariften: aynı tarif aynı kimlik, depo adıyla uyumlu', () => { const r = { look: 'a', motion: 'b', waves: 'c', shapes: 'a', warp: '', comp: 'd' }; const id = X.idOf(r); - assert.match(id, /^md_mix1_[0-9a-f]{16}$/); + assert.match(id, /^md_mix2_[0-9a-f]{16}$/); assert.strictEqual(X.idOf(Object.assign({}, r)), id); assert.notStrictEqual(X.idOf(Object.assign({}, r, { warp: 'b' })), id); assert.notStrictEqual(X.idOf(Object.assign({}, r, { look: 'b', motion: 'a' })), id, 'parçanın yeri de kimlikte'); From ef5fc76bab8911239e3c6b77b03dd593164031de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Wed, 23 Sep 2026 16:38:49 +0300 Subject: [PATCH 6/7] docs: mash-up version, and what the defaults cover so far The README now says most values a preset leaves out take MilkDrop's defaults, not all of them: the five wave keys MilkDrop reads as 0 when missing, wave smoothing, the volume fade range and shape colours come in the next change. The ROADMAP records the mash-up rule version, the part test's stage condition, and that a partial fShader lands twice in a generated composite stage until the hue fix (the one corpus preset with such a stage has fShader at 0). Refs #580, #579 --- README.md | 6 +++--- README.tr.md | 6 +++--- ROADMAP.md | 21 +++++++++++++++------ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 641d36e..0b4d019 100644 --- a/README.md +++ b/README.md @@ -252,9 +252,9 @@ that asserts the bar profile has no step in it. `sqrt(c)` and `4c(1−c)` the engine used (410 and 83 presets of a 10,332-preset corpus turn them on); echo orientation is `(int)x % 4`; gamma below 1 is ignored while echo is on; and when two presets whose echoes point different ways blend, the echo fades out and back in instead of - flipping. Keys a preset does not write take MilkDrop's defaults — decay 0.98, gamma 2.0 — instead - of 0, and a rotation centre of 0 stays in the corner. Rendered through the engine, every case - matches MilkDrop's formula within 2/255. + flipping. A preset that leaves out its decay or gamma gets MilkDrop's 0.98 and 2.0 instead of 0, + and so do most other values a preset leaves out; a rotation centre of 0 stays in the corner. + Rendered through the engine, every case matches MilkDrop's formula within 2/255. - **Textured shapes, motion vectors, rotation matrices, mesh density, internal resolution scale, mouse input and preset transitions** are all implemented, and each sampler is read with the filtering and wrapping its name asks for (`sampler_pw_main` is point-sampled, `sampler_fc_main` is diff --git a/README.tr.md b/README.tr.md index b308e22..771e520 100644 --- a/README.tr.md +++ b/README.tr.md @@ -245,9 +245,9 @@ profilinde basamak olmadığını doğruluyor. `sqrt(c)` ve `4c(1−c)` değil (10.332 presetlik bir korpusta 410 preset parlatmayı, 83'ü solarize'ı açıyor); yankı yönü `(int)x % 4`; yankı açıkken 1'in altındaki gama uygulanmıyor; yankıları farklı yöne bakan iki preset geçişte karışırken yankı bir anda dönmüyor, sönüp - yeniden beliriyor. Presetin yazmadığı anahtarlar 0 yerine MilkDrop'un varsayılanlarını alıyor — - sönme 0,98, gama 2,0 — ve dönme merkezi 0 ise köşede kalıyor. Motordan geçirilerek çizildi: - her durum MilkDrop'un formülüyle 2/255 içinde aynı. + yeniden beliriyor. Sönmesini ya da gamasını yazmayan bir preset 0 yerine MilkDrop'un 0,98 ve + 2,0'ını alıyor, yazmadığı öteki değerlerin çoğu da öyle; dönme merkezi 0 ise köşede kalıyor. + Motordan geçirilerek çizildi: her durum MilkDrop'un formülüyle 2/255 içinde aynı. - **Dokulu şekiller, hareket vektörleri, dönme matrisleri, ağ sıklığı, iç çözünürlük, fare girdisi ve preset geçişleri** uygulandı; her sampler adının istediği süzme ve sarma ile okunuyor (`sampler_pw_main` noktasal, `sampler_fc_main` süzülmüş+kenetli — presetlerin %22,7'si aynı diff --git a/ROADMAP.md b/ROADMAP.md index 95d076b..c6ae655 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1961,12 +1961,14 @@ rest after. No version number yet. generated preset or a mash-up) is never in the list, so it never gives a part. ◀ ▶ step through a history of recipes (part → preset); a recipe whose preset was deleted is skipped. The id comes from the - recipe (`md_mix1_` and two 32-bit hashes), so the same mash-up saved - twice leaves one file, and the version in it keeps later rule changes - from overwriting earlier saves. + recipe (`md_mix`, the rule version and two 32-bit hashes; the + version is 2 since #580), so the same mash-up saved twice leaves one + file, and the version in it keeps later rule changes from + overwriting earlier saves. - *Part tests* decide which presets can give a part without parsing them (13.5 µs a test): a non-comment equation line, an enabled wave - or shape block, a non-empty shader line. + or shape block, a non-empty shader line (since #580, one whose stage + version MilkDrop reads as above 0). - **Mash-ups measured** on the whole 10,332-preset corpus. The part tests agree with the parser for every preset and part. A mash-up whose six parts come from one preset is that preset again for all 10,332 — @@ -2094,7 +2096,11 @@ rest after. No version number yet. presets with a centre of 0 and 7 of 38 that write `echo_orient` changed. - **Mash-ups follow the same rule.** A preset whose shader MilkDrop ignores cannot give that part, and the version written for a shader is - the one its preset has for that stage. + the one its preset has for that stage. A recipe from the history can + therefore give different version lines than before — a warp from a + preset with `PSVERSION_WARP=0` was written as version 2 and drawn — so + the mash-up rule version went from 1 to 2: a mash-up saved under the + old rule keeps its file, and saving the recipe again writes a new one. - **Found while finishing this, fixed next.** MilkDrop hands every composite shader the full hue colour ("since we don't know if shader uses it or not", in both the D3D11 and the D3D9 code); `fShader` scales @@ -2102,7 +2108,10 @@ rest after. No version number yet. preset without one. The engine has applied `fShader` to preset shaders too since the `hue_shader` change of 16 September, so with fidelity on the 914 corpus presets that read `hue_shader` and leave `fShader` at 0 - lost their colour, and 36 got part of it. The engine also takes the + lost their colour, and 36 got part of it. In the shader written for a + composite stage with a version and no text, a partial `fShader` now + lands twice, in the text and in the colour; the one corpus preset with + such a stage has `fShader` at 0. The engine also takes the amount from per-frame values: 16 presets write `fshader` in code, which MilkDrop never sees, all of them with a composite shader. Some keys fall back to something else when a file leaves them out. MilkDrop From 205eac1c8072bfef2df109499d6bdeae647618f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Wed, 23 Sep 2026 16:39:36 +0300 Subject: [PATCH 7/7] docs: #580 test and mutation counts after the version bump Refs #580 --- ROADMAP.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index c6ae655..0936204 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2138,10 +2138,13 @@ rest after. No version number yet. formulas behind the switch, and our own presets — the builtins, 64 generated ones and 30 mash-ups of them — writing only keys MilkDrop reads, each once, unindented, with code lines numbered from 1 without - a gap. Seven older tests were updated: two used presets with + a gap. Eight older test files were updated: two used presets with `PSVERSION` lines but no `MILKDROP_PRESET_VERSION`, which MilkDrop - reads as MilkDrop 1, and the generator's golden hashes and wave-alpha - check follow the new key. 35 of 35 mutations are caught. + reads as MilkDrop 1; the generator's golden hashes and wave-alpha + check follow the new key; the mash-up tests check the part test + against the engine's stage choice and expect `md_mix2_` ids. 36 of 36 + mutations are caught, one of them putting the mash-up rule version + back to 1. ## v3.1.6 — Comprehensive video export