PS5-style UI: backdrop blur, time-based animation, and two new theme elements#1
Draft
danielnuld wants to merge 13 commits into
Draft
PS5-style UI: backdrop blur, time-based animation, and two new theme elements#1danielnuld wants to merge 13 commits into
danielnuld wants to merge 13 commits into
Conversation
added 13 commits
July 12, 2026 00:02
Añade la propuesta OpenSpec para extender OPL con: - backdrop blur en el GS por render-to-texture, sin shaders - animación de UI basada en tiempo (no en frames) - elementos de tema FrostedPanel y CardShelf - un tema PS5 incluido, no predeterminado El cambio es aditivo: los temas existentes no se alteran. Sólo planificación. Ningún cambio de código todavía.
La auditoría del consumo de VRAM invalidó la premisa original. - OPL usa el TexManager de gsKit: la VRAM tras CurrentPointer es un pool de STREAMING, no memoria residente. Reservar los buffers de blur no puede fallar; encoge el pool. El riesgo real es thrashing de texturas, no un fallo de asignación. - Los assets integrados suman 6,508 KiB en CT32, más que la eDRAM total. OPL los hace caber paletizando (background.png es 1024x512 T4: 256 KiB en vez de 2 MiB). - Hallazgo inesperado: el consumidor grande no es el blur (210 KiB) sino el CardShelf, que quiere 7 portadas simultáneas (~784 KiB). Hay que dimensionarlas antes de implementarlo.
Añade src/rmblur.c, el módulo que reapunta FRAME a un buffer de VRAM para
reducir y desenfocar el framebuffer. Aislado de renderman.c a propósito: es
la operación más invasiva del proyecto y así el blast radius queda acotado.
renderman.c sólo gana dos líneas: rmBlurInit() tras gsKit_init_screen() (la
reserva de VRAM tiene que ir después de que CurrentPointer se haya movido) y
rmBlurEnd() en rmEnd().
Tres cosas que el diseño tenía mal o no contemplaba, encontradas al escribir
el código contra las fuentes de gsKit:
1. El segundo nivel de la pirámide NO puede ser de 160 de ancho. gsKit
escribe ese buffer con FRAME.FBW = Width/64, que TRUNCA (160 → 2), y lo
lee con TEX0.TBW = ceil(Width/64), que REDONDEA (160 → 3). Stride de
escritura 128, stride de lectura 192: el buffer sale rasgado. Pasa a 128,
que es múltiplo exacto de 64 y coincide bajo ambos redondeos.
2. Los render targets van en GSKIT_ALLOC_SYSBUFFER, no USERBUFFER. FRAME.FBP
direcciona la VRAM en unidades de 8192 bytes; USERBUFFER alinea a 256, así
que la dirección se truncaría y el GS dibujaría en otro sitio.
3. Los offsets de las pasadas alternan signo ({+0.5, −0.5, +1.5, −1.5}). Un
desplazamiento siempre en el mismo sentido ensancha el kernel pero arrastra
la imagen. Alternando, la magnitud crece y la suma es cero.
Compila limpio y enlaza. Pero nada llama todavía a rmBlurBackdrop() —el
enlazador se la come por falta de referencias—, así que no hay un solo píxel
verificado. El enganche es rmDrawFrosted(), fase 3.
rmDrawFrosted() dibuja el backdrop desenfocado en una región y le compone el tinte encima. Vive en renderman.c porque ahí están X_SCALE() y fRenderXOff, que son los que traducen la región lógica de 640x480 a píxeles reales del framebuffer -- y por tanto a UVs de la cadena, que es una reducción del framebuffer entero (anisotropa: 128 de ancho pero H/4 de alto, de ahí que la escala de U y la de V no sean la misma). El backdrop va sin blending, por la razón de siempre: componerlo metería el alpha del framebuffer en la ecuacion, y en CT16S ese canal es de un bit que no controlamos. El tinte es lo unico que compone. Si no hay cadena, rmBlurTexture() devuelve NULL y queda solo el tinte: la degradacion documentada, no un fallo. Y lo que faltaba de verdad: NADIE llamaba a rmBlurBackdrop(), asi que el enlazador se comia el modulo entero -- en el commit anterior solo sobrevivian rmBlurInit y rmBlurEnd. Anadido un test puntual en guiShow() bajo #ifdef __DEBUG (gEnableBlurTest), que es lo que por fin hace ejecutable la tarea 1.4. El tema PS5 lo sustituira en la fase 6. Compila limpio. Ahora si enlazan rmBlurBackdrop, rmBlurTexture y rmDrawFrosted. Sigue sin verificarse un solo pixel: eso es PCSX2.
El panel de cristal se dibuja con el fondo desenfocado dentro y el resto del frame queda intacto. Eso confirma las dos cosas que daban miedo: el bind y el restore del destino de dibujado son correctos, y vaciar la cola con gsKit_queue_exec() antes de cada gsKit_setactive() preserva el orden de dibujado. El riesgo #1 del diseño queda despejado. Lo que PCSX2 no contesta y sigue abierto: los fps reales (el emulador no modela el coste del GS ni la presion sobre el pool del TexManager), PAL, y hardware fisico. Eso es la fase 7.
src/uianim.c. Dos primitivas, porque hacen falta las dos: uiApproach() es suavizado exponencial asintotico, para perseguir un objetivo que se mueve (el foco); uiAdvance() mas los easings tienen duracion definida, para cuando "200 ms" tiene que significar 200 ms. Lo que hace que sea independiente del framerate es la forma 1 - exp(-rate*dt): el paso depende del tiempo transcurrido, no de que se llame a la funcion. Dos pasos de 8 ms acaban exactamente donde uno de 16 ms. El ingenuo cur += (target-cur)*k no tiene esa propiedad y correria un 20% mas lento en PAL, a 50 Hz. El tick va en guiStartFrame(), no en menusys.c como decia la tarea: ahi es donde empieza el frame de verdad, asi que una sola muestra de reloj sirve a todos los elementos y todos ven el mismo delta. Reutiliza el clock() / CLOCKS_PER_SEC que OPL ya usa; no mete un segundo reloj. El delta se recorta a 60 ms. Sin eso, un tiron -escanear un dispositivo, arrancar el DVD- teletransportaria todas las animaciones a su destino. Todo en float y verificado con -Wdouble-promotion y -Wfloat-conversion: cero avisos. El R5900 no tiene doble precision en hardware y cada double se emula por software via libgcc. El panel de prueba de gEnableBlurTest ahora se desplaza con uiApproach(), que es lo que mantiene el modulo enlazado y lo hace visible.
texLoadCover(): filtro de caja de la portada a un tile fijo de 128x192 CT16, 48 KiB. No es una optimizacion, es lo que hace que el shelf quepa en la consola: OPL sube las portadas a resolucion NATIVA, con un tope por textura de 720*512*4 = 1,440 KiB, o sea casi todo el pool de 1,856 KiB para UNA. Funciona hoy porque el menu solo ensena una a la vez. Siete a tamano nativo son entre 3 y 10 MiB: no caben. El filtro corre sobre las filas RGBA de libpng, no sobre el Mem de una GSTEXTURE ya empaquetada. Empaquetar es especifico del GS -el T8 guarda la CLUT swizzleada, el T4 intercambia los nibbles- y filtrar ahi obligaria a deshacer las dos cosas. Por las filas de libpng un solo camino sirve para CT32, CT24, gris y paleta. Y de paso la imagen nativa nunca llega a existir como textura, asi que no puede subir a VRAM ni transitoriamente, que es justo lo que pide la spec. Filtro de caja y no vecino mas cercano: decimar una portada de 512x720 a 128x192 tirando 15 de cada 16 pixeles aliasea de forma muy visible. Arnes para la tarea 0.6: el build DEBUG reescala background.png una vez al arrancar y escribe los ms al log. Ese PNG a proposito, porque es 1024x512 -mas grande que el tope de una portada- y ademas paletizado: peor caso y camino de paleta de una vez. El tile se dibuja para poder mirar la salida del filtro, no solo cronometrarla.
El coste del reescalado se escribia con LOG(), que acaba en vprintf: solo aparece si tienes abierta la consola EE de PCSX2 o ps2link enganchado. En la practica no se ve nada. Ahora las cifras salen dibujadas en el overlay de debug, justo debajo del medidor de FPS y del contador de VRAM, que es donde ya se mira todo lo demas. Se separan las dos mitades del coste, porque no son la misma cosa y solo una es culpa nuestra: PNG <n>ms - decodificar el PNG. Coste que OPL ya pagaba antes. BOX <n>ms - el filtro de caja. Esto es lo que anade este cambio. El LOG() se queda igualmente, para cuando haya ps2link.
Medido sobre background.png (1024x512 paletizado, peor caso -- mas grande que el tope de 720x512 de una portada): decodificar el PNG 152 ms filtro de caja 37 ms Asumible por tres razones, no por una: 1. No bloquea el render. Las portadas se cargan con ioPutRequest en el hilo de IO (ioman.c:200), no en el de dibujado. Los 37 ms no tiran ningun frame. 2. Es una fraccion de lo que OPL ya paga: decodificar el PNG cuesta 152 ms hoy, sin tocar nada. El filtro anade un +24% a algo que ya era caro y ya era asincrono. 3. Ocurre una vez por portada, y el image_cache_t lo cachea. Numeros de PCSX2, no de consola: el emulador suele correr el codigo escalar mas rapido que el EE real, asi que esto da el orden de magnitud y confirma que la idea es viable, pero NO cierra la fase 7. Si en hardware sale caro, la salida es muestrear el filtro cada 2 pixeles: 4x mas rapido y poca perdida visible a 128x192.
Los dos tipos nuevos se anaden AL FINAL de elementsType[] y no se reordena nada. Un tema que no los nombra no los instancia, no reserva VRAM de blur y renderiza igual que antes. Es tambien la condicion para que esto sea upstreameable: una reescritura de la UI no se mergea nunca; una extension aditiva del sistema de temas, si. FrostedPanel envuelve rmDrawFrosted. El blur es una operacion de frame entero, pero un tema puede poner varios paneles: se desenfoca una sola vez, en el primero que dibuje, guardado con guiFrameId. CardShelf: fila de portadas, la enfocada crece y se eleva, las demas se atenuan. El crecimiento y la elevacion se derivan de la proximidad al foco ANIMADO (uiApproach), no al indice seleccionado -- por eso suavizan en vez de saltar cuando cambias de juego. Atenuar modulando el color (0x80 es el neutro del GS) evita tener que dibujar ningun borde. Culling: solo recorre la pagina que OPL ya calcula, nunca la lista entera. Como pide los tiles reescalados: image_cache_t gana un campo psm. CT24 (el defecto) significa "a tamano nativo", que es lo que quiere todo elemento existente; CT16 pide el tile de 128x192. Los tres backends ya recibian un psm que IGNORABAN; ahora lo propagan. El CardShelf lleva cache propia a proposito. initMutableImage deduplica cachees por patron de arte, y un ItemCover del mismo tema tambien usa "COV": compartirla mandaria a uno de los dos por el cargador equivocado -- o el shelf recibiria portadas a tamano nativo (justo lo que no cabe), o el ItemCover recibiria tiles de 128x192. Y lo que arregla la tarea 5.5: rmBlurInit() YA NO RESERVA NADA. El modo de video se fija antes de cargar el tema (opl.c, applyConfig), asi que ahi es imposible saber si habra cristal. La cadena se reclama en el primer rmBlurBackdrop(), de modo que un tema antiguo nunca entra ahi y nunca paga los 224 KiB. Mas themes/PS5/conf_theme.cfg. El orden de los elementos no es cosmetico: FrostedPanel desenfoca lo ya dibujado, asi que el fondo va antes y el shelf despues, para quedar nitido encima del cristal.
Elemento nuevo CoverWallpaper. La gracia es que no hace falta maquinaria nueva para desenfocarlo: la cadena samplea el FRAMEBUFFER, asi que si el wallpaper dibuja primero la portada estirada, el framebuffer ya la contiene y un rmDrawFrosted() a pantalla completa ES el wallpaper desenfocado. Y la degradacion sale sola: sin blur, rmDrawFrosted dibuja solo el tinte y deja la portada nitida debajo -- exactamente el fallback que pide la spec, sin escribir una linea para ello. El velo va en degradado (rmDrawRectGradient, un solo quad gouraud: el GS interpola el color entre vertices gratis, asi que cuesta lo mismo que un rectangulo plano). Mas oscuro abajo, que es donde se apoya el titulo. No es decoracion: una portada puede ser muy clara y el texto de la UI es blanco. Se estira un tile de 128x192 a pantalla completa y da igual, porque acaba desenfocado. Eso mantiene la portada nativa -hasta 1,440 KiB- fuera de VRAM. Transicion: crossfade de 350 ms. Hizo falta rmDrawPixmapBlend() porque rmDrawQuad decide el alpha SEGUN EL FORMATO y solo lo activa para CT32: un tile CT16 era literalmente imposible de fundir. El crossfade guarda punteros a GSTEXTURE, no a items del menu. Los GSTEXTURE de una cache viven lo que vive la cache; un item de submenu puede liberarse bajo tus pies al reconstruirse la lista de dispositivos. En el peor caso se mezcla el arte equivocado durante unos cientos de ms, pero nunca queda colgado. Ajuste: validateBackgroundElems() antepone un Background por defecto si el primer elemento no lo es. Un CoverWallpaper ES el fondo, asi que ahora cuenta como tal; sin eso OPL dibujaba debajo un fondo a pantalla completa que quedaba tapado del todo, mas una cache que no leia nadie.
No hay hardware fisico y no lo va a haber, asi que el plan original -"no se pasa de fase sin correr en consola"- deja de ser cumplible. En vez de dejar la fase 7 pendiente para siempre, se separa lo que SI se puede cerrar sin consola de lo que hay que preguntarle a la comunidad. Lo que se cierra: el RELLENO SE CALCULA. Son pixeles escritos por frame, no dependen de nada que PCSX2 falsee. La cadena de desenfoque escribe 143,360 px = media pantalla, entre el 0.7% y el 1.7% del presupuesto de un frame a 60 Hz segun lo que se asuma que rinde el GS. El requisito de la spec (<20%) se cumple con margen enorme, y eso no depende de medir nada. Lo que queda abierto, y NO es el blur: el tema completo apila ~6.1 pantallas de relleno por frame (9%-21% del frame). Eso es la composicion, no la cadena. Solo se mide en hardware. Igual que el thrashing del TexManager, que se manifiesta como perdida de fps y no como fallo visual. Y de paso, recortado el desperdicio obvio que destaparon las cuentas: en estado estable el wallpaper dibujaba la capa saliente del crossfade y luego la entrante encima, opaca del todo. Una pantalla completa de relleno tirada en cada frame. Ahora solo se dibujan las dos capas mientras el fundido esta en curso. La fase 8 pasa a ser una PR en borrador que pide testers: la comunidad de OPL si tiene hardware, y una PR honesta con una build de debug y tres preguntas concretas vale mas que un cambio que nadie puede validar.
#1, master <- feature/ps5-ui. En el fork y no contra ps2homebrew: revisar en casa antes de exponerlo. El cuerpo va en ingles a proposito, para poder re-apuntar la PR a upstream sin reescribirla. Los mensajes de commit, en cambio, siguen en espanol: habria que rehacerlos antes de mandarla a ps2homebrew.
danielnuld
pushed a commit
that referenced
this pull request
Jul 12, 2026
#1, master <- feature/ps5-ui. En el fork y no contra ps2homebrew: revisar en casa antes de exponerlo. El cuerpo va en ingles a proposito, para poder re-apuntar la PR a upstream sin reescribirla. Los mensajes de commit, en cambio, siguen en espanol: habria que rehacerlos antes de mandarla a ps2homebrew. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
danielnuld
force-pushed
the
feature/ps5-ui
branch
from
July 12, 2026 17:49
4ea1dbd to
0a9ae6e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
An additive PS5-style visual language for OPL: backdrop blur ("frosted glass"), a real animation engine, three new theme element types, and an included
PS5theme that composes them.Nothing is rewritten. OPL already had everything needed — gsKit, FreeType, a data-driven theme system, cover art downloading. This grafts onto that.
No existing theme changes. The new element types are appended to the end of
elementsType[]; nothing is reordered. A theme that doesn't name them doesn't instantiate them, and — importantly — doesn't even reserve the blur's VRAM:rmBlurInit()allocates nothing, and the chain is claimed lazily on the firstrmBlurBackdrop().The interesting part: blur without shaders
The GS can sample its own framebuffer and chain bilinear taps. A reduction pyramid plus ping-pong passes gives a gaussian-ish blur for a fraction of a frame. No shaders required.
Three things that are easy to get wrong, and cost real debugging:
FRAME.FBW = Width / 64(truncating) but reads it back withTEX0.TBW = ceil(Width / 64). A 160-wide RT is written at a 128-texel stride and sampled at 192: torn. The chain uses 320 and 128.GSKIT_ALLOC_SYSBUFFER.FRAME.FBPaddresses VRAM in 8192-byte units;USERBUFFERonly aligns to 256, so the base address would be truncated and the GS would draw somewhere else.gsKit_setactive()emitsFRAMEimmediately — it does not queue it. The draw queue has to be drained withgsKit_queue_exec()before every target switch, or queued primitives land in the new target.New API
rmBlurBackdrop()/rmDrawFrosted()inrenderman(the chain itself is isolated insrc/rmblur.c— it repointsFRAMEmid-frame, which is the most invasive thing here, so it stays behind the narrowest surface that works)src/uianim.c— time-based easing. The1 - exp(-rate*dt)form is what makes it framerate-independent: two 8 ms steps land exactly where one 16 ms step does. Frame-indexed easing would run 20% slow in PAL.FrostedPanel,CardShelf,CoverWallpapertheme element typestexLoadCover()— box-filters cover art down to a fixed 128×192 CT16 tile on the EEWhy the cover rescaler is a hard prerequisite, not an optimization
OPL uploads covers at the PNG's native resolution, capped only by
maxSize = 720*512*4= 1,440 KiB per texture (textures.c). That is most of the ~1,856 KiB TexManager pool spent on one cover. It works today because the menu only ever shows one at a time and the pool streams.A shelf of seven wants 3–10 MiB. That does not fit in the console. So shelf covers are box-filtered to a 128×192 CT16 tile (48 KiB) at load time, cached, and the native image is never allocated as a texture at all — it cannot reach VRAM even transiently.
The filter runs over libpng's decoded RGBA rows rather than over a
GSTEXTURE, which sidesteps the GS-specific packing entirely (T8 stores a swizzled CLUT, T4 swaps its nibbles) and keeps one code path for CT32, CT24, grayscale and palette.Cost, measured in PCSX2 on a 1024×512 palettized PNG (worse than any real cover): PNG decode 152 ms, box filter 37 ms. It runs on the IO thread (
ioPutRequest), not the render thread, so it drops no frames — and it's a +24% on a decode cost OPL already pays.Measurements
VRAM — measured in PCSX2. This one is reliable: it's address accounting, not performance.
The chain costs 224 KiB, not the 210 first estimated:
gsKit_texture_size()aligns to pages, so the RT heights round up. It comes out the same in PAL, because those heights land on the same aligned boundaries.Fill rate — calculated. Pixels written per frame don't depend on anything an emulator fakes.
The blur chain writes
320·224 + 128·112 + 4·(128·112)= 143,360 px = half a screen.Verified in PCSX2. NOT verified on real hardware. I no longer have a physical PS2, which is why this is a draft.
PCSX2 is fine for VRAM accounting and for confirming that pixels land where they should. It is useless for frame rate: it models neither GS fill cost nor TexManager pressure.
Three questions I cannot answer, and would be grateful for help with:
Build with
make DEBUG=1: the debug overlay shows fps,KiB FIXED/KiB TEXMAN, and the cover rescaler's timings — exactly what's needed to answer the above.To try the theme: copy
themes/PS5/to<device>/THM/PS5/and select it in Settings → Interface. It is not the default.Degradation
CoverWallpaperdraws the cover sharp, and the gradient veil still guarantees text contrastThe UI stays usable in every case.