From 1e2958a797f4a84f40f08b84d12732e05bbba62f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Wed, 23 Sep 2026 23:43:02 +0300 Subject: [PATCH 1/6] fix(milkdrop): truth tests and internal names as MilkDrop 2 compiles them With fidelity on, truth and equality tests use the closeness factor of MilkDrop 2's expression compiler (NSEEL_CLOSEFACTOR, 0.00001, in Nullsoft's ns-eel2): - if, !, &&, ||, while and bnot treat |x| >= 0.00001 as true; - the band and bor functions need |x| > 0.00001; - ==, != and equal compare |a - b| with it; - <, >, <=, >=, above and below stay exact; - NaN counts as false, and as equal, like the x87 compare. The compiler's operator functions compile too (_aboeq, _if, _and, _or, _not, _equal, _noteq, _below, _above, _beleq, _mod, _set, _addop and the other assignment forms, _mem, _gmem). _and and _or are && and ||, which skip the right side; band and bor evaluate both. Three corpus presets call _aboeq. Compiled code reads a mode object that all blocks of a preset share, so flipping fidelity switches the tests in the same frame. Internal names are decided when the code is parsed, so readingsDiffer reports presets that use them and the visualizer rebuilds those on a flip. Sprites follow the same switch, and the self-test compiles in the default mode. With fidelity off nothing changes. Refs #580, #560 --- README.md | 4 +- README.tr.md | 4 +- ROADMAP.md | 2 +- src/main/main.js | 2 +- src/shared/milkdrop-sprites.js | 12 +- src/shared/milkdrop.js | 188 ++++++++++++++++++++--- src/visualizer/modes/milkdrop.js | 2 + tests/milkdrop-md2-eel.test.js | 249 +++++++++++++++++++++++++++++++ 8 files changed, 432 insertions(+), 31 deletions(-) create mode 100644 tests/milkdrop-md2-eel.test.js diff --git a/README.md b/README.md index 6a066cfd..eb9bc1d7 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-2219%20passing-2ea043.svg)](#tests) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1315,7 +1315,7 @@ npm test npm start -- --smoke ``` -**2202 unit tests, all passing.** They are written to check answers, not to exercise lines: +**2219 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..ee91035b 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-2219%20geçiyor-2ea043.svg)](#testler) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1313,7 +1313,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: +**2219 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..4bfb5f25 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,7 +129,7 @@ npm test npm start -- --smoke ``` -- **2202 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; +- **2219 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 on `main` since. 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..0b1e9d4c 100644 --- a/src/shared/milkdrop.js +++ b/src/shared/milkdrop.js @@ -183,6 +183,56 @@ }], }; + /* 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); }; + // ========================================================================== // Ayrıştırıcı // ========================================================================== @@ -194,7 +244,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 +305,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 +384,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 +559,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': { @@ -563,8 +638,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,8 +654,14 @@ /* && 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); + 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 case '&': return (P) => (a(P) | 0) & (b(P) | 0); case '|': return (P) => (a(P) | 0) | (b(P) | 0); @@ -584,11 +672,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 +711,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; }; @@ -639,13 +748,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 }; } @@ -673,7 +789,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: o.mode || { md2: false } }; const LOOP_BUDGET = Math.max(0, Number(o.loopBudget) || 65536); let prog; @@ -1042,7 +1158,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 +1180,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 +1429,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 +1521,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 +1557,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 +1616,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 +1676,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..3f7f11fa --- /dev/null +++ b/tests/milkdrop-md2-eel.test.js @@ -0,0 +1,249 @@ +'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'); +}); + +// ------------------------------------------------------------ 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 ? /(? Date: Wed, 23 Sep 2026 23:50:44 +0300 Subject: [PATCH 2/6] fix(milkdrop): the remainder as MilkDrop 2 computes it With fidelity on, `%`, `%=` and _mod work the way MilkDrop 2's compiler does (nseel_asm_mod). Both sides become the integer part of their absolute value, and the division is unsigned, so the result is never negative: -7 % 3 is 1 in MilkDrop and was -1 here. MilkDrop runs its code with the FPU set to truncate, and a value that does not fit in 32 bits becomes the x87 integer indefinite, -2^31, where JavaScript's |0 wrapped it modulo 2^32. A divisor of 0 still gives 0. With fidelity off the old signed remainder stays. Refs #580, #560 --- README.md | 4 ++-- README.tr.md | 4 ++-- ROADMAP.md | 2 +- src/shared/milkdrop.js | 24 +++++++++++++++++++++++- tests/milkdrop-md2-eel.test.js | 15 +++++++++++++++ 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index eb9bc1d7..dbedcedf 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-2219%20passing-2ea043.svg)](#tests) +[![Tests](https://img.shields.io/badge/tests-2221%20passing-2ea043.svg)](#tests) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1315,7 +1315,7 @@ npm test npm start -- --smoke ``` -**2219 unit tests, all passing.** They are written to check answers, not to exercise lines: +**2221 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 ee91035b..fefee0d0 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-2219%20geçiyor-2ea043.svg)](#testler) +[![Test](https://img.shields.io/badge/test-2221%20geçiyor-2ea043.svg)](#testler) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1313,7 +1313,7 @@ npm test npm start -- --smoke ``` -**2219 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: +**2221 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 4bfb5f25..bd9750b5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,7 +129,7 @@ npm test npm start -- --smoke ``` -- **2219 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; +- **2221 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 on `main` since. diff --git a/src/shared/milkdrop.js b/src/shared/milkdrop.js index 0b1e9d4c..70f40f36 100644 --- a/src/shared/milkdrop.js +++ b/src/shared/milkdrop.js @@ -233,6 +233,25 @@ 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; + }; + // ========================================================================== // Ayrıştırıcı // ========================================================================== @@ -769,11 +788,14 @@ 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; }; @@ -789,7 +811,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, mode: o.mode || { md2: false } }; + const cx = { F, D, M, R, budget, mode }; const LOOP_BUDGET = Math.max(0, Number(o.loopBudget) || 65536); let prog; diff --git a/tests/milkdrop-md2-eel.test.js b/tests/milkdrop-md2-eel.test.js index 3f7f11fa..b14793bd 100644 --- a/tests/milkdrop-md2-eel.test.js +++ b/tests/milkdrop-md2-eel.test.js @@ -133,6 +133,21 @@ test('kip çalışma anında: aynı derleme mode.md2 çevrilince öbür sınamay 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]); +}); + // ------------------------------------------------------------ preset bağlantısı const PRESET = [ From 7b4c7eeb130802af18b7c19c82324928274dbce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Thu, 24 Sep 2026 00:00:12 +0300 Subject: [PATCH 3/6] fix(milkdrop): bitwise and/or on 64-bit integers as in MilkDrop 2 With fidelity on, `&` and `|` (and _andop, _orop) convert both sides to 64-bit integers the way MilkDrop 2's compiler does (nseel_asm_and/or, fistp qword under truncation), where the engine used JavaScript's 32-bit |0. Values that fit in 32 bits give the same result and keep the fast path; beyond that the 64-bit value is used, and one that does not fit in 64 bits becomes -2^63. No corpus preset reaches those values: the 319 that use the operators render the same. Refs #580, #560 --- README.md | 4 ++-- README.tr.md | 4 ++-- ROADMAP.md | 2 +- src/shared/milkdrop.js | 27 ++++++++++++++++++++++++--- tests/milkdrop-md2-eel.test.js | 10 ++++++++++ 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dbedcedf..def8971c 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-2221%20passing-2ea043.svg)](#tests) +[![Tests](https://img.shields.io/badge/tests-2222%20passing-2ea043.svg)](#tests) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1315,7 +1315,7 @@ npm test npm start -- --smoke ``` -**2221 unit tests, all passing.** They are written to check answers, not to exercise lines: +**2222 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 fefee0d0..c5ba0222 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-2221%20geçiyor-2ea043.svg)](#testler) +[![Test](https://img.shields.io/badge/test-2222%20geçiyor-2ea043.svg)](#testler) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1313,7 +1313,7 @@ npm test npm start -- --smoke ``` -**2221 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: +**2222 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 bd9750b5..ef2fbd6a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,7 +129,7 @@ npm test npm start -- --smoke ``` -- **2221 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; +- **2222 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 on `main` since. diff --git a/src/shared/milkdrop.js b/src/shared/milkdrop.js index 70f40f36..327b7823 100644 --- a/src/shared/milkdrop.js +++ b/src/shared/milkdrop.js @@ -252,6 +252,21 @@ return ub === 0 ? 0 : (FIST(Math.abs(a)) >>> 0) % ub; }; + /* 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ı // ========================================================================== @@ -681,9 +696,15 @@ 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 - case '&': return (P) => (a(P) | 0) & (b(P) | 0); - case '|': return (P) => (a(P) | 0) | (b(P) | 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; } } diff --git a/tests/milkdrop-md2-eel.test.js b/tests/milkdrop-md2-eel.test.js index b14793bd..2e062aa7 100644 --- a/tests/milkdrop-md2-eel.test.js +++ b/tests/milkdrop-md2-eel.test.js @@ -148,6 +148,16 @@ test('%: 32 bite sığmayan değer MilkDrop\'ta 2^31 oluyor (x87 belirsiz tam sa 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'); +}); + // ------------------------------------------------------------ preset bağlantısı const PRESET = [ From c2bef3cfa79c518f05edff35754b085ad1554bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Thu, 24 Sep 2026 00:10:50 +0300 Subject: [PATCH 4/6] fix(milkdrop): megabuf and gmegabuf indices as MilkDrop 2 computes them With fidelity on, memory access follows MilkDrop 2's compiler (_asm_megabuf, nseel-ram.c): - an index is trunc(x + 0.00001), so a computed 2.9999999 finds cell 3 and -1 finds cell 0; - megabuf holds 8,388,608 cells (128 blocks of 65,536; it was 1,048,576), and cells outside read 0 and drop writes; - gmegabuf is a single array of 2^20 cells that indices wrap around, negative ones included, since MilkDrop gives its compilers no separate global block. Memory is now kept in 4096-cell blocks allocated on first write, so the larger limit costs nothing until a preset uses it. With fidelity off the old index and limit stay. Refs #580, #560 --- README.md | 4 +- README.tr.md | 4 +- ROADMAP.md | 2 +- src/shared/milkdrop.js | 78 ++++++++++++++++++++++------------ tests/milkdrop-md2-eel.test.js | 28 ++++++++++++ 5 files changed, 83 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index def8971c..1aa74abe 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-2222%20passing-2ea043.svg)](#tests) +[![Tests](https://img.shields.io/badge/tests-2225%20passing-2ea043.svg)](#tests) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1315,7 +1315,7 @@ npm test npm start -- --smoke ``` -**2222 unit tests, all passing.** They are written to check answers, not to exercise lines: +**2225 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 c5ba0222..d4413baf 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-2222%20geçiyor-2ea043.svg)](#testler) +[![Test](https://img.shields.io/badge/test-2225%20geçiyor-2ea043.svg)](#testler) [![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com) @@ -1313,7 +1313,7 @@ npm test npm start -- --smoke ``` -**2222 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar: +**2225 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 ef2fbd6a..9f43f2ef 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,7 +129,7 @@ npm test npm start -- --smoke ``` -- **2222 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0; +- **2225 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 on `main` since. diff --git a/src/shared/milkdrop.js b/src/shared/milkdrop.js index 327b7823..9530af8e 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; }, }; @@ -252,6 +259,10 @@ 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ı @@ -629,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. */ @@ -644,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': @@ -767,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 diff --git a/tests/milkdrop-md2-eel.test.js b/tests/milkdrop-md2-eel.test.js index 2e062aa7..e0f3fe51 100644 --- a/tests/milkdrop-md2-eel.test.js +++ b/tests/milkdrop-md2-eel.test.js @@ -158,6 +158,34 @@ test('& ve |: uyum açıkken 64 bitlik tam sayılarla; 32 bite sığanda aynı', 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'); +}); + // ------------------------------------------------------------ preset bağlantısı const PRESET = [ From ce92ca2192dcda988fde8251ee8446b556d2d656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Thu, 24 Sep 2026 00:18:18 +0300 Subject: [PATCH 5/6] fix(milkdrop): rand returns a real number, as in MilkDrop 2 With fidelity on, rand(n) returns a real number between 0 and max(1, floor(n)), as MilkDrop 2's compiler does (nseel_int_rand: genrand_int32() * (1.0 / 0xFFFFFFFF) * x). The integer branch belongs to NSEEL_EEL1_COMPAT_MODE, which neither MilkDrop codebase turns on; the int(rand(4)) that is common in the corpus is how presets ask for an integer. The engine returned an integer below n, so rand(1) was always 0. The generator stays our seeded one, so exports and thumbnails remain reproducible; only the range and type change. 5,361 corpus presets call rand. With fidelity off it still returns an integer. Refs #580, #560 --- README.md | 4 ++-- README.tr.md | 4 ++-- ROADMAP.md | 2 +- src/shared/milkdrop.js | 16 +++++++++++-- tests/milkdrop-md2-eel.test.js | 43 ++++++++++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1aa74abe..f25429d5 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-2225%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) @@ -1315,7 +1315,7 @@ npm test npm start -- --smoke ``` -**2225 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 d4413baf..d935e7d5 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-2225%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) @@ -1313,7 +1313,7 @@ npm test npm start -- --smoke ``` -**2225 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 9f43f2ef..40508110 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,7 +129,7 @@ npm test npm start -- --smoke ``` -- **2225 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 on `main` since. diff --git a/src/shared/milkdrop.js b/src/shared/milkdrop.js index 9530af8e..059518b4 100644 --- a/src/shared/milkdrop.js +++ b/src/shared/milkdrop.js @@ -842,11 +842,23 @@ 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; }; diff --git a/tests/milkdrop-md2-eel.test.js b/tests/milkdrop-md2-eel.test.js index e0f3fe51..3de19c80 100644 --- a/tests/milkdrop-md2-eel.test.js +++ b/tests/milkdrop-md2-eel.test.js @@ -186,6 +186,49 @@ test('gmegabuf uyum açıkken 2^20\'de sarıyor (negatif de); presetler arasınd 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 = [ From 699fb5af41d11404026267133c13224b1f347ce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=87A=C4=9EAN=20TURGUT?= Date: Mon, 28 Sep 2026 23:06:39 +0300 Subject: [PATCH 6/6] docs: equations run by MilkDrop 2's compiler rules ROADMAP (#580) describes the expression rules, the measurements against the previous step and the 900-preset sample, and rewrites the list of what is not done yet: variable scope, division by zero and the loop cap are next; newer syntax and unused functions are left out with reasons. The since-tag count follows the test total. Both READMEs gain a bullet. Refs #580, #560 --- README.md | 5 +++ README.tr.md | 5 +++ ROADMAP.md | 89 +++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f25429d5..55e633d0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/README.tr.md b/README.tr.md index d935e7d5..6b087f7b 100644 --- a/README.tr.md +++ b/README.tr.md @@ -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 diff --git a/ROADMAP.md b/ROADMAP.md index 40508110..074186c4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -131,7 +131,7 @@ npm start -- --smoke - **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