diff --git a/skeleton/BASE/README.txt b/skeleton/BASE/README.txt index 5f6b965bd..ad509c5a0 100644 --- a/skeleton/BASE/README.txt +++ b/skeleton/BASE/README.txt @@ -97,6 +97,21 @@ Cheats use RetroArch .cht file format. Many cheat files are here /.cfg`. Gamepad type is not written to generated set overrides. This tag override freezes those set-specific values for that emulator until it is regenerated or removed. + +Save for game continues to save the currently effective shader values into the game config. The active set stays visually in control; selecting Disabled reveals the saved game values again. + ---------------------------------------- Disc-based games diff --git a/skeleton/BASE/Shaders/glsl/crt-perfect.glsl b/skeleton/BASE/Shaders/glsl/crt-perfect.glsl new file mode 100644 index 000000000..ebda5a26b --- /dev/null +++ b/skeleton/BASE/Shaders/glsl/crt-perfect.glsl @@ -0,0 +1,251 @@ +// crt-perfect v14 - scanlines, an RGB mask and curvature, pixel-perfect scale. +// ----------------------------------------------------------------------------- +// Author: sinedied +// Licence: MIT - Copyright (c) 2026 sinedied +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: the above copyright +// notice and this permission notice shall be included in all copies or +// substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", +// WITHOUT WARRANTY OF ANY KIND. +// ----------------------------------------------------------------------------- +// PARAMETERS +// +// cp_scanlines 0.00 - 1.00 Scanline visibility. 0 disables them. +// cp_rgb_mask 0.00 - 1.00 RGB mask visibility. 0 disables it. +// cp_mask_type 0 / 1 / 2 Off, aperture grille, slot grille. +// cp_mask_size 0.25 - 2.00 Mask triads per source pixel. +// cp_min_pitch 2.00 - 6.00 Smallest pattern pitch, in output pixels. +// cp_curvature 0.00 - 0.15 Screen curvature. 0 disables it. +// cp_brightness 0.25 - 4.00 Output gain. 1.00 disables it. +// cp_gamma 0.50 - 2.00 Output gamma. 1.00 disables it. +// ----------------------------------------------------------------------------- +// A CRT look: soft scanlines and an RGB shadow mask over a clean pixel scale, +// with optional screen curvature. Reads like a small tube TV, sharp rather than +// blurry, and neither pattern beats against the pixel grid at any scale. +// +// Notes: +// - Needs a LINEAR filter, set in the preset. Under NEAREST the scale becomes +// ordinary nearest-neighbour and the picture gets ragged edges. +// - Render at the output resolution, 1:1 with the display. +// - At min. pitch 2.00 the mask becomes 2 colours: use 2.50 or more to keep +// the triads visible. +// - Brightness above 1.00 clips, may create pattern artifacts against the +// pixel grid unless the output is an integer scale. + +#pragma parameter cp_scanlines "Scanline visibility" 0.60 0.00 1.00 0.05 +#pragma parameter cp_rgb_mask "RGB mask visibility" 0.20 0.00 1.00 0.05 +#pragma parameter cp_mask_type "Mask 0=off 1=grille 2=slot" 1.00 0.00 2.00 1.00 +#pragma parameter cp_mask_size "Mask triads per pixel" 1.00 0.25 2.00 0.25 +#pragma parameter cp_min_pitch "Min. pitch in px" 3.00 2.00 6.00 0.25 +#pragma parameter cp_curvature "Screen curvature" 0.00 0.00 0.15 0.01 +#pragma parameter cp_brightness "Brightness" 1.25 0.25 4.00 0.05 +#pragma parameter cp_gamma "Gamma" 1.00 0.50 2.00 0.05 + +#if defined(VERTEX) + +#if __VERSION__ >= 130 +#define COMPAT_VARYING out +#define COMPAT_ATTRIBUTE in +#define COMPAT_TEXTURE texture +#else +#define COMPAT_VARYING varying +#define COMPAT_ATTRIBUTE attribute +#define COMPAT_TEXTURE texture2D +#endif + +#ifdef GL_ES +#define COMPAT_PRECISION mediump +#else +#define COMPAT_PRECISION +#endif + +COMPAT_ATTRIBUTE vec4 VertexCoord; +COMPAT_ATTRIBUTE vec4 COLOR; +COMPAT_ATTRIBUTE vec4 TexCoord; +COMPAT_VARYING vec4 COL0; +COMPAT_VARYING vec4 TEX0; + +uniform mat4 MVPMatrix; +uniform COMPAT_PRECISION int FrameDirection; +uniform COMPAT_PRECISION int FrameCount; +uniform COMPAT_PRECISION vec2 OutputSize; +uniform COMPAT_PRECISION vec2 TextureSize; +uniform COMPAT_PRECISION vec2 InputSize; + +void main() +{ + gl_Position = MVPMatrix * VertexCoord; + COL0 = COLOR; + TEX0.xy = TexCoord.xy; +} + +#elif defined(FRAGMENT) + +#if __VERSION__ >= 130 +#define COMPAT_VARYING in +#define COMPAT_TEXTURE texture +out vec4 FragColor; +#else +#define COMPAT_VARYING varying +#define FragColor gl_FragColor +#define COMPAT_TEXTURE texture2D +#endif + +#ifdef GL_ES +#ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +#else +precision mediump float; +#endif +#define COMPAT_PRECISION highp +#else +#define COMPAT_PRECISION +#endif + +uniform COMPAT_PRECISION int FrameDirection; +uniform COMPAT_PRECISION int FrameCount; +uniform COMPAT_PRECISION vec2 OutputSize; +uniform COMPAT_PRECISION vec2 TextureSize; +uniform COMPAT_PRECISION vec2 InputSize; +uniform sampler2D Texture; +COMPAT_VARYING vec4 TEX0; + +#define Source Texture +#define vTexCoord TEX0.xy +#define outsize vec4(OutputSize, 1.0 / OutputSize) + +#define PI 3.141592654 +#define TAU 6.283185307 + +#ifdef PARAMETER_UNIFORM +uniform COMPAT_PRECISION float cp_scanlines; +uniform COMPAT_PRECISION float cp_rgb_mask; +uniform COMPAT_PRECISION float cp_mask_type; +uniform COMPAT_PRECISION float cp_mask_size; +uniform COMPAT_PRECISION float cp_brightness; +uniform COMPAT_PRECISION float cp_min_pitch; +uniform COMPAT_PRECISION float cp_gamma; +uniform COMPAT_PRECISION float cp_curvature; +#else +#define cp_scanlines 0.60 +#define cp_rgb_mask 0.20 +#define cp_mask_type 1.00 +#define cp_mask_size 1.00 +#define cp_brightness 1.25 +#define cp_min_pitch 3.00 +#define cp_gamma 1.00 +#define cp_curvature 0.00 +#endif + +// Average of a unit sinusoid of frequency f, in cycles per output pixel, over +// one pixel-wide box. Reaches zero at one cycle per pixel. +float boxSinc(float f) +{ + float x = PI * max(f, 1e-4); + return sin(x) / x; +} + +// Nothing above Nyquist can be drawn, so fade the pattern out entirely there - +// amplitude and darkening together, leaving no uniform dimming behind. +float nyquistFade(float f) +{ + return 1.0 - smoothstep(0.34, 0.5, f); +} + +void main() +{ + vec2 uv = vTexCoord; + float tube = 1.0; + float noWarp = 1.0; + + if (cp_curvature > 0.0) { + // Dividing by the edge-midpoint value keeps the image edges on the + // screen edges, so only the corners curve away and nothing is cropped. + float norm = 1.0 / (1.0 + cp_curvature); + vec2 c = uv * 2.0 - 1.0; + vec2 cc = c * c; + float r2 = cc.x + cc.y; + + uv = c * (1.0 + cp_curvature * r2) * norm * 0.5 + 0.5; + noWarp = 0.0; + + // The corners now sample past the image, where the sampler would + // stretch the border texel; mask them instead. + vec2 e = outsize.zw; + vec2 aa = clamp(uv / e, 0.0, 1.0) * clamp((1.0 - uv) / e, 0.0, 1.0); + tube = aa.x * aa.y; + } + + // h is deliberately independent of the warp: it keeps everything below it + // uniform across the draw, which the driver can then compute once. + vec2 p = uv * TextureSize; + vec2 h = max(0.4995 * InputSize / OutputSize, 1e-6); + + // B is the nearest texel boundary and w the share of the footprint on its + // low side, handed to the texture unit: one LINEAR tap returns the blend. + vec2 B = floor(p + 0.5); + vec2 w = clamp((B - p + h) / (2.0 * h), 0.0, 1.0); + vec3 color = COMPAT_TEXTURE(Source, (B + 0.5 - w) / TextureSize).rgb; + + // The base is clamped because pow(0, g) is undefined and returns NaN on + // real drivers. 1e-8, not 1e-5, which would lift pure black to 1/255. + if (abs(cp_gamma - 1.0) > 0.001) { + color = pow(max(color, 1e-8), vec3(cp_gamma)); + } + + float scanSrcPitch = OutputSize.y / max(InputSize.y, 1.0); + float scanPitch = max(scanSrcPitch, cp_min_pitch); + float scanLocked = (1.0 - smoothstep(cp_min_pitch * 1.001, cp_min_pitch * 1.02, scanSrcPitch)) * noWarp; + float scanFreq = 1.0 / scanPitch; + + // The flat pitch, used even under curvature: it holds the pattern to one + // cycle per source line, and keeps this uniform across the draw. + float scanLocal = scanFreq; + + float scanAmp = cp_scanlines * mix(nyquistFade(scanLocal), 1.0, scanLocked); + float scanAC = 0.5 * scanAmp * mix(boxSinc(scanLocal), 1.0, scanLocked); + + float scan = 1.0; + if (scanAmp > 0.0) { + float y = uv.y * OutputSize.y - 0.5 * scanLocked; + scan = (1.0 - 0.5 * scanAmp) - scanAC * cos(TAU * fract(y * scanFreq)); + } + + float maskSrcPitch = OutputSize.x / max(InputSize.x * cp_mask_size, 1.0); + float maskPitch = max(maskSrcPitch, cp_min_pitch); + float maskLocked = (1.0 - smoothstep(cp_min_pitch * 1.001, cp_min_pitch * 1.02, maskSrcPitch)) * noWarp; + float maskFreq = 1.0 / maskPitch; + float maskLocal = maskFreq; + + float maskAmp = cp_rgb_mask * mix(nyquistFade(maskLocal), 1.0, maskLocked); + + vec3 mask = vec3(1.0); + if (maskAmp > 0.0 && cp_mask_type >= 0.5) { + float x = uv.x * OutputSize.x - 0.5 * maskLocked; + float phase = x * maskFreq - (1.0 / 6.0); + + if (cp_mask_type >= 1.5) { + float row = floor((uv.y * OutputSize.y - 0.5 * scanLocked) * scanFreq + 1e-3); + phase += 0.5 * mod(row, 2.0); + } + + float dc = 1.0 - 0.5 * maskAmp; + float ac = 0.5 * maskAmp * mix(boxSinc(maskLocal), 1.0, maskLocked); + mask.rg = dc + ac * cos(TAU * (fract(phase) - vec2(0.0, 1.0 / 3.0))); + mask.b = max(3.0 * dc - mask.r - mask.g, 0.0); + } + + // Brightness rides the pattern, so the clamp below lands on the product. + // Clamping the picture instead flattens highlights before the mask shapes + // them. + vec3 gain = sqrt(max(mask * (scan * cp_brightness), 0.0)); + + FragColor = vec4(clamp(color * gain * tube, 0.0, 1.0), 1.0); +} + +#endif diff --git a/skeleton/BASE/Shaders/glsl/dmg-perfect.glsl b/skeleton/BASE/Shaders/glsl/dmg-perfect.glsl new file mode 100644 index 000000000..d92e50814 --- /dev/null +++ b/skeleton/BASE/Shaders/glsl/dmg-perfect.glsl @@ -0,0 +1,231 @@ +// dmg-perfect v11 - a Game Boy dot matrix over a pixel-perfect scale. +// ----------------------------------------------------------------------------- +// Licence: MIT - Copyright (c) 2026 sinedied +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: the above copyright +// notice and this permission notice shall be included in all copies or +// substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", +// WITHOUT WARRANTY OF ANY KIND. +// ----------------------------------------------------------------------------- +// PARAMETERS +// +// dp_grid 0.00 - 1.00 Grid visibility. 0 disables it. +// dp_gap 0.25 - 2.00 Grid line thickness, in pixels. +// dp_shadow 0.00 - 1.00 Shadow cast by driven dots. 0 disables it. +// dp_brightness 0.25 - 4.00 Output gain. 1.00 disables it. +// dp_gamma 0.50 - 2.00 Output gamma. 1.00 disables it. +// dp_temperature -1.00 - 1.00 Warm above 0, cool below. 0.00 is off. +// dp_tint -1.00 - 1.00 Green above 0, magenta below. 0.00 is off. +// ----------------------------------------------------------------------------- +// An original Game Boy look: the dot matrix grid with its pale gaps, over a +// clean pixel scale. Dots can cast a shadow so they sit above the panel. The +// grid is invisible on white and strongest on dark content, as a real DMG is. +// +// Notes: +// - Needs a LINEAR filter, set in the preset. Under NEAREST the scale becomes +// ordinary nearest-neighbour and the picture gets ragged edges. +// - Render at the output resolution, 1:1 with the display. +// - Grid line thickness is in output pixels, so the panel reads the same at +// every resolution. 1.00 is a one-pixel line. +// - Brightness above 1.00 clips, may create pattern artifacts against the +// pixel grid unless the output is an integer scale. + +#pragma parameter dp_grid "Grid visibility" 0.30 0.00 1.00 0.01 +#pragma parameter dp_gap "Grid line thickness" 1.00 0.25 2.00 0.05 +#pragma parameter dp_shadow "Dot shadow" 0.00 0.00 1.00 0.01 +#pragma parameter dp_brightness "Brightness" 1.00 0.25 4.00 0.05 +#pragma parameter dp_gamma "Gamma" 1.20 0.50 2.00 0.05 +#pragma parameter dp_temperature "Cool / warm balance" 0.00 -1.00 1.00 0.01 +#pragma parameter dp_tint "Magenta / green balance" 0.00 -1.00 1.00 0.01 + +#if defined(VERTEX) + +#if __VERSION__ >= 130 +#define COMPAT_VARYING out +#define COMPAT_ATTRIBUTE in +#define COMPAT_TEXTURE texture +#else +#define COMPAT_VARYING varying +#define COMPAT_ATTRIBUTE attribute +#define COMPAT_TEXTURE texture2D +#endif + +#ifdef GL_ES +#define COMPAT_PRECISION mediump +#else +#define COMPAT_PRECISION +#endif + +COMPAT_ATTRIBUTE vec4 VertexCoord; +COMPAT_ATTRIBUTE vec4 COLOR; +COMPAT_ATTRIBUTE vec4 TexCoord; +COMPAT_VARYING vec4 COL0; +COMPAT_VARYING vec4 TEX0; + +uniform mat4 MVPMatrix; +uniform COMPAT_PRECISION int FrameDirection; +uniform COMPAT_PRECISION int FrameCount; +uniform COMPAT_PRECISION vec2 OutputSize; +uniform COMPAT_PRECISION vec2 TextureSize; +uniform COMPAT_PRECISION vec2 InputSize; + +void main() +{ + gl_Position = MVPMatrix * VertexCoord; + COL0 = COLOR; + TEX0.xy = TexCoord.xy; +} + +#elif defined(FRAGMENT) + +#if __VERSION__ >= 130 +#define COMPAT_VARYING in +#define COMPAT_TEXTURE texture +out vec4 FragColor; +#else +#define COMPAT_VARYING varying +#define FragColor gl_FragColor +#define COMPAT_TEXTURE texture2D +#endif + +#ifdef GL_ES +#ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +#else +precision mediump float; +#endif +#define COMPAT_PRECISION highp +#else +#define COMPAT_PRECISION +#endif + +uniform COMPAT_PRECISION int FrameDirection; +uniform COMPAT_PRECISION int FrameCount; +uniform COMPAT_PRECISION vec2 OutputSize; +uniform COMPAT_PRECISION vec2 TextureSize; +uniform COMPAT_PRECISION vec2 InputSize; +uniform sampler2D Texture; +COMPAT_VARYING vec4 TEX0; + +#ifdef PARAMETER_UNIFORM +uniform COMPAT_PRECISION float dp_grid; +uniform COMPAT_PRECISION float dp_gap; +uniform COMPAT_PRECISION float dp_shadow; +uniform COMPAT_PRECISION float dp_brightness; +uniform COMPAT_PRECISION float dp_gamma; +uniform COMPAT_PRECISION float dp_temperature; +uniform COMPAT_PRECISION float dp_tint; +#else +#define dp_grid 0.30 +#define dp_gap 1.0 +#define dp_shadow 0.0 +#define dp_brightness 1.0 +#define dp_gamma 1.2 +#define dp_temperature 0.0 +#define dp_tint 0.0 +#endif + +// The substrate a DMG's gaps show: undriven crystal at its lightest state. +#define DMG_SUBSTRATE 1.0 + +#define LUMA vec3(0.299, 0.587, 0.114) + +// Floor under the paper level the shadow measures opacity against. Must sit +// below the darkest palette anyone ships. +#define PAPER_FLOOR 0.35 + +// In source pixels, so the shadow holds its proportions at every scale. +#define SHADOW_OFFSET vec2(0.45, 0.85) + +// Extra half-width on the box that samples the displaced aperture. Softens the +// aperture's gaps only; the shadow's outer edge comes from the opacity field. +#define APERTURE_SOFT 0.5 + +// Antiderivative of the dot profile. Differencing it gives the exact box mean. +// Peak-normalised, so it is a coverage mask topping out at 1. +vec2 dotInt(vec2 x, vec2 w) +{ + vec2 n = floor(x); + return n * w + clamp(x - n, vec2(0.0), w); +} + +void main() +{ + // Source texels. The max() guards an unset uniform, which is 0 and would + // make h a zero divisor below. + vec2 p = TEX0.xy * TextureSize; + vec2 h = max(0.4995 * InputSize / OutputSize, 1e-6); + vec2 B = floor(p + 0.5); + + // N is the whole scale that fits, so a dp_gap line is dp_gap/N of a cell + // and stays a pixel wide at any scale. The nudge guards floor() on a + // division landing a ULP short. + vec2 sc = OutputSize / max(InputSize, 1.0); + float N = max(floor(min(sc.x, sc.y) + 1e-3), 1.0); + vec2 lit = clamp(vec2(1.0 - dp_gap / N), 1e-3, 1.0); + + // Coverage of the lit dot over this output pixel, exactly, per axis. + vec2 cov = max(dotInt(p + h, lit) - dotInt(p - h, lit), vec2(1e-6)) + / (2.0 * h); + + // Below two output pixels per cell the pattern folds to a coarser pitch, + // so it has to reach zero at two, not at one. + cov = mix(vec2(1.0), cov, smoothstep(vec2(2.0), vec2(2.9), sc)); + float dot2d = cov.x * cov.y; + + // B is the nearest texel boundary and w the share of the footprint on its + // low side, handed to the texture unit: one LINEAR tap returns the blend. + vec2 w = clamp((B - p + h) / (2.0 * h), 0.0, 1.0); + vec3 area = COMPAT_TEXTURE(Texture, (B + 0.5 - w) / TextureSize).rgb; + + // Not applied to the SUBSTRATE, so brightening lifts the dots toward a + // fixed paper rather than washing the whole panel out. + vec3 grade = (1.0 + dp_temperature * vec3(1.0, 0.0, -1.0) + + dp_tint * vec3(-0.5, 1.0, -0.5)) * dp_brightness; + + // Gaps show the substrate, dots show the picture; k is the share of this + // pixel reading as gap. + float k = dp_grid * (1.0 - dot2d); + vec3 col = mix(area * grade, vec3(DMG_SUBSTRATE), k); + + // Multiplies everything rather than darkening the gap colour, which is + // what puts the shadow underneath the dots. + if (dp_shadow > 0.0) { + // In source pixels, so the offset is a fixed fraction of a cell. + vec2 q = p - SHADOW_OFFSET; + + // The dot's own shape, displaced and widened: a box blur of it. + vec2 hs = h + APERTURE_SOFT; + vec2 covS = max(dotInt(q + hs, lit) - dotInt(q - hs, lit), vec2(0.0)) + / (2.0 * hs); + covS = mix(vec2(1.0), covS, smoothstep(vec2(2.0), vec2(2.9), sc)); + + // dot() is linear, so the luma of a blend is the blend of lumas and + // one tap suffices. + float casterLum = dot(COMPAT_TEXTURE(Texture, q / TextureSize).rgb, LUMA); + + // The undriven level to measure opacity against - not white, since no + // Game Boy palette is near it. + float paper = max(dot(area, LUMA), PAPER_FLOOR); + + // Both sides raw, so an output gain cancels out of the ratio. + float opacity = clamp(1.0 - casterLum / paper, 0.0, 1.0); + + col *= 1.0 - dp_shadow * opacity * covS.x * covS.y; + } + + // The base is clamped because pow(0, g) is undefined and returns NaN on + // real drivers. 1e-8, not 1e-5, which would lift pure black to 1/255. + if (abs(dp_gamma - 1.0) > 0.001) { + col = pow(max(col, 1e-8), vec3(dp_gamma)); + } + + FragColor = vec4(clamp(col, 0.0, 1.0), 1.0); +} + +#endif diff --git a/skeleton/BASE/Shaders/glsl/lcd-perfect.glsl b/skeleton/BASE/Shaders/glsl/lcd-perfect.glsl new file mode 100644 index 000000000..1998ffc93 --- /dev/null +++ b/skeleton/BASE/Shaders/glsl/lcd-perfect.glsl @@ -0,0 +1,258 @@ +// lcd-perfect v10 - an LCD matrix and RGB stripes over a pixel-perfect scale. +// ----------------------------------------------------------------------------- +// Licence: MIT - Copyright (c) 2026 sinedied +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: the above copyright +// notice and this permission notice shall be included in all copies or +// substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", +// WITHOUT WARRANTY OF ANY KIND. +// ----------------------------------------------------------------------------- +// PARAMETERS +// +// lp_grid 0.00 - 1.00 Grid visibility. 0 disables it. +// lp_balance 0.00 - 1.00 Row/column balance. 0 rows, 1 columns. +// lp_min_pitch 2.00 - 6.00 Smallest pattern pitch, in output pixels. +// lp_subpixels 0.00 - 1.00 RGB stripe visibility. 0 disables them. +// lp_layout 0 / 1 Stripe order: RGB or BGR. +// lp_brightness 0.25 - 4.00 Output gain. 1.00 disables it. +// lp_gamma 0.50 - 2.00 Output gamma. 1.00 disables it. +// ----------------------------------------------------------------------------- +// A handheld LCD look: a soft backlit mesh with RGB subpixel stripes, over a +// clean pixel scale. Reads like a Game Boy Color or GBA screen in good light - +// a gentle grid rather than a hard black matrix, and it stays even at every +// scale instead of breaking into a pattern. +// +// Notes: +// - Needs a LINEAR filter, set in the preset. Under NEAREST the scale becomes +// ordinary nearest-neighbour and the picture gets ragged edges. +// - Render at the output resolution, 1:1 with the display. +// - Row/column balance sets which axis dominates. Real panels are row-dominant; +// 0.80 or so matches lcd1x. +// - Brightness above 1.00 clips, may create pattern artifacts against the +// pixel grid unless the output is an integer scale. + +#pragma parameter lp_grid "Grid visibility" 0.30 0.00 1.00 0.01 +#pragma parameter lp_balance "Row/column balance" 0.60 0.00 1.00 0.01 +#pragma parameter lp_min_pitch "Minimum pitch in px" 3.00 2.00 6.00 0.25 +#pragma parameter lp_subpixels "RGB stripe visibility" 0.20 0.00 1.00 0.05 +#pragma parameter lp_layout "Stripe order 0=RGB 1=BGR" 0.00 0.00 1.00 1.00 +#pragma parameter lp_brightness "Brightness" 1.25 0.25 4.00 0.05 +#pragma parameter lp_gamma "Gamma" 1.00 0.50 2.00 0.05 + +#if defined(VERTEX) + +#if __VERSION__ >= 130 +#define COMPAT_VARYING out +#define COMPAT_ATTRIBUTE in +#define COMPAT_TEXTURE texture +#else +#define COMPAT_VARYING varying +#define COMPAT_ATTRIBUTE attribute +#define COMPAT_TEXTURE texture2D +#endif + +#ifdef GL_ES +#define COMPAT_PRECISION mediump +#else +#define COMPAT_PRECISION +#endif + +COMPAT_ATTRIBUTE vec4 VertexCoord; +COMPAT_ATTRIBUTE vec4 COLOR; +COMPAT_ATTRIBUTE vec4 TexCoord; +COMPAT_VARYING vec4 COL0; +COMPAT_VARYING vec4 TEX0; + +uniform mat4 MVPMatrix; +uniform COMPAT_PRECISION int FrameDirection; +uniform COMPAT_PRECISION int FrameCount; +uniform COMPAT_PRECISION vec2 OutputSize; +uniform COMPAT_PRECISION vec2 TextureSize; +uniform COMPAT_PRECISION vec2 InputSize; + +void main() +{ + gl_Position = MVPMatrix * VertexCoord; + COL0 = COLOR; + TEX0.xy = TexCoord.xy; +} + +#elif defined(FRAGMENT) + +#if __VERSION__ >= 130 +#define COMPAT_VARYING in +#define COMPAT_TEXTURE texture +out vec4 FragColor; +#else +#define COMPAT_VARYING varying +#define FragColor gl_FragColor +#define COMPAT_TEXTURE texture2D +#endif + +#ifdef GL_ES +#ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +#else +precision mediump float; +#endif +#define COMPAT_PRECISION highp +#else +#define COMPAT_PRECISION +#endif + +uniform COMPAT_PRECISION int FrameDirection; +uniform COMPAT_PRECISION int FrameCount; +uniform COMPAT_PRECISION vec2 OutputSize; +uniform COMPAT_PRECISION vec2 TextureSize; +uniform COMPAT_PRECISION vec2 InputSize; +uniform sampler2D Texture; +COMPAT_VARYING vec4 TEX0; + +#ifdef PARAMETER_UNIFORM +uniform COMPAT_PRECISION float lp_grid; +uniform COMPAT_PRECISION float lp_balance; +uniform COMPAT_PRECISION float lp_min_pitch; +uniform COMPAT_PRECISION float lp_subpixels; +uniform COMPAT_PRECISION float lp_layout; +uniform COMPAT_PRECISION float lp_brightness; +uniform COMPAT_PRECISION float lp_gamma; +#else +#define lp_grid 0.30 +#define lp_balance 0.60 +#define lp_min_pitch 3.00 +#define lp_subpixels 0.20 +#define lp_layout 0.0 +#define lp_brightness 1.25 +#define lp_gamma 1.00 +#endif + +#define TAU 6.283185307 +#define PI 3.141592654 + +// cos and sin of TAU/6, the angle from a cell's centre to the red stripe. +// Green sits half a turn from there, so its pair is (-1, 0) and costs nothing. +#define COS_TAU_6 0.5 +#define SIN_TAU_6 0.866025404 + +// Mean of a unit sinusoid over one output pixel. The mesh gets this exactly +// from the integral below; the stripes are sampled, so they need it named. +float boxSinc(float f) +{ + float x = PI * max(f, 1e-4); + return sin(x) / x; +} + +// Past Nyquist the pattern would fold to a coarser pitch at nearly full +// strength, so take it out entirely instead. +vec2 nyquistFade(vec2 f) +{ + return 1.0 - smoothstep(0.34, 0.5, f); +} + +void main() +{ + vec2 p = TEX0.xy * TextureSize; + vec2 d = max(InputSize / OutputSize, 1e-6); + vec2 B = floor(p + 0.5); + + // Cells per period: a WHOLE number of cells, never a fixed pixel size, so + // the pattern stays periodic on the source grid. The bias is load-bearing - + // a/b is a*rcp(b), so a ratio of exactly 1 can land a hair above it. + vec2 N = max(ceil(lp_min_pitch * d - 1e-4), 1.0); + + vec2 f = d / N; + + // A sinusoid does not band-limit itself, so this cannot be dropped. + vec2 fade = nyquistFade(f); + + // Once a period spans several cells only one boundary in N carries a line, + // so the amplitude is spread back out. N == 1 is untouched. + vec2 amp = clamp(lp_grid * 2.0 * vec2(lp_balance, 1.0 - lp_balance), 0.0, 1.0) + * fade * (2.0 / (N + 1.0)); + + // Half an output pixel, in cycles: puts one sample per cycle on the + // trough, which is what lets a two-pixel pitch resolve at all. + vec2 phase = 0.5 * f; + + // The pattern coordinate, in periods. With N == 1 this is exactly p. + vec2 t = p / N; + vec2 hh = 0.4995 * f; + + // The aperture integral over the footprint, the exact box filter. Both ends + // are symmetric about X, so one sin and one cos of X do the work of four + // and the stripes below reuse the pair. + vec2 X = TAU * (t - phase); + vec2 sinX = sin(X); + vec2 cosX = cos(X); + + vec2 Y = TAU * hh; + vec2 sinY = sin(Y); + vec2 cosY = cos(Y); + vec2 k = amp / TAU; + + // Alo must use the UNCLAMPED Iraw, or the two disagree where I clamps. + vec2 Iraw = 2.0 * hh - k * (2.0 * cosX * sinY); + vec2 Alo = t - 0.5 * Iraw - (k * cosY) * sinX; + vec2 I = max(Iraw, 1e-6); + + // Peak-normalised, so the flat top lands at 1 and nothing meets the clamp. + vec2 g = I * (1.0 / (2.0 * hh * (1.0 + amp))); + float gain = g.x * g.y; + + // The mesh's dark line and the scaler's transition pixel both sit on the + // cell boundary, so the blend is weighted by aperture, not by area. + vec2 Bt = B / N; + vec2 AB = Bt - k * sin(TAU * (Bt - phase)); + vec2 w = clamp((AB - Alo) / I, 0.0, 1.0); + + // One LINEAR tap, handed the weights above: this texcoord asks the texture + // unit for exactly mix(T[B], T[B-1], w). + vec3 color = COMPAT_TEXTURE(Texture, (B + 0.5 - w) / TextureSize).rgb; + + // Three sinusoids 120 degrees apart, summing to exactly 3 at every pixel, + // so they are luminance neutral and blue costs no third cosine. + vec3 stripe = vec3(1.0); + if (lp_subpixels > 0.0) { + float sinc = boxSinc(f.x); + float ac = lp_subpixels * sinc * fade.x; + vec2 rg = 1.0 + ac * vec2(COS_TAU_6 * cosX.x + SIN_TAU_6 * sinX.x, + -cosX.x); + stripe = vec3(rg, 3.0 - rg.x - rg.y); + + // A column mesh and the stripes share a pitch, so whichever stripe + // lands on the dark line is dimmed. Divide the cast back out; M must be + // the BOX-FILTERED amplitude or it overshoots where the filter bites. + float M = amp.x * sinc; + vec3 corr = 1.0 - 0.5 * M * ac * vec3(COS_TAU_6, -1.0, COS_TAU_6); + // sqrt() below halves any deviation on the way to the encoded value, + // so the correction is halved here to match. + stripe /= sqrt(max(corr, 1e-3)); + + if (lp_layout >= 0.5) { + stripe = stripe.bgr; + } + } + + // Treating the encoding as a gamma of 2 makes sqrt(linear * m) equal + // encoded * sqrt(m), so one square root replaces the decode and re-encode. + // Brightness rides the pattern, so the clamp lands on the product. + vec3 m = sqrt(max(stripe * (gain * lp_brightness), 0.0)); + + // The base is clamped because pow(0, g) is undefined and returns NaN on + // real drivers. 1e-8, not 1e-5, which would lift pure black to 1/255. + if (abs(lp_gamma - 1.0) > 0.001) { + color = pow(max(color, 1e-8), vec3(lp_gamma)); + } + + vec3 outc = color * m; + + FragColor = vec4(clamp(outc, 0.0, 1.0), 1.0); +} + + +#endif diff --git a/skeleton/BASE/Shaders/glsl/pixel-perfect.glsl b/skeleton/BASE/Shaders/glsl/pixel-perfect.glsl new file mode 100644 index 000000000..379b04e80 --- /dev/null +++ b/skeleton/BASE/Shaders/glsl/pixel-perfect.glsl @@ -0,0 +1,175 @@ +// pixel-perfect v8 - uniform pixel blocks and colour controls. +// ----------------------------------------------------------------------------- +// Licence: MIT - Copyright (c) 2026 sinedied +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: the above copyright +// notice and this permission notice shall be included in all copies or +// substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", +// WITHOUT WARRANTY OF ANY KIND. +// ----------------------------------------------------------------------------- +// PARAMETERS +// +// pp_brightness 0.50 - 4.00 Output gain. 1.00 disables it. +// pp_contrast 0.00 - 2.00 Contrast. 1.00 disables it. +// pp_saturation 0.00 - 2.00 Colour intensity. 1.00 disables it. +// pp_gamma 0.50 - 2.00 Output gamma. 1.00 disables it. +// pp_temperature -1.00 - 1.00 Warm above 0, cool below. 0.00 is off. +// pp_tint -1.00 - 1.00 Green above 0, magenta below. 0.00 is off. +// ----------------------------------------------------------------------------- +// A clean upscale: every source pixel becomes an even block, with no shimmer +// and no blur. The plain, fast default when you want the picture and nothing +// else, plus simple colour controls for tuning it to a screen. +// +// Notes: +// - Needs a LINEAR filter, set in the preset. Under NEAREST the scale becomes +// ordinary nearest-neighbour and the picture gets ragged edges. +// - Render at the output resolution, 1:1 with the display. +// - Brightness above 1.00 clips, may create pattern artifacts against the +// pixel grid unless the output is an integer scale. + +#pragma parameter pp_brightness "Brightness" 1.00 0.50 4.00 0.05 +#pragma parameter pp_contrast "Contrast" 1.00 0.00 2.00 0.05 +#pragma parameter pp_saturation "Saturation" 1.00 0.00 2.00 0.05 +#pragma parameter pp_gamma "Gamma" 1.00 0.50 2.00 0.05 +#pragma parameter pp_temperature "Cool / warm balance" 0.00 -1.00 1.00 0.01 +#pragma parameter pp_tint "Magenta / green balance" 0.00 -1.00 1.00 0.01 + +#if defined(VERTEX) + +#if __VERSION__ >= 130 +#define COMPAT_VARYING out +#define COMPAT_ATTRIBUTE in +#define COMPAT_TEXTURE texture +#else +#define COMPAT_VARYING varying +#define COMPAT_ATTRIBUTE attribute +#define COMPAT_TEXTURE texture2D +#endif + +#ifdef GL_ES +#define COMPAT_PRECISION mediump +#else +#define COMPAT_PRECISION +#endif + +COMPAT_ATTRIBUTE vec4 VertexCoord; +COMPAT_ATTRIBUTE vec4 COLOR; +COMPAT_ATTRIBUTE vec4 TexCoord; +COMPAT_VARYING vec4 COL0; +COMPAT_VARYING vec4 TEX0; + +uniform mat4 MVPMatrix; +uniform COMPAT_PRECISION int FrameDirection; +uniform COMPAT_PRECISION int FrameCount; +uniform COMPAT_PRECISION vec2 OutputSize; +uniform COMPAT_PRECISION vec2 TextureSize; +uniform COMPAT_PRECISION vec2 InputSize; + +void main() +{ + gl_Position = MVPMatrix * VertexCoord; + COL0 = COLOR; + TEX0.xy = TexCoord.xy; +} + +#elif defined(FRAGMENT) + +#if __VERSION__ >= 130 +#define COMPAT_VARYING in +#define COMPAT_TEXTURE texture +out vec4 FragColor; +#else +#define COMPAT_VARYING varying +#define FragColor gl_FragColor +#define COMPAT_TEXTURE texture2D +#endif + +#ifdef GL_ES +#ifdef GL_FRAGMENT_PRECISION_HIGH +precision highp float; +#else +precision mediump float; +#endif +#define COMPAT_PRECISION highp +#else +#define COMPAT_PRECISION +#endif + +uniform COMPAT_PRECISION int FrameDirection; +uniform COMPAT_PRECISION int FrameCount; +uniform COMPAT_PRECISION vec2 OutputSize; +uniform COMPAT_PRECISION vec2 TextureSize; +uniform COMPAT_PRECISION vec2 InputSize; +uniform sampler2D Texture; +COMPAT_VARYING vec4 TEX0; + +#ifdef PARAMETER_UNIFORM +uniform COMPAT_PRECISION float pp_brightness; +uniform COMPAT_PRECISION float pp_contrast; +uniform COMPAT_PRECISION float pp_saturation; +uniform COMPAT_PRECISION float pp_gamma; +uniform COMPAT_PRECISION float pp_temperature; +uniform COMPAT_PRECISION float pp_tint; +#else +#define pp_brightness 1.0 +#define pp_contrast 1.0 +#define pp_saturation 1.0 +#define pp_gamma 1.0 +#define pp_temperature 0.0 +#define pp_tint 0.0 +#endif + +// Rec.709 luma, for the saturation mix. Applied to encoded values: the round +// trip to linear is the construction the scaler exists to avoid. +const vec3 LUMA = vec3(0.2126, 0.7152, 0.0722); + +void main() +{ + // Source texels. The max() guards an unset InputSize, which is 0 and would + // make h a zero divisor below. + vec2 p = TEX0.xy * TextureSize; + vec2 h = max(0.4995 * InputSize / OutputSize, 1e-6); + + // B is the nearest texel boundary; w is the share of the footprint on its + // low side. Clamps to 0 or 1 wherever the footprint sits inside one texel, + // which is what keeps the blocks flat. + vec2 B = floor(p + 0.5); + vec2 w = clamp((B - p + h) / (2.0 * h), 0.0, 1.0); + + // One LINEAR tap, handed w: this texcoord asks the texture unit for + // exactly mix(T[B], T[B-1], w). + vec3 col = COMPAT_TEXTURE(Texture, (B + 0.5 - w) / TextureSize).rgb; + + // Balance first, so saturation sees the tinted colour and 0 really is + // monochrome. Brightness, contrast and saturation then fold into one affine + // map - do not un-fold it, or the defaults stop being exactly col * 1.0. + // Tested separately, not summed, or warm could cancel a cool tint. + if (pp_brightness != 1.0 || pp_contrast != 1.0 || pp_saturation != 1.0 + || pp_temperature != 0.0 || pp_tint != 0.0) { + // Warm/cool trades red against blue, tint trades green against both. + // Not luma-normalised, so they shift the level a little too. + col *= 1.0 + pp_temperature * vec3(1.0, 0.0, -1.0) + + pp_tint * vec3(-0.5, 1.0, -0.5); + + float ga = pp_brightness * pp_contrast; + float gb = 0.5 - 0.5 * pp_contrast; + col = col * (ga * pp_saturation) + + (dot(col, LUMA) * (ga * (1.0 - pp_saturation)) + gb); + } + + // The base is clamped because pow(0, g) is undefined and returns NaN on + // real drivers. 1e-8, not 1e-5, which would lift pure black to 1/255. + if (abs(pp_gamma - 1.0) > 0.001) { + col = pow(max(col, 1e-8), vec3(pp_gamma)); + } + + // Last, always: a grade or a gamma can leave 0 to 1, the blend cannot. + FragColor = vec4(clamp(col, 0.0, 1.0), 1.0); +} + +#endif diff --git a/skeleton/BASE/Shaders/sets/GB/Retro.cfg b/skeleton/BASE/Shaders/sets/GB/Retro.cfg new file mode 100755 index 000000000..d38b241e0 --- /dev/null +++ b/skeleton/BASE/Shaders/sets/GB/Retro.cfg @@ -0,0 +1,24 @@ +minarch_screen_effect = None +minarch_screen_scaling = Native +minarch_scale_filter = LINEAR + +minarch_nrofshaders = 1 +minarch_shader1 = dmg-perfect.glsl +minarch_shader1_filter = LINEAR +minarch_shader1_srctype = source +minarch_shader1_scaletype = source +minarch_shader1_upscale = screen + +dp_grid = 0.30 +dp_gap = 1.00 +dp_shadow = 0.20 +dp_brightness = 1.75 +dp_gamma = 1.50 +dp_temperature = -0.12 +dp_tint = -0.15 + +gambatte_gb_colorization = internal +gambatte_gb_internal_palette = TWB64 - Pack 1 +gambatte_gb_palette_twb64_1 = TWB64 040 - DMG Ver. +gambatte_gbc_color_correction = GBC only +gambatte_gbc_color_correction_mode = accurate diff --git a/skeleton/BASE/Shaders/sets/GB/Sharp.cfg b/skeleton/BASE/Shaders/sets/GB/Sharp.cfg new file mode 100755 index 000000000..8d792f282 --- /dev/null +++ b/skeleton/BASE/Shaders/sets/GB/Sharp.cfg @@ -0,0 +1,23 @@ +minarch_screen_effect = None +minarch_screen_scaling = Native +minarch_scale_filter = LINEAR + +minarch_nrofshaders = 1 +minarch_shader1 = pixel-perfect.glsl +minarch_shader1_filter = LINEAR +minarch_shader1_srctype = source +minarch_shader1_scaletype = source +minarch_shader1_upscale = screen + +pp_brightness = 1.75 +pp_contrast = 1.00 +pp_saturation = 1.00 +pp_gamma = 1.50 +pp_temperature = -0.12 +pp_tint = -0.15 + +gambatte_gb_colorization = internal +gambatte_gb_internal_palette = TWB64 - Pack 1 +gambatte_gb_palette_twb64_1 = TWB64 040 - DMG Ver. +gambatte_gbc_color_correction = GBC only +gambatte_gbc_color_correction_mode = accurate diff --git a/skeleton/BASE/Shaders/sets/GBA/Retro.cfg b/skeleton/BASE/Shaders/sets/GBA/Retro.cfg new file mode 100755 index 000000000..ed3cf0e6e --- /dev/null +++ b/skeleton/BASE/Shaders/sets/GBA/Retro.cfg @@ -0,0 +1,18 @@ +minarch_screen_effect = None +minarch_screen_scaling = Aspect +minarch_scale_filter = LINEAR + +minarch_nrofshaders = 1 +minarch_shader1 = lcd-perfect.glsl +minarch_shader1_filter = LINEAR +minarch_shader1_srctype = source +minarch_shader1_scaletype = source +minarch_shader1_upscale = screen + +lp_grid = 0.30 +lp_balance = 0.60 +lp_min_pitch = 3.00 +lp_subpixels = 0.00 +lp_layout = 1.00 +lp_brightness = 1.00 +lp_gamma = 1.00 diff --git a/skeleton/BASE/Shaders/sets/GBC/Retro.cfg b/skeleton/BASE/Shaders/sets/GBC/Retro.cfg new file mode 100755 index 000000000..5bba5413e --- /dev/null +++ b/skeleton/BASE/Shaders/sets/GBC/Retro.cfg @@ -0,0 +1,21 @@ +minarch_screen_effect = None +minarch_screen_scaling = Aspect +minarch_scale_filter = LINEAR + +minarch_nrofshaders = 1 +minarch_shader1 = lcd-perfect.glsl +minarch_shader1_filter = LINEAR +minarch_shader1_srctype = source +minarch_shader1_scaletype = source +minarch_shader1_upscale = screen + +lp_grid = 0.20 +lp_balance = 0.80 +lp_min_pitch = 3.00 +lp_subpixels = 0.20 +lp_layout = 0.00 +lp_brightness = 1.25 +lp_gamma = 1.00 + +gambatte_gbc_color_correction = GBC only +gambatte_gbc_color_correction_mode = accurate diff --git a/skeleton/BASE/Shaders/sets/GBC/Sharp.cfg b/skeleton/BASE/Shaders/sets/GBC/Sharp.cfg new file mode 100755 index 000000000..c2d79b09c --- /dev/null +++ b/skeleton/BASE/Shaders/sets/GBC/Sharp.cfg @@ -0,0 +1,20 @@ +minarch_screen_effect = None +minarch_screen_scaling = Aspect +minarch_scale_filter = LINEAR + +minarch_nrofshaders = 1 +minarch_shader1 = pixel-perfect.glsl +minarch_shader1_filter = LINEAR +minarch_shader1_srctype = source +minarch_shader1_scaletype = source +minarch_shader1_upscale = screen + +pp_brightness = 1.00 +pp_contrast = 1.00 +pp_saturation = 1.00 +pp_gamma = 1.00 +pp_temperature = -0.00 +pp_tint = -0.00 + +gambatte_gbc_color_correction = GBC only +gambatte_gbc_color_correction_mode = accurate diff --git a/skeleton/BASE/Shaders/sets/GG/Retro.cfg b/skeleton/BASE/Shaders/sets/GG/Retro.cfg new file mode 100755 index 000000000..e9a92ef6f --- /dev/null +++ b/skeleton/BASE/Shaders/sets/GG/Retro.cfg @@ -0,0 +1,18 @@ +minarch_screen_effect = None +minarch_screen_scaling = Aspect +minarch_scale_filter = LINEAR + +minarch_nrofshaders = 1 +minarch_shader1 = lcd-perfect.glsl +minarch_shader1_filter = LINEAR +minarch_shader1_srctype = source +minarch_shader1_scaletype = source +minarch_shader1_upscale = screen + +lp_grid = 0.20 +lp_balance = 0.80 +lp_min_pitch = 3.00 +lp_subpixels = 0.20 +lp_layout = 0.00 +lp_brightness = 1.40 +lp_gamma = 1.00 diff --git a/skeleton/BASE/Shaders/sets/MD/Retro.cfg b/skeleton/BASE/Shaders/sets/MD/Retro.cfg new file mode 100755 index 000000000..bf4ab2908 --- /dev/null +++ b/skeleton/BASE/Shaders/sets/MD/Retro.cfg @@ -0,0 +1,19 @@ +minarch_screen_effect = None +minarch_screen_scaling = Fullscreen +minarch_scale_filter = LINEAR + +minarch_nrofshaders = 1 +minarch_shader1 = crt-perfect.glsl +minarch_shader1_filter = LINEAR +minarch_shader1_srctype = source +minarch_shader1_scaletype = source +minarch_shader1_upscale = screen + +cp_scanlines = 0.60 +cp_rgb_mask = 0.05 +cp_mask_type = 1.00 +cp_mask_size = 1.00 +cp_min_pitch = 5.00 +cp_curvature = 0.00 +cp_brightness = 1.50 +cp_gamma = 1.00 diff --git a/skeleton/BASE/Shaders/sets/MD/Sharp.cfg b/skeleton/BASE/Shaders/sets/MD/Sharp.cfg new file mode 100755 index 000000000..c8330e938 --- /dev/null +++ b/skeleton/BASE/Shaders/sets/MD/Sharp.cfg @@ -0,0 +1,17 @@ +minarch_screen_effect = None +minarch_screen_scaling = Fullscreen +minarch_scale_filter = LINEAR + +minarch_nrofshaders = 1 +minarch_shader1 = pixel-perfect.glsl +minarch_shader1_filter = LINEAR +minarch_shader1_srctype = source +minarch_shader1_scaletype = source +minarch_shader1_upscale = screen + +pp_brightness = 1.00 +pp_contrast = 1.00 +pp_saturation = 1.00 +pp_gamma = 1.00 +pp_temperature = -0.00 +pp_tint = -0.00 diff --git a/skeleton/BASE/Shaders/sets/P8/Retro.cfg b/skeleton/BASE/Shaders/sets/P8/Retro.cfg new file mode 100755 index 000000000..74124e733 --- /dev/null +++ b/skeleton/BASE/Shaders/sets/P8/Retro.cfg @@ -0,0 +1,19 @@ +minarch_screen_effect = None +minarch_screen_scaling = Aspect +minarch_scale_filter = LINEAR + +minarch_nrofshaders = 1 +minarch_shader1 = lcd-perfect.glsl +minarch_shader1_filter = LINEAR +minarch_shader1_srctype = source +minarch_shader1_scaletype = source +minarch_shader1_upscale = screen + +lp_grid = 0.30 +lp_balance = 0.60 +lp_min_pitch = 3.00 +lp_subpixels = 0.20 +lp_layout = 0.00 +lp_brightness = 1.50 +lp_gamma = 1.00 + diff --git a/skeleton/BASE/Shaders/sets/Retro.cfg b/skeleton/BASE/Shaders/sets/Retro.cfg new file mode 100755 index 000000000..eb38f8fb1 --- /dev/null +++ b/skeleton/BASE/Shaders/sets/Retro.cfg @@ -0,0 +1,19 @@ +minarch_screen_effect = None +minarch_screen_scaling = Aspect +minarch_scale_filter = LINEAR + +minarch_nrofshaders = 1 +minarch_shader1 = crt-perfect.glsl +minarch_shader1_filter = LINEAR +minarch_shader1_srctype = source +minarch_shader1_scaletype = source +minarch_shader1_upscale = screen + +cp_scanlines = 0.60 +cp_rgb_mask = 0.05 +cp_mask_type = 1.00 +cp_mask_size = 1.00 +cp_min_pitch = 5.00 +cp_curvature = 0.00 +cp_brightness = 1.50 +cp_gamma = 1.00 diff --git a/skeleton/BASE/Shaders/sets/Sharp.cfg b/skeleton/BASE/Shaders/sets/Sharp.cfg new file mode 100755 index 000000000..6d38c4156 --- /dev/null +++ b/skeleton/BASE/Shaders/sets/Sharp.cfg @@ -0,0 +1,17 @@ +minarch_screen_effect = None +minarch_screen_scaling = Aspect +minarch_scale_filter = LINEAR + +minarch_nrofshaders = 1 +minarch_shader1 = pixel-perfect.glsl +minarch_shader1_filter = LINEAR +minarch_shader1_srctype = source +minarch_shader1_scaletype = source +minarch_shader1_upscale = screen + +pp_brightness = 1.00 +pp_contrast = 1.00 +pp_saturation = 1.00 +pp_gamma = 1.00 +pp_temperature = -0.00 +pp_tint = -0.00 diff --git a/workspace/all/common/generic_video.c b/workspace/all/common/generic_video.c index 00cb0cbf8..c2a12eea7 100644 --- a/workspace/all/common/generic_video.c +++ b/workspace/all/common/generic_video.c @@ -92,7 +92,7 @@ ShaderPass shaders[MAXSHADERS]; // memcpy these in initShaders() const ShaderProgram blank_shader_program = { - .shader_p = 0, .filename = "stock.glsl" + .shader_p = 0, .filename = NULL }; const ShaderPass blank_shader_pass = { .program = NULL, .alpha = 0, .target_texture = 0, .target_updated = 1 @@ -475,6 +475,14 @@ GLuint load_shader_from_file(GLenum type, const char* filepath) { #define MAX_SHADER_PRAGMAS 32 void loadShaderPragmas(ShaderProgram *shader, const char *shaderSource) { + if (shader->pragmas) { + free(shader->pragmas); + shader->pragmas = NULL; + shader->num_pragmas = 0; + } + if (!shaderSource) + return; + shader->pragmas = calloc(MAX_SHADER_PRAGMAS, sizeof(ShaderParam)); if (!shader->pragmas) { fprintf(stderr, "Out of memory allocating pragmas for %s\n", shader->filename); @@ -491,57 +499,75 @@ void init_shader_program(ShaderProgram * shader, const char * path, const char * char filepath[512]; snprintf(filepath, sizeof(filepath), "%s/%s", path, filename); - const char *shaderSource = load_shader_source(filepath); - loadShaderPragmas(shader,shaderSource); + char *shaderSource = load_shader_source(filepath); + if (!shaderSource) + return; GLuint vertex_shader1 = load_shader_from_file(GL_VERTEX_SHADER, filepath); GLuint fragment_shader1 = load_shader_from_file(GL_FRAGMENT_SHADER, filepath); - - // Link the shader program - if (shader->shader_p != 0) { - LOG_info("Deleting previous shader %i\n",shader->shader_p); - glDeleteProgram(shader->shader_p); + if (!vertex_shader1 || !fragment_shader1) { + if (vertex_shader1) glDeleteShader(vertex_shader1); + if (fragment_shader1) glDeleteShader(fragment_shader1); + free(shaderSource); + return; } - shader->shader_p = link_program(vertex_shader1, fragment_shader1, filename); + GLuint new_program = link_program(vertex_shader1, fragment_shader1, filename); + glDeleteShader(vertex_shader1); + glDeleteShader(fragment_shader1); - if (shader->shader_p == 0) { + if (new_program == 0) { LOG_info("Shader linking failed for %s\n", filename); + free(shaderSource); + return; } GLint success = 0; - glGetProgramiv(shader->shader_p, GL_LINK_STATUS, &success); + glGetProgramiv(new_program, GL_LINK_STATUS, &success); if (!success) { char infoLog[512]; - glGetProgramInfoLog(shader->shader_p, 512, NULL, infoLog); + glGetProgramInfoLog(new_program, 512, NULL, infoLog); LOG_info("Shader Program Linking Failed: %s\n", infoLog); - } else { - LOG_info("Shader Program Linking Success %s shader ID is %i\n", filename,shader->shader_p); - - // Populate uniforms and pragma uniforms - shader->u_FrameDirection = glGetUniformLocation( shader->shader_p, "FrameDirection"); - shader->u_FrameCount = glGetUniformLocation( shader->shader_p, "FrameCount"); - shader->u_OutputSize = glGetUniformLocation( shader->shader_p, "OutputSize"); - shader->u_TextureSize = glGetUniformLocation( shader->shader_p, "TextureSize"); - shader->u_InputSize = glGetUniformLocation( shader->shader_p, "InputSize"); - shader->u_OrigTextureSize = glGetUniformLocation( shader->shader_p, "OrigTextureSize"); - shader->u_OrigInputSize = glGetUniformLocation( shader->shader_p, "OrigInputSize"); - shader->u_Texture = glGetUniformLocation(shader->shader_p, "Texture"); - shader->u_OrigTexture = glGetUniformLocation(shader->shader_p, "OrigTexture"); - shader->u_texelSize = glGetUniformLocation(shader->shader_p, "texelSize"); - for (int i = 0; i < shader->num_pragmas; ++i) { - shader->pragmas[i].uniformLocation = glGetUniformLocation(shader->shader_p, shader->pragmas[i].name); - shader->pragmas[i].value = shader->pragmas[i].def; - - LOG_info("Param: %s = %f (min: %f, max: %f, step: %f)\n", - shader->pragmas[i].name, - shader->pragmas[i].def, - shader->pragmas[i].min, - shader->pragmas[i].max, - shader->pragmas[i].step); - } + glDeleteProgram(new_program); + free(shaderSource); + return; + } + if (shader->shader_p != 0) { + LOG_info("Deleting previous shader %i\n",shader->shader_p); + glDeleteProgram(shader->shader_p); } + shader->shader_p = new_program; + loadShaderPragmas(shader, shaderSource); + free(shaderSource); + + LOG_info("Shader Program Linking Success %s shader ID is %i\n", filename,shader->shader_p); + + // Populate uniforms and pragma uniforms + shader->u_FrameDirection = glGetUniformLocation( shader->shader_p, "FrameDirection"); + shader->u_FrameCount = glGetUniformLocation( shader->shader_p, "FrameCount"); + shader->u_OutputSize = glGetUniformLocation( shader->shader_p, "OutputSize"); + shader->u_TextureSize = glGetUniformLocation( shader->shader_p, "TextureSize"); + shader->u_InputSize = glGetUniformLocation( shader->shader_p, "InputSize"); + shader->u_OrigTextureSize = glGetUniformLocation( shader->shader_p, "OrigTextureSize"); + shader->u_OrigInputSize = glGetUniformLocation( shader->shader_p, "OrigInputSize"); + shader->u_Texture = glGetUniformLocation(shader->shader_p, "Texture"); + shader->u_OrigTexture = glGetUniformLocation(shader->shader_p, "OrigTexture"); + shader->u_texelSize = glGetUniformLocation(shader->shader_p, "texelSize"); + for (int i = 0; i < shader->num_pragmas; ++i) { + shader->pragmas[i].uniformLocation = glGetUniformLocation(shader->shader_p, shader->pragmas[i].name); + shader->pragmas[i].value = shader->pragmas[i].def; + + LOG_info("Param: %s = %f (min: %f, max: %f, step: %f)\n", + shader->pragmas[i].name, + shader->pragmas[i].def, + shader->pragmas[i].min, + shader->pragmas[i].max, + shader->pragmas[i].step); + } + + if (shader->filename) + free(shader->filename); shader->filename = strdup(filename); } diff --git a/workspace/all/common/shader_sets.c b/workspace/all/common/shader_sets.c new file mode 100644 index 000000000..a01fbd760 --- /dev/null +++ b/workspace/all/common/shader_sets.c @@ -0,0 +1,312 @@ +#include "shader_sets.h" +#include "defines.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static bool isSafeComponent(const char *value) +{ + if (!value || !value[0] || !strcmp(value, ".") || !strcmp(value, "..")) + return false; + + return !strchr(value, '/') && !strchr(value, '\\'); +} + +static bool isConfigFile(const char *name) +{ + size_t len; + + if (!name || name[0] == '.') + return false; + + len = strlen(name); + return len > 4 && !strcmp(name + len - 4, ".cfg"); +} + +static bool isRegularFile(const char *path) +{ + struct stat st; + return stat(path, &st) == 0 && S_ISREG(st.st_mode); +} + +static int compareNames(const void *left, const void *right) +{ + const char *a = *(const char * const *)left; + const char *b = *(const char * const *)right; + int result = strcasecmp(a, b); + return result ? result : strcmp(a, b); +} + +static void readActiveName(char *name, size_t name_size) +{ + FILE *file; + + if (!name || name_size == 0) + return; + + name[0] = '\0'; + file = fopen(SHADER_SET_STATE_PATH, "r"); + if (!file) + return; + + if (fgets(name, name_size, file)) + name[strcspn(name, "\r\n")] = '\0'; + + fclose(file); +} + +static bool writeActiveName(const char *name) +{ + char temp_path[MAX_PATH]; + FILE *file; + int fd; + + if (!name || !name[0]) { + if (unlink(SHADER_SET_STATE_PATH) == 0 || errno == ENOENT) { + sync(); + return true; + } + return false; + } + + if (snprintf(temp_path, sizeof(temp_path), "%s.tmp.%ld", + SHADER_SET_STATE_PATH, (long)getpid()) >= (int)sizeof(temp_path)) + return false; + + file = fopen(temp_path, "w"); + if (!file) + return false; + + if (fprintf(file, "%s\n", name) < 0 || fflush(file) != 0) { + fclose(file); + unlink(temp_path); + return false; + } + + fd = fileno(file); + if (fd >= 0 && fsync(fd) != 0) { + fclose(file); + unlink(temp_path); + return false; + } + + if (fclose(file) != 0) { + unlink(temp_path); + return false; + } + + if (rename(temp_path, SHADER_SET_STATE_PATH) != 0) { + unlink(temp_path); + return false; + } + + sync(); + return true; +} + +bool ShaderSets_list(ShaderSetList *list) +{ + DIR *dir; + struct dirent *entry; + char **names; + int count = 1; + + if (!list) + return false; + + list->names = NULL; + list->count = 0; + + names = calloc(1, sizeof(char *)); + if (!names) + return false; + + names[0] = strdup(""); + if (!names[0]) { + free(names); + return false; + } + + dir = opendir(SHADER_SETS_PATH); + if (dir) { + while ((entry = readdir(dir)) != NULL) { + char path[MAX_PATH]; + char *stem; + char **expanded; + size_t len; + + if (!isConfigFile(entry->d_name)) + continue; + + if (snprintf(path, sizeof(path), "%s/%s", + SHADER_SETS_PATH, entry->d_name) >= (int)sizeof(path)) + continue; + + if (!isRegularFile(path)) + continue; + + len = strlen(entry->d_name) - 4; + stem = strndup(entry->d_name, len); + if (!stem) + goto fail; + + if (!strcasecmp(stem, SHADER_SET_DISABLED_LABEL)) { + free(stem); + continue; + } + + expanded = realloc(names, sizeof(char *) * (count + 1)); + if (!expanded) { + free(stem); + goto fail; + } + + names = expanded; + names[count++] = stem; + } + closedir(dir); + } + + if (count > 2) + qsort(names + 1, count - 1, sizeof(char *), compareNames); + + list->names = names; + list->count = count; + return true; + +fail: + if (dir) + closedir(dir); + for (int i = 0; i < count; i++) + free(names[i]); + free(names); + return false; +} + +void ShaderSets_freeList(ShaderSetList *list) +{ + if (!list) + return; + + for (int i = 0; i < list->count; i++) + free(list->names[i]); + free(list->names); + list->names = NULL; + list->count = 0; +} + +const char *ShaderSets_displayName(const char *name) +{ + return name && name[0] ? name : SHADER_SET_DISABLED_LABEL; +} + +int ShaderSets_activeIndex(const ShaderSetList *list) +{ + char active[MAX_PATH]; + + if (!list || !list->names || list->count <= 0) + return 0; + + readActiveName(active, sizeof(active)); + if (!isSafeComponent(active)) + return 0; + + for (int i = 1; i < list->count; i++) { + if (!strcmp(list->names[i], active)) + return i; + } + + writeActiveName(""); + return 0; +} + +bool ShaderSets_getActive(char *name, size_t name_size) +{ + ShaderSetList list; + FILE *file; + + if (!name || name_size == 0) + return false; + + name[0] = '\0'; + file = fopen(SHADER_SET_STATE_PATH, "r"); + if (!file) + return errno == ENOENT; + if (fgets(name, name_size, file)) + name[strcspn(name, "\r\n")] = '\0'; + if (ferror(file) || fclose(file) != 0) { + name[0] = '\0'; + return false; + } + if (!name[0]) + return true; + if (!isSafeComponent(name)) { + name[0] = '\0'; + return false; + } + + if (!ShaderSets_list(&list)) + return false; + + for (int i = 1; i < list.count; i++) { + if (!strcmp(list.names[i], name)) { + ShaderSets_freeList(&list); + return true; + } + } + + ShaderSets_freeList(&list); + name[0] = '\0'; + return false; +} + +bool ShaderSets_setActive(const char *name) +{ + char path[MAX_PATH]; + + if (!name || !name[0]) + return writeActiveName(""); + + if (!ShaderSets_rootPath(name, path, sizeof(path)) || !isRegularFile(path)) + return false; + + return writeActiveName(name); +} + +bool ShaderSets_advance(const ShaderSetList *list, char *name, size_t name_size) +{ + int next; + + if (!list || !list->names || list->count <= 0 || !name || name_size == 0) + return false; + + next = (ShaderSets_activeIndex(list) + 1) % list->count; + if (snprintf(name, name_size, "%s", list->names[next]) >= (int)name_size) + return false; + + return ShaderSets_setActive(name); +} + +bool ShaderSets_rootPath(const char *name, char *path, size_t path_size) +{ + if (!isSafeComponent(name) || !path || path_size == 0) + return false; + + return snprintf(path, path_size, "%s/%s.cfg", SHADER_SETS_PATH, name) < (int)path_size; +} + +bool ShaderSets_overridePath(const char *name, const char *tag, char *path, size_t path_size) +{ + if (!isSafeComponent(name) || !isSafeComponent(tag) || !path || path_size == 0) + return false; + + return snprintf(path, path_size, "%s/%s/%s.cfg", + SHADER_SETS_PATH, tag, name) < (int)path_size; +} diff --git a/workspace/all/common/shader_sets.h b/workspace/all/common/shader_sets.h new file mode 100644 index 000000000..6e21f44ec --- /dev/null +++ b/workspace/all/common/shader_sets.h @@ -0,0 +1,32 @@ +#ifndef SHADER_SETS_H +#define SHADER_SETS_H + +#include +#include + +#ifndef SHADER_SETS_PATH +#define SHADER_SETS_PATH SDCARD_PATH "/Shaders/sets" +#endif +#ifndef SHADER_SET_STATE_PATH +#define SHADER_SET_STATE_PATH SHARED_USERDATA_PATH "/shader-set.txt" +#endif +#define SHADER_SET_DISABLED_LABEL "Disabled" + +typedef struct ShaderSetList { + char **names; + int count; +} ShaderSetList; + +bool ShaderSets_list(ShaderSetList *list); +void ShaderSets_freeList(ShaderSetList *list); + +const char *ShaderSets_displayName(const char *name); +int ShaderSets_activeIndex(const ShaderSetList *list); +bool ShaderSets_getActive(char *name, size_t name_size); +bool ShaderSets_setActive(const char *name); +bool ShaderSets_advance(const ShaderSetList *list, char *name, size_t name_size); + +bool ShaderSets_rootPath(const char *name, char *path, size_t path_size); +bool ShaderSets_overridePath(const char *name, const char *tag, char *path, size_t path_size); + +#endif diff --git a/workspace/all/minarch/ma_config.c b/workspace/all/minarch/ma_config.c index 438a0a65b..e7942b02b 100644 --- a/workspace/all/minarch/ma_config.c +++ b/workspace/all/minarch/ma_config.c @@ -3,10 +3,18 @@ #include #include #include +#include +#include +#include +#include #include "ma_internal.h" #include "ma_options.h" #include "ma_config.h" +#include "ma_runframe.h" +#include "shader_sets.h" + +static void freeShaderSettings(int i); static ButtonMapping button_label_mapping[] = { // used to lookup the retro_id and local btn_id from button name {"NONE", -1, BTN_ID_NONE}, @@ -404,35 +412,32 @@ void Config_init(void) { config.initialized = 1; } void Config_quit(void) { - if (!config.initialized) return; - for (int i=0; core_button_mapping[i].name; i++) { - free(core_button_mapping[i].name); + if (config.initialized) { + for (int i=0; core_button_mapping[i].name; i++) { + free(core_button_mapping[i].name); + } } + for (int i = 0; i < 3; i++) + freeShaderSettings(i); + Config_free(); } -static void Config_readOptionsString(char* cfg) { + +static void Config_readFrontendOptionsString(char* cfg, int sync) { if (!cfg) return; - LOG_info("Config_readOptions\n"); - char key[256]; char value[256]; for (int i=0; config.frontend.options[i].key; i++) { Option* option = &config.frontend.options[i]; if (!Config_getValue(cfg, option->key, value, &option->lock)) continue; OptionList_setOptionValue(&config.frontend, option->key, value); - Config_syncFrontend(option->key, option->value); - } - - if (has_custom_controllers && Config_getValue(cfg,"minarch_gamepad_type",value,NULL)) { - gamepad_type = strtol(value, NULL, 0); - int device = strtol(gamepad_values[gamepad_type], NULL, 0); - core.set_controller_port_device(0, device); - } - for (int i=0; config.core.options[i].key; i++) { - Option* option = &config.core.options[i]; - // LOG_info("%s\n",option->key); - if (!Config_getValue(cfg, option->key, value, &option->lock)) continue; - OptionList_setOptionValue(&config.core, option->key, value); + if (sync) Config_syncFrontend(option->key, option->value); } +} + +static void Config_readShaderOptionsString(char* cfg) { + if (!cfg) return; + + char value[256]; for (int i=0; config.shaders.options[i].key; i++) { Option* option = &config.shaders.options[i]; if (!Config_getValue(cfg, option->key, value, &option->lock)) continue; @@ -448,6 +453,64 @@ static void Config_readOptionsString(char* cfg) { } } } + +static int Config_getGamepadType(char *cfg, int *type) { + char value[256]; + char *end; + long parsed; + int count = 0; + + if (!cfg || !Config_getValue(cfg, "minarch_gamepad_type", value, NULL)) + return 0; + + errno = 0; + parsed = strtol(value, &end, 0); + if (end == value) + return -1; + while (isspace((unsigned char)*end)) end++; + while (gamepad_values[count]) count++; + if (errno == ERANGE || *end || parsed < 0 || parsed >= count) + return -1; + + *type = parsed; + return 1; +} + +static void Config_readCoreOptionsString(char* cfg) { + if (!cfg) return; + + if (has_custom_controllers) { + int type; + int result = Config_getGamepadType(cfg, &type); + if (result > 0) { + gamepad_type = type; + int device = strtol(gamepad_values[gamepad_type], NULL, 0); + core.set_controller_port_device(0, device); + } + else if (result < 0) + LOG_warn("invalid minarch_gamepad_type\n"); + } + char value[256]; + for (int i=0; config.core.options[i].key; i++) { + Option* option = &config.core.options[i]; + // LOG_info("%s\n",option->key); + if (!Config_getValue(cfg, option->key, value, &option->lock)) continue; + OptionList_setOptionValue(&config.core, option->key, value); + } +} + +static void Config_readSetOptionsString(char* cfg, int sync) { + Config_readFrontendOptionsString(cfg, sync); + Config_readCoreOptionsString(cfg); + Config_readShaderOptionsString(cfg); +} + +static void Config_readOptionsString(char* cfg) { + if (!cfg) return; + + LOG_info("Config_readOptions\n"); + Config_readSetOptionsString(cfg, 1); +} static void Config_readControlsString(char* cfg) { if (!cfg) return; @@ -509,6 +572,36 @@ static void Config_readControlsString(char* cfg) { mapping->mod = mod; } } + +static void Config_loadShaderSet(void) { + ShaderSetList list; + int active; + char path[MAX_PATH]; + + if (config.shader_set_cfg) { + free(config.shader_set_cfg); + config.shader_set_cfg = NULL; + } + if (config.shader_set_override_cfg) { + free(config.shader_set_override_cfg); + config.shader_set_override_cfg = NULL; + } + + if (!ShaderSets_list(&list)) + return; + + active = ShaderSets_activeIndex(&list); + if (active > 0 && ShaderSets_rootPath(list.names[active], path, sizeof(path))) + config.shader_set_cfg = allocFile(path); + + if (active > 0 && + ShaderSets_overridePath(list.names[active], core.tag, path, sizeof(path)) && + exists(path)) + config.shader_set_override_cfg = allocFile(path); + + ShaderSets_freeList(&list); +} + void Config_load(void) { LOG_info("Config_load\n"); @@ -568,58 +661,61 @@ void Config_load(void) { if (!override) Config_getPath(path, CONFIG_WRITE_ALL); config.user_cfg = allocFile(path); - if (!config.user_cfg) return; - - LOG_info("using user config: %s\n", path); - - config.loaded = override ? CONFIG_GAME : CONFIG_CONSOLE; + if (config.user_cfg) { + LOG_info("using user config: %s\n", path); + config.loaded = override ? CONFIG_GAME : CONFIG_CONSOLE; + } + + Config_loadShaderSet(); } void Config_free(void) { if (config.system_cfg) free(config.system_cfg); if (config.default_cfg) free(config.default_cfg); if (config.user_cfg) free(config.user_cfg); + if (config.shader_set_cfg) free(config.shader_set_cfg); + if (config.shader_set_override_cfg) free(config.shader_set_override_cfg); + if (config.shaders_preset) free(config.shaders_preset); + config.system_cfg = NULL; + config.default_cfg = NULL; + config.user_cfg = NULL; + config.shader_set_cfg = NULL; + config.shader_set_override_cfg = NULL; + config.shaders_preset = NULL; } void Config_readOptions(void) { Config_readOptionsString(config.system_cfg); Config_readOptionsString(config.default_cfg); Config_readOptionsString(config.user_cfg); + // Active sets intentionally override both console and game visual settings. + Config_readSetOptionsString(config.shader_set_cfg, 1); + Config_readSetOptionsString(config.shader_set_override_cfg, 1); } void Config_readControls(void) { Config_readControlsString(config.default_cfg); Config_readControlsString(config.user_cfg); } -void Config_write(int override) { - char path[MAX_PATH]; - // sprintf(path, "%s/%s.cfg", core.config_dir, game.alt_name); - Config_getPath(path, CONFIG_WRITE_GAME); - - if (!override) { - if (config.loaded==CONFIG_GAME) unlink(path); - Config_getPath(path, CONFIG_WRITE_ALL); - } - config.loaded = override ? CONFIG_GAME : CONFIG_CONSOLE; - - FILE *file = fopen(path, "wb"); - if (!file) return; - + +static int Config_writeFrontendShaders(FILE *file, int preserve_locks) { for (int i=0; config.frontend.options[i].key; i++) { Option* option = &config.frontend.options[i]; int count = 0; while ( option->values && option->values[count]) count++; if (option->value >= 0 && option->value < count) { - fprintf(file, "%s = %s\n", option->key, option->values[option->value]); + if (fprintf(file, "%s%s = %s\n", + preserve_locks && option->lock ? "-" : "", + option->key, option->values[option->value]) < 0) + return 0; } } - for (int i=0; config.core.options[i].key; i++) { - Option* option = &config.core.options[i]; - fprintf(file, "%s = %s\n", option->key, option->values[option->value]); - } for (int i=0; config.shaders.options[i].key; i++) { Option* option = &config.shaders.options[i]; int count = 0; while ( option->values && option->values[count]) count++; if (option->value >= 0 && option->value < count) { - fprintf(file, "%s = %s\n", option->key, option->values[option->value]); + if (fprintf(file, "%s%s = %s\n", + preserve_locks && option->lock ? "-" : "", + option->key, option->values[option->value]) < 0) + return 0; } } for (int y=0; y < config.shaders.options[SH_NROFSHADERS].value; y++) { @@ -628,28 +724,278 @@ void Config_write(int override) { int count = 0; while ( option->values && option->values[count]) count++; if (option->value >= 0 && option->value < count) { - fprintf(file, "%s = %s\n", option->key, option->values[option->value]); + if (fprintf(file, "%s%s = %s\n", + preserve_locks && option->lock ? "-" : "", + option->key, option->values[option->value]) < 0) + return 0; } } } - - if (has_custom_controllers) fprintf(file, "%s = %i\n", "minarch_gamepad_type", gamepad_type); - + return 1; +} + +static int Config_writeSetCoreOptions(FILE *file) { + for (int i=0; config.core.options[i].key; i++) { + Option* option = &config.core.options[i]; + if (fprintf(file, "%s%s = %s\n", option->lock ? "-" : "", + option->key, option->values[option->value]) < 0) + return 0; + } + return 1; +} + +static int Config_writeNonvisual(FILE *file) { + for (int i=0; config.core.options[i].key; i++) { + Option* option = &config.core.options[i]; + if (fprintf(file, "%s = %s\n", option->key, option->values[option->value]) < 0) + return 0; + } + + if (has_custom_controllers && + fprintf(file, "%s = %i\n", "minarch_gamepad_type", gamepad_type) < 0) + return 0; + for (int i=0; config.controls[i].name; i++) { ButtonMapping* mapping = &config.controls[i]; int j = mapping->local + 1; if (mapping->mod) j += LOCAL_BUTTON_COUNT; - fprintf(file, "bind %s = %s\n", mapping->name, button_labels[j]); + if (fprintf(file, "bind %s = %s\n", mapping->name, button_labels[j]) < 0) + return 0; } for (int i=0; config.shortcuts[i].name; i++) { ButtonMapping* mapping = &config.shortcuts[i]; int j = mapping->local + 1; if (mapping->mod) j += LOCAL_BUTTON_COUNT; - fprintf(file, "bind %s = %s\n", mapping->name, button_labels[j]); + if (fprintf(file, "bind %s = %s\n", mapping->name, button_labels[j]) < 0) + return 0; } - - fclose(file); + return 1; +} + +static int Config_finishFile(FILE *file) { + if (fflush(file) != 0) { + fclose(file); + return 0; + } + int fd = fileno(file); + if (fd >= 0 && fsync(fd) != 0) { + fclose(file); + return 0; + } + return fclose(file) == 0; +} + +static int Config_refreshUserConfig(const char *path) { + char *updated_user_cfg = allocFile((char *)path); + if (!updated_user_cfg) { + LOG_error("failed to refresh user config: %s\n", path); + return 0; + } + if (config.user_cfg) free(config.user_cfg); + config.user_cfg = updated_user_cfg; + return 1; +} + +static int Config_writeStandard(const char *path) { + FILE *file = fopen(path, "wb"); + if (!file) + return 0; + + if (!Config_writeFrontendShaders(file, 0) || !Config_writeNonvisual(file)) { + fclose(file); + return 0; + } + if (!Config_finishFile(file)) + return 0; + + return Config_refreshUserConfig(path); +} + +static int Config_isNonvisualLine(const char *line, size_t len) { + char key[256]; + const char *start = line; + const char *delimiter; + + while (len && (*start == ' ' || *start == '\t')) { + start++; + len--; + } + if (len && *start == '-') { + start++; + len--; + } + if (len >= 5 && !strncmp(start, "bind ", 5)) + return 1; + + delimiter = NULL; + for (size_t i = 0; i + 2 < len; i++) { + if (start[i] == ' ' && start[i + 1] == '=' && start[i + 2] == ' ') { + delimiter = start + i; + break; + } + } + if (!delimiter) + return 0; + + size_t key_len = delimiter - start; + if (key_len >= sizeof(key)) + return 0; + memcpy(key, start, key_len); + key[key_len] = '\0'; + + if (!strcmp(key, "minarch_gamepad_type")) + return 1; + for (int i = 0; config.core.options[i].key; i++) { + if (!strcmp(key, config.core.options[i].key)) + return 1; + } + return 0; +} + +static int Config_writePreservedVisual(FILE *file, const char *cfg) { + if (!cfg) + return 1; + + const char *line = cfg; + while (*line) { + const char *end = line; + while (*end && *end != '\n' && *end != '\r') + end++; + size_t len = end - line; + + if (!Config_isNonvisualLine(line, len)) { + if (len && fwrite(line, 1, len, file) != len) + return 0; + if (fputc('\n', file) == EOF) + return 0; + } + + while (*end == '\n' || *end == '\r') + end++; + line = end; + } + return 1; +} + +static int Config_makeTempPath(const char *path, char *temp_path, size_t temp_size) { + return snprintf(temp_path, temp_size, "%s.tmp.%ld", path, (long)getpid()) < (int)temp_size; +} + +static int Config_writeActiveSetConsole(const char *set_name, const char *game_path, + const char *console_path) { + char override_dir[MAX_PATH]; + char override_path[MAX_PATH]; + char console_temp[MAX_PATH] = {0}; + char override_temp[MAX_PATH] = {0}; + char *console_cfg = allocFile((char *)console_path); + char *new_user_cfg = NULL; + char *new_override_cfg = NULL; + FILE *console_file = NULL; + FILE *override_file = NULL; + int result = 0; + + if (!ShaderSets_overridePath(set_name, core.tag, override_path, sizeof(override_path)) || + snprintf(override_dir, sizeof(override_dir), "%s/%s", SHADER_SETS_PATH, core.tag) >= (int)sizeof(override_dir) || + !Config_makeTempPath(console_path, console_temp, sizeof(console_temp)) || + !Config_makeTempPath(override_path, override_temp, sizeof(override_temp))) + goto cleanup; + + if (exists((char *)console_path) && !console_cfg) + goto cleanup; + + if (mkdir(override_dir, 0755) != 0 && errno != EEXIST) + goto cleanup; + + console_file = fopen(console_temp, "wb"); + if (!console_file) + goto cleanup; + if (!Config_writePreservedVisual(console_file, console_cfg) || + !Config_writeNonvisual(console_file)) + goto cleanup; + if (!Config_finishFile(console_file)) { + console_file = NULL; + goto cleanup; + } + console_file = NULL; + + override_file = fopen(override_temp, "wb"); + if (!override_file) + goto cleanup; + if (!Config_writeFrontendShaders(override_file, 1) || + !Config_writeSetCoreOptions(override_file)) + goto cleanup; + if (!Config_finishFile(override_file)) { + override_file = NULL; + goto cleanup; + } + override_file = NULL; + + new_user_cfg = allocFile(console_temp); + if (!new_user_cfg) + goto cleanup; + new_override_cfg = allocFile(override_temp); + if (!new_override_cfg) + goto cleanup; + + if (rename(console_temp, console_path) != 0) + goto cleanup; + if (rename(override_temp, override_path) != 0) + goto cleanup; + if (config.loaded == CONFIG_GAME && + unlink(game_path) != 0 && errno != ENOENT) + goto cleanup; + + config.loaded = CONFIG_CONSOLE; + if (config.user_cfg) free(config.user_cfg); + config.user_cfg = new_user_cfg; + new_user_cfg = NULL; + if (config.shader_set_override_cfg) free(config.shader_set_override_cfg); + config.shader_set_override_cfg = new_override_cfg; + new_override_cfg = NULL; sync(); + result = 1; + +cleanup: + if (console_file) fclose(console_file); + if (override_file) fclose(override_file); + if (console_temp[0]) unlink(console_temp); + if (override_temp[0]) unlink(override_temp); + free(console_cfg); + free(new_user_cfg); + free(new_override_cfg); + return result; +} + +int Config_write(int override) { + char game_path[MAX_PATH]; + char path[MAX_PATH]; + char active_set[MAX_PATH]; + + Config_getPath(game_path, CONFIG_WRITE_GAME); + + if (!override) { + if (!ShaderSets_getActive(active_set, sizeof(active_set))) + return CONFIG_WRITE_FAILED; + if (active_set[0]) { + Config_getPath(path, CONFIG_WRITE_ALL); + return Config_writeActiveSetConsole(active_set, game_path, path) + ? CONFIG_WRITE_SHADER_SET + : CONFIG_WRITE_FAILED; + } + } + + strcpy(path, game_path); + if (!override) { + if (config.loaded==CONFIG_GAME) unlink(path); + Config_getPath(path, CONFIG_WRITE_ALL); + } + + if (!Config_writeStandard(path)) + return CONFIG_WRITE_FAILED; + + config.loaded = override ? CONFIG_GAME : CONFIG_CONSOLE; + sync(); + return CONFIG_WRITE_STANDARD; } void Config_restore(void) { char path[MAX_PATH]; @@ -698,10 +1044,10 @@ void Config_restore(void) { mapping->mod = 0; } + Config_free(); Config_load(); Config_readOptions(); Config_readControls(); - Config_free(); renderer.dst_p = 0; } @@ -710,15 +1056,35 @@ void readShadersPreset(int i) { char shaderspath[MAX_PATH] = {0}; sprintf(shaderspath, SHADERS_FOLDER "/%s", config.shaders.options[SH_SHADERS_PRESET].values[i]); LOG_info("read shaders preset %s\n",shaderspath); + if (config.shaders_preset) { + free(config.shaders_preset); + config.shaders_preset = NULL; + } if (exists(shaderspath)) { config.shaders_preset = allocFile(shaderspath); Config_readOptionsString(config.shaders_preset); } - else config.shaders_preset = NULL; +} + +static void freeShaderSettings(int i) { + if (!config.shaderpragmas[i].options) + return; + + for (int j = 0; j < config.shaderpragmas[i].count; j++) { + Option *option = &config.shaderpragmas[i].options[j]; + for (int k = 0; option->values && option->values[k]; k++) + free(option->values[k]); + free(option->values); + free(option->labels); + } + free(config.shaderpragmas[i].options); + config.shaderpragmas[i].options = NULL; + config.shaderpragmas[i].count = 0; } void loadShaderSettings(int i) { int menucount = 0; + freeShaderSettings(i); config.shaderpragmas[i].options = calloc(32 + 1, sizeof(Option)); ShaderParam *params = PLAT_getShaderPragmas(i); if(params == NULL) return; @@ -888,6 +1254,202 @@ void initShaders() { } } +static void resetFrontendShaders(void) { + for (int i = 0; config.frontend.options[i].key; i++) { + config.frontend.options[i].value = config.frontend.options[i].default_value; + config.frontend.options[i].lock = 0; + } + for (int i = 0; config.shaders.options[i].key; i++) { + config.shaders.options[i].value = config.shaders.options[i].default_value; + config.shaders.options[i].lock = 0; + } + for (int i = 0; i < 3; i++) + freeShaderSettings(i); +} + +static void readEffectiveFrontendOptions(int sync) { + Config_readFrontendOptionsString(config.system_cfg, sync); + Config_readFrontendOptionsString(config.default_cfg, sync); + Config_readFrontendOptionsString(config.user_cfg, sync); + Config_readFrontendOptionsString(config.shader_set_cfg, sync); + Config_readFrontendOptionsString(config.shader_set_override_cfg, sync); +} + +static void readEffectiveShaderOptions(void) { + Config_readShaderOptionsString(config.system_cfg); + Config_readShaderOptionsString(config.default_cfg); + Config_readShaderOptionsString(config.user_cfg); + Config_readShaderOptionsString(config.shader_set_cfg); + Config_readShaderOptionsString(config.shader_set_override_cfg); +} + +static void readEffectiveFrontendShaders(int sync) { + readEffectiveFrontendOptions(sync); + readEffectiveShaderOptions(); +} + +static int shaderSetTouchesFrontendOption(int index) { + char value[256]; + const char *key = config.frontend.options[index].key; + + return (config.shader_set_cfg && + Config_getValue(config.shader_set_cfg, key, value, NULL)) || + (config.shader_set_override_cfg && + Config_getValue(config.shader_set_override_cfg, key, value, NULL)); +} + +static int shaderSetTouchesKey(const char *key) { + char value[256]; + return (config.shader_set_cfg && + Config_getValue(config.shader_set_cfg, key, value, NULL)) || + (config.shader_set_override_cfg && + Config_getValue(config.shader_set_override_cfg, key, value, NULL)); +} + +static int shaderSetTouchesGamepad(void) { + int type; + return Config_getGamepadType(config.shader_set_cfg, &type) > 0 || + Config_getGamepadType(config.shader_set_override_cfg, &type) > 0; +} + +static void reloadCoreOptions(const int *old_values, const int *old_locks, + const int *old_set_options, int old_gamepad, int old_set_gamepad) { + int new_set_gamepad = shaderSetTouchesGamepad(); + int palette_changed = 0; + + for (int i = 0; i < config.core.count; i++) { + config.core.options[i].value = config.core.options[i].default_value; + config.core.options[i].lock = 0; + } + if (has_custom_controllers) { + gamepad_type = 0; + core.set_controller_port_device(0, RETRO_DEVICE_JOYPAD); + } + + Config_readCoreOptionsString(config.system_cfg); + Config_readCoreOptionsString(config.default_cfg); + Config_readCoreOptionsString(config.user_cfg); + Config_readCoreOptionsString(config.shader_set_cfg); + Config_readCoreOptionsString(config.shader_set_override_cfg); + + // Keep unsaved core changes that neither the old nor new set manages. + for (int i = 0; i < config.core.count; i++) { + int new_set_option = shaderSetTouchesKey(config.core.options[i].key); + if ((old_set_options[i] || new_set_option) && + containsString(config.core.options[i].key, "palette")) + palette_changed = 1; + if (!old_set_options[i] && + !new_set_option) { + config.core.options[i].value = old_values[i]; + config.core.options[i].lock = old_locks[i]; + } + } + if (has_custom_controllers && !old_set_gamepad && !new_set_gamepad) { + gamepad_type = old_gamepad; + int device = strtol(gamepad_values[gamepad_type], NULL, 0); + core.set_controller_port_device(0, device); + } + config.core.changed = 1; + has_pending_opt_change = 1; + if (palette_changed && exactMatch((char*)core.tag, "GB")) + Special_updatedDMGPalette(2); +} + +static void resetShaderPragmas(int pass) { + ShaderParam *params = PLAT_getShaderPragmas(pass); + if (!params) + return; + + for (int i = 0; i < 32; i++) + params[i].value = params[i].def; +} + +static void reloadShaders(const int *old_values) { + for (int i = 0; config.shaders.options[i].key; i++) { + if (i == SH_EXTRASETTINGS || i == SH_SHADERS_PRESET) + continue; + + if (i == SH_SHADER1 || i == SH_SHADER2 || i == SH_SHADER3) { + int pass = i == SH_SHADER1 ? 0 : i == SH_SHADER2 ? 1 : 2; + if (config.shaders.options[i].value != old_values[i]) + Config_syncShaders(config.shaders.options[i].key, config.shaders.options[i].value); + else { + resetShaderPragmas(pass); + loadShaderSettings(pass); + } + } + else if (config.shaders.options[i].value != old_values[i]) { + Config_syncShaders(config.shaders.options[i].key, config.shaders.options[i].value); + } + } +} + +bool Config_reloadFrontendShaders(void) { + int old_values[FE_OPT_COUNT]; + int old_locks[FE_OPT_COUNT]; + int old_set_options[FE_OPT_COUNT]; + int old_shader_values[SH_NONE]; + int core_count = config.core.count; + int old_core_values[core_count ? core_count : 1]; + int old_core_locks[core_count ? core_count : 1]; + int old_core_set_options[core_count ? core_count : 1]; + int old_gamepad = gamepad_type; + int old_set_gamepad = shaderSetTouchesGamepad(); + int reload_core = old_set_gamepad; + int apply_overclock = 0; + int apply_sync_ref = 0; + + for (int i = 0; i < FE_OPT_COUNT; i++) { + old_values[i] = config.frontend.options[i].value; + old_locks[i] = config.frontend.options[i].lock; + old_set_options[i] = shaderSetTouchesFrontendOption(i); + } + for (int i = 0; i < SH_NONE; i++) + old_shader_values[i] = config.shaders.options[i].value; + for (int i = 0; i < core_count; i++) { + old_core_values[i] = config.core.options[i].value; + old_core_locks[i] = config.core.options[i].lock; + old_core_set_options[i] = shaderSetTouchesKey(config.core.options[i].key); + if (old_core_set_options[i]) reload_core = 1; + } + + Config_loadShaderSet(); + if (shaderSetTouchesGamepad()) + reload_core = 1; + for (int i = 0; i < config.core.count && !reload_core; i++) { + if (shaderSetTouchesKey(config.core.options[i].key)) + reload_core = 1; + } + if (reload_core) + reloadCoreOptions(old_core_values, old_core_locks, old_core_set_options, + old_gamepad, old_set_gamepad); + + resetFrontendShaders(); + readEffectiveFrontendShaders(0); + + for (int i = 0; i < FE_OPT_COUNT; i++) { + int new_set_option = shaderSetTouchesFrontendOption(i); + if (!old_set_options[i] && !new_set_option) { + config.frontend.options[i].value = old_values[i]; + config.frontend.options[i].lock = old_locks[i]; + } + else if (config.frontend.options[i].value != old_values[i]) { + Config_syncFrontend(config.frontend.options[i].key, config.frontend.options[i].value); + if (i == FE_OPT_OVERCLOCK) apply_overclock = 1; + if (i == FE_OPT_SYNC_REFERENCE) apply_sync_ref = 1; + } + } + + if (apply_overclock) setOverclock(overclock); + if (apply_sync_ref) chooseSyncRef(); + + reloadShaders(old_shader_values); + readEffectiveShaderOptions(); + applyShaderSettings(); + apply_live_video_reset(); + return true; +} + /* ----------------------------------------------------------------------- Config data: label arrays, button mappings, and struct Config initializer. Moved from minarch.c; these are the static data backing the config module. @@ -1717,6 +2279,7 @@ struct Config config = { [SHORTCUT_HOLD_REWIND] = {"Hold Rewind", -1, BTN_ID_NONE, 0}, [SHORTCUT_GAMESWITCHER] = {"Game Switcher", -1, BTN_ID_NONE, 0}, [SHORTCUT_SCREENSHOT] = {"Screenshot", -1, BTN_ID_NONE, 0}, + [SHORTCUT_NEXT_SHADER_SET] = {"Next Shader Set", -1, BTN_ID_NONE, 0}, // Trimui only [SHORTCUT_TOGGLE_TURBO_A] = {"Toggle Turbo A", -1, BTN_ID_NONE, 0}, [SHORTCUT_TOGGLE_TURBO_B] = {"Toggle Turbo B", -1, BTN_ID_NONE, 0}, @@ -1730,5 +2293,3 @@ struct Config config = { {NULL} }, }; - - diff --git a/workspace/all/minarch/ma_config.h b/workspace/all/minarch/ma_config.h index 85293afbd..f427db2f3 100644 --- a/workspace/all/minarch/ma_config.h +++ b/workspace/all/minarch/ma_config.h @@ -11,8 +11,14 @@ void Config_load(void); void Config_free(void); void Config_readOptions(void); void Config_readControls(void); -void Config_write(int override); +enum { + CONFIG_WRITE_FAILED, + CONFIG_WRITE_STANDARD, + CONFIG_WRITE_SHADER_SET, +}; +int Config_write(int override); void Config_restore(void); +bool Config_reloadFrontendShaders(void); void Config_syncShaders(char* key, int value); void applyShaderSettings(void); void initShaders(void); diff --git a/workspace/all/minarch/ma_frontend_opts.c b/workspace/all/minarch/ma_frontend_opts.c index 3c786b96c..cfe04f76c 100644 --- a/workspace/all/minarch/ma_frontend_opts.c +++ b/workspace/all/minarch/ma_frontend_opts.c @@ -3,6 +3,7 @@ #include "ma_cheats.h" #include "ra_integration.h" #include "notification.h" +#include "shader_sets.h" #include #include @@ -431,13 +432,19 @@ static int OptionSaveChanges_onConfirm(MenuList* list, int i) { char* message; switch (i) { case 0: { - Config_write(CONFIG_WRITE_ALL); - message = "Saved for console."; + int result = Config_write(CONFIG_WRITE_ALL); + if (result == CONFIG_WRITE_SHADER_SET) + message = "Saved console and shader set."; + else if (result == CONFIG_WRITE_STANDARD) + message = "Saved for console."; + else + message = "Unable to save for console."; break; } case 1: { - Config_write(CONFIG_WRITE_GAME); - message = "Saved for game."; + message = Config_write(CONFIG_WRITE_GAME) + ? "Saved for game." + : "Unable to save for game."; break; } default: { @@ -651,19 +658,54 @@ static int OptionPragmas_openMenu(MenuList* list, int i) { return MENU_CALLBACK_NOP; } + +#define SHADER_SET_MENU_ID -1 + +static ShaderSetList ShaderSetOptions = {0}; +static char **ShaderSetLabels = NULL; + +static void OptionShaders_freeFileList(char **files) { + if (!files) + return; + for (int i = 0; files[i]; i++) + free(files[i]); + free(files); +} + +static void OptionShaders_refreshItems(MenuList *list) { + for (int i = 0; i < config.shaders.count; i++) + list->items[i + 1].value = config.shaders.options[i].value; +} + static int OptionShaders_optionChanged(MenuList* list, int i) { MenuItem* item = &list->items[i]; + if (item->id == SHADER_SET_MENU_ID) { + int previous = ShaderSets_activeIndex(&ShaderSetOptions); + if (item->value == previous) + return MENU_CALLBACK_NOP; + + if (item->value < 0 || item->value >= ShaderSetOptions.count || + !ShaderSets_setActive(ShaderSetOptions.names[item->value])) { + item->value = previous; + Menu_message("Unable to save shader set", (char*[]){"B", "BACK", NULL}); + return MENU_CALLBACK_NOP; + } + + Config_reloadFrontendShaders(); + item->value = ShaderSets_activeIndex(&ShaderSetOptions); + OptionShaders_refreshItems(list); + return MENU_CALLBACK_NOP; + } + + int shader_index = item->id; // Process menu entry change, update underlying config cruft and call handler Config_syncShaders(item->key, item->value); // Apply shader pragmas if needed applyShaderSettings(); // Update menu entries to reflect any changes made by the handler - for (int y = 0; y < config.shaders.count; y++) { - MenuItem* item = &list->items[y]; - item->value = config.shaders.options[y].value; - } + OptionShaders_refreshItems(list); - if(i==SH_SHADERS_PRESET) { + if(shader_index==SH_SHADERS_PRESET) { // On shader preset change: // Push all new shader settings to shader engine, // compile shaders if needed, populate pragmas list @@ -696,9 +738,54 @@ int OptionShaders_openMenu(MenuList* list, int i) { return MENU_CALLBACK_NOP; } - ShaderOptions_menu.items = calloc(config.shaders.count + 1, sizeof(MenuItem)); + if (!ShaderSets_list(&ShaderSetOptions)) { + OptionShaders_freeFileList(filelist); + Menu_message("Unable to read shader sets", (char*[]){"B", "BACK", NULL}); + return MENU_CALLBACK_NOP; + } + + ShaderSetLabels = calloc(ShaderSetOptions.count + 1, sizeof(char *)); + if (!ShaderSetLabels) { + OptionShaders_freeFileList(filelist); + ShaderSets_freeList(&ShaderSetOptions); + Menu_message("Unable to read shader sets", (char*[]){"B", "BACK", NULL}); + return MENU_CALLBACK_NOP; + } + for (int i = 0; i < ShaderSetOptions.count; i++) { + ShaderSetLabels[i] = strdup(ShaderSets_displayName(ShaderSetOptions.names[i])); + if (!ShaderSetLabels[i]) { + for (int j = 0; j < i; j++) + free(ShaderSetLabels[j]); + free(ShaderSetLabels); + ShaderSetLabels = NULL; + OptionShaders_freeFileList(filelist); + ShaderSets_freeList(&ShaderSetOptions); + Menu_message("Unable to read shader sets", (char*[]){"B", "BACK", NULL}); + return MENU_CALLBACK_NOP; + } + } + + ShaderOptions_menu.items = calloc(config.shaders.count + 2, sizeof(MenuItem)); + if (!ShaderOptions_menu.items) { + for (int i = 0; i < ShaderSetOptions.count; i++) + free(ShaderSetLabels[i]); + free(ShaderSetLabels); + ShaderSetLabels = NULL; + OptionShaders_freeFileList(filelist); + ShaderSets_freeList(&ShaderSetOptions); + Menu_message("Unable to open shader settings", (char*[]){"B", "BACK", NULL}); + return MENU_CALLBACK_NOP; + } + + MenuItem *set_item = &ShaderOptions_menu.items[0]; + set_item->id = SHADER_SET_MENU_ID; + set_item->name = "Shader Set"; + set_item->desc = "Globally applies a frontend and shader configuration immediately."; + set_item->value = ShaderSets_activeIndex(&ShaderSetOptions); + set_item->values = ShaderSetLabels; + for (int i = 0; i < config.shaders.count; i++) { - MenuItem* item = &ShaderOptions_menu.items[i]; + MenuItem* item = &ShaderOptions_menu.items[i + 1]; Option* configitem = &config.shaders.options[i]; item->id = i; item->name = configitem->name; @@ -718,11 +805,15 @@ int OptionShaders_openMenu(MenuList* list, int i) { } - if (ShaderOptions_menu.items[0].name) { - Menu_options(&ShaderOptions_menu); - } else { - Menu_message("No shaders available\n/Shaders folder or shader files not found", (char*[]){"B", "BACK", NULL}); - } + Menu_options(&ShaderOptions_menu); + + free(ShaderOptions_menu.items); + ShaderOptions_menu.items = NULL; + for (int i = 0; i < ShaderSetOptions.count; i++) + free(ShaderSetLabels[i]); + free(ShaderSetLabels); + ShaderSetLabels = NULL; + ShaderSets_freeList(&ShaderSetOptions); return MENU_CALLBACK_NOP; } diff --git a/workspace/all/minarch/ma_input.c b/workspace/all/minarch/ma_input.c index 3233b214d..f49383939 100644 --- a/workspace/all/minarch/ma_input.c +++ b/workspace/all/minarch/ma_input.c @@ -1,6 +1,9 @@ #include "ma_internal.h" #include "ma_input.h" +#include "notification.h" +#include "shader_sets.h" +#include #include int setFastForward(int enable) { @@ -12,6 +15,41 @@ int setFastForward(int enable) { return val; } +static void nextShaderSet(void) { + ShaderSetList list; + char name[MAX_PATH]; + char message[NOTIFICATION_MAX_MESSAGE]; + + if (!ShaderSets_list(&list)) { + Notification_push(NOTIFICATION_SETTING, "Unable to read shader sets", NULL); + return; + } + + if (list.count == 1) { + int had_active_set = config.shader_set_cfg || config.shader_set_override_cfg; + if (ShaderSets_setActive("")) { + if (had_active_set) + Config_reloadFrontendShaders(); + Notification_push(NOTIFICATION_SETTING, "Shader set: Disabled", NULL); + } + else + Notification_push(NOTIFICATION_SETTING, "Unable to change shader set", NULL); + ShaderSets_freeList(&list); + return; + } + + if (!ShaderSets_advance(&list, name, sizeof(name))) { + ShaderSets_freeList(&list); + Notification_push(NOTIFICATION_SETTING, "Unable to change shader set", NULL); + return; + } + + Config_reloadFrontendShaders(); + snprintf(message, sizeof(message), "Shader set: %s", ShaderSets_displayName(name)); + Notification_push(NOTIFICATION_SETTING, message, NULL); + ShaderSets_freeList(&list); +} + static uint32_t buttons = 0; // RETRO_DEVICE_ID_JOYPAD_* buttons static int ignore_menu = 0; void input_poll_callback(void) { @@ -167,6 +205,9 @@ void input_poll_callback(void) { screen_effect = (screen_effect + 1) % config.frontend.options[FE_OPT_EFFECT].count; Config_syncFrontend(config.frontend.options[FE_OPT_EFFECT].key, screen_effect); break; + case SHORTCUT_NEXT_SHADER_SET: + nextShaderSet(); + break; default: break; } diff --git a/workspace/all/minarch/ma_internal.h b/workspace/all/minarch/ma_internal.h index 49d4a8935..b27721a4c 100644 --- a/workspace/all/minarch/ma_internal.h +++ b/workspace/all/minarch/ma_internal.h @@ -127,6 +127,7 @@ extern int rewind_cfg_audio; extern int rewind_cfg_compress; extern int rewind_cfg_lz4_acceleration; extern int rewind_init_ready; +extern int has_pending_opt_change; #include "ma_rewind.h" @@ -182,6 +183,8 @@ struct Config { char* system_cfg; char* default_cfg; char* user_cfg; + char* shader_set_cfg; + char* shader_set_override_cfg; char* shaders_preset; char* device_tag; OptionList frontend; @@ -245,6 +248,7 @@ enum { SHORTCUT_HOLD_REWIND, SHORTCUT_GAMESWITCHER, SHORTCUT_SCREENSHOT, + SHORTCUT_NEXT_SHADER_SET, // Trimui only SHORTCUT_TOGGLE_TURBO_A, SHORTCUT_TOGGLE_TURBO_B, diff --git a/workspace/all/minarch/makefile b/workspace/all/minarch/makefile index ae10f2529..45446fd4f 100644 --- a/workspace/all/minarch/makefile +++ b/workspace/all/minarch/makefile @@ -24,7 +24,7 @@ PRODUCT= build/$(PLATFORM)/$(TARGET).elf INCDIR = -I. -I./libretro-common/include/ -I../common/ -I../../$(PLATFORM)/platform/ SOURCE = $(TARGET).c ma_cheats.c ma_rewind.c ma_audio.c ma_input.c \ ma_options.c ma_frontend_opts.c ma_saves.c ma_video.c ma_core.c ma_game.c ma_environment.c ma_config.c ma_menu.c ma_runframe.c \ - ../common/scaler.c ../common/utils.c ../common/config.c ../common/api.c \ + ../common/scaler.c ../common/utils.c ../common/config.c ../common/api.c ../common/shader_sets.c \ ../common/notification.c ../../$(PLATFORM)/platform/platform.c # RA support diff --git a/workspace/all/minarch/minarch.c b/workspace/all/minarch/minarch.c index b3e156eda..6ef291329 100644 --- a/workspace/all/minarch/minarch.c +++ b/workspace/all/minarch/minarch.c @@ -67,6 +67,7 @@ int DEVICE_WIDTH = 0; int DEVICE_HEIGHT = 0; int DEVICE_PITCH = 0; int shader_reset_suppressed = 0; +int has_pending_opt_change = 0; GFX_Renderer renderer; @@ -236,8 +237,6 @@ int main(int argc , char* argv[]) { chooseSyncRef(); - int has_pending_opt_change = 0; - // then initialize custom shaders from settings initShaders(); Config_readOptions(); @@ -245,8 +244,6 @@ int main(int argc , char* argv[]) { int rewind_initialized = Rewind_init(core.serialize_size ? core.serialize_size() : 0); rewind_init_ready = 1; // Mark setup as attempted, even if rewind init failed, so option changes can retry it later. if (rewind_initialized && core.serialize_size) Rewind_on_state_change(); - // release config when all is loaded - Config_free(); LOG_info("total startup time %ims\n\n",SDL_GetTicks()); @@ -302,11 +299,12 @@ int main(int argc , char* argv[]) { Notification_renderToLayer(5); // Always call - handles cleanup when inactive - if (has_pending_opt_change) { + if (has_pending_opt_change && !config.core.changed) { has_pending_opt_change = 0; if (Core_updateAVInfo()) { LOG_info("AV info changed, reset sound system"); SND_resetAudio(core.sample_rate, core.fps); + renderer.dst_p = 0; } chooseSyncRef(); }