diff --git a/.claude/skills/shadertoy-recur/SKILL.md b/.claude/skills/shadertoy-recur/SKILL.md index caff19b..1bc3103 100644 --- a/.claude/skills/shadertoy-recur/SKILL.md +++ b/.claude/skills/shadertoy-recur/SKILL.md @@ -165,6 +165,21 @@ Aquests són els que no es veuen venir llegint el codi: - **`u_x3` pot no arribar.** Si l'ajust `X3_AS_SPEED` està actiu a l'aparell, aquell comandament passa a controlar la velocitat. Dissenya per a tres i deixa el quart per al menys important. +- **Un macro amb el parametre sense parentesis NO es pot "arreglar" en portar.** Mesurat + el 2026-09-05 al shader de Noztol: `#define p(t,a,b,c,d) (a + b*cos(6.28318*(c*t+d)))`, + amb `t` pelat. Se'l crida amb una suma --`sp(sin(x) + terme)`-- i en expandir queda + `c*sin(x) + terme`, o sia que el segon sumand **no** va multiplicat per `c`. Posant els + parentesis «be» el port se separava de l'original **36/255 en el 72% dels pixels**, i + sense parentesis torna a 0/255. Es el mateix principi que el soroll de precisio: + **l'artefacte es el shader**. En portar, expandir el macro a ma amb la raresa inclosa i + dir en un comentari que es a proposit, o el seguent que ho llegeixi ho «arregla» un + altre cop. +- **La captura PNG de glslViewer surt capgirada en vertical.** La fila 0 del fitxer es + `gl_FragCoord.y = 0`, o sia la part de BAIX del quadre. Comprovat amb una sonda sis + vegades seguides: sempre igual. Importa perque convida a «arreglar» una orientacio que + no esta trencada: si el paisatge surt del reves a la captura pero els comptes diuen que + el terra va a la meitat de baix, la captura es la que menteix. Per mirar-la, capgirar-la; + per comparar dues captures entre si, tant es, perque totes dues surten capgirades. - **Sense multipassada.** Un shader de Shadertoy amb `Buffer A` no es porta: es redissenya la idea aprofitant el `detour` de r_e_c_u_r, o es descarta. Digues-ho aviat, abans de gastar una hora traduint una cosa que no pot funcionar. diff --git a/check_port.py b/check_port.py index 2ae20a1..17b6529 100755 --- a/check_port.py +++ b/check_port.py @@ -15,6 +15,7 @@ Exit codes: 0 nothing fatal, 1 something BREAKS, 2 usage error. Part of . GPL-3.0. """ +import math import re import sys @@ -315,27 +316,98 @@ def _bloque(code, i): return code[j + 1:k - 1], k +RE_FOR = re.compile(r'\bfor\s*\(') + + +def _parentesis(code, i): + """Devuelve (interior, fin) del ( ... ) que empieza en el primer ( desde i.""" + j = code.find('(', i) + if j < 0: + return '', len(code) + nivel, k = 1, j + 1 + while k < len(code) and nivel: + if code[k] == '(': + nivel += 1 + elif code[k] == ')': + nivel -= 1 + k += 1 + return code[j + 1:k - 1], k + + +def _vueltas(cabecera): + """Vueltas de un bucle a partir de su cabecera "arranque; condicion; paso". + + Contar solo el limite superior era un error caro: `for(float x=-2.; x<3.; x++)` + da CINCO vueltas, no tres, y `for(float y=0.; y>-4.; y--)` da cuatro sin que el + limite sea siquiera positivo. Un bucle anidado de esa forma --que es la que usan + los shaders de pinceladas-- se subestimaba por un factor de 6, o desaparecia + entero de la cuenta si el limite era negativo, porque la expresion no encajaba. + """ + partes = cabecera.split(';') + if len(partes) != 3: + return None + ini_txt, cond_txt, paso_txt = partes + + m = re.search(r'=\s*([+-]?[\d.]+)\s*$', ini_txt.strip()) + # Si el arranque no se lee (declarado fuera del for), se supone 0, que es lo que + # se hacia antes: mejor la estimacion vieja que ninguna. + ini = float(m.group(1)) if m else 0.0 + + m = re.search(r'([<>]=?)(.*)$', cond_txt) + if not m: + return None + op = m.group(1) + # El limite puede ser una cuenta constante, no un numero suelto: el bucle de + # antialiasing se escribe `i < AA*AA - 0.5`, y leer de ahi solo el primer numero + # daba 2 vueltas donde hay 4. Se evalua si es aritmetica pura (los #define ya se + # han sustituido antes de llegar aqui). + expr = m.group(2).strip() + if not expr or not re.fullmatch(r'[\d.+\-*/() ]+', expr): + return None + try: + fin = float(eval(expr, {'__builtins__': {}}, {})) + except (SyntaxError, ZeroDivisionError, TypeError, NameError, ValueError): + return None + + m = re.search(r'[+-]=\s*([\d.]+)', paso_txt) + paso = abs(float(m.group(1))) if m else 1.0 + if paso == 0: + return None + + recorrido = (fin - ini) if op.startswith('<') else (ini - fin) + n = math.ceil(recorrido / paso) + (1 if op.endswith('=') else 0) + return int(n) if n >= 1 else None + + def _coste_propio(cuerpo, cuerpos, profundidad=0): """Operaciones matematicas de un cuerpo, expandiendo funciones propias y bucles. Los bucles se sacan aparte y se cuentan multiplicados por sus vueltas: si no, el cuerpo del bucle --que en un raymarch es TODO el coste-- se cuenta una sola vez y el shader parece barato. + + Se recorren en orden saltando por encima del cuerpo de cada uno, para que un + bucle anidado se cuente UNA vez (dentro de la recursion del de fuera) y no dos. """ if profundidad > 6: return 0 total = 0 resto = cuerpo - for m in list(re.finditer(r'\bfor\s*\([^;]*;[^<>]*[<>]=?\s*([\d.]+)\s*;', cuerpo)): - interior, fin = _bloque(cuerpo, m.end()) - try: - vueltas = int(float(m.group(1))) - except ValueError: - continue - if 1 <= vueltas <= 4096: + pos = 0 + while True: + m = RE_FOR.search(cuerpo, pos) + if not m: + break + cabecera, fin_cab = _parentesis(cuerpo, m.end() - 1) + vueltas = _vueltas(cabecera) + interior, fin = _bloque(cuerpo, fin_cab) + if vueltas is not None and 1 <= vueltas <= 4096: total += vueltas * _coste_propio(interior, cuerpos, profundidad + 1) resto = resto.replace(cuerpo[m.start():fin], ' ') + pos = fin # saltar el cuerpo: los de dentro ya van en la recursion + else: + pos = m.end() total += len(re.findall(MATEMATICAS, resto)) for nom, c in cuerpos.items(): diff --git a/docs/conversion-guide.ca.md b/docs/conversion-guide.ca.md index c6041cd..ff5d38c 100644 --- a/docs/conversion-guide.ca.md +++ b/docs/conversion-guide.ca.md @@ -239,6 +239,38 @@ artefacte.** `check_port.py` avisa de qualsevol literal per sobre de 65504 justament per aixo. +### Un macro amb el parametre sense parentesis NO s'«arregla» + +Mesurat el 2026-09-05 portant el shader de Noztol. Defineix la seva paleta aixi: + +```glsl +#define p(t, a, b, c, d) ( a + b*cos( 6.28318*(c*t+d) ) ) +``` + +`t` va pelat. Aixo es inofensiu fins que es crida el macro amb una suma, que es el que +fa: `sp(sin(x) + terme)`. En expandir queda `c*sin(x) + terme`, o sia que el segon sumand +**no** va multiplicat per `c`. Si li poses els parentesis «bons» al voltant de `(t)`, la +imatge canvia: el port se'n va separar **36/255 en el 72% dels pixels**, i va tornar a +0/255 tan bon punt es van treure. + +Mateix principi que el soroll de precisio a proposit: **l'artefacte es el shader.** En +portar, expandir el macro a ma amb la raresa inclosa i dir en un comentari que es volent, +o el seguent que ho llegeixi ho «arregla» un altre cop. + +Es tambe la rao per expandir el macro: el de dalt es diu `p`, i l'original te tres +funcions el parametre de les quals tambe es diu `p`. No va arribar a disparar mai, perque +un macro amb parametres nomes s'expandeix davant d'un `(`, pero es un cep parat. + +### La captura PNG de glslViewer surt capgirada en vertical + +La fila 0 del fitxer es `gl_FragCoord.y = 0`, o sia la part de BAIX del quadre. Comprovat +amb una sonda sis vegades seguides: sempre igual. + +Importa perque convida a «arreglar» una orientacio que no esta trencada. Si un paisatge +surt del reves a la captura pero els comptes diuen que el terra va a la meitat de baix, +la captura es la que menteix. Per mirar-la, capgirar-la; per comparar dues captures entre +si tant es, perque totes dues surten capgirades. + ### El raymarching no hi cap --- mesurat, no suposat Un shader que marxa raigs no entra en aquest aparell, i la manera de saber-ho no es diff --git a/docs/conversion-guide.en.md b/docs/conversion-guide.en.md index 47b258a..fc66087 100644 --- a/docs/conversion-guide.en.md +++ b/docs/conversion-guide.en.md @@ -240,6 +240,38 @@ its reference disagree, ask whether the reference is showing you an artefact.** `check_port.py` flags any literal above 65504 for this reason. +### A macro whose parameter has no parentheses must NOT be "fixed" + +Measured on 2026-09-05 porting Noztol's shader. It defines its palette as: + +```glsl +#define p(t, a, b, c, d) ( a + b*cos( 6.28318*(c*t+d) ) ) +``` + +`t` is bare. That is harmless until the macro is called with a sum, which it is: +`sp(sin(x) + term)`. The expansion is `c*sin(x) + term`, so the second addend is **not** +multiplied by `c`. Add the "correct" parentheses around `(t)` and the image changes: the +port drifted **36/255 across 72% of the pixels**, and went back to 0/255 once the +parentheses came out again. + +Same principle as deliberate precision noise: **the artifact IS the shader.** When you +port, expand the macro by hand keeping the quirk, and say in a comment that it is +deliberate -- otherwise the next person to read it "fixes" it again. + +This is also why a macro is worth expanding at all: the one above is named `p`, and the +original has three functions whose parameter is also called `p`. It never fired, because +function-like macros only expand before a `(`, but it is a trap lying in the code. + +### The glslViewer PNG capture comes out vertically flipped + +Row 0 of the file is `gl_FragCoord.y = 0`, that is the BOTTOM of the frame. Checked with +a probe six times in a row and also through the preview wrapper: always the same. + +It matters because it invites you to "fix" an orientation that is not broken. If a +landscape looks upside down in the capture but the arithmetic says the ground belongs in +the lower half, the capture is what is lying. Flip it to look at it; for comparing two +captures against each other it makes no difference, since both come out flipped. + ### Raymarching does not fit — measured, not guessed A shader that marches rays does not belong on this device, and the way to find out is