diff --git a/README.md b/README.md index 5f099a4..0b4d019 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) @@ -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. 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 @@ -1289,7 +1299,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..771e520 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) @@ -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. 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ı @@ -1287,7 +1297,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..0936204 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 @@ -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 — @@ -2028,6 +2030,121 @@ 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. 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 + 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. 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 + 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. Eight older test files were updated: two used presets with + `PSVERSION` lines but no `MILKDROP_PRESET_VERSION`, which MilkDrop + 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 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/milkdrop-mashup.js b/src/shared/milkdrop-mashup.js index 4445e17..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 = ''; @@ -60,6 +67,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 +99,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 +116,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 +159,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/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/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/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-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. */ 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 7290988..3aaad8e 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)) { @@ -210,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'); 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',