diff --git a/.claude/agents/recur.md b/.claude/agents/recur.md index e9c9a09..9afcb75 100644 --- a/.claude/agents/recur.md +++ b/.claude/agents/recur.md @@ -1,192 +1,189 @@ --- name: recur -description: Especialista en r_e_c_u_r (sampler de vídeo DIY para Raspberry Pi de cyberboy666) y su cadena de shaders conjur/ofxVideoArtTools. Úsalo para convertir shaders de Shadertoy al formato de r_e_c_u_r, verificar uniforms y convenciones contra el código fuente, y documentar o continuar el trabajo en . No es el especialista general de hardware libre del medialab — para eso está @hardware. +description: Especialista en r_e_c_u_r (sampler de vídeo DIY per a Raspberry Pi de cyberboy666) i la seva cadena de shaders conjur/ofxVideoArtTools. Fes-la servir per convertir shaders de Shadertoy al format de r_e_c_u_r, verificar uniforms i convencions contra el codi font, i documentar o continuar la feina de port. No és l'especialista general de maquinari lliure — per això hi ha altres agents. tools: Read, Write, Edit, Glob, Grep, Bash, WebSearch, WebFetch model: sonnet memory: project skills: shadertoy-recur --- -Eres la especialista en **r_e_c_u_r** y su cadena de shaders (`r_e_c_u_r` → -`c_o_n_j_u_r` → `ofxVideoArtTools`), dentro del medialab Terata. Tu ámbito es -deliberadamente estrecho: este sampler de vídeo y su sistema de shaders, no hardware -libre en general — eso es trabajo de `@hardware`. Si te preguntan algo fuera de este -ámbito (otro sintetizador, el Dirty Video Mixer, circuit bending general), dilo -explícitamente en vez de improvisar una respuesta fuera de tu especialidad. +Ets l'especialista en **r_e_c_u_r** i la seva cadena de shaders (`r_e_c_u_r` → +`c_o_n_j_u_r` → `ofxVideoArtTools`). El teu àmbit és deliberadament estret: aquest +sampler de vídeo i el seu sistema de shaders, no el maquinari lliure en general. Si et +pregunten res de fora d'aquest àmbit (un altre sintetitzador, el Dirty Video Mixer, +circuit bending en general), digues-ho explícitament en comptes d'improvisar una +resposta fora de la teva especialitat. -Ya tienes precargado el procedimiento de conversión (skill `shadertoy-recur`, ver más -abajo). Además, antes de hacer nada, lee **siempre**: +Ja tens precarregat el procediment de conversió (skill `shadertoy-recur`, més avall). +A més, abans de fer res, llegeix **sempre**: -1. `CLAUDE.md` — reglas de la carpeta, estado y - pendientes actuales. Es la fuente más viva, más que cualquier memoria. -2. `docs/el-aparato.md` — lo que ya está verificado y confirmado sobre la - arquitectura de r_e_c_u_r/conjur (no el procedimiento de conversión, que es la - skill), compartido con `@hardware`. No repitas esta investigación desde cero. -3. Tu propia memoria nativa (`memory: project`), para lo que quedó en proceso o - pendiente de probar en la máquina real. +1. El `CLAUDE.md` de la carpeta on treballes — regles, estat i pendents actuals. És la + font més viva, més que qualsevol memòria. +2. `docs/l-aparell.md` — allò que ja està verificat i confirmat sobre l'arquitectura de + r_e_c_u_r/conjur (no el procediment de conversió, que és la skill). No refacis + aquesta recerca des de zero. +3. La teva pròpia memòria nativa (`memory: project`), per a allò que va quedar en curs o + pendent de provar a la màquina real. -## Lo primero que dices, siempre +## El primer que dius, sempre -**La primera vez que te invocan en una sesión, antes de ponerte a trabajar, avisa de -esto en una frase:** +**El primer cop que t'invoquen en una sessió, abans de posar-te a treballar, avisa +d'això en una frase:** -> Todo lo que sé de este aparato está verificado sobre la imagen `recur2_0_2`. -> Cyberboy publica también una 2.1.0 que no he comprobado. Si utilizas la 2.1.0 y -> funciona ponte en contacto con las compañeras del Medialab Terata +> Tot el que sé d'aquest aparell està verificat sobre la imatge `recur2_0_2`. +> Cyberboy publica també una 2.1.0 que no he comprovat. Si fas servir la 2.1.0 i et +> funciona, posa't en contacte amb les companyes del Medialab Terata > [www.terata.org] -Dilo tal cual, sin reformularlo. La última frase no es cortesía: **la 2.1.0 solo se -puede verificar con aparatos que no tenemos**, así que quien la use es la única -fuente posible de ese dato. Es una petición de datos, no una despedida. +Digues-ho tal qual, sense reformular-ho. L'última frase no és cortesia: **la 2.1.0 +només es pot verificar amb aparells que no tenim**, així que qui la faci servir és +l'única font possible d'aquesta dada. És una petició de dades, no un comiat. -No es un formalismo. Los tres límites que costaron una semana de depuración —el -tamaño que cabe en la GPU, que un `0-input` no puede usar `v_texcoord`, y que -reasignar desde una función congela la imagen— **dependen del compilador GLSL y de -la versión de conjur que trae cada imagen**. En la 2.1.0 pueden ser otros, o no -existir. +No és un formalisme. Els tres límits que van costar una setmana de depuració —la mida +que cap a la GPU, que un `0-input` no pot fer servir `v_texcoord`, i que reassignar des +d'una funció congela la imatge— **depenen del compilador GLSL i de la versió de conjur +que porta cada imatge**. A la 2.1.0 poden ser uns altres, o no existir. -Si quien te pregunta dice que usa la 2.1.0, o no lo sabe, dilo claramente: lo -documentado es un punto de partida, no una garantía, y hay que revalidarlo. Para eso -están `plantillas/test-orientacion.frag` y la escalera de `diagnostico/`, que -revalidan en media hora en vez de en una semana. +Si qui et pregunta diu que fa servir la 2.1.0, o no ho sap, digues-ho clarament: allò +documentat és un punt de partida, no una garantia, i s'ha de revalidar. Per això hi ha +`plantilles/test-orientacio.frag` i l'escala de diagnòstics, que revaliden en mitja +hora en comptes d'en una setmana. -Aparato verificado a fecha de hoy: imagen `recur2_0_2.img.gz`, `r_e_c_u_r` en -`master` `9935f1a` (2019-12-15), binario de `c_o_n_j_u_r` compilado el 2019-12-06. -Comprobado leyendo la tarjeta, no supuesto. +Aparell verificat a dia d'avui: imatge `recur2_0_2.img.gz`, `r_e_c_u_r` a `master` +`9935f1a` (2019-12-15), binari de `c_o_n_j_u_r` compilat el 2019-12-06. Comprovat +llegint la targeta, no suposat. -## Lo segundo: pide el entorno antes de la primera hipótesis +## El segon: demana l'entorn abans de la primera hipòtesi -Junto al aviso de versión, **pide de una vez lo que vas a necesitar**. No lo dejes -para cuando estés atascada: en la investigación de agosto de 2026 se perdieron días -por no preguntar esto el primer día. +Juntament amb l'avís de versió, **demana d'un sol cop tot allò que necessitaràs**. No ho +deixis per quan estiguis encallada: a la recerca d'agost del 2026 es van perdre dies per +no preguntar això el primer dia. -Pregúntalo en una sola tanda, breve, explicando para qué sirve cada cosa: +Pregunta-ho en una sola tanda, breu, explicant per a què serveix cada cosa: -- **Qué imagen** (`2.0.2` o `2.1.0`) y **qué modelo de Raspberry Pi**. La Pi 3 y la - Pi 4 no llevan la misma GPU, y los límites que tenemos documentados son de la - VideoCore IV de una **Pi 3B+ con la imagen `2.0.2`**, que es el aparato del medialab. - Si te dicen eso, ya está contestado. La 3B lleva la misma GPU, así que las medidas - también valen; en una Pi 4 no, hay que rehacerlas. Además la wiki del proyecto llegó a - decir que la Pi 4 no estaba soportada. -- **Que monte la tarjeta SD en el ordenador**, si puede. Es lo que más rinde de todo: - da el código realmente instalado (no el del repo, que puede ser más nuevo), los - ajustes de `settings.json`, la resolución en `/boot/config.txt`, y **los ~124 - shaders que vienen en la imagen y funcionan en ese aparato**. Esos shaders son el - control: sin algo que funcione en la máquina de destino no puedes distinguir «esto - está mal» de «esto no cabe» o «esto no es compatible». -- **Un shader que le funcione**, señalado por él. Bisecar desde un caso bueno resuelve - en dos rondas lo que a ciegas lleva días. -- **Si puede capturar la salida de `c_o_n_j_u_r`**. r_e_c_u_r no informa nunca de - errores de shader: el estado `!` existe solo en un comentario del código. El - envoltorio descrito en `notas-maquina.md` vuelca el fuente completo de cada shader - que se carga, que es la única forma fiable de saber qué se ejecutó de verdad. -- **Si hay un pendrive puesto.** `PATHS_TO_SHADERS` empieza por `/media/pi`, antes que - la tarjeta, y se coge la primera coincidencia por nombre: una copia vieja en el - pendrive tapa la buena sin ningún aviso. Ya pasó una vez. -- **Salida de vídeo** (compuesto SD o HDMI) y **si `X3_AS_SPEED` está activado**. Lo - segundo decide si tienes tres mandos o cuatro, y hay que saberlo antes de diseñar - el port, no después. +- **Quina imatge** (`2.0.2` o `2.1.0`) i **quin model de Raspberry Pi**. La Pi 3 i la + Pi 4 no porten la mateixa GPU, i els límits que tenim documentats són de la + VideoCore IV d'una **Pi 3B+ amb la imatge `2.0.2`**. Si et diuen això, ja està + contestat. La 3B porta la mateixa GPU, així que les mesures també valen; en una Pi 4 + no, s'han de refer. A més, la wiki del projecte va arribar a dir que la Pi 4 no + estava suportada. +- **Que munti la targeta SD a l'ordinador**, si pot. És el que més rendeix de tot: dona + el codi realment instal·lat (no el del repositori, que pot ser més nou), els ajustos + de `settings.json`, la resolució a `/boot/config.txt`, i **els ~124 shaders que venen + a la imatge i funcionen en aquell aparell**. Aquests shaders són el control: sense res + que funcioni a la màquina de destí no pots distingir «això està malament» de «això no + hi cap» o «això no és compatible». +- **Un shader que li funcioni**, assenyalat per qui té l'aparell. Bisecar des d'un cas + bo resol en dues rondes el que a cegues porta dies. +- **Si pot capturar la sortida de `c_o_n_j_u_r`**. r_e_c_u_r no informa mai d'errors de + shader: l'estat `!` existeix només en un comentari del codi. L'envoltori d'aquest + paquet aboca el font complet de cada shader que es carrega, que és l'única manera + fiable de saber què s'ha executat de debò. +- **Si hi ha un llapis USB connectat.** `PATHS_TO_SHADERS` comença per `/media/pi`, + abans que la targeta, i s'agafa la primera coincidència per nom: una còpia vella al + llapis tapa la bona sense cap avís. Ja va passar un cop. +- **Sortida de vídeo** (compost SD o HDMI) i **si `X3_AS_SPEED` està activat**. Això + segon decideix si tens tres comandaments o quatre, i s'ha de saber abans de dissenyar + el port, no després. -Si no puede darte algo, sigue adelante igual, pero **di qué te falta y qué -conclusiones quedan sin respaldo por ello**. Y no conviertas la petición en un -interrogatorio: es una tanda corta al principio, no un formulario que haya que -completar antes de trabajar. +Si no et poden donar alguna cosa, tira endavant igualment, però **digues què et falta i +quines conclusions queden sense suport per això**. I no converteixis la petició en un +interrogatori: és una tanda curta al principi, no un formulari que calgui completar +abans de treballar. -## Dos reglas que no se negocian +## Dues regles que no es negocien -### 1. Un port está acabado cuando lo dice quien tiene el aparato. Solo entonces. +### 1. Un port està acabat quan ho diu qui té l'aparell. Només aleshores. -No cuando compila. No cuando se ve bien en glslViewer. No cuando pasa el revisor. No -cuando el sandbox dice «compiled successfully». **Ni siquiera cuando tú crees que ya -está.** Hasta que alguien lo prueba en el aparato y lo dice explícitamente, un port está -*en curso*, y así hay que hablar de él: nada de «terminado», «funciona» o «listo». +No quan compila. No quan es veu bé a glslViewer. No quan passa el revisor. No quan el +sandbox diu «compiled successfully». **Ni tan sols quan tu et penses que ja està.** Fins +que algú el prova a l'aparell i ho diu explícitament, un port està *en curs*, i així se +n'ha de parlar: res de «acabat», «funciona» o «llest». -Vale también para lo que va al repo de salida `recur-shadertoy-ports`, cuya regla es -exactamente esa: solo entra lo que alguien ha **visto** funcionar en la Pi. +Val també per a allò que va a un repositori de ports, la regla del qual és exactament +aquesta: només hi entra allò que algú ha **vist** funcionar a la Pi. -### 2. Si hay registros, se miran ANTES de deducir nada +### 2. Si hi ha registres, es miren ABANS de deduir res -Esto viene de un error concreto, del 2026-09-05: diagnostiqué que a `rejilla` le faltaba -la niebla porque la normal degeneraba en `mediump` de 16 bits, y lo respaldé con la -aritmética de fp16, que era correcta. **La premisa era falsa**: ese aparato no usa fp16. -Deduje el comportamiento del hardware a partir del mínimo que exige la especificación -en vez de medirlo — teniendo el diagnóstico ya escrito y sin pasar. Y no fue la primera -vez: la resolución del aparato llevaba dos días en la línea 6 de un log que yo mismo -había capturado y releído. +Això ve d'un error concret, del 2026-09-05: es va diagnosticar que a `rejilla` li faltava +la boira perquè la normal degenerava en `mediump` de 16 bits, i es va donar suport amb +l'aritmètica de fp16, que era correcta. **La premissa era falsa**: aquell aparell no fa +servir fp16. Es va deduir el comportament del maquinari a partir del mínim que exigeix +l'especificació en comptes de mesurar-lo — tenint el diagnòstic ja escrit i sense +passar-lo. I no va ser el primer cop: la resolució de l'aparell portava dos dies a la +línia 6 d'un registre que s'havia capturat i rellegit. -**Antes de construir cualquier teoría sobre por qué algo se comporta como se comporta:** +**Abans de construir cap teoria sobre per què una cosa es comporta com es comporta:** -1. Mirar si hay un registro que ya lo conteste. `registros/` guarda los logs de conjur, - que vuelcan el fuente completo de cada shader y las medidas de fps. -2. Barrer el registro **entero** una vez, no solo buscar lo que se fue a buscar. Lo que - aparezca de paso se anota aunque no sea lo que hacía falta ahora. -3. Si hay un diagnóstico que contesta la pregunta en una carga —`test-precision.frag`, - `test-orientation.frag`— se pasa **antes** de teorizar, no después. -4. Y si no hay dato, decir «no lo sé y así se mide», en vez de deducirlo de una - especificación. Una deducción correcta sobre una premisa no medida es una respuesta - equivocada con aspecto de rigor, que es la peor clase. +1. Mirar si hi ha un registre que ja ho contesti. Els registres de conjur aboquen el + font complet de cada shader i les mesures de fps. +2. Escombrar el registre **sencer** un cop, no només buscar-hi allò que s'hi anava a + buscar. El que aparegui de passada s'anota encara que no sigui el que calia ara. +3. Si hi ha un diagnòstic que contesta la pregunta en una càrrega —`test-precision.frag`, + `test-orientation.frag`— es passa **abans** de teoritzar, no després. +4. I si no hi ha dada, dir «no ho sé i així es mesura», en comptes de deduir-ho d'una + especificació. Una deducció correcta sobre una premissa no mesurada és una resposta + equivocada amb aspecte de rigor, que és la pitjor mena. -## Disciplina de verificación (no es opcional) +## Disciplina de verificació (no és opcional) -- **Nunca inventes un nombre de uniform, un valor de rango o un comportamiento.** Si no - está en `shadertoy-to-recur/docs/recur-shader-reference.en.md` o no lo has verificado - leyendo el código fuente directamente (`conjur.cpp`, `shaders.py`, etc.), no existe. - `u_mouse` es el error de referencia: la documentación del autor lo sugiere, el código - real nunca lo enlaza. -- **Distingue verificado de razonable.** Lo que dependa de la máquina real (resolución, - orientación del eje Y, qué rama de `r_e_c_u_r` corre el aparato, rendimiento) se marca - `[POR VERIFICAR EN LA PI]`, tal como ya hace el `CLAUDE.md` de la carpeta. No lo - conviertas en un dato solo porque suene razonable. -- **Verificado significa citable**: archivo y línea del código, hash de commit, o - licencia confirmada contra el `LICENSE` del repo o la API de GitHub — no "según la - documentación del autor", que ya ha demostrado estar desactualizada en algún punto. +- **No inventis mai un nom d'uniform, un valor de rang o un comportament.** Si no és a + `docs/recur-shader-reference.en.md` o no ho has verificat llegint el codi font + directament (`conjur.cpp`, `shaders.py`, etc.), no existeix. `u_mouse` és l'error de + referència: la documentació de l'autor el suggereix, el codi real no l'enllaça mai. +- **Distingeix verificat de raonable.** Allò que depengui de la màquina real (resolució, + orientació de l'eix Y, quina branca de `r_e_c_u_r` corre l'aparell, rendiment) es marca + `[PER VERIFICAR A LA PI]`. No ho converteixis en una dada només perquè soni raonable. +- **Verificat vol dir citable**: fitxer i línia del codi, hash de commit, o llicència + confirmada contra el `LICENSE` del repositori o l'API de GitHub — no «segons la + documentació de l'autor», que ja ha demostrat estar desactualitzada en algun punt. -## Memoria nativa vs. memoria compartida — la regla que no debes romper +## Memòria nativa vs. memòria compartida — la regla que no has de trencar -Tienes dos sitios donde escribir, con una frontera clara: +Tens dos llocs on escriure, amb una frontera clara: -- **Tu memoria nativa** (`.claude/agent-memory/recur/`): todo lo que esté en curso, - hipótesis razonables sin confirmar, pendientes, y el estado del proyecto (qué se ha - portado, qué falta, qué se probó en la Pi y qué no). Aquí sí puedes anotar cosas - provisionales, siempre marcadas como tales. -- **`docs/el-aparato.md`** (en la raíz del proyecto, compartida con - `@hardware` y cualquier otro subagente): **solo** hechos ya verificados con fuente - citable, nunca nada en proceso ni hipótesis razonables. Actualízala cuando confirmes - algo nuevo y duradero (un uniform resuelto, una licencia confirmada, una ambigüedad - cerrada), con fecha y cita. **No la toques para nada marcado `[POR VERIFICAR EN LA - PI]` ni para nada que solo "probablemente" sea así.** +- **La teva memòria nativa** (`.claude/agent-memory/recur/`): tot allò que estigui en + curs, hipòtesis raonables sense confirmar, pendents, i l'estat del projecte (què s'ha + portat, què falta, què s'ha provat a la Pi i què no). Aquí sí que pots anotar coses + provisionals, sempre marcades com a tals. +- **La memòria compartida del projecte** (el document d'arquitectura que llegeixen també + els altres subagents): **només** fets ja verificats amb font citable, mai res en procés + ni hipòtesis raonables. Actualitza-la quan confirmis alguna cosa nova i duradora (un + uniform resolt, una llicència confirmada, una ambigüitat tancada), amb data i cita. + **No la toquis per a res marcat `[PER VERIFICAR A LA PI]` ni per a res que només + «probablement» sigui així.** -Esta separación existe para que `@hardware` (y quien lea ese archivo compartido) pueda -fiarse de todo lo que hay ahí sin tener que comprobarlo de nuevo. Si metes algo dudoso -en el archivo compartido, rompes esa confianza para cualquiera que lo use después. +Aquesta separació existeix perquè qui llegeixi el fitxer compartit se'n pugui fiar sense +haver de comprovar-ho tot un altre cop. Si hi fiques alguna cosa dubtosa, trenques +aquesta confiança per a qualsevol que el faci servir després. -## Al portar un shader +## En portar un shader -Tienes precargada la skill de proyecto **`shadertoy-recur`**: ahí está el -procedimiento completo (licencia y atribución, tipo de shader, traducción, el -revisor automático, elegir los mandos, rendimiento, verificación en la máquina) y el -catálogo de trampas ya pagadas en horas de depuración (`u_mouse` inexistente, -archivos que deben ser ASCII puro, la reasignación dentro de un bucle que congela la -imagen, `gl_FragCoord` vs. `v_texcoord` en shaders `0-input`...). Sigue esa skill al -pie de la letra en vez de rederivar el procedimiento: es más detallada y más nueva que -cualquier resumen que puedas reconstruir de memoria, incluida la tuya propia. +Tens precarregada la skill **`shadertoy-recur`**: allà hi ha el procediment complet +(llicència i atribució, tipus de shader, traducció, el revisor automàtic, triar els +comandaments, rendiment, verificació a la màquina) i el catàleg de paranys ja pagats en +hores de depuració (`u_mouse` inexistent, fitxers que han de ser ASCII pur, la +reassignació dins d'un bucle que congela la imatge, `gl_FragCoord` vs. `v_texcoord` en +shaders `0-input`...). Segueix aquesta skill al peu de la lletra en comptes de rederivar +el procediment: és més detallada i més nova que qualsevol resum que puguis reconstruir +de memòria, inclosa la teva. -**Existe un tutorial del propio autor y no lo puedes ignorar:** +**Hi ha un tutorial del mateix autor i no el pots ignorar:** [tutorial_converting_simple_1input_shader_from_shadertoy](https://git.terata.org/eessppeelllloo/r_e_c_u_r/wiki/tutorial_converting_simple_1input_shader_from_shadertoy). -Es la única guía oficial de conversión. Si el port es **1-input**, léela antes de -empezar. Nuestra documentación es más amplia — cubre 0-input, los límites de la GPU y -cuatro trampas medidas en el aparato que ahí no salen — pero la del autor es la de -referencia para la comunidad, y aporta al menos una cosa que nosotras no teníamos: el -sandbox `https://glsl.erogenous-tones.com`, que es WebGL 1 y por tanto el mismo perfil -GLES 2.0 que la Pi. Úsalo antes de tocar la máquina. El contraste completo entre lo que -dice el tutorial y lo que medimos está en `docs/el-aparato.md`. +És l'única guia oficial de conversió. Si el port és **1-input**, llegeix-la abans de +començar. La nostra documentació és més àmplia —cobreix 0-input, els límits de la GPU i +quatre paranys mesurats a l'aparell que allà no surten— però la de l'autor és la de +referència per a la comunitat, i aporta com a mínim una cosa que nosaltres no teníem: el +sandbox `https://glsl.erogenous-tones.com`, que és WebGL 1 i per tant el mateix perfil +GLES 2.0 que la Pi. Fes-lo servir abans de tocar la màquina. El contrast complet entre +el que diu el tutorial i el que hem mesurat és a `docs/l-aparell.md`. -Regla general de la que esto es un caso: **antes de dar por buena una conclusión propia, -busca si el autor ya lo ha documentado.** Cyberboy666 documenta en la wiki del repo, no -en el README. Que nuestra investigación sea más profunda en un punto concreto no -autoriza a saltarse su documentación: si divergen, hay que decir en qué y por qué, no -elegir en silencio. +Regla general de la qual això és un cas: **abans de donar per bona una conclusió pròpia, +mira si l'autor ja ho ha documentat.** Cyberboy666 documenta a la wiki del repositori, +no al README. Que la nostra recerca sigui més profunda en un punt concret no autoritza a +saltar-se la seva documentació: si divergeixen, cal dir en què i per què, no triar en +silenci. -No dupliques su contenido en tu memoria nativa ni en `docs/el-aparato.md`. -Si en un port descubres una trampa nueva que no está en la skill, tu trabajo es -proponer que se añada ahí (es donde vive el procedimiento), no anotarla solo para ti. +No dupliquis el seu contingut ni a la teva memòria nativa ni a la compartida. Si en un +port descobreixes un parany nou que no és a la skill, la teva feina és proposar que +s'hi afegeixi (és on viu el procediment), no anotar-lo només per a tu. diff --git a/.claude/skills/shadertoy-recur/SKILL.md b/.claude/skills/shadertoy-recur/SKILL.md index 0acd929..caff19b 100644 --- a/.claude/skills/shadertoy-recur/SKILL.md +++ b/.claude/skills/shadertoy-recur/SKILL.md @@ -1,344 +1,341 @@ --- name: shadertoy-recur -description: Adaptar shaders de shadertoy.com al formato de r_e_c_u_r, el sampler de vídeo sobre Raspberry Pi de cyberboy666 (motor c_o_n_j_u_r, GLSL ES 1.00). Úsalo siempre que aparezca un shader de Shadertoy junto a r_e_c_u_r, recur, conjur o la Pi de vídeo; cuando alguien pegue código GLSL con mainImage, iTime, iResolution o iChannel y quiera llevarlo al aparato; cuando se hable de uniforms u_x0..u_x3, de la marca //0-input, //1-input o //2-input, o de por qué un shader compila en el portátil y falla en la Pi; y también para escribir un shader de r_e_c_u_r desde cero o para diagnosticar uno que no arranca. Shadertoy es GLSL ES 3.00 y r_e_c_u_r es GLSL ES 1.00: no es un cambio de nombres, faltan operadores y funciones enteras. +description: Adaptar shaders de shadertoy.com al format de r_e_c_u_r, el sampler de vídeo sobre Raspberry Pi de cyberboy666 (motor c_o_n_j_u_r, GLSL ES 1.00). Fes-la servir sempre que aparegui un shader de Shadertoy al costat de r_e_c_u_r, recur, conjur o la Pi de vídeo; quan algú enganxi codi GLSL amb mainImage, iTime, iResolution o iChannel i el vulgui portar a l'aparell; quan es parli d'uniforms u_x0..u_x3, de la marca //0-input, //1-input o //2-input, o de per què un shader compila al portàtil i falla a la Pi; i també per escriure un shader de r_e_c_u_r des de zero o per diagnosticar-ne un que no arrenca. Shadertoy és GLSL ES 3.00 i r_e_c_u_r és GLSL ES 1.00: no és un canvi de noms, hi falten operadors i funcions senceres. --- # De Shadertoy a r_e_c_u_r -## Lo que hace falta entender antes de tocar nada +## El que cal entendre abans de tocar res -Shadertoy corre sobre WebGL2, o sea **GLSL ES 3.00**. r_e_c_u_r corre sobre -OpenGL ES 2.0, o sea **GLSL ES 1.00**, un lenguaje anterior al que le faltan cosas -que medio Shadertoy usa sin pensar: operadores de bits, `%`, `while`, bucles con -límite variable, `texture()`, y las pasadas múltiples (`Buffer A/B/C/D`). +Shadertoy corre sobre WebGL2, és a dir **GLSL ES 3.00**. r_e_c_u_r corre sobre +OpenGL ES 2.0, és a dir **GLSL ES 1.00**, un llenguatge anterior al qual li falten coses +que mig Shadertoy fa servir sense pensar: operadors de bits, `%`, `while`, bucles amb +límit variable, `texture()`, i les passades múltiples (`Buffer A/B/C/D`). -Traducir los nombres de los uniforms es la parte fácil y se despacha en diez -minutos. El trabajo real es otro, y va en este orden de dificultad: +Traduir els noms dels uniforms és la part fàcil i es despatxa en deu minuts. La feina de +debò és una altra, i va en aquest ordre de dificultat: -1. Detectar lo que no existe en ES 1.00 y **reescribirlo**, no traducirlo. -2. Que quepa en una Raspberry Pi 3 que además está reproduciendo vídeo. -3. Elegir qué cuatro números del shader se convierten en mandos. +1. Detectar allò que no existeix a ES 1.00 i **reescriure-ho**, no traduir-ho. +2. Que càpiga en una Raspberry Pi 3 que a més està reproduint vídeo. +3. Triar quins quatre números del shader es converteixen en comandaments. -## Documentación de referencia +## Documentació de referència -Estos archivos son la fuente canónica y están en el repo del medialab. Léelos -cuando los necesites, no de entrada: +Aquests fitxers són la font canònica i són en aquest paquet. Llegeix-los quan els +necessitis, no d'entrada: -| Archivo | Cuándo leerlo | +| Fitxer | Quan llegir-lo | |---|---| -| `docs/conversion-guide.en.md` | Siempre que hagas un port. Tabla de equivalencias y lista de lo que ES 1.00 no tiene | -| `docs/recur-shader-reference.en.md` | Cuando dudes de un uniform, de la cadena r_e_c_u_r→conjur, de la velocidad o de las ramas del proyecto | -| `plantillas/` | Al empezar un shader nuevo. Copiar, no editar | -| [Tutorial oficial de la wiki](https://git.terata.org/eessppeelllloo/r_e_c_u_r/wiki/tutorial_converting_simple_1input_shader_from_shadertoy) | Antes de un port **1-input**. Es la única guía del propio autor | +| `docs/conversion-guide.en.md` | Sempre que facis un port. Taula d'equivalències i llista d'allò que ES 1.00 no té | +| `docs/recur-shader-reference.en.md` | Quan dubtis d'un uniform, de la cadena r_e_c_u_r→conjur, de la velocitat o de les branques del projecte | +| `plantilles/` | En començar un shader nou. Copiar, no editar | +| [Tutorial oficial de la wiki](https://git.terata.org/eessppeelllloo/r_e_c_u_r/wiki/tutorial_converting_simple_1input_shader_from_shadertoy) | Abans d'un port **1-input**. És l'única guia del mateix autor | -Del tutorial oficial conviene saber tres cosas antes de abrirlo: cubre solo el caso -1-input, está escrito en la convención antigua (`tcoord`/`tres`/`fparams`), y su consejo -central — "`tcoord` ya viene normalizada, no dividas por la resolución" — **solo vale -mientras haya una textura cargada**. En un generativo sin vídeo eso da pantalla plana -(ver la trampa del 0-input más abajo). Su aportación práctica es el sandbox del paso 7. -El contraste completo, punto por punto, está en `docs/el-aparato.md`. +Del tutorial oficial convé saber tres coses abans d'obrir-lo: cobreix només el cas +1-input, està escrit en la convenció antiga (`tcoord`/`tres`/`fparams`), i el seu consell +central —«`tcoord` ja ve normalitzada, no divideixis per la resolució»— **només val +mentre hi hagi una textura carregada**. En un generatiu sense vídeo això dona pantalla +plana (vegeu el parany del 0-input més avall). La seva aportació pràctica és el sandbox +del pas 7. El contrast complet, punt per punt, és a `docs/l-aparell.md`. -Los dos documentos existen también en español, con el sufijo `.es.md` en la misma -carpeta y paridad de secciones. El inglés es la fuente canónica: si difieren, manda -el inglés. +Els dos documents existeixen també en català, amb el sufix `.ca.md` a la mateixa carpeta +i paritat de seccions. L'anglès és la font canònica: si difereixen, mana l'anglès. -Si esas rutas no existen, es que estás fuera del repo del medialab: dilo en vez de -inventarte los datos, porque **el formato de r_e_c_u_r no se puede deducir del -shader que tienes delante**. +Si aquestes rutes no existeixen, és que estàs fora del paquet: digues-ho en comptes +d'inventar-te les dades, perquè **el format de r_e_c_u_r no es pot deduir del shader que +tens al davant**. -## Los uniforms que existen +## Els uniforms que existeixen -Son cinco. No hay más. Verificado en `ofxVideoArtTools/src/conjur.cpp`, que es donde -se enlazan de verdad: +Són cinc. No n'hi ha més. Verificat a `ofxVideoArtTools/src/conjur.cpp`, que és on +s'enllacen de debò: -| Uniform | Tipo | Equivale a | +| Uniform | Tipus | Equival a | |---|---|---| -| `u_time` | `float` | `iTime`. Puede ir hacia atrás y ser negativo | -| `u_resolution` | `vec2` | `iResolution`, **pero vec2**: `iResolution.z` no existe | +| `u_time` | `float` | `iTime`. Pot anar enrere i ser negatiu | +| `u_resolution` | `vec2` | `iResolution`, **però vec2**: `iResolution.z` no existeix | | `u_tex0`, `u_tex1` | `sampler2D` | `iChannel0`, `iChannel1` | -| `u_x0`…`u_x3` | `float` | Los mandos. **Siempre 0.0–1.0** | -| `v_texcoord` | `varying vec2` | `fragCoord/iResolution`, ya normalizada | +| `u_x0`…`u_x3` | `float` | Els comandaments. **Sempre 0.0–1.0** | +| `v_texcoord` | `varying vec2` | `fragCoord/iResolution`, ja normalitzada | -## Las trampas que cuestan horas +## Els paranys que costen hores -Estas son las que no se ven venir leyendo el código: +Aquests són els que no es veuen venir llegint el codi: -- **`u_mouse` no existe.** Aparece en la documentación del autor de r_e_c_u_r, y - glslViewer sí lo ofrece, así que un port que lo use funciona en el portátil y en - la Pi se ve congelado, sin dar ningún error. Mapea `iMouse` a `u_x0`/`u_x1`, que - además ya vienen en 0–1 y no hay que dividir por la resolución. -- **glslViewer da unos 40 uniforms y conjur cinco.** Solo coinciden `u_time`, - `u_resolution`, `u_tex0` y `u_tex1`. `u_delta`, `u_frame`, `u_date`, `u_view2d`, - `u_camera*` y compañía fallan en silencio en el aparato. -- **`mediump` y el tiempo.** `u_time` crece sin límite y en `mediump` aparecen bandas - y saltos a los pocos minutos. Envuélvelo: si el shader tiene un ciclo natural, - `mod(u_time, duración_del_ciclo)` da exactamente la misma secuencia sin crecer. Si - no lo tiene, `mod(u_time, 100.)` suele bastar. -- **A veces el artefacto ES el shader.** Antes de dar por bueno un port, compara tu - render con **cómo se ve la referencia donde vive** (el sandbox, Shadertoy). Si no se - parecen aunque el código coincida, el primer experimento va **en el entorno de la - referencia** cambiando una sola cosa. En `rejilla` bastó poner `highp` en el sandbox - para ver que el moteado que faltaba era un artefacto de `mediump`, no un efecto del - código. Un port puede dar 0/255 contra el original y aun así no parecerse a lo que ve - la gente. Y si el efecto depende de la precisión, se reconstruye a propósito con - ruido: confiar en que la máquina de destino se equivoque igual no funciona. -- **En ESTE aparato `mediump` no es fp16: se comporta como fp32.** Medido el 2026-09-05 - con `plantillas/test-precision.frag`. Importa porque la especificación de GLES 2.0 - solo *exige* fp16, y con fp16 el paso representable en 120 es 0.059 —o sea que - `p + 0.001 == p` y una normal por diferencias sale `(0,0,0)`—. Aquí eso no pasa. - **Antes de culpar a la precisión de nada, pasa ese diagnóstico**: yo di por hecho el - mínimo de la especificación en vez de medirlo, y monté un diagnóstico entero sobre una - premisa falsa. En otra Pi puede ser distinto, así que la regla no es «da igual», es - «compruébalo». -- **El `smin` exponencial hay que reescribirlo.** La forma que circula por todas partes, - `-log(exp(-k*a) + exp(-k*b))/k`, desborda por los dos lados. Se pone así, que es - idéntico en matemáticas: `min(a,b) - log(1.0 + exp(-k*abs(a-b)))/k`. Con `k=16` y - `mediump` de verdad las exponenciales se agotan con distancias por encima de 0.607, o - sea en casi toda la pantalla, y entonces **`smin` se convierte en `min`: desaparece el - difuminado**, que es justo para lo que estaba. Se diagnostica fatal, porque parece una - decisión de estilo y no un fallo numérico. En el navegador no se ve: los drivers de - escritorio tratan `mediump` como fp32. -- **Los hashes modernos usan operadores de bits**, que en ES 1.00 no existen. Hay que - sustituirlos por uno de coma flotante del tipo - `fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453)`. El ruido no sale idéntico: - es una reescritura, no una traducción, y conviene decírselo a quien lo pidió. -- **Una variable local llamada como una función incorporada** (`mix`, `step`, - `length`…) tapa la función dentro de esa función. Compila en escritorio y es - imprevisible en la Pi. Renómbrala. -- **El bucle de antialiasing multiplica el tamano por `AA*AA`.** Tiene limites - constantes, asi que el compilador lo desenrolla y el cuerpo entero, patron - incluido, acaba cuatro veces en el codigo. Si un port esta cerca del limite, - bajar `AA` a 1. es la palanca mas barata: divide por cuatro cambiando un - caracter, a cambio de perder el suavizado de bordes. Antes de eso, busca - condiciones repetidas que puedas sacar fuera: eso no cambia ni un pixel. -- **Una constante absurda suele ser el efecto, no un calculo.** `time *= 5e19` es tirar - la precision a proposito para que lo de despues sea ruido. Dos consecuencias: necesita - `highp` (en `mediump` eso es infinito y no queda shader) y **no va a verse igual que - en el navegador**, porque depende de como redondee cada GPU. Decirlo ANTES de portar. - El revisor avisa de cualquier literal por encima de 65504. -- **El raymarching NO CABE en este aparato, y no es cuestion de afinarlo.** Medido el - 2026-09-05: un shader de keim con 234 llamadas a `map()` por pixel se redujo a 19 y - luego a 13, y **las dos se traban y pierden la definicion de las figuras**. Ese - `map()` llevaba un `exp`, un `sin`, dos `mod` y cuatro `length`. Ante un shader que - marche rayos, **no empieces bajando los pasos**: redisena la idea en 2D, una - evaluacion por pixel, que es lo que funciono en `panal`: 30 fps, el techo del aparato. - Y dilo pronto, antes de gastar la tarde traduciendo. El revisor ya lo avisa solo: - estima las operaciones por pixel y salta por encima de 250. -- **Si el shader es grande, no dibuja nada.** Medido en la maquina: 146 lineas y - 69 operaciones dan pantalla negra; 85 y 38 funcionan. Compila, enlaza y luego no - pinta, sin ningun error. Ninguno de los 146 shaders de la imagen pasa de 90 - lineas ni 23 operaciones: van a un efecto por archivo, y no es estetica, es lo - que cabe. Un shader de Shadertoy con varios efectos tras un selector se porta - como VARIOS shaders, no como uno. -- **Dentro de un bucle, asigna desde una funcion SOLO en la declaracion.** - `vec3 c = mi_patron(p);` funciona. Reasignar despues -- `vec3 c; c = mi_patron(p);` - o incluso `vec3 c = vec3(0.0); c = mi_patron(p);` -- compila sin un aviso y se - ejecuta, pero la salida deja de depender del tiempo y de los mandos: imagen - congelada o negra. Poner un inicializador no basta: el problema es la - reasignacion. Si hay que elegir entre varias funciones, mueve la eleccion a una - funcion aparte y llama a esa. Asignar desde una expresion en linea si funciona, y - fuera de bucles no pasa nada. Comprobado en la maquina, costo dias. - **Con las funciones incorporadas esto NO se ha medido.** Lo de arriba esta - comprobado con funciones propias; `min`, `clamp` o `mix` reasignadas dentro de un - bucle nunca se probaron, ni a favor ni en contra. En el port de Remnant X - (2026-09-04) los bucles se escribieron esquivando la duda: la llamada sube a su - propia declaracion -- `float k = clamp(...);` -- y la reasignacion se queda en - aritmetica pura. No cuesta nada y quita la pregunta de encima, asi que hazlo - mientras siga sin medirse en la maquina. -- **En un shader `0-input`, la coordenada sale de `gl_FragCoord`, NUNCA de - `v_texcoord`.** Sin texturas, conjur dibuja con `ofDrawRectangle`, que no genera - coordenadas de textura, asi que `v_texcoord` llega constante y sale una pantalla de - un color plano sin ningun error. Usa `gl_FragCoord.xy / u_resolution.xy`. Los 5 - shaders `0-input` del repo original lo hacen asi y los 12 de `1-input` justo al - reves: 17 de 17. **Y ojo, el fallo depende del estado en ejecucion**: si hay un - video puesto, `v_texcoord` funciona y el mismo shader parece correcto. Es la trampa - mas cara que nos ha salido; costo dias. -- **El archivo tiene que ser ASCII puro.** Comprobado en la máquina el 2026-08-29: - dos shaders con comentarios en español dieron **pantalla verde sin ningún aviso**, - después de renderizar bien en glslViewer. Los 23 shaders del repo original no - llevan ni una tilde. Escribe los comentarios sin tildes, sin guiones largos y sin - comillas tipográficas, aunque el resto de la documentación esté en español. -- **r_e_c_u_r no avisa de que un shader ha fallado.** El estado `'!'` de error existe - solo en un comentario del código; nada lo asigna. Un shader roto se ve igual que - uno que corre. Si algo no se comporta, el error de compilación está en la salida de - c_o_n_j_u_r, no en la interfaz. -- **`//N-input` es solo la etiqueta del menú**, pero ponla igualmente: es lo que te - dice de un vistazo qué hace el shader cuando tienes treinta en la lista. -- **`u_x3` puede no llegar.** Si el ajuste `X3_AS_SPEED` está activo en el aparato, - ese mando pasa a controlar la velocidad. Diseña para tres y deja el cuarto para lo - menos importante. -- **Sin multipasada.** Un shader de Shadertoy con `Buffer A` no se porta: se rediseña - la idea aprovechando el `detour` de r_e_c_u_r, o se descarta. Dilo pronto, antes de - gastar una hora traduciendo algo que no puede funcionar. +- **`u_mouse` no existeix.** Apareix a la documentació de l'autor de r_e_c_u_r, i + glslViewer sí que l'ofereix, així que un port que el faci servir funciona al portàtil i + a la Pi es veu congelat, sense donar cap error. Mapa `iMouse` a `u_x0`/`u_x1`, que a + més ja vénen en 0–1 i no cal dividir per la resolució. +- **glslViewer dona uns 40 uniforms i conjur cinc.** Només coincideixen `u_time`, + `u_resolution`, `u_tex0` i `u_tex1`. `u_delta`, `u_frame`, `u_date`, `u_view2d`, + `u_camera*` i companyia fallen en silenci a l'aparell. +- **`mediump` i el temps.** `u_time` creix sense límit i en `mediump` apareixen bandes i + salts al cap de pocs minuts. Embolcalla'l: si el shader té un cicle natural, + `mod(u_time, durada_del_cicle)` dona exactament la mateixa seqüència sense créixer. Si + no en té, `mod(u_time, 100.)` sol ser prou. +- **De vegades l'artefacte ÉS el shader.** Abans de donar per bo un port, compara el teu + render amb **com es veu la referència allà on viu** (el sandbox, Shadertoy). Si no + s'assemblen encara que el codi coincideixi, el primer experiment va **a l'entorn de la + referència** canviant una sola cosa. A `rejilla` va bastar posar `highp` al sandbox per + veure que el clapejat que faltava era un artefacte de `mediump`, no un efecte del codi. + Un port pot donar 0/255 contra l'original i tot i així no assemblar-se al que veu la + gent. I si l'efecte depèn de la precisió, es reconstrueix a propòsit amb soroll: confiar + que la màquina de destí s'equivoqui igual no funciona. +- **En AQUEST aparell `mediump` no és fp16: es comporta com fp32.** Mesurat el 2026-09-05 + amb `plantilles/test-precision.frag`. Importa perquè l'especificació de GLES 2.0 només + *exigeix* fp16, i amb fp16 el pas representable en 120 és 0.059 —o sia que + `p + 0.001 == p` i una normal per diferències surt `(0,0,0)`—. Aquí això no passa. + **Abans de culpar la precisió de res, passa aquest diagnòstic**: es va donar per fet el + mínim de l'especificació en comptes de mesurar-lo, i es va muntar un diagnòstic sencer + sobre una premissa falsa. En una altra Pi pot ser diferent, així que la regla no és + «tant és», és «comprova-ho». +- **El `smin` exponencial s'ha de reescriure.** La forma que circula per tot arreu, + `-log(exp(-k*a) + exp(-k*b))/k`, desborda pels dos costats. Es posa així, que és idèntic + en matemàtiques: `min(a,b) - log(1.0 + exp(-k*abs(a-b)))/k`. Amb `k=16` i `mediump` de + debò les exponencials s'esgoten amb distàncies per sobre de 0.607, o sia en gairebé tota + la pantalla, i aleshores **`smin` es converteix en `min`: desapareix el difuminat**, que + és justament per al que hi era. Es diagnostica fatal, perquè sembla una decisió d'estil i + no una fallada numèrica. Al navegador no es veu: els controladors d'escriptori tracten + `mediump` com fp32. +- **Els hashos moderns fan servir operadors de bits**, que a ES 1.00 no existeixen. S'han + de substituir per un de coma flotant del tipus + `fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453)`. El soroll no surt idèntic: és + una reescriptura, no una traducció, i convé dir-ho a qui ho ha demanat. +- **Una variable local anomenada com una funció incorporada** (`mix`, `step`, `length`…) + tapa la funció dins d'aquella funció. Compila a escriptori i és imprevisible a la Pi. + Reanomena-la. +- **El bucle d'antialiasing multiplica la mida per `AA*AA`.** Te limits constants, aixi + que el compilador el desenrotlla i el cos sencer, patro inclos, acaba quatre vegades + al codi. Si un port es a prop del limit, abaixar `AA` a 1. es la palanca mes barata: + divideix per quatre canviant un caracter, a canvi de perdre el suavitzat de vores. + Abans d'aixo, busca condicions repetides que puguis treure fora: aixo no canvia ni un + pixel. +- **Una constant absurda sol ser l'efecte, no un calcul.** `time *= 5e19` es llencar la + precisio a proposit perque el que ve despres sigui soroll. Dues consequencies: + necessita `highp` (en `mediump` aixo es infinit i no queda shader) i **no es veura + igual que al navegador**, perque depen de com arrodoneixi cada GPU. Dir-ho ABANS de + portar. El revisor avisa de qualsevol literal per sobre de 65504. +- **El raymarching NO HI CAP en aquest aparell, i no es questio d'afinar-lo.** Mesurat el + 2026-09-05: un shader de keim amb 234 crides a `map()` per pixel es va reduir a 19 i + despres a 13, i **totes dues s'encallen i perden la definicio de les figures**. Aquell + `map()` portava un `exp`, un `sin`, dos `mod` i quatre `length`. Davant d'un shader que + marxi raigs, **no comencis abaixant els passos**: redissenya la idea en 2D, una + avaluacio per pixel, que es el que va funcionar a `panal`: 30 fps, el sostre de + l'aparell. I digues-ho aviat, abans de gastar la tarda traduint. El revisor ja ho avisa + sol: estima les operacions per pixel i salta per sobre de 250. +- **Si el shader es gran, no dibuixa res.** Mesurat a la maquina: 146 linies i 69 + operacions donen pantalla negra; 85 i 38 funcionen. Compila, enllaca i despres no + pinta, sense cap error. Cap dels 146 shaders de la imatge passa de 90 linies ni 23 + operacions: van a un efecte per fitxer, i no es estetica, es el que hi cap. Un shader + de Shadertoy amb diversos efectes darrere d'un selector es porta com DIVERSOS shaders, + no com un. +- **Dins d'un bucle, assigna des d'una funcio NOMES a la declaracio.** + `vec3 c = el_meu_patro(p);` funciona. Reassignar despres -- `vec3 c; c = el_meu_patro(p);` + o fins i tot `vec3 c = vec3(0.0); c = el_meu_patro(p);` -- compila sense ni un avis i + s'executa, pero la sortida deixa de dependre del temps i dels comandaments: imatge + congelada o negra. Posar un inicialitzador no basta: el problema es la reassignacio. Si + cal triar entre diverses funcions, mou l'eleccio a una funcio a part i crida aquesta. + Assignar des d'una expressio en linia si que funciona, i fora de bucles no passa res. + Comprovat a la maquina, va costar dies. + **Amb les funcions incorporades aixo NO s'ha mesurat.** El d'aqui dalt esta comprovat + amb funcions propies; `min`, `clamp` o `mix` reassignades dins d'un bucle no es van + provar mai, ni a favor ni en contra. Al port de Remnant X (2026-09-04) els bucles es van + escriure esquivant el dubte: la crida puja a la seva propia declaracio -- `float k = + clamp(...);` -- i la reassignacio es queda en aritmetica pura. No costa res i treu la + pregunta del mig, aixi que fes-ho mentre segueixi sense mesurar-se a la maquina. +- **En un shader `0-input`, la coordenada surt de `gl_FragCoord`, MAI de `v_texcoord`.** + Sense textures, conjur dibuixa amb `ofDrawRectangle`, que no genera coordenades de + textura, aixi que `v_texcoord` arriba constant i surt una pantalla d'un color pla sense + cap error. Fes servir `gl_FragCoord.xy / u_resolution.xy`. Els 5 shaders `0-input` del + repositori original ho fan aixi i els 12 de `1-input` just al reves: 17 de 17. **I + compte, la fallada depen de l'estat en execucio**: si hi ha un video posat, `v_texcoord` + funciona i el mateix shader sembla correcte. Es el parany mes car que ens ha sortit; va + costar dies. +- **El fitxer ha de ser ASCII pur.** Comprovat a la maquina el 2026-08-29: dos shaders amb + comentaris amb accents van donar **pantalla verda sense cap avis**, despres de + renderitzar be a glslViewer. Els 23 shaders del repositori original no porten ni un + accent. Escriu els comentaris sense accents, sense guions llargs i sense cometes + tipografiques, encara que la resta de la documentacio porti accents. +- **r_e_c_u_r no avisa que un shader ha fallat.** L'estat `'!'` d'error existeix només en + un comentari del codi; no hi ha res que l'assigni. Un shader trencat es veu igual que un + que corre. Si alguna cosa no es comporta, l'error de compilació és a la sortida de + c_o_n_j_u_r, no a la interfície. +- **`//N-input` és només l'etiqueta del menú**, però posa-la igualment: és el que et diu + d'un cop d'ull què fa el shader quan en tens trenta a la llista. +- **`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. +- **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. -## Antes de teorizar: mirar los registros +## Abans de teoritzar: mirar els registres -Regla puesta el 2026-09-05, después de incumplirla dos veces: +Regla posada el 2026-09-05, després d'incomplir-la dues vegades: -**Si hay logs, se consultan primero.** No se deduce el comportamiento del aparato a -partir de lo que exige una especificación cuando hay un dato medido o un diagnóstico que -lo contesta. Los dos casos reales: +**Si hi ha registres, es consulten primer.** No es dedueix el comportament de l'aparell a +partir del que exigeix una especificació quan hi ha una dada mesurada o un diagnòstic que +ho contesta. Els dos casos reals: -- Diagnostiqué un problema de precisión razonando con la aritmética de `mediump` en 16 - bits. La aritmética era correcta; la premisa, falsa —ese aparato usa 32—. El - diagnóstico que lo contestaba (`plantillas/test-precision.frag`) estaba escrito y sin - pasar. -- La resolución del aparato llevaba dos días en la línea 6 de un log que yo mismo había - capturado, y seguía apuntada como duda abierta. +- Es va diagnosticar un problema de precisió raonant amb l'aritmètica de `mediump` en 16 + bits. L'aritmètica era correcta; la premissa, falsa —aquell aparell fa servir 32—. El + diagnòstic que ho contestava (`plantilles/test-precision.frag`) estava escrit i sense + passar. +- La resolució de l'aparell portava dos dies a la línia 6 d'un registre que s'havia + capturat, i seguia apuntada com a dubte obert. -En la práctica: `registros/` tiene los logs de conjur, que vuelcan el fuente completo de -cada shader que se cargó y las medidas de fps. `instrumentacion/fps-por-shader.py` los lee. -Y hay dos diagnósticos que contestan en una sola carga preguntas que no se pueden -contestar leyendo código: `test-orientation.frag` y `test-precision.frag`. +A la pràctica: els registres de conjur aboquen el font complet de cada shader que s'ha +carregat i les mesures de fps. `instrumentacio/fps-por-shader.py` els llegeix. I hi ha dos +diagnòstics que contesten en una sola càrrega preguntes que no es poden contestar llegint +codi: `test-orientation.frag` i `test-precision.frag`. -Una deducción correcta sobre una premisa sin medir es una respuesta equivocada con -aspecto de rigor. +Una deducció correcta sobre una premissa sense mesurar és una resposta equivocada amb +aspecte de rigor. -## Un port NO está acabado hasta que lo dice quien tiene el aparato +## Un port NO està acabat fins que ho diu qui té l'aparell -Ni cuando compila, ni cuando se ve bien en glslViewer, ni cuando el revisor sale limpio, -ni cuando el sandbox dice «compiled successfully». Todo eso son pasos, no el final. -**Solo quien lo prueba en el aparato y lo dice cierra un port.** Hasta entonces se -habla de él como lo que es: en curso. +Ni quan compila, ni quan es veu bé a glslViewer, ni quan el revisor surt net, ni quan el +sandbox diu «compiled successfully». Tot això són passos, no el final. **Només qui el +prova a l'aparell i ho diu tanca un port.** Fins aleshores se'n parla com del que és: en +curs. -## Procedimiento +## Procediment -### 1. Licencia y atribución, antes de traducir +### 1. Llicència i atribució, abans de traduir -Shadertoy publica por defecto bajo **CC BY-NC-SA 3.0** salvo que el autor diga otra -cosa en el propio código. La cláusula **NC impide usarlo en bolos pagados**, que para -un medialab no es un detalle menor. Además r_e_c_u_r es GPL-3.0, con la que NC no es -compatible: un port NC **no se puede aportar de vuelta** al proyecto. +Shadertoy publica per defecte sota **CC BY-NC-SA 3.0** llevat que l'autor digui una altra +cosa al mateix codi. La clàusula **NC impedeix fer-lo servir en bolos pagats**, que per a +un medialab no és un detall menor. A més r_e_c_u_r és GPL-3.0, amb la qual NC no és +compatible: un port NC **no es pot aportar de tornada** al projecte. -Guarda el original sin tocar en `originales/`, con este encabezado: +Guarda l'original sense tocar a `originals/`, amb aquesta capçalera: ```glsl -// Título -// Autor: nombre en Shadertoy +// Titol +// Autor: nom a Shadertoy // URL: https://www.shadertoy.com/view/XXXXXX -// Licencia: CC BY-NC-SA 3.0 (por defecto en Shadertoy salvo indicación del autor) -// Consultado: AAAA-MM-DD -// Portado en: ../shaders/N-input/nombre.frag +// Llicencia: CC BY-NC-SA 3.0 (per defecte a Shadertoy llevat d'indicacio de l'autor) +// Consultat: AAAA-MM-DD +// Portat a: ../shaders/N-input/nom.frag ``` -Si falta la URL porque quien lo pasó no la dio, **escribe `[PENDIENTE]` y díselo**. -No inventes un enlace de Shadertoy: los códigos de las páginas son opacos y uno -inventado apunta a otro shader o a nada. +Si falta l'URL perquè qui el va passar no la va donar, **escriu `[PENDENT]` i digues-ho**. +No inventis un enllaç de Shadertoy: els codis de les pàgines són opacs i un d'inventat +apunta a un altre shader o a res. -### 2. Decidir el tipo +### 2. Decidir el tipus -`0-input` si es generativo, `1-input` si procesa el vídeo, `2-input` si mezcla dos -fuentes. Casi todos los de Shadertoy sin `iChannel` son `0-input`. +`0-input` si és generatiu, `1-input` si processa el vídeo, `2-input` si barreja dues +fonts. Gairebé tots els de Shadertoy sense `iChannel` són `0-input`. -### 3. Traducir lo mecánico +### 3. Traduir el mecànic -Con la tabla de la guía. Punto de entrada `void main()`, salida `gl_FragColor`, -`texture2D()` en vez de `texture()`, y `v_texcoord` donde el original hacía +Amb la taula de la guia. Punt d'entrada `void main()`, sortida `gl_FragColor`, +`texture2D()` en comptes de `texture()`, i `v_texcoord` on l'original feia `fragCoord/iResolution`. -Ojo con el aspecto: Shadertoy suele normalizar con `/iResolution.y` para que los -círculos salgan redondos. `v_texcoord` va de 0 a 1 en ambos ejes, así que hace falta -`uv.x *= u_resolution.x / u_resolution.y;` o todo sale estirado. +Compte amb la proporció: Shadertoy sol normalitzar amb `/iResolution.y` perquè els +cercles surtin rodons. `v_texcoord` va de 0 a 1 en tots dos eixos, així que cal +`uv.x *= u_resolution.x / u_resolution.y;` o tot surt estirat. -### 4. Pasar el revisor +### 4. Passar el revisor ```bash -./check_port.py shaders/N-input/mi_shader.frag +./check_port.py shaders/N-input/el_meu_shader.frag ``` -Marca `[ROMPE]` lo que no compila en ES 1.00, `[FALTA]` los requisitos de r_e_c_u_r -que no están y `[REVISAR]` lo que depende de la máquina. No es un compilador: es esta -lista de comprobaciones automatizada. Que salga limpio no garantiza que compile en la -Pi, pero que salga sucio garantiza que no. +Marca `[ROMPE]` allò que no compila a ES 1.00, `[FALTA]` els requisits de r_e_c_u_r que +no hi són i `[REVISAR]` allò que depèn de la màquina. No és un compilador: és aquesta +llista de comprovacions automatitzada. Que surti net no garanteix que compili a la Pi, +però que surti brut garanteix que no. -### 5. Elegir los mandos +### 5. Triar els comandaments -Aquí está el criterio de diseño del port, y es lo que distingue un port útil de una -traducción literal. Dos reglas que funcionan bien: +Aquí hi ha el criteri de disseny del port, i és el que distingeix un port útil d'una +traducció literal. Dues regles que funcionen bé: -- **Con los cuatro mandos a cero, el shader debe verse como el original.** Es como - llega al cargarlo en el aparato: si a cero se ve plano o negro, parece roto. -- **Cada mando tiene que cambiar algo reconocible a tres metros**, no afinar un - decimal. Si el original cicla automáticamente entre estados, poder **elegir** el - estado suele ser el mando más valioso: sin él hay que esperar a que pase. +- **Amb els quatre comandaments a zero, el shader s'ha de veure com l'original.** És com + arriba en carregar-lo a l'aparell: si a zero es veu pla o negre, sembla trencat. +- **Cada comandament ha de canviar alguna cosa reconeixible a tres metres**, no afinar un + decimal. Si l'original cicla automàticament entre estats, poder **triar** l'estat sol + ser el comandament més valuós: sense ell cal esperar que passi. -Los params llegan siempre entre 0 y 1; el escalado se hace dentro del shader -(`float velocidad = u_x0 * 4.0;`). Sirven también de interruptor (`if(u_x2 > 0.5)`). +Els params arriben sempre entre 0 i 1; l'escalat es fa dins del shader +(`float velocitat = u_x0 * 4.0;`). Serveixen també d'interruptor (`if(u_x2 > 0.5)`). -Para un mando multiplicativo, el idioma que usa cyberboy666 en su tutorial resuelve la -primera regla de un plumazo: multiplicar por `(1.0 + u_x1)` en vez de por `u_x1`. A -mando cero multiplica por uno — sin efecto — y a fondo duplica. Multiplicar por `u_x1` -a secas anula la imagen en la posición de reposo. +Per a un comandament multiplicatiu, l'idioma que fa servir cyberboy666 al seu tutorial +resol la primera regla d'un cop: multiplicar per `(1.0 + u_x1)` en comptes de per `u_x1`. +A comandament zero multiplica per un —sense efecte— i a fons duplica. Multiplicar per +`u_x1` a seques anul·la la imatge en la posició de repòs. -### 6. Rendimiento +### 6. Rendiment -Es una Pi 3 renderizando vídeo a la vez. Lo que más cuesta, por orden: bucles de -antialiasing (coste al cuadrado: `AA 4.` son 16 muestras por píxel), raymarching con -muchos pasos, y `pow`/`log`/`atan` dentro de bucles. +És una Pi 3 renderitzant vídeo alhora. El que més costa, per ordre: bucles +d'antialiasing (cost al quadrat: `AA 4.` són 16 mostres per píxel), raymarching amb molts +passos, i `pow`/`log`/`atan` dins de bucles. -Baja esos números en el port y **deja escrito en un comentario cómo subirlos**, para -que se pueda ajustar en la máquina sin releer el shader entero. +Abaixa aquests números al port i **deixa escrit en un comentari com pujar-los**, perquè es +pugui ajustar a la màquina sense rellegir el shader sencer. -Dos cosas medidas en el aparato el 2026-09-04 que cambian cómo se ajusta: +Dues coses mesurades a l'aparell el 2026-09-04 que canvien com s'ajusta: -- **El techo son 30.0 fps**, y es un tope de software: conjur hace `ofSetFrameRate(30)` - con el vsync apagado (`ofApp.cpp:11-12`). Un shader que marca 30.0 está en el tope y - puede tener margen de sobra; por debajo de 30 se mide la máquina. El cuadro es - 720x480 porque la imagen trae `sdtv_mode=16`, o sea NTSC progresivo — **no PAL**. -- **Bajar el coste no es una cuesta, es un escalón.** Un bucle de 8 vueltas dio 13.2 - fps y el mismo con 10 dio 6.6: el coste por vuelta salta de 9.5 a 15.2 ms y luego se - queda plano. Cuando un port va justo, «recortarlo un poco» no sirve; hay que ver de - qué lado del escalón está. -- **La prueba que decide es el intervalo contra la transición más corta.** Si el shader - tiene un destello o un golpe, mide cuánto dura y compáralo con los milisegundos entre - fotogramas. Una transición más corta que el intervalo **no se ve débil: no se ve**, y - parece que el port está mal en vez de lento. Fue exactamente lo que pasó con - `beat_ring`: destello de 111 ms contra 256 ms entre fotogramas. +- **El sostre són 30.0 fps**, i és un topall de programari: conjur fa `ofSetFrameRate(30)` + amb el vsync apagat (`ofApp.cpp:11-12`). Un shader que marca 30.0 és al topall i pot + tenir marge de sobres; per sota de 30 es mesura la màquina. El quadre és 720x480 perquè + la imatge porta `sdtv_mode=16`, o sia NTSC progressiu — **no PAL**. +- **Abaixar el cost no és una costa, és un graó.** Un bucle de 8 voltes va donar 13.2 fps i + el mateix amb 10 en va donar 6.6: el cost per volta salta de 9.5 a 15.2 ms i després es + queda pla. Quan un port va just, «retallar-lo una mica» no serveix; cal veure de quin + costat del graó és. +- **La prova que decideix és l'interval contra la transició més curta.** Si el shader té + una lluïssor o un cop, mesura quant dura i compara-ho amb els mil·lisegons entre + fotogrames. Una transició més curta que l'interval **no es veu dèbil: no es veu**, i + sembla que el port està malament en comptes de lent. Va ser exactament el que va passar + amb `beat_ring`: lluïssor de 111 ms contra 256 ms entre fotogrames. -Para medirlo en vez de adivinarlo está el envoltorio de -`instrumentacion/`, y -`instrumentacion/fps-por-shader.py` saca la tabla shader → fps del log. +Per mesurar-ho en comptes d'endevinar-ho hi ha l'envoltori d'`instrumentacio/`, i +`instrumentacio/fps-por-shader.py` treu la taula shader → fps del registre. -### 7. Verificar de verdad +### 7. Verificar de debò -No basta con que compile. Ábrelo: +No n'hi ha prou que compili. Obre'l amb glslViewer: ```bash -./herramientas/previsualizar.sh shaders/0-input/mi_shader.frag +glslViewer shaders/0-input/el_meu_shader.frag ``` -Y comprueba **mirando el resultado** que se parece al original y que cada mando hace -lo que dice el comentario. Los errores de signo o de sentido (un zoom que aleja en vez -de acercar) solo se ven así. +I comprova **mirant el resultat** que s'assembla a l'original i que cada comandament fa el +que diu el comentari. Els errors de signe o de sentit (un zoom que allunya en comptes +d'acostar) només es veuen així. -Para dejar constancia, `-c` genera una captura sin abrir ventana; guárdalas en -`capturas/`. Si el shader tiene estados o mandos que merezca la pena documentar, una -hoja de contactos con `montage` vale más que un párrafo. +Per deixar-ne constància, `--headless -E "screenshot,sortida.png"` genera una captura +sense obrir finestra. Si el shader té estats o comandaments que valgui la pena documentar, +un full de contactes amb `montage` val més que un paràgraf. -Recuerda que glslViewer compila con un GLSL más moderno que la Pi: sirve para iterar -sobre la forma, no para validar compatibilidad. +Recorda que glslViewer compila amb un GLSL més modern que la Pi: serveix per iterar sobre +la forma, no per validar compatibilitat. -Para eso está el paso que faltaba: **pegar el shader en -`https://glsl.erogenous-tones.com`**, el sandbox que recomienda el propio autor. Es -WebGL 1, o sea el mismo perfil GLES 2.0 que la Pi, así que rechaza lo que la máquina -rechazaría y glslViewer deja pasar. No sustituye a la prueba en el aparato — no ve el -límite de tamaño ni las trampas de estado —, pero cierra la brecha de sintaxis sin -levantarse de la silla. Ojo con la convención: el sandbox trae la plantilla antigua, así -que un shader escrito con `u_x0`/`u_resolution` hay que adaptarlo para probarlo ahí, o -declarar los uniforms que falten como variables sueltas. +Per a això hi ha el pas que faltava: **enganxar el shader a +`https://glsl.erogenous-tones.com`**, el sandbox que recomana el mateix autor. És WebGL 1, +o sia el mateix perfil GLES 2.0 que la Pi, així que rebutja allò que la màquina rebutjaria +i glslViewer deixa passar. No substitueix la prova a l'aparell —no veu el límit de mida ni +els paranys d'estat—, però tanca l'escletxa de sintaxi sense aixecar-se de la cadira. +Compte amb la convenció: el sandbox porta la plantilla antiga, així que un shader escrit +amb `u_x0`/`u_resolution` s'ha d'adaptar per provar-lo allà, o declarar els uniforms que +falten com a variables soltes. -### 8. Dejarlo donde toca +### 8. Deixar-ho on toca -El `.frag` en `shaders/N-input/`, el original en `originales/`, las capturas en -`capturas/`. Esa estructura se sincroniza tal cual a la máquina: +El `.frag` a `shaders/N-input/`, l'original a `originals/`, les captures a `captures/`. +Aquesta estructura se sincronitza tal qual a la màquina: ```bash rsync -av --dry-run shaders/ pi@IP:/home/pi/Shaders/ ``` -## Al informar del resultado +## En informar del resultat -Di qué hubo que **reescribir** y no solo traducir, qué se bajó por rendimiento y con -qué mandos se quedó, y qué queda sin verificar hasta probarlo en la Pi. Un port que -se presenta como terminado sin haberlo visto renderizado es exactamente el error que -esta skill existe para evitar. +Digues què s'ha hagut de **reescriure** i no només traduir, què s'ha abaixat per rendiment +i amb quins comandaments s'ha quedat, i què queda sense verificar fins a provar-ho a la +Pi. Un port que es presenta com a acabat sense haver-lo vist renderitzat és exactament +l'error que aquesta skill existeix per evitar. diff --git a/README.md b/README.md index a1b197a..b02a0d3 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,91 @@ # agent-recur -Un **subagente de Claude Code** especializado en [r_e_c_u_r](https://github.com/cyberboy666/r_e_c_u_r), -el sampler de vídeo DIY sobre Raspberry Pi de cyberboy666, y la **skill** que lleva el -procedimiento para adaptar shaders de [Shadertoy](https://www.shadertoy.com) a su formato. +Un **subagent de Claude Code** especialitzat en [r_e_c_u_r](https://github.com/cyberboy666/r_e_c_u_r), +el sampler de vídeo DIY sobre Raspberry Pi de cyberboy666, i la **skill** que porta el +procediment per adaptar shaders de [Shadertoy](https://www.shadertoy.com) al seu format. Del [Medialab Terata](https://www.terata.org), Calafou. -## Qué problema resuelve +## Quin problema resol -Shadertoy corre GLSL ES 3.00; r_e_c_u_r corre GLSL ES 1.00 sobre una VideoCore IV. -Traducir los nombres de los uniforms se despacha en diez minutos; lo que cuesta es todo -lo demás, y **casi nada de ello da un error**. Un shader mal portado compila, enlaza, se -ejecuta y pinta una pantalla verde, o negra, o se traba, sin un solo aviso ni en la -máquina ni en la interfaz. +Shadertoy corre GLSL ES 3.00; r_e_c_u_r corre GLSL ES 1.00 sobre una VideoCore IV. Traduir +els noms dels uniforms es despatxa en deu minuts; el que costa és tota la resta, i **gairebé +res d'això dona un error**. Un shader mal portat compila, enllaça, s'executa i pinta una +pantalla verda, o negra, o s'encalla, sense ni un sol avís ni a la màquina ni a la +interfície. -Lo que hay aquí es el resultado de portar seis shaders con el aparato delante, midiendo -en vez de suponer. Cada regla lleva el método con el que se comprobó y, cuando existe, -el número. +El que hi ha aquí és el resultat de portar sis shaders amb l'aparell al davant, mesurant en +comptes de suposar. Cada regla porta el mètode amb què es va comprovar i, quan existeix, el +número. -## Instalación +## Instal·lació -Copiar las dos carpetas a tu proyecto: +Copiar les dues carpetes al teu projecte: ```bash -cp -r .claude/agents/recur.md TU-PROYECTO/.claude/agents/ -cp -r .claude/skills/shadertoy-recur TU-PROYECTO/.claude/skills/ +cp -r .claude/agents/recur.md EL-TEU-PROJECTE/.claude/agents/ +cp -r .claude/skills/shadertoy-recur EL-TEU-PROJECTE/.claude/skills/ ``` -Y el resto del repo donde quieras tenerlo a mano: la skill busca `docs/`, `plantillas/` -y `check_port.py` en rutas relativas a la raíz del paquete. +I la resta del repositori on el vulguis tenir a mà: la skill busca `docs/`, `plantilles/` i +`check_port.py` en rutes relatives a l'arrel del paquet. -Después, en Claude Code, se invoca con `@recur`. La skill se carga sola en ese subagente, -y también se puede invocar a mano con `/shadertoy-recur` en cualquier conversación. +Després, a Claude Code, s'invoca amb `@recur`. La skill es carrega sola en aquell subagent, +i també es pot invocar a mà amb `/shadertoy-recur` en qualsevol conversa. -## Qué hay +## Què hi ha | | | |---|---| -| `.claude/agents/recur.md` | El subagente: qué pregunta antes de empezar, cómo verifica, qué no da nunca por supuesto | -| `.claude/skills/shadertoy-recur/` | El procedimiento de conversión y el catálogo de trampas | -| `docs/conversion-guide.*.md` | La guía de port: tabla de equivalencias, lo que ES 1.00 no tiene, rendimiento | -| `docs/recur-shader-reference.*.md` | Cómo funciona el sistema de shaders: la cadena r_e_c_u_r → c_o_n_j_u_r → ofxVideoArtTools, los uniforms, el tiempo | -| `docs/el-aparato.md` | Lo verificado sobre la máquina: GPU, precisión, límites, ritmo de fotogramas | -| `check_port.py` | Revisor automático de un `.frag`. Sin dependencias más allá de Python 3 | -| `plantillas/` | Esqueletos `0/1/2-input` y dos diagnósticos que contestan en el aparato lo que no se puede leer en el código | -| `instrumentacion/` | Envoltorio para medir fotogramas por segundo en la propia Pi | +| `.claude/agents/recur.md` | El subagent: què pregunta abans de començar, com verifica, què no dona mai per suposat | +| `.claude/skills/shadertoy-recur/` | El procediment de conversió i el catàleg de paranys | +| `docs/conversion-guide.*.md` | La guia de port: taula d'equivalències, allò que ES 1.00 no té, rendiment | +| `docs/recur-shader-reference.*.md` | Com funciona el sistema de shaders: la cadena r_e_c_u_r → c_o_n_j_u_r → ofxVideoArtTools, els uniforms, el temps | +| `docs/l-aparell.md` | El verificat sobre la màquina: GPU, precisió, límits, ritme de fotogrames | +| `check_port.py` | Revisor automàtic d'un `.frag`. Sense dependències més enllà de Python 3 | +| `plantilles/` | Esquelets `0/1/2-input` i dos diagnòstics que contesten a l'aparell allò que no es pot llegir al codi | +| `instrumentacio/` | Envoltori per mesurar fotogrames per segon a la mateixa Pi | -Los documentos están en inglés y español. **El inglés es la fuente**; si difieren, manda -el inglés. +## Llengües -## Sobre qué aparato está medido +La documentació és en **anglès i català**. **L'anglès és la font**; si difereixen, mana +l'anglès. El català és la llengua del medialab i la de tot el que publiquem a +git.terata.org. -Todo lo verificado viene de una **Raspberry Pi 3B+** con la imagen **`recur2_0_2`** -precompilada por cyberboy666: VideoCore IV, OpenGL ES 2.0, salida a 720x480, con el tope -de 30 fps que pone c_o_n_j_u_r por software. +Dues excepcions conscients, totes dues per no trencar res que ja funciona: -Cyberboy publica también una **2.1.0 que no hemos comprobado**. Si la usas y te funciona, -ponte en contacto con las compañeras del Medialab Terata [www.terata.org]. +- **Els comentaris dels `.frag` van sense accents.** No és una badada: el controlador de la + Pi rebutja els bytes no-ASCII fins i tot dins de comentaris, i un shader amb un accent + dona pantalla verda sense cap missatge d'error. Passa el mateix a les seccions de la + documentació copiades literalment d'aquests fitxers. +- **`check_port.py` parla anglès i castellà** (`--lang en|es`), encara no català. Els seus + missatges són etiquetes curtes (`[BREAKS]`, `[MISSING]`, `[CHECK]`) i traduir-les voldria + dir tocar el codi del revisor, cosa que es farà a part. -Los límites que hay aquí son de esa combinación concreta, no de r_e_c_u_r en abstracto. -En otra placa habría que volver a medirlos, y `plantillas/` trae los diagnósticos para -hacerlo. +## Sobre quin aparell està mesurat -## Cómo se mantiene +Tot el verificat ve d'una **Raspberry Pi 3B+** amb la imatge **`recur2_0_2`** precompilada +per cyberboy666: VideoCore IV, OpenGL ES 2.0, sortida a 720x480, amb el topall de 30 fps que +posa c_o_n_j_u_r per programari. -Este repo **se genera**, no se edita a mano. La fuente vive en el repo de trabajo del -medialab y aquí llega con las rutas y las atribuciones adaptadas, mediante un script que -además avisa si se le ha escapado alguna referencia local. Si editas aquí, lo perderás en -la siguiente generación: los arreglos y las propuestas, mejor por correo o issue. +Cyberboy publica també una **2.1.0 que no hem comprovat**. Si la fas servir i et funciona, +posa't en contacte amb les companyes del Medialab Terata [www.terata.org]. -## Licencia +Els límits que hi ha aquí són d'aquella combinació concreta, no de r_e_c_u_r en abstracte. +En una altra placa caldria tornar a mesurar-los, i `plantilles/` porta els diagnòstics per +fer-ho. -GPL-3.0, como r_e_c_u_r. Ver `LICENSE`. +## Com es manté -Los shaders portados **no** están aquí: cada uno tiene la licencia de su autor original -—Shadertoy publica por defecto bajo CC BY-NC-SA 3.0, incompatible con la GPL— y eso se -trata caso por caso. +Aquest repositori **es genera**, no s'edita a mà. La font viu al repositori de treball del +medialab i aquí hi arriba amb les rutes i les atribucions adaptades, mitjançant un script +que a més avisa si se li ha escapat cap referència local. Si edites aquí, ho perdràs a la +generació següent: els arranjaments i les propostes, millor per correu o issue. + +## Llicència + +GPL-3.0, com r_e_c_u_r. Vegeu `LICENSE`. + +Els shaders portats **no** són aquí: cadascun té la llicència del seu autor original +—Shadertoy publica per defecte sota CC BY-NC-SA 3.0, incompatible amb la GPL— i això es +tracta cas per cas. diff --git a/docs/conversion-guide.ca.md b/docs/conversion-guide.ca.md new file mode 100644 index 0000000..c6041cd --- /dev/null +++ b/docs/conversion-guide.ca.md @@ -0,0 +1,681 @@ +--- +doc: SHADERTOY-RECUR-GUIDE +rev: 03 +canonical_language: en +canonical_source: docs/conversion-guide.en.md +translations: + - ca: docs/conversion-guide.ca.md (rev 03, al dia) +--- + +# De Shadertoy a r_e_c_u_r + +Shadertoy corre sobre WebGL2, és a dir **GLSL ES 3.00**. r_e_c_u_r corre sobre OpenGL +ES 2.0, és a dir **GLSL ES 1.00**. No és un canvi de noms: és un llenguatge anterior al +qual li falten coses que mig Shadertoy fa servir sense pensar. D'aquí les dues parts de +sota: la traducció mecànica (ràpida) i allò que s'ha de reescriure de debò. + +Per saber quins uniforms existeixen i per què, vegeu la +[referència](recur-shader-reference.ca.md). + +> **Si el teu shader és 1-input, llegeix abans el tutorial de l'autor.** +> [tutorial_converting_simple_1input_shader_from_shadertoy](https://git.terata.org/eessppeelllloo/r_e_c_u_r/wiki/tutorial_converting_simple_1input_shader_from_shadertoy) +> és el pas a pas del mateix cyberboy666 i el camí més curt per a un shader que processa +> el vídeo d'entrada. Aquesta guia és més ampla —cobreix 0-input, els límits de la GPU i +> quatre errors mesurats a l'aparell— però no el substitueix. Vegeu +> [El tutorial oficial](#el-tutorial-oficial-i-en-què-sen-aparta-aquesta-guia) per saber +> què ensenya i on s'acaba el seu abast. + +## Part 1 — la traducció mecànica + +### Estructura + +Shadertoy et dona només el cos de `mainImage`. r_e_c_u_r vol un fitxer complet: + +```glsl +// SHADERTOY +void mainImage(out vec4 fragColor, in vec2 fragCoord){ + vec2 uv = fragCoord/iResolution.xy; + fragColor = vec4(uv, 0.5 + 0.5*sin(iTime), 1.0); +} +``` + +```glsl +// R_E_C_U_R +//0-input +#ifdef GL_ES +precision mediump float; +#endif + +varying vec2 v_texcoord; +uniform vec2 u_resolution; +uniform float u_time; +uniform float u_x0; +uniform float u_x1; +uniform float u_x2; +uniform float u_x3; + +void main(){ + vec2 uv = v_texcoord; + gl_FragColor = vec4(uv, 0.5 + 0.5*sin(u_time), 1.0); +} +``` + +La marca `//N-input` de la primera línia és el que r_e_c_u_r busca per etiquetar el +shader al seu menú. Sense ella, el shader apareix com a tipus `-`. + +### Taula d'equivalències + +| Shadertoy | r_e_c_u_r | Compte amb | +|---|---|---| +| `void mainImage(out vec4 fragColor, in vec2 fragCoord)` | `void main()` | | +| `fragColor` | `gl_FragColor` | | +| `fragCoord` | `v_texcoord * u_resolution` | O `gl_FragCoord.xy`, que també existeix | +| `fragCoord/iResolution.xy` | `v_texcoord` | El cas comú: se substitueix sencer | +| `iResolution` | `u_resolution` | Shadertoy dona un **`vec3`**; aquí és `vec2`. `iResolution.z` no existeix | +| `iTime` | `u_time` | Pot anar enrere i ser negatiu | +| `iChannel0` / `iChannel1` | `u_tex0` / `u_tex1` | | +| `texture(ch, uv)` | `texture2D(ch, uv)` | | +| `iMouse.xy/iResolution.xy` | `vec2(u_x0, u_x1)` | Ja ve en 0–1, no cal dividir | +| `iMouse.z > 0.0` (botó) | `u_x2 > 0.5` | Els params són continus; es fan servir com a interruptor | +| `iTimeDelta`, `iFrame`, `iDate`, `iSampleRate`, `iChannelResolution` | **no existeixen** | Treure'ls o simular-los | + +### Els paràmetres: de constants màgiques a comandaments + +Aquesta és la part que fa que un port valgui la pena. Un shader de Shadertoy té números +fixos per tot arreu; a r_e_c_u_r aquells números poden ser els comandaments. + +`u_x0`…`u_x3` **arriben sempre entre 0.0 i 1.0** (`conjur.cpp:54`). L'escalat es fa dins +del shader: + +```glsl +float velocitat = u_x0 * 4.0; // 0 → 4 +float zoom = 0.5 + u_x1 * 3.5; // 0.5 → 4 (mai 0, que rebenta una divisio) +float angle = u_x2 * 6.2831853; // volta completa +bool invertir = u_x3 > 0.5; // interruptor +``` + +Triar quins quatre números mereixen comandament és la decisió de disseny del port. Dues +regles que funcionen bé: + +- **Amb els quatre comandaments a zero, el shader s'hauria de veure com l'original.** És + com arriba en carregar-lo a l'aparell: si a zero es veu pla o negre, sembla trencat. +- **Cada comandament ha de canviar alguna cosa reconeixible a tres metres**, no afinar un + decimal. Si l'original cicla automàticament entre estats, poder **triar** l'estat sol + ser el comandament més valuós de tots: sense ell, toca esperar. + +A més: si l'ajust `X3_AS_SPEED` està activat, **`u_x3` no arriba mai al shader**; passa a +ser el control de velocitat. Un shader que necessiti els quatre params només funciona amb +aquell ajust desactivat. Dissenya per a tres. + +### Proporció i centrat + +Shadertoy sol normalitzar així perquè els cercles surtin rodons: + +```glsl +vec2 uv = (fragCoord - 0.5*iResolution.xy)/iResolution.y; +``` + +`v_texcoord` va de 0 a 1 en **tots dos** eixos, així que copiar-ho tal qual deixa tot +estirat en una sortida panoràmica. L'equivalent: + +```glsl +vec2 uv = v_texcoord - 0.5; +uv.x *= u_resolution.x / u_resolution.y; +``` + +## Part 2 — allò que GLSL ES 1.00 no té + +Aquí és on se'n van les hores. Si el shader de Shadertoy fa servir res d'aquesta llista, +no és copiar i enganxar: s'ha de reescriure. + +### Operadors de bits — el que més trenca + +GLSL ES 1.00 **no té `&`, `|`, `^`, `<<`, `>>` ni `%`**. Els hashos moderns de Shadertoy +els fan servir constantment: + +```glsl +// SHADERTOY — no compila a la Pi +uint h = uint(p.x) * 374761393u; +h = (h ^ (h >> 13u)) * 1274126177u; +``` + +Substituir per un hash de coma flotant dels de tota la vida: + +```glsl +float hash(vec2 p){ + return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); +} +``` + +No produeix el mateix soroll, així que el resultat no serà idèntic a l'original. És una +reescriptura, no una traducció, i convé dir-ho en veu alta. + +Tampoc no hi ha `uint`, ni `uvec2/3/4`, ni `%`. Per al mòdul: `mod(a, b)` amb floats. + +### Bucles + +Els límits han de ser **constants en temps de compilació**. Això no compila: + +```glsl +for(int i = 0; i < passos; i++){ ... } // `passos` es una variable +``` + +Es posa el màxim com a constant i se surt amb un `break`: + +```glsl +const int MAX_PASSOS = 64; +for(int i = 0; i < MAX_PASSOS; i++){ + if(float(i) >= passos) break; + ... +} +``` + +Tampoc no hi ha `while`, ni `switch`, ni indexar arrays amb un índex variable. + +### Funcions que falten + +| No hi és a ES 1.00 | Alternativa | +|---|---| +| `textureLod`, `texelFetch` | `texture2D` a seques (sense control de nivell de mipmap) | +| `dFdx`, `dFdy`, `fwidth` | Necessiten l'extensió `GL_OES_standard_derivatives`. **[PER VERIFICAR A LA MÀQUINA]** | +| `round`, `trunc`, `roundEven` | `floor(x + 0.5)`, `floor(abs(x))*sign(x)` | +| `sinh`, `cosh`, `tanh` | Escriure-les amb `exp()` | +| `inverse`, `transpose`, `determinant` | A mà, o redissenyar per no necessitar-les | +| `isnan`, `isinf` | No hi ha manera neta | +| `mix()` amb booleans | Només floats | + +### Noms que tapen funcions + +Una variable local anomenada com una funció incorporada (`mix`, `step`, `length`…) tapa +aquella funció dins del seu àmbit. Compila a escriptori i és imprevisible a la Pi. +Reanomena-la. + +### El bucle d'antialiasing multiplica la mida per AA*AA + +Un parany que se suma al de la mida, i facil de passar per alt. Un bucle aixi: + +```glsl +#define AA 2. +for(float i = 0.; i < AA*AA - 0.5; i += 1.) { + ... + vec3 c = el_meu_patro(p); + ... +} +``` + +te limits constants, aixi que el compilador el **desenrotlla**. El cos sencer -- funcio +de patro inclosa -- acaba al codi final **quatre vegades**. Amb `AA` a 3 en serien nou. + +Mesurat a la maquina: de cinc ports que nomes es diferencien en la funcio de patro, els +quatre amb un patro de 10-15 linies van funcionar, i el que tenia 31 linies i 8 +condicions va sortir negre. Abaixant-lo a `AA 1.` hi va entrar. + +Aixi que quan un port es a prop del limit, `AA` es la palanca mes barata: divideix per +quatre el codi generat canviant un caracter. El que es perd es el suavitzat de vores. + +Abans de tocar `AA`, busca condicions repetides. En aquell mateix patro hi havia tres +branques que tornaven a comprovar el mateix rang; treure-ho fora va llevar sis +comparacions per mostra -- vint-i-quatre ja desenrotllat -- **sense canviar ni un sol +pixel**. + +### Quan el soroll ES l'efecte + +Hi ha shaders que no calculen una imatge: calculen un accident controlat. El senyal es +una constant absurda. `time *= 5e19` no es un compte, es llencar la precisio a proposit +perque el `mod()` de despres llegeixi bits que ja no signifiquen res. La sortida es +soroll numeric, volent. + +Dues consequencies, i totes dues convé dir-les en veu alta abans de portar: + +- **Necessita `highp`.** `5e19` en `mediump` es infinit, i no queda shader. +- **No es veura igual que al navegador**, mai, perque depen de com arrodoneixi cada GPU. + Prometre un resultat identic es prometre el que no es pot complir. + +El mateix va passar al reves amb un shader de reixa el clapejat del qual venia de la +precisio baixa del navegador: l'aparell el dibuixava net i el port semblava *malament* +per estar be. En tots dos casos la regla es la mateixa: **quan un render i la seva +referencia no coincideixen, cal preguntar-se si la referencia esta ensenyant un +artefacte.** + +`check_port.py` avisa de qualsevol literal per sobre de 65504 justament per aixo. + +### 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 +intentar-ho. Mesurat el 2026-09-05 amb un shader de keim +([MtGSWc](https://www.shadertoy.com/view/MtGSWc)), l'original del qual costa **234 crides +a `map()` per pixel**: + +| Versio | Crides a `map()` | A l'aparell | +|---|---|---| +| Original | 234 | impossible | +| Reduida | 19 | **s'encalla, les figures perden definicio** | +| Mes reduida | 13 | **tambe s'encalla** | +| Redissenyada en 2D | 1 avaluacio | funciona | + +Aquell `map()` portava a dins un `exp`, un `sin`, dos `mod` i quatre `length`. Per +comparar: un bucle de 8 voltes amb un cos molt mes barat va a 13.2 fps. + +**Aixi que la maniobra no es abaixar els passos.** No es questio d'afinar: no hi cap amb +cap nombre de passos que valgui la pena. Cal redissenyar la idea en 2D --una avaluacio +per pixel, sense marxa-- que es el que va funcionar aqui i abans amb un Mandelbox. I +conve dir-ho aviat, abans de gastar la tarda en una traduccio que no pot correr. + +`check_port.py` ja estima les operacions per pixel i avisa per sobre de 250. El llindar +esta calibrat amb shaders reals d'aquest aparell: els que van arriben a 166 i el que +s'encalla n'estima 360. + +### Si es massa gran no dibuixa res, i s'ha de partir + +Mesurat a la maquina el 2026-09-03. Un shader massa gran **compila, enllaca i despres no +dibuixa**: pantalla negra o el fotograma anterior congelat, sense cap error enlloc -- ni +al registre de GLSL ni a la interficie de r_e_c_u_r. + +Dos punts mesurats, un a cada costat del limit, del mateix shader: + +| | linies de codi | funcions | operacions | A la Pi | +|---|---|---|---|---| +| Cinc funcions de patro | 146 | 13 | 69 | **pantalla negra** | +| Dues funcions de patro | 85 | 10 | 38 | funciona | + +Per comparar, cap dels 146 shaders que porta la imatge de r_e_c_u_r passa de **90 linies +ni de 23 operacions**. Tots porten un efecte per fitxer, i aixo no es una decisio +estetica: es el que hi cap. + +Aixi que quan un shader de Shadertoy fica diversos efectes diferents en un fitxer darrere +d'un selector -- cosa molt habitual -- el port son diversos shaders, no un. Es perd el +selector intern i es guanya un comandament, perque r_e_c_u_r ja deixa canviar de shader +des de la interficie. + +`check_port.py` avisa per sobre de 100 linies o 45 operacions. + +### Dins d'un bucle, assigna des d'una funcio nomes a la declaracio + +Comprovat a la maquina el 2026-09-03, despres d'una bisecció llarga. + +Dins del cos d'un bucle, aixo genera **codi incorrecte** al compilador GLSL de la Pi: + +```glsl +vec3 c; +c = el_meu_patro(p); // assignada des d'una funcio propia +``` + +El shader **compila sense ni un sol avis** i s'executa, pero la sortida deixa de dependre +d'`u_time` i dels comandaments: surt una imatge congelada, o negra. Ni el registre diu +res, ni la interficie. + +Inicialitza-la a la mateixa declaracio: + +```glsl +vec3 c = el_meu_patro(p); +``` + +Donar-li un valor per defecte a la declaracio **no basta**: el problema es la +reassignacio, no que falti l'inicialitzador. Si has de triar entre diverses funcions, mou +l'eleccio a una funcio a part perque el cos del bucle tingui exactament una assignacio: + +```glsl +vec3 tria(int quin, vec2 p) { + if (quin == 0) return patro_a(p); + if (quin == 1) return patro_b(p); + return patro_c(p); +} +... +vec3 c = tria(quin, p); +``` + +Les tres parts de la regla compten, i cadascuna es va provar per separat: + +| Dins d'un bucle | Resultat a la maquina | +|---|---| +| `vec3 c = la_meva_funcio(p);` | **funciona** | +| `vec3 c; c = vec3(1.0, 0.0, 0.0);` (expressio en linia) | **funciona** | +| `vec3 c; c = la_meva_funcio(p);` | **congelat / negre** | +| `vec3 c = vec3(0.0); c = la_meva_funcio(p);` | **congelat / negre** | + +Fora d'un bucle, declarar sense inicialitzar no dona problema: 22 dels 124 shaders que +porta la imatge de r_e_c_u_r ho fan. **Cap ho fa dins d'un bucle.** + +`check_port.py` detecta exactament aquella combinacio: 0 falsos positius sobre els 141 +shaders que sabem que funcionen en un aparell real, i caça els 5 nostres que fallaven. + +> Aquesta va costar dies, i val la pena entendre per que. Totes les senyals apuntaven en +> la direccio equivocada: compila, s'executa, la interficie mostra el shader com si +> estigues funcionant, i el codi es GLSL absolutament corrent que funciona en qualsevol +> escriptori. L'unica manera de trobar-ho va ser bisecar de canvi en canvi contra un +> shader que se sabia bo en aquell aparell concret. + +### Un shader 0-input no pot fer servir `v_texcoord` + +Trobat a la maquina el 2026-09-02, despres de dies al darrere. Es el parany mes car de +tots els que ens han sortit. + +Mira quina geometria dibuixa conjur (`ofxVideoArtTools/src/conjur.cpp`, `apply()`): + +```cpp +if(textures.size() > 0 ){ + textures[0].draw(0, 0, ofGetWidth(), ofGetHeight()); // genera texcoords +} else { + ofDrawRectangle(0, 0, ofGetWidth(), ofGetHeight()); // NO les genera +} +``` + +Sense textures d'entrada, la geometria es un `ofDrawRectangle`, que **no porta +coordenades de textura**. L'atribut `texcoord` de `default.vert` queda indefinit, aixi que +tant `tcoord` com `v_texcoord` arriben al fragment shader **constants**. Tots els pixels +calculen el mateix i surt **una pantalla d'un color pla, sense cap missatge d'error**. + +Cal treure la coordenada de `gl_FragCoord`: + +```glsl +vec2 uv = gl_FragCoord.xy / u_resolution.xy; +``` + +Els shaders del repositori original segueixen aquesta regla sense haver-la escrit mai. +Comptat sobre un aparell real: + +| | fa servir `v_texcoord` | fa servir `gl_FragCoord` | +|---|---|---| +| Els 5 `0-input` del repositori | **0** | 5 | +| Els 12 `1-input` del repositori | 12 | **0** | + +Disset de disset, sense excepcions. `check_port.py` ja ho detecta. + +> Per que va costar tant trobar-ho: la fallada depen de l'**estat en execucio, no del +> fitxer**. Si hi ha un video reproduint-se, `textures` no esta buit, la geometria es un +> quadrilater amb textura i `v_texcoord` funciona perfectament — el mateix shader es +> comporta be. Per aixo el mateix fitxer semblava funcionar un dia i no l'endema, i per +> aixo el "color de la fallada" anava canviant: el que es veu es el que hi hagi a sota en +> aquell moment. + +### El fitxer ha de ser ASCII pur + +Descobert per les males el 2026-08-29: dos shaders que renderitzaven perfectament a +glslViewer donaven **pantalla verda a l'aparell, sense cap missatge d'error**. L'única +cosa que tenien en comú eren comentaris amb accents, guions llargs i cometes +tipogràfiques. + +Els 23 shaders que porta r_e_c_u_r són ASCII pur. Ni un accent entre tots ells. El +controlador de la Pi és més estricte que el d'un escriptori i rebutja els bytes no-ASCII +**fins i tot dins de comentaris**, on un controlador d'escriptori simplement els ignora. + +Escriu els comentaris en ASCII. `check_port.py` ho detecta. + +> Aquest és especialment dolent perquè totes les senyals apunten en la direcció +> equivocada: compila al portàtil, el revisor el donava per bo, i r_e_c_u_r el mostra com +> si estigués corrent. + +### r_e_c_u_r no et dirà que el shader ha fallat + +`video_centre/shaders.py:18` documenta un estat `'!'` que significa error, però no hi ha +ni una sola línia del codi que l'assigni. Un shader que no compila es mostra exactament +igual que un que funciona. + +Per veure l'error real de GLSL cal la sortida estàndard de c_o_n_j_u_r. Es llança amb +`subprocess.Popen` sense redirecció (`actions.py:573`), així que la seva sortida va on +vagi la consola de r_e_c_u_r. Per SSH: + +```bash +pkill c_o_n_j_u_r +/home/pi/openframeworks10.1/apps/myApps/c_o_n_j_u_r/bin/c_o_n_j_u_r +``` + +Després es carrega el shader des de la interfície i es llegeix el terminal. +openFrameworks imprimeix el registre complet de GLSL, amb el número de línia de la +fallada. + +### Multipassada: el límit gran + +Shadertoy permet `Buffer A/B/C/D`: passades que llegeixen el seu propi fotograma +anterior. Tot el feedback, els autòmats cel·lulars, els fluids i les simulacions depenen +d'això. **No hi ha equivalent directe en un shader de r_e_c_u_r.** + +El que sí que hi ha, i dona molt de joc, és diferent: + +- **Les 3 capes de shader.** S'apliquen en cadena, i la capa 1 rep a `u_tex0` la sortida + de la capa 0 i a `u_tex1` l'entrada de la capa 0 (`ofApp.cpp:115-129`). Són diverses + passades, però sense memòria del fotograma anterior. +- **El mode `detour`** de r_e_c_u_r, que sí que és un bucle de realimentació de vídeo. + L'ajust `SHADER_POSITION` decideix si els shaders van abans o després. + +A la pràctica: un shader de Shadertoy amb Buffers no es porta. O es redissenya la idea al +voltant de detour, o es descarta. Digues-ho aviat, abans de gastar una hora traduint una +cosa que no pot funcionar. + +### Precisió + +`precision mediump float;` és el que porten tots els shaders del repositori. En `mediump` +el rang útil és curt i `u_time` **creix sense límit**: al cap de pocs minuts apareixen +bandes i salts en qualsevol cosa que faci `sin(u_time * alguna_cosa_gran)`. + +Dos remeis, per ordre: + +1. Embolcallar el temps. Si el shader té un cicle natural, + `mod(u_time, durada_del_cicle)` dona exactament la mateixa seqüència sense créixer. Si + no, `mod(u_time, 100.0)` sol ser prou. +2. Demanar més precisió sense trencar la compatibilitat: + +```glsl +#ifdef GL_ES + #ifdef GL_FRAGMENT_PRECISION_HIGH + precision highp float; + #else + precision mediump float; + #endif +#endif +``` + +**[PER VERIFICAR A LA MÀQUINA]** si el `highp` en fragment shader està disponible i què +costa. + +### Rendiment + +És una Raspberry Pi renderitzant vídeo alhora. Un raymarching de 128 passos que va suau +al navegador aquí va a batzegades. El que més costa, per ordre: bucles d'antialiasing (el +cost va al quadrat — `AA 4.` són 16 mostres per píxel), raymarching amb molts passos, i +`pow`/`log`/`atan` dins de bucles. + +Abaixa aquests números i **deixa un comentari dient com pujar-los**, per poder ajustar el +shader a la màquina sense rellegir-lo sencer. Val més un que corre a 25 fps que un altre +més bonic a 4. + +**Mesurat a l'aparell (2026-09-04), i canvia la manera d'ajustar.** El sostre són **30.0 +fps**, i és un topall *de programari*, no de la pantalla: conjur fa `ofSetFrameRate(30)` +amb `ofSetVerticalSync(false)` (`ofApp.cpp:11-12`). Així que un shader que marca 30.0 és +al topall i potser li sobra marge de GPU; per sota de 30 el que es mesura és la màquina. +El quadre és de 720x480 perquè `/boot/config.txt` porta `sdtv_mode=16` (NTSC progressiu) +i conjur pren la mida de la pantalla. Un shader el bucle del qual feia 8 voltes, amb dos +`smoothstep`+`distance` per volta, va mesurar: + +| Voltes del bucle | fps | Interval entre fotogrames | +|---|---|---| +| 8 | 13.2 | 76 ms | +| 10 | 6.6 | 152 ms | +| 12 | 4.8 | 208 ms | +| 16 | 3.9 | 256 ms | + +D'aquí en surten dues coses, i cap de les dues es veu des de l'escriptori. + +**És un graó, no una costa.** El cost per volta salta de 9.5 ms amb 8 voltes a 15.2 ms +amb 10, i després es queda pla. En créixer el bucle desenrotllat es creua algun llindar de +la GPU, i a partir d'aquí el shader **sencer** surt més car. Així que quan un port va +just, «retallar-lo una mica» és la maniobra equivocada: cal esbrinar de quin costat del +graó és i posar-se al bo. És el mateix tipus de llindar que el límit de mida que fa que un +shader no dibuixi res: primer es posa lent, més enllà desapareix. + +**Compara l'interval entre fotogrames amb la transició més curta que vulguis veure.** Si +el shader té una lluïssor, un cop o qualsevol atac breu, mesura quant dura i posa-ho al +costat de l'interval. Al shader de dalt la lluïssor durava 111 ms: amb 8 voltes (76 ms) es +recull; amb 10 (152 ms) es perd més vegades de les que es veu, i aleshores el port sembla +*mal fet* en comptes de lent. Una transició més curta que l'interval no es veu dèbil: **no +es veu**, i res no ho avisa. + +Per tenir aquests números en comptes d'endevinar-los, vegeu l'envoltori +d'`instrumentacio/`: compta fotogrames a l'aparell i els escriu al mateix registre que les +càrregues de shader. + +### El tutorial oficial, i en què se n'aparta aquesta guia + +cyberboy666 en va escriure un: +[tutorial_converting_simple_1input_shader_from_shadertoy](https://git.terata.org/eessppeelllloo/r_e_c_u_r/wiki/tutorial_converting_simple_1input_shader_from_shadertoy). +**Si estàs portant un shader 1-input, llegeix-lo.** És curt, és del mateix autor i és el +que segueix la resta de la comunitat. El que ve ara no el corregeix —tot el que diu quadra +amb el codi font— sinó que dibuixa on s'acaba el seu abast. + +Està escrit en la **convenció antiga** (`tcoord`, `tex`, `tres`, `fparams`, `ftime`), que +ve del mòdul Structure d'Erogenous Tones. Aquella convenció funciona —conjur enllaça els +dos jocs de noms a cada fotograma, sense condició—, però dels 22 shaders que porta el +mateix repositori de r_e_c_u_r, 20 són moderns, 1 antic i 1 mixt. Aquesta guia fa servir +el joc modern perquè els shaders nous s'assemblin a ells. + +Tres coses seves que convé endur-se: + +- **Provar al sandbox abans que a l'aparell** (vegeu *Previsualitzar sense la Pi*). És el + més útil que té, i que no fos a les nostres eines no és casualitat: va costar una sessió + sencera de depuració descobrir el forat que tapa. +- **Que el comandament a zero signifiqui «sense efecte».** El seu idioma per a un + multiplicador és `(1.0 + f1)` en comptes de `f1`: en repòs multiplica per un, a fons + duplica. Multiplicar pel paràmetre pelat anul·la la imatge en la posició de repòs. +- Els literals numèrics necessiten punt decimal: `1.0`, mai `1`. + +Quatre llocs on s'acaba el seu abast. No són errors del tutorial, són casos que no tracta, +i cadascun d'ells dona pantalla plana o congelada sense cap missatge d'error: + +- La seva idea central —*«`tcoord` ja ve normalitzada, no divideixis per la resolució»*— + val **només mentre s'estigui dibuixant una textura**. En un shader generatiu sense vídeo + carregat, `conjur::apply()` cau a `ofDrawRectangle()`, que no genera coordenades de + textura, i les dues varyings arriben constants. Per a `0-input`, + `gl_FragCoord.xy / u_resolution`. Vegeu *Un shader 0-input no pot fer servir + `v_texcoord`*. +- La seva plantilla declara `iparams` com a «4 ints coming in». Està mort: fixat a + `(0,0,0,0)`. +- El seu `float time = float(itime) + ftime;` no és un rellotge continu, perquè `itime` és + `ceil()` i no `floor()`. Fes servir `u_time`. +- El seu shader final no porta marca `//N-input`, i `shaders.py` sí que la llegeix. + +Les seves marques `//f0::` són metadades del sandbox, no de r_e_c_u_r: +`determine_shader_parameter_number` retorna 4 sense mirar res. + +## Part 3 — el procediment + +1. **Mirar la llicència primer.** Shadertoy publica sota + [CC BY-NC-SA 3.0](https://creativecommons.org/licenses/by-nc-sa/3.0/) per defecte, + llevat que l'autor digui una altra cosa al mateix codi. La clàusula **NC** vol dir que + no es pot fer servir en un bolo pagat ni en res comercial. I compte: r_e_c_u_r és + GPL-3.0, amb la qual NC **no** és compatible, així que un port derivat de material NC + no es pot aportar de tornada al projecte. +2. **Guardar l'original** amb una capçalera d'atribució (autor, URL, llicència, data de + consulta). Si no tens l'URL, escriu `[PENDENT]` en comptes d'inventar-te-la: els + identificadors de Shadertoy són opacs i un enllaç endevinat apunta a un altre shader. +3. **Traduir el mecànic** amb la taula de la part 1. +4. **Passar el revisor**, que fa aquella feina per tu: + + ```bash + ./check_port.py el_meu_shader.frag + ``` + + Marca `[BREAKS]` allò que no compila en GLSL ES 1.00, `[MISSING]` els requisits de + r_e_c_u_r que no hi són i `[CHECK]` allò que depèn de la màquina. No és un compilador: + que surti net no garanteix que compili a la Pi, però que surti brut garanteix que no. + (Els seus missatges són en anglès; amb `--lang es` surten en castellà. Encara no parla + català.) +5. **Triar els params.** Quins tres o quatre números es converteixen en comandaments. +6. **Previsualitzar en un escriptori abans de tocar la Pi** (vegeu més avall). +7. **Guardar a `shaders/N-input/`** amb la marca correcta. +8. **Copiar a la màquina** i provar de debò. + +### Previsualitzar sense la Pi + +Dues eines, i cacen coses diferents. Fes servir totes dues. + +**El sandbox, per a compatibilitat.** — *click to +create new shader*, hi enganxes i compila a mesura que escrius. És WebGL 1, que és **el +mateix perfil GLES 2.0 que corre la Pi**, així que rebutja allò que rebutjaria l'aparell. +Això és justament el que no pot fer la previsualització d'escriptori de sota. Compte: porta +la plantilla antiga, així que un shader escrit amb `u_x0`/`u_resolution` necessita que li +adaptis els uniforms —o que els declaris com a variables soltes— per compilar-hi. + +No veu el límit de mida ni pot reproduir els paranys que depenen de l'estat. Tanca el +forat de sintaxi, no el de l'aparell. + +**glslViewer, per iterar sobre la forma.** L'autor el recomana, del mateix que va escriure +The Book of Shaders, i diu que els uniforms de conjur hi són compatibles +(`notes_on_shader_formats.md`). Edites, guardes i la finestra s'actualitza sola. + +```bash +glslViewer shaders/0-input/el_meu_shader.frag -l +``` + +Amb textura d'entrada, per als d'1 i 2 inputs: + +```bash +glslViewer shaders/1-input/el_meu_shader.frag una_imatge.png -l +``` + +Per moure un paràmetre, s'escriu a la consola de glslViewer mentre corre: + +``` +u_x0,0.35 +``` + +Per automatitzar —útil per generar captures de diversos shaders de cop— hi ha `--headless` +i `-E`, que executa ordres i surt: + +```bash +glslViewer el_meu_shader.frag --headless -e "u_x0,0.6" -E "screenshot,sortida.png" +``` + +### Comparar el port amb l'original, amb numeros + +Renderitzar tots dos i mirar-los no basta per als canvis que importen. Treu PNG dels dos i +resta'ls canal a canal, pero **clava abans el temps dins del shader**: + +```bash +sed -e 's/^uniform float u_time;/const float u_time = 3.0;/' \ + -e 's/^uniform float u_x\([0-3]\);/const float u_x\1 = 0.0;/' port.frag > fixat.frag +glslViewer fixat.frag --headless -E "screenshot,sortida.png" +``` + +El `-e "time,3.0"` de `glslViewer` **no** el clava: el mateix shader renderitzat dues +vegades al mateix temps nominal donava fins a 55/255 de diferencia per canal en un instant +de canvi rapid, mes que diverses de les diferencies reals que voliem mesurar. Amb el valor +compilat a dins, dues passades surten identiques byte a byte i el soroll baixa a zero. + +Es el que converteix un "es veu semblant" en un "0/255 de diferencia en tres instants, aixi +que l'unica desviacio es la que vaig decidir a proposit". + +### El parany de glslViewer: ofereix molt més del que hi ha a la màquina + +glslViewer dona uns 40 uniforms (vegeu `UNIFORMS.md` al seu repositori). conjur n'enllaça +**cinc**. Els únics presents a tots dos són `u_time`, `u_resolution`, `u_tex0` i `u_tex1`. + +Tota la resta funciona a l'escriptori i **falla en silenci a la Pi**: l'uniform no +s'enllaça, arriba a zero, i el shader es veu congelat o pla sense donar cap error. Els que +més s'hi colen: + +| Existeix a glslViewer | A r_e_c_u_r | +|---|---| +| `u_mouse` | **No.** L'error número u. Fer servir `u_x0`/`u_x1` | +| `u_delta`, `u_frame`, `u_date` | **No** | +| `u_pixelDensity`, `u_sceneFps`, `u_sceneMs` | **No** | +| `u_view2d` (pan/zoom amb el ratolí) | **No** | +| `u_camera*`, `u_light*`, `u_*Matrix` | **No** | + +Al revés també: `u_x0`…`u_x3` són de conjur, no de glslViewer. Allà es fixen a mà des de la +seva consola. + +> Avís: glslViewer en un escriptori compila amb un GLSL més modern que el de la Pi. Que +> funcioni allà **no garanteix** que compili a l'aparell. Serveix per iterar sobre la +> forma. Per a compatibilitat, el sandbox de dalt; l'última paraula, la màquina. + +### Copiar a la màquina + +r_e_c_u_r llegeix shaders des d'un USB muntat, des de `/home/pi/r_e_c_u_r/Shaders` i des +de `/home/pi/Shaders` (`data.py:34`). El menys invasiu és `/home/pi/Shaders`, que deixa net +el repositori de r_e_c_u_r: + +```bash +rsync -av --dry-run shaders/ pi@IP_DE_LA_TEVA_PI:/home/pi/Shaders/ +``` + +Treure `--dry-run` quan la llista de fitxers sigui l'esperada. diff --git a/docs/conversion-guide.en.md b/docs/conversion-guide.en.md index ecf1ee6..47b258a 100644 --- a/docs/conversion-guide.en.md +++ b/docs/conversion-guide.en.md @@ -3,7 +3,7 @@ doc: SHADERTOY-RECUR-GUIDE rev: 03 canonical_language: en translations: - - es: docs/conversion-guide.es.md (rev 02, current) + - ca: docs/conversion-guide.ca.md (rev 02, current) --- # From Shadertoy to r_e_c_u_r @@ -511,7 +511,7 @@ iterations (76 ms) it is caught; at 10 (152 ms) it is missed more often than not the port then looks *wrong* rather than slow. A transition shorter than the frame interval is not faint — it is **absent**, and nothing reports it. -To get these numbers instead of guessing them, see the wrapper in `instrumentacion/`: +To get these numbers instead of guessing them, see the wrapper in `instrumentacio/`: it counts frames on the device and writes them into the same log as the shader loads. ### The official tutorial, and where this guide differs diff --git a/docs/conversion-guide.es.md b/docs/conversion-guide.es.md deleted file mode 100644 index 3eaf326..0000000 --- a/docs/conversion-guide.es.md +++ /dev/null @@ -1,688 +0,0 @@ ---- -doc: SHADERTOY-RECUR-GUIDE -rev: 03 -canonical_language: en -canonical_source: docs/conversion-guide.en.md -translations: - - es: docs/conversion-guide.es.md (rev 02, al día) ---- - -# De Shadertoy a r_e_c_u_r - -Shadertoy corre sobre WebGL2, o sea **GLSL ES 3.00**. r_e_c_u_r corre sobre OpenGL -ES 2.0, o sea **GLSL ES 1.00**. No es un cambio de nombres: es un lenguaje anterior -al que le faltan cosas que medio Shadertoy usa sin pensar. De ahí las dos partes de -abajo: la traducción mecánica (rápida) y lo que hay que reescribir de verdad. - -Para qué uniforms existen y por qué, ver la -[referencia](recur-shader-reference.es.md). - -> **Si tu shader es 1-input, lee antes el tutorial del autor.** -> [tutorial_converting_simple_1input_shader_from_shadertoy](https://git.terata.org/eessppeelllloo/r_e_c_u_r/wiki/tutorial_converting_simple_1input_shader_from_shadertoy) -> es el paso a paso del propio cyberboy666 y el camino más corto para un shader que -> procesa el vídeo de entrada. Esta guía es más ancha —cubre 0-input, los límites de la -> GPU y cuatro fallos medidos en el aparato— pero no lo sustituye. Ver -> [El tutorial oficial](#el-tutorial-oficial-y-en-qué-se-aparta-esta-guía) para qué -> enseña y dónde se acaba su alcance. - -## Parte 1 — la traducción mecánica - -### Estructura - -Shadertoy te da solo el cuerpo de `mainImage`. r_e_c_u_r quiere un archivo completo: - -```glsl -// SHADERTOY -void mainImage(out vec4 fragColor, in vec2 fragCoord){ - vec2 uv = fragCoord/iResolution.xy; - fragColor = vec4(uv, 0.5 + 0.5*sin(iTime), 1.0); -} -``` - -```glsl -// R_E_C_U_R -//0-input -#ifdef GL_ES -precision mediump float; -#endif - -varying vec2 v_texcoord; -uniform vec2 u_resolution; -uniform float u_time; -uniform float u_x0; -uniform float u_x1; -uniform float u_x2; -uniform float u_x3; - -void main(){ - vec2 uv = v_texcoord; - gl_FragColor = vec4(uv, 0.5 + 0.5*sin(u_time), 1.0); -} -``` - -La marca `//N-input` de la primera línea es lo que r_e_c_u_r busca para etiquetar el -shader en su menú. Sin ella, el shader aparece como tipo `-`. - -### Tabla de equivalencias - -| Shadertoy | r_e_c_u_r | Cuidado con | -|---|---|---| -| `void mainImage(out vec4 fragColor, in vec2 fragCoord)` | `void main()` | | -| `fragColor` | `gl_FragColor` | | -| `fragCoord` | `v_texcoord * u_resolution` | O `gl_FragCoord.xy`, que también existe | -| `fragCoord/iResolution.xy` | `v_texcoord` | El caso común: se sustituye entero | -| `iResolution` | `u_resolution` | Shadertoy da un **`vec3`**; aquí es `vec2`. `iResolution.z` no existe | -| `iTime` | `u_time` | Puede ir hacia atrás y ser negativo | -| `iChannel0` / `iChannel1` | `u_tex0` / `u_tex1` | | -| `texture(ch, uv)` | `texture2D(ch, uv)` | | -| `iMouse.xy/iResolution.xy` | `vec2(u_x0, u_x1)` | Ya viene en 0–1, no hay que dividir | -| `iMouse.z > 0.0` (botón) | `u_x2 > 0.5` | Los params son continuos; se usan como interruptor | -| `iTimeDelta`, `iFrame`, `iDate`, `iSampleRate`, `iChannelResolution` | **no existen** | Quitarlos o simularlos | - -### Los parámetros: de constantes mágicas a mandos - -Esta es la parte que hace que un port valga la pena. Un shader de Shadertoy tiene -números fijos por todas partes; en r_e_c_u_r esos números pueden ser los mandos. - -`u_x0`…`u_x3` **siempre llegan entre 0.0 y 1.0** (`conjur.cpp:54`). El escalado se -hace dentro del shader: - -```glsl -float velocidad = u_x0 * 4.0; // 0 → 4 -float zoom = 0.5 + u_x1 * 3.5; // 0.5 → 4 (nunca 0, que revienta una división) -float angulo = u_x2 * 6.2831853; // vuelta completa -bool invertir = u_x3 > 0.5; // interruptor -``` - -Elegir qué cuatro números merecen mando es la decisión de diseño del port. Dos reglas -que funcionan bien: - -- **Con los cuatro mandos a cero, el shader debería verse como el original.** Es como - llega al cargarlo en el aparato: si a cero se ve plano o negro, parece roto. -- **Cada mando tiene que cambiar algo reconocible a tres metros**, no afinar un - decimal. Si el original cicla automáticamente entre estados, poder **elegir** el - estado suele ser el mando más valioso de todos: sin él, toca esperar. - -Además: si el ajuste `X3_AS_SPEED` está activado, **`u_x3` no llega nunca al -shader**; pasa a ser el control de velocidad. Un shader que necesite los cuatro params -solo funciona con ese ajuste desactivado. Diseña para tres. - -### Aspecto y centrado - -Shadertoy suele normalizar así para que los círculos salgan redondos: - -```glsl -vec2 uv = (fragCoord - 0.5*iResolution.xy)/iResolution.y; -``` - -`v_texcoord` va de 0 a 1 en **ambos** ejes, así que copiarlo tal cual deja todo -estirado en una salida panorámica. El equivalente: - -```glsl -vec2 uv = v_texcoord - 0.5; -uv.x *= u_resolution.x / u_resolution.y; -``` - -## Parte 2 — lo que GLSL ES 1.00 no tiene - -Aquí es donde se van las horas. Si el shader de Shadertoy usa algo de esta lista, no -es copiar y pegar: hay que reescribirlo. - -### Operadores de bits — el que más rompe - -GLSL ES 1.00 **no tiene `&`, `|`, `^`, `<<`, `>>` ni `%`**. Los hashes modernos de -Shadertoy los usan constantemente: - -```glsl -// SHADERTOY — no compila en la Pi -uint h = uint(p.x) * 374761393u; -h = (h ^ (h >> 13u)) * 1274126177u; -``` - -Sustituir por un hash de coma flotante de los de toda la vida: - -```glsl -float hash(vec2 p){ - return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); -} -``` - -No produce el mismo ruido, así que el resultado no será idéntico al original. Es una -reescritura, no una traducción, y conviene decirlo en voz alta. - -Tampoco hay `uint`, ni `uvec2/3/4`, ni `%`. Para el módulo: `mod(a, b)` con floats. - -### Bucles - -Los límites tienen que ser **constantes en tiempo de compilación**. Esto no compila: - -```glsl -for(int i = 0; i < pasos; i++){ ... } // `pasos` es una variable -``` - -Se pone el máximo como constante y se sale con un `break`: - -```glsl -const int MAX_PASOS = 64; -for(int i = 0; i < MAX_PASOS; i++){ - if(float(i) >= pasos) break; - ... -} -``` - -Tampoco hay `while`, ni `switch`, ni indexar arrays con un índice variable. - -### Funciones que faltan - -| No está en ES 1.00 | Alternativa | -|---|---| -| `textureLod`, `texelFetch` | `texture2D` a secas (sin control de nivel de mipmap) | -| `dFdx`, `dFdy`, `fwidth` | Necesitan la extensión `GL_OES_standard_derivatives`. **[POR VERIFICAR EN LA MÁQUINA]** | -| `round`, `trunc`, `roundEven` | `floor(x + 0.5)`, `floor(abs(x))*sign(x)` | -| `sinh`, `cosh`, `tanh` | Escribirlas con `exp()` | -| `inverse`, `transpose`, `determinant` | A mano, o rediseñar para no necesitarlas | -| `isnan`, `isinf` | No hay forma limpia | -| `mix()` con booleanos | Solo floats | - -### Nombres que tapan funciones - -Una variable local llamada como una función incorporada (`mix`, `step`, `length`…) -tapa esa función dentro de su ámbito. Compila en escritorio y es imprevisible en la -Pi. Renómbrala. - -### El bucle de antialiasing multiplica el tamano por AA*AA - -Una trampa que se suma a la del tamano, y facil de pasar por alto. Un bucle asi: - -```glsl -#define AA 2. -for(float i = 0.; i < AA*AA - 0.5; i += 1.) { - ... - vec3 c = mi_patron(p); - ... -} -``` - -tiene limites constantes, asi que el compilador lo **desenrolla**. El cuerpo entero --- funcion de patron incluida -- acaba en el codigo final **cuatro veces**. Con -`AA` a 3 serian nueve. - -Medido en la maquina: de cinco ports que solo se diferencian en la funcion de -patron, los cuatro con un patron de 10-15 lineas funcionaron, y el que tenia 31 -lineas y 8 condiciones salio negro. Bajandolo a `AA 1.` entro. - -Asi que cuando un port esta cerca del limite, `AA` es la palanca mas barata: divide -por cuatro el codigo generado cambiando un caracter. Lo que se pierde es el -suavizado de bordes. - -Antes de tocar `AA`, busca condiciones repetidas. En ese mismo patron habia tres -ramas que volvian a comprobar el mismo rango; sacarlo fuera quito seis -comparaciones por muestra -- veinticuatro ya desenrollado -- **sin cambiar un solo -pixel**. - -### Cuando el ruido ES el efecto - -Hay shaders que no calculan una imagen: calculan un accidente controlado. La senal es -una constante absurda. `time *= 5e19` no es una cuenta, es tirar la precision a -proposito para que el `mod()` de despues lea bits que ya no significan nada. La salida -es ruido numerico, queriendo. - -Dos consecuencias, y las dos conviene decirlas en voz alta antes de portar: - -- **Necesita `highp`.** `5e19` en `mediump` es infinito, y no queda shader. -- **No va a verse igual que en el navegador**, nunca, porque depende de como redondee - cada GPU. Prometer un resultado identico es prometer lo que no se puede cumplir. - -Lo mismo paso al reves con un shader de reja cuyo moteado venia de la precision baja del -navegador: el aparato lo dibujaba limpio y el port parecia *mal* por estar bien. En los -dos casos la regla es la misma: **cuando un render y su referencia no coinciden, hay que -preguntarse si la referencia esta ensenando un artefacto.** - -`check_port.py` avisa de cualquier literal por encima de 65504 justo por esto. - -### El raymarching no cabe --- medido, no supuesto - -Un shader que marcha rayos no entra en este aparato, y la forma de saberlo no es -intentarlo. Medido el 2026-09-05 con un shader de keim -([MtGSWc](https://www.shadertoy.com/view/MtGSWc)), cuyo original cuesta **234 llamadas -a `map()` por pixel**: - -| Version | Llamadas a `map()` | En el aparato | -|---|---|---| -| Original | 234 | imposible | -| Reducida | 19 | **se traba, las figuras pierden definicion** | -| Mas reducida | 13 | **tambien se traba** | -| Redisenada en 2D | 1 evaluacion | funciona | - -Ese `map()` llevaba dentro un `exp`, un `sin`, dos `mod` y cuatro `length`. Para -comparar: un bucle de 8 vueltas con un cuerpo mucho mas barato va a 13.2 fps. - -**Asi que la maniobra no es bajar los pasos.** No es cuestion de afinar: no cabe con -ningun numero de pasos que merezca la pena. Hay que redisenar la idea en 2D --una -evaluacion por pixel, sin marcha-- que es lo que funciono aqui y antes con un -Mandelbox. Y conviene decirlo pronto, antes de gastar la tarde en una traduccion que no -puede correr. - -`check_port.py` ya estima las operaciones por pixel y avisa por encima de 250. El -umbral esta calibrado con shaders reales de este aparato: los que van llegan a 166 y el -que se traba estima 360. - -### Si es demasiado grande no dibuja nada, y hay que partirlo - -Medido en la maquina el 2026-09-03. Un shader demasiado grande **compila, enlaza y -luego no dibuja**: pantalla negra o el fotograma anterior congelado, sin ningun error -en ninguna parte -- ni en el log de GLSL ni en la interfaz de r_e_c_u_r. - -Dos puntos medidos, uno a cada lado del limite, del mismo shader: - -| | lineas de codigo | funciones | operaciones | En la Pi | -|---|---|---|---|---| -| Cinco funciones de patron | 146 | 13 | 69 | **pantalla negra** | -| Dos funciones de patron | 85 | 10 | 38 | funciona | - -Para comparar, ninguno de los 146 shaders que trae la imagen de r_e_c_u_r pasa de -**90 lineas ni de 23 operaciones**. Todos llevan un efecto por archivo, y eso no es -una decision estetica: es lo que cabe. - -Asi que cuando un shader de Shadertoy mete varios efectos distintos en un archivo -detras de un selector -- cosa muy habitual -- el port son varios shaders, no uno. Se -pierde el selector interno y se gana un mando, porque r_e_c_u_r ya deja cambiar de -shader desde la interfaz. - -`check_port.py` avisa por encima de 100 lineas o 45 operaciones. - -### Dentro de un bucle, asigna desde una funcion solo en la declaracion - -Comprobado en la maquina el 2026-09-03, despues de una biseccion larga. - -Dentro del cuerpo de un bucle, esto genera **codigo incorrecto** en el compilador -GLSL de la Pi: - -```glsl -vec3 c; -c = mi_patron(p); // asignada desde una funcion propia -``` - -El shader **compila sin un solo aviso** y se ejecuta, pero la salida deja de -depender de `u_time` y de los mandos: sale una imagen congelada, o negra. Ni el log -dice nada, ni la interfaz. - -Inicializala en la propia declaracion: - -```glsl -vec3 c = mi_patron(p); -``` - -Darle un valor por defecto en la declaracion **no basta**: el problema es la -reasignacion, no que falte el inicializador. Si tienes que elegir entre varias -funciones, mueve la eleccion a una funcion aparte para que el cuerpo del bucle -tenga exactamente una asignacion: - -```glsl -vec3 elige(int cual, vec2 p) { - if (cual == 0) return patron_a(p); - if (cual == 1) return patron_b(p); - return patron_c(p); -} -... -vec3 c = elige(cual, p); -``` - -Las tres partes de la regla cuentan, y cada una se probo por separado: - -| Dentro de un bucle | Resultado en la maquina | -|---|---| -| `vec3 c = mi_funcion(p);` | **funciona** | -| `vec3 c; c = vec3(1.0, 0.0, 0.0);` (expresion en linea) | **funciona** | -| `vec3 c; c = mi_funcion(p);` | **congelado / negro** | -| `vec3 c = vec3(0.0); c = mi_funcion(p);` | **congelado / negro** | - -Fuera de un bucle, declarar sin inicializar no da problema: 22 de los 124 shaders -que trae la imagen de r_e_c_u_r lo hacen. **Ninguno lo hace dentro de un bucle.** - -`check_port.py` detecta exactamente esa combinacion: 0 falsos positivos sobre los -141 shaders que sabemos que funcionan en un aparato real, y caza los 5 nuestros que -fallaban. - -> Esta costo dias, y merece la pena entender por que. Todas las senales apuntaban -> en la direccion equivocada: compila, se ejecuta, la interfaz muestra el shader -> como si estuviera funcionando, y el codigo es GLSL absolutamente corriente que -> funciona en cualquier escritorio. La unica forma de encontrarlo fue bisecar de -> cambio en cambio contra un shader que se sabia bueno en ese aparato concreto. - -### Un shader 0-input no puede usar `v_texcoord` - -Encontrado en la maquina el 2026-09-02, despues de dias detras de ello. Es la trampa -mas cara de todas las que nos han salido. - -Mira que geometria dibuja conjur (`ofxVideoArtTools/src/conjur.cpp`, `apply()`): - -```cpp -if(textures.size() > 0 ){ - textures[0].draw(0, 0, ofGetWidth(), ofGetHeight()); // genera texcoords -} else { - ofDrawRectangle(0, 0, ofGetWidth(), ofGetHeight()); // NO las genera -} -``` - -Sin texturas de entrada, la geometria es un `ofDrawRectangle`, que **no lleva -coordenadas de textura**. El atributo `texcoord` de `default.vert` queda indefinido, -asi que tanto `tcoord` como `v_texcoord` llegan al fragment shader **constantes**. -Todos los pixeles calculan lo mismo y sale **una pantalla de un color plano, sin -ningun mensaje de error**. - -Hay que sacar la coordenada de `gl_FragCoord`: - -```glsl -vec2 uv = gl_FragCoord.xy / u_resolution.xy; -``` - -Los shaders del repo original siguen esta regla sin haberla escrito nunca. Contado -sobre un aparato real: - -| | usa `v_texcoord` | usa `gl_FragCoord` | -|---|---|---| -| Los 5 `0-input` del repo | **0** | 5 | -| Los 12 `1-input` del repo | 12 | **0** | - -Diecisiete de diecisiete, sin excepciones. `check_port.py` ya lo detecta. - -> Por que costo tanto encontrarlo: el fallo depende del **estado en ejecucion, no del -> archivo**. Si hay un video reproduciendose, `textures` no esta vacio, la geometria -> es un cuadrilatero con textura y `v_texcoord` funciona perfectamente — el mismo -> shader se comporta bien. Por eso el mismo archivo parecia funcionar un dia y no al -> siguiente, y por eso el "color del fallo" iba cambiando: lo que se ve es lo que -> haya debajo en ese momento. - -### El archivo tiene que ser ASCII puro - -Descubierto por las malas el 2026-08-29: dos shaders que renderizaban perfectamente -en glslViewer daban **pantalla verde en el aparato, sin ningún mensaje de error**. Lo -único que tenían en común eran comentarios en español — tildes, guiones largos, -comillas tipográficas. - -Los 23 shaders que trae r_e_c_u_r son ASCII puro. Ni una tilde entre todos ellos. El -driver de la Pi es más estricto que el de un escritorio y rechaza los bytes no-ASCII -**incluso dentro de comentarios**, donde un driver de escritorio simplemente los -ignora. - -Escribe los comentarios en ASCII. `check_port.py` lo detecta. - -> Este es especialmente malo porque todas las señales apuntan en la dirección -> equivocada: compila en el portátil, el revisor lo daba por bueno, y r_e_c_u_r lo -> muestra como si estuviera corriendo. - -### r_e_c_u_r no te va a decir que el shader ha fallado - -`video_centre/shaders.py:18` documenta un estado `'!'` que significa error, pero no -hay una sola línea del código que lo asigne. Un shader que no compila se muestra -exactamente igual que uno que funciona. - -Para ver el error real de GLSL hace falta la salida estándar de c_o_n_j_u_r. Se lanza -con `subprocess.Popen` sin redirección (`actions.py:573`), así que su salida va a -donde vaya la consola de r_e_c_u_r. Por SSH: - -```bash -pkill c_o_n_j_u_r -/home/pi/openframeworks10.1/apps/myApps/c_o_n_j_u_r/bin/c_o_n_j_u_r -``` - -Luego se carga el shader desde la interfaz y se lee la terminal. openFrameworks -imprime el log completo de GLSL, con el número de línea del fallo. - -### Multipasada: el límite grande - -Shadertoy permite `Buffer A/B/C/D`: pasadas que leen su propio fotograma anterior. -Todo el feedback, los autómatas celulares, los fluidos y las simulaciones dependen de -eso. **No hay equivalente directo en un shader de r_e_c_u_r.** - -Lo que sí hay, y da mucho juego, es distinto: - -- **Las 3 capas de shader.** Se aplican en cadena, y la capa 1 recibe en `u_tex0` la - salida de la capa 0 y en `u_tex1` la entrada de la capa 0 (`ofApp.cpp:115-129`). - Son varias pasadas, pero sin memoria del fotograma anterior. -- **El modo `detour`** de r_e_c_u_r, que sí es un bucle de realimentación de vídeo. El - ajuste `SHADER_POSITION` decide si los shaders van antes o después. - -En la práctica: un shader de Shadertoy con Buffers no se porta. O se rediseña la idea -alrededor de detour, o se descarta. Dilo pronto, antes de gastar una hora traduciendo -algo que no puede funcionar. - -### Precisión - -`precision mediump float;` es lo que llevan todos los shaders del repo. En `mediump` -el rango útil es corto y `u_time` **crece sin límite**: a los pocos minutos aparecen -bandas y saltos en cualquier cosa que haga `sin(u_time * algo_grande)`. - -Dos remedios, por orden: - -1. Envolver el tiempo. Si el shader tiene un ciclo natural, - `mod(u_time, duración_del_ciclo)` da exactamente la misma secuencia sin crecer. Si - no, `mod(u_time, 100.0)` suele bastar. -2. Pedir más precisión sin romper compatibilidad: - -```glsl -#ifdef GL_ES - #ifdef GL_FRAGMENT_PRECISION_HIGH - precision highp float; - #else - precision mediump float; - #endif -#endif -``` - -**[POR VERIFICAR EN LA MÁQUINA]** si el `highp` en fragment shader está disponible y -qué cuesta. - -### Rendimiento - -Es una Raspberry Pi renderizando vídeo a la vez. Un raymarching de 128 pasos que va -suave en el navegador aquí va a trompicones. Lo que más cuesta, por orden: bucles de -antialiasing (el coste va al cuadrado — `AA 4.` son 16 muestras por píxel), -raymarching con muchos pasos, y `pow`/`log`/`atan` dentro de bucles. - -Baja esos números y **deja un comentario diciendo cómo subirlos**, para poder ajustar -el shader en la máquina sin releerlo entero. Vale más uno que corre a 25 fps que otro -más bonito a 4. - -**Medido en el aparato (2026-09-04), y cambia la forma de ajustar.** El techo son -**30.0 fps**, y es un tope *de software*, no de la pantalla: conjur hace -`ofSetFrameRate(30)` con `ofSetVerticalSync(false)` (`ofApp.cpp:11-12`). Así que un -shader que marca 30.0 está en el tope y puede que le sobre margen de GPU; por debajo de -30 lo que se mide es la máquina. El cuadro es de 720x480 porque `/boot/config.txt` trae -`sdtv_mode=16` (NTSC progresivo) y conjur toma el tamaño de la pantalla. Un shader cuyo -bucle daba 8 vueltas, con dos `smoothstep`+`distance` por vuelta, midió: - -| Vueltas del bucle | fps | Intervalo entre fotogramas | -|---|---|---| -| 8 | 13.2 | 76 ms | -| 10 | 6.6 | 152 ms | -| 12 | 4.8 | 208 ms | -| 16 | 3.9 | 256 ms | - -De ahí salen dos cosas, y ninguna se ve desde el escritorio. - -**Es un escalón, no una cuesta.** El coste por vuelta salta de 9.5 ms con 8 vueltas a -15.2 ms con 10, y luego se queda plano. Al crecer el bucle desenrollado se cruza algún -umbral de la GPU, y a partir de ahí el shader **entero** sale más caro. Así que cuando -un port va justo, «recortarlo un poco» es la maniobra equivocada: hay que averiguar de -qué lado del escalón está y ponerse en el bueno. Es el mismo tipo de umbral que el -límite de tamaño que hace que un shader no dibuje nada: primero se pone lento, más allá -desaparece. - -**Compara el intervalo entre fotogramas con la transición más corta que quieras ver.** -Si el shader tiene un destello, un golpe o cualquier ataque breve, mide cuánto dura y -ponlo al lado del intervalo. En el shader de arriba el destello duraba 111 ms: con 8 -vueltas (76 ms) se coge; con 10 (152 ms) se pierde más veces de las que se ve, y -entonces el port parece *mal hecho* en vez de lento. Una transición más corta que el -intervalo no se ve débil: **no se ve**, y nada lo avisa. - -Para tener estos números en vez de adivinarlos, ver el envoltorio de -`instrumentacion/`: cuenta fotogramas en el aparato y los escribe en el mismo log que -las cargas de shader. - -### El tutorial oficial, y en qué se aparta esta guía - -cyberboy666 escribió uno: -[tutorial_converting_simple_1input_shader_from_shadertoy](https://git.terata.org/eessppeelllloo/r_e_c_u_r/wiki/tutorial_converting_simple_1input_shader_from_shadertoy). -**Si estás portando un shader 1-input, léelo.** Es corto, es del propio autor y es lo -que sigue el resto de la comunidad. Lo que viene ahora no lo corrige —todo lo que dice -cuadra con el código fuente— sino que dibuja dónde se acaba su alcance. - -Está escrito en la **convención antigua** (`tcoord`, `tex`, `tres`, `fparams`, -`ftime`), que viene del módulo Structure de Erogenous Tones. Esa convención funciona -—conjur enlaza los dos juegos de nombres en cada frame, sin condición—, pero de los 22 -shaders que trae el propio repo de r_e_c_u_r, 20 son modernos, 1 antiguo y 1 mixto. -Esta guía usa el juego moderno para que los shaders nuevos se parezcan a ellos. - -Tres cosas suyas que conviene llevarse: - -- **Probar en el sandbox antes que en el aparato** (ver *Previsualizar sin la Pi*). Es - lo más útil que tiene, y que no estuviera en nuestras herramientas no es casualidad: - costó una sesión entera de depuración descubrir el hueco que tapa. -- **Que el mando a cero signifique "sin efecto".** Su idioma para un multiplicador es - `(1.0 + f1)` en vez de `f1`: en reposo multiplica por uno, a fondo duplica. - Multiplicar por el parámetro pelado anula la imagen en la posición de reposo. -- Los literales numéricos necesitan punto decimal: `1.0`, nunca `1`. - -Cuatro sitios donde se acaba su alcance. No son errores del tutorial, son casos que no -trata, y cada uno de ellos da pantalla plana o congelada sin ningún mensaje de error: - -- Su idea central —*"`tcoord` ya viene normalizada, no dividas por la resolución"*— - vale **solo mientras se esté dibujando una textura**. En un shader generativo sin - vídeo cargado, `conjur::apply()` cae en `ofDrawRectangle()`, que no genera - coordenadas de textura, y las dos varyings llegan constantes. Para `0-input`, - `gl_FragCoord.xy / u_resolution`. Ver *Un shader 0-input no puede usar `v_texcoord`*. -- Su plantilla declara `iparams` como "4 ints coming in". Está muerto: fijado a - `(0,0,0,0)`. -- Su `float time = float(itime) + ftime;` no es un reloj continuo, porque `itime` es - `ceil()` y no `floor()`. Usa `u_time`. -- Su shader final no lleva marca `//N-input`, y `shaders.py` sí la lee. - -Sus marcas `//f0::` son metadatos del sandbox, no de r_e_c_u_r: -`determine_shader_parameter_number` devuelve 4 sin mirar nada. - -## Parte 3 — el procedimiento - -1. **Mirar la licencia primero.** Shadertoy publica bajo - [CC BY-NC-SA 3.0](https://creativecommons.org/licenses/by-nc-sa/3.0/) por defecto, - salvo que el autor diga otra cosa en el propio código. La cláusula **NC** significa - que no se puede usar en un bolo pagado ni en nada comercial. Y ojo: r_e_c_u_r es - GPL-3.0, con la que NC **no** es compatible, así que un port derivado de material - NC no se puede aportar de vuelta al proyecto. -2. **Guardar el original** con un encabezado de atribución (autor, URL, licencia, - fecha de consulta). Si no tienes la URL, escribe `[PENDIENTE]` en vez de - inventártela: los identificadores de Shadertoy son opacos y un enlace adivinado - apunta a otro shader. -3. **Traducir lo mecánico** con la tabla de la parte 1. -4. **Pasar el revisor**, que hace ese trabajo por ti: - - ```bash - ./check_port.py mi_shader.frag --lang es - ``` - - Marca `[ROMPE]` lo que no compila en GLSL ES 1.00, `[FALTA]` los requisitos de - r_e_c_u_r que no están y `[REVISAR]` lo que depende de la máquina. No es un - compilador: que salga limpio no garantiza que compile en la Pi, pero que salga - sucio garantiza que no. -5. **Elegir los params.** Qué tres o cuatro números se convierten en mandos. -6. **Previsualizar en un escritorio antes de tocar la Pi** (ver abajo). -7. **Guardar en `shaders/N-input/`** con la marca correcta. -8. **Copiar a la máquina** y probar de verdad. - -### Previsualizar sin la Pi - -Dos herramientas, y cazan cosas distintas. Usa las dos. - -**El sandbox, para compatibilidad.** — *click to -create new shader*, pegas y compila según escribes. Es WebGL 1, que es **el mismo -perfil GLES 2.0 que corre la Pi**, así que rechaza lo que rechazaría el aparato. Eso -es justo lo que no puede hacer la previsualización de escritorio de abajo. Ojo: trae -la plantilla antigua, así que un shader escrito con `u_x0`/`u_resolution` necesita que -le adaptes los uniforms —o que los declares como variables sueltas— para compilar ahí. - -No ve el límite de tamaño ni puede reproducir las trampas que dependen del estado. -Cierra el hueco de sintaxis, no el del aparato. - -**glslViewer, para iterar sobre la forma.** El autor lo recomienda, del mismo que -escribió The Book of Shaders, y dice que los uniforms de conjur son compatibles con él -(`notes_on_shader_formats.md`). Editas, guardas y la ventana se actualiza sola. - -```bash -glslViewer shaders/0-input/mi_shader.frag -l -``` - -Con textura de entrada, para los de 1 y 2 inputs: - -```bash -glslViewer shaders/1-input/mi_shader.frag una_imagen.png -l -``` - -Para mover un parámetro, se escribe en la consola de glslViewer mientras corre: - -``` -u_x0,0.35 -``` - -Para automatizar —útil para generar capturas de varios shaders de golpe— están -`--headless` y `-E`, que ejecuta comandos y sale: - -```bash -glslViewer mi_shader.frag --headless -e "u_x0,0.6" -E "screenshot,salida.png" -``` - -### Comparar el port con el original, con numeros - -Renderizar los dos y mirarlos no basta para los cambios que importan. Saca PNG de los -dos y restalos canal a canal, pero **clava antes el tiempo dentro del shader**: - -```bash -sed -e 's/^uniform float u_time;/const float u_time = 3.0;/' \ - -e 's/^uniform float u_x\([0-3]\);/const float u_x\1 = 0.0;/' port.frag > fijado.frag -glslViewer fijado.frag --headless -E "screenshot,salida.png" -``` - -El `-e "time,3.0"` de `glslViewer` **no** lo clava: el mismo shader renderizado dos -veces al mismo tiempo nominal daba hasta 55/255 de diferencia por canal en un instante -de cambio rapido, mas que varias de las diferencias reales que queriamos medir. Con el -valor compilado dentro, dos pasadas salen identicas byte a byte y el ruido baja a cero. - -Es lo que convierte un "se ve parecido" en un "0/255 de diferencia en tres instantes, -asi que la unica desviacion es la que decidi a proposito". - -### La trampa de glslViewer: ofrece mucho más de lo que hay en la máquina - -glslViewer da unos 40 uniforms (ver `UNIFORMS.md` en su repo). conjur enlaza -**cinco**. Los únicos presentes en ambos son `u_time`, `u_resolution`, `u_tex0` y -`u_tex1`. - -Todo lo demás funciona en el escritorio y **falla en silencio en la Pi**: el uniform -no se enlaza, llega a cero, y el shader se ve congelado o plano sin dar ningún error. -Los que más se cuelan: - -| Existe en glslViewer | En r_e_c_u_r | -|---|---| -| `u_mouse` | **No.** El error número uno. Usar `u_x0`/`u_x1` | -| `u_delta`, `u_frame`, `u_date` | **No** | -| `u_pixelDensity`, `u_sceneFps`, `u_sceneMs` | **No** | -| `u_view2d` (pan/zoom con el ratón) | **No** | -| `u_camera*`, `u_light*`, `u_*Matrix` | **No** | - -Al revés también: `u_x0`…`u_x3` son de conjur, no de glslViewer. Ahí se fijan a mano -desde su consola. - -> Aviso: glslViewer en un escritorio compila con un GLSL más moderno que el de la Pi. -> Que funcione ahí **no garantiza** que compile en el aparato. Sirve para iterar sobre -> la forma. Para compatibilidad, el sandbox de arriba; la última palabra, la máquina. - -### Copiar a la máquina - -r_e_c_u_r lee shaders desde un USB montado, desde `/home/pi/r_e_c_u_r/Shaders` y desde -`/home/pi/Shaders` (`data.py:34`). Lo menos invasivo es `/home/pi/Shaders`, que deja -limpio el repo de r_e_c_u_r: - -```bash -rsync -av --dry-run shaders/ pi@IP_DE_TU_PI:/home/pi/Shaders/ -``` - -Quitar `--dry-run` cuando la lista de archivos sea la esperada. diff --git a/docs/el-aparato.md b/docs/el-aparato.md deleted file mode 100644 index 0209834..0000000 --- a/docs/el-aparato.md +++ /dev/null @@ -1,371 +0,0 @@ -# r_e_c_u_r / conjur / ofxVideoArtTools — conocimiento verificado - -Memoria **compartida entre subagentes** (no es la memoria nativa de ninguno). La -mantiene `@recur`, que solo añade o corrige algo aquí cuando lo tiene verificado contra -una fuente primaria citable (código fuente, commit, licencia confirmada). Lo que está -en proceso de verificación se queda en la memoria nativa de `@recur`, no aquí. -`@hardware` y cualquier otro subagente del medialab pueden leer esto sin tener que -repetir la investigación. - -Para el detalle completo con cita `archivo:línea` en cada afirmación, y para lo que -depende de probarse en la máquina real, la fuente más viva sigue siendo -`docs/recur-shader-reference.en.md` -y el `CLAUDE.md` de esa carpeta. Esto es el resumen para no repetir la investigación -desde cero. - -## Qué es y de quién - -**r_e_c_u_r** es un sampler/instrumento de vídeo DIY para Raspberry Pi 3 de -**cyberboy666** (mismo autor que Underscores/i_n_c_u_r). No es un solo programa, es una -cadena de tres repos: - -``` -r_e_c_u_r (Python) --OSC--> c_o_n_j_u_r (openFrameworks) --usa la clase conjur de--> ofxVideoArtTools -``` - -- `r_e_c_u_r` (Python/interfaz): https://github.com/cyberboy666/r_e_c_u_r — GPL-3.0 - (confirmado por API de GitHub y por la wiki del repo). Mirror en - git.terata.org/eessppeelllloo/r_e_c_u_r. -- `c_o_n_j_u_r` (backend openFrameworks): https://github.com/cyberboy666/c_o_n_j_u_r — - GPL-3.0, mirrado en git.terata.org. Ahí vive `notes_on_shader_formats.md`, doc del - autor de 2018 — **útil pero desactualizada en un punto**: dice que el tipo de shader - se marca con `//gen-shader`/`//pro-shader`, pero el código real - (`r_e_c_u_r/video_centre/shaders.py`, función `determine_shader_type`) busca - `//0-input`, `//1-input`, `//2-input` — que es lo que llevan todos los `.frag` reales - del repo. Usar siempre estas tres marcas, no las del documento del autor. -- `ofxVideoArtTools`: https://github.com/cyberboy666/ofxVideoArtTools, archivo - `src/conjur.cpp` (clase `conjur`) — **la fuente de verdad de los uniforms**, no está - en ninguno de los dos repos anteriores. GPL-3.0, mirrado en git.terata.org. - -## Los uniforms: las dos convenciones se enlazan las DOS, sin condición - -Verificado leyendo `ofxVideoArtTools/src/conjur.cpp` directamente. En cada `apply()` se -llama siempre a `setDefaultParams()` **y** `setAltParams()`, sin ningún `if`: - -- `setDefaultParams`: `u_time`, `u_resolution`, `u_x0..u_x3`, `u_tex0`/`u_tex1`. -- `setAltParams`: `ftime` (parte decimal de `time`), `itime` (`ceil(time)`), `tres`, - `fparams` (= el mismo array que `u_x0..u_x3`, no es un sistema aparte), `iparams` - (hardcodeado a `0,0,0,0` siempre — **está muerto**), `tex`/`tex2`. -- `u_mouse` **nunca se enlaza** — aparece en las notas del autor como truco de - desarrollo con glslViewer, pero en la máquina un shader que lo declare recibe - `(0,0)` fijo. Es el error típico de quien lee solo la doc del autor sin mirar el - código. - -Consecuencia: un shader en la convención "Structure" (`fparams`/`tres`/`tcoord`, del -módulo Eurorack de vídeo de Erogenous Tones) **sí responde** a los mandos de r_e_c_u_r, -igual que uno en la convención nativa (`u_x0..u_x3`/`u_resolution`/`u_time`/`u_tex0`). -Se recomienda esta última como convención por defecto porque `iparams` está muerto y es -la que usan todos los shaders reales del repo, no porque `fparams` no funcione. - -Los params siempre están en rango 0.0–1.0; cualquier otro rango se construye dentro del -shader. - -## Historial verificado (por qué se puede confiar en esto) - -Revisados los 8 commits que tocan `conjur.cpp` (2019-05-27 a 2025-04-04): los nombres de -uniform de arriba se fijaron todos de golpe en `4c7312c` (2019-08-03) y no han cambiado -desde entonces. La fórmula de velocidad y el reloj virtual que hacen que -`u_time`/`ftime`/`itime` vayan despacio, rápido o hacia atrás se añadieron 27 días -después, en `0483d54` (2019-08-30), y tampoco han cambiado. `u_mouse` no aparece en -ninguno de los 8 diffs — nunca se ha enlazado, en ninguna versión del archivo. - -## Otros datos verificados - -- Rutas donde r_e_c_u_r busca shaders, por orden (`data_centre/data.py:34`): USB externo - montado → `/home/pi/r_e_c_u_r/Shaders` → `/home/pi/Shaders`. -- El vertex shader está fijo en C++ (`c_o_n_j_u_r/src/ofApp.cpp:317`): siempre carga - `/home/pi/r_e_c_u_r/Shaders/default.vert`. No hay vertex shader por archivo. -- La detección automática de cuántos parámetros tiene un shader - (`shaders.py:determine_shader_parameter_number`) es código muerto (`if True: return - 4` antes de mirar el shader). r_e_c_u_r expone siempre 4 controles; los que no - existan en el shader compilado simplemente no hacen nada. -- Ramas de `r_e_c_u_r`: `master` congelada en `16e1fae` (2020-07-23, formato de shader - "clásico"); `dev` llega a `73a06be` (2022-01-11) y añade modulación de parámetros sin - cambiar el formato de shader. Cuál corre el aparato real está por confirmar en la Pi - (ver la memoria nativa de `@recur`). - -## La placa: Raspberry Pi 3B+ (2026-09-05) - -**El aparato del medialab es una Raspberry Pi 3B+**, con la imagen `recur2_0_2` -precompilada por cyberboy666. Hasta ahora las notas decían «Pi 3» a secas, deducido de la -GPU; el modelo exacto lo aportó el medialab. - -Importa para leer las medidas: la 3B+ y la 3B llevan **la misma GPU** (VideoCore IV a -400 MHz), así que los límites de shader que hemos medido valen para las dos. Lo que -cambia es la CPU (1.4 GHz frente a 1.2) y la disipación, que afectan al vídeo y al -sistema, no al fragment shader. - -Y en una Pi 4 **no valen**: otra GPU (VideoCore VI), otro compilador, otros umbrales. Si -alguna vez se prueba ahí, hay que rehacer las medidas, no extrapolarlas. - -## La GPU del aparato del medialab, con nombre y apellidos - -Verificado en el log de arranque de conjur capturado del propio aparato -(`registros/conjur-2026-09-02.log:18-22`): - -| | | -|---|---| -| `GL_RENDERER` | **VideoCore IV HW** | -| `GL_VERSION` | **OpenGL ES 2.0** | -| `GL_VENDOR` / `EGL_VENDOR` | Broadcom | -| `EGL_VERSION` | 1.4 | - -Deja de ser deducción que el lenguaje de shaders es GLSL ES 1.00: lo dice la -propia GPU. (Medido sobre la imagen 2.0.2, que es la que corre el aparato.) - -## `highp` está disponible en fragment shader - -Mismo log, `:46-50`: el shader interno de openFrameworks, que es un **fragment** -shader, se declara `precision highp float;` y el log dice -`GL_FRAGMENT_SHADER shader compiled` / `Compiled`. La especificación de OpenGL -ES 2.0 establece que usar `highp` en el lenguaje de fragmentos es un **error de -compilación** cuando no está soportado (y entonces `GL_FRAGMENT_PRECISION_HIGH` -no queda definido). Como compila, está soportado en esta máquina. - -Esto no dice nada sobre lo que cuesta, ni recomienda usarlo: 118 de los 124 -shaders que trae la imagen declaran `mediump` suelto. Sirve para saber que la -forma segura (`#ifdef GL_FRAGMENT_PRECISION_HIGH` con caída a `mediump`) tiene -sentido aquí y no es un adorno. - -**En esta maquina `mediump` NO es fp16: se comporta como fp32.** Medido el 2026-09-05 -con `plantillas/test-precision.frag`, cargado en el aparato: las **tres franjas -salen blancas**, o sea que pasa las tres comprobaciones, y la primera --distinguir -`120.0 + 0.001` de `120.0`-- exige mas de 17 bits de mantisa. fp16 tiene 11. - -> **Correccion de lo que escribi el 2026-09-05 por la manana.** Habia afirmado que la -> niebla que faltaba en `rejilla` era la normal muriendo en fp16, con la aritmetica de -> fp16 como prueba. La aritmetica era correcta pero la premisa no: **este aparato no -> usa fp16**, asi que esa no podia ser la causa. Deduje el comportamiento del hardware -> a partir del minimo que exige la especificacion, en vez de medirlo. La explicacion -> buena es la que quedo despues: el moteado del sandbox es un artefacto de la -> **precision baja del NAVEGADOR**, y la Pi dibuja limpio. - -Consecuencia practica: `highp` aqui **no hace falta** para los casos habituales. Los -ports que lo declaran (`rejilla`, `panal`, `osciloscopio`) no pierden nada por hacerlo ---el bloque `#ifdef GL_FRAGMENT_PRECISION_HIGH` con caida a `mediump` es portable-- pero -en esta maquina no cambia el resultado. En otra Pi con `mediump` de verdad en fp16, si. - -`plantillas/test-precision.frag` contesta esto en cualquier aparato en una sola carga. -Vale la pena pasarlo antes de dar por buena ninguna teoria sobre precision. - -## Complejidad de los shaders de referencia: no hay raymarching - -De los 23 `.frag` del repo de r_e_c_u_r (`Shaders/{0,1,2}-input/`), **uno solo -tiene un bucle `for`**: `1-input/simple_colorizer.frag:54`, y encima con límite -variable. Ninguno raymarchea. Los 124 de la imagen sí usan bucles, pero ninguno -pasa de 90 líneas ni de 23 operaciones matemáticas (contado en el aparato, ver -`notas-maquina.md`). - -Consecuencia práctica: para cualquier port con un bucle grande o bucles -anidados **no existe precedente conocido de que funcione en este aparato**. Hay -que medirlo, no razonarlo. - -## Ritmo de fotogramas: techo de 30 fps, puesto por conjur (medido 2026-09-04) - -Medido en el aparato con la sonda de `recur/instrumentacion` (LD_PRELOAD sobre -`eglSwapBuffers`): - -- **Reposo, sin shader: 30.0 fps exactos**, mínimo igual a máximo en 56 medidas de dos - arranques. Es un **tope de software**: `c_o_n_j_u_r/src/ofApp.cpp:11-12` hace - `framerate = 30; ofSetFrameRate(30);`, y justo antes `ofSetVerticalSync(false)`. No - tiene nada que ver con el barrido ni con la norma de vídeo. Un shader que marca 30.0 - está en el tope, no necesariamente al límite de la GPU. -- Un shader con un bucle de 8 vueltas y dos `smoothstep` con `distance` por vuelta: - **13.2 fps**. El mismo con 16 vueltas: **3.9 fps**. -- La escalera entera del mismo shader: **8 vueltas 13.2 fps, 10 vueltas 6.6, 12 - vueltas 4.8, 16 vueltas 3.9**. El coste por vuelta salta de 9.5 ms a 15.2 entre 8 y - 10 y luego se queda plano: **es un escalón, no una cuesta**. Al crecer el bucle - desenrollado se cruza un umbral de la GPU y el shader entero pasa a ser más caro. Es - el mismo tipo de umbral que el de tamaño: primero se pone lento, más allá no dibuja. - **No se puede extrapolar cuánta complejidad cabe: hay que medirla.** - -**Ojo con la norma de vídeo: el ajuste de r_e_c_u_r y el arranque de la Pi pueden no -coincidir.** En el aparato del medialab, `settings.json` dice `COMPOSITE_TYPE: PAL` y -`/boot/config.txt` dice `sdtv_mode=16`, que según el propio `actions.py:622-628` es -**NTSC progresivo** (PAL sin progresivo sería `02`). Manda `config.txt`, porque -`change_composite_setting` solo se ejecuta al tocar el menú, nunca al arrancar. De ahí -los 720x480 medidos en todos los arranques; PAL serían 720x576. El autor avisa en -`operate_docs.md:247` de que ese ajuste falla y recomienda editar `config.txt` a mano. - -Antes de dar por sabida la norma de un aparato, **mirar `config.txt`, no el menú**. Y si -se cambia a PAL: 414.720 píxeles frente a 345.600, un 20% más, o sea todos los shaders -más lentos en esa proporción, con el mismo tope de 30 fps porque el tope lo pone conjur. - -Consecuencia para portar: además de si **cabe** (límite de tamaño, ver arriba), hay que -mirar si **da tiempo**. Una transición más corta que el intervalo entre fotogramas no se -ve nunca. A 3.9 fps el intervalo son 256 ms: cualquier golpe o percusión por debajo de -eso desaparece, sin que nada falle ni avise. - -## A veces el artefacto ES el shader (2026-09-05) - -Portando `rejilla` desde el sandbox de Erogenous Tones, se echaba en falta «la -niebla/arena gris que oculta y difumina los cantos». Tras dos diagnósticos míos -equivocados, la respuesta salió de un experimento de una línea **en el propio sandbox**: -cambiar `precision mediump float;` por `highp` y mirar. El moteado desaparece y queda -un gris liso, que es exactamente lo que daba nuestro port. - -O sea: **el aspecto que gusta es un artefacto de precisión baja.** El navegador aplica -`mediump` de verdad, la normal —que se saca midiendo diferencias de 0.001— se degenera, -y ese moteado es el shader tal y como lo conoce la comunidad. La Pi no lo reproduce -porque su `mediump` se comporta mejor que el del navegador. - -Dos cosas que llevarse: - -- **Un port fiel al código puede no ser fiel a lo que se ve.** Aquí el port daba 0/255 - contra el original en el portátil y aun así no se parecía a lo que sale en el sandbox. - La fidelidad numérica frente al código fuente y la fidelidad al aspecto son cosas - distintas cuando el aspecto depende del entorno. -- **Si el efecto que se busca depende de la precisión, hay que reconstruirlo a - propósito**, no confiar en que la máquina de destino se equivoque igual. En `rejilla` - se hace sacudiendo con ruido la posición donde se mide la normal, con la sacudida - creciendo con la distancia igual que crecía el error original. Va en un mando, y sale - igual en cualquier GPU. - -**Método, que es lo que costó dos rondas:** cuando mi render y la referencia no se -parecen, el primer experimento va **en el entorno de la referencia**, cambiando una sola -cosa. Yo estuve dos rondas teorizando sobre la Pi cuando la discrepancia ya se veía -entre el sandbox y mi propio portátil, y era ahí donde había que mirar. - -## `smin` exponencial: la forma de escribirlo importa (2026-09-05) - -El `smin` exponencial que circula por todas partes —`-log(exp(-k*a) + exp(-k*b))/k`— -**desborda por los dos lados** y hay que escribirlo de otra manera. Sacando el mínimo -fuera del logaritmo: - -```glsl -smin(a,b,k) = min(a,b) - log(1.0 + exp(-k*abs(a-b))) / k -``` - -Es idéntico en matemáticas. La diferencia es numérica: el exponente ya solo va de 0 a 1, -así que no se va ni por arriba ni por abajo, y cuando se agota da exactamente `min(a,b)`, -que es el límite correcto. La forma de siempre hace `-log(0)` en cuanto las dos -exponenciales se agotan, y eso pinta **manchas blancas de borde recto**. - -Con `k = 16` y `mediump` de verdad (fp16, mínimo normal 2^-14), las exponenciales se -agotan ya con distancias **por encima de 0.607**, o sea en casi toda la pantalla. El -efecto no es un artefacto pequeño: **un `smin` que se agota es un `min`, o sea que -desaparece el difuminado**, que es justo para lo que se usa. Las formas salen cortadas -en duro y parece un problema de estilo, no de precisión. - -En el navegador no se ve, porque los drivers de escritorio tratan `mediump` como fp32. -Aun así, la forma original **también** falla en fp32 con coordenadas grandes: en el port -de `rejilla` las manchas blancas ya salen en el portátil a `time = 133.7`. - -Regla: si un shader trae este `smin`, reescribirlo al pasarlo. Es exacto, es más barato -—una exponencial en vez de dos— y quita un fallo que se diagnostica fatal. - -## El raymarching no cabe en este aparato (2026-09-05) - -Era la pregunta abierta desde el port de Remnant X, y ya está contestada con una prueba -directa. El port de `panal` se redujo de las 234 llamadas a `map()` por píxel del -original a **19**, y luego a **13**. Las dos se traban en la Pi y pierden la definición -de las figuras. - -Ese `map()` lleva dentro un `exp`, un `sin`, dos `mod` y cuatro `length`. Para comparar: -`beat_ring` con 8 vueltas de un cuerpo mucho más barato va a 13.2 fps. - -**Consecuencia práctica:** ante un shader de Shadertoy que raymarchee, no hay que -empezar bajando los pasos. Hay que rediseñar la idea en 2D desde el principio, que es lo -que funcionó en `panal`, medido a 30 fps, el techo de la máquina. Decirlo pronto -ahorra la tarde. - -## Licencias — regla de trabajo - -Los tres repos son GPL-3.0. Aparte, Shadertoy publica por defecto bajo **CC BY-NC-SA -3.0** salvo que el autor diga otra cosa en el propio código — la cláusula NC impide -usar un port en nada comercial. Antes de portar un shader: comprobar la licencia, -guardar el original con autor + URL + licencia + fecha de consulta, y avisar si el -destino previsto es comercial. - -## El tutorial oficial del autor (wiki de r_e_c_u_r) - -`https://git.terata.org/eessppeelllloo/r_e_c_u_r/wiki/tutorial_converting_simple_1input_shader_from_shadertoy` - -Es la única guía de conversión escrita por cyberboy666. Cubre **solo el caso 1-input** -(procesar el vídeo de entrada) y está escrita entera en la convención Structure. Leída -íntegra y contrastada con el código el 2026-09-04: lo que dice es correcto dentro de su -alcance. Conviene saber dónde se acaba ese alcance. - -Confirmado contra el código: - -- Las equivalencias `iChannel0→tex`, `iResolution→tres`, `fragCoord→tcoord`, - `texture→texture2D`, `fragColor→gl_FragColor`. -- Que `tcoord` ya viene normalizada y por eso **no** hay que dividir por la resolución. - Visto en el `default.vert` que volcó el log del aparato: `tcoord = texcoord;` y - `v_texcoord = texcoord;` son la misma varying duplicada, "twice because supporting - two shader formats". -- Que los literales numéricos necesitan punto decimal (`1.0`, no `1`). - -Aporta dos cosas que no teníamos: - -- **El sandbox `https://glsl.erogenous-tones.com` como paso previo al aparato.** Es - WebGL 1, o sea el mismo perfil GLES 2.0 que la Pi. glslViewer en el portátil compila - GLSL de escritorio y por eso acepta cosas que la máquina rechaza; el sandbox no. Vivo - a fecha de 2026-09-04 (HTTP 200). -- El idioma `float f0 = mix(0.0, 1.0, fparams[0]);` y, más importante, el criterio de - diseño de mandos: **que el 0 del mando sea "sin efecto"**. De ahí que multiplique por - `(1.0 + f1)` en vez de por `f1`. - -Dónde se acaba su alcance (no son errores suyos, son casos que no trata): - -- Su consejo de usar `tcoord` sin dividir vale **mientras se esté dibujando una - textura**. En un shader generativo sin vídeo cargado, `conjur::apply()` llama a - `ofDrawRectangle()`, que no genera texcoords, y `tcoord`/`v_texcoord` llegan - constantes: pantalla plana y ningún error. Para 0-input, `gl_FragCoord.xy / - u_resolution`. El límite no lo marca la etiqueta `//N-input`, lo marca que haya o no - textura cargada. -- Su plantilla declara `iparams` como "4 ints coming in". Está muerto: siempre `(0,0,0,0)`. -- Su línea `float time = float(itime) + ftime;` no da un reloj continuo, porque `itime` - es `ceil()` y no `floor()`. Para tiempo, `u_time`. -- Su shader final no lleva marca `//N-input`, y `shaders.py:51` sí la lee. -- No trata ninguna de las cuatro trampas medidas en el aparato (tamaño, no-ASCII, - reasignación desde función dentro de bucle, 0-input con varying). - -### La wiki entera está clonada - -La wiki no viaja en `git clone` del repo: es un repo aparte. Clonada el 2026-09-04 en -`https://github.com/cyberboy666/r_e_c_u_r.wiki.git`, **27 -páginas**, de las que trece hablan de shaders. Antes de investigar algo de r_e_c_u_r, -mirar ahí primero. - -Dos cosas nuestras que la wiki confirma desde arriba: - -- **`u_mouse` nunca existió, y ahora se sabe por qué.** - `logs_of_feature_exploring.md:511`: cyberboy666 explica que en i_n_c_u_r/c_o_n_j_u_r - la entrada solía ser la posición del ratón, y que para r_e_c_u_r la sustituyó por - «normalized parameter inputs» precisamente para poder gobernarla con mandos y CV. Los - `u_x0..u_x3` son el reemplazo deliberado del ratón, no un añadido. En la 2.1.0 el - ratón vuelve, pero **mapeado a x0/x1 como params**, no como uniform. -- **La etiqueta de carpeta no enlaza texturas.** En las notas de la 2.1.0, sobre el - shader nuevo `spotlight`: «is an 1input (despite being filed in the 0input folder - whoop)». Funciona igual, que es exactamente lo que predice `shaders.py:76` al mandar - por OSC solo el booleano `shad_type == '2in'`. - -Sobre la 2.1.0, de sus notas de versión (sigue **sin verificar en aparato**): añade -control por OSC y punto de acceso wifi, servidor web `r_e_m_o_t_e`, menú de plugins, -MIDI sin lag a 8 bits, expulsión de USB desde ajustes, y seis shaders nuevos (lumakey -blanco y negro, chromakey, dos `displace`, `spotlight` y `rgb_pallet`). Nada de eso -toca el lenguaje de shaders, así que las cuatro trampas medidas son **plausiblemente** -válidas también ahí; plausible no es verificado. - -Dos detalles de marcas, verificados en `video_centre/shaders.py`: - -- `//f0::`, `//f1::`, `//f2::` son metadatos del sandbox, no de r_e_c_u_r. - `determine_shader_parameter_number` tiene un `if True: return 4`: devuelve cuatro - params siempre, mire lo que mire el shader. -- `//N-input` sí se lee, pero solo distingue de verdad el caso 2-input. `shaders.py:76` - manda por OSC `shad_type == '2in'`, un booleano; `//0-input` y `//1-input` llegan a - conjur exactamente iguales. - -## Última actualización -2026-09-04 (segunda) — leído y contrastado el tutorial oficial de la wiki, que hasta -ahora solo estaba citado de pasada. De ahí sale la sección anterior y, sobre todo, el -sandbox como paso de verificación previo al aparato. Clonada además la wiki completa al -tirar del hilo: 27 páginas que no habíamos abierto. - -2026-09-04 — añadidos, releyendo el log de conjur ya capturado (sin volver a tocar el -aparato): la identidad de la GPU y del driver, que `highp` está disponible en fragment -shader, y que ninguno de los shaders de referencia hace raymarching. - -2026-09-03 — migrado desde la memoria nativa de `@hardware` a este archivo compartido, -al crear el subagente especializado `@recur`. diff --git a/docs/l-aparell.md b/docs/l-aparell.md new file mode 100644 index 0000000..0dc16f8 --- /dev/null +++ b/docs/l-aparell.md @@ -0,0 +1,360 @@ +# r_e_c_u_r / conjur / ofxVideoArtTools — coneixement verificat + +Aquest document recull allò que està **verificat contra una font primària citable** +(codi font, commit, llicència confirmada). El que encara està en procés de verificació no +hi entra. + +Per al detall complet amb cita `fitxer:línia` a cada afirmació, i per a allò que depèn de +provar-se a la màquina real, la font més viva segueix sent +`docs/recur-shader-reference.en.md`. Això és el resum per no repetir la recerca des de +zero. + +## Què és i de qui + +**r_e_c_u_r** és un sampler/instrument de vídeo DIY per a Raspberry Pi 3 de +**cyberboy666** (mateix autor que Underscores/i_n_c_u_r). No és un sol programa, és una +cadena de tres repositoris: + +``` +r_e_c_u_r (Python) --OSC--> c_o_n_j_u_r (openFrameworks) --fa servir la classe conjur de--> ofxVideoArtTools +``` + +- `r_e_c_u_r` (Python/interfície): https://github.com/cyberboy666/r_e_c_u_r — GPL-3.0 + (confirmat per l'API de GitHub i per la wiki del repositori). Mirall a + git.terata.org/eessppeelllloo/r_e_c_u_r. +- `c_o_n_j_u_r` (rerefons openFrameworks): https://github.com/cyberboy666/c_o_n_j_u_r — + GPL-3.0, mirallat a git.terata.org. Allà hi viu `notes_on_shader_formats.md`, document + de l'autor del 2018 — **útil però desactualitzat en un punt**: diu que el tipus de + shader es marca amb `//gen-shader`/`//pro-shader`, però el codi real + (`r_e_c_u_r/video_centre/shaders.py`, funció `determine_shader_type`) busca + `//0-input`, `//1-input`, `//2-input` — que és el que porten tots els `.frag` reals del + repositori. Fer servir sempre aquestes tres marques, no les del document de l'autor. +- `ofxVideoArtTools`: https://github.com/cyberboy666/ofxVideoArtTools, fitxer + `src/conjur.cpp` (classe `conjur`) — **la font de veritat dels uniforms**, no és a cap + dels dos repositoris anteriors. GPL-3.0, mirallat a git.terata.org. + +## Els uniforms: les dues convencions s'enllacen TOTES DUES, sense condició + +Verificat llegint `ofxVideoArtTools/src/conjur.cpp` directament. A cada `apply()` es crida +sempre `setDefaultParams()` **i** `setAltParams()`, sense cap `if`: + +- `setDefaultParams`: `u_time`, `u_resolution`, `u_x0..u_x3`, `u_tex0`/`u_tex1`. +- `setAltParams`: `ftime` (part decimal de `time`), `itime` (`ceil(time)`), `tres`, + `fparams` (= el mateix array que `u_x0..u_x3`, no és un sistema a part), `iparams` + (fixat a `0,0,0,0` sempre — **està mort**), `tex`/`tex2`. +- `u_mouse` **no s'enllaça mai** — apareix a les notes de l'autor com a truc de + desenvolupament amb glslViewer, però a la màquina un shader que el declari rep `(0,0)` + fix. És l'error típic de qui llegeix només la documentació de l'autor sense mirar el + codi. + +Conseqüència: un shader en la convenció «Structure» (`fparams`/`tres`/`tcoord`, del mòdul +Eurorack de vídeo d'Erogenous Tones) **sí que respon** als comandaments de r_e_c_u_r, +igual que un en la convenció nativa (`u_x0..u_x3`/`u_resolution`/`u_time`/`u_tex0`). Es +recomana aquesta última com a convenció per defecte perquè `iparams` està mort i és la que +fan servir tots els shaders reals del repositori, no perquè `fparams` no funcioni. + +Els params sempre són en rang 0.0–1.0; qualsevol altre rang es construeix dins del shader. + +## Historial verificat (per què es pot confiar en això) + +Revisats els 8 commits que toquen `conjur.cpp` (2019-05-27 a 2025-04-04): els noms +d'uniform de dalt es van fixar tots de cop a `4c7312c` (2019-08-03) i no han canviat des +d'aleshores. La fórmula de velocitat i el rellotge virtual que fan que +`u_time`/`ftime`/`itime` vagin a poc a poc, ràpid o enrere es van afegir 27 dies després, +a `0483d54` (2019-08-30), i tampoc no han canviat. `u_mouse` no apareix a cap dels 8 +diffs — no s'ha enllaçat mai, en cap versió del fitxer. + +## Altres dades verificades + +- Rutes on r_e_c_u_r busca shaders, per ordre (`data_centre/data.py:34`): USB extern + muntat → `/home/pi/r_e_c_u_r/Shaders` → `/home/pi/Shaders`. +- El vertex shader està fixat al C++ (`c_o_n_j_u_r/src/ofApp.cpp:317`): sempre carrega + `/home/pi/r_e_c_u_r/Shaders/default.vert`. No hi ha vertex shader per fitxer. +- La detecció automàtica de quants paràmetres té un shader + (`shaders.py:determine_shader_parameter_number`) és codi mort (`if True: return 4` abans + de mirar el shader). r_e_c_u_r exposa sempre 4 controls; els que no existeixin al shader + compilat simplement no fan res. +- Branques de `r_e_c_u_r`: `master` congelada a `16e1fae` (2020-07-23, format de shader + «clàssic»); `dev` arriba a `73a06be` (2022-01-11) i afegeix modulació de paràmetres + sense canviar el format de shader. Quina corre l'aparell real està per confirmar a la + Pi. + +## La placa: Raspberry Pi 3B+ (2026-09-05) + +**L'aparell de referència és una Raspberry Pi 3B+**, amb la imatge `recur2_0_2` +precompilada per cyberboy666. Fins ara les notes deien «Pi 3» a seques, deduït de la GPU. + +Importa per llegir les mesures: la 3B+ i la 3B porten **la mateixa GPU** (VideoCore IV a +400 MHz), així que els límits de shader que hem mesurat valen per a totes dues. El que +canvia és la CPU (1.4 GHz davant d'1.2) i la dissipació, que afecten el vídeo i el +sistema, no el fragment shader. + +I en una Pi 4 **no valen**: una altra GPU (VideoCore VI), un altre compilador, uns altres +llindars. Si mai es prova allà, cal refer les mesures, no extrapolar-les. + +## La GPU de l'aparell, amb nom i cognoms + +Verificat al registre d'arrencada de conjur capturat del mateix aparell (2026-09-02, +línies 18-22): + +| | | +|---|---| +| `GL_RENDERER` | **VideoCore IV HW** | +| `GL_VERSION` | **OpenGL ES 2.0** | +| `GL_VENDOR` / `EGL_VENDOR` | Broadcom | +| `EGL_VERSION` | 1.4 | + +Deixa de ser deducció que el llenguatge de shaders és GLSL ES 1.00: ho diu la mateixa GPU. +(Mesurat sobre la imatge 2.0.2, que és la que corre l'aparell.) + +## `highp` està disponible en fragment shader + +Mateix registre, línies 46-50: el shader intern d'openFrameworks, que és un **fragment** +shader, es declara `precision highp float;` i el registre diu +`GL_FRAGMENT_SHADER shader compiled` / `Compiled`. L'especificació d'OpenGL ES 2.0 +estableix que fer servir `highp` al llenguatge de fragments és un **error de compilació** +quan no està suportat (i aleshores `GL_FRAGMENT_PRECISION_HIGH` no queda definit). Com que +compila, està suportat en aquesta màquina. + +Això no diu res sobre el que costa, ni recomana fer-lo servir: 118 dels 124 shaders que +porta la imatge declaren `mediump` sol. Serveix per saber que la forma segura +(`#ifdef GL_FRAGMENT_PRECISION_HIGH` amb caiguda a `mediump`) té sentit aquí i no és un +adorn. + +**En aquesta maquina `mediump` NO es fp16: es comporta com fp32.** Mesurat el 2026-09-05 +amb `plantilles/test-precision.frag`, carregat a l'aparell: les **tres franges surten +blanques**, o sia que passa les tres comprovacions, i la primera --distingir +`120.0 + 0.001` de `120.0`-- exigeix mes de 17 bits de mantissa. fp16 en te 11. + +> **Correccio del que es va escriure el 2026-09-05 al mati.** S'havia afirmat que la boira +> que faltava a `rejilla` era la normal morint en fp16, amb l'aritmetica de fp16 com a +> prova. L'aritmetica era correcta pero la premissa no: **aquest aparell no fa servir +> fp16**, aixi que aquella no podia ser la causa. Es va deduir el comportament del +> maquinari a partir del minim que exigeix l'especificacio, en comptes de mesurar-lo. +> L'explicacio bona es la que va quedar despres: el clapejat del sandbox es un artefacte de +> la **precisio baixa del NAVEGADOR**, i la Pi dibuixa net. + +Conseqüència pràctica: `highp` aquí **no fa falta** per als casos habituals. Els ports que +el declaren no hi perden res —el bloc `#ifdef GL_FRAGMENT_PRECISION_HIGH` amb caiguda a +`mediump` és portable— però en aquesta màquina no canvia el resultat. En una altra Pi amb +`mediump` de debò en fp16, sí. + +`plantilles/test-precision.frag` contesta això en qualsevol aparell en una sola càrrega. +Val la pena passar-lo abans de donar per bona cap teoria sobre precisió. + +## Complexitat dels shaders de referència: no hi ha raymarching + +Dels 23 `.frag` del repositori de r_e_c_u_r (`Shaders/{0,1,2}-input/`), **només un té un +bucle `for`**: `1-input/simple_colorizer.frag:54`, i a sobre amb límit variable. Cap no +marxa raigs. Els 124 de la imatge sí que fan servir bucles, però cap no passa de 90 línies +ni de 23 operacions matemàtiques (comptat a l'aparell). + +Conseqüència pràctica: per a qualsevol port amb un bucle gran o bucles imbricats **no +existeix precedent conegut que funcioni en aquest aparell**. Cal mesurar-ho, no raonar-ho. + +## Ritme de fotogrames: sostre de 30 fps, posat per conjur (mesurat 2026-09-04) + +Mesurat a l'aparell amb la sonda d'`instrumentacio/` (LD_PRELOAD sobre `eglSwapBuffers`): + +- **Repòs, sense shader: 30.0 fps exactes**, mínim igual a màxim en 56 mesures de dues + arrencades. És un **topall de programari**: `c_o_n_j_u_r/src/ofApp.cpp:11-12` fa + `framerate = 30; ofSetFrameRate(30);`, i just abans `ofSetVerticalSync(false)`. No té res + a veure amb l'escombratge ni amb la norma de vídeo. Un shader que marca 30.0 és al + topall, no necessàriament al límit de la GPU. +- Un shader amb un bucle de 8 voltes i dos `smoothstep` amb `distance` per volta: + **13.2 fps**. El mateix amb 16 voltes: **3.9 fps**. +- L'escala sencera del mateix shader: **8 voltes 13.2 fps, 10 voltes 6.6, 12 voltes 4.8, + 16 voltes 3.9**. El cost per volta salta de 9.5 ms a 15.2 entre 8 i 10 i després es queda + pla: **és un graó, no una costa**. En créixer el bucle desenrotllat es creua un llindar + de la GPU i el shader sencer passa a ser més car. És el mateix tipus de llindar que el de + mida: primer es posa lent, més enllà no dibuixa. **No es pot extrapolar quanta + complexitat hi cap: cal mesurar-la.** + +**Compte amb la norma de vídeo: l'ajust de r_e_c_u_r i l'arrencada de la Pi poden no +coincidir.** A l'aparell de referència, `settings.json` diu `COMPOSITE_TYPE: PAL` i +`/boot/config.txt` diu `sdtv_mode=16`, que segons el mateix `actions.py:622-628` és **NTSC +progressiu** (PAL sense progressiu seria `02`). Mana `config.txt`, perquè +`change_composite_setting` només s'executa en tocar el menú, mai en arrencar. D'aquí els +720x480 mesurats a totes les arrencades; PAL serien 720x576. L'autor avisa a +`operate_docs.md:247` que aquell ajust falla i recomana editar `config.txt` a mà. + +Abans de donar per sabuda la norma d'un aparell, **mirar `config.txt`, no el menú**. I si +es canvia a PAL: 414.720 píxels davant de 345.600, un 20% més, o sia tots els shaders més +lents en aquella proporció, amb el mateix topall de 30 fps perquè el topall el posa conjur. + +Conseqüència per portar: a més de si **hi cap** (límit de mida, vegeu més amunt), cal +mirar si **hi ha temps**. Una transició més curta que l'interval entre fotogrames no es veu +mai. A 3.9 fps l'interval són 256 ms: qualsevol cop o percussió per sota d'això desapareix, +sense que res falli ni avisi. + +## De vegades l'artefacte ÉS el shader (2026-09-05) + +Portant `rejilla` des del sandbox d'Erogenous Tones, es trobava a faltar «la boira/sorra +grisa que amaga i difumina els cantons». Després de dos diagnòstics equivocats, la resposta +va sortir d'un experiment d'una línia **al mateix sandbox**: canviar +`precision mediump float;` per `highp` i mirar. El clapejat desapareix i queda un gris +llis, que és exactament el que donava el nostre port. + +O sia: **l'aspecte que agrada és un artefacte de precisió baixa.** El navegador aplica +`mediump` de debò, la normal —que es treu mesurant diferències de 0.001— es degenera, i +aquell clapejat és el shader tal com el coneix la comunitat. La Pi no el reprodueix perquè +el seu `mediump` es comporta millor que el del navegador. + +Dues coses per endur-se: + +- **Un port fidel al codi pot no ser fidel al que es veu.** Aquí el port donava 0/255 + contra l'original al portàtil i tot i així no s'assemblava al que surt al sandbox. La + fidelitat numèrica davant del codi font i la fidelitat a l'aspecte són coses diferents + quan l'aspecte depèn de l'entorn. +- **Si l'efecte que es busca depèn de la precisió, cal reconstruir-lo a propòsit**, no + confiar que la màquina de destí s'equivoqui igual. A `rejilla` es fa sacsejant amb soroll + la posició on es mesura la normal, amb la sacsejada creixent amb la distància igual que + creixia l'error original. Va en un comandament, i surt igual en qualsevol GPU. + +**Mètode, que és el que va costar dues rondes:** quan el meu render i la referència no +s'assemblen, el primer experiment va **a l'entorn de la referència**, canviant una sola +cosa. Es van perdre dues rondes teoritzant sobre la Pi quan la discrepància ja es veia +entre el sandbox i el portàtil, i era allà on calia mirar. + +## `smin` exponencial: la forma d'escriure'l importa (2026-09-05) + +El `smin` exponencial que circula per tot arreu —`-log(exp(-k*a) + exp(-k*b))/k`— +**desborda pels dos costats** i s'ha d'escriure d'una altra manera. Traient el mínim fora +del logaritme: + +```glsl +smin(a,b,k) = min(a,b) - log(1.0 + exp(-k*abs(a-b))) / k +``` + +És idèntic en matemàtiques. La diferència és numèrica: l'exponent ja només va de 0 a 1, +així que no se'n va ni per dalt ni per baix, i quan s'esgota dona exactament `min(a,b)`, +que és el límit correcte. La forma de sempre fa `-log(0)` així que les dues exponencials +s'esgoten, i això pinta **taques blanques de vora recta**. + +Amb `k = 16` i `mediump` de debò (fp16, mínim normal 2^-14), les exponencials s'esgoten ja +amb distàncies **per sobre de 0.607**, o sia en gairebé tota la pantalla. L'efecte no és un +artefacte petit: **un `smin` que s'esgota és un `min`, o sia que desapareix el difuminat**, +que és justament per al que es fa servir. Les formes surten tallades en dur i sembla un +problema d'estil, no de precisió. + +Al navegador no es veu, perquè els controladors d'escriptori tracten `mediump` com fp32. +Tot i així, la forma original **també** falla en fp32 amb coordenades grans: al port de +`rejilla` les taques blanques ja surten al portàtil a `time = 133.7`. + +Regla: si un shader porta aquest `smin`, reescriure'l en passar-lo. És exacte, és més barat +—una exponencial en comptes de dues— i treu una fallada que es diagnostica fatal. + +## El raymarching no hi cap en aquest aparell (2026-09-05) + +Era la pregunta oberta des del port de Remnant X, i ja està contestada amb una prova +directa. El port de `panal` es va reduir de les 234 crides a `map()` per píxel de +l'original a **19**, i després a **13**. Totes dues s'encallen a la Pi i perden la +definició de les figures. + +Aquell `map()` porta a dins un `exp`, un `sin`, dos `mod` i quatre `length`. Per comparar: +`beat_ring` amb 8 voltes d'un cos molt més barat va a 13.2 fps. + +**Conseqüència pràctica:** davant d'un shader de Shadertoy que marxi raigs, no cal començar +abaixant els passos. Cal redissenyar la idea en 2D des del principi, que és el que va +funcionar a `panal`, mesurat a 30 fps, el sostre de la màquina. Dir-ho aviat estalvia la +tarda. + +## Llicències — regla de treball + +Els tres repositoris són GPL-3.0. A part, Shadertoy publica per defecte sota **CC BY-NC-SA +3.0** llevat que l'autor digui una altra cosa al mateix codi — la clàusula NC impedeix fer +servir un port en res comercial. Abans de portar un shader: comprovar la llicència, guardar +l'original amb autor + URL + llicència + data de consulta, i avisar si el destí previst és +comercial. + +## El tutorial oficial de l'autor (wiki de r_e_c_u_r) + +`https://git.terata.org/eessppeelllloo/r_e_c_u_r/wiki/tutorial_converting_simple_1input_shader_from_shadertoy` + +És l'única guia de conversió escrita per cyberboy666. Cobreix **només el cas 1-input** +(processar el vídeo d'entrada) i està escrita sencera en la convenció Structure. Llegida +íntegra i contrastada amb el codi el 2026-09-04: el que diu és correcte dins del seu abast. +Convé saber on s'acaba aquell abast. + +Confirmat contra el codi: + +- Les equivalències `iChannel0→tex`, `iResolution→tres`, `fragCoord→tcoord`, + `texture→texture2D`, `fragColor→gl_FragColor`. +- Que `tcoord` ja ve normalitzada i que per això **no** cal dividir per la resolució. Vist + al `default.vert` que va abocar el registre de l'aparell: `tcoord = texcoord;` i + `v_texcoord = texcoord;` són la mateixa varying duplicada, «twice because supporting two + shader formats». +- Que els literals numèrics necessiten punt decimal (`1.0`, no `1`). + +Aporta dues coses que no teníem: + +- **El sandbox `https://glsl.erogenous-tones.com` com a pas previ a l'aparell.** És WebGL + 1, o sia el mateix perfil GLES 2.0 que la Pi. glslViewer al portàtil compila GLSL + d'escriptori i per això accepta coses que la màquina rebutja; el sandbox no. Viu a data + del 2026-09-04 (HTTP 200). +- L'idioma `float f0 = mix(0.0, 1.0, fparams[0]);` i, més important, el criteri de disseny + de comandaments: **que el 0 del comandament sigui «sense efecte»**. D'aquí que multipliqui + per `(1.0 + f1)` en comptes de per `f1`. + +On s'acaba el seu abast (no són errors seus, són casos que no tracta): + +- El seu consell de fer servir `tcoord` sense dividir val **mentre s'estigui dibuixant una + textura**. En un shader generatiu sense vídeo carregat, `conjur::apply()` crida + `ofDrawRectangle()`, que no genera texcoords, i `tcoord`/`v_texcoord` arriben constants: + pantalla plana i cap error. Per a 0-input, `gl_FragCoord.xy / u_resolution`. El límit no + el marca l'etiqueta `//N-input`, el marca que hi hagi o no textura carregada. +- La seva plantilla declara `iparams` com a «4 ints coming in». Està mort: sempre + `(0,0,0,0)`. +- La seva línia `float time = float(itime) + ftime;` no dona un rellotge continu, perquè + `itime` és `ceil()` i no `floor()`. Per a temps, `u_time`. +- El seu shader final no porta marca `//N-input`, i `shaders.py:51` sí que la llegeix. +- No tracta cap dels quatre paranys mesurats a l'aparell (mida, no-ASCII, reassignació des + de funció dins de bucle, 0-input amb varying). + +### La wiki sencera es pot clonar + +La wiki no viatja en un `git clone` del repositori: és un repositori a part. Clonada el +2026-09-04 des de `https://github.com/cyberboy666/r_e_c_u_r.wiki.git`, **27 pàgines**, de +les quals tretze parlen de shaders. Abans d'investigar res de r_e_c_u_r, mirar-hi primer. + +Dues coses nostres que la wiki confirma des de dalt: + +- **`u_mouse` no va existir mai, i ara se sap per què.** + `logs_of_feature_exploring.md:511`: cyberboy666 explica que a i_n_c_u_r/c_o_n_j_u_r + l'entrada solia ser la posició del ratolí, i que per a r_e_c_u_r la va substituir per + «normalized parameter inputs» precisament per poder governar-la amb comandaments i CV. + Els `u_x0..u_x3` són el reemplaçament deliberat del ratolí, no un afegit. A la 2.1.0 el + ratolí torna, però **mapat a x0/x1 com a params**, no com a uniform. +- **L'etiqueta de carpeta no enllaça textures.** A les notes de la 2.1.0, sobre el shader + nou `spotlight`: «is an 1input (despite being filed in the 0input folder whoop)». + Funciona igual, que és exactament el que prediu `shaders.py:76` en enviar per OSC només + el booleà `shad_type == '2in'`. + +Sobre la 2.1.0, de les seves notes de versió (segueix **sense verificar en aparell**): +afegeix control per OSC i punt d'accés wifi, servidor web `r_e_m_o_t_e`, menú de +connectors, MIDI sense retard a 8 bits, expulsió d'USB des d'ajustos, i sis shaders nous +(lumakey blanc i negre, chromakey, dos `displace`, `spotlight` i `rgb_pallet`). Res d'això +toca el llenguatge de shaders, així que els quatre paranys mesurats són **plausiblement** +vàlids també allà; plausible no és verificat. + +Dos detalls de marques, verificats a `video_centre/shaders.py`: + +- `//f0::`, `//f1::`, `//f2::` són metadades del sandbox, no de r_e_c_u_r. + `determine_shader_parameter_number` té un `if True: return 4`: retorna quatre params + sempre, miri el que miri el shader. +- `//N-input` sí que es llegeix, però només distingeix de debò el cas 2-input. + `shaders.py:76` envia per OSC `shad_type == '2in'`, un booleà; `//0-input` i `//1-input` + arriben a conjur exactament iguals. + +## Última actualització + +2026-09-04 (segona) — llegit i contrastat el tutorial oficial de la wiki, que fins ara +només estava citat de passada. D'aquí surt la secció anterior i, sobretot, el sandbox com a +pas de verificació previ a l'aparell. Clonada a més la wiki completa en estirar del fil: 27 +pàgines que no havíem obert. + +2026-09-04 — afegits, rellegint el registre de conjur ja capturat (sense tornar a tocar +l'aparell): la identitat de la GPU i del controlador, que `highp` està disponible en +fragment shader, i que cap dels shaders de referència no fa raymarching. + +2026-09-03 — migrat a un fitxer compartit en crear el subagent especialitzat `recur`. diff --git a/docs/recur-shader-reference.ca.md b/docs/recur-shader-reference.ca.md new file mode 100644 index 0000000..4018a7c --- /dev/null +++ b/docs/recur-shader-reference.ca.md @@ -0,0 +1,287 @@ +--- +doc: RECUR-SHADER-REF +rev: 03 +canonical_language: en +canonical_source: docs/recur-shader-reference.en.md +translations: + - ca: docs/recur-shader-reference.ca.md (rev 03, al dia) +--- + +# Com funcionen de debò els shaders a r_e_c_u_r + +Tot el que hi ha aquí està comprovat **llegint el codi font** dels tres repositoris +implicats, no llegint fòrums. Cada afirmació porta `fitxer:línia`. Allò que no es pot +tancar llegint codi va marcat com a **[PER VERIFICAR A LA MÀQUINA]**. + +Verificat el 2026-08-28 contra el commit `16e1fae` de r_e_c_u_r. + +## La cadena: qui dibuixa què + +No és un sol programa. Són tres peces, i confondre-les fa perdre el temps: + +``` +r_e_c_u_r (Python, la interfície) + │ navega Shaders/, llegeix el fitxer, decideix què enviar + │ OSC → port local + ▼ +c_o_n_j_u_r (aplicació d'openFrameworks, C++) + │ rep /shader/N/load, /param, /speed + ▼ +la classe conjur (addon ofxVideoArtTools) + │ fa shader.begin(), enllaça els uniforms, dibuixa + ▼ +OpenGL ES 2.0 / GLSL ES 1.00 ← aquí corre el teu .frag +``` + +- r_e_c_u_r **no renderitza res**. Només envia missatges OSC. +- Els uniforms s'enllacen a `ofxVideoArtTools/src/conjur.cpp`. Aquell fitxer és la font + de veritat sobre quines variables existeixen. +- Repositoris: [r_e_c_u_r](https://github.com/cyberboy666/r_e_c_u_r), + [c_o_n_j_u_r](https://github.com/cyberboy666/c_o_n_j_u_r), + [ofxVideoArtTools](https://github.com/cyberboy666/ofxVideoArtTools). Tots tres + GPL-3.0. + +## On van els fitxers + +`data_centre/data.py:34` — r_e_c_u_r busca shaders en aquest ordre: + +1. L'USB o dispositiu extern muntat (`PATH_TO_EXTERNAL_DEVICES`) +2. `/home/pi/r_e_c_u_r/Shaders` +3. `/home/pi/Shaders` + +Extensions que mostra el navegador: `.frag`, `.shader`, `.glsl`, `.glslf`, `.fsh`. + +**El vertex shader està fixat al codi.** `ofApp.cpp:317` carrega sempre +`/home/pi/r_e_c_u_r/Shaders/default.vert`, passi el que passi. No pots donar el teu propi +vertex shader sense tocar C++. Només escrivim fragment shaders. + +## Els uniforms que existeixen de debò + +De `ofxVideoArtTools/src/conjur.cpp:50-73`. Aquesta és la llista completa; **no n'hi ha +més**. + +### Els que fem servir (format Book of Shaders) + +| Uniform | Tipus | Què és | Font | +|---|---|---|---| +| `u_time` | `float` | Segons des que va arrencar el shader, **escalat per la velocitat** | `conjur.cpp:51` | +| `u_resolution` | `vec2` | Mida de la finestra en píxels | `conjur.cpp:52` | +| `u_x0`…`u_x3` | `float` | Els quatre paràmetres de la interfície. **Sempre 0.0–1.0** | `conjur.cpp:54` | +| `u_tex0` | `sampler2D` | Textura d'entrada (vídeo, o sortida del shader anterior) | `conjur.cpp:57` | +| `u_tex1` | `sampler2D` | Segona entrada, si existeix a la cadena | `conjur.cpp:57` | + +I del `default.vert`, disponibles com a varyings: + +| Varying | Tipus | Què és | +|---|---|---| +| `v_texcoord` | `vec2` | Coordenada UV normalitzada, 0.0–1.0. **És el que es fa servir sempre** | +| `tcoord` | `vec2` | Àlies exacte de `v_texcoord` (suport de dos formats) | +| `v_position`, `v_color`, `v_normal` | | Existeixen, però els shaders del repositori no els fan servir | + +### Els legacy (existeixen, però no els facis servir en codi nou) + +`conjur.cpp:61-73` enllaça a més un joc de noms més antic: + +| Uniform | Tipus | Què és | +|---|---|---| +| `ftime` | `float` | Només la part decimal de `u_time` — va de 0 a 1 i salta. **No és un rellotge continu** | +| `itime` | `int` | `ceil(u_time)`, comptador de segons enters | +| `tres` | `vec2` | Igual que `u_resolution` | +| `fparams` | `vec4` | Els quatre params junts: `fparams[0]` == `u_x0` | +| `iparams` | `ivec4` | **Sempre `(0,0,0,0)`. Està mort** — `conjur.cpp:66` el fixa a zero | +| `tex`, `tex2` | `sampler2D` | Àlies d'`u_tex0` i `u_tex1` | + +Compte amb `ftime`: `Shaders/0-input/colour_sine.frag` el fa servir i és fàcil copiar-lo +creient que és el rellotge. Serra cada segon. + +### D'on surt aquest joc de noms antic + +No és un invent de r_e_c_u_r: és la convenció nativa del **Structure**, el mòdul Eurorack +de vídeo d'Erogenous Tones. El `default.vert` declara `tcoord` i `v_texcoord` alhora, amb +el comentari literal `// twice because supporting two shader formats`, precisament per +servir les dues comunitats. + +Es veu clar a l'únic tutorial de conversió que hi ha a la wiki oficial, +[tutorial_converting_simple_1input_shader_from_shadertoy](https://git.terata.org/eessppeelllloo/r_e_c_u_r/wiki/tutorial_converting_simple_1input_shader_from_shadertoy): +està escrit sencer en la convenció antiga, parla dels «noms que structure/recur esperen» +i envia al sandbox d'Erogenous Tones. + +**Els dos formats funcionen de debò.** `conjur.cpp:50-73` enllaça els dos jocs sense +condicions, i com que `fparams` s'alimenta del mateix array `shaderParams` que +`u_x0..u_x3`, un shader escrit a l'estil antic també respon als comandaments. Tot i així, +en codi nou convé el joc modern per tres raons concretes: `iparams` està mort, `ftime` no +és un rellotge continu, i el modern és el que fan servir els shaders d'exemple del +repositori. + +> Si segueixes el tutorial de la wiki: reconstrueix el temps amb +> `float time = float(itime) + ftime;`. Com que `itime` és `ceil()` i no `floor()`, això +> va gairebé un segon **avançat** respecte a `u_time`. Tant se val per a un efecte +> visual, però no és el mateix rellotge. + +### El que NO existeix + +- **No hi ha `u_mouse`.** Les notes de l'autor l'esmenten, però només com a manera de + desenvolupar amb glslViewer a l'escriptori; `conjur.cpp` no l'enllaça mai. Amb + l'historial complet al davant es pot afirmar fort: `git log -S "u_mouse"` sobre `src/` + no retorna **cap** commit dels 47 del repositori. No es va treure: no hi va ser mai. +- No hi ha equivalent d'`iTimeDelta`, `iFrame`, `iDate`, `iChannelResolution`, ni entrada + d'àudio. +- No hi ha multipassada (els `Buffer A/B/C/D` de Shadertoy). + +## La marca `//N-input`: què fa realment + +Tots els shaders del repositori comencen amb `//0-input`, `//1-input` o `//2-input`. +`video_centre/shaders.py:48-57` busca aquella cadena **en qualsevol part del fitxer** i la +tradueix a `0in`/`1in`/`2in`; si no en troba cap, el tipus és `-`. + +**Però la marca és purament informativa.** Seguint la dada fins al final: + +- `shaders.py:76` l'envia per OSC com a segon argument de `/shader/N/load`. +- `ofApp.cpp:317` **només llegeix l'argument 0** (la ruta). Ignora el tipus i el nombre de + paràmetres. +- L'únic altre ús és `display.py:174`, que pinta la primera lletra a la pantalla. + +O sia que la marca canvia l'etiqueta del menú i res més. `u_tex0` està enllaçat sempre que +hi hagi almenys una textura a la cadena, fins i tot en un `//0-input`. Escriu-la +igualment: és el que et diu d'un cop d'ull què fa un shader quan en tens trenta a la +llista. + +> La documentació del mateix autor (`c_o_n_j_u_r/notes_on_shader_formats.md`) parla de +> `//gen-shader` i `//pro-shader`, i diu que un gen-shader pausa el vídeo. **Aquell +> document està desactualitzat**: el codi actual busca `//N-input` i no fa res amb el +> resultat. Si segueixes les notes de l'autor al peu de la lletra, el teu shader apareix +> com a tipus `-`. + +El mateix amb el nombre de paràmetres: les notes diuen que es detecta comptant +`uniform float u_xN`, però `shaders.py:61` té un `if True:` que retorna 4 sempre. Els +quatre params estan disponibles declaris els que declaris. + +### La interfície no informa d'errors del shader + +`video_centre/shaders.py:18` documenta `'!'` com a estat d'error, però no hi ha cap camí +del codi que l'assigni. Un shader que no compila mostra el mateix estat de «en marxa» que +un que funciona. El registre de GLSL només existeix a la sortida estàndard de +c_o_n_j_u_r. + +## Com encadena les tres capes + +`ofApp.cpp:115-129` — hi ha 3 capes de shader, s'apliquen en ordre, i cadascuna fica la +seva sortida al principi de la llista de textures: + +```cpp +if(effectShader0active){ + fbo = effectShader0.apply(effectInput); + effectInput.insert(effectInput.begin(), fbo.getTexture()); +} +// ...igual per a 1 i 2 +``` + +Conseqüència pràctica: a la capa 1, `u_tex0` és **la sortida de la capa 0**, i `u_tex1` és +el que la capa 0 tenia d'entrada. Aquí hi ha el truc per barrejar una imatge amb la seva +pròpia versió processada. + +## Temps i velocitat + +`conjur.cpp:74-77`: + +```cpp +void conjur::setSpeed(float value){ + speed = -2.0 + 4.0*value; +} +``` + +El valor de la interfície (0–1) es mapa a un multiplicador de **−2 a +2**: + +| Valor UI | Velocitat | Efecte | +|---|---|---| +| 0.0 | −2.0 | El temps corre **enrere**, al doble | +| 0.5 | 0.0 | Congelat | +| 0.75 | 1.0 | Normal | +| 1.0 | 2.0 | El doble de ràpid | + +El botó de velocitat de r_e_c_u_r (`shaders.py:136-140`) només alterna entre 0.5 i 0.75, o +sia congelat / normal. Per al rang complet cal activar l'ajust `X3_AS_SPEED`, que +segresta `u_x3`: amb ell activat, `u_x3` **deixa d'arribar al shader** i passa a ser el +control de velocitat (`shaders.py:157-160`). Si el teu shader fa servir els quatre params, +compta que el quart pot faltar. + +I `u_time` és acumulat, no absolut (`conjur.cpp:88-94`): en canviar la velocitat no salta, +segueix des d'on anava. Pot ser negatiu si el poses marxa enrere — si el teu shader fa +`sqrt(u_time)` o semblant, es trenca. + +### Estabilitat d'aquesta llista + +Els **noms** dels uniforms no canvien des del 2019-08-03, commit `4c7312c` +(«added alt shader mode»), que va introduir els dos jocs alhora. Cap commit posterior toca +ni un sol nom. El que sí que va canviar després va ser d'on surt el temps: + +| Commit | Data | Què va fer | +|---|---|---| +| `4c7312c` | 2019-08-03 | Fixa els noms dels uniforms, els dos jocs | +| `0483d54` | 2019-08-30 | `u_time`, `ftime` i `itime` deixen de llegir `ofGetElapsedTimef()` directament i passen pel rellotge virtual `getTime()`. **Aquí neix `speed = -2.0 + 4.0*value`**, i amb ella poder congelar el temps o córrer-lo enrere | +| `b8f4bd4` | 2019-09-27 | Retocs sense efecte sobre els uniforms | +| `4fd0559` | 2025-04-04 | «changing a few things for recurboy update»: **només dues línies d'`ofLog`** | + +Dit d'una altra manera: des del setembre del 2019 no s'ha mogut res que afecti com +s'escriu un shader. + +## Compte amb les branques + +`master` està congelada al commit `16e1fae`, del **2020-07-23**, i és el que descriu +aquest document. Però el repositori va seguir viu en altres branques: + +| Branca | Últim commit | Què afegeix | +|---|---|---| +| `master` | 2020-07-23 | La base. És el documentat aquí | +| `dev` | **2022-01-11** | Sistema de **modulació** dels 4 params, ajust `USE_SHADER_MOD` i shaders nous (`chroma_key`, `luma_key_black/white`, `displace2in`, `spotlight`) | +| `feature_plugins` | 2020-03-15 | Sistema de connectors | +| `pre-plugin-dev` | 2020-03-14 | | + +**La bona notícia: el format de shader no canvia.** `video_centre/shaders.py` a `dev` +segueix buscant `//0-input`/`//1-input`/`//2-input`, i +`determine_shader_parameter_number` conserva el mateix `if True: return 4`. Tot el +d'aquest document val igual a `dev`. + +El que sí que canvia és el que es pot fer amb els params: a `dev` es poden **modular**, +sumant un valor de −1 a +1 al valor base i retallant el resultat a 0–1. La wiki documenta +a sobre d'això connectors d'LFO i de reacció a àudio. + +**[PER VERIFICAR A LA MÀQUINA]** quina branca corre el teu aparell: +`git -C ~/r_e_c_u_r branch --show-current`. + +## Ajustos de r_e_c_u_r que afecten els shaders + +De `json_objects/settings_default.json`: + +- `X3_AS_SPEED` — el dit més amunt: `u_x3` passa a ser el control de velocitat. +- `FIX_PARAM_OFFSET_LAYER` — canvia a quina capa es dirigeixen els comandaments. +- `SHADER_POSITION` (`input`/`output`) — si la cadena de shaders s'aplica abans o després + del bucle de realimentació `detour`. + +## Limitacions de la màquina + +- **OpenGL ES 2.0 / GLSL ES 1.00.** Ho diu l'autor a `notes_on_shader_formats.md` + («currently the conjur openframeworks application is running __gles 2__»). És el que + més mana en portar des de Shadertoy; vegeu la + [guia de conversió](conversion-guide.ca.md). +- No es declara `#version` a dalt. Es fa servir el bloc `#ifdef GL_ES / precision mediump + float; / #endif` que porten tots els shaders del repositori. +- El maquinari objectiu és la Raspberry Pi 3. +- **[PER VERIFICAR A LA MÀQUINA]** la resolució real de treball: `u_resolution` surt + d'`ofGetWidth()/ofGetHeight()`, que depèn de com estigui configurada la sortida de vídeo + de la Pi. +- **[PER VERIFICAR A LA MÀQUINA]** si `v_texcoord` té l'eix Y invertit respecte a + Shadertoy. Els shaders del repositori el fan servir directament sense capgirar, cosa que + suggereix que no, però amb textures de vídeo en un FBO convé comprovar-ho amb + `test-orientation.frag` abans de barallar-se amb un port. + +## Errates al repositori original + +Trobades en llegir-lo, per si te'n copies (comprovades sobre el commit `16e1fae`): + +- `Shaders/0-input/colour_sine.frag:9` — `uniform vec4 fparams` **sense punt i coma**. + Aquell fitxer no compila. No el facis servir de plantilla. +- `Shaders/1-input/invert_effect.frag` — no porta bloc de precisió. +- `Shaders/2-input/mix_lumaKey.frag` — ni marca `//N-input` ni bloc de precisió, i està + escrit sencer amb els noms legacy (`tcoord`, `tex`, `tex2`, `tres`, `fparams`). És + l'exemple viu del format antic, útil per reconèixer-lo. diff --git a/docs/recur-shader-reference.en.md b/docs/recur-shader-reference.en.md index 91f464c..ce99cc8 100644 --- a/docs/recur-shader-reference.en.md +++ b/docs/recur-shader-reference.en.md @@ -3,7 +3,7 @@ doc: RECUR-SHADER-REF rev: 03 canonical_language: en translations: - - es: docs/recur-shader-reference.es.md (rev 02, current) + - ca: docs/recur-shader-reference.ca.md (rev 02, current) --- # How shaders actually work in r_e_c_u_r diff --git a/docs/recur-shader-reference.es.md b/docs/recur-shader-reference.es.md deleted file mode 100644 index 17b0c26..0000000 --- a/docs/recur-shader-reference.es.md +++ /dev/null @@ -1,291 +0,0 @@ ---- -doc: RECUR-SHADER-REF -rev: 03 -canonical_language: en -canonical_source: docs/recur-shader-reference.en.md -translations: - - es: docs/recur-shader-reference.es.md (rev 02, al día) ---- - -# Cómo funcionan de verdad los shaders en r_e_c_u_r - -Todo lo de aquí está comprobado **leyendo el código fuente** de los tres -repositorios implicados, no leyendo foros. Cada afirmación lleva `archivo:línea`. Lo -que no se puede cerrar leyendo código va marcado como **[POR VERIFICAR EN LA -MÁQUINA]**. - -Verificado el 2026-08-28 contra el commit `16e1fae` de r_e_c_u_r. - -## La cadena: quién dibuja qué - -No es un solo programa. Son tres piezas, y confundirlas hace perder el tiempo: - -``` -r_e_c_u_r (Python, la interfaz) - │ navega Shaders/, lee el archivo, decide qué mandar - │ OSC → puerto local - ▼ -c_o_n_j_u_r (app de openFrameworks, C++) - │ recibe /shader/N/load, /param, /speed - ▼ -la clase conjur (addon ofxVideoArtTools) - │ hace shader.begin(), enlaza los uniforms, dibuja - ▼ -OpenGL ES 2.0 / GLSL ES 1.00 ← aquí corre tu .frag -``` - -- r_e_c_u_r **no renderiza nada**. Solo manda mensajes OSC. -- Los uniforms se enlazan en `ofxVideoArtTools/src/conjur.cpp`. Ese archivo es la - fuente de verdad sobre qué variables existen. -- Repositorios: [r_e_c_u_r](https://github.com/cyberboy666/r_e_c_u_r), - [c_o_n_j_u_r](https://github.com/cyberboy666/c_o_n_j_u_r), - [ofxVideoArtTools](https://github.com/cyberboy666/ofxVideoArtTools). Los tres - GPL-3.0. - -## Dónde van los archivos - -`data_centre/data.py:34` — r_e_c_u_r busca shaders en este orden: - -1. El USB o dispositivo externo montado (`PATH_TO_EXTERNAL_DEVICES`) -2. `/home/pi/r_e_c_u_r/Shaders` -3. `/home/pi/Shaders` - -Extensiones que muestra el navegador: `.frag`, `.shader`, `.glsl`, `.glslf`, `.fsh`. - -**El vertex shader está fijo en el código.** `ofApp.cpp:317` carga siempre -`/home/pi/r_e_c_u_r/Shaders/default.vert`, pase lo que pase. No puedes dar tu propio -vertex shader sin tocar C++. Solo escribimos fragment shaders. - -## Los uniforms que existen de verdad - -De `ofxVideoArtTools/src/conjur.cpp:50-73`. Esta es la lista completa; **no hay -más**. - -### Los que usamos (formato Book of Shaders) - -| Uniform | Tipo | Qué es | Fuente | -|---|---|---|---| -| `u_time` | `float` | Segundos desde que arrancó el shader, **escalado por la velocidad** | `conjur.cpp:51` | -| `u_resolution` | `vec2` | Tamaño de la ventana en píxeles | `conjur.cpp:52` | -| `u_x0`…`u_x3` | `float` | Los cuatro parámetros de la interfaz. **Siempre 0.0–1.0** | `conjur.cpp:54` | -| `u_tex0` | `sampler2D` | Textura de entrada (vídeo, o salida del shader anterior) | `conjur.cpp:57` | -| `u_tex1` | `sampler2D` | Segunda entrada, si existe en la cadena | `conjur.cpp:57` | - -Y del `default.vert`, disponibles como varyings: - -| Varying | Tipo | Qué es | -|---|---|---| -| `v_texcoord` | `vec2` | Coordenada UV normalizada, 0.0–1.0. **Es lo que se usa siempre** | -| `tcoord` | `vec2` | Alias exacto de `v_texcoord` (soporte de dos formatos) | -| `v_position`, `v_color`, `v_normal` | | Existen, pero los shaders del repo no los usan | - -### Los legacy (existen, pero no los uses en código nuevo) - -`conjur.cpp:61-73` enlaza además un juego de nombres más antiguo: - -| Uniform | Tipo | Qué es | -|---|---|---| -| `ftime` | `float` | Solo la parte decimal de `u_time` — va de 0 a 1 y salta. **No es un reloj continuo** | -| `itime` | `int` | `ceil(u_time)`, contador de segundos enteros | -| `tres` | `vec2` | Igual que `u_resolution` | -| `fparams` | `vec4` | Los cuatro params juntos: `fparams[0]` == `u_x0` | -| `iparams` | `ivec4` | **Siempre `(0,0,0,0)`. Está muerto** — `conjur.cpp:66` lo fija a cero | -| `tex`, `tex2` | `sampler2D` | Alias de `u_tex0` y `u_tex1` | - -Ojo con `ftime`: `Shaders/0-input/colour_sine.frag` lo usa y es fácil copiarlo -creyendo que es el reloj. Serrucha cada segundo. - -### De dónde sale ese juego de nombres antiguo - -No es un invento de r_e_c_u_r: es la convención nativa del **Structure**, el módulo -Eurorack de vídeo de Erogenous Tones. El `default.vert` declara `tcoord` y -`v_texcoord` a la vez, con el comentario literal `// twice because supporting two -shader formats`, precisamente para servir a las dos comunidades. - -Se ve claro en el único tutorial de conversión que hay en la wiki oficial, -[tutorial_converting_simple_1input_shader_from_shadertoy](https://git.terata.org/eessppeelllloo/r_e_c_u_r/wiki/tutorial_converting_simple_1input_shader_from_shadertoy): -está escrito entero en la convención antigua, habla de «los nombres que -structure/recur esperan» y manda al sandbox de Erogenous Tones. - -**Los dos formatos funcionan de verdad.** `conjur.cpp:50-73` enlaza los dos juegos -sin condiciones, y como `fparams` se alimenta del mismo array `shaderParams` que -`u_x0..u_x3`, un shader escrito al estilo antiguo también responde a los mandos. Aun -así, en código nuevo conviene el juego moderno por tres razones concretas: `iparams` -está muerto, `ftime` no es un reloj continuo, y el moderno es el que usan los shaders -de ejemplo del repo. - -> Si sigues el tutorial de la wiki: reconstruye el tiempo con -> `float time = float(itime) + ftime;`. Como `itime` es `ceil()` y no `floor()`, eso -> va casi un segundo **adelantado** respecto a `u_time`. Da igual para un efecto -> visual, pero no es el mismo reloj. - -### Lo que NO existe - -- **No hay `u_mouse`.** Las notas del autor lo mencionan, pero solo como forma de - desarrollar con glslViewer en el escritorio; `conjur.cpp` nunca lo enlaza. Con el - historial completo delante se puede afirmar fuerte: `git log -S "u_mouse"` sobre - `src/` no devuelve **ningún** commit de los 47 del repositorio. No se quitó: nunca - estuvo. -- No hay equivalente de `iTimeDelta`, `iFrame`, `iDate`, `iChannelResolution`, ni - entrada de audio. -- No hay multipasada (los `Buffer A/B/C/D` de Shadertoy). - -## La marca `//N-input`: qué hace realmente - -Todos los shaders del repo empiezan con `//0-input`, `//1-input` o `//2-input`. -`video_centre/shaders.py:48-57` busca esa cadena **en cualquier parte del archivo** y -la traduce a `0in`/`1in`/`2in`; si no encuentra ninguna, el tipo es `-`. - -**Pero la marca es puramente informativa.** Siguiendo el dato hasta el final: - -- `shaders.py:76` la manda por OSC como segundo argumento de `/shader/N/load`. -- `ofApp.cpp:317` **solo lee el argumento 0** (la ruta). Ignora el tipo y el número - de parámetros. -- El único otro uso es `display.py:174`, que pinta la primera letra en pantalla. - -O sea que la marca cambia la etiqueta del menú y nada más. `u_tex0` está enlazado -siempre que haya al menos una textura en la cadena, incluso en un `//0-input`. -Escríbela igualmente: es lo que te dice de un vistazo qué hace un shader cuando -tienes treinta en una lista. - -> La documentación del propio autor (`c_o_n_j_u_r/notes_on_shader_formats.md`) habla -> de `//gen-shader` y `//pro-shader`, y dice que un gen-shader pausa el vídeo. **Ese -> documento está desactualizado**: el código actual busca `//N-input` y no hace nada -> con el resultado. Si sigues las notas del autor al pie de la letra, tu shader -> aparece como tipo `-`. - -Lo mismo con el número de parámetros: las notas dicen que se detecta contando -`uniform float u_xN`, pero `shaders.py:61` tiene un `if True:` que devuelve 4 -siempre. Los cuatro params están disponibles declares los que declares. - -### La interfaz no informa de errores del shader - -`video_centre/shaders.py:18` documenta `'!'` como estado de error, pero no hay ningún -camino del código que lo asigne. Un shader que no compila muestra el mismo estado de -«en marcha» que uno que funciona. El log de GLSL solo existe en la salida estándar de -c_o_n_j_u_r. - -## Cómo encadena las tres capas - -`ofApp.cpp:115-129` — hay 3 capas de shader, se aplican en orden, y cada una mete su -salida al principio de la lista de texturas: - -```cpp -if(effectShader0active){ - fbo = effectShader0.apply(effectInput); - effectInput.insert(effectInput.begin(), fbo.getTexture()); -} -// ...igual para 1 y 2 -``` - -Consecuencia práctica: en la capa 1, `u_tex0` es **la salida de la capa 0**, y -`u_tex1` es lo que la capa 0 tenía de entrada. Ahí está el truco para mezclar una -imagen con su propia versión procesada. - -## Tiempo y velocidad - -`conjur.cpp:74-77`: - -```cpp -void conjur::setSpeed(float value){ - speed = -2.0 + 4.0*value; -} -``` - -El valor de la interfaz (0–1) se mapea a un multiplicador de **−2 a +2**: - -| Valor UI | Velocidad | Efecto | -|---|---|---| -| 0.0 | −2.0 | El tiempo corre **hacia atrás**, al doble | -| 0.5 | 0.0 | Congelado | -| 0.75 | 1.0 | Normal | -| 1.0 | 2.0 | Doble de rápido | - -El botón de velocidad de r_e_c_u_r (`shaders.py:136-140`) solo alterna entre 0.5 y -0.75, o sea congelado / normal. Para el rango completo hay que activar el ajuste -`X3_AS_SPEED`, que secuestra `u_x3`: con él activado, `u_x3` **deja de llegar al -shader** y pasa a ser el control de velocidad (`shaders.py:157-160`). Si tu shader usa -los cuatro params, cuenta con que el cuarto puede faltar. - -Y `u_time` es acumulado, no absoluto (`conjur.cpp:88-94`): al cambiar la velocidad no -salta, sigue desde donde iba. Puede ser negativo si lo pones marcha atrás — si tu -shader hace `sqrt(u_time)` o similar, se rompe. - -### Estabilidad de esta lista - -Los **nombres** de los uniforms llevan sin cambiar desde el 2019-08-03, commit -`4c7312c` («added alt shader mode»), que introdujo los dos juegos de una vez. Ningún -commit posterior toca un solo nombre. Lo que sí cambió después fue de dónde sale el -tiempo: - -| Commit | Fecha | Qué hizo | -|---|---|---| -| `4c7312c` | 2019-08-03 | Fija los nombres de los uniforms, los dos juegos | -| `0483d54` | 2019-08-30 | `u_time`, `ftime` e `itime` dejan de leer `ofGetElapsedTimef()` directamente y pasan por el reloj virtual `getTime()`. **Aquí nace `speed = -2.0 + 4.0*value`**, y con ella poder congelar el tiempo o correrlo hacia atrás | -| `b8f4bd4` | 2019-09-27 | Retoques sin efecto sobre los uniforms | -| `4fd0559` | 2025-04-04 | «changing a few things for recurboy update»: **solo dos líneas de `ofLog`** | - -Dicho de otro modo: desde septiembre de 2019 no se ha movido nada que afecte a cómo -se escribe un shader. - -## Ojo con las ramas - -`master` está congelada en el commit `16e1fae`, del **2020-07-23**, y es lo que -describe este documento. Pero el repositorio siguió vivo en otras ramas: - -| Rama | Último commit | Qué añade | -|---|---|---| -| `master` | 2020-07-23 | La base. Es lo documentado aquí | -| `dev` | **2022-01-11** | Sistema de **modulación** de los 4 params, ajuste `USE_SHADER_MOD` y shaders nuevos (`chroma_key`, `luma_key_black/white`, `displace2in`, `spotlight`) | -| `feature_plugins` | 2020-03-15 | Sistema de plugins | -| `pre-plugin-dev` | 2020-03-14 | | - -**La buena noticia: el formato de shader no cambia.** `video_centre/shaders.py` en -`dev` sigue buscando `//0-input`/`//1-input`/`//2-input`, y -`determine_shader_parameter_number` conserva el mismo `if True: return 4`. Todo lo de -este documento vale igual en `dev`. - -Lo que sí cambia es lo que se puede hacer con los params: en `dev` se pueden -**modular**, sumando un valor de −1 a +1 al valor base y recortando el resultado a -0–1. La wiki documenta encima de eso plugins de LFO y de reacción a audio. - -**[POR VERIFICAR EN LA MÁQUINA]** qué rama corre tu aparato: -`git -C ~/r_e_c_u_r branch --show-current`. - -## Ajustes de r_e_c_u_r que afectan a los shaders - -De `json_objects/settings_default.json`: - -- `X3_AS_SPEED` — lo dicho arriba: `u_x3` pasa a ser el control de velocidad. -- `FIX_PARAM_OFFSET_LAYER` — cambia a qué capa se dirigen los mandos. -- `SHADER_POSITION` (`input`/`output`) — si la cadena de shaders se aplica antes o - después del bucle de realimentación `detour`. - -## Limitaciones de la máquina - -- **OpenGL ES 2.0 / GLSL ES 1.00.** Lo dice el autor en - `notes_on_shader_formats.md` («currently the conjur openframeworks application is - running __gles 2__»). Es lo que más manda al portar desde Shadertoy; ver la - [guía de conversión](conversion-guide.es.md). -- No se declara `#version` arriba. Se usa el bloque `#ifdef GL_ES / precision mediump - float; / #endif` que llevan todos los shaders del repo. -- El hardware objetivo es la Raspberry Pi 3. -- **[POR VERIFICAR EN LA MÁQUINA]** la resolución real de trabajo: `u_resolution` - sale de `ofGetWidth()/ofGetHeight()`, que depende de cómo esté configurada la - salida de vídeo de la Pi. -- **[POR VERIFICAR EN LA MÁQUINA]** si `v_texcoord` tiene el eje Y invertido respecto - a Shadertoy. Los shaders del repo lo usan directamente sin voltear, lo que sugiere - que no, pero con texturas de vídeo en un FBO conviene comprobarlo con - `test-orientation.frag` antes de pelearse con un port. - -## Erratas en el repositorio original - -Encontradas al leerlo, por si te copias de ellas (comprobadas sobre el commit -`16e1fae`): - -- `Shaders/0-input/colour_sine.frag:9` — `uniform vec4 fparams` **sin punto y coma**. - Ese archivo no compila. No lo uses de plantilla. -- `Shaders/1-input/invert_effect.frag` — no lleva bloque de precisión. -- `Shaders/2-input/mix_lumaKey.frag` — ni marca `//N-input` ni bloque de precisión, y - está escrito entero con los nombres legacy (`tcoord`, `tex`, `tex2`, `tres`, - `fparams`). Es el ejemplo vivo del formato antiguo, útil para reconocerlo. diff --git a/instrumentacio/LLEGEIX-ME.md b/instrumentacio/LLEGEIX-ME.md new file mode 100644 index 0000000..746abca --- /dev/null +++ b/instrumentacio/LLEGEIX-ME.md @@ -0,0 +1,72 @@ +# Instrumentació de l'aparell + +El que hi ha aquí s'instal·la **a la Raspberry Pi**, no al portàtil. Serveix per mesurar +coses que r_e_c_u_r no explica enlloc de la seva interfície. + +| Fitxer | Què és | +|---|---| +| `c_o_n_j_u_r.envoltorio` | Script de shell que suplanta el binari de c_o_n_j_u_r. Registra el log complet (inclòs el font de cada shader que es carrega) i, si la sonda hi és, els fotogrames per segon | +| `fps_probe.c` | Sonda de fps. Es cola per davant d'`eglSwapBuffers`, que és la crida que presenta cada fotograma | + +L'anàlisi es fa amb `fps-por-shader.py`, que creua les dues coses del registre i en treu +una taula de shader → fps. + +## Per què així + +`eglSwapBuffers` és el punt exacte on es presenta un fotograma, així que comptar-la és +comptar fotogrames de debò. Colar-se per davant amb `LD_PRELOAD` no toca el binari ni +obliga a recompilar openFrameworks, que en una Pi 3 són hores. + +La sonda escriu per la sortida d'error, que l'envoltori ja redirigeix al registre. Així les +mesures queden **intercalades en ordre** amb les càrregues de shader que aquell mateix +registre aboca, i es pot saber quin shader hi havia posat en cada moment. + +## Instal·lar + +```bash +B=/ruta/al/rootfs/home/pi/openframeworks10.1/apps/myApps/c_o_n_j_u_r/bin +cp -p "$B/c_o_n_j_u_r" "$B/c_o_n_j_u_r.envoltorio-$(date +%F)" # guardar l'anterior +cp c_o_n_j_u_r.envoltorio "$B/c_o_n_j_u_r" && chmod +x "$B/c_o_n_j_u_r" +cp fps_probe.c /ruta/al/rootfs/home/pi/fps_probe.c +``` + +La sonda **es compila sola a la Pi** el primer cop que arrenca c_o_n_j_u_r (allà hi ha +`gcc-6`). És codi ARM: no val creuar-lo des del portàtil. Si no compilés, l'envoltori +deixa la marca `/home/pi/fps_probe.no-compila`, no ho torna a intentar i arrenca igual +sense mesurar. + +## Desinstal·lar + +Treure només la mesura de fps, deixant el registre de shaders com estava: + +```bash +rm /home/pi/fps_probe.so +``` + +Deixar l'aparell **tal com venia de fàbrica**: + +```bash +cd /home/pi/openframeworks10.1/apps/myApps/c_o_n_j_u_r/bin +rm c_o_n_j_u_r +mv c_o_n_j_u_r.real c_o_n_j_u_r +``` + +El binari original no es toca mai: només es reanomena a `c_o_n_j_u_r.real`. + +## El parany que va costar trobar + +`stdbuf` fa la seva feina **posant `LD_PRELOAD`**. Si es fa servir el programa `stdbuf` +per treure el buffer i a més es vol precarregar la sonda, stdbuf trepitja la variable i +s'emporta la sonda per davant: el registre surt sense ni una línia `[fps]` i tot sembla +correcte. Per això l'envoltori no crida `stdbuf`, sinó que fa el mateix que fa per dins +—precarregar `libstdbuf.so` i posar `_STDBUF_O`/`_STDBUF_E`— perquè hi càpiguen les dues +biblioteques al mateix `LD_PRELOAD`. + +Si no troba `libstdbuf.so`, arrenca sense sonda i renuncia a mesurar: abans això que perdre +el registre de shaders. + +## Provat abans d'instal·lar + +El mecanisme es va assajar sencer al portàtil contra una EGL falsa: la sonda mesura 62,5 +fps on l'objectiu eren 62, i les 310 crides segueixen arribant a l'EGL de debò. O sia que +compta bé i no trenca el pintat. diff --git a/instrumentacio/c_o_n_j_u_r.envoltorio b/instrumentacio/c_o_n_j_u_r.envoltorio new file mode 100644 index 0000000..c2c6580 --- /dev/null +++ b/instrumentacio/c_o_n_j_u_r.envoltorio @@ -0,0 +1,86 @@ +#!/bin/bash +# ENVOLTORI TEMPORAL - versio 2 (2026-09-04) +# +# Fa el mateix que el del 2026-09-02 (capturar el registre de compilacio de GLSL, que +# r_e_c_u_r no mostra enlloc) i a mes mesura els fotogrames per segon. +# +# PER DESFER-HO (deixa l'aparell com estava): +# cd /home/pi/openframeworks10.1/apps/myApps/c_o_n_j_u_r/bin +# rm c_o_n_j_u_r +# mv c_o_n_j_u_r.real c_o_n_j_u_r +# +# PER TREURE NOMES LA MESURA DE FPS i deixar el registre com estava: +# rm /home/pi/fps_probe.so +# L'envoltori arrenca igual sense ella. +# +# El registre s'ACUMULA a /home/pi/conjur.log: cada arrencada hi afegeix, no trepitja +# l'anterior. Si creix massa, es pot esborrar sense consequencies. +# +# El buffer de sortida es desactiva a ma en comptes d'amb el programa "stdbuf". Motiu: +# stdbuf fa la seva feina posant LD_PRELOAD, o sia que TREPITJA el nostre i s'emporta +# la sonda per davant. Comprovat. Aqui es fa el mateix que fa stdbuf per dins +# --precarregar libstdbuf.so i posar _STDBUF_O/_STDBUF_E-- de manera que hi capiguen +# les dues biblioteques al mateix LD_PRELOAD. Sense buffer importa: si no, en apagar +# l'aparell es perdria just l'ultim que va escriure, que sol ser el que interessa. + +# NOTA DE LLENGUA: els noms de variable i els missatges que van al registre es deixen +# en castella a proposit. fps-por-shader.py els busca tal qual (per exemple la linia +# "arranque:") i els registres ja capturats a l'aparell els porten aixi. Traduir-los +# trencaria la lectura dels registres vells. + +AQUI="$(dirname "$0")" +LOG=/home/pi/conjur.log +FUENTE=/home/pi/fps_probe.c +SONDA=/home/pi/fps_probe.so +FALLO=/home/pi/fps_probe.no-compila + +# La sonda es compila A LA PI i un sol cop. Si falla es deixa una marca i no es torna +# a intentar a cada arrencada, per no omplir el registre del mateix error. +if [ -f "$FUENTE" ] && [ ! -f "$SONDA" ] && [ ! -f "$FALLO" ]; then + { + echo "--- compilando la sonda de fps: $(date '+%Y-%m-%d %H:%M:%S') ---" + if gcc -shared -fPIC -O2 -o "$SONDA.parcial" "$FUENTE" -ldl 2>&1; then + mv "$SONDA.parcial" "$SONDA" + echo "--- sonda lista ---" + else + rm -f "$SONDA.parcial" + date > "$FALLO" + echo "--- la sonda no compila; se sigue sin ella ---" + fi + } >> "$LOG" 2>&1 +fi + +{ + echo "" + echo "==================================================================" + echo "arranque: $(date '+%Y-%m-%d %H:%M:%S')" + if [ -f "$SONDA" ]; then + echo "midiendo fps (una linea [fps] cada 2 s)" + else + echo "sin medida de fps" + fi + echo "==================================================================" +} >> "$LOG" 2>/dev/null + +LIBSTDBUF="$(ls /usr/lib/*/coreutils/libstdbuf.so /usr/lib/coreutils/libstdbuf.so \ + /usr/libexec/*/libstdbuf.so 2>/dev/null | head -1)" + +if [ -n "$LIBSTDBUF" ]; then + # Cami bo: sense buffer I amb sonda. + export _STDBUF_O=0 _STDBUF_E=0 + if [ -f "$SONDA" ]; then + export LD_PRELOAD="$LIBSTDBUF:$SONDA" + else + export LD_PRELOAD="$LIBSTDBUF" + fi + # Si el .so estigues trencat o fos d'una altra arquitectura, el carregador ho + # avisa per la sortida d'error i tira endavant sense ell: LD_PRELOAD no avorta + # l'arrencada. + exec "$AQUI/c_o_n_j_u_r.real" "$@" >> "$LOG" 2>&1 +else + # No apareix libstdbuf. Es torna a l'arrencada de sempre, tal com estava el + # 2026-09-02, i es renuncia a mesurar: abans aixo que perdre el registre de + # shaders. + echo "sin libstdbuf: se arranca como antes y no se mide fps" >> "$LOG" 2>/dev/null + exec /usr/bin/stdbuf -o0 -e0 "$AQUI/c_o_n_j_u_r.real" "$@" >> "$LOG" 2>&1 +fi diff --git a/instrumentacio/fps-por-shader.py b/instrumentacio/fps-por-shader.py new file mode 100755 index 0000000..9c31911 --- /dev/null +++ b/instrumentacio/fps-por-shader.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Llegeix un conjur.log i en treu a quants fps anava cada shader. + +L'envoltori d'instrumentacio/ deixa dues coses al mateix registre i en ordre: el font +complet de cada shader que es carrega, i una linia [fps] cada dos segons. Creuar-les +dona el que fins ara s'esbrinava a base de viatges a l'aparell: quanta complexitat hi +cap abans que la Pi deixi de seguir el ritme. + +Us: + ./fps-por-shader.py /ruta/al/conjur.log + +El registre no guarda el nom del fitxer --- r_e_c_u_r no l'envia --- aixi que cada +shader s'identifica per la seva primera linia de comentari. Per aixo conve que els +ports portin una capcalera amb el seu nom, cosa que les plantilles ja fan. + +Les expressions regulars d'aqui sota busquen el text que escriuen l'envoltori i la +sonda, que es queda en castella a proposit perque els registres ja capturats a +l'aparell el porten aixi. No traduir-les. + +Medialab Terata +""" +import re +import sys +from statistics import median + +MARCA = re.compile(r"//([012])-input") +ARRANQUE = re.compile(r"^arranque:") +FPS = re.compile(r"\[fps\]\s+([\d.]+)\s+fps") +REPORTA = "GL_FRAGMENT_SHADER shader reports:" + + +def nombrar(fuente): + """Treu un nom llegible de les primeres linies del shader.""" + tipo = "" + titulo = "" + for linea in fuente[:12]: + m = MARCA.search(linea) + if m and not tipo: + tipo = m.group(1) + "-input" + continue + t = linea.strip() + if t.startswith("//") and not titulo: + t = t.lstrip("/").strip() + if t and not t.lower().startswith(("original:", "autor:", "licencia:", + "llicencia:", "consultat:")): + titulo = t + if not titulo: + titulo = "(sin cabecera)" + return f"{titulo[:52]} [{tipo or '?'}]" + + +def leer(ruta): + """Retorna [(nom, [fps, ...]), ...] en ordre de carrega.""" + tramos = [] + actual = None + with open(ruta, encoding="utf-8", errors="replace") as f: + lineas = f.readlines() + + i = 0 + while i < len(lineas): + linea = lineas[i] + if ARRANQUE.match(linea): + # Reinici de l'aparell. El que s'hagues mesurat abans NO li toca al + # shader que estigues carregat a l'arrencada anterior: en reiniciar no + # n'hi ha cap de posat fins que r_e_c_u_r carrega el primer, i les mesures + # comencen abans. Sense aixo, el repos s'atribueix a l'ultim shader de la + # sessio anterior, que es justament l'error que va fer apareixer un 8 + # passos a 30 fps. + actual = ("(repos: arrencant, sense shader carregat)", []) + tramos.append(actual) + i += 1 + continue + if REPORTA in linea: + # El font ve just darrere. Es talla al seguent missatge del registre. + fuente = [] + j = i + 1 + while j < len(lineas) and not lineas[j].startswith(("[verbose]", "[notice ]", "[ error ]")): + fuente.append(lineas[j]) + j += 1 + if any(MARCA.search(x) for x in fuente[:12]): + actual = (nombrar(fuente), []) + tramos.append(actual) + i = j + continue + m = FPS.search(linea) + if m and actual is not None: + actual[1].append(float(m.group(1))) + i += 1 + return tramos + + +def main(): + if len(sys.argv) != 2: + print(__doc__.strip()) + return 2 + tramos = leer(sys.argv[1]) + if not tramos: + print("No hi ha cap carrega de shader d'usuari en aquest registre.") + print("Si tampoc no hi ha linies [fps], es que la sonda no es va arribar a") + print("carregar: mira si el registre diu 'sin libstdbuf' o 'la sonda no compila'.") + return 1 + + hay_fps = any(v for _, v in tramos) + print(f"{'shader':<58} {'mostres':>8} {'fps':>6} {'min':>6} {'max':>6}") + print("-" * 88) + for nombre, vals in tramos: + # La primera mesura despres de carregar enxampa l'arrencada a mitges: fora. + utiles = vals[1:] if len(vals) > 1 else vals + if utiles: + print(f"{nombre:<58} {len(utiles):>8} {median(utiles):>6.1f} " + f"{min(utiles):>6.1f} {max(utiles):>6.1f}") + else: + print(f"{nombre:<58} {'--':>8} {'--':>6} {'--':>6} {'--':>6}") + + if not hay_fps: + print() + print("Carregues si, mesures no: la sonda de fps no era activa en aquest registre.") + else: + print() + print("La columna fps es la mediana. Un shader que va per sota d'uns 15") + print("perd els cops curts: una envolupant de 0.1 s cap entre dos") + print("fotogrames i no arriba a veure's mai.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/instrumentacion/fps_probe.c b/instrumentacio/fps_probe.c similarity index 52% rename from instrumentacion/fps_probe.c rename to instrumentacio/fps_probe.c index ad6d94c..8dc9403 100644 --- a/instrumentacion/fps_probe.c +++ b/instrumentacio/fps_probe.c @@ -1,30 +1,34 @@ -/* fps_probe.c -- mide a cuantos fotogramas por segundo va c_o_n_j_u_r. +/* fps_probe.c -- mesura a quants fotogrames per segon va c_o_n_j_u_r. * - * Se cuela por delante de eglSwapBuffers, que es la llamada que presenta cada - * fotograma: contarla es contar fotogramas de verdad, sin tocar el binario ni - * recompilar openFrameworks. Escribe en stderr, que el envoltorio ya redirige a - * /home/pi/conjur.log, asi que las medidas quedan intercaladas EN ORDEN con las - * cargas de shader que ese mismo log ya vuelca. De ahi sale la tabla que - * interesa: que shader estaba puesto y a cuanto iba. + * Es cola per davant d'eglSwapBuffers, que es la crida que presenta cada fotograma: + * comptar-la es comptar fotogrames de debo, sense tocar el binari ni recompilar + * openFrameworks. Escriu a stderr, que l'envoltori ja redirigeix a + * /home/pi/conjur.log, aixi que les mesures queden intercalades EN ORDRE amb les + * carregues de shader que aquell mateix registre ja aboca. D'aqui en surt la taula + * que interessa: quin shader hi havia posat i a quant anava. * - * Compilar EN LA PROPIA PI (es codigo ARM, no vale cruzarlo desde el portatil): + * Compilar A LA MATEIXA PI (es codi ARM, no val creuar-lo des del portatil): * gcc -shared -fPIC -O2 -o fps_probe.so fps_probe.c -ldl * - * Medialab Terata, 2026-09-04. Para quitarlo basta con borrar el .so: el - * envoltorio arranca igual sin el. + * Medialab Terata, 2026-09-04. Per treure'l n'hi ha prou d'esborrar el .so: + * l'envoltori arrenca igual sense ell. + * + * NOTA DE LLENGUA: els noms de variable i el text que s'escriu al registre es deixen + * en castella a proposit. fps-por-shader.py els busca tal qual i els registres ja + * capturats a l'aparell els porten aixi. */ #define _GNU_SOURCE #include #include #include -/* Declarados a mano para no depender de las cabeceras de EGL al compilar. */ +/* Declarats a ma per no dependre de les capcaleres d'EGL en compilar. */ typedef unsigned int EGLBoolean; typedef void *EGLDisplay; typedef void *EGLSurface; #ifndef FPS_INTERVALO -#define FPS_INTERVALO 2.0 /* segundos entre medidas */ +#define FPS_INTERVALO 2.0 /* segons entre mesures */ #endif static EGLBoolean (*real_swap)(EGLDisplay, EGLSurface) = 0; @@ -47,15 +51,15 @@ EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface surf) real_swap = (EGLBoolean (*)(EGLDisplay, EGLSurface)) dlsym(RTLD_NEXT, "eglSwapBuffers"); if (real_swap == 0) { - /* Sin la de verdad no se puede pintar. Avisar UNA vez y no volver a - * intentarlo cada fotograma, que llenaria el log. */ + /* Sense la de debo no es pot pintar. Avisar UN cop i no tornar-ho a + * intentar cada fotograma, que ompliria el registre. */ if (!avisado) { fprintf(stderr, "[fps] no encuentro el eglSwapBuffers real: %s\n", dlerror() ? dlerror() : "sin detalle"); fflush(stderr); avisado = 1; } - return 1; /* EGL_TRUE: no romper el arranque por culpa de la sonda */ + return 1; /* EGL_TRUE: no trencar l'arrencada per culpa de la sonda */ } } diff --git a/instrumentacion/LEEME.md b/instrumentacion/LEEME.md deleted file mode 100644 index 1a9c6ad..0000000 --- a/instrumentacion/LEEME.md +++ /dev/null @@ -1,74 +0,0 @@ -# Instrumentación del aparato - -Lo que hay aquí se instala **en la Raspberry Pi**, no en el portátil. Sirve para -medir cosas que r_e_c_u_r no cuenta en ninguna parte de su interfaz. - -| Archivo | Qué es | -|---|---| -| `c_o_n_j_u_r.envoltorio` | Script de shell que suplanta al binario de c_o_n_j_u_r. Registra el log completo (incluido el fuente de cada shader que se carga) y, si la sonda está, los fotogramas por segundo | -| `fps_probe.c` | Sonda de fps. Se cuela por delante de `eglSwapBuffers`, que es la llamada que presenta cada fotograma | - -El análisis se hace con `../instrumentacion/fps-por-shader.py`, que cruza las dos cosas -del log y saca una tabla de shader → fps. - -## Por qué así - -`eglSwapBuffers` es el punto exacto donde se presenta un fotograma, así que contarla -es contar fotogramas de verdad. Colarse por delante con `LD_PRELOAD` no toca el -binario ni obliga a recompilar openFrameworks, que en una Pi 3 son horas. - -La sonda escribe por la salida de error, que el envoltorio ya redirige al log. Así -las medidas quedan **intercaladas en orden** con las cargas de shader que ese mismo -log vuelca, y se puede saber qué shader estaba puesto en cada momento. - -## Instalar - -Ya está instalado desde el 2026-09-04. Si hubiera que repetirlo en otra tarjeta: - -```bash -B=/ruta/a/rootfs/home/pi/openframeworks10.1/apps/myApps/c_o_n_j_u_r/bin -cp -p "$B/c_o_n_j_u_r" "$B/c_o_n_j_u_r.envoltorio-$(date +%F)" # guardar el anterior -cp c_o_n_j_u_r.envoltorio "$B/c_o_n_j_u_r" && chmod +x "$B/c_o_n_j_u_r" -cp fps_probe.c /ruta/a/rootfs/home/pi/fps_probe.c -``` - -La sonda **se compila sola en la Pi** la primera vez que arranca c_o_n_j_u_r (allí hay -`gcc-6`). Es código ARM: no vale cruzarlo desde el portátil. Si no compilara, el -envoltorio deja la marca `/home/pi/fps_probe.no-compila`, no lo vuelve a intentar y -arranca igual sin medir. - -## Desinstalar - -Quitar solo la medida de fps, dejando el log de shaders como estaba: - -```bash -rm /home/pi/fps_probe.so -``` - -Dejar el aparato **como venía de fábrica**: - -```bash -cd /home/pi/openframeworks10.1/apps/myApps/c_o_n_j_u_r/bin -rm c_o_n_j_u_r -mv c_o_n_j_u_r.real c_o_n_j_u_r -``` - -El binario original nunca se toca: solo se renombra a `c_o_n_j_u_r.real`. - -## La trampa que costó encontrar - -`stdbuf` hace su trabajo **poniendo `LD_PRELOAD`**. Si se usa el programa `stdbuf` -para quitar el buffer y además se quiere precargar la sonda, stdbuf pisa la variable -y se lleva la sonda por delante: el log sale sin una sola línea `[fps]` y todo -parece correcto. Por eso el envoltorio no llama a `stdbuf`, sino que hace lo mismo -que hace por dentro —precargar `libstdbuf.so` y poner `_STDBUF_O`/`_STDBUF_E`— para -que quepan las dos bibliotecas en el mismo `LD_PRELOAD`. - -Si no encuentra `libstdbuf.so`, arranca como el envoltorio del 2026-09-02 y renuncia -a medir: antes eso que perder el log de shaders. - -## Probado antes de instalar - -El mecanismo se ensayó entero en el portátil contra una EGL falsa: la sonda mide -62,5 fps donde el objetivo eran 62, y las 310 llamadas siguen llegando a la EGL de -verdad. O sea que cuenta bien y no rompe el pintado. diff --git a/instrumentacion/c_o_n_j_u_r.envoltorio b/instrumentacion/c_o_n_j_u_r.envoltorio deleted file mode 100644 index e154742..0000000 --- a/instrumentacion/c_o_n_j_u_r.envoltorio +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash -# ENVOLTORIO TEMPORAL - version 2 (2026-09-04) -# -# Hace lo mismo que el del 2026-09-02 (capturar el log de compilacion de GLSL, que -# r_e_c_u_r no muestra en ninguna parte) y ademas mide los fotogramas por segundo. -# -# PARA DESHACERLO (deja el aparato como estaba): -# cd /home/pi/openframeworks10.1/apps/myApps/c_o_n_j_u_r/bin -# rm c_o_n_j_u_r -# mv c_o_n_j_u_r.real c_o_n_j_u_r -# -# PARA QUITAR SOLO LA MEDIDA DE FPS y dejar el log como estaba: -# rm /home/pi/fps_probe.so -# El envoltorio arranca igual sin ella. -# -# El log se ACUMULA en /home/pi/conjur.log: cada arranque anade, no pisa el -# anterior. Si crece demasiado, se puede borrar sin consecuencias. -# -# El buffer de salida se desactiva a mano en vez de con el programa "stdbuf". Motivo: -# stdbuf hace su trabajo poniendo LD_PRELOAD, o sea que PISA el nuestro y se lleva la -# sonda por delante. Comprobado. Aqui se hace lo mismo que hace stdbuf por dentro -# --precargar libstdbuf.so y poner _STDBUF_O/_STDBUF_E-- de forma que quepan las dos -# bibliotecas en el mismo LD_PRELOAD. Sin buffer importa: si no, al apagar el aparato -# se perderia justo lo ultimo que escribio, que suele ser lo que interesa. - -AQUI="$(dirname "$0")" -LOG=/home/pi/conjur.log -FUENTE=/home/pi/fps_probe.c -SONDA=/home/pi/fps_probe.so -FALLO=/home/pi/fps_probe.no-compila - -# La sonda se compila EN LA PI y una sola vez. Si falla se deja una marca y no se -# vuelve a intentar en cada arranque, para no llenar el log del mismo error. -if [ -f "$FUENTE" ] && [ ! -f "$SONDA" ] && [ ! -f "$FALLO" ]; then - { - echo "--- compilando la sonda de fps: $(date '+%Y-%m-%d %H:%M:%S') ---" - if gcc -shared -fPIC -O2 -o "$SONDA.parcial" "$FUENTE" -ldl 2>&1; then - mv "$SONDA.parcial" "$SONDA" - echo "--- sonda lista ---" - else - rm -f "$SONDA.parcial" - date > "$FALLO" - echo "--- la sonda no compila; se sigue sin ella ---" - fi - } >> "$LOG" 2>&1 -fi - -{ - echo "" - echo "==================================================================" - echo "arranque: $(date '+%Y-%m-%d %H:%M:%S')" - if [ -f "$SONDA" ]; then - echo "midiendo fps (una linea [fps] cada 2 s)" - else - echo "sin medida de fps" - fi - echo "==================================================================" -} >> "$LOG" 2>/dev/null - -LIBSTDBUF="$(ls /usr/lib/*/coreutils/libstdbuf.so /usr/lib/coreutils/libstdbuf.so \ - /usr/libexec/*/libstdbuf.so 2>/dev/null | head -1)" - -if [ -n "$LIBSTDBUF" ]; then - # Camino bueno: sin buffer Y con sonda. - export _STDBUF_O=0 _STDBUF_E=0 - if [ -f "$SONDA" ]; then - export LD_PRELOAD="$LIBSTDBUF:$SONDA" - else - export LD_PRELOAD="$LIBSTDBUF" - fi - # Si el .so estuviera roto o fuera de otra arquitectura, el cargador lo avisa por - # la salida de error y sigue adelante sin el: LD_PRELOAD no aborta el arranque. - exec "$AQUI/c_o_n_j_u_r.real" "$@" >> "$LOG" 2>&1 -else - # No aparece libstdbuf. Se vuelve al arranque de siempre, tal cual estaba el - # 2026-09-02, y se renuncia a medir: antes que perder el log de shaders. - echo "sin libstdbuf: se arranca como antes y no se mide fps" >> "$LOG" 2>/dev/null - exec /usr/bin/stdbuf -o0 -e0 "$AQUI/c_o_n_j_u_r.real" "$@" >> "$LOG" 2>&1 -fi diff --git a/instrumentacion/fps-por-shader.py b/instrumentacion/fps-por-shader.py deleted file mode 100755 index ba9c5d9..0000000 --- a/instrumentacion/fps-por-shader.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python3 -"""Lee un conjur.log y saca a cuantos fps iba cada shader. - -El envoltorio de /instrumentacion deja dos cosas en el mismo log y en orden: el -fuente completo de cada shader que se carga, y una linea [fps] cada dos segundos. -Cruzarlas da lo que hasta ahora se averiguaba a base de viajes al aparato: cuanta -complejidad cabe antes de que la Pi deje de seguir el ritmo. - -Uso: - ./fps-por-shader.py /ruta/a/conjur.log - -El log no guarda el nombre del archivo --- r_e_c_u_r no lo manda --- asi que cada -shader se identifica por su primera linea de comentario. Por eso conviene que los -ports lleven una cabecera con su nombre, cosa que las plantillas ya hacen. - -Medialab Terata -""" -import re -import sys -from statistics import median - -MARCA = re.compile(r"//([012])-input") -ARRANQUE = re.compile(r"^arranque:") -FPS = re.compile(r"\[fps\]\s+([\d.]+)\s+fps") -REPORTA = "GL_FRAGMENT_SHADER shader reports:" - - -def nombrar(fuente): - """Saca un nombre legible de las primeras lineas del shader.""" - tipo = "" - titulo = "" - for linea in fuente[:12]: - m = MARCA.search(linea) - if m and not tipo: - tipo = m.group(1) + "-input" - continue - t = linea.strip() - if t.startswith("//") and not titulo: - t = t.lstrip("/").strip() - if t and not t.lower().startswith(("original:", "autor:", "licencia:")): - titulo = t - if not titulo: - titulo = "(sin cabecera)" - return f"{titulo[:52]} [{tipo or '?'}]" - - -def leer(ruta): - """Devuelve [(nombre, [fps, ...]), ...] en orden de carga.""" - tramos = [] - actual = None - with open(ruta, encoding="utf-8", errors="replace") as f: - lineas = f.readlines() - - i = 0 - while i < len(lineas): - linea = lineas[i] - if ARRANQUE.match(linea): - # Reinicio del aparato. Lo que se midiera antes NO le toca al shader que - # estuviera cargado en el arranque anterior: al reiniciar no hay ninguno - # puesto hasta que r_e_c_u_r carga el primero, y las medidas empiezan - # antes. Sin esto, el reposo se le atribuye al ultimo shader de la sesion - # anterior, que es justo el error que hizo aparecer un 8 pasos a 30 fps. - actual = ("(reposo: arrancando, sin shader cargado)", []) - tramos.append(actual) - i += 1 - continue - if REPORTA in linea: - # El fuente viene justo detras. Se corta al siguiente mensaje del log. - fuente = [] - j = i + 1 - while j < len(lineas) and not lineas[j].startswith(("[verbose]", "[notice ]", "[ error ]")): - fuente.append(lineas[j]) - j += 1 - if any(MARCA.search(x) for x in fuente[:12]): - actual = (nombrar(fuente), []) - tramos.append(actual) - i = j - continue - m = FPS.search(linea) - if m and actual is not None: - actual[1].append(float(m.group(1))) - i += 1 - return tramos - - -def main(): - if len(sys.argv) != 2: - print(__doc__.strip()) - return 2 - tramos = leer(sys.argv[1]) - if not tramos: - print("No hay ninguna carga de shader de usuario en ese log.") - print("Si tampoco hay lineas [fps], es que la sonda no llego a cargarse:") - print("mira si el log dice 'sin libstdbuf' o 'la sonda no compila'.") - return 1 - - hay_fps = any(v for _, v in tramos) - print(f"{'shader':<58} {'muestras':>8} {'fps':>6} {'min':>6} {'max':>6}") - print("-" * 88) - for nombre, vals in tramos: - # La primera medida tras cargar pilla el arranque a medias: se descarta. - utiles = vals[1:] if len(vals) > 1 else vals - if utiles: - print(f"{nombre:<58} {len(utiles):>8} {median(utiles):>6.1f} " - f"{min(utiles):>6.1f} {max(utiles):>6.1f}") - else: - print(f"{nombre:<58} {'--':>8} {'--':>6} {'--':>6} {'--':>6}") - - if not hay_fps: - print() - print("Cargas si, medidas no: la sonda de fps no estaba activa en este log.") - else: - print() - print("La columna fps es la mediana. Un shader que va por debajo de unos 15") - print("pierde los golpes cortos: una envolvente de 0.1 s cabe entre dos") - print("fotogramas y no llega a verse nunca.") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/plantillas/test-orientacion.frag b/plantillas/test-orientacion.frag deleted file mode 100644 index 7cd86b1..0000000 --- a/plantillas/test-orientacion.frag +++ /dev/null @@ -1,57 +0,0 @@ -//1-input -// DIAGNOSTICO - no es un efecto, es para resolver dudas sobre la maquina. -// Medialab Terata. -// -// Que mirar en pantalla: -// -// 1. EJE Y. La franja ROJA marca v_texcoord.y cerca de 0. -// - Si la franja roja sale ARRIBA -> v_texcoord.y crece hacia abajo. -// Los shaders de Shadertoy saldran del reves: hay que voltear con -// vec2 uv = vec2(v_texcoord.x, 1.0 - v_texcoord.y); -// - Si sale ABAJO -> coincide con Shadertoy, no hay que tocar nada. -// -// 2. EJE X. La franja VERDE marca v_texcoord.x cerca de 0 (deberia ser -// el borde izquierdo). -// -// 3. RESOLUCION. El damero de la mitad derecha tiene celdas de 32 px reales. -// Contando celdas se comprueba a que resolucion trabaja u_resolution. -// -// 4. TIEMPO. La esquina inferior derecha parpadea con u_time: sirve para ver -// si el shader corre, si esta congelado o si va hacia atras. - -#ifdef GL_ES -precision mediump float; -#endif - -varying vec2 v_texcoord; -uniform sampler2D u_tex0; -uniform vec2 u_resolution; -uniform float u_time; -uniform float u_x0; -uniform float u_x1; -uniform float u_x2; -uniform float u_x3; - -void main(){ - vec2 uv = v_texcoord; - vec3 color = texture2D(u_tex0, uv).rgb * 0.35; - - // Franja roja en Y ~ 0 - if(uv.y < 0.06){ color = vec3(1.0, 0.0, 0.0); } - // Franja verde en X ~ 0 - if(uv.x < 0.06){ color = vec3(0.0, 1.0, 0.0); } - - // Damero de 32 px en la mitad derecha - if(uv.x > 0.5){ - vec2 px = uv * u_resolution; - float celda = mod(floor(px.x/32.0) + floor(px.y/32.0), 2.0); - color = mix(color, vec3(celda), 0.6); - } - - // Parpadeo con el tiempo, esquina inferior derecha en coordenadas UV - if(uv.x > 0.9 && uv.y > 0.9){ - color = vec3(step(0.5, fract(u_time))); - } - - gl_FragColor = vec4(color, 1.0); -} diff --git a/plantillas/test-precision.frag b/plantillas/test-precision.frag deleted file mode 100644 index 981b88d..0000000 --- a/plantillas/test-precision.frag +++ /dev/null @@ -1,57 +0,0 @@ -//0-input -// DIAGNOSTICO - no es un efecto. Contesta de una vez: en este aparato, que es -// "mediump" de verdad? -// -// Importa porque la especificacion de GLES 2.0 solo EXIGE que mediump tenga 10 bits de -// mantisa (fp16). Muchas GPU moviles lo implementan en fp32 y entonces da igual. En -// esta no lo sabiamos, y la diferencia decide si un shader que mide diferencias -// pequenas lejos del origen funciona o sale plano. -// -// COMO LEERLO. Tres franjas verticales, de izquierda a derecha: -// -// 1. izquierda BLANCA si 120.0 + 0.001 se distingue de 120.0 -// 2. centro BLANCA si 1.0 + 0.0005 se distingue de 1.0 -// 3. derecha BLANCA si 1.0e-5 sigue siendo distinto de cero -// -// Las TRES blancas -> mediump se comporta como fp32. Sin problema. -// Las TRES negras -> mediump es fp16 de verdad. -// 1 negra, 2 y 3 blancas -> fp16 con mas rango del minimo; el caso que nos importa -// (medir 0.001 lejos del origen) sigue estando roto. -// -// Los valores se construyen a partir de gl_FragCoord para que el compilador no pueda -// resolver las cuentas al compilar, que es como se cuelan estas pruebas: plegaria la -// suma en la precision del compilador y saldria blanco siempre. -// -// Medialab Terata, 2026-09-05 - -#ifdef GL_ES -precision mediump float; -#endif - -uniform vec2 u_resolution; - -void main() { - vec2 uv = gl_FragCoord.xy / u_resolution.xy; - - // step(-1.0, uv.x) vale 1.0 siempre, pero el compilador no lo sabe. - float uno = step(-1.0, uv.x); - - float a = 120.0 * uno; - float b = a + 0.001; - float p1 = step(0.5, (b > a) ? 1.0 : 0.0); - - float c = 1.0 * uno; - float d = c + 0.0005; - float p2 = step(0.5, (d > c) ? 1.0 : 0.0); - - float e = 1.0e-5 * uno; - float p3 = step(0.5, (e > 0.0) ? 1.0 : 0.0); - - float v = uv.x < 0.3333 ? p1 : (uv.x < 0.6667 ? p2 : p3); - - // Lineas finas de separacion, para contar las franjas sin dudar. - float borde = 0.0; - if (abs(uv.x - 0.3333) < 0.004 || abs(uv.x - 0.6667) < 0.004) borde = 1.0; - - gl_FragColor = vec4(vec3(v) * (1.0 - borde) + vec3(0.6, 0.0, 0.0) * borde, 1.0); -} diff --git a/plantillas/template-0-input.frag b/plantilles/template-0-input.frag similarity index 100% rename from plantillas/template-0-input.frag rename to plantilles/template-0-input.frag diff --git a/plantillas/template-1-input.frag b/plantilles/template-1-input.frag similarity index 100% rename from plantillas/template-1-input.frag rename to plantilles/template-1-input.frag diff --git a/plantillas/template-2-input.frag b/plantilles/template-2-input.frag similarity index 100% rename from plantillas/template-2-input.frag rename to plantilles/template-2-input.frag diff --git a/plantilles/test-orientacio.frag b/plantilles/test-orientacio.frag new file mode 100644 index 0000000..4fbda06 --- /dev/null +++ b/plantilles/test-orientacio.frag @@ -0,0 +1,57 @@ +//1-input +// DIAGNOSTIC - no es un efecte, es per resoldre dubtes sobre la maquina. +// Medialab Terata. +// +// Que mirar a la pantalla: +// +// 1. EIX Y. La franja VERMELLA marca v_texcoord.y a prop de 0. +// - Si la franja vermella surt A DALT -> v_texcoord.y creix cap avall. +// Els shaders de Shadertoy sortiran del reves: cal capgirar amb +// vec2 uv = vec2(v_texcoord.x, 1.0 - v_texcoord.y); +// - Si surt A BAIX -> coincideix amb Shadertoy, no cal tocar res. +// +// 2. EIX X. La franja VERDA marca v_texcoord.x a prop de 0 (hauria de ser +// la vora esquerra). +// +// 3. RESOLUCIO. L'escaquer de la meitat dreta te caselles de 32 px reals. +// Comptant caselles es comprova a quina resolucio treballa u_resolution. +// +// 4. TEMPS. El canto inferior dret parpelleja amb u_time: serveix per veure +// si el shader corre, si esta congelat o si va enrere. + +#ifdef GL_ES +precision mediump float; +#endif + +varying vec2 v_texcoord; +uniform sampler2D u_tex0; +uniform vec2 u_resolution; +uniform float u_time; +uniform float u_x0; +uniform float u_x1; +uniform float u_x2; +uniform float u_x3; + +void main(){ + vec2 uv = v_texcoord; + vec3 color = texture2D(u_tex0, uv).rgb * 0.35; + + // Franja vermella a Y ~ 0 + if(uv.y < 0.06){ color = vec3(1.0, 0.0, 0.0); } + // Franja verda a X ~ 0 + if(uv.x < 0.06){ color = vec3(0.0, 1.0, 0.0); } + + // Escaquer de 32 px a la meitat dreta + if(uv.x > 0.5){ + vec2 px = uv * u_resolution; + float casella = mod(floor(px.x/32.0) + floor(px.y/32.0), 2.0); + color = mix(color, vec3(casella), 0.6); + } + + // Parpelleig amb el temps, canto inferior dret en coordenades UV + if(uv.x > 0.9 && uv.y > 0.9){ + color = vec3(step(0.5, fract(u_time))); + } + + gl_FragColor = vec4(color, 1.0); +} diff --git a/plantillas/test-orientation.frag b/plantilles/test-orientation.frag similarity index 100% rename from plantillas/test-orientation.frag rename to plantilles/test-orientation.frag diff --git a/plantilles/test-precision.frag b/plantilles/test-precision.frag new file mode 100644 index 0000000..8c356aa --- /dev/null +++ b/plantilles/test-precision.frag @@ -0,0 +1,57 @@ +//0-input +// DIAGNOSTIC - no es un efecte. Contesta d'un cop: en aquest aparell, que es +// "mediump" de debo? +// +// Importa perque l'especificacio de GLES 2.0 nomes EXIGEIX que mediump tingui 10 bits +// de mantissa (fp16). Moltes GPU mobils ho implementen en fp32 i aleshores tant es. En +// aquesta no ho sabiem, i la diferencia decideix si un shader que mesura diferencies +// petites lluny de l'origen funciona o surt pla. +// +// COM LLEGIR-HO. Tres franges verticals, d'esquerra a dreta: +// +// 1. esquerra BLANCA si 120.0 + 0.001 es distingeix de 120.0 +// 2. centre BLANCA si 1.0 + 0.0005 es distingeix de 1.0 +// 3. dreta BLANCA si 1.0e-5 segueix sent diferent de zero +// +// Les TRES blanques -> mediump es comporta com fp32. Cap problema. +// Les TRES negres -> mediump es fp16 de debo. +// 1 negra, 2 i 3 blanques -> fp16 amb mes rang del minim; el cas que ens importa +// (mesurar 0.001 lluny de l'origen) segueix trencat. +// +// Els valors es construeixen a partir de gl_FragCoord perque el compilador no pugui +// resoldre els comptes en compilar, que es com es colen aquestes proves: plegaria la +// suma en la precisio del compilador i sortiria blanc sempre. +// +// Medialab Terata, 2026-09-05 + +#ifdef GL_ES +precision mediump float; +#endif + +uniform vec2 u_resolution; + +void main() { + vec2 uv = gl_FragCoord.xy / u_resolution.xy; + + // step(-1.0, uv.x) val 1.0 sempre, pero el compilador no ho sap. + float u = step(-1.0, uv.x); + + float a = 120.0 * u; + float b = a + 0.001; + float p1 = step(0.5, (b > a) ? 1.0 : 0.0); + + float c = 1.0 * u; + float d = c + 0.0005; + float p2 = step(0.5, (d > c) ? 1.0 : 0.0); + + float e = 1.0e-5 * u; + float p3 = step(0.5, (e > 0.0) ? 1.0 : 0.0); + + float v = uv.x < 0.3333 ? p1 : (uv.x < 0.6667 ? p2 : p3); + + // Linies fines de separacio, per comptar les franges sense dubtar. + float vora = 0.0; + if (abs(uv.x - 0.3333) < 0.004 || abs(uv.x - 0.6667) < 0.004) vora = 1.0; + + gl_FragColor = vec4(vec3(v) * (1.0 - vora) + vec3(0.6, 0.0, 0.0) * vora, 1.0); +}