Regenerado: dos trampas nuevas en la skill y en las guias

- Un macro con el parametro sin parentesis no se puede "arreglar" al portar.
  Medido con el shader de Noztol: poniendole los parentesis "bien" el port se
  separaba 36/255 en el 72% de los pixeles; sin ellos vuelve a 0/255.
- Las capturas PNG de glslViewer salen volteadas en vertical: la fila 0 es
  gl_FragCoord.y = 0. Invita a "arreglar" una orientacion que no esta rota.

Y el arreglo del estimador de coste de check_port.py: los bucles float que
cuentan hacia atras o arrancan en negativo se contaban mal o desaparecian de
la cuenta, y los anidados se contaban dos veces.

Generado con generar-agente-portable.py. No editar aqui.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Xose
2026-09-05 17:22:45 +02:00
parent 593abbb06d
commit b1f6755663
4 changed files with 158 additions and 7 deletions
+15
View File
@@ -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.
+79 -7
View File
@@ -15,6 +15,7 @@ Exit codes: 0 nothing fatal, 1 something BREAKS, 2 usage error.
Part of <shadertoy-to-recur>. 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():
+32
View File
@@ -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
+32
View File
@@ -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