diff --git a/README.md b/README.md index 6a066cfd..55e633d0 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-2202%20passing-2ea043.svg)](#tests) +[![Tests](https://img.shields.io/badge/tests-2227%20passing-2ea043.svg)](#tests) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -263,6 +263,11 @@ that asserts the bar profile has no step in it. first missing number, integer settings drop their fractions, and equation lines are glued the way MilkDrop glues them, `\\` comments included. 32 presets of the corpus read differently; with fidelity off the old parser stays. +- **Equations run by MilkDrop 2's compiler rules.** With fidelity on, truth and equality tests use + its 0.00001 tolerance, `%` works on whole numbers without sign, `&` and `|` on 64-bit integers, + `megabuf` and `gmegabuf` indices are rounded and bounded the way it does, `rand(n)` returns a + real number (5,361 corpus presets call it), and the compiler's internal names such as `_aboeq` + run. With fidelity off nothing changes. - **Composite shaders get the hue colour MilkDrop gives them.** MilkDrop passes every composite shader four slowly drifting corner colours as `hue_shader`, whatever the preset's `fShader` says; `fShader` only scales them on the fixed pipeline. The engine applied `fShader` to shaders @@ -1315,7 +1320,7 @@ npm test npm start -- --smoke ``` -**2202 unit tests, all passing.** They are written to check answers, not to exercise lines: +**2227 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 35ffa38a..6b087f7b 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-2202%20geçiyor-2ea043.svg)](#testler) +[![Test](https://img.shields.io/badge/test-2227%20geçiyor-2ea043.svg)](#testler) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -256,6 +256,11 @@ profilinde basamak olmadığını doğruluyor. numaralı kod ilk eksik numarada bitiyor, tam sayı ayarları kesirlerini atıyor ve denklem satırları MilkDrop'un yapıştırdığı gibi, `\\` yorumları dahil, yapışıyor. Korpusta 32 preset farklı okunuyor; uyum kapalıyken eski ayrıştırıcı duruyor. +- **Denklemler MilkDrop 2 derleyicisinin kurallarıyla çalışıyor.** Uyum açıkken doğruluk ve eşitlik + sınamaları onun 0.00001 toleransını kullanıyor, `%` işaretsiz tam sayılarla, `&` ve `|` 64 bit + tam sayılarla çalışıyor, `megabuf` ve `gmegabuf` indisleri onun yuvarladığı ve sınırladığı gibi + hesaplanıyor, `rand(n)` gerçel sayı döndürüyor (korpusta 5.361 preset çağırıyor) ve derleyicinin + `_aboeq` gibi iç adları çalışıyor. Uyum kapalıyken hiçbir şey değişmiyor. - **Birleştirme shader'ları MilkDrop'un verdiği ton rengini alıyor.** MilkDrop her birleştirme shader'ına `hue_shader` olarak dört köşede yavaşça gezinen renkler veriyor, presetin `fShader`ı ne derse desin; `fShader` onları yalnız sabit yolda ölçekliyor. Motor `fShader`ı shader'lara da @@ -1313,7 +1318,7 @@ npm test npm start -- --smoke ``` -**2202 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: +**2227 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 89fe2397..074186c4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,9 +129,9 @@ npm test npm start -- --smoke ``` -- **2202 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; +- **2227 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 600 + that tag — 469 more with v3.1.4, most of them from the MilkDrop work, and 630 on `main` since. Formulas are checked against values derived by hand from their definitions — Viviani's curve staying on its sphere, the @@ -2033,12 +2033,17 @@ rest after. No version number yet. 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, - MilkDrop's defaults, the hue colour, the values that stay in the file and - MilkDrop's way of reading a file are done; the other audits are next. + MilkDrop's defaults, the hue colour, the values that stay in the file, + MilkDrop's way of reading a file and its expression compiler's rules are + done; the other audits 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. + 53d83ee). Both agree on everything below. Expressions are the + exception: the D3D11 fork evaluates them with projectM's + re-implementation by default, which differs from MilkDrop in places + (its `%` keeps the sign, its memory index adds 0.0001), so there the + reference is Nullsoft's compiler, ns-eel2, as BeatDrop carries it. - **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 @@ -2213,24 +2218,75 @@ rest after. No version number yet. differently, 15 change with fidelity on, 12 by more than 1% (up to 32%), and all 32 are identical with it off. The 900-preset sample sorts into the same classes, preset for preset. + - **Expressions follow MilkDrop 2's compiler.** With fidelity on the + equations run by the rules of ns-eel2 as MilkDrop 2 builds it: + - truth tests (`if`, `!`, `&&`, `||`, `while`, `bnot`) count |x| of + 0.00001 or more as true, `band` and `bor` need more than 0.00001, + `==`, `!=` and `equal` compare with the same tolerance, and `<`, `>`, + `above`, `below` stay exact; NaN is false, and equal to anything; + - the compiler's own operator names compile (`_aboeq`, `_if`, `_and`, + `_or`, `_equal`, `_set`, `_addop` and the rest, `_mem`, `_gmem`); + `_and` and `_or` skip their right side like `&&` and `||`; + - `%` works on whole numbers without sign — both sides lose their + sign and fraction, so `-7 % 3` is 1 and `5 % 0.5` is 0 — and a value + outside the signed 32-bit range turns into 2^31, as the x87 + conversion does; + - `&` and `|` work on 64-bit integers; + - a `megabuf` index is rounded down after adding 0.00001, reaches + 8,388,608 cells, and reads 0 outside them; `gmegabuf` has 2^20 + cells and its index wraps; + - `rand(n)` returns a real number between 0 and n, not an integer. + Every block of a preset and its sprites read one switch, so turning + fidelity over changes all of them in the same frame, and a preset that + uses the internal names is rebuilt. With fidelity off nothing changes. + - **Measured** against the previous step, the last ten of 60 frames, 120 + presets per group. Truth tests: 119 of 120 presets with conditions are + identical, and one moves 4% — it asks `equal(sin(ang), 0)`, where + sin(π) is 1.2e-16; the three `_aboeq` presets change by up to 29%, + since the engine did not run that code before. `%`: 118 of 120 + identical; one takes the remainder of negative samples (6%), one + hashes with numbers past 2^31 and its points pile up (48%). `&` and + `|`: 120 of 120 identical. Memory: 119 of 120; one computes its + `gmegabuf` indices (3%). `rand`, which 5,361 presets call: 92 of 120 + identical, 14 change by more than 1%, up to 18%, the median of those + that change 1.07%. Control groups without the feature are identical, + and every group is identical with fidelity off. The whole change costs + no time: 0.62-0.64 ms for a 1,728-point per-pixel frame, 0.71-0.75 ms + before. The 900-preset sample keeps 899 of 900 classes; the one that + moves, Goody's Molten Wavepool (blown → clean), uses none of these + features and renders the same alone before and after (clean, + brightness 0.038); main itself classed it blown in the sample run, so + its class there depends on the order of the run, not on this change. - **Not done yet:** the fixed warp path, the blur chain, borders and centre darkening, and the rest of the blend snap points; whether `uv` in preset shaders runs the way MilkDrop's does — read back from the screen, our `uv.y` is 1 at the top, where MilkDrop's texture coordinate is 0; sampling agrees, but a shader doing arithmetic on - `uv.y` may come out mirrored, which needs its own check; MilkDrop's - internal comparison functions such as `_aboeq`, which three corpus - presets call and our compiler does not know; the reference comparison - with an external renderer, which needs one installed and waits for the - user's approval. Left out of the reader on purpose: MilkDrop reads + `uv.y` may come out mirrored, which needs its own check; the scope of + expression variables — MilkDrop gives per-frame, per-pixel, each + wave's per-frame and per-point and each shape their own variables and + `megabuf`, and hands per-pixel only a fixed list, where the engine + shares one set between init, per-frame and per-pixel (next); division + by zero, which is infinity in MilkDrop and 0 here; the loop cap — + MilkDrop stops `loop` and `while` at 1,048,576 turns a call, the engine + at a budget, which six corpus presets reach (five in init, one in a + shape); the reference comparison with an external renderer, which + needs one installed and waits for the user's approval. Left out of + the compiler: syntax newer than MilkDrop 2's grammar (`?:`, `&=`, + `|=`, `^=`, `<<`, `$` constants) and the functions `invsqrt`, + `memcpy`, `memset` and `freembuf`, which no corpus preset uses in code; + and the old grammar's `%`, which ns-eel2's parser table maps to a + function that returns its right side — the corpus writes `%` as a + remainder, and the operator function MilkDrop compiles is the + remainder above. Left out of the reader on purpose: MilkDrop reads bytes and the engine gets decoded text, so its two byte rules — a 0xFF byte ends the file, a value over 251 characters splits into a second index line — cannot be kept exactly, and neither touches a corpus file; values stay double where MilkDrop stores float, a difference below 1e-7; and a block that fails to compile is dropped whole in MilkDrop but recovered statement by statement here, because - our parser and MilkDrop's do not agree on what an error is (`_aboeq` - is one case), so dropping whole blocks would drop some MilkDrop runs. + our parser and MilkDrop's may still disagree on what an error is, and + dropping whole blocks would then drop code MilkDrop runs. - **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 @@ -2270,6 +2326,17 @@ rest after. No version number yet. donor's reading. The own-presets test also requires both readings to agree, and the mash-up part test compares with the new reader. 23 of 23 mutations are caught. + The expression rules add 25 (`milkdrop-md2-eel.test.js`): the internal + names with fidelity on and off, their arity and case, `_and` and `_or` + skipping their right side, the truth boundaries at 0.00001 for every + test, NaN, `while`, switching mode at run time, the remainder's sign, + fractions and values outside the signed 32-bit range, 64-bit `&` and + `|`, the memory index, capacity and wrap (with a compound write past + the old limit and two `gmegabuf` indices that alias only under the + wrong mask), the real + `rand` and its repeatability, the Preset switching all its blocks, + `readingsDiffer` seeing the names, sprites, and every expression compile + in `src` passing the mode. 71 of 71 mutations are caught. ## v3.1.6 — Comprehensive video export diff --git a/src/main/main.js b/src/main/main.js index 60dbd61b..63d47ec0 100644 --- a/src/main/main.js +++ b/src/main/main.js @@ -5096,7 +5096,7 @@ async function runSmoke() { try { var m = window.SVMilkdrop; var pool = new m.Pool(); - var c = m.compile('x = 2 + 3; y = x * 5; z = sqrt(y); w = if(above(y,10), 7, 9); v = 10 % 3; u = 2 ^ 3; t = 1/0;', pool); + var c = m.compile('x = 2 + 3; y = x * 5; z = sqrt(y); w = if(above(y,10), 7, 9); v = 10 % 3; u = 2 ^ 3; t = 1/0;', pool, { mode: { md2: true }, md2Funcs: true }); c.run(); return JSON.stringify({ hata: '', diff --git a/src/shared/milkdrop-sprites.js b/src/shared/milkdrop-sprites.js index 5c16af67..91899e0f 100644 --- a/src/shared/milkdrop-sprites.js +++ b/src/shared/milkdrop-sprites.js @@ -149,8 +149,15 @@ this.M = M; this.slots = new Array(SLOTS).fill(null); this.launches = 0; // başlatma sırası: en yeni / en eski buradan + /* MilkDrop 2 uyumu (#580): sprite kodu da MilkDrop'un ifade + derleyicisiyle koşuyor. Derlenen kodun paylaştığı anahtar; motor + her karede yazıyor (setAccurate). İç işlev adları (`_aboeq` …) + sprite başlatılırken o anki değere göre tanınıyor. */ + this.mode = { md2: true }; } + setAccurate(on) { this.mode.md2 = on !== false; } + count() { return this.slots.reduce((n, s) => n + (s ? 1 : 0), 0); } /* Sprite'ı başlatır, yuvasını döndürür (-1: başlatılamadı). @@ -183,8 +190,9 @@ for (const k of INPUTS) pool.set(k, 0); for (const k of Object.keys(DEFAULTS)) pool.set(k, DEFAULTS[k]); const seed = (Number(opt.seed) >>> 0) || 1; - const init = M.compile(def.init || '', pool, { seed }); - const code = M.compile(def.code || '', pool, { seed: (seed ^ 0x9e3779b9) >>> 0 || 1 }); + const eel = { mode: this.mode, md2Funcs: this.mode.md2 }; + const init = M.compile(def.init || '', pool, Object.assign({ seed }, eel)); + const code = M.compile(def.code || '', pool, Object.assign({ seed: (seed ^ 0x9e3779b9) >>> 0 || 1 }, eel)); init.run(pool.values); this.slots[slot] = { num: def.num, diff --git a/src/shared/milkdrop.js b/src/shared/milkdrop.js index 9f306e45..059518b4 100644 --- a/src/shared/milkdrop.js +++ b/src/shared/milkdrop.js @@ -32,31 +32,38 @@ // ========================================================================== /* megabuf/gmegabuf: MilkDrop'un karalama bellekleri. - MilkDrop bunları 1.048.576 girdilik sabit bir dizi olarak tutar. Burada - 4096'dan başlayıp ikiye katlayarak büyütüyoruz: yönetici paneli her - çizimde presetin derlemesini doğrulamak için yeni bir Preset kuruyor ve - preset başına 8 MB ayırmak kabul edilemezdi. Yazılmamış girdi 0'dır, - dolayısıyla dizinin kısa olması okumayı değiştirmiyor. */ + Bellek 4096 girdilik bloklar hâlinde tutuluyor, bir blok ilk + yazıldığında ayrılıyor: yönetici paneli her çizimde presetin + derlemesini doğrulamak için yeni bir Preset kuruyor ve preset başına + megabaytlar ayırmak kabul edilemezdi. Yazılmamış girdi 0'dır. + + İndis ve sınır kipe göre (memKey, #580): + • kapalıyken eski kural: `x | 0`, 0..1.048.575 (MEM_MAX); + • açıkken MilkDrop'unki: indis trunc(x + 0,00001) — MilkDrop indise + yakınlık payını ekleyip kırpıyor (asm `_asm_megabuf`: fadd + closefact; fistp), yani 2,9999999 gibi bir hesap 3. gözü, −1 ise + 0. gözü buluyor. megabuf 0..8.388.607 (NSEEL_RAM_BLOCKS × + NSEEL_RAM_ITEMSPERBLOCK = 128 × 65.536); dışı 0 okuyor, yazılanı + atıyor. gmegabuf tek bir 2^20'lik dizi ve indis ona SARIYOR + (nseel-ram.c __NSEEL_RAMAllocGMEM: `w & (NSEEL_SHARED_GRAM_SIZE − + 1)`; MilkDrop sanal makinelerine ayrı bir GRAM bloğu vermiyor). */ const MEM_MAX = 1048576; + const MD2_MEM_MAX = 8388608; + const GMEM_MASK = 0xFFFFF; function makeMem() { - let a = new Float64Array(4096); + const blocks = []; return { - get(i) { - const k = i | 0; - return k >= 0 && k < a.length ? a[k] : 0; + // k: tam sayı indis, max: sınır (hariç) + get(k, max) { + if (!(k >= 0 && k < max)) return 0; + const b = blocks[k >>> 12]; + return b ? b[k & 4095] : 0; }, - set(i, v) { - const k = i | 0; - if (k < 0 || k >= MEM_MAX) return v; - if (k >= a.length) { - let n = a.length; - while (n <= k) n *= 2; - if (n > MEM_MAX) n = MEM_MAX; - const b = new Float64Array(n); - b.set(a); - a = b; - } - a[k] = v; + set(k, v, max) { + if (!(k >= 0 && k < max)) return v; + let b = blocks[k >>> 12]; + if (!b) b = blocks[k >>> 12] = new Float64Array(4096); + b[k & 4095] = v; return v; }, }; @@ -183,6 +190,94 @@ }], }; + /* MilkDrop 2'NİN İÇ İŞLEVLERİ (#580). MilkDrop 2'nin ifade derleyicisi + (Nullsoft'un ns-eel2'si; BeatDrop kopyasında nseel-compiler.c fnTable1, + ad çevirisi nseel-eval.c) işleçleri adlı işlevler olarak da + çağrılabilir tutuyor: `_aboeq(a, b)` `a >= b` demek. Korpusta üç preset + `_aboeq` çağırıyor ve motor onları "bilinmeyen işlev" diye atlıyordu. + D3D11 çatalı (jecassis) ifadeleri projectM'in yeniden yazdığı + kütüphaneyle değerlendiriyor; burada presetlerin yazıldığı Nullsoft + derleyicisi izleniyor. Yalnız uyum açıkken derleniyor — kapalıyken eski + davranış (bilinmeyen işlev) duruyor. + + `_and` ve `_or` `band`/`bor` DEĞİL: onlar `&&` ve `||`nin kendisi — + sağ tarafı gerekmedikçe hesaplamıyorlar (derleyici bu iki işlevi + döngüyle birlikte ayrı bir yoldan kuruyor). `band`/`bor` ise iki + tarafı da hep hesaplıyor. `_mem`/`_gmem` `megabuf`/`gmegabuf`un iç adı. + + Değer verenler var olan işleçlere eşleniyor; atama biçimleri + (`_set(x, v)`, `_addop(x, v)` …) ilk bağımsız değişkeni değişken olan + `x = v`, `x += v` … gibi derleniyor. */ + const MD2_FUNCS = { + _if: 'if', _and: '&&', _or: '||', _not: 'bnot', _equal: 'equal', _noteq: '!=', + _below: '<', _above: '>', _beleq: '<=', _aboeq: '>=', _mod: '%', + }; + const MD2_ASSIGN_FUNCS = { + _set: null, _addop: '+', _subop: '-', _mulop: '*', _divop: '/', _modop: '%', + _orop: '|', _andop: '&', _powop: '^', + }; + const MD2_MEM = { _mem: 'megabuf', _gmem: 'gmegabuf' }; + const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k); + // Adlar büyük/küçük harf ayırmıyor (MilkDrop strcasecmp ile arıyor) + const MD2_FUNC_RE = /(^|[^a-z0-9_])_(if|and|or|not|equal|noteq|below|above|beleq|aboeq|mod|set|addop|subop|mulop|divop|modop|orop|andop|powop|mem|gmem)\s*\(/i; + + /* MilkDrop'un doğruluk ve eşitlik sınamaları bir YAKINLIK PAYIYLA + (NSEEL_CLOSEFACTOR = 0,00001; ns-eel-int.h): mutlak değeri bunun + altındaki sayı "yanlış", farkı bunun altındaki iki sayı "eşit". + • `if`, `!`/`bnot`, `&&`, `||`, `while`: |x| >= pay doğru + (asm: fabs; fcomp closefact); + • `band`/`bor` işlevleri: |x| > pay doğru — sınırda farklılar + (C: `fabs(a) > g_closefact`); + • `==`/`equal`: |a − b| < pay eşit; `!=` tersi; + • `<`, `>`, `<=`, `>=`, `above`, `below`: birebir. + NaN'da x87 karşılaştırması "sırasız" dönüyor ve C0 bitini kuruyor: + NaN yanlış sayılıyor, NaN'lı eşitlik doğru. Uyum açıkken geçerli; + kapalıyken eski birebir sınamalar. Anahtar çalışma anında + çevrilebildiği için karar derlemede değil çağrıda: presetin paylaştığı + `mode` nesnesi anahtarla birlikte güncelleniyor (Preset.accurate). */ + const CLOSE = 0.00001; + const T_MD2 = (v) => v >= CLOSE || v <= -CLOSE; + const T_STRICT = (v) => v > CLOSE || v < -CLOSE; + const EQ_MD2 = (a, b) => { const d = a - b; return !(d >= CLOSE || d <= -CLOSE); }; + + /* x87 `fistp` ile 32 bitlik tam sayıya çevirme. MilkDrop kodu koşturmadan + önce yuvarlama kipini KIRPMAYA alıyor (nseel-compiler.c GLUE_CALL_CODE: + `_controlfp(_RC_CHOP, _MCW_RC)`), yani sıfıra doğru kırpılıyor; sığmayan + değer ve NaN "belirsiz tam sayı" veriyor: −2^31. */ + const FIST = (x) => { + const t = Math.trunc(x); + return t >= -2147483648 && t <= 2147483647 ? t : -2147483648; + }; + + /* MilkDrop'un KALANI (`%`, `%=`, `_mod`): iki taraf da MUTLAK değerinin + tam kısmına iniyor ve bölme işaretsiz — sonuç hiç negatif olmuyor + (asm-nseel-x86-msvc.c nseel_asm_mod: fabs; fistp; div). −7 % 3 MilkDrop'ta + 1, JavaScript'te −1. Sığmayan değer −2^31, işaretsiz 2^31 oluyor. + Bölen 0 ise 0. */ + const MOD_MD2 = (a, b) => { + const ub = FIST(Math.abs(b)) >>> 0; + return ub === 0 ? 0 : (FIST(Math.abs(a)) >>> 0) % ub; + }; + + // Bellek indisi ve sınırı: uyum açıkken MilkDrop'unki (bkz. makeMem) + const memKey = (x, md2, g) => (!md2 ? x | 0 : g ? FIST(x + CLOSE) & GMEM_MASK : FIST(x + CLOSE)); + const memMax = (md2, g) => (md2 && !g ? MD2_MEM_MAX : MEM_MAX); + + /* Bit işleçleri (`&`, `|`, `&=`, `|=`): MilkDrop iki tarafı 64 bitlik tam + sayıya çeviriyor (nseel_asm_and/or: fistp qword, kırpma kipinde). + 32 bite sığan değerde JavaScript'in `|0`ıyla aynı sonuç — yol hızlı + kalıyor; sığmayanda 64 bit. Sığmayan 64 bit değer ve NaN −2^63. */ + const I64_MIN = -9223372036854775808; + const toI64 = (x) => { + const t = Math.trunc(x); + return t >= I64_MIN && t < -I64_MIN ? BigInt(t) : BigInt(I64_MIN); + }; + const fits32 = (x) => x > -2147483649 && x < 2147483648; + const AND_MD2 = (a, b) => (fits32(a) && fits32(b) ? (a | 0) & (b | 0) + : Number(BigInt.asIntN(64, toI64(a) & toI64(b)))); + const OR_MD2 = (a, b) => (fits32(a) && fits32(b) ? (a | 0) | (b | 0) + : Number(BigInt.asIntN(64, toI64(a) | toI64(b)))); + // ========================================================================== // Ayrıştırıcı // ========================================================================== @@ -194,7 +289,9 @@ ['+', '-'], ['*', '/', '%'], ]; - function parse(src) { + // `popts.md2`: MilkDrop 2'nin iç işlevleri (`_aboeq` …) tanınsın mı + function parse(src, popts) { + const md2 = !!(popts && popts.md2); const toks = tokenize(src); let pos = 0; const peek = () => toks[pos]; @@ -253,6 +350,24 @@ if (tk.v === 'assign' && args.length === 2 && args[0] && args[0].k === 'var') { return { k: 'assign', name: args[0].name, v: args[1] }; } + // MilkDrop 2'nin iç işlevleri, yalnız uyum açıkken (MD2_FUNCS) + if (md2 && own(MD2_ASSIGN_FUNCS, tk.v)) { + if (args.length !== 2 || !args[0] || args[0].k !== 'var') { + throw new SyntaxError(`'${tk.v}' bir değişken ve bir değer ister (satır ${tk.line})`); + } + const op = MD2_ASSIGN_FUNCS[tk.v]; + const name = args[0].name; + return op ? { k: 'assign', name, v: { k: 'bin', op, a: { k: 'var', name }, b: args[1] } } + : { k: 'assign', name, v: args[1] }; + } + if (md2 && (own(MD2_FUNCS, tk.v) || own(MD2_MEM, tk.v))) { + const to = MD2_FUNCS[tk.v] || MD2_MEM[tk.v]; + const need = to === 'if' ? 3 : to === 'bnot' || MD2_MEM[tk.v] ? 1 : 2; + if (args.length !== need) { + throw new SyntaxError(`'${tk.v}' ${need} argüman ister, ${args.length} verildi (satır ${tk.line})`); + } + return FUNCS[to] ? { k: 'call', name: to, args } : { k: 'bin', op: to, a: args[0], b: args[1] }; + } const def = FUNCS[tk.v]; if (!def) throw new SyntaxError(`bilinmeyen fonksiyon '${tk.v}' (satır ${tk.line})`); if (def[0] !== args.length) { @@ -314,9 +429,11 @@ MilkDrop'un ifade dilinde megabuf() bir GÖSTERGE döndürür, dolayısıyla atamanın sol tarafında durabilir. Dilin geri kalanında çağrıya atama yoktur; bu yüzden yalnızca bu iki ad için açılıyor. */ - if (peek().t === 'id' && (peek().v === 'megabuf' || peek().v === 'gmegabuf') + const memOf = (v) => (v === 'megabuf' || v === 'gmegabuf' ? v + : md2 && own(MD2_MEM, v) ? MD2_MEM[v] : ''); + if (peek().t === 'id' && memOf(peek().v) && toks[pos + 1] && toks[pos + 1].t === 'op' && toks[pos + 1].v === '(') { - const buf = peek().v; + const buf = memOf(peek().v); pos += 2; const idx = expr(); const nxt = toks[pos + 1]; @@ -487,7 +604,10 @@ case 'un': { const a = emit(node.a, pool, cx); if (node.op === '-') return (P) => -a(P); - if (node.op === '!') return (P) => (a(P) === 0 ? 1 : 0); + if (node.op === '!') { + const mode = cx.mode; + return (P) => { const v = a(P); return (mode.md2 ? !T_MD2(v) : v === 0) ? 1 : 0; }; + } return a; } case 'seq': { @@ -520,11 +640,17 @@ }; } case 'bufset': { - const mem = node.buf === 'gmegabuf' ? GMEM : pool.mem; + const g = node.buf === 'gmegabuf'; + const mem = g ? GMEM : pool.mem; const i = emit(node.i, pool, cx); const v = emit(node.v, pool, cx); - const F = cx.F; - if (!node.compound) return (P) => mem.set(i(P), F(v(P))); + const F = cx.F, mode = cx.mode; + if (!node.compound) { + return (P) => { + const md2 = mode.md2; + return mem.set(memKey(i(P), md2, g), F(v(P)), memMax(md2, g)); + }; + } /* Bileşik atamada indeks BİR KEZ değerlendirilir: `megabuf(n=n+1) *= 2` gibi yan etkili bir indeks iki kez çalışsaydı iki farklı gözü okuyup yazardı. İşleç de burada, derleme anında seçiliyor. */ @@ -535,8 +661,9 @@ : op === '/' ? (a, b) => D(a, b) : (a, b) => MM(a, b); return (P) => { - const k = i(P); - return mem.set(k, F(apply(mem.get(k), v(P)))); + const md2 = mode.md2, max = memMax(md2, g); + const k = memKey(i(P), md2, g); + return mem.set(k, F(apply(mem.get(k, max), v(P))), max); }; } case 'bin': @@ -563,8 +690,15 @@ case '/': { const D = cx.D; return (P) => D(a(P), b(P)); } case '%': { const M = cx.M; return (P) => M(a(P), b(P)); } case '^': return (P) => F(Math.pow(a(P), b(P))); - case '==': return (P) => (a(P) === b(P) ? 1 : 0); - case '!=': return (P) => (a(P) !== b(P) ? 1 : 0); + // Uyum açıkken yakınlık payıyla (CLOSE) + case '==': { + const mode = cx.mode; + return (P) => { const x = a(P), y = b(P); return (mode.md2 ? EQ_MD2(x, y) : x === y) ? 1 : 0; }; + } + case '!=': { + const mode = cx.mode; + return (P) => { const x = a(P), y = b(P); return (mode.md2 ? !EQ_MD2(x, y) : x !== y) ? 1 : 0; }; + } case '<': return (P) => (a(P) < b(P) ? 1 : 0); case '>': return (P) => (a(P) > b(P) ? 1 : 0); case '<=': return (P) => (a(P) <= b(P) ? 1 : 0); @@ -572,11 +706,23 @@ /* && ve || JavaScript'te olduğu gibi kısa devre yapar: sağ taraf gerekmedikçe ÇAĞRILMAZ. Eski üretilen kod da öyleydi; atama içeren bir sağ taraf iki davranış arasında fark yaratırdı. */ - case '&&': return (P) => (a(P) !== 0 && b(P) !== 0 ? 1 : 0); - case '||': return (P) => (a(P) !== 0 || b(P) !== 0 ? 1 : 0); - // Bit işleçleri tam sayıya yuvarlar - case '&': return (P) => (a(P) | 0) & (b(P) | 0); - case '|': return (P) => (a(P) | 0) | (b(P) | 0); + case '&&': { + const mode = cx.mode; + return (P) => ((mode.md2 ? T_MD2(a(P)) && T_MD2(b(P)) : a(P) !== 0 && b(P) !== 0) ? 1 : 0); + } + case '||': { + const mode = cx.mode; + return (P) => ((mode.md2 ? T_MD2(a(P)) || T_MD2(b(P)) : a(P) !== 0 || b(P) !== 0) ? 1 : 0); + } + // Bit işleçleri tam sayıya yuvarlar; uyum açıkken 64 bit (AND_MD2) + case '&': { + const mode = cx.mode; + return (P) => (mode.md2 ? AND_MD2(a(P), b(P)) : (a(P) | 0) & (b(P) | 0)); + } + case '|': { + const mode = cx.mode; + return (P) => (mode.md2 ? OR_MD2(a(P), b(P)) : (a(P) | 0) | (b(P) | 0)); + } default: return () => 0; } } @@ -584,11 +730,31 @@ function callExpr(node, pool, cx) { const name = node.name; const a = node.args.map((x) => emit(x, pool, cx)); + const mode = cx.mode; // if() kısa devre yapmalı: her iki dalı da hesaplamak yan etkileri // (atamaları) yanlışlıkla çalıştırırdı if (name === 'if') { const c = a[0], t = a[1], f = a[2]; - return (P) => (c(P) !== 0 ? t(P) : f(P)); + return (P) => ((mode.md2 ? T_MD2(c(P)) : c(P) !== 0) ? t(P) : f(P)); + } + /* Doğruluk ve eşitlik sınayan işlevler uyum açıkken yakınlık payıyla + (CLOSE). band/bor iki tarafı da HEP hesaplıyor — && ve || gibi kısa + devre yapmıyorlar; sınırları da onlardan farklı (T_STRICT). */ + if (name === 'equal') { + const x = a[0], y = a[1]; + return (P) => { const u = x(P), v = y(P); return (mode.md2 ? EQ_MD2(u, v) : u === v) ? 1 : 0; }; + } + if (name === 'bnot') { + const x = a[0]; + return (P) => { const u = x(P); return (mode.md2 ? !T_MD2(u) : u === 0) ? 1 : 0; }; + } + if (name === 'band') { + const x = a[0], y = a[1]; + return (P) => { const u = x(P), v = y(P); return (mode.md2 ? T_STRICT(u) && T_STRICT(v) : u !== 0 && v !== 0) ? 1 : 0; }; + } + if (name === 'bor') { + const x = a[0], y = a[1]; + return (P) => { const u = x(P), v = y(P); return (mode.md2 ? T_STRICT(u) || T_STRICT(v) : u !== 0 || v !== 0) ? 1 : 0; }; } if (name === 'rand') { const R = cx.R, n = a[0]; @@ -603,7 +769,8 @@ return (P) => { for (;;) { if (--budget.n < 0) break; - if (body(P) === 0) break; + const v = body(P); + if (mode.md2 ? !T_MD2(v) : v === 0) break; } return 0; }; @@ -618,9 +785,13 @@ }; } if (name === 'megabuf' || name === 'gmegabuf') { - const mem = name === 'gmegabuf' ? GMEM : pool.mem; + const g = name === 'gmegabuf'; + const mem = g ? GMEM : pool.mem; const i = a[0]; - return (P) => mem.get(i(P)); + return (P) => { + const md2 = mode.md2; + return mem.get(memKey(i(P), md2, g), memMax(md2, g)); + }; } /* Ayrıştırıcı adı zaten beyaz listeye karşı doğruladı (bilinmeyen ad SyntaxError atar), burada da doğrudan o tablodan çözülüyor: çalışma @@ -639,13 +810,20 @@ Dönüş: { run(P), pool, error } — hata varsa run yine çalışır ama hiçbir şey yapmaz. Bozuk bir preset uygulamayı durdurmamalı; olan biteni - kullanıcıya söylemek yeterli. */ + kullanıcıya söylemek yeterli. + + opts.mode: { md2 } — MilkDrop 2 uyumu (#580). Kapanışlar bunu HER + ÇAĞRIDA okuyor, yani anahtar çevrildiğinde yeniden derlemek + gerekmiyor. Verilmezse eski davranış. + opts.md2Funcs: MilkDrop'un iç işlev adları (`_aboeq` …) tanınsın mı. + Ayrıştırma anında karar verildiği için anahtar çevrilince bu adları + kullanan preset yeniden kuruluyor (readingsDiffer). */ function compile(src, pool, opts) { const p = pool || new Pool(); const o = opts || {}; let stmts; try { - stmts = parse(src); + stmts = parse(src, { md2: !!o.md2Funcs }); } catch (e) { return { run: () => {}, pool: p, error: String(e.message || e), statements: 0 }; } @@ -653,19 +831,34 @@ panel bunu göstersin diye. Sessizce çalıştırmak, kullanıcıya yanlış görünen bir sahnenin sebebini saklardı. */ const skipped = (stmts.errors || []).slice(); + const mode = o.mode || { md2: false }; // Yardımcılar kapanışa dışarıdan verilir; üretilen kodda serbest // tanımlayıcı yoktur. const F = (v) => (isFinite(v) ? v : 0); const D = (a, b) => (b === 0 ? 0 : F(a / b)); + // Kalan: uyum açıkken MilkDrop'unki (MOD_MD2), kapalıyken işaretli const M = (a, b) => { + if (mode.md2) return MOD_MD2(a, b); const bi = b | 0; return bi === 0 ? 0 : (a | 0) % bi; }; - // rand(n): 0..n-1 tam sayı. Tohumlu, çünkü çevrimdışı dışa aktarımın - // kare kare tekrarlanabilir olması gerekiyor. + /* rand(n). Tohumlu, çünkü çevrimdışı dışa aktarımın kare kare + tekrarlanabilir olması gerekiyor. + + Uyum açıkken MilkDrop'unki: TAM SAYI DEĞİL, 0 ile max(1, floor(n)) + arasında ondalıklı bir sayı (nseel-cfunc.c nseel_int_rand: + `genrand_int32() * (1.0 / 0xFFFFFFFF) * x`). Tam sayı veren dal + NSEEL_EEL1_COMPAT_MODE'a bağlı ve iki MilkDrop kod tabanı da onu + açmıyor; korpusta sık görülen `int(rand(4))` bu yüzden var. Üreteç + bizim (MilkDrop'unki Mersenne Twister), aralık ve dağılım aynı. + Kapalıyken eski davranış: 0..n−1 tam sayı. */ let seed = (o.seed || 12345) >>> 0; const R = (n) => { seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; + if (mode.md2) { + const x = Math.floor(n); + return seed * (1 / 4294967295) * (x < 1 ? 1 : x); + } const k = Math.max(1, Math.floor(n) || 1); return (seed / 4294967296) * k | 0; }; @@ -673,7 +866,7 @@ /* Yardımcılar kapanışlara buradan verilir. R tohumu dışarıda tuttuğu için resetSeed sonradan da çalışır. */ const budget = { n: 0 }; - const cx = { F, D, M, R, budget }; + const cx = { F, D, M, R, budget, mode }; const LOOP_BUDGET = Math.max(0, Number(o.loopBudget) || 65536); let prog; @@ -1042,7 +1235,9 @@ ve kare değişkeni adları (eski ayrıştırıcı `decay=` gibi bir başlık satırını da okuyordu); - denklemler boşluksuz, shader'lar satır sonları kırpılarak: MilkDrop - satır sonundaki boşluğu koruyor, eski ayrıştırıcı kırpıyordu. + satır sonundaki boşluğu koruyor, eski ayrıştırıcı kırpıyordu; + - kodda MilkDrop'un iç işlev adları (`_aboeq` …) varsa metin aynı + olsa da DERLEME farklı: o adlar yalnız uyum açıkken tanınıyor. Boşluksuz karşılaştırma, satır sonundaki bir boşluğun iki simgeyi ayırdığı durumu kaçırabilir; o preset bir sonraki yüklenişinde doğru okunuyor, yalnız anahtar çevrildiği an eski okuyuşla kalıyor. */ @@ -1062,6 +1257,10 @@ })); const shaders = (f) => [f.warpShader, f.compShader].map((s) => String(s || '').replace(/[ \t]+$/gm, '')).join('\u0000'); if (eqs(a) !== eqs(b) || shaders(a) !== shaders(b)) return true; + const code = (f) => [f.init, f.perFrame, f.perPixel] + .concat(...(f.waves || []).map((w) => [w.init, w.per_frame, w.per_point]), + ...(f.shapes || []).map((s) => [s.init, s.per_frame])).join('\n'); + if (MD2_FUNC_RE.test(code(a)) || MD2_FUNC_RE.test(code(b))) return true; if (JSON.stringify(md2Versions(a.params)) !== JSON.stringify(md2Versions(b.params))) return true; // Sürüm anahtarları bu kümede yok (readVersions kaydetmiyor): yukarıda karşılaştırıldılar for (const k of MD2_READ_KEYS) { @@ -1307,7 +1506,13 @@ seçiyor (bkz. SHARED_LEGACY). Dosyanın hangi kuralla OKUNDUĞUNU da o seçiyor (#580, `readMilk`) — okuyuş kurulumda bir kez yapılıyor ve `readAcc`ta kalıyor; anahtar sonradan çevrilirse görselleştirici - iki okuyuş ayrışıyorsa presetini yeniden kuruyor. */ + iki okuyuş ayrışıyorsa presetini yeniden kuruyor. + + Derlenmiş kodun hepsi (ana bloklar, dalgalar, şekiller) `_mode`u + paylaşıyor; anahtar yazılınca o da çevriliyor (`accurate` + erişimcisi), yani ifadelerin MilkDrop sınamaları aynı karede + değişiyor. */ + this._mode = { md2: true }; this.accurate = o.accurate !== false; this.readAcc = this.accurate; this.file = readMilk(text, this.accurate); @@ -1393,9 +1598,10 @@ per_frame saniyede 60 kez, per_pixel ise ağın 1271 düğümünde yani saniyede ~76 bin kez. Tek bir sabit bütçe ya init'i boğardı ya da per_pixel'de uygulamayı dondururdu. */ - this.cInit = compile(this.file.init, this.pool, { seed: o.seed, loopBudget: 1048576 }); - this.cFrame = compile(this.file.perFrame, this.pool, { seed: o.seed, loopBudget: 65536 }); - this.cPixel = compile(this.file.perPixel, this.pool, { seed: o.seed, loopBudget: 1024 }); + const eel = this._eel(); + this.cInit = compile(this.file.init, this.pool, Object.assign({ seed: o.seed, loopBudget: 1048576 }, eel)); + this.cFrame = compile(this.file.perFrame, this.pool, Object.assign({ seed: o.seed, loopBudget: 65536 }, eel)); + this.cPixel = compile(this.file.perPixel, this.pool, Object.assign({ seed: o.seed, loopBudget: 1024 }, eel)); for (const c of [this.cInit, this.cFrame, this.cPixel]) { if (c.error) this.errors.push(c.error); } @@ -1428,6 +1634,17 @@ this.shapes = this._collect('shapecode', this.file.shapes).map((s) => this._buildShape(s, o)); } + get accurate() { return this._acc; } + set accurate(v) { + this._acc = v !== false; + this._mode.md2 = this._acc; + } + + /* Derleme seçenekleri: paylaşılan `mode` ve iç adlar. İç adların kararı + OKUYUŞLA birlikte veriliyor (`readAcc`) — ikisi de kurulumda bir kez; + anahtar çevrilince görselleştirici presetini yeniden kuruyor. */ + _eel() { return { mode: this._mode, md2Funcs: this.readAcc !== false }; } + /* Blok numaralarını DENKLEMLERDEN ve PARAMETRELERDEN birlikte toplar. Yalnızca denklem bloklarına bakmak yetmiyor: bir şekil tamamen @@ -1476,9 +1693,10 @@ }; /* per_point saniyede samples×60 kez koşuyor; bütçe per_pixel'inkiyle aynı mantıkta, blok başına veriliyor. */ - wave.cInit = compile(w.init || '', pool, { seed: o.seed, loopBudget: 65536 }); - wave.cFrame = compile(w.per_frame || '', pool, { seed: o.seed, loopBudget: 65536 }); - wave.cPoint = compile(w.per_point || '', pool, { seed: o.seed, loopBudget: 1024 }); + const eel = this._eel(); + wave.cInit = compile(w.init || '', pool, Object.assign({ seed: o.seed, loopBudget: 65536 }, eel)); + wave.cFrame = compile(w.per_frame || '', pool, Object.assign({ seed: o.seed, loopBudget: 65536 }, eel)); + wave.cPoint = compile(w.per_point || '', pool, Object.assign({ seed: o.seed, loopBudget: 1024 }, eel)); for (const c of [wave.cInit, wave.cFrame, wave.cPoint]) { if (c.error) this.errors.push('wave ' + i + ': ' + c.error); } @@ -1535,8 +1753,9 @@ renklerde — korpustaki 15.982 açık şeklin hiçbiri atlamıyor. Uyum açıkken bu taban geçerli (shapeFrame; #580). */ shape.baseMd2 = Object.assign({}, shape.base, { g: g('g', 0), b: g('b', 0), g2: g('g2', 1) }); - shape.cInit = compile(s.init || '', pool, { seed: o.seed, loopBudget: 65536 }); - shape.cFrame = compile(s.per_frame || '', pool, { seed: o.seed, loopBudget: 65536 }); + const eel = this._eel(); + shape.cInit = compile(s.init || '', pool, Object.assign({ seed: o.seed, loopBudget: 65536 }, eel)); + shape.cFrame = compile(s.per_frame || '', pool, Object.assign({ seed: o.seed, loopBudget: 65536 }, eel)); for (const c of [shape.cInit, shape.cFrame]) { if (c.error) this.errors.push('shape ' + i + ': ' + c.error); } diff --git a/src/visualizer/modes/milkdrop.js b/src/visualizer/modes/milkdrop.js index f4de499e..ece62b27 100644 --- a/src/visualizer/modes/milkdrop.js +++ b/src/visualizer/modes/milkdrop.js @@ -5495,6 +5495,7 @@ void main(){ const M = typeof window !== 'undefined' && window.SVMilkdrop; if (!S || !M) return; if (!this.sprites) this.sprites = new S.SpriteSet(M); + this.sprites.setAccurate(this._wantAcc !== false); if (c.op === 'launch' && c.def) { this.sprites.launch(c.def, { seed: c.seed, time: this.time, frame: this.frameNo, key: c.key }); } else if (c.op === 'kill') this.sprites.killNum(c.num); @@ -5609,6 +5610,7 @@ void main(){ this._takeSpriteCommands(); if (!this.sprites || !this.sprites.count()) { this._releaseSpriteTex(); return; } const S = window.SVMilkdropSprites; + this.sprites.setAccurate(this._wantAcc !== false); const list = this.sprites.step(this._spriteIn || {}); if (list.length && this._ensureSpriteGL(gl)) { const L = this.locSprite; diff --git a/tests/milkdrop-md2-eel.test.js b/tests/milkdrop-md2-eel.test.js new file mode 100644 index 00000000..3de19c80 --- /dev/null +++ b/tests/milkdrop-md2-eel.test.js @@ -0,0 +1,345 @@ +'use strict'; +/* MILKDROP 2'NİN İFADE DERLEYİCİSİ (#580). + * + * Uyum açıkken denklemler MilkDrop 2'nin ifade derleyicisinin kurallarıyla + * koşuyor. Kaynak Nullsoft'un ns-eel2'si (BeatDrop kopyası 53d83ee): + * - işleçlerin iç adları (nseel-compiler.c fnTable1, ad çevirisi + * nseel-eval.c): `_aboeq(a, b)` `a >= b`, `_and` kısa devreli `&&` …; + * - doğruluk ve eşitlik sınamalarında 0,00001'lik yakınlık payı + * (NSEEL_CLOSEFACTOR, ns-eel-int.h; asm-nseel-x86-msvc.c `fcomp + * closefact`): `if`, `!`, `&&`, `||`, `while`, `bnot` için |x| >= pay + * doğru, `band`/`bor` işlevlerinde |x| > pay; `==`/`equal` |a − b| < pay. + * Uyum kapalıyken eski birebir sınamalar ve eski ad kümesi duruyor. */ +const test = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); + +const M = require('../src/shared/milkdrop.js'); +const S = require('../src/shared/milkdrop-sprites.js'); +const ROOT = path.join(__dirname, '..'); + +// Kodu verilen kipte koşturur, havuzu döndürür +function run(src, md2, pre) { + const pool = new M.Pool(); + for (const k in pre || {}) pool.set(k, pre[k]); + const c = M.compile(src, pool, { mode: { md2 }, md2Funcs: md2 }); + c.run(); + return { pool, error: c.error, get: (k) => pool.get(k) }; +} + +test('iç adlar uyum açıkken işleçlerin kendisi, kapalıyken bilinmeyen işlev', () => { + const src = [ + 'a = _aboeq(3, 3); b = _aboeq(2, 3); c = _beleq(3, 3); d = _above(3, 3); e = _below(2, 3);', + 'f = _equal(2, 2); g = _noteq(2, 2); h = _not(0); i = _if(0, 5, 6); j = _mod(7, 3);', + ].join('\n'); + const on = run(src, true); + assert.strictEqual(on.error, ''); + assert.deepStrictEqual('abcdefghij'.split('').map(on.get), [1, 0, 1, 0, 1, 1, 0, 1, 6, 1]); + const off = run(src, false); + assert.match(off.error, /bilinmeyen fonksiyon '_aboeq'/); + assert.strictEqual(off.get('a'), 0, 'kapalıyken deyim atlanıyor'); +}); + +test('iç atama adları: _set, _addop … ilk argümana yazıyor', () => { + const on = run('_set(a, 4); _addop(a, 2); _mulop(a, 3); _subop(a, 1); _divop(a, 17); _set(b, 7); _modop(b, 4); _set(c, 1); _orop(c, 6); _andop(c, 3); _set(d, 2); _powop(d, 10);', true); + assert.strictEqual(on.error, ''); + assert.strictEqual(on.get('a'), 1); + assert.strictEqual(on.get('b'), 3); + assert.strictEqual(on.get('c'), 3); + assert.strictEqual(on.get('d'), 1024); + assert.match(run('_set(1, 2);', true).error, /bir değişken ve bir değer ister/); +}); + +test('_mem/_gmem megabuf/gmegabuf\'un iç adı: okunuyor ve yazılıyor', () => { + const on = run('_mem(3) = 7; megabuf(4) = 8; a = _mem(3) + megabuf(4); _gmem(12345) = 2; b = gmegabuf(12345);', true); + assert.strictEqual(on.error, ''); + assert.strictEqual(on.get('a'), 15); + assert.strictEqual(on.get('b'), 2); + assert.match(run('a = _mem(1, 2);', true).error, /1 argüman ister/); +}); + +test('iç adlar büyük/küçük harf ayırmıyor', () => { + const on = run('a = _AboEq(2, 1); b = _IF(1, 3, 4);', true); + assert.strictEqual(on.error, ''); + assert.strictEqual(on.get('a'), 1); + assert.strictEqual(on.get('b'), 3); +}); + +test('_and/_or kısa devreli (&& ve ||); band/bor iki tarafı da hesaplıyor', () => { + const on = run('a = _and(0, x = 5); b = _or(1, y = 5); c = band(0, z = 5); d = bor(1, w = 5);', true); + assert.strictEqual(on.get('x'), 0, '_and sağ tarafı hesaplamamalı'); + assert.strictEqual(on.get('y'), 0, '_or sağ tarafı hesaplamamalı'); + assert.strictEqual(on.get('z'), 5, 'band iki tarafı da hesaplıyor'); + assert.strictEqual(on.get('w'), 5, 'bor iki tarafı da hesaplıyor'); + assert.deepStrictEqual(['a', 'b', 'c', 'd'].map(on.get), [0, 1, 0, 1]); +}); + +// ------------------------------------------------------------ yakınlık payı + +const CLOSE = 0.00001; +const BELOW = 0.0000099; +const ABOVE = 0.0000101; + +test('if, !, &&, ||, bnot: |x| >= 0,00001 doğru (sınır dahil)', () => { + for (const [v, want] of [[CLOSE, 1], [-CLOSE, 1], [BELOW, 0], [-BELOW, 0], [ABOVE, 1], [0, 0]]) { + const on = run('a = if(v, 1, 0); b = !v; c = v && 1; d = v || 0; e = bnot(v);', true, { v }); + assert.deepStrictEqual(['a', 'b', 'c', 'd', 'e'].map(on.get), [want, 1 - want, want, want, 1 - want], 'v=' + v); + } +}); + +test('band/bor işlevleri: |x| > 0,00001 doğru — sınırda && ve ||den farklı', () => { + for (const [v, want] of [[CLOSE, 0], [-CLOSE, 0], [BELOW, 0], [ABOVE, 1], [-ABOVE, 1]]) { + const on = run('a = band(v, 1); b = bor(v, 0);', true, { v }); + assert.deepStrictEqual([on.get('a'), on.get('b')], [want, want], 'v=' + v); + } +}); + +test('==, !=, equal: |a − b| < 0,00001 eşit; <, >, above, below birebir', () => { + const on = run('a = 0.3 + 0.6 == 0.9; b = equal(1, 1 + 0.0000099); c = equal(0.00001, 0); d = 1 != 1.0000099; e = 0.00001 != 0; f = above(1.0000001, 1); g = 1 < 1.0000001; h = _aboeq(1, 1.0000001);', true); + assert.deepStrictEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'].map(on.get), [1, 1, 0, 0, 1, 1, 1, 0]); + const off = run('a = 0.3 + 0.6 == 0.9; b = equal(1, 1 + 0.0000099); d = 1 != 1.0000099;', false); + assert.deepStrictEqual(['a', 'b', 'd'].map(off.get), [0, 0, 1], 'kapalıyken birebir'); +}); + +test('while: son değer |x| < 0,00001 olunca duruyor', () => { + // Sıra: 0,00001 (sınır, doğru) → 0,000005 → 0 + const src = 'n = 0; v = 3; while(n = n + 1; v = v - 1; v * 0.000005);'; + assert.strictEqual(run(src, true).get('n'), 2, '0,000005 yanlış'); + assert.strictEqual(run(src, false).get('n'), 3, 'kapalıyken tam sıfıra kadar'); +}); + +test('NaN: x87 karşılaştırması sırasız — yanlış sayılıyor, NaN\'lı eşitlik doğru', () => { + const nan = '(0 * (1e308 * 10))'; + const on = run(`a = if(${nan}, 1, 2); b = equal(${nan}, 3); c = !${nan};`, true); + assert.deepStrictEqual(['a', 'b', 'c'].map(on.get), [2, 1, 1]); + const off = run(`a = if(${nan}, 1, 2); b = equal(${nan}, 3); c = !${nan};`, false); + assert.deepStrictEqual(['a', 'b', 'c'].map(off.get), [1, 0, 0]); +}); + +test('kip çalışma anında: aynı derleme mode.md2 çevrilince öbür sınamayı kullanıyor', () => { + const pool = new M.Pool(); + const mode = { md2: false }; + const c = M.compile('a = if(0.000005, 1, 2); b = equal(1, 1.000005);', pool, { mode }); + c.run(); + assert.deepStrictEqual([pool.get('a'), pool.get('b')], [1, 0]); + mode.md2 = true; + c.run(); + assert.deepStrictEqual([pool.get('a'), pool.get('b')], [2, 1]); + // Seçenek verilmezse eski davranış + const p2 = new M.Pool(); + M.compile('a = if(0.000005, 1, 2); b = _aboeq(1, 0);', p2).run(); + assert.strictEqual(p2.get('a'), 1); + assert.strictEqual(p2.get('b'), 0, 'iç ad yok'); +}); + +// ------------------------------------------------------------ kalan + +test('%: uyum açıkken iki tarafın mutlak değerinin tam kısmıyla, sonuç hiç negatif değil', () => { + const src = 'a = -7 % 3; b = 7 % -3; c = -7 % -3; d = 7.9 % 3.9; e = 5 % 0.5 + 1; f = -7.5 % 2; x %= -4; h = _mod(-9, 4); megabuf(1) = -10; megabuf(1) %= 3; k = megabuf(1);'; + const on = run(src, true, { x: -10 }); + assert.deepStrictEqual(['a', 'b', 'c', 'd', 'e', 'f', 'x', 'h', 'k'].map(on.get), [1, 1, 1, 1, 1, 1, 2, 1, 1]); + const off = run(src, false, { x: -10 }); + assert.deepStrictEqual(['a', 'b', 'c', 'd', 'e', 'f', 'x', 'k'].map(off.get), [-1, 1, -1, 1, 1, -1, -2, -1], 'kapalıyken işaretli'); +}); + +test('%: 32 bite sığmayan değer MilkDrop\'ta 2^31 oluyor (x87 belirsiz tam sayısı, işaretsiz)', () => { + const on = run('a = 3000000000 % 7; b = 7 % 3000000000; c = -3000000000 % 1000;', true); + assert.deepStrictEqual(['a', 'b', 'c'].map(on.get), [2147483648 % 7, 7, 2147483648 % 1000]); +}); + +test('& ve |: uyum açıkken 64 bitlik tam sayılarla; 32 bite sığanda aynı', () => { + const src = 'a = 3000000000 & 4294967295; b = 1099511627776 | 1; c = 5.9 & 3; d = -1 & 255; e = -7.5 | 0; z = 1e30 & 1; w = 1e30 | 0; v = -3000000000.5 & -1;'; + const on = run(src + ' _andop(x, 6); _orop(y, 4294967296);', true, { x: 13, y: 1 }); + assert.deepStrictEqual(['a', 'b', 'c', 'd', 'e', 'z', 'x', 'y'].map(on.get), [3000000000, 1099511627777, 1, 255, -7, 0, 4, 4294967297]); + assert.strictEqual(on.get('w'), -9223372036854775808, 'sığmayan 64 bit −2^63'); + assert.strictEqual(on.get('v'), -3000000000, 'sıfıra doğru kırpma'); + const off = run(src, false); + assert.deepStrictEqual(['a', 'b', 'c', 'd', 'e'].map(off.get), [-1294967296, 1, 1, 255, -7], 'kapalıyken 32 bit'); +}); + +// ------------------------------------------------------------ bellek + +test('megabuf indisi uyum açıkken trunc(x + 0,00001): 2,99999999 3. göz, −1 0. göz', () => { + const src = 'megabuf(2.99999999) = 5; a = megabuf(3); megabuf(-1) = 7; b = megabuf(0); megabuf(1.99999999) += 2; c = megabuf(2); d = megabuf(-1.5);'; + const on = run(src, true); + assert.deepStrictEqual(['a', 'b', 'c', 'd'].map(on.get), [5, 7, 2, 0]); + const off = run(src, false); + assert.deepStrictEqual(['a', 'b', 'c', 'd'].map(off.get), [0, 0, 5, 0], 'kapalıyken |0: 2,99999999 → 2, −1 atılıyor, 1,99999999 → 1'); + assert.strictEqual(off.pool.mem.get(2, 1048576), 5, 'kapalıyken 2,99999999 → 2. göz'); +}); + +test('megabuf uyum açıkken 8.388.608 girdi, kapalıyken 1.048.576; dışı 0 ve yazılan atılıyor', () => { + const src = 'megabuf(5000000) = 9; a = megabuf(5000000); b = (megabuf(8388608) = 4); c = megabuf(8388608); megabuf(8388607) = 3; d = megabuf(8388607); megabuf(6000000) = 1; megabuf(6000000) += 2; e = megabuf(6000000);'; + const on = run(src, true); + assert.deepStrictEqual(['a', 'b', 'c', 'd', 'e'].map(on.get), [9, 4, 0, 3, 3], 'atama değerini yine döndürüyor'); + const off = run(src, false); + assert.deepStrictEqual(['a', 'b', 'c', 'd', 'e'].map(off.get), [0, 4, 0, 0, 0]); +}); + +test('gmegabuf uyum açıkken 2^20\'de sarıyor (negatif de); presetler arasında ortak', () => { + const on = run('gmegabuf(1048576 + 777001) = 4; a = gmegabuf(777001); gmegabuf(-2) = 6; b = gmegabuf(1048575); c = gmegabuf(-2); gmegabuf(777003) = 8; e = gmegabuf(777003 - 524288);', true); + assert.deepStrictEqual(['a', 'b', 'c', 'e'].map(on.get), [4, 6, 6, 0], 'sarma 2^20 girdide, 2^19 girdide değil'); + const other = run('d = gmegabuf(777001);', true); + assert.strictEqual(other.get('d'), 4, 'başka havuz aynı gmegabuf\'u görüyor'); + const off = run('gmegabuf(1048576 + 777002) = 4; a = gmegabuf(777002); gmegabuf(-3) = 6; b = gmegabuf(1048574);', false); + assert.deepStrictEqual(['a', 'b'].map(off.get), [0, 0], 'kapalıyken sarmıyor, negatif atılıyor'); +}); + +// ------------------------------------------------------------ rand + +test('rand(n) uyum açıkken 0 ile max(1, floor(n)) arasında ONDALIKLI; kapalıyken 0..n−1 tam sayı', () => { + const pool = new M.Pool(); + const mode = { md2: true }; + const c = M.compile('a = rand(4); b = rand(0.5); d = rand(-3); e = rand(4.9);', pool, { mode, seed: 7 }); + let frac = 0, minA = 99, maxA = 0, maxB = 0, maxD = 0, maxE = 0; + for (let i = 0; i < 2000; i++) { + c.run(); + const a = pool.get('a'); + if (a !== Math.floor(a)) frac++; + minA = Math.min(minA, a); maxA = Math.max(maxA, a); + maxB = Math.max(maxB, pool.get('b')); maxD = Math.max(maxD, pool.get('d')); maxE = Math.max(maxE, pool.get('e')); + } + assert.ok(frac > 1990, 'ondalıklı: ' + frac); + assert.ok(minA >= 0 && maxA <= 4 && maxA > 3.9, 'aralık 0..4: ' + minA + '..' + maxA); + assert.ok(maxB <= 1 && maxB > 0.9, 'n < 1 → 1: ' + maxB); + assert.ok(maxD <= 1 && maxD > 0.9, 'negatif → 1: ' + maxD); + assert.ok(maxE <= 4 && maxE > 3.9, 'floor(4,9) = 4: ' + maxE); + mode.md2 = false; + let ints = true, maxL = 0; + for (let i = 0; i < 2000; i++) { + c.run(); + const a = pool.get('a'); + if (a !== Math.floor(a)) ints = false; + maxL = Math.max(maxL, a); + } + assert.ok(ints && maxL === 3, 'kapalıyken 0..3 tam sayı'); +}); + +test('rand: aynı tohum aynı sayılar; iki kip aynı üreteci ilerletiyor', () => { + const seq = (md2) => { + const p = new M.Pool(); + const c = M.compile('a = rand(10);', p, { mode: { md2 }, seed: 99 }); + const out = []; + for (let i = 0; i < 5; i++) { c.run(); out.push(p.get('a')); } + return out; + }; + const on = seq(true); + assert.deepStrictEqual(seq(true), on, 'tekrarlanabilir'); + assert.deepStrictEqual(on.map(Math.floor), seq(false), 'kapalıyken aynı sayının tam kısmı'); +}); + +// ------------------------------------------------------------ preset bağlantısı + +const PRESET = [ + 'MILKDROP_PRESET_VERSION=201', + 'per_frame_1=q1 = if(0.000005, 1, 2);', + 'per_frame_2=q2 = _aboeq(2, 1);', + 'wavecode_0_enabled=1', + 'wave_0_per_frame1=t1 = equal(1, 1.000005);', + 'shapecode_0_enabled=1', + 'shape_0_per_frame1=x = if(0.000005, 0.1, 0.9);', + '', +].join('\n'); + +test('Preset: anahtar bütün blokların ortak kipini çeviriyor (dalga ve şekil dahil)', () => { + const p = new M.Preset(PRESET, { seed: 1, accurate: true }); + p.frame({ time: 0, frame: 0 }); + assert.deepStrictEqual([p.get('q1'), p.get('q2')], [2, 1]); + p.waveFrame(p.waves[0]); + assert.strictEqual(p.waves[0].pool.get('t1'), 1); + assert.strictEqual(p.shapeFrame(p.shapes[0], 0).x, 0.9); + + p.accurate = false; + p.frame({ time: 0, frame: 1 }); + assert.strictEqual(p.get('q1'), 1, 'kapalıyken birebir: 0,000005 doğru'); + p.waveFrame(p.waves[0]); + assert.strictEqual(p.waves[0].pool.get('t1'), 0); + assert.strictEqual(p.shapeFrame(p.shapes[0], 0).x, 0.1); + assert.strictEqual(p.accurate, false); + p.accurate = undefined; + assert.strictEqual(p.accurate, true, 'false dışındaki her değer açık'); +}); + +test('Preset: iç adlar okuyuşla birlikte kuruluşta kararlaştırılıyor', () => { + const off = new M.Preset(PRESET, { seed: 1, accurate: false }); + off.frame({ time: 0, frame: 0 }); + assert.strictEqual(off.get('q2'), 0, 'kapalı kurulan preset iç adı tanımıyor'); + assert.ok(off.errors.some((e) => /_aboeq/.test(e))); + off.accurate = true; + off.frame({ time: 0, frame: 1 }); + assert.strictEqual(off.get('q2'), 0, 'yeniden kurulmadan iç ad derlenmiyor — görselleştirici yeniden kuruyor'); +}); + +test('readingsDiffer: kod iç ad kullanıyorsa anahtar çevrilince preset yeniden kuruluyor', () => { + assert.strictEqual(M.readingsDiffer('MILKDROP_PRESET_VERSION=201\nper_frame_1=a = above(2, 1);\n'), false); + assert.strictEqual(M.readingsDiffer('MILKDROP_PRESET_VERSION=201\nper_frame_1=a = _aboeq(2, 1);\n'), true); + assert.strictEqual(M.readingsDiffer('MILKDROP_PRESET_VERSION=201\nwave_0_per_point1=a = _ABOEQ (2, 1);\n'), true, 'harf ve boşluk'); + assert.strictEqual(M.readingsDiffer('MILKDROP_PRESET_VERSION=201\nper_frame_1=my_aboeq = 1; b = my_aboeq(2);\n'), false, 'adın parçası değil'); +}); + +test('sprite kodu da MilkDrop kurallarıyla: anahtar setAccurate ile', () => { + const def = { num: '01', img: 'a.png', colorkey: 0, init: 'k1 = if(0.000005, 1, 2); k2 = _aboeq(1, 0);', code: 'k3 = equal(1, 1.000005);', desc: '' }; + const set = new S.SpriteSet(M); + set.launch(def, { seed: 1 }); + const P = set.slots[0].pool; + assert.deepStrictEqual([P.get('k1'), P.get('k2')], [2, 1]); + set.step({ time: 0, frame: 0 }); + assert.strictEqual(P.get('k3'), 1); + set.setAccurate(false); + set.step({ time: 0, frame: 1 }); + assert.strictEqual(P.get('k3'), 0, 'kapalıyken birebir'); + set.launch(def, { seed: 1 }); + const P2 = set.slots[1].pool; + assert.deepStrictEqual([P2.get('k1'), P2.get('k2')], [1, 0], 'kapalıyken başlatılan iç adı tanımıyor'); +}); + +test('görselleştirici sprite setine anahtarı başlatmadan ve her kareden önce veriyor', () => { + const src = fs.readFileSync(path.join(ROOT, 'src/visualizer/modes/milkdrop.js'), 'utf8'); + const set = 'this.sprites.setAccurate(this._wantAcc !== false);'; + const i = src.indexOf(set); + assert.ok(i > 0 && i < src.indexOf('this.sprites.launch('), 'başlatmadan önce'); + const j = src.indexOf(set, i + 1); + assert.ok(j > 0 && j < src.indexOf('this.sprites.step('), 'kareden önce'); +}); + +/* İfade derleyicisini çağıran her yer kipini AÇIKÇA veriyor. Verilmeyen bir + yer uyum açıkken bile eski sınamalarla koşardı ve bu hiçbir yerde + görünmezdi. */ +// Açılan parantezin kapandığı yere kadar (dize içindeki parantezler sayılmıyor) +function callArgs(src, from) { + let depth = 1; + let q = null; + let i = from; + for (; i < src.length && depth > 0; i++) { + const ch = src[i]; + if (q) { + if (ch === '\\') i++; + else if (ch === q) q = null; + continue; + } + if (ch === '"' || ch === "'" || ch === '`') q = ch; + else if (ch === '(') depth++; + else if (ch === ')') depth--; + } + return src.slice(from, i - 1); +} + +test('kaynakta her ifade derlemesi kipini veriyor', () => { + const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).flatMap((e) => + (e.isDirectory() ? walk(path.join(d, e.name)) : e.name.endsWith('.js') ? [path.join(d, e.name)] : [])); + let found = 0; + for (const file of walk(path.join(ROOT, 'src'))) { + const src = fs.readFileSync(file, 'utf8'); + const isEngine = file.endsWith(path.join('shared', 'milkdrop.js')); + // Motorun kendi içinde `compile(`, dışarıda SVMilkdrop api'si üzerinden `M.compile(` + const re = isEngine ? /(?