From 71302c0938f638922ca187bca7d5649c3ec6ad85 Mon Sep 17 00:00:00 2001 From: Luke Usher Date: Tue, 13 Feb 2024 09:23:47 +0000 Subject: [PATCH] shaders: switch to slang-shaders rather than quark shaders via librashader --- .github/workflows/build.yml | 25 + LICENSE | 359 +++++++++++ README.md | 1 + ares/Shaders/AANN.shader/AANN.fs | 81 --- ares/Shaders/AANN.shader/manifest.bml | 8 - ares/Shaders/CRT-Lottes.shader/manifest.bml | 10 - ares/Shaders/CRT-Lottes.shader/shadertoy.fs | 145 ----- ares/Shaders/CRT-Lottes2.shader/lottes.fs | 609 ------------------ ares/Shaders/CRT-Lottes2.shader/lottes.vs | 16 - ares/Shaders/CRT-Lottes2.shader/manifest.bml | 12 - ares/Shaders/Curvature.shader/curvature.fs | 21 - ares/Shaders/Curvature.shader/manifest.bml | 4 - .../Edge Detection.shader/edge-detection.fs | 25 - .../Edge Detection.shader/manifest.bml | 4 - .../Fake-Self-Illumination.png | Bin 2589 -> 0 bytes .../GritsScanlines.shader/GritsScanlines.fs | 99 --- .../Scanline-LUT-4px.png | Bin 2831 -> 0 bytes ...nitron D50 Absolute Colorimetric - LUT.png | Bin 72062 -> 0 bytes .../GritsScanlines.shader/manifest.bml | 35 - ares/Shaders/Interlacing.shader/interlace.fs | 39 -- ares/Shaders/Interlacing.shader/manifest.bml | 6 - ares/Shaders/PAL-r57shell.shader/manifest.bml | 11 - .../PAL-r57shell.shader/pal-r57shell.fs | 368 ----------- ares/Shaders/Phosphorish.shader/manifest.bml | 2 - .../Shaders/Phosphorish.shader/phosphorish.fs | 56 -- ares/Shaders/Scanline.shader/manifest.bml | 4 - ares/Shaders/Scanline.shader/scanline.fs | 20 - .../Sharp-Bilinear.shader/manifest.bml | 8 - .../Sharp-Bilinear.shader/sharp-bilinear.fs | 33 - desktop-ui/GNUmakefile | 44 +- desktop-ui/presentation/presentation.cpp | 16 +- nall/intrinsics.hpp | 1 - ruby/GNUmakefile | 3 +- ruby/video/glx2.cpp | 417 ------------ ruby/video/opengl/bind.hpp | 4 + ruby/video/opengl/main.hpp | 239 +++---- ruby/video/opengl/opengl.hpp | 39 +- ruby/video/opengl/program.hpp | 108 ---- ruby/video/opengl/shaders.hpp | 91 --- ruby/video/opengl/surface.hpp | 112 +--- ruby/video/video.cpp | 16 - scripts/update-subtrees.sh | 4 +- tests/i8080/GNUmakefile | 4 - thirdparty/librashader/build-librashader.sh | 56 ++ .../librashader/include/librashader_ld.h | 10 +- 45 files changed, 636 insertions(+), 2529 deletions(-) delete mode 100644 ares/Shaders/AANN.shader/AANN.fs delete mode 100644 ares/Shaders/AANN.shader/manifest.bml delete mode 100644 ares/Shaders/CRT-Lottes.shader/manifest.bml delete mode 100644 ares/Shaders/CRT-Lottes.shader/shadertoy.fs delete mode 100644 ares/Shaders/CRT-Lottes2.shader/lottes.fs delete mode 100644 ares/Shaders/CRT-Lottes2.shader/lottes.vs delete mode 100644 ares/Shaders/CRT-Lottes2.shader/manifest.bml delete mode 100644 ares/Shaders/Curvature.shader/curvature.fs delete mode 100644 ares/Shaders/Curvature.shader/manifest.bml delete mode 100644 ares/Shaders/Edge Detection.shader/edge-detection.fs delete mode 100644 ares/Shaders/Edge Detection.shader/manifest.bml delete mode 100644 ares/Shaders/GritsScanlines.shader/Fake-Self-Illumination.png delete mode 100644 ares/Shaders/GritsScanlines.shader/GritsScanlines.fs delete mode 100644 ares/Shaders/GritsScanlines.shader/Scanline-LUT-4px.png delete mode 100644 ares/Shaders/GritsScanlines.shader/Trinitron D50 Absolute Colorimetric - LUT.png delete mode 100644 ares/Shaders/GritsScanlines.shader/manifest.bml delete mode 100644 ares/Shaders/Interlacing.shader/interlace.fs delete mode 100644 ares/Shaders/Interlacing.shader/manifest.bml delete mode 100644 ares/Shaders/PAL-r57shell.shader/manifest.bml delete mode 100644 ares/Shaders/PAL-r57shell.shader/pal-r57shell.fs delete mode 100644 ares/Shaders/Phosphorish.shader/manifest.bml delete mode 100644 ares/Shaders/Phosphorish.shader/phosphorish.fs delete mode 100644 ares/Shaders/Scanline.shader/manifest.bml delete mode 100644 ares/Shaders/Scanline.shader/scanline.fs delete mode 100644 ares/Shaders/Sharp-Bilinear.shader/manifest.bml delete mode 100644 ares/Shaders/Sharp-Bilinear.shader/sharp-bilinear.fs delete mode 100644 ruby/video/glx2.cpp delete mode 100644 ruby/video/opengl/program.hpp delete mode 100644 ruby/video/opengl/shaders.hpp create mode 100755 thirdparty/librashader/build-librashader.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7c1e51b43..6e54272a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -87,6 +87,24 @@ jobs: keychain-password: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} - name: Checkout source code uses: actions/checkout@v2 + - name: Install Windows Dependencies + if: runner.os == 'Windows' + run: | + export PATH="/c/Users/runneradmin/.cargo/bin:$PATH" # correct on windows-latest as of 2024-02-19 + if [[ ${{ matrix.platform.name }} == *-arm64 ]]; then + rustup toolchain install nightly + rustup default nightly + rustup target add aarch64-pc-windows-msvc + pushd thirdparty/librashader + ./build-librashader.sh aarch64-pc-windows-msvc + popd + else + rustup toolchain install nightly + rustup default nightly + pushd thirdparty/librashader + ./build-librashader.sh + popd + fi - name: "macOS: recover MoltenVK cache" if: runner.os == 'macOS' uses: actions/cache@v3 @@ -108,6 +126,10 @@ jobs: run: | brew install make ninja cmake pip install --no-input setuptools + rustup toolchain install nightly + rustup default nightly + rustup target add x86_64-apple-darwin + rustup target add aarch64-apple-darwin echo "MAKE=gmake" >> $GITHUB_ENV pushd thirdparty/SDL ./build-SDL.sh @@ -115,6 +137,9 @@ jobs: pushd thirdparty/MoltenVK ./build-moltenvk.sh popd + pushd thirdparty/librashader + ./build-librashader.sh + popd - name: Set up MSVC environment if: matrix.platform.msvc-arch != '' uses: ilammy/msvc-dev-cmd@v1 diff --git a/LICENSE b/LICENSE index 39e58b51f..0422f916d 100644 --- a/LICENSE +++ b/LICENSE @@ -470,3 +470,362 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---------------------------------------------------------------------- +---------------------------------------------------------------------- +librashader +Mozilla Public License Version 2.0 +================================== + +### 1. Definitions + +**1.1. “Contributor”** +means each individual or legal entity that creates, contributes to +the creation of, or owns Covered Software. + +**1.2. “Contributor Version”** +means the combination of the Contributions of others (if any) used +by a Contributor and that particular Contributor's Contribution. + +**1.3. “Contribution”** +means Covered Software of a particular Contributor. + +**1.4. “Covered Software”** +means Source Code Form to which the initial Contributor has attached +the notice in Exhibit A, the Executable Form of such Source Code +Form, and Modifications of such Source Code Form, in each case +including portions thereof. + +**1.5. “Incompatible With Secondary Licenses”** +means + +* **(a)** that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or +* **(b)** that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +**1.6. “Executable Form”** +means any form of the work other than Source Code Form. + +**1.7. “Larger Work”** +means a work that combines Covered Software with other material, in +a separate file or files, that is not Covered Software. + +**1.8. “License”** +means this document. + +**1.9. “Licensable”** +means having the right to grant, to the maximum extent possible, +whether at the time of the initial grant or subsequently, any and +all of the rights conveyed by this License. + +**1.10. “Modifications”** +means any of the following: + +* **(a)** any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or +* **(b)** any new file in Source Code Form that contains any Covered + Software. + +**1.11. “Patent Claims” of a Contributor** +means any patent claim(s), including without limitation, method, +process, and apparatus claims, in any patent Licensable by such +Contributor that would be infringed, but for the grant of the +License, by the making, using, selling, offering for sale, having +made, import, or transfer of either its Contributions or its +Contributor Version. + +**1.12. “Secondary License”** +means either the GNU General Public License, Version 2.0, the GNU +Lesser General Public License, Version 2.1, the GNU Affero General +Public License, Version 3.0, or any later versions of those +licenses. + +**1.13. “Source Code Form”** +means the form of the work preferred for making modifications. + +**1.14. “You” (or “Your”)** +means an individual or a legal entity exercising rights under this +License. For legal entities, “You” includes any entity that +controls, is controlled by, or is under common control with You. For +purposes of this definition, “control” means **(a)** the power, direct +or indirect, to cause the direction or management of such entity, +whether by contract or otherwise, or **(b)** ownership of more than +fifty percent (50%) of the outstanding shares or beneficial +ownership of such entity. + + +### 2. License Grants and Conditions + +#### 2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +* **(a)** under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and +* **(b)** under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +#### 2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +#### 2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +* **(a)** for any code that a Contributor has removed from Covered Software; + or +* **(b)** for infringements caused by: **(i)** Your and any other third party's + modifications of Covered Software, or **(ii)** the combination of its + Contributions with other software (except as part of its Contributor + Version); or +* **(c)** under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +#### 2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +#### 2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +#### 2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +#### 2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + + +### 3. Responsibilities + +#### 3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +#### 3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +* **(a)** such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +* **(b)** You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +#### 3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +#### 3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +#### 3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + + +### 4. Inability to Comply Due to Statute or Regulation + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: **(a)** comply with +the terms of this License to the maximum extent possible; and **(b)** +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + + +### 5. Termination + +**5.1.** The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated **(a)** provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and **(b)** on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +**5.2.** If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +**5.3.** In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + + +### 6. Disclaimer of Warranty + +> Covered Software is provided under this License on an “as is” +> basis, without warranty of any kind, either expressed, implied, or +> statutory, including, without limitation, warranties that the +> Covered Software is free of defects, merchantable, fit for a +> particular purpose or non-infringing. The entire risk as to the +> quality and performance of the Covered Software is with You. +> Should any Covered Software prove defective in any respect, You +> (not any Contributor) assume the cost of any necessary servicing, +> repair, or correction. This disclaimer of warranty constitutes an +> essential part of this License. No use of any Covered Software is +> authorized under this License except under this disclaimer. + +### 7. Limitation of Liability + +> Under no circumstances and under no legal theory, whether tort +> (including negligence), contract, or otherwise, shall any +> Contributor, or anyone who distributes Covered Software as +> permitted above, be liable to You for any direct, indirect, +> special, incidental, or consequential damages of any character +> including, without limitation, damages for lost profits, loss of +> goodwill, work stoppage, computer failure or malfunction, or any +> and all other commercial damages or losses, even if such party +> shall have been informed of the possibility of such damages. This +> limitation of liability shall not apply to liability for death or +> personal injury resulting from such party's negligence to the +> extent applicable law prohibits such limitation. Some +> jurisdictions do not allow the exclusion or limitation of +> incidental or consequential damages, so this exclusion and +> limitation may not apply to You. + + +### 8. Litigation + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + + +### 9. Miscellaneous + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + + +### 10. Versions of the License + +#### 10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +#### 10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +#### 10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +#### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +## Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +## Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + +---------------------------------------------------------------------- \ No newline at end of file diff --git a/README.md b/README.md index 53bf29dc7..0f9660666 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Once complete, open a CLANG64 terminal window and proceed with building ares. ###### Debug Symbols When building with clang, by default symbols will be generated for debug builds using an MSVC compatible format (CodeView) for use with Windows debugging tools. In order to generate GDB compatible symbols, specify the following option: `symformat=gdb` + Compilation ----------- diff --git a/ares/Shaders/AANN.shader/AANN.fs b/ares/Shaders/AANN.shader/AANN.fs deleted file mode 100644 index 1f7bc4d0c..000000000 --- a/ares/Shaders/AANN.shader/AANN.fs +++ /dev/null @@ -1,81 +0,0 @@ -#version 150 - -//anti-aliased nearest-neighbor - -precision highp float; - -uniform sampler2D source[]; -uniform vec4 sourceSize[]; -uniform vec4 targetSize[]; - -in Vertex { - vec2 texCoord; -}; - -out vec4 fragColor; - -vec4 vpow(vec4 n, float e) { - return vec4(pow(n.x, e), pow(n.y, e), pow(n.z, e), pow(n.w, e)); -} - -vec4 toLQV(vec3 c) { - return vec4(c.r, c.g, c.b, c.r * 0.2989 + c.g * 0.5870 + c.b * 0.1140); -} - -vec3 fromLQV(vec4 c) { - float f = c.w / (c.r * 0.2989 + c.g * 0.5870 + c.b * 0.1140); - return vec3(c.rgb) * f; -} - -vec3 percent(float ssize, float tsize, float coord) { - float minfull = (coord * tsize - 0.5) / tsize * ssize; - float maxfull = (coord * tsize + 0.5) / tsize * ssize; - float realfull = floor(maxfull); - - if(minfull > realfull) { - return vec3( - 1, - (realfull + 0.5) / ssize, - (realfull + 0.5) / ssize - ); - } - - return vec3( - (maxfull - realfull) / (maxfull - minfull), - (realfull - 0.5) / ssize, - (realfull + 0.5) / ssize - ); -} - -void main() { - float srgb = 2.1; - float gamma = 3.0; - - vec3 x = percent(sourceSize[0].x, targetSize[0].x, texCoord.x); - vec3 y = percent(sourceSize[0].y, targetSize[0].y, texCoord.y); - - //get points to interpolate across in linear RGB - vec4 a = toLQV(vpow(texture(source[0], vec2(x[1], y[1])), srgb).rgb); - vec4 b = toLQV(vpow(texture(source[0], vec2(x[2], y[1])), srgb).rgb); - vec4 c = toLQV(vpow(texture(source[0], vec2(x[1], y[2])), srgb).rgb); - vec4 d = toLQV(vpow(texture(source[0], vec2(x[2], y[2])), srgb).rgb); - - //use perceptual gamma for luminance component - a.w = pow(a.w, 1 / gamma); - b.w = pow(b.w, 1 / gamma); - c.w = pow(c.w, 1 / gamma); - d.w = pow(d.w, 1 / gamma); - - //interpolate - vec4 gammaLQV = - (1.0 - x[0]) * (1.0 - y[0]) * a + - (0.0 + x[0]) * (1.0 - y[0]) * b + - (1.0 - x[0]) * (0.0 + y[0]) * c + - (0.0 + x[0]) * (0.0 + y[0]) * d; - - //convert luminance gamma back to linear - gammaLQV.w = pow(gammaLQV.w, gamma); - - //convert color back to sRGB - fragColor = vpow(vec4(fromLQV(gammaLQV), 1), 1 / srgb); -} diff --git a/ares/Shaders/AANN.shader/manifest.bml b/ares/Shaders/AANN.shader/manifest.bml deleted file mode 100644 index abf36dba1..000000000 --- a/ares/Shaders/AANN.shader/manifest.bml +++ /dev/null @@ -1,8 +0,0 @@ -input - filter: nearest - -program - fragment: AANN.fs - -output - filter: nearest diff --git a/ares/Shaders/CRT-Lottes.shader/manifest.bml b/ares/Shaders/CRT-Lottes.shader/manifest.bml deleted file mode 100644 index d025afcf0..000000000 --- a/ares/Shaders/CRT-Lottes.shader/manifest.bml +++ /dev/null @@ -1,10 +0,0 @@ -input - filter: nearest - -program - filter: nearest - wrap: border - fragment: shadertoy.fs - -output - filter: nearest \ No newline at end of file diff --git a/ares/Shaders/CRT-Lottes.shader/shadertoy.fs b/ares/Shaders/CRT-Lottes.shader/shadertoy.fs deleted file mode 100644 index 3b69c43e9..000000000 --- a/ares/Shaders/CRT-Lottes.shader/shadertoy.fs +++ /dev/null @@ -1,145 +0,0 @@ -// -// PUBLIC DOMAIN CRT STYLED SCAN-LINE SHADER -// -// by Timothy Lottes -// -// This is more along the style of a really good CGA arcade monitor. -// With RGB inputs instead of NTSC. -// The shadow mask example has the mask rotated 90 degrees for less chromatic aberration. -// -// Left it unoptimized to show the theory behind the algorithm. -// -// It is an example what I personally would want as a display option for pixel art games. -// Please take and use, change, or whatever. -// - -#version 150 - -uniform sampler2D source[]; -uniform vec4 sourceSize[]; -uniform int phase; - -in Vertex { - vec2 texCoord; -}; - -out vec4 fragColor; - -// Emulated input resolution. - vec2 res=sourceSize[0].xy; - -// Hardness of scanline. -// -8.0 = soft -// -16.0 = medium -float hardScan=-8.0; - -// Hardness of pixels in scanline. -// -2.0 = soft -// -4.0 = hard -float hardPix=-3.0; - -// Display warp. -// 0.0 = none -// 1.0/8.0 = extreme -vec2 warp=vec2(1.0/32.0,1.0/24.0); - -// Amount of shadow mask. -float maskDark=0.5; -float maskLight=1.5; - -//------------------------------------------------------------------------ - -// sRGB to Linear. -// Assuing using sRGB typed textures this should not be needed. -float ToLinear1(float c){return(c<=0.04045)?c/12.92:pow((c+0.055)/1.055,2.4);} -vec3 ToLinear(vec3 c){return vec3(ToLinear1(c.r),ToLinear1(c.g),ToLinear1(c.b));} - -// Linear to sRGB. -// Assuing using sRGB typed textures this should not be needed. -float ToSrgb1(float c){return(c<0.0031308?c*12.92:1.055*pow(c,0.41666)-0.055);} -vec3 ToSrgb(vec3 c){return vec3(ToSrgb1(c.r),ToSrgb1(c.g),ToSrgb1(c.b));} - -// Nearest emulated sample given floating point position and texel offset. -// Also zero's off screen. -vec3 Fetch(vec2 pos,vec2 off){ - pos=(floor(pos*res+off)+vec2(0.5,0.5))/res; - return ToLinear(1.2 * texture(source[0],pos.xy,-16.0).rgb);} - -// Distance in emulated pixels to nearest texel. -vec2 Dist(vec2 pos){pos=pos*res;return -((pos-floor(pos))-vec2(0.5));} - -// 1D Gaussian. -float Gaus(float pos,float scale){return exp2(scale*pos*pos);} - -// 3-tap Gaussian filter along horz line. -vec3 Horz3(vec2 pos,float off){ - vec3 b=Fetch(pos,vec2(-1.0,off)); - vec3 c=Fetch(pos,vec2( 0.0,off)); - vec3 d=Fetch(pos,vec2( 1.0,off)); - float dst=Dist(pos).x; - // Convert distance to weight. - float scale=hardPix; - float wb=Gaus(dst-1.0,scale); - float wc=Gaus(dst+0.0,scale); - float wd=Gaus(dst+1.0,scale); - // Return filtered sample. - return (b*wb+c*wc+d*wd)/(wb+wc+wd);} - -// 5-tap Gaussian filter along horz line. -vec3 Horz5(vec2 pos,float off){ - vec3 a=Fetch(pos,vec2(-2.0,off)); - vec3 b=Fetch(pos,vec2(-1.0,off)); - vec3 c=Fetch(pos,vec2( 0.0,off)); - vec3 d=Fetch(pos,vec2( 1.0,off)); - vec3 e=Fetch(pos,vec2( 2.0,off)); - float dst=Dist(pos).x; - // Convert distance to weight. - float scale=hardPix; - float wa=Gaus(dst-2.0,scale); - float wb=Gaus(dst-1.0,scale); - float wc=Gaus(dst+0.0,scale); - float wd=Gaus(dst+1.0,scale); - float we=Gaus(dst+2.0,scale); - // Return filtered sample. - return (a*wa+b*wb+c*wc+d*wd+e*we)/(wa+wb+wc+wd+we);} - -// Return scanline weight. -float Scan(vec2 pos,float off){ - float dst=Dist(pos).y; - return Gaus(dst+off,hardScan);} - -// Allow nearest three lines to effect pixel. -vec3 Tri(vec2 pos){ - vec3 a=Horz3(pos,-1.0); - vec3 b=Horz5(pos, 0.0); - vec3 c=Horz3(pos, 1.0); - float wa=Scan(pos,-1.0); - float wb=Scan(pos, 0.0); - float wc=Scan(pos, 1.0); - return a*wa+b*wb+c*wc;} - -// Distortion of scanlines, and end of screen alpha. -vec2 Warp(vec2 pos){ - pos=pos*2.0-1.0; - pos*=vec2(1.0+(pos.y*pos.y)*warp.x,1.0+(pos.x*pos.x)*warp.y); - return pos*0.5+0.5;} - -// Shadow mask. -vec3 Mask(vec2 pos){ - pos.x+=pos.y*3.0; - vec3 mask=vec3(maskDark,maskDark,maskDark); - pos.x=fract(pos.x/6.0); - if(pos.x<0.333)mask.r=maskLight; - else if(pos.x<0.666)mask.g=maskLight; - else mask.b=maskLight; - return mask;} - -void main() { - vec2 pos = texCoord.xy * 1.0001; - hardScan=-12.0; -// maskDark=maskLight; - pos=Warp(pos.xy); - fragColor.rgb=Tri(pos)*Mask(gl_FragCoord.xy); - fragColor.a=1.0; - fragColor = vec4(ToSrgb(fragColor.rgb), fragColor.a); -} diff --git a/ares/Shaders/CRT-Lottes2.shader/lottes.fs b/ares/Shaders/CRT-Lottes2.shader/lottes.fs deleted file mode 100644 index 998050d04..000000000 --- a/ares/Shaders/CRT-Lottes2.shader/lottes.fs +++ /dev/null @@ -1,609 +0,0 @@ -#version 150 - -//_____________________________/\_______________________________ -//============================================================== -// -// -// [CRTS] PUBLIC DOMAIN CRT-STYLED SCALAR - 20180120b -// -// by Timothy Lottes -// https://www.shadertoy.com/view/MtSfRK -// adapted for quark syntax by hunterk -// -// -//============================================================== -//////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////// -//_____________________________/\_______________________________ -//============================================================== -// -// WHAT'S NEW -// -//-------------------------------------------------------------- -// Evolution of prior shadertoy example -//-------------------------------------------------------------- -// This one is semi-optimized -// - Less texture fetches -// - Didn't get to instruction level optimization -// - Could likely use texture fetch to generate phosphor mask -//-------------------------------------------------------------- -// Added options to disable unused features -//-------------------------------------------------------------- -// Added in exposure matching -// - Given scan-line effect and mask always darkens image -// - Uses generalized tonemapper to boost mid-level -// - Note this can compress highlights -// - And won't get back peak brightness -// - But best option if one doesn't want as much darkening -//-------------------------------------------------------------- -// Includes option saturation and contrast controls -//-------------------------------------------------------------- -// Added in subtractive aperture grille -// - This is a bit brighter than prior -//-------------------------------------------------------------- -// Make sure input to this filter is already low-resolution -// - This is not designed to work on titles doing the following -// - Rendering to hi-res with nearest sampling -//-------------------------------------------------------------- -// Added a fast and more pixely option for 2 tap/pixel -//-------------------------------------------------------------- -// Improved the vignette when WARP is enabled -//-------------------------------------------------------------- -// Didn't test HLSL or CPU options -// - Will incorportate patches if they are broken -// - But out of time to try them myself -//============================================================== -//////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////// -//_____________________________/\_______________________________ -//============================================================== -// -// LICENSE = UNLICENSE (aka PUBLIC DOMAIN) -// -//-------------------------------------------------------------- -// This is free and unencumbered software released into the -// public domain. -//-------------------------------------------------------------- -// Anyone is free to copy, modify, publish, use, compile, sell, -// or distribute this software, either in source code form or as -// a compiled binary, for any purpose, commercial or -// non-commercial, and by any means. -//-------------------------------------------------------------- -// In jurisdictions that recognize copyright laws, the author or -// authors of this software dedicate any and all copyright -// interest in the software to the public domain. We make this -// dedication for the benefit of the public at large and to the -// detriment of our heirs and successors. We intend this -// dedication to be an overt act of relinquishment in perpetuity -// of all present and future rights to this software under -// copyright law. -//-------------------------------------------------------------- -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY -// KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE -// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR -// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN -// AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT -// OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -//-------------------------------------------------------------- -// For more information, please refer to -// -//============================================================== -//////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////// - -#define MASK 1.0 -#define MASK_INTENSITY 0.5 -#define SCANLINE_THINNESS 0.5 -#define SCAN_BLUR 2.5 -#define CURVATURE 0.02 -#define TRINITRON_CURVE 0.0 -#define CORNER 3.0 -#define CRT_GAMMA 2.4 - -uniform sampler2D source[]; -uniform vec4 sourceSize[]; -uniform vec4 outputSize; - -in Vertex { - vec2 vTexCoord; -}; - -out vec4 FragColor; - -//_____________________________/\_______________________________ -//============================================================== -// -// GAMMA FUNCTIONS -// -//-------------------------------------------------------------- -//-------------------------------------------------------------- -// Since shadertoy doesn't have sRGB textures -// And we need linear input into shader -// Don't do this in your code -float FromSrgb1(float c){ - return (c<=0.04045)?c*(1.0/12.92): - pow(c*(1.0/1.055)+(0.055/1.055),CRT_GAMMA);} -//-------------------------------------------------------------- -vec3 FromSrgb(vec3 c){return vec3( - FromSrgb1(c.r),FromSrgb1(c.g),FromSrgb1(c.b));} - -// Convert from linear to sRGB -// Since shader toy output is not linear -float ToSrgb1(float c){ - return(c<0.0031308?c*12.92:1.055*pow(c,0.41666)-0.055);} -//-------------------------------------------------------------- -vec3 ToSrgb(vec3 c){return vec3( - ToSrgb1(c.r),ToSrgb1(c.g),ToSrgb1(c.b));} -//-------------------------------------------------------------- - -//_____________________________/\_______________________________ -//============================================================== -// -// DEFINES -// -//-------------------------------------------------------------- -// CRTS_CPU - CPU code -// CRTS_GPU - GPU code -//-------------------------------------------------------------- -// CRTS_GLSL - GLSL -// CRTS_HLSL - HLSL (not tested yet) -//-------------------------------------------------------------- -// CRTS_DEBUG - Define to see on/off split screen -//-------------------------------------------------------------- -// CRTS_WARP - Apply screen warp -//-------------------------------------------------------------- -// CRTS_2_TAP - Faster very pixely 2-tap filter (off is 8) -//-------------------------------------------------------------- -// CRTS_MASK_GRILLE - Aperture grille (aka Trinitron) -// CRTS_MASK_GRILLE_LITE - Brighter (subtractive channels) -// CRTS_MASK_NONE - No mask -// CRTS_MASK_SHADOW - Horizontally stretched shadow mask -//-------------------------------------------------------------- -// CRTS_TONE - Normalize mid-level and process color -// CRTS_CONTRAST - Process color - enable contrast control -// CRTS_SATURATION - Process color - enable saturation control -//-------------------------------------------------------------- -#define CRTS_STATIC -#define CrtsPow -#define CRTS_RESTRICT -//============================================================== -//////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////// - -//============================================================== -// SETUP FOR CRTS -//-------------------------------------------------------------- -//============================================================== -//#define CRTS_DEBUG 1 -#define CRTS_GPU 1 -#define CRTS_GLSL 1 -//-------------------------------------------------------------- -//#define CRTS_2_TAP 1 -//-------------------------------------------------------------- -#define CRTS_TONE 1 -#define CRTS_CONTRAST 0 -#define CRTS_SATURATION 0 -//-------------------------------------------------------------- -#define CRTS_WARP 1 -//-------------------------------------------------------------- -// Try different masks -> moved to runtime parameters -//#define CRTS_MASK_GRILLE 1 -//#define CRTS_MASK_GRILLE_LITE 1 -//#define CRTS_MASK_NONE 1 -//#define CRTS_MASK_SHADOW 1 -//-------------------------------------------------------------- -// Scanline thinness -// 0.50 = fused scanlines -// 0.70 = recommended default -// 1.00 = thinner scanlines (too thin) -#define INPUT_THIN (0.5 + (0.5 * SCANLINE_THINNESS)) -//-------------------------------------------------------------- -// Horizonal scan blur -// -3.0 = pixely -// -2.5 = default -// -2.0 = smooth -// -1.0 = too blurry -#define INPUT_BLUR (-1.0 * SCAN_BLUR) -//-------------------------------------------------------------- -// Shadow mask effect, ranges from, -// 0.25 = large amount of mask (not recommended, too dark) -// 0.50 = recommended default -// 1.00 = no shadow mask -#define INPUT_MASK (1.0 - MASK_INTENSITY) -//-------------------------------------------------------------- -#define INPUT_X sourceSize[0].x -#define INPUT_Y sourceSize[0].y -//-------------------------------------------------------------- -// Setup the function which returns input image color -vec3 CrtsFetch(vec2 uv){ - // For shadertoy, scale to get native texels in the image - uv*=vec2(INPUT_X,INPUT_Y)/sourceSize[0].xy; - // Move towards intersting parts -// uv+=vec2(0.5,0.5); - // Non-shadertoy case would not have the color conversion - return FromSrgb(texture(source[0],uv.xy,-16.0).rgb);} - -//////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////// -//////////////////////////////////////////////////////////////// -//_____________________________/\_______________________________ -//============================================================== -// -// GPU CODE -// -//============================================================== -#ifdef CRTS_GPU -//_____________________________/\_______________________________ -//============================================================== -// PORTABILITY -//============================================================== - #ifdef CRTS_GLSL - #define CrtsF1 float - #define CrtsF2 vec2 - #define CrtsF3 vec3 - #define CrtsF4 vec4 - #define CrtsFractF1 fract - #define CrtsRcpF1(x) (1.0/(x)) - #define CrtsSatF1(x) clamp((x),0.0,1.0) -//-------------------------------------------------------------- - CrtsF1 CrtsMax3F1(CrtsF1 a,CrtsF1 b,CrtsF1 c){ - return max(a,max(b,c));} - #endif -//============================================================== - #ifdef CRTS_HLSL - #define CrtsF1 float - #define CrtsF2 float2 - #define CrtsF3 float3 - #define CrtsF4 float4 - #define CrtsFractF1 frac - #define CrtsRcpF1(x) (1.0/(x)) - #define CrtsSatF1(x) saturate(x) -//-------------------------------------------------------------- - CrtsF1 CrtsMax3F1(CrtsF1 a,CrtsF1 b,CrtsF1 c){ - return max(a,max(b,c));} - #endif -//_____________________________/\_______________________________ -//============================================================== -// TONAL CONTROL CONSTANT GENERATION -//-------------------------------------------------------------- -// This is in here for rapid prototyping -// Please use the CPU code and pass in as constants -//============================================================== - CrtsF4 CrtsTone( - CrtsF1 contrast, - CrtsF1 saturation, - CrtsF1 thin, - CrtsF1 mask){ -//-------------------------------------------------------------- - if(MASK == 0.0) mask=1.0; -//-------------------------------------------------------------- - if(MASK == 1.0){ - // Normal R mask is {1.0,mask,mask} - // LITE R mask is {mask,1.0,1.0} - mask=0.5+mask*0.5; - } -//-------------------------------------------------------------- - CrtsF4 ret; - CrtsF1 midOut=0.18/((1.5-thin)*(0.5*mask+0.5)); - CrtsF1 pMidIn=pow(0.18,contrast); - ret.x=contrast; - ret.y=((-pMidIn)+midOut)/((1.0-pMidIn)*midOut); - ret.z=((-pMidIn)*midOut+pMidIn)/(midOut*(-pMidIn)+midOut); - ret.w=contrast+saturation; - return ret;} -//_____________________________/\_______________________________ -//============================================================== -// MASK -//-------------------------------------------------------------- -// Letting LCD/OLED pixel elements function like CRT phosphors -// So "phosphor" resolution scales with display resolution -//-------------------------------------------------------------- -// Not applying any warp to the mask (want high frequency) -// Real aperture grille has a mask which gets wider on ends -// Not attempting to be "real" but instead look the best -//-------------------------------------------------------------- -// Shadow mask is stretched horizontally -// RRGGBB -// GBBRRG -// RRGGBB -// This tends to look better on LCDs than vertical -// Also 2 pixel width is required to get triad centered -//-------------------------------------------------------------- -// The LITE version of the Aperture Grille is brighter -// Uses {dark,1.0,1.0} for R channel -// Non LITE version uses {1.0,dark,dark} -//-------------------------------------------------------------- -// 'pos' - This is 'fragCoord.xy' -// Pixel {0,0} should be {0.5,0.5} -// Pixel {1,1} should be {1.5,1.5} -//-------------------------------------------------------------- -// 'dark' - Exposure of of masked channel -// 0.0=fully off, 1.0=no effect -//============================================================== - CrtsF3 CrtsMask(CrtsF2 pos,CrtsF1 dark){ - if(MASK == 2.0){ - CrtsF3 m=CrtsF3(dark,dark,dark); - CrtsF1 x=CrtsFractF1(pos.x*(1.0/3.0)); - if(x<(1.0/3.0))m.r=1.0; - else if(x<(2.0/3.0))m.g=1.0; - else m.b=1.0; - return m; - } -//-------------------------------------------------------------- - if(MASK == 1.0){ - CrtsF3 m=CrtsF3(1.0,1.0,1.0); - CrtsF1 x=CrtsFractF1(pos.x*(1.0/3.0)); - if(x<(1.0/3.0))m.r=dark; - else if(x<(2.0/3.0))m.g=dark; - else m.b=dark; - return m; - } -//-------------------------------------------------------------- - if(MASK == 0.0){ - return CrtsF3(1.0,1.0,1.0); - } -//-------------------------------------------------------------- - if(MASK == 3.0){ - pos.x+=pos.y*2.9999; - CrtsF3 m=CrtsF3(dark,dark,dark); - CrtsF1 x=CrtsFractF1(pos.x*(1.0/6.0)); - if(x<(1.0/3.0))m.r=1.0; - else if(x<(2.0/3.0))m.g=1.0; - else m.b=1.0; - return m; - } - } -//_____________________________/\_______________________________ -//============================================================== -// FILTER ENTRY -//-------------------------------------------------------------- -// Input must be linear -// Output color is linear -//-------------------------------------------------------------- -// Must have fetch function setup: CrtsF3 CrtsFetch(CrtsF2 uv) -// - The 'uv' range is {0.0 to 1.0} for input texture -// - Output of this must be linear color -//-------------------------------------------------------------- -// SCANLINE MATH & AUTO-EXPOSURE NOTES -// =================================== -// Each output line has contribution from at most 2 scanlines -// Scanlines are shaped by a windowed cosine function -// This shape blends together well with only 2 lines of overlap -//-------------------------------------------------------------- -// Base scanline intensity is as follows -// which leaves output intensity range from {0 to 1.0} -// -------- -// thin := range {thick 0.5 to thin 1.0} -// off := range {0.0 to <1.0}, -// sub-pixel offset between two scanlines -// -------- -// a0=cos(min(0.5, off *thin)*2pi)*0.5+0.5; -// a1=cos(min(0.5,(1.0-off)*thin)*2pi)*0.5+0.5; -//-------------------------------------------------------------- -// This leads to a image darkening factor of roughly: -// {(1.5-thin)/1.0} -// This is further reduced by the mask: -// {1.0/2.0+mask*1.0/2.0} -// Reciprocal of combined effect is used for auto-exposure -// to scale up the mid-level in the tonemapper -//============================================================== - CrtsF3 CrtsFilter( -//-------------------------------------------------------------- - // SV_POSITION, fragCoord.xy - CrtsF2 ipos, -//-------------------------------------------------------------- - // inputSize / outputSize (in pixels) - CrtsF2 inputSizeDivOutputSize, -//-------------------------------------------------------------- - // 0.5 * inputSize (in pixels) - CrtsF2 halfInputSize, -//-------------------------------------------------------------- - // 1.0 / inputSize (in pixels) - CrtsF2 rcpInputSize, -//-------------------------------------------------------------- - // 1.0 / outputSize (in pixels) - CrtsF2 rcpOutputSize, -//-------------------------------------------------------------- - // 2.0 / outputSize (in pixels) - CrtsF2 twoDivOutputSize, -//-------------------------------------------------------------- - // inputSize.y - CrtsF1 inputHeight, -//-------------------------------------------------------------- - // Warp scanlines but not phosphor mask - // 0.0 = no warp - // 1.0/64.0 = light warping - // 1.0/32.0 = more warping - // Want x and y warping to be different (based on aspect) - CrtsF2 warp, -//-------------------------------------------------------------- - // Scanline thinness - // 0.50 = fused scanlines - // 0.70 = recommended default - // 1.00 = thinner scanlines (too thin) - // Shared with CrtsTone() function - CrtsF1 thin, -//-------------------------------------------------------------- - // Horizonal scan blur - // -3.0 = pixely - // -2.5 = default - // -2.0 = smooth - // -1.0 = too blurry - CrtsF1 blur, -//-------------------------------------------------------------- - // Shadow mask effect, ranges from, - // 0.25 = large amount of mask (not recommended, too dark) - // 0.50 = recommended default - // 1.00 = no shadow mask - // Shared with CrtsTone() function - CrtsF1 mask, -//-------------------------------------------------------------- - // Tonal curve parameters generated by CrtsTone() - CrtsF4 tone -//-------------------------------------------------------------- - ){ -//-------------------------------------------------------------- - #ifdef CRTS_DEBUG - CrtsF2 uv=ipos*rcpOutputSize; - // Show second half processed, and first half un-processed - if(uv.x<0.5){ - // Force nearest to get squares - uv*=1.0/rcpInputSize; - uv=floor(uv)+CrtsF2(0.5,0.5); - uv*=rcpInputSize; - CrtsF3 color=CrtsFetch(uv); - return color;} - #endif -//-------------------------------------------------------------- - // Optional apply warp - CrtsF2 pos; - #ifdef CRTS_WARP - // Convert to {-1 to 1} range - pos=ipos*twoDivOutputSize-CrtsF2(1.0,1.0); - // Distort pushes image outside {-1 to 1} range - pos*=CrtsF2( - 1.0+(pos.y*pos.y)*warp.x, - 1.0+(pos.x*pos.x)*warp.y); - // TODO: Vignette needs optimization - CrtsF1 vin=(1.0-( - (1.0-CrtsSatF1(pos.x*pos.x))*(1.0-CrtsSatF1(pos.y*pos.y)))) * (0.998 + (0.001 * CORNER)); - vin=CrtsSatF1((-vin)*inputHeight+inputHeight); - // Leave in {0 to inputSize} - pos=pos*halfInputSize+halfInputSize; - #else - pos=ipos*inputSizeDivOutputSize; - #endif -//-------------------------------------------------------------- - // Snap to center of first scanline - CrtsF1 y0=floor(pos.y-0.5)+0.5; - #ifdef CRTS_2_TAP - // Using Inigo's "Improved Texture Interpolation" - // http://iquilezles.org/www/articles/texture/texture.htm - pos.x+=0.5; - CrtsF1 xi=floor(pos.x); - CrtsF1 xf=pos.x-xi; - xf=xf*xf*xf*(xf*(xf*6.0-15.0)+10.0); - CrtsF1 x0=xi+xf-0.5; - CrtsF2 p=CrtsF2(x0*rcpInputSize.x,y0*rcpInputSize.y); - // Coordinate adjusted bilinear fetch from 2 nearest scanlines - CrtsF3 colA=CrtsFetch(p); - p.y+=rcpInputSize.y; - CrtsF3 colB=CrtsFetch(p); - #else - // Snap to center of one of four pixels - CrtsF1 x0=floor(pos.x-1.5)+0.5; - // Inital UV position - CrtsF2 p=CrtsF2(x0*rcpInputSize.x,y0*rcpInputSize.y); - // Fetch 4 nearest texels from 2 nearest scanlines - CrtsF3 colA0=CrtsFetch(p); - p.x+=rcpInputSize.x; - CrtsF3 colA1=CrtsFetch(p); - p.x+=rcpInputSize.x; - CrtsF3 colA2=CrtsFetch(p); - p.x+=rcpInputSize.x; - CrtsF3 colA3=CrtsFetch(p); - p.y+=rcpInputSize.y; - CrtsF3 colB3=CrtsFetch(p); - p.x-=rcpInputSize.x; - CrtsF3 colB2=CrtsFetch(p); - p.x-=rcpInputSize.x; - CrtsF3 colB1=CrtsFetch(p); - p.x-=rcpInputSize.x; - CrtsF3 colB0=CrtsFetch(p); - #endif -//-------------------------------------------------------------- - // Vertical filter - // Scanline intensity is using sine wave - // Easy filter window and integral used later in exposure - CrtsF1 off=pos.y-y0; - CrtsF1 pi2=6.28318530717958; - CrtsF1 hlf=0.5; - CrtsF1 scanA=cos(min(0.5, off *thin )*pi2)*hlf+hlf; - CrtsF1 scanB=cos(min(0.5,(-off)*thin+thin)*pi2)*hlf+hlf; -//-------------------------------------------------------------- - #ifdef CRTS_2_TAP - #ifdef CRTS_WARP - // Get rid of wrong pixels on edge - scanA*=vin; - scanB*=vin; - #endif - // Apply vertical filter - CrtsF3 color=(colA*scanA)+(colB*scanB); - #else - // Horizontal kernel is simple gaussian filter - CrtsF1 off0=pos.x-x0; - CrtsF1 off1=off0-1.0; - CrtsF1 off2=off0-2.0; - CrtsF1 off3=off0-3.0; - CrtsF1 pix0=exp2(blur*off0*off0); - CrtsF1 pix1=exp2(blur*off1*off1); - CrtsF1 pix2=exp2(blur*off2*off2); - CrtsF1 pix3=exp2(blur*off3*off3); - CrtsF1 pixT=CrtsRcpF1(pix0+pix1+pix2+pix3); - #ifdef CRTS_WARP - // Get rid of wrong pixels on edge - pixT*=vin; - #endif - scanA*=pixT; - scanB*=pixT; - // Apply horizontal and vertical filters - CrtsF3 color= - (colA0*pix0+colA1*pix1+colA2*pix2+colA3*pix3)*scanA + - (colB0*pix0+colB1*pix1+colB2*pix2+colB3*pix3)*scanB; - #endif -//-------------------------------------------------------------- - // Apply phosphor mask - color*=CrtsMask(ipos,mask); -//-------------------------------------------------------------- - // Optional color processing - #ifdef CRTS_TONE - // Tonal control, start by protecting from /0 - CrtsF1 peak=max(1.0/(256.0*65536.0), - CrtsMax3F1(color.r,color.g,color.b)); - // Compute the ratios of {R,G,B} - CrtsF3 ratio=color*CrtsRcpF1(peak); - // Apply tonal curve to peak value - #ifdef CRTS_CONTRAST - peak=pow(peak,tone.x); - #endif - peak=peak*CrtsRcpF1(peak*tone.y+tone.z); - // Apply saturation - #ifdef CRTS_SATURATION - ratio=pow(ratio,CrtsF3(tone.w,tone.w,tone.w)); - #endif - // Reconstruct color - return ratio*peak; - #else - return color; - #endif -//-------------------------------------------------------------- - } -#endif - -void main() { - vec2 warp_factor; - warp_factor.x = CURVATURE; - warp_factor.y = (3.0 / 4.0) * warp_factor.x; // assume 4:3 aspect - warp_factor.x *= (1.0 - TRINITRON_CURVE); - FragColor.rgb = CrtsFilter(vTexCoord.xy * outputSize.xy, - sourceSize[0].xy * outputSize.zw, - sourceSize[0].xy * vec2(0.5,0.5), - sourceSize[0].zw, - outputSize.zw, - 2.0 * outputSize.zw, - sourceSize[0].y, - warp_factor, - INPUT_THIN, - INPUT_BLUR, - INPUT_MASK, - CrtsTone(1.0,0.0,INPUT_THIN,INPUT_MASK)); - - // Shadertoy outputs non-linear color - FragColor.rgb = ToSrgb(FragColor.rgb); -} \ No newline at end of file diff --git a/ares/Shaders/CRT-Lottes2.shader/lottes.vs b/ares/Shaders/CRT-Lottes2.shader/lottes.vs deleted file mode 100644 index 2b819b46e..000000000 --- a/ares/Shaders/CRT-Lottes2.shader/lottes.vs +++ /dev/null @@ -1,16 +0,0 @@ -#version 150 - -in vec4 position; -in vec2 texCoord; - -out Vertex { - vec2 vTexCoord; -}; - -uniform vec4 targetSize; -uniform vec4 sourceSize[]; - -void main() { - gl_Position = position; - vTexCoord = texCoord; -} \ No newline at end of file diff --git a/ares/Shaders/CRT-Lottes2.shader/manifest.bml b/ares/Shaders/CRT-Lottes2.shader/manifest.bml deleted file mode 100644 index 2808ea2da..000000000 --- a/ares/Shaders/CRT-Lottes2.shader/manifest.bml +++ /dev/null @@ -1,12 +0,0 @@ -input - filter: nearest - -program - filter: nearest - vertex: lottes.vs - fragment: lottes.fs - -output - height: 0 - width: 0 - filter: linear \ No newline at end of file diff --git a/ares/Shaders/Curvature.shader/curvature.fs b/ares/Shaders/Curvature.shader/curvature.fs deleted file mode 100644 index d6db5b838..000000000 --- a/ares/Shaders/Curvature.shader/curvature.fs +++ /dev/null @@ -1,21 +0,0 @@ -#version 150 -#define distortion 0.2 - -uniform sampler2D source[]; -uniform vec4 sourceSize[]; - -in Vertex { - vec2 texCoord; -}; - -out vec4 fragColor; - -vec2 radialDistortion(vec2 coord) { - vec2 cc = coord - vec2(0.5); - float dist = dot(cc, cc) * distortion; - return coord + cc * (1.0 - dist) * dist; -} - -void main() { - fragColor = texture(source[0], radialDistortion(texCoord)); -} diff --git a/ares/Shaders/Curvature.shader/manifest.bml b/ares/Shaders/Curvature.shader/manifest.bml deleted file mode 100644 index 7bec47091..000000000 --- a/ares/Shaders/Curvature.shader/manifest.bml +++ /dev/null @@ -1,4 +0,0 @@ -program - filter: linear - wrap: border - fragment: curvature.fs diff --git a/ares/Shaders/Edge Detection.shader/edge-detection.fs b/ares/Shaders/Edge Detection.shader/edge-detection.fs deleted file mode 100644 index bf00fc254..000000000 --- a/ares/Shaders/Edge Detection.shader/edge-detection.fs +++ /dev/null @@ -1,25 +0,0 @@ -#version 150 - -uniform sampler2D source[]; -uniform vec4 sourceSize[]; - -in Vertex { - vec2 texCoord; -}; - -out vec4 fragColor; - -vec3 grayscale(vec3 color) { - return vec3(dot(color, vec3(0.3, 0.59, 0.11))); -} - -void main() { - vec2 offset = fract(texCoord * sourceSize[0].xy) - 0.5; - offset /= sourceSize[0].xy; - - vec3 cx = texture(source[0], texCoord - offset).xyz; - vec3 cy = texture(source[0], texCoord).xyz; - vec3 cz = vec3(5.0 * grayscale(abs(cx - cy))); - - fragColor = vec4(clamp(cz, 0.0, 1.0), 1.0); -} diff --git a/ares/Shaders/Edge Detection.shader/manifest.bml b/ares/Shaders/Edge Detection.shader/manifest.bml deleted file mode 100644 index 166db6dea..000000000 --- a/ares/Shaders/Edge Detection.shader/manifest.bml +++ /dev/null @@ -1,4 +0,0 @@ -program - filter: linear - wrap: edge - fragment: edge-detection.fs diff --git a/ares/Shaders/GritsScanlines.shader/Fake-Self-Illumination.png b/ares/Shaders/GritsScanlines.shader/Fake-Self-Illumination.png deleted file mode 100644 index a686e10fe46eb5d519a08550b1e9f1848496f977..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2589 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 diff --git a/ares/Shaders/GritsScanlines.shader/GritsScanlines.fs b/ares/Shaders/GritsScanlines.shader/GritsScanlines.fs deleted file mode 100644 index 67ee615db..000000000 --- a/ares/Shaders/GritsScanlines.shader/GritsScanlines.fs +++ /dev/null @@ -1,99 +0,0 @@ -#version 150 - -// GritsScanlines by torridgristle -// license: public domain (https://forums.libretro.com/t/lightweight-lut-based-scanline-glow-concept-prototype-glsl/18336/7) - -/* Runtime parameters -#in LuminanceDawnbringer -#in LuminanceLUT -#in ScanlinesOpacity -#in GammaCorrection -#in TrinitronColors -*/ - -// static parameters -//#define LuminanceDawnbringer -#define LuminanceLUT -//#define TrinitronColors -#define ScanlinesOpacity 0.9 -//#define GammaCorrection 1.2 - -uniform sampler2D source[]; -uniform vec4 sourceSize[]; -uniform vec4 targetSize; -uniform sampler2D pixmap[]; - -in Vertex { - vec2 texCoord; -}; - -out vec4 fragColor; - -#ifdef LuminanceLUT -#define LUT_SizeLum 16.0 - - // Code taken from RetroArch's LUT shader -float luminancelut(vec4 org) -{ - vec4 imgColorLum = org; - float redLum = ( imgColorLum.r * (LUT_SizeLum - 1.0) + 0.4999 ) / (LUT_SizeLum * LUT_SizeLum); - float greenLum = ( imgColorLum.g * (LUT_SizeLum - 1.0) + 0.4999 ) / LUT_SizeLum; - float blue1Lum = (floor( imgColorLum.b * (LUT_SizeLum - 1.0) ) / LUT_SizeLum) + redLum; - float blue2Lum = (ceil( imgColorLum.b * (LUT_SizeLum - 1.0) ) / LUT_SizeLum) + redLum; - float mixerLum = clamp(max((imgColorLum.b - blue1Lum) / (blue2Lum - blue1Lum), 0.0), 0.0, 32.0); - float color1Lum = texture(pixmap[1], vec2( blue1Lum, greenLum )).x; - float color2Lum = texture(pixmap[1], vec2( blue2Lum, greenLum )).x; - return mix(color1Lum, color2Lum, mixerLum); -} -#endif - -#ifdef TrinitronColors -#define LUT_SizeTrinitron 32.0 - -vec4 TrinitronD50(vec4 org) -{ - vec4 imgColorTrinitron = org; - float redTrinitron = ( imgColorTrinitron.r * (LUT_SizeTrinitron - 1.0) + 0.4999 ) / (LUT_SizeTrinitron * LUT_SizeTrinitron); - float greenTrinitron = ( imgColorTrinitron.g * (LUT_SizeTrinitron - 1.0) + 0.4999 ) / LUT_SizeTrinitron; - float blue1Trinitron = (floor( imgColorTrinitron.b * (LUT_SizeTrinitron - 1.0) ) / LUT_SizeTrinitron) + redTrinitron; - float blue2Trinitron = (ceil( imgColorTrinitron.b * (LUT_SizeTrinitron - 1.0) ) / LUT_SizeTrinitron) + redTrinitron; - float mixerTrinitron = clamp(max((imgColorTrinitron.b - blue1Trinitron) / (blue2Trinitron - blue1Trinitron), 0.0), 0.0, 32.0); - vec4 color1Trinitron = texture(pixmap[2], vec2( blue1Trinitron, greenTrinitron )); - vec4 color2Trinitron = texture(pixmap[2], vec2( blue2Trinitron, greenTrinitron )); - vec4 fragColorTrinitron = mix(color1Trinitron, color2Trinitron, mixerTrinitron); - return vec4(pow(fragColorTrinitron.rgb,vec3(GammaCorrection,GammaCorrection,GammaCorrection)),1.0); -} -#endif - -void main() { - -//Source Image - vec4 org = texture(source[0], texCoord); - -#ifdef LuminanceLUT -// Use a 3DLUT instead of an equation so that it can use any arbitrary mess you can come up with. - float luminance = luminancelut(org); -#elif defined LuminanceDawnbringer -// Dawnbringer's brightness equation from Dawnbringer's Toolbox scripts for Grafx2 - float luminance = sqrt(org.r*org.r*0.0676 + org.g*org.g*0.3025 + org.b*org.b*0.0361) * 1.5690256395005606; -#else -// Plain, standard, fine; slightly faster - float luminance = ((0.299*org.r) + (0.587*org.g) + (0.114*org.b)); -#endif - -// Don't let it exceed 1.0 - luminance = clamp(luminance, 0.0, 1.0); - -// Scanline Mapping, based on the Phosphor LUT shader's method of tiling a texture over the screen - vec2 LUTeffectiveCoord = vec2(luminance,fract((texCoord.y*targetSize.y)/4.0)); - -// Scanline Layer - vec4 screen = texture(pixmap[0], LUTeffectiveCoord); - -// Output multiplying the scanlines into the original image, with control over opacity -#ifdef TrinitronColors - org = TrinitronD50(org); -#endif - fragColor = ((screen*ScanlinesOpacity)+(1.0 - ScanlinesOpacity)) * (org); -} - diff --git a/ares/Shaders/GritsScanlines.shader/Scanline-LUT-4px.png b/ares/Shaders/GritsScanlines.shader/Scanline-LUT-4px.png deleted file mode 100644 index c9cd03ef7310e371e5566095688b739f9b4698ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2831 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 diff --git a/ares/Shaders/GritsScanlines.shader/Trinitron D50 Absolute Colorimetric - LUT.png b/ares/Shaders/GritsScanlines.shader/Trinitron D50 Absolute Colorimetric - LUT.png deleted file mode 100644 index 87731e8c6007df49eee6328785a02db762fa3c19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72062 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

D%{`VDZ|m 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 400.0) y = sourceSize[0].y * texCoord.y + phase; - else - y = 2.00001 * sourceSize[0].y * texCoord.y; - - if (mod(y, 2.0) > 0.99999) fragColor = res; - else - fragColor = vec4(percent) * res; -} diff --git a/ares/Shaders/Interlacing.shader/manifest.bml b/ares/Shaders/Interlacing.shader/manifest.bml deleted file mode 100644 index 3d985499f..000000000 --- a/ares/Shaders/Interlacing.shader/manifest.bml +++ /dev/null @@ -1,6 +0,0 @@ -input - filter: nearest - -program - fragment: interlace.fs - modulo: 2 \ No newline at end of file diff --git a/ares/Shaders/PAL-r57shell.shader/manifest.bml b/ares/Shaders/PAL-r57shell.shader/manifest.bml deleted file mode 100644 index d512fa63d..000000000 --- a/ares/Shaders/PAL-r57shell.shader/manifest.bml +++ /dev/null @@ -1,11 +0,0 @@ -input - filter: nearest - -program - filter: nearest - height: 100% - fragment: pal-r57shell.fs - -output - height: 0 - filter: linear \ No newline at end of file diff --git a/ares/Shaders/PAL-r57shell.shader/pal-r57shell.fs b/ares/Shaders/PAL-r57shell.shader/pal-r57shell.fs deleted file mode 100644 index bdb058f6b..000000000 --- a/ares/Shaders/PAL-r57shell.shader/pal-r57shell.fs +++ /dev/null @@ -1,368 +0,0 @@ -#version 150 - -// NES PAL composite signal simulation for RetroArch -// shader by r57shell -// thanks to feos & HardWareMan & NewRisingSun - -// also TV subpixels and scanlines - -// LICENSE: PUBLIC DOMAIN - -// NOTE: for nice TV subpixels and scanlines I recommend to -// disable this features here and apply CRT-specialized shader. - -// Quality considerations - -// there are three main options: -// USE_RAW (R), USE_DELAY_LINE (D), USE_COLORIMETRY (C) -// here is table of quality in decreasing order: -// RDC, RD, RC, DC, D, C - -// compatibility macros -#define float2 vec2 -#define float3 vec3 -#define float4 vec4 -#define frac(c) fract(c) -#define saturate(c) clamp(c, 0.0, 1.0) -#define fmod(x,y) mod(x,y) -#define mul(x,y) (y*x) -#define float2x2 mat2 -#define float3x3 mat3 -#define float4x4 mat4 -#define bool2 bvec2 -#define bool3 bvec3 -#define bool4 bvec4 -#define static - -uniform sampler2D source[]; -uniform vec4 sourceSize[]; -uniform vec4 outputSize; -uniform int phase; - -in Vertex { - vec2 texCoord; -}; - -out vec4 fragColor; - -// TWEAKS start - -// use delay line technique -// without delay line technique, color would interleave -// to avoid this, set HueRotation to zero. -#define USE_DELAY_LINE - -// use this if you need to swap even/odd V sign. -// sign of V changes each scanline -// so if some scanline is positive, then next is negative -// and if you want to match picture -// to actual running PAL NES on TV -// you may want to have this option, to change signs -// if they don't match -//#define SWAP_VSIGN - -// rough simulation of scanlines -// better if you use additional shader instead -// if you still use it, make sure that SizeY -// is at least twice lower than output height -#define USE_SCANLINES // FIXME: scanlines are broken and I'm too lazy to fix it right now - -// to change gamma of virtual TV from 2.2 to something else -#define USE_GAMMA - -// use sampled version. it's much more slower version of shader. -// because it is computing x4 more values. NOT RECOMMENDED. -//#define USE_SAMPLED - -#ifndef PARAMETER_UNIFORM -// NTSC standard gamma = 2.2 -// PAL standard gamma = 2.8 -// according to many sources, very unlikely gamma of TV is 2.8 -// most likely gamma of PAL TV is in range 2.4-2.5 -const float Gamma_static = 2.8; // gamma of virtual TV - -const float Brightness_static = 0.25; -const float Contrast_static = 1.1; -const float Saturation_static = 1.1; - -const int - Ywidth_static = 12, - Uwidth_static = 23, - Vwidth_static = 23; - -#define Brightness Brightness_static -#define Gamma Gamma_static - -#define Ywidth Ywidth_static -#define Uwidth Uwidth_static -#define Vwidth Vwidth_static - -#define SCANLINE_MUL (sw.x*dark_scanline+sw.y) - -int Mwidth = int(max(float(Ywidth), max(float(Uwidth), float(Vwidth)))); - -// correct one is -2.5 -// works only with USE_RAW -const float HueShift = -2.5; - -// rotation of hue due to luma level. -const float HueRotation = 2.; - -// touch this only if you know what you doing -const float Phase_Y = 2.; // fmod(341*10,12) -const float Phase_One = 0.; // alternating phases. -const float Phase_Two = 8.; - -// screen size, scanlines = y*2; y one field, and y other field. -const int SizeX = 256; -const int SizeY = 240; - -// count of pixels of virtual TV. -// value close to 1000 produce small artifacts -const int TV_Pixels = 400; - -const float dark_scanline = 0.5; // half -#endif - -// this is using following matrixes. -// it provides more scientific approach -// by conversion into linear XYZ space -// and back to sRGB. -// it's using Gamma setting too. -// define USE_GAMMA is not required. -#define USE_COLORIMETRY - -const float3x3 RGB_to_XYZ = -mat3( - 0.4306190, 0.3415419, 0.1783091, - 0.2220379, 0.7066384, 0.0713236, - 0.0201853, 0.1295504, 0.9390944 -); - -const float3x3 XYZ_to_sRGB = -mat3( - 3.2406, -1.5372, -0.4986, - -0.9689, 1.8758, 0.0415, - 0.0557, -0.2040, 1.0570 -); - -// TWEAKS end - -const float YUV_u = 0.492; -const float YUV_v = 0.877; - -const mat3 RGB_to_YUV = -mat3( - float3( 0.299, 0.587, 0.114), //Y - float3(-0.299,-0.587, 0.886)*YUV_u, //B-Y - float3( 0.701,-0.587,-0.114)*YUV_v //R-Y -); - -#ifdef USE_DELAY_LINE -const float comb_line = 1.; -#else -const float comb_line = 2.; -#endif - -static const float Voltage_0 = 0.518; -static const float Voltage_1 = 1.962; -static const float DeltaV = (Voltage_1-Voltage_0); - -float RGB_y = Contrast_static/Ywidth_static/DeltaV; -float RGB_u = comb_line*Contrast_static*Saturation_static/YUV_u/Uwidth_static/DeltaV; -float RGB_v = comb_line*Contrast_static*Saturation_static/YUV_v/Vwidth_static/DeltaV; - -mat3 YUV_to_RGB = -mat3( - float3(1., 1., 1.)*RGB_y, - float3(0., -0.114/0.587, 1.)*RGB_u, - float3(1., -0.299/0.587, 0.)*RGB_v -); - -const float pi = 3.1415926535897932384626433832795; - -float sinn(float x) -{ - return sin(/*fmod(x,24)*/x*(pi*2./24.)); -} - -float coss(float x) -{ - return cos(/*fmod(x,24)*/x*(pi*2./24.)); -} - -float3 monitor(sampler2D tex, float2 p) -{ - float2 size = sourceSize[0].xy;//float2(SizeX,SizeY); - // align vertical coord to center of texel - float2 uv = float2( - - p.x, - - (floor(p.y*sourceSize[0].y)+0.5)/sourceSize[0].y); -#ifdef USE_DELAY_LINE - float2 sh = (sourceSize[0].xy/sourceSize[0].xy/size)*float2(14./10.,-1.0); -#endif - float2 pc = uv*sourceSize[0].xy/sourceSize[0].xy*size*float2(10.,1.); - float alpha = dot(floor(float2(pc.x,pc.y)),float2(2.,Phase_Y*2.)); - alpha += Phase_One*2.; - - // 1/size.x of screen in uv coords = sourceSize[0].x/sourceSize[0].x/size.x; - // then 1/10*size.x of screen: - float ustep = sourceSize[0].x/sourceSize[0].x/size.x/10.; - - float border = sourceSize[0].x/sourceSize[0].x; - float ss = 2.0; -#ifdef SWAP_VSIGN -#define PAL_SWITCH(A) A < 1. -#else -#define PAL_SWITCH(A) A > 1. -#endif - if (PAL_SWITCH(fmod(uv.y*sourceSize[0].y/sourceSize[0].y*size.y,2.0))) - { - // cos(pi-alpha) = -cos(alpha) - // sin(pi-alpha) = sin(alpha) - // pi - alpha - alpha = -alpha+12012.0; - ss = -2.0; - } - - float ysum = 0., usum = 0., vsum = 0.; - for (int i=0; i= 2.0) // Blue - return vec3(pixel - 2.0, 0.0, 3.0 - pixel); - else if (pixel >= 1.0) // Green - return vec3(0.0, 2.0 - pixel, pixel - 1.0); - else // Red - return vec3(1.0 - pixel, pixel, 0.0); - } - - void main() - { - float y = mod(texCoord.y * sourceSize[0].y, 1.0); - float intensity = exp(-0.2 * y); - - vec2 one_x = vec2(1.0 / (3.0 * sourceSize[0].x), 0.0); - - vec3 color = texture(source[0], texCoord.xy - 0.0 * one_x).rgb; - vec3 color_prev = texture(source[0], texCoord.xy - 1.0 * one_x).rgb; - vec3 color_prev_prev = texture(source[0], texCoord.xy - 2.0 * one_x).rgb; - - float pixel_x = 3.0 * texCoord.x * sourceSize[0].x; - - vec3 focus = to_focus(pixel_x - 0.0); - vec3 focus_prev = to_focus(pixel_x - 1.0); - vec3 focus_prev_prev = to_focus(pixel_x - 2.0); - - vec3 result = - 0.8 * color * focus + - 0.6 * color_prev * focus_prev + - 0.3 * color_prev_prev * focus_prev_prev; - - result = 2.3 * pow(result, vec3(1.4)); - - fragColor = vec4(intensity * result, 1.0); - } \ No newline at end of file diff --git a/ares/Shaders/Scanline.shader/manifest.bml b/ares/Shaders/Scanline.shader/manifest.bml deleted file mode 100644 index a5db6351b..000000000 --- a/ares/Shaders/Scanline.shader/manifest.bml +++ /dev/null @@ -1,4 +0,0 @@ -program - filter: linear - wrap: border - fragment: scanline.fs diff --git a/ares/Shaders/Scanline.shader/scanline.fs b/ares/Shaders/Scanline.shader/scanline.fs deleted file mode 100644 index 42a9603a5..000000000 --- a/ares/Shaders/Scanline.shader/scanline.fs +++ /dev/null @@ -1,20 +0,0 @@ -#version 150 - -uniform sampler2D source[]; - -in Vertex { - vec2 texCoord; -}; - -out vec4 fragColor; - -void main() { - vec4 rgba = texture(source[0], texCoord); - vec4 intensity; - if(fract(gl_FragCoord.y * (0.5 * 4.0 / 3.0)) > 0.5) { - intensity = vec4(0); - } else { - intensity = smoothstep(0.2, 0.8, rgba) + normalize(rgba); - } - fragColor = intensity * -0.25 + rgba * 1.1; -} diff --git a/ares/Shaders/Sharp-Bilinear.shader/manifest.bml b/ares/Shaders/Sharp-Bilinear.shader/manifest.bml deleted file mode 100644 index f04d6106f..000000000 --- a/ares/Shaders/Sharp-Bilinear.shader/manifest.bml +++ /dev/null @@ -1,8 +0,0 @@ -input - filter: linear - -program - fragment: sharp-bilinear.fs - -output - filter: linear \ No newline at end of file diff --git a/ares/Shaders/Sharp-Bilinear.shader/sharp-bilinear.fs b/ares/Shaders/Sharp-Bilinear.shader/sharp-bilinear.fs deleted file mode 100644 index 4d19fa38c..000000000 --- a/ares/Shaders/Sharp-Bilinear.shader/sharp-bilinear.fs +++ /dev/null @@ -1,33 +0,0 @@ -#version 150 - -// Sharp Bilinear -// Author: Themaister -// License: Public Domain - -uniform sampler2D source[]; -uniform vec4 sourceSize[]; -uniform vec4 targetSize; - -in Vertex { - vec2 texCoord; -}; - -out vec4 fragColor; - -void main() { -float prescale = floor(targetSize.y / sourceSize[0].y); - vec2 texel = texCoord.xy * sourceSize[0].xy; - vec2 texel_floored = floor(texel); - vec2 s = fract(texel); - float region_range = 0.5 - 0.5 / prescale; - - // Figure out where in the texel to sample to get correct pre-scaled bilinear. - // Uses the hardware bilinear interpolator to avoid having to sample 4 times manually. - - vec2 center_dist = s - 0.5; - vec2 f = (center_dist - clamp(center_dist, -region_range, region_range)) * prescale + 0.5; - - vec2 mod_texel = texel_floored + f; - - fragColor = texture(source[0], mod_texel / sourceSize[0].xy);; -} \ No newline at end of file diff --git a/desktop-ui/GNUmakefile b/desktop-ui/GNUmakefile index 1497dcc62..adf19559b 100644 --- a/desktop-ui/GNUmakefile +++ b/desktop-ui/GNUmakefile @@ -6,7 +6,8 @@ vulkan := true sdl2 := true local := true lto := true -flags += -I. -I.. -I../ares -I../thirdparty -DMIA_LIBRARY +librashader := true +flags += -I. -I.. -I../ares -I../thirdparty -I../thirdparty/librashader/include -DMIA_LIBRARY nall.path := ../nall include $(nall.path)/GNUmakefile @@ -36,6 +37,16 @@ tzxfile.path := $(thirdparty.path)/TZXFile ymfm.path := $(thirdparty.path)/ymfm include $(thirdparty.path)/GNUmakefile +ifeq ($(platform),macos) + librashader.path := $(thirdparty.path)/librashader/target/optimized/librashader.dylib +else ifeq ($(platform), windows) + ifneq ($(wildcard $(thirdparty.path)/librashader/target/aarch64-pc-windows-msvc/optimized/librashader.dll),) + librashader.path := $(thirdparty.path)/librashader/target/aarch64-pc-windows-msvc/optimized/librashader.dll + else + librashader.path := $(thirdparty.path)/librashader/target/optimized/librashader.dll + endif +endif + ruby.path := ../ruby include $(ruby.path)/GNUmakefile @@ -79,7 +90,23 @@ all.options := $(options) $(libco.options) $(sljit.options) $(libchdr.options) $ $(all.objects): | $(object.path) -all: $(all.objects) | $(output.path) +all: output + ifeq ($(platform),macos) + ifeq ($(librashader), true) + ifeq ($(wildcard $(librashader.path)),) + $(error Tried to compile ares for macOS with librashader enabled, but no librashader library was found. Compile it with thirdparty/librashader/build-librashader.sh, or disable librashader by compiling ares with librashader=false) + endif + ares.dylibs += $(librashader.path) + endif + else ifeq ($(platform), windows) + ifeq ($(librashader), true) + ifeq ($(wildcard $(librashader.path)),) + $(error Tried to compile ares for Windows with librashader enabled, but no librashader library was found. Compile it with thirdparty/librashader/build-librashader.sh, or disable librashader by compiling ares with librashader=false) + endif + endif + endif + +output: $(all.objects) | $(output.path) $(info Linking $(output.path)/$(name)$(extension) ...) +@$(compiler) $(call exe,$(output.path)/$(name)$(extension)) $(all.objects) $(all.options) $(call copy,../LICENSE,$(output.path)/LICENSE.txt) @@ -98,15 +125,20 @@ ifneq ($(ares.dylibs),) endif mv $(output.path)/$(name) $(output.path)/$(name).app/Contents/MacOS/$(name) cp resource/$(name).plist $(output.path)/$(name).app/Contents/Info.plist - cp -R $(ares.path)/Shaders $(output.path)/$(name).app/Contents/Resources/ - cp -R $(mia.path)/Database $(output.path)/$(name).app/Contents/Resources/ + $(call mkdir,$(output.path)/$(name).app/Contents/Resources/Shaders/) + $(call mkdir,$(output.path)/$(name).app/Contents/Resources/Database/) + $(call rcopy,$(thirdparty.path)/slang-shaders/*,$(output.path)/$(name).app/Contents/Resources/Shaders/) + $(call rcopy,$(mia.path)/Database/*,$(output.path)/$(name).app/Contents/Resources/Database/) sips -s format icns resource/$(name).png --out $(output.path)/$(name).app/Contents/Resources/$(name).icns codesign --force --deep --options runtime --entitlements resource/$(name).selfsigned.entitlements --sign - $(output.path)/$(name).app else ifeq ($(platform),windows) $(call mkdir,$(output.path)/Shaders/) $(call mkdir,$(output.path)/Database/) - $(call rcopy,$(ares.path)/Shaders/*,$(output.path)/Shaders/) + $(call rcopy,$(thirdparty.path)/slang-shaders/*,$(output.path)/Shaders/) $(call rcopy,$(mia.path)/Database/*,$(output.path)/Database/) +ifeq ($(librashader), true) + $(call copy,$(librashader.path),$(output.path)/) +endif endif verbose: nall.verbose ruby.verbose hiro.verbose all; @@ -133,7 +165,7 @@ else ifneq ($(filter $(platform),linux bsd),) mkdir -p $(prefix)/share/$(name)/Shaders/ mkdir -p $(prefix)/share/$(name)/Database/ cp $(output.path)/$(name) $(prefix)/bin/$(name) - cp -R $(ares.path)/Shaders/* $(prefix)/share/$(name)/Shaders/ + cp -R $(thirdparty.path)/slang-shaders/* $(prefix)/share/$(name)/Shaders/ cp -R $(mia.path)/Database/* $(prefix)/share/$(name)/Database/ cp resource/$(name).desktop $(prefix)/share/applications/$(name).desktop cp resource/$(name).png $(prefix)/share/icons/hicolor/256x256/apps/$(name).png diff --git a/desktop-ui/presentation/presentation.cpp b/desktop-ui/presentation/presentation.cpp index 778f1ce80..3b00cbb2d 100644 --- a/desktop-ui/presentation/presentation.cpp +++ b/desktop-ui/presentation/presentation.cpp @@ -593,10 +593,16 @@ auto Presentation::loadShaders() -> void { auto location = locate("Shaders/"); if(ruby::video.driver() == "OpenGL 3.2") { - for(auto shader : directory::folders(location, "*.shader")) { - if(shaders.objectCount() == 2) videoShaderMenu.append(MenuSeparator()); + auto files = directory::files(location, "*.slangp"); + for(auto dir : directory::folders(location)) { + for(auto file : directory::files({location, "/", dir}, "*.slangp")) { + files.append({dir, file}); + } + } + + for(auto shader : files) { MenuRadioItem item{&videoShaderMenu}; - item.setText(string{shader}.trimRight(".shader/", 1L)).onActivate([=] { + item.setText(string{shader}.trimRight(".slangp", 1L)).onActivate([=] { settings.video.shader = {location, shader}; ruby::video.setShader(settings.video.shader); }); @@ -609,7 +615,7 @@ auto Presentation::loadShaders() -> void { if(!program.startShader.imatch("None") && !program.startShader.imatch("Blur")) { - settings.video.shader = {location, program.startShader, ".shader/"}; + settings.video.shader = {location, program.startShader, ".slangp"}; } else { settings.video.shader = program.startShader; } @@ -631,7 +637,7 @@ auto Presentation::loadShaders() -> void { if(settings.video.shader.imatch("None")) {none.setChecked(); settings.video.shader = "None";} if(settings.video.shader.imatch("Blur")) {blur.setChecked(); settings.video.shader = "Blur";} for(auto item : shaders.objects()) { - string fullPath = {location, item.text(), ".shader/"}; + string fullPath = {location, item.text(), ".slangp"}; if(settings.video.shader.imatch(fullPath)) { item.setChecked(); settings.video.shader = fullPath; diff --git a/nall/intrinsics.hpp b/nall/intrinsics.hpp index 370151d3c..d00b54bc0 100644 --- a/nall/intrinsics.hpp +++ b/nall/intrinsics.hpp @@ -25,7 +25,6 @@ namespace nall { static constexpr bool GCC = 0; static constexpr bool Microsoft = 0; }; - #pragma clang diagnostic error "-Wc++20-extensions" #pragma clang diagnostic error "-Wgnu-case-range" #pragma clang diagnostic error "-Wgnu-statement-expression" #pragma clang diagnostic error "-Wvla" diff --git a/ruby/GNUmakefile b/ruby/GNUmakefile index e98d95666..7f7c899ef 100644 --- a/ruby/GNUmakefile +++ b/ruby/GNUmakefile @@ -30,7 +30,7 @@ ifeq ($(ruby),) endif else ifeq ($(platform),linux) pkg_check = $(if $(shell $(pkg_config) $1 && echo 1),$2) - ruby += video.glx video.glx2 video.xshm + ruby += video.glx video.xshm ruby += $(call pkg_check,xv,video.xvideo) ruby += audio.oss audio.alsa ruby += $(call pkg_check,openal,audio.openal) @@ -66,6 +66,7 @@ else ruby.flags := $(flags.cpp) endif +ruby.flags += -I../thirdparty ruby.flags += $(foreach c,$(subst .,_,$(call strupper,$(ruby))),-D$c) ifeq ($(pkg_config),) # TODO: add SDL2 cflags diff --git a/ruby/video/glx2.cpp b/ruby/video/glx2.cpp deleted file mode 100644 index 4d4ef1da5..000000000 --- a/ruby/video/glx2.cpp +++ /dev/null @@ -1,417 +0,0 @@ -//Xorg/GLX OpenGL 2.0 driver - -//note: this is a fallback driver for use when OpenGL 3.2 is not available. -//see glx.cpp for comments on how this driver operates (they are very similar.) - -#if defined(DISPLAY_XORG) - #include - #include - #ifndef glGetProcAddress - #define glGetProcAddress(name) (*glXGetProcAddress)((const GLubyte*)(name)) - #endif -#elif defined(DISPLAY_QUARTZ) - #include -#elif defined(DISPLAY_WINDOWS) - #include - #include - #ifndef glGetProcAddress - #define glGetProcAddress(name) wglGetProcAddress(name) - #endif -#else - #error "ruby::OpenGL2: unsupported platform" -#endif - -struct VideoGLX2 : VideoDriver { - VideoGLX2& self = *this; - VideoGLX2(Video& super) : VideoDriver(super) { construct(); } - ~VideoGLX2() { destruct(); } - - auto create() -> bool override { - VideoDriver::exclusive = true; - VideoDriver::format = "ARGB24"; - return initialize(); - } - - auto driver() -> string override { return "OpenGL 2.0"; } - auto ready() -> bool override { return _ready; } - - auto hasFullScreen() -> bool override { return true; } - auto hasMonitor() -> bool override { return true; } - auto hasContext() -> bool override { return true; } - auto hasBlocking() -> bool override { return true; } - auto hasFlush() -> bool override { return true; } - auto hasShader() -> bool override { return true; } - - auto hasFormats() -> vector override { - if(_depth == 30) return {"ARGB30", "ARGB24"}; - if(_depth == 24) return {"ARGB24"}; - return {"ARGB24"}; //fallback - } - - auto setFullScreen(bool fullScreen) -> bool override { - return initialize(); - } - - auto setMonitor(string monitor) -> bool override { - return initialize(); - } - - auto setContext(uintptr context) -> bool override { - return initialize(); - } - - auto setBlocking(bool blocking) -> bool override { - acquireContext(); - glXSwapInterval(blocking); - releaseContext(); - return true; - } - - auto setFormat(string format) -> bool override { - if(format == "ARGB24") { - _glFormat = GL_UNSIGNED_INT_8_8_8_8_REV; - return initialize(); - } - - if(format == "ARGB30") { - _glFormat = GL_UNSIGNED_INT_2_10_10_10_REV; - return initialize(); - } - - return false; - } - - auto setShader(string shader) -> bool override { - return true; - } - - auto focused() -> bool override { - return true; - } - - auto clear() -> void override { - acquireContext(); - memory::fill(_glBuffer, _glWidth * _glHeight); - glClearColor(0.0, 0.0, 0.0, 1.0); - glClear(GL_COLOR_BUFFER_BIT); - glFlush(); - if(_isDoubleBuffered) glXSwapBuffers(_display, _glXWindow); - releaseContext(); - } - - auto size(u32& width, u32& height) -> void override { - if(self.fullScreen) { - width = _monitorWidth; - height = _monitorHeight; - } else { - XWindowAttributes parent; - XGetWindowAttributes(_display, _parent, &parent); - width = parent.width; - height = parent.height; - } - } - - auto acquire(u32*& data, u32& pitch, u32 width, u32 height) -> bool override { - if(width != _width || height != _height) resize(width, height); - pitch = _glWidth * sizeof(u32); - return data = _glBuffer; - } - - auto release() -> void override { - } - - auto output(u32 width, u32 height) -> void override { - acquireContext(); - XWindowAttributes window; - XGetWindowAttributes(_display, _window, &window); - - XWindowAttributes parent; - XGetWindowAttributes(_display, _parent, &parent); - - if(window.width != parent.width || window.height != parent.height) { - XResizeWindow(_display, _window, parent.width, parent.height); - } - - u32 viewportX = 0; - u32 viewportY = 0; - u32 viewportWidth = parent.width; - u32 viewportHeight = parent.height; - - if(self.fullScreen) { - viewportX = _monitorX; - viewportY = _monitorY; - viewportWidth = _monitorWidth; - viewportHeight = _monitorHeight; - } - - if(!width) width = viewportWidth; - if(!height) height = viewportHeight; - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, self.shader == "Blur" ? GL_LINEAR : GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, self.shader == "Blur" ? GL_LINEAR : GL_NEAREST); - - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - //vertex coordinates range from (0,0) to (1,1) for the entire desktop (all monitors) - glOrtho(0, 1, 0, 1, -1.0, 1.0); - //set the viewport to the entire desktop (all monitors) - glViewport(0, 0, parent.width, parent.height); - - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - glPixelStorei(GL_UNPACK_ROW_LENGTH, _glWidth); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, _width, _height, GL_BGRA, _glFormat, _glBuffer); - - //normalize texture coordinates and adjust for NPOT textures - f64 w = (f64)_width / (f64)_glWidth; - f64 h = (f64)_height / (f64)_glHeight; - - //size of the active monitor - f64 mw = (f64)viewportWidth / (f64)parent.width; - f64 mh = (f64)viewportHeight / (f64)parent.height; - - //offset of the active monitor - f64 mx = (f64)viewportX / (f64)parent.width; - f64 my = (f64)viewportY / (f64)parent.height; - - //size of the render area - f64 vw = (f64)width / (f64)parent.width; - f64 vh = (f64)height / (f64)parent.height; - - //center the render area within the active monitor - f64 vl = mx + (mw - vw) / 2; - f64 vt = my + (mh - vh) / 2; - f64 vr = vl + vw; - f64 vb = vt + vh; - - //OpenGL places (0,0) at the bottom left; convert our (0,0) at the top left to this form: - vt = 1.0 - vt; - vb = 1.0 - vb; - - glBegin(GL_TRIANGLE_STRIP); - glTexCoord2f(0, 0); glVertex3f(vl, vt, 0); - glTexCoord2f(w, 0); glVertex3f(vr, vt, 0); - glTexCoord2f(0, h); glVertex3f(vl, vb, 0); - glTexCoord2f(w, h); glVertex3f(vr, vb, 0); - glEnd(); - glFlush(); - - if(_isDoubleBuffered) glXSwapBuffers(_display, _glXWindow); - if(self.flush) glFinish(); - releaseContext(); - } - - auto poll() -> void override { - while(XPending(_display)) { - XEvent event; - XNextEvent(_display, &event); - if(event.type == Expose) { - XWindowAttributes attributes; - XGetWindowAttributes(_display, _window, &attributes); - super.doUpdate(attributes.width, attributes.height); - } - } - } - -private: - auto construct() -> void { - _display = XOpenDisplay(nullptr); - _screen = DefaultScreen(_display); - - XWindowAttributes attributes{}; - XGetWindowAttributes(_display, RootWindow(_display, _screen), &attributes); - _depth = attributes.depth; - } - - auto destruct() -> void { - terminate(); - XCloseDisplay(_display); - } - - auto acquireContext() -> void { - if(!_glXContext) return; - while(!glXMakeCurrent(_display, _glXWindow, _glXContext)) spinloop(); - } - - auto releaseContext() -> void { - if(!_glXContext) return; - while(!glXMakeCurrent(_display, 0, nullptr)) spinloop(); - } - - auto initialize() -> bool { - terminate(); - if(!self.fullScreen && !self.context) return false; - - s32 versionMajor = 0, versionMinor = 0; - glXQueryVersion(_display, &versionMajor, &versionMinor); - if(versionMajor < 1 || (versionMajor == 1 && versionMinor < 2)) return false; - - s32 redDepth = self.format == "RGB30" ? 10 : 8; - s32 greenDepth = self.format == "RGB30" ? 10 : 8; - s32 blueDepth = self.format == "RGB30" ? 10 : 8; - - s32 attributeList[] = { - GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, - GLX_RENDER_TYPE, GLX_RGBA_BIT, - GLX_DOUBLEBUFFER, True, - GLX_RED_SIZE, redDepth, - GLX_GREEN_SIZE, greenDepth, - GLX_BLUE_SIZE, blueDepth, - None - }; - - s32 fbCount = 0; - auto fbConfig = glXChooseFBConfig(_display, _screen, attributeList, &fbCount); - if(fbCount == 0) return false; - - auto visual = glXGetVisualFromFBConfig(_display, fbConfig[0]); - - _parent = self.fullScreen ? RootWindow(_display, visual->screen) : (Window)self.context; - XWindowAttributes windowAttributes; - XGetWindowAttributes(_display, _parent, &windowAttributes); - - auto monitor = Video::monitor(self.monitor); - _monitorX = monitor.x; - _monitorY = monitor.y; - _monitorWidth = monitor.width; - _monitorHeight = monitor.height; - - _colormap = XCreateColormap(_display, RootWindow(_display, visual->screen), visual->visual, AllocNone); - XSetWindowAttributes attributes{}; - attributes.border_pixel = 0; - attributes.colormap = _colormap; - attributes.override_redirect = self.fullScreen; - _window = XCreateWindow(_display, _parent, - 0, 0, windowAttributes.width, windowAttributes.height, - 0, visual->depth, InputOutput, visual->visual, - CWBorderPixel | CWColormap | CWOverrideRedirect, &attributes); - XSelectInput(_display, _window, ExposureMask); - XSetWindowBackground(_display, _window, 0); - XMapWindow(_display, _window); - XFlush(_display); - - while(XPending(_display)) { - XEvent event; - XNextEvent(_display, &event); - } - - _glXContext = glXCreateContext(_display, visual, 0, GL_TRUE); - glXMakeCurrent(_display, _glXWindow = _window, _glXContext); - - glXSwapIntervalEXT = (int (*)(Display*, GLXDrawable drawable, int))glGetProcAddress("glXSwapIntervalEXT"); - glXSwapIntervalMESA = (int (*)(int))glGetProcAddress("glXSwapIntervalMESA"); - glXSwapIntervalSGI = (int (*)(int))glGetProcAddress("glXSwapIntervalSGI"); - - glXSwapInterval(self.blocking); - - s32 value = 0; - glXGetConfig(_display, visual, GLX_DOUBLEBUFFER, &value); - _isDoubleBuffered = value; - _isDirect = glXIsDirect(_display, _glXContext); - - glDisable(GL_ALPHA_TEST); - glDisable(GL_BLEND); - glDisable(GL_DEPTH_TEST); - glDisable(GL_POLYGON_SMOOTH); - glDisable(GL_STENCIL_TEST); - - glEnable(GL_DITHER); - glEnable(GL_TEXTURE_2D); - releaseContext(); - - resize(256, 256); - return _ready = true; - } - - auto terminate() -> void { - acquireContext(); - _ready = false; - - if(_glTexture) { - glDeleteTextures(1, &_glTexture); - _glTexture = 0; - } - - if(_glBuffer) { - delete[] _glBuffer; - _glBuffer = nullptr; - } - - _glWidth = 0; - _glHeight = 0; - - if(_glXContext) { - glXDestroyContext(_display, _glXContext); - _glXContext = nullptr; - } - - if(_window) { - XUnmapWindow(_display, _window); - _window = 0; - } - - if(_colormap) { - XFreeColormap(_display, _colormap); - _colormap = 0; - } - } - - auto resize(u32 width, u32 height) -> void { - acquireContext(); - _width = width; - _height = height; - - if(_glTexture == 0) glGenTextures(1, &_glTexture); - _glWidth = max(_glWidth, width); - _glHeight = max(_glHeight, height); - delete[] _glBuffer; - _glBuffer = new u32[_glWidth * _glHeight](); - - glBindTexture(GL_TEXTURE_2D, _glTexture); - glPixelStorei(GL_UNPACK_ROW_LENGTH, _glWidth); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, _glWidth, _glHeight, 0, GL_BGRA, _glFormat, _glBuffer); - releaseContext(); - } - - bool _ready = false; - bool blur = false; - - Display* _display = nullptr; - u32 _monitorX = 0; - u32 _monitorY = 0; - u32 _monitorWidth = 0; - u32 _monitorHeight = 0; - s32 _screen = 0; - u32 _depth = 24; //depth of the default root window - Window _parent = 0; - Window _window = 0; - Colormap _colormap = 0; - GLXContext _glXContext = nullptr; - GLXWindow _glXWindow = 0; - - bool _isDoubleBuffered = false; - bool _isDirect = false; - - u32 _width = 256; - u32 _height = 256; - - GLuint _glTexture = 0; - u32* _glBuffer = nullptr; - u32 _glWidth = 0; - u32 _glHeight = 0; - u32 _glFormat = GL_UNSIGNED_INT_8_8_8_8_REV; - - auto glXSwapInterval(int blocking) -> int { - //note that the ordering is very important! MESA declares SGI, but the SGI function does nothing - if(glXSwapIntervalEXT && glXSwapIntervalEXT(_display, glXGetCurrentDrawable(), blocking)) return 1; - if(glXSwapIntervalMESA && glXSwapIntervalMESA(blocking)) return 1; - if(glXSwapIntervalSGI && glXSwapIntervalSGI(blocking)) return 1; - return 0; - } - - auto (*glXSwapIntervalMESA)(int) -> int = nullptr; - auto (*glXSwapIntervalSGI)(int) -> int = nullptr; - auto (*glXSwapIntervalEXT)(Display *dpy, GLXDrawable drawable, int interval) -> int = nullptr; -}; diff --git a/ruby/video/opengl/bind.hpp b/ruby/video/opengl/bind.hpp index e65bf0d70..6e733e7ca 100644 --- a/ruby/video/opengl/bind.hpp +++ b/ruby/video/opengl/bind.hpp @@ -38,6 +38,8 @@ PFNGLUNIFORMMATRIX4FVPROC glUniformMatrix4fv = nullptr; PFNGLGENFRAMEBUFFERSPROC glGenFramebuffers = nullptr; PFNGLDELETEFRAMEBUFFERSPROC glDeleteFramebuffers = nullptr; PFNGLBINDFRAMEBUFFERPROC glBindFramebuffer = nullptr; +PFNGLBLITFRAMEBUFFERPROC glBlitFramebuffer = nullptr; +PFNGLGENERATEMIPMAPPROC glGenerateMipmap = nullptr; PFNGLFRAMEBUFFERTEXTURE2DPROC glFramebufferTexture2D = nullptr; #endif #if defined(DISPLAY_WINDOWS) @@ -89,6 +91,8 @@ static bool OpenGLBind() { bind(PFNGLGENFRAMEBUFFERSPROC, glGenFramebuffers); bind(PFNGLDELETEFRAMEBUFFERSPROC, glDeleteFramebuffers); bind(PFNGLBINDFRAMEBUFFERPROC, glBindFramebuffer); + bind(PFNGLBLITFRAMEBUFFERPROC, glBlitFramebuffer); + bind(PFNGLGENERATEMIPMAPPROC, glGenerateMipmap); bind(PFNGLFRAMEBUFFERTEXTURE2DPROC, glFramebufferTexture2D); #endif #if defined(DISPLAY_WINDOWS) diff --git a/ruby/video/opengl/main.hpp b/ruby/video/opengl/main.hpp index f63f7f9b2..0c0affb5a 100644 --- a/ruby/video/opengl/main.hpp +++ b/ruby/video/opengl/main.hpp @@ -1,82 +1,41 @@ +#if defined(PLATFORM_MACOS) +#import +#endif auto OpenGL::setShader(const string& pathname) -> void { - for(auto& program : programs) program.release(); - programs.reset(); - settings.reset(); format = inputFormat; - filter = GL_LINEAR; + filter = GL_NEAREST; wrap = GL_CLAMP_TO_BORDER; absoluteWidth = 0, absoluteHeight = 0; - relativeWidth = 0, relativeHeight = 0; - - u32 historySize = 0; - if(pathname == "None") { - filter = GL_NEAREST; - } else if(pathname == "Blur") { - filter = GL_LINEAR; - } else if(directory::exists(pathname)) { - auto document = BML::unserialize(file::read({pathname, "manifest.bml"})); - for(auto node : document["settings"]) { - settings.insert({node.name(), node.text()}); - } + if(_chain != NULL) { + _libra.gl_filter_chain_free(&_chain); + } - for(auto node : document["input"]) { - if(node.name() == "history") historySize = node.natural(); - if(node.name() == "format") format = glrFormat(node.text()); - if(node.name() == "filter") filter = glrFilter(node.text()); - if(node.name() == "wrap") wrap = glrWrap(node.text()); - } + if(_preset != NULL) { + _libra.preset_free(&_preset); + } - for(auto node : document["output"]) { - string text = node.text(); - if(node.name() == "width") { - if(text.endsWith("%")) relativeWidth = toReal(text.trimRight("%", 1L)) / 100.0; - else absoluteWidth = text.natural(); - } - if(node.name() == "height") { - if(text.endsWith("%")) relativeHeight = toReal(text.trimRight("%", 1L)) / 100.0; - else absoluteHeight = text.natural(); - } + if(pathname == "Blur") { + filter = GL_LINEAR; + } else if(file::exists(pathname)) { + if(_libra.preset_create(pathname.data(), &_preset) != NULL) { + print(string{"OpenGL: Failed to load shader: ", pathname, "\n"}); + setShader(""); + return; } - for(auto node : document.find("program")) { - u32 n = programs.size(); - programs(n).bind(this, node, pathname); + if(auto error = _libra.gl_filter_chain_create(&_preset, NULL, &_chain)) { + print(string{"OpenGL: Failed to create filter chain for: ", pathname, "\n"}); + _libra.error_print(error); + setShader(""); + return; } } - - //changing shaders may change input format, which requires the input texture to be recreated - if(texture) { glDeleteTextures(1, &texture); texture = 0; } - glGenTextures(1, &texture); - glBindTexture(GL_TEXTURE_2D, texture); - glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, getFormat(), getType(), buffer); - allocateHistory(historySize); -} - -auto OpenGL::allocateHistory(u32 size) -> void { - for(auto& frame : history) glDeleteTextures(1, &frame.texture); - history.reset(); - while(size--) { - OpenGLTexture frame; - frame.filter = filter; - frame.wrap = wrap; - glGenTextures(1, &frame.texture); - glBindTexture(GL_TEXTURE_2D, frame.texture); - glTexImage2D(GL_TEXTURE_2D, 0, format, frame.width = width, frame.height = height, 0, getFormat(), getType(), buffer); - history.append(frame); - } } auto OpenGL::clear() -> void { - for(auto& p : programs) { - glUseProgram(p.program); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, p.framebuffer); - glClearColor(0, 0, 0, 1); - glClear(GL_COLOR_BUFFER_BIT); - } - glUseProgram(0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); @@ -93,6 +52,7 @@ auto OpenGL::output() -> void { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, getFormat(), getType(), buffer); + glGenerateMipmap(GL_TEXTURE_2D); struct Source { GLuint texture; @@ -102,86 +62,57 @@ auto OpenGL::output() -> void { vector sources; sources.prepend({texture, width, height, filter, wrap}); - for(auto& p : programs) { - u32 targetWidth = p.absoluteWidth ? p.absoluteWidth : outputWidth; - u32 targetHeight = p.absoluteHeight ? p.absoluteHeight : outputHeight; - if(p.relativeWidth) targetWidth = sources[0].width * p.relativeWidth; - if(p.relativeHeight) targetHeight = sources[0].height * p.relativeHeight; - - p.size(targetWidth, targetHeight); - glUseProgram(p.program); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, p.framebuffer); - - glrUniform1i("phase", p.phase); - glrUniform1i("historyLength", history.size()); - glrUniform1i("sourceLength", sources.size()); - glrUniform1i("pixmapLength", p.pixmaps.size()); - glrUniform4f("targetSize", targetWidth, targetHeight, 1.0 / targetWidth, 1.0 / targetHeight); - glrUniform4f("outputSize", outputWidth, outputHeight, 1.0 / outputWidth, 1.0 / outputHeight); - - u32 aid = 0; - for(auto& frame : history) { - glrUniform1i({"history[", aid, "]"}, aid); - glrUniform4f({"historySize[", aid, "]"}, frame.width, frame.height, 1.0 / frame.width, 1.0 / frame.height); - glActiveTexture(GL_TEXTURE0 + (aid++)); - glBindTexture(GL_TEXTURE_2D, frame.texture); - glrParameters(frame.filter, frame.wrap); - } - - u32 bid = 0; - for(auto& source : sources) { - glrUniform1i({"source[", bid, "]"}, aid + bid); - glrUniform4f({"sourceSize[", bid, "]"}, source.width, source.height, 1.0 / source.width, 1.0 / source.height); - glActiveTexture(GL_TEXTURE0 + aid + (bid++)); - glBindTexture(GL_TEXTURE_2D, source.texture); - glrParameters(source.filter, source.wrap); - } - - u32 cid = 0; - for(auto& pixmap : p.pixmaps) { - glrUniform1i({"pixmap[", cid, "]"}, aid + bid + cid); - glrUniform4f({"pixmapSize[", bid, "]"}, pixmap.width, pixmap.height, 1.0 / pixmap.width, 1.0 / pixmap.height); - glActiveTexture(GL_TEXTURE0 + aid + bid + (cid++)); - glBindTexture(GL_TEXTURE_2D, pixmap.texture); - glrParameters(pixmap.filter, pixmap.wrap); - } - - glActiveTexture(GL_TEXTURE0); - glrParameters(sources[0].filter, sources[0].wrap); - p.render(sources[0].width, sources[0].height, 0, 0, targetWidth, targetHeight); - glBindTexture(GL_TEXTURE_2D, p.texture); - - p.phase = (p.phase + 1) % p.modulo; - sources.prepend({p.texture, p.width, p.height, p.filter, p.wrap}); - } - u32 targetWidth = absoluteWidth ? absoluteWidth : outputWidth; u32 targetHeight = absoluteHeight ? absoluteHeight : outputHeight; - if(relativeWidth) targetWidth = sources[0].width * relativeWidth; - if(relativeHeight) targetHeight = sources[0].height * relativeHeight; - glUseProgram(program); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + u32 x = (outputWidth - targetWidth) / 2; + u32 y = (outputHeight - targetHeight) / 2; - glrUniform1i("source[0]", 0); - glrUniform4f("targetSize", targetWidth, targetHeight, 1.0 / targetWidth, 1.0 / targetHeight); - glrUniform4f("outputSize", outputWidth, outputHeight, 1.0 / outputWidth, 1.0 / outputHeight); + if(_chain != NULL) { + // Shader path: our intermediate framebuffer matches the output size + if(!framebuffer || framebufferWidth != outputWidth || framebufferHeight != outputHeight) { + if(framebuffer) { + glDeleteFramebuffers(1, &framebuffer); + framebuffer = 0; + } + if(framebufferTexture) { + glDeleteTextures(1, &framebufferTexture); + framebufferTexture = 0; + } - glrParameters(sources[0].filter, sources[0].wrap); - render(sources[0].width, sources[0].height, outputX, outputY, outputWidth, outputHeight); + framebufferWidth = outputWidth, framebufferHeight = outputHeight; + glGenFramebuffers(1, &framebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + glGenTextures(1, &framebufferTexture); + glBindTexture(GL_TEXTURE_2D, framebufferTexture); + framebufferFormat = GL_RGB; - if(history.size() > 0) { - OpenGLTexture frame = history.takeRight(); + glTexImage2D(GL_TEXTURE_2D, 0, framebufferFormat, framebufferWidth, framebufferHeight, 0, framebufferFormat, + GL_UNSIGNED_BYTE, nullptr); - glBindTexture(GL_TEXTURE_2D, frame.texture); - if(width == frame.width && height == frame.height) { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, getFormat(), getType(), buffer); - } else { - glTexImage2D(GL_TEXTURE_2D, 0, format, frame.width = width, frame.height = height, 0, getFormat(), getType(), buffer); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, framebufferTexture, 0); } + } else { + // Non-shader path: our intermediate framebuffer matches the source size and re-uses the source texture + if(!framebuffer || framebufferWidth != width || framebufferHeight != height) { + if(framebuffer) { + glDeleteFramebuffers(1, &framebuffer); + framebuffer = 0; + } + + if(framebufferTexture) { + glDeleteTextures(1, &framebufferTexture); + framebufferTexture = 0; + } - history.prepend(frame); + framebufferWidth = width, framebufferHeight = height; + glGenFramebuffers(1, &framebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); + } } + + render(sources[0].width, sources[0].height, outputX + x, outputY + y, targetWidth, targetHeight); } auto OpenGL::initialize(const string& shader) -> bool { @@ -193,20 +124,48 @@ auto OpenGL::initialize(const string& shader) -> bool { glDisable(GL_STENCIL_TEST); glEnable(GL_DITHER); - program = glCreateProgram(); - vertex = glrCreateShader(program, GL_VERTEX_SHADER, OpenGLOutputVertexShader); -//geometry = glrCreateShader(program, GL_GEOMETRY_SHADER, OpenGLGeometryShader); - fragment = glrCreateShader(program, GL_FRAGMENT_SHADER, OpenGLFragmentShader); - OpenGLSurface::allocate(); - glrLinkProgram(program); + _libra = librashader_load_instance(); + if(!_libra.instance_loaded) { + print("OpenGL: Failed to load librashader: shaders will be disabled\n"); + } + + if(_libra.gl_init_context(resolveSymbol) != NULL) { + print("OpenGL: Failed to initialize librashader context: shaders will be disabled\n"); + }; setShader(shader); return initialized = true; } +auto OpenGL::resolveSymbol(const char* name) -> const void * { +#if defined(PLATFORM_MACOS) + NSSymbol symbol; + char *symbolName; + symbolName = (char*)malloc(strlen(name) + 2); + strcpy(symbolName + 1, name); + symbolName[0] = '_'; + symbol = NULL; + if(NSIsSymbolNameDefined (symbolName)) symbol = NSLookupAndBindSymbol (symbolName); + free(symbolName); // 5 + return (void*)(symbol ? NSAddressOfSymbol(symbol) : NULL); +#else + void* symbol = (void*)glGetProcAddress(name); + #if defined(PLATFORM_WINDOWS) + if(!symbol) { + // (w)glGetProcAddress will not return function pointers from any OpenGL functions + // that are directly exported by the opengl32.dll + HMODULE module = LoadLibraryA("opengl32.dll"); + symbol = (void*)GetProcAddress(module, name); + } + #endif +#endif + + return symbol; +} + auto OpenGL::terminate() -> void { if(!initialized) return; - setShader(""); //release shader resources (eg frame[] history) + setShader(""); OpenGLSurface::release(); if(buffer) { delete[] buffer; buffer = nullptr; } initialized = false; diff --git a/ruby/video/opengl/opengl.hpp b/ruby/video/opengl/opengl.hpp index f3ec0bc3c..15e90bbc7 100644 --- a/ruby/video/opengl/opengl.hpp +++ b/ruby/video/opengl/opengl.hpp @@ -17,8 +17,8 @@ #endif #include "bind.hpp" -#include "shaders.hpp" #include "utility.hpp" +#include "librashader_ld.h" struct OpenGL; @@ -35,47 +35,35 @@ struct OpenGLTexture { }; struct OpenGLSurface : OpenGLTexture { - auto allocate() -> void; auto size(u32 width, u32 height) -> void; auto release() -> void; auto render(u32 sourceWidth, u32 sourceHeight, u32 targetX, u32 targetY, u32 targetWidth, u32 targetHeight) -> void; - GLuint program = 0; GLuint framebuffer = 0; - GLuint vao = 0; - GLuint vbo[3] = {0, 0, 0}; - GLuint vertex = 0; - GLuint geometry = 0; - GLuint fragment = 0; + GLuint framebufferTexture = 0; + GLuint framebufferFormat = 0; + GLuint framebufferWidth = 0; + GLuint framebufferHeight = 0; u32* buffer = nullptr; -}; - -struct OpenGLProgram : OpenGLSurface { - auto bind(OpenGL* instance, const Markup::Node& node, const string& pathname) -> void; - auto parse(OpenGL* instance, string& source) -> void; - auto release() -> void; - u32 phase = 0; //frame counter - u32 modulo = 0; //frame counter modulus - u32 absoluteWidth = 0; - u32 absoluteHeight = 0; - f64 relativeWidth = 0; - f64 relativeHeight = 0; - vector pixmaps; + libra_instance_t _libra; + libra_shader_preset_t _preset = NULL; + libra_gl_filter_chain_t _chain = NULL; + u32 frameCount = 0; }; -struct OpenGL : OpenGLProgram { +struct OpenGL : OpenGLSurface { auto setShader(const string& pathname) -> void; - auto allocateHistory(u32 size) -> void; auto clear() -> void; auto lock(u32*& data, u32& pitch) -> bool; auto output() -> void; auto initialize(const string& shader) -> bool; auto terminate() -> void; + static auto resolveSymbol(const char* name) -> const void*; - vector programs; - vector history; GLuint inputFormat = GL_RGBA8; + u32 absoluteWidth = 0; + u32 absoluteHeight = 0; u32 outputX = 0; u32 outputY = 0; u32 outputWidth = 0; @@ -95,5 +83,4 @@ struct OpenGL : OpenGLProgram { #include "texture.hpp" #include "surface.hpp" -#include "program.hpp" #include "main.hpp" diff --git a/ruby/video/opengl/program.hpp b/ruby/video/opengl/program.hpp deleted file mode 100644 index a94901003..000000000 --- a/ruby/video/opengl/program.hpp +++ /dev/null @@ -1,108 +0,0 @@ -auto OpenGLProgram::bind(OpenGL* instance, const Markup::Node& node, const string& pathname) -> void { - filter = glrFilter(node["filter"].text()); - wrap = glrWrap(node["wrap"].text()); - modulo = glrModulo(node["modulo"].integer()); - - string w = node["width"].text(), h = node["height"].text(); - if(w.endsWith("%")) relativeWidth = toReal(w.trimRight("%", 1L)) / 100.0; - else absoluteWidth = w.natural(); - if(h.endsWith("%")) relativeHeight = toReal(h.trimRight("%", 1L)) / 100.0; - else absoluteHeight = h.natural(); - - format = glrFormat(node["format"].text()); - - program = glCreateProgram(); - glGenFramebuffers(1, &framebuffer); - - if(file::exists({pathname, node["vertex"].text()})) { - string source = file::read({pathname, node["vertex"].text()}); - parse(instance, source); - vertex = glrCreateShader(program, GL_VERTEX_SHADER, source); - } else { - vertex = glrCreateShader(program, GL_VERTEX_SHADER, OpenGLVertexShader); - } - - if(file::exists({pathname, node["geometry"].text()})) { - string source = file::read({pathname, node["geometry"].text()}); - parse(instance, source); - geometry = glrCreateShader(program, GL_GEOMETRY_SHADER, source); - } else { - //geometry shaders, when attached, must pass all vertex output through to the fragment shaders - //geometry = glrCreateShader(program, GL_GEOMETRY_SHADER, OpenGLGeometryShader); - } - - if(file::exists({pathname, node["fragment"].text()})) { - string source = file::read({pathname, node["fragment"].text()}); - parse(instance, source); - fragment = glrCreateShader(program, GL_FRAGMENT_SHADER, source); - } else { - fragment = glrCreateShader(program, GL_FRAGMENT_SHADER, OpenGLFragmentShader); - } - - for(auto& leaf : node.find("pixmap")) { - nall::image image({pathname, leaf.text()}); - if(!image) continue; - image.transform(); - - GLuint texture; - glGenTextures(1, &texture); - - u32 n = pixmaps.size(); - pixmaps(n).texture = texture; - pixmaps(n).width = image.width(); - pixmaps(n).height = image.height(); - pixmaps(n).format = format; - pixmaps(n).filter = filter; - pixmaps(n).wrap = wrap; - if(leaf["format"]) pixmaps(n).format = glrFormat(leaf["format"].text()); - if(leaf["filter"]) pixmaps(n).filter = glrFilter(leaf["filter"].text()); - if(leaf["wrap"]) pixmaps(n).wrap = glrWrap(leaf["wrap"].text()); - - u32 w = glrSize(image.width()), h = glrSize(image.height()); - u32* buffer = new u32[w * h](); - glBindTexture(GL_TEXTURE_2D, texture); - glTexImage2D(GL_TEXTURE_2D, 0, pixmaps(n).format, w, h, 0, pixmaps(n).getFormat(), pixmaps(n).getType(), buffer); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image.width(), image.height(), getFormat(), getType(), image.data()); - delete[] buffer; - } - - OpenGLSurface::allocate(); - glrLinkProgram(program); -} - -//apply manifest settings to shader source #in tags -auto OpenGLProgram::parse(OpenGL* instance, string& source) -> void { - auto lines = source.split("\n"); - for(auto& line : lines) { - string s = line; - if(auto position = s.find("//")) s.resize(position()); //strip comments - s.strip(); //remove extraneous whitespace - if(s.match("#in ?*")) { - s.trimLeft("#in ", 1L).strip(); - if(auto setting = instance->settings.find({s})) { - line = {"#define ", setting().name, " ", setting().value}; - } else { - line.reset(); //undefined variable (test in source with #ifdef) - } - } - } - source = lines.merge("\n"); -} - -auto OpenGLProgram::release() -> void { - OpenGLSurface::release(); - for(auto& pixmap : pixmaps) glDeleteTextures(1, &pixmap.texture); - pixmaps.reset(); - - width = 0; - height = 0; - format = GL_RGBA8; - filter = GL_LINEAR; - wrap = GL_CLAMP_TO_BORDER; - phase = 0; - modulo = 0; - absoluteWidth = 0; - absoluteHeight = 0; - relativeWidth = 0; - relativeHeight = 0; -} diff --git a/ruby/video/opengl/shaders.hpp b/ruby/video/opengl/shaders.hpp deleted file mode 100644 index 91f9efce6..000000000 --- a/ruby/video/opengl/shaders.hpp +++ /dev/null @@ -1,91 +0,0 @@ -static string OpenGLOutputVertexShader = R"( - #version 150 - - uniform vec4 targetSize; - uniform vec4 outputSize; - - in vec2 texCoord; - - out Vertex { - vec2 texCoord; - } vertexOut; - - void main() { - //center image within output window - if(gl_VertexID == 0 || gl_VertexID == 2) { - gl_Position.x = -(targetSize.x / outputSize.x); - } else { - gl_Position.x = +(targetSize.x / outputSize.x); - } - - //center and flip vertically (buffer[0, 0] = top-left; OpenGL[0, 0] = bottom-left) - if(gl_VertexID == 0 || gl_VertexID == 1) { - gl_Position.y = +(targetSize.y / outputSize.y); - } else { - gl_Position.y = -(targetSize.y / outputSize.y); - } - - //align image to even pixel boundary to prevent aliasing - vec2 align = fract((outputSize.xy + targetSize.xy) / 2.0) * 2.0; - gl_Position.xy -= align / outputSize.xy; - gl_Position.zw = vec2(0.0, 1.0); - - vertexOut.texCoord = texCoord; - } -)"; - -static string OpenGLVertexShader = R"( - #version 150 - - in vec4 position; - in vec2 texCoord; - - out Vertex { - vec2 texCoord; - } vertexOut; - - void main() { - gl_Position = position; - vertexOut.texCoord = texCoord; - } -)"; - -static string OpenGLGeometryShader = R"( - #version 150 - - layout(triangles) in; - layout(triangle_strip, max_vertices = 3) out; - - in Vertex { - vec2 texCoord; - } vertexIn[]; - - out Vertex { - vec2 texCoord; - }; - - void main() { - for(int i = 0; i < gl_in.length(); i++) { - gl_Position = gl_in[i].gl_Position; - texCoord = vertexIn[i].texCoord; - EmitVertex(); - } - EndPrimitive(); - } -)"; - -static string OpenGLFragmentShader = R"( - #version 150 - - uniform sampler2D source[]; - - in Vertex { - vec2 texCoord; - }; - - out vec4 fragColor; - - void main() { - fragColor = texture(source[0], texCoord); - } -)"; diff --git a/ruby/video/opengl/surface.hpp b/ruby/video/opengl/surface.hpp index 475ad69c6..93dd464fb 100644 --- a/ruby/video/opengl/surface.hpp +++ b/ruby/video/opengl/surface.hpp @@ -1,9 +1,3 @@ -auto OpenGLSurface::allocate() -> void { - glGenVertexArrays(1, &vao); - glBindVertexArray(vao); - glGenBuffers(3, &vbo[0]); -} - auto OpenGLSurface::size(u32 w, u32 h) -> void { if(width == w && height == h) return; width = w, height = h; @@ -16,100 +10,36 @@ auto OpenGLSurface::size(u32 w, u32 h) -> void { glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); glTexImage2D(GL_TEXTURE_2D, 0, format, w, h, 0, getFormat(), getType(), buffer); - - if(framebuffer) { - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); - delete[] buffer; - buffer = nullptr; - } } auto OpenGLSurface::release() -> void { - if(vbo[0]) { glDeleteBuffers(3, &vbo[0]); for(auto &o : vbo) o = 0; } - if(vao) { glDeleteVertexArrays(1, &vao); vao = 0; } - if(vertex) { glDetachShader(program, vertex); glDeleteShader(vertex); vertex = 0; } - if(geometry) { glDetachShader(program, geometry); glDeleteShader(geometry); geometry = 0; } - if(fragment) { glDetachShader(program, fragment); glDeleteShader(fragment); fragment = 0; } if(texture) { glDeleteTextures(1, &texture); texture = 0; } if(framebuffer) { glDeleteFramebuffers(1, &framebuffer); framebuffer = 0; } - if(program) { glDeleteProgram(program); program = 0; } width = 0, height = 0; } auto OpenGLSurface::render(u32 sourceWidth, u32 sourceHeight, u32 targetX, u32 targetY, u32 targetWidth, u32 targetHeight) -> void { - glViewport(targetX, targetY, targetWidth, targetHeight); - - f32 w = (f32)sourceWidth / (f32)glrSize(sourceWidth); - f32 h = (f32)sourceHeight / (f32)glrSize(sourceHeight); - - f32 u = (f32)targetWidth; - f32 v = (f32)targetHeight; - - GLfloat modelView[] = { - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1, - }; - - GLfloat projection[] = { - 2.0f/u, 0.0f, 0.0f, 0.0f, - 0.0f, 2.0f/v, 0.0f, 0.0f, - 0.0f, 0.0f, -1.0f, 0.0f, - -1.0f, -1.0f, 0.0f, 1.0f, - }; - - GLfloat modelViewProjection[4 * 4]; - MatrixMultiply(modelViewProjection, modelView, 4, 4, projection, 4, 4); - - GLfloat vertices[] = { - 0, 0, 0, 1, - u, 0, 0, 1, - 0, v, 0, 1, - u, v, 0, 1, - }; - - GLfloat positions[4 * 4]; - for(u32 n = 0; n < 16; n += 4) { - MatrixMultiply(&positions[n], &vertices[n], 1, 4, modelViewProjection, 4, 4); + glBindTexture(GL_TEXTURE_2D, texture); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + if(_chain != NULL) { + libra_source_image_gl_t input = {texture, format, sourceWidth, sourceHeight}; + libra_viewport_t viewport{(float)targetX, (float)targetY, targetWidth, targetHeight}; + libra_output_framebuffer_gl_t output = {framebuffer, framebufferTexture, framebufferFormat}; + + if (auto error = _libra.gl_filter_chain_frame(&_chain, frameCount++, input, viewport, output, NULL, NULL)) { + _libra.error_print(error); + } + + glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBlitFramebuffer(0, framebufferHeight, framebufferWidth, 0, 0, 0, framebufferWidth, framebufferHeight, GL_COLOR_BUFFER_BIT, filter); + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + } else { + glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBlitFramebuffer(0, framebufferHeight, framebufferWidth, 0, targetX, targetY, targetWidth + targetX, targetHeight + targetY, GL_COLOR_BUFFER_BIT, filter); + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); } - GLfloat texCoords[] = { - 0, 0, - w, 0, - 0, h, - w, h, - }; - - glrUniformMatrix4fv("modelView", modelView); - glrUniformMatrix4fv("projection", projection); - glrUniformMatrix4fv("modelViewProjection", modelViewProjection); - - glBindVertexArray(vao); - - glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); - glBufferData(GL_ARRAY_BUFFER, 16 * sizeof(GLfloat), vertices, GL_STATIC_DRAW); - GLint locationVertex = glGetAttribLocation(program, "vertex"); - glEnableVertexAttribArray(locationVertex); - glVertexAttribPointer(locationVertex, 4, GL_FLOAT, GL_FALSE, 0, 0); - - glBindBuffer(GL_ARRAY_BUFFER, vbo[1]); - glBufferData(GL_ARRAY_BUFFER, 16 * sizeof(GLfloat), positions, GL_STATIC_DRAW); - GLint locationPosition = glGetAttribLocation(program, "position"); - glEnableVertexAttribArray(locationPosition); - glVertexAttribPointer(locationPosition, 4, GL_FLOAT, GL_FALSE, 0, 0); - - glBindBuffer(GL_ARRAY_BUFFER, vbo[2]); - glBufferData(GL_ARRAY_BUFFER, 8 * sizeof(GLfloat), texCoords, GL_STATIC_DRAW); - GLint locationTexCoord = glGetAttribLocation(program, "texCoord"); - glEnableVertexAttribArray(locationTexCoord); - glVertexAttribPointer(locationTexCoord, 2, GL_FLOAT, GL_FALSE, 0, 0); - - glBindFragDataLocation(program, 0, "fragColor"); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - glDisableVertexAttribArray(locationVertex); - glDisableVertexAttribArray(locationPosition); - glDisableVertexAttribArray(locationTexCoord); } diff --git a/ruby/video/video.cpp b/ruby/video/video.cpp index 40cd6cfe9..06b2a58e9 100644 --- a/ruby/video/video.cpp +++ b/ruby/video/video.cpp @@ -14,10 +14,6 @@ #include #endif -#if defined(VIDEO_GLX2) - #include -#endif - #if defined(VIDEO_WGL) #include #endif @@ -172,10 +168,6 @@ auto Video::create(string driver) -> bool { if(driver == "OpenGL 3.2") self.instance = new VideoGLX(*this); #endif - #if defined(VIDEO_GLX2) - if(driver == "OpenGL 2.0") self.instance = new VideoGLX2(*this); - #endif - #if defined(VIDEO_WGL) if(driver == "OpenGL 3.2") self.instance = new VideoWGL(*this); #endif @@ -216,10 +208,6 @@ auto Video::hasDrivers() -> vector { "OpenGL 3.2", #endif - #if defined(VIDEO_GLX2) - "OpenGL 2.0", - #endif - #if defined(VIDEO_XVIDEO) "XVideo", #endif @@ -242,8 +230,6 @@ auto Video::optimalDriver() -> string { return "OpenGL 3.2"; #elif defined(VIDEO_GLX) return "OpenGL 3.2"; - #elif defined(VIDEO_GLX2) - return "OpenGL 2.0"; #elif defined(VIDEO_XVIDEO) return "XVideo"; #elif defined(VIDEO_XSHM) @@ -266,8 +252,6 @@ auto Video::safestDriver() -> string { return "XShm"; #elif defined(VIDEO_XVIDEO) return "XVideo"; - #elif defined(VIDEO_GLX2) - return "OpenGL 2.0"; #elif defined(VIDEO_GLX) return "OpenGL 3.2"; #else diff --git a/scripts/update-subtrees.sh b/scripts/update-subtrees.sh index e86c477dd..e962816cc 100755 --- a/scripts/update-subtrees.sh +++ b/scripts/update-subtrees.sh @@ -13,5 +13,5 @@ cd "$(dirname "$0")"/.. || exit 1 git subtree pull --prefix=ares/n64/vulkan/parallel-rdp https://github.com/Themaister/parallel-rdp-standalone.git master --squash git subtree pull --prefix=thirdparty/sljit https://github.com/zherczeg/sljit.git master --squash git subtree pull --prefix=thirdparty/libchdr https://github.com/rtissera/libchdr master --squash -git subtree pull --prefix=thirdparty/librashader https://github.com/SnowflakePowered/librashader/ master --squash -git subtree pull --prefix=thirdparty/slang-shaders https://github.com/libretro/slang-shaders master --squash +git subtree pull --prefix=thirdparty/librashader https://github.com/SnowflakePowered/librashader master --squash +git subtree pull --prefix=thirdparty/slang-shaders https://github.com/libretro/slang-shaders master --squash \ No newline at end of file diff --git a/tests/i8080/GNUmakefile b/tests/i8080/GNUmakefile index 6a801bec3..2c5e6be11 100644 --- a/tests/i8080/GNUmakefile +++ b/tests/i8080/GNUmakefile @@ -69,11 +69,7 @@ else ifneq ($(filter $(platform),linux bsd),) mkdir -p $(prefix)/bin/ mkdir -p $(prefix)/share/applications/ mkdir -p $(prefix)/share/icons/hicolor/256x256/apps/ - mkdir -p $(prefix)/share/$(name)/Shaders/ - mkdir -p $(prefix)/share/$(name)/Database/ cp $(output.path)/$(name) $(prefix)/bin/$(name) - cp -R $(ares.path)/Shaders/* $(prefix)/share/$(name)/Shaders/ - cp -R $(mia.path)/Database/* $(prefix)/share/$(name)/Database/ cp resource/$(name).desktop $(prefix)/share/applications/$(name).desktop cp resource/$(name).png $(prefix)/share/icons/hicolor/256x256/apps/$(name).png endif diff --git a/thirdparty/librashader/build-librashader.sh b/thirdparty/librashader/build-librashader.sh new file mode 100755 index 000000000..dc6bbb765 --- /dev/null +++ b/thirdparty/librashader/build-librashader.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# On non-macOS posix systems, we can check for the presence of librashader using pkg-config +if [ "$(uname)" != "Darwin" ] && [ "$(uname)" != "Windows_NT" ]; then + if pkg-config --exists librashader; then + echo "librashader is already installed on the system. Skipping build." + exit 0 + fi +fi + +# Check for the presence of rustup to verify that we have a working rust installation +if ! command -v rustup &> /dev/null; then + echo "rustup not found. Please install rustup from https://rustup.rs/. If prompted, please install the nightly toolchain." + exit 1 +fi + +# Check for a nightly toolchain +if ! rustup toolchain list | grep -q nightly; then + echo "No nightly toolchain installed. Please run 'rustup toolchain install nightly' to install it." + exit 1 +fi + +# If we're on macOS, we need to build for both x86_64 and arm64 targets and merge into a universal binary +if [ "$(uname)" = "Darwin" ]; then + # Check for the presence of the x86_64 and aarch64 targets + if ! rustup target list --installed | grep -q x86_64-apple-darwin; then + echo "x86_64-apple-darwin target not found. Please run 'rustup target add x86_64-apple-darwin' to install it." + exit 1 + fi + + if ! rustup target list --installed | grep -q aarch64-apple-darwin; then + echo "aarch64-apple-darwin target not found. Please run 'rustup target add aarch64-apple-darwin' to install it." + exit 1 + fi + + cargo +nightly run -p librashader-build-script -- --profile optimized --target x86_64-apple-darwin + cargo +nightly run -p librashader-build-script -- --profile optimized --target aarch64-apple-darwin + lipo -create -output target/optimized/librashader.dylib target/x86_64-apple-darwin/optimized/librashader.dylib target/aarch64-apple-darwin/optimized/librashader.dylib + rm target/x86_64-apple-darwin/optimized/librashader.dylib target/aarch64-apple-darwin/optimized/librashader.dylib +else + # If a parameter is passed, we build for the specified target + if [ $# -eq 1 ]; then + cargo +nightly run -p librashader-build-script -- --profile optimized --target $1 + else + cargo +nightly run -p librashader-build-script -- --profile optimized + fi + + if [ "$(uname)" = "Linux" ]; then + echo "\nlibrashader built successfully, to install, please run the following commands:" + echo "!IMPORTANT!: Make sure to substitute the correct target paths for your system" + echo "sudo cp target/optimized/librashader.so /usr/local/lib/" + echo "sudo cp target/optimized/librashader.so /usr/local/lib/librashader.so.1" + echo "sudo cp pkg/librashader.pc /usr/local/lib/pkgconfig/" + fi +fi + diff --git a/thirdparty/librashader/include/librashader_ld.h b/thirdparty/librashader/include/librashader_ld.h index b8119d982..ea17ddadb 100644 --- a/thirdparty/librashader/include/librashader_ld.h +++ b/thirdparty/librashader/include/librashader_ld.h @@ -29,13 +29,13 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // Uncomment the following defines to activate runtimes. -// #define LIBRA_RUNTIME_OPENGL +#define LIBRA_RUNTIME_OPENGL // #define LIBRA_RUNTIME_VULKAN -// #if defined(_WIN32) -// #define LIBRA_RUNTIME_D3D11 -// #define LIBRA_RUNTIME_D3D12 -// #endif +#if defined(_WIN32) +#define LIBRA_RUNTIME_D3D11 +#define LIBRA_RUNTIME_D3D12 +#endif // #if (defined(__APPLE__) && defined(__OBJC__)) // #define LIBRA_RUNTIME_METAL