Blog


Final Fantasy XIII technically did release while CRTs were still common.
Preset: koko-aio Ambilight Immersive

It’s been almost exactly a year since librashader was released and while I’ve had lots of positive feedback, this release hopes to address the concerns I’ve received regarding its developer experience, both for Rust, and C API usage. This release of librashader finally brings complete reference parity with RetroArch slang shaders with two brand new runtimes, support for preset path wildcards, and support for new shader semantics added to RetroArch since last year.

Full macOS and Metal support


The default Xcode Metal template, now with 100% more bezels.

librashader was primarily focused on Windows and Linux, and it wasn’t until recently that I was able to get my hands on a macOS machine to be able to do some basic testing. This release of librashader brings full macOS support by fixing a bug in the OpenGL runtime that prevented shaders from running on macOS, as well as an entirely new Metal runtime that is fully compatible with Objective-C. With the addition of the Metal runtime, librashader is now able to run on all modern desktop environments.

After completing the (also new) wgpu runtime with minimal rendering bugs, I became convinced that I could write a fully working Metal runtime for librashader without needing a lot of testing. This was a moment of hubris: I was fortunately able to borrow a friend’s MacBook Pro for a couple of days to iron out one or two bugs, as well as writing a quick Objective-C example to test the validity of the C bindings. Still, thanks to the many common abstractions behind each librashader runtime, I was able to hammer out most of the Metal runtime in a little less than 4 days, with a couple of days extra to debug.

The Metal runtime is written completely in Rust, mostly on a Windows environment. Since I still don’t have easy access to a macOS machine, testing will have to rely on users and developers on macOS. It also does not support shader caching at the moment, but it seems shader compilation on Apple Silicon machines is fairly fast. The Metal runtime supports both Intel Macs and Apple Silicon, as well as iOS if you build your own binaries.

A huge thanks to @LukeUsher for kicking off this effort by setting up CI builds for macOS and testing the OpenGL runtime on macOS.

wgpu runtime

librashader now provides a wgpu runtime, currently only available in the Rust API. This will allow developers writing wgpu applications to more easily integrate librashader into their rendering pipelines.

There were considerable technical difficulties to bringing this all-new runtime to librashader. While it is an ‘abstraction layer’ over native graphics APIs, it only properly supports the WGSL shader language, which does not support combined “texture samplers”, (sampler2D) but this is a requirement of the slang-shader spec. The solution is to write a custom compiler pass that runs after the shader files have been parsed and compiled into SPIR-V bytecode, to “lower” or split apart textures and samplers so they can be recompiled to WGSL. This was the biggest blocker to writing this backend, and having now written this lowering pass, it opens up whole new options for librashader to eventually switch completely to naga for shader transpilation over SPIRV-Cross.

There are some caveats, especially compared to some other shaders. wgpu does not support any shaders that use the inverse function, this includes Mega Bezel shaders. wgpu also does not support shader caching in any way. While wgpu works on the web through WebGPU, librashader currently uses some C dependencies like glslang that prevent it from building, which means it only runs on native platforms. This is not an insurmountable issue however, and I hope to see librashader in browsers in the future.

Thanks to @dbalsom for requesting this runtime for his MartyPC emulator, and @eddyb for helping me figure out some of the more difficult SPIR-V transforms.

Preset path wildcard replacements

librashader now supports loading presets with shader preset path wildcards, from both the Rust and the C API. Presets can now be created with a context object that can map wildcard strings like $VID-USER-ROT$ with the value of your choice. All wildcards supported by RetroArch are also supported by librashader, and you can even have custom wildcards. This should bring enhanced support for some of the Mega Bezel shaders that rely on these wildcards to provide a better experience for users.

New shader semantics and scaling options

librashader now supports the Rotation, CurrentSubFrame, and TotalSubFrames semantics introduced to RetroArch fairly recently. As librashader just handles applying effects to a quad texture, applications that use librashader will need to supply it with the relevant and correct values to take advantage of these features.

The new original scaling type, still in draft, is also now supported by librashader. This PR has yet to land in RetroArch, so no shader presets currently use this scaling type, but when it lands, librashader will correctly handle scaling for presets that use this scale type.

Build and portability improvements

This release placed a large focus on making librashader easier to build, on more platforms. The build-time requirements for librashader 0.1.0 used to require Python, CMake, and some more tooling due to upstream dependencies. After much effort on improving or replacing those dependencies, librashader now only needs a fairly recent Rust compiler, and a C toolchain to build.

This was done by bypassing Meson for spirv-to-dxil and replacing the shader compiler with new bindings directly to glslang. Glslang is a compiler used to compile the shader source files, which are written in GLSL, to SPIR-V, which is then converted back into a variety of different shader formats for each runtime. While RetroArch uses glslang directly, for many years shaderc was the only solution for Rust. Shaderc is a wrapper around glslang written by Google, but included many features not used by librashader. Many of librashader’s non-Rust build requirements came from Shaderc, and with its removal, these build requirements are no longer required.

The buildtime dependency on libvulkan was also removed. A sneaky feature flag that had been enabled in the Vulkan runtime caused librashader.dll to need to link with the Vulkan SDK; this flag was removed and now librashader can build on environments without libvulkan present.

librashader now no longer ships an extra copy of SQLite for caching, which should also help with statically linked environments. The shader cache is now backed by persy, which is a pure-Rust persistence key-value store. This means that updating librashader will invalidate existing shader caches, but persy should be a tiny bit faster than SQLite when saving and restoring shaders (although SQLite was already plenty fast).

Linux packages

librashader now provides packages for a variety of Linux distributions across various architectures. I hope this will make using librashader on Linux a little easier for end users and developers. librashader provides packages on Arch, Ubuntu 23.10 and Fedora 39, all of which ship relatively recent versions of rustc. Unfortunately, as Debian Bookworm’s rustc is still stuck on 1.63, which is over a year and a half old, I am unable to properly support Debian until they upgrade their Rust compiler to 1.70 or higher.

As librashader no longer has buildtime linkage requirements, the binary packages are standalone and do not pull in any dependencies. Source packages require cargo, gcc and patchelf (to update the SONAME), but no other external libraries or build tooling.

Next steps

With the completion of the Metal and wgpu runtimes, librashader brings slang-shader support to every modern graphics API. The only remaining frontier is WebGPU support, which would involve adding emscripten bindings to glslang-rs. There are also some ecosystem challenges with WebAssembly, so as the WebAssembly environment matures, I’ll be sure to bring librashader along with it although how long that will take is anyone’s guess.

I would also like eventually allow a fully pure-Rust shader compilation pipeline. While in theory naga would be able to replace both glslang, its GLSL parser as well as its output are not as mature as glslang and SPIRV-Cross—this is the main reason why complex shaders don’t work with the wgpu backend. It will take some more time in order to bring a full Rust dependency chain to librashader.

Of course, I intend to fix bugs as usual, as well as keep up with new shader features released by RetroArch. librashader’s runtime abstractions means that the work to add new semantic uniforms and features only have to be done once. However, with this release I expect things to slow down quite a lot, as I consider librashader pretty much feature complete. I hope that with the addition of macOS support and the improvements to the build-time experience, developers will now find it easier than ever to bring slang-shaders to their games and emulators.

As for Snowflake, it’s a marathon not a sprint. In the background I’ve been slowly working on bindings to WinFSP, which will power the next-generation virtualized filesystem aincradfs for Snowflake. aincradfs will enable true filesystem projection and allow for advanced scenarios such as on-the-fly ROM-hack patching, automatic DLC installation, and better support for save data, especially for modern systems like PS3 and Nintendo Switch.

You can find librashader on GitHub.


I don't remember Advanced Wars looking that good on GBA.

A little over a month ago I unveiled librashader, a standalone shader runtime for RetroArch ‘slang’ shaders in this blog post. I’ve been blown away by the positive response and in the end couldn’t keep away at polishing and tackling some of those “Future Work” items I alluded to at the end. With all the bugfixes and new features this month, I’m happy to announce the first numbered release of librashader, version 0.1.0, bringing it officially out of beta.

The new release includes a ton of bugfixes to correct rendering accuracy as well as host of new features that try to make librashader a ”better-than-reference” implementation of RetroArch’s slang-shader runtime; features like a working Direct3D 12 implementation, multithreaded shader compilation, and a persistent global shader cache. librashader 0.1.0 also begins a commitment to semantic versioning of the library as well as an explicit versioning policy for C ABI compatibility.

Before anything gets out of hand, the hero image is just a screenshot of the Advanced Wars remake running Duimon’s GBA Mega Bezel preset on the Direct3D 11 test program, for illustrative purposes only.

Direct3D 12 Runtime


Mega Bezel SMOOTH-ADV-GLASS on DirectX 12.

In the last blog post, I said something along the lines of “D3D12 should be pretty easy to add support for”. Instead, adding D3D12 support sent me down an even deeper rabbit hole than Vulkan or any other runtime ever did. I’m really proud of the end result; to explain a little bit more, let’s take a look at how crt-royale looks like on RetroArch today.


That doesn't look right.

When writing a new runtime, I generally begin my journey at looking at what is “correct” output in RetroArch’s image display. Unfortunately, RetroArch’s Direct3D 12 driver seems to be extremely broken. I wasn’t even able to debug it with RenderDoc because it would just crash whenever it tried to hook the process.

Despite that, I forged on and hoped for the best. By this point I had three different runtimes to compare with, and a collection of abstractions that I knew had to be correct for any graphics API, so I started off with translating parts of the Direct3D 11 runtime into Direct3D 12, referencing the Vulkan runtime whenever certain parts in the API needed a more “Vulkan-y” approach. Each runtime does basically the same things, in the same order, just with a different way of doing so.

Generating mipmaps in D3D12 was one of the things that were done very differently than in any other of the runtimes. While OpenGL, Direct3D 11, and Vulkan all have ways to issue commands from the CPU to the GPU to tell it to generate a mipmap chain (Vulkan is a little more low-level but vkCmdBlitImage in a loop is close enough), Direct3D 12 requires the use of a compute shader. RetroArch uses a custom written mipmap shader to generate these, but it was important for me that librashader remain MPL 2.0 compatible, so I couldn’t use this code. Instead, Microsoft provides an MIT licensed mipmap shader that comes with no documentation or usage examples. Having never used the Direct3D 12 API before, it was a bit of a struggle to figure this out.


crt-royale on librashader Direct3D 12.

I eventually got everything working, and to my surprise, crt-royale loaded and displayed somewhat correct output. When testing other shaders though, it soon became apparent that something was wrong because my test program would refuse to load some shaders. It turns out that SPIRV-Cross, which is the library RetroArch and librashader uses to translates ‘slang’ (Vulkan GLSL) into HLSL for Direct3D 11 and 12, has problems with how some shaders are written; despite that, it mostly works out alright for Direct3D 11. Direct3D 12 however, is a lot stricter and will straight up refuse to run the shader if everything isn’t ✨perfect✨, so shaders that run into this issue will just not load at all. These shaders also refuse to load in RetroArch’s Direct3D 12 driver as well.


crt-lottes fails to load with RetroArch's Direct3D 12 driver.

Thanks to the folks on the DirectX discord, I learned about spirv-to-dxil, which could translate SPIR-V (the bytecode version of the shaders) directly to DXIL, which is the bytecode format that Direct3D 12 understands. Before, the D3D12 runtime had to go through a conversion from GLSL, to SPIR-V, to HLSL, then to DXIL. This resulted in me having to shave a yak for a couple of days to write Rust bindings to spirv-to-dxil. Since spirv-to-dxil is a smaller part of a huge graphics library called Mesa, this mostly involved figuring out how to build as little of Mesa as possible to not bloat librashader too much.

The end result being that the shaders that were broken before with SPIR-V Cross could now load in librashader! However, while spirv-to-dxil seemed like a really great solution, it didn’t work for everything. Some shaders that would load with the SPIR-V Cross HLSL pipeline, no longer loaded with spirv-to-dxil.

The problem turned out to be an issue with “linking” shaders together. It’s a bit too technical for this blogpost but basically the compiler sometimes doesn’t have enough information to properly compile the shader. However, in those cases, SPIR-V Cross mostly seemed to just happen to work for whatever reason. While I was assured that eventually the linking step would be exposed to library consumers, I wasn’t going to wait for that to happen. In the end, I went with the incredibly cursed option of just trying both and seeing what works. If spirv-to-dxil fails, then it will fall back to SPIR-V Cross. If even that fails, then the shader won’t be able to load, but I fortunately in my testing I haven’t encountered this situation yet. In the future as spirv-to-dxil improves, I hope to be able to remove the SPIR-V Cross HLSL pipeline from the D3D12 runtime entirely.

Note that the Direct3D 12 runtime requires dxil.dll to be available.

Multithreaded Shader Compilation and Thread Safety Improvements

librashader being written in Rust makes it almost trivial to allow shaders to compile on multiple threads. On Direct3D 11, Direct3D 12, and Vulkan, shaders and graphics pipelines are now compiled with multiple threads if possible. Note that this only applies to the final compilation of shaders at the driver level; reflection via SPIR-V Cross is still single-threaded because SPIR-V Cross is fast enough that the overhead of multiple threads is probably more than the speed gains. Heavy shaders with a ton of passes will definitely see a noticeable speed-up when loading a shader preset.

Loading LUT images into CPU memory is also now done with multiple threads. This is also available to OpenGL as well as Direct3D 11, Direct3D 12, and Vulkan, and substantially speeds up load times post-compilation.

There were also API improvements in allowing filter chains to be built asynchronously in a different thread. A lot of this involved shoring up various internal datastructures to make sure they were thread safe to read from separate threads, but the biggest additions are the new filter_chain_create_deferred APIs that allow the caller to defer GPU-side initialization of the filter chain until after all the CPU work is done in a different thread. Properly utilized, loading a new shader preset should not “lock up” the emulator or game.

Shader Caching (#10)

Heavy shaders with a lot of passes take incredibly long to load, particuarly for Direct3D 11 which relies on the old and slow FXC shader compiler. Multithreading shader compilation already helps a lot, but I still wasn’t satisfied with load times on certain shaders.

Caching shaders is a technique used by a lot of emulators that need to compile a lot of shaders on the fly to reduce stutters. librashader can use the same technique to bring a substantial load time decrease when the cache is properly hydrated.


Direct3D 11 sees substantial improvements from bypassing FXC.

Loading the Mega Bezel Smooth shader goes from 42 seconds with singlethreaded shader compilation, to around 18 seconds without caching with just multithreading, to a mere 1 second load time on load with a hydrated cache. Other runtimes like Vulkan, OpenGL 4.6, and Direct3D 12 also benefit from caching SPIR-V artifacts, driver-specific compiled programs, and pipeline state objects, albeit not as much as Direct3D 11 as they were already plenty fast.


3X load time decrease by caching Vulkan pipeline caches.

librashader’s shader cache is global, meaning that every program that uses librashader will benefit from the same shader cache. Programs that use librashader don’t have to do anything special to use the shader cache as its enabled by default, but it can be disabled at runtime with a configuration flag. Updating driver versions or switching GPUs will automatically evict old entries and recompile the shader object or pipeline.

Combined with deferring GPU-side initialization, these improvements make it possible for librashader to load shader presets extremely fast and completely without the long pauses it sometimes takes to load a shader in RetroArch.

Improved Rendering Accuracy and Bugfixes

There have been a bunch of improvements to rendering accuracy to be more in line with RetroArch (i.e. more correct) since the beta release last blog post.

  • dffea95 The restriction on only allowing a uniform to be bound to either the UBO or push constants has been lifted. This was causing problems for shaders that bound a uniform to the UBO in the vertex shader, but a push constant in the fragment shader, causing inaccurate rendering or just refusing to load.
  • 92caad2 Output framebuffers were not correctly scaled and caused a lot of shaders to have incorrect output if the source image size was different from the output image size. With this fix, crt-royale goes from looking absolutely horrifying to what it’s supposed to look like. In hindsight, I should have caught this because this is how gbc-lcd-grid-v2 is supposed to look, and not whatever this was, which I thought was a result of poor viewport handling. The triangle is clipped because the source image is too big for what the shader expects, but that is the proper behaviour.
  • 5ffcf00 D3D11: The blend state wasn’t set properly in Direct3D 11 so some shaders would just render a black texture.
  • 3c15a3a The number of frames required for history wasn’t calculated properly, so shaders that used history would have corrupt or no output.
  • 6cb2859 Filter passes with unspecified filters should default to nearest neighbour rather than linear filtering.
  • Direct3D 11, 12, and Vulkan now use the identity MVP with to render intermediate passes. This should make rotation easier to deal with and more closely matches RetroArch’s behaviour.

Thanks to @star69rem for helping me find most of these inaccuracies.

These bugfixes and QOL improvements should help with anyone wanting to use librashader in their project.

  • 43b7d6f Add support for reading shader source files encoded in Latin-1/Windows-1252 rather than UTF-8.
  • 3db89e5 The quad VBO is bound only once per frame rather than per-pass for improved performance.
  • 48a1b28 Path resolution logic in the preset parser was fixed and a bunch of hacks were added to better support certain presets.
  • e2e6357 Parse referenced presets depth-first to better support overriding parameters.
  • c5b2b50 D3D11: Disable CPU access by default on textures to improve performance.
  • fb827b7 Vulkan: Optional support for environments without dynamic rendering support. VK_KHR_dynamic_rendering is no longer required, but is still recommended if possible. Without dynamic rendering, graphics pipelines will be recompiled if the output format changes.
  • 009e740 Vulkan: Memory is now managed with gpu-allocator rather than creating a buffer for each new texture. This should reduce the number of Vulkan buffer allocations that librashader takes up in an application.
  • Lots of soundness fixes and improvements to the C API headers.
  • Lots of cleanup and refactoring of common code to make sure behaviour is as close as possible in all runtimes. If anyone ever wants to take a shot at a Metal runtime, take a look at the librashader-runtime crate.

Next steps

This 0.1.0 release stops short of a “1.0” release in terms of a full commitment to a stable API, but I’m satisfied with the current state of things and hope that I have everything in place to allow librashader minimize the chance of needing to break API or ABI. The changes made to the C headers since the last blog post should hopefully be sufficient to allow librashader to evolve and add new features while maintaining backwards compatibility; and in the worse case, safely refuse to load in the chance of an ABI incompatibility. I’ve put a lot of thought into being able to integrate librashader in a project safely, cleanly, and easily; making sure that updates are done in a compatible manner is one of the most important things to consider.

There are a couple of performance improvements I want to pursue eventually. As stated before, switching to naga is a long term goal that requires upstream changes to support some features we need. If this ever comes to fruition, a WebGPU runtime would be right around the corner. There’s also a SPIR-V to DXBC compiler PR in Mesa that could help to improve initial Direct3D 11 build times and get rid of FXC, but I’ve spoken to the author and it doesn’t seem that it will be brought into tree very soon. All this can be done without breaking the current ABI. The shader cache also provides enough of a speedup that these improvements aren’t as important as they would be without, only improving load times on fresh shader compiles, but there’s always room for improvement.

The other improvements I could think of are the logging API I wrote briefly about as well as possibly more descriptive error strings, but those are not really exciting to write about or work on, and I think the current state of the library is plenty usable and ergonomic even without.

For the time being though, I’ve accomplished most of my short term goals with librashader and plan to let it bake a little, bugfixes notwithstanding. If or when it gains some more adoption, I’ll look into seeing what tweaks (if any) it needs to release a proper 1.0 version and commit long term to a stable API.

As always, feel free to reach out via GitHub if you have any questions or trouble regarding using librashader in your project.

librashader is a complete reimplementation of the RetroArch slang shader pipeline that allows standalone emulators to easily and optionally implement support for RetroArch-style shaders and shader presets.

I’ve been sitting on this for a while since the last Snowflake progress report where I teased it at the very end, but it wasn’t until a few months ago that I nerd sniped myself into investigating the feasibility of doing so. The end result of this effort is a complete reimplementation of a shader pipeline that should be compatible with all shaders in slang-shaders, including the preset parser, the shader preprocessor, and SPIR-V translation, as well as runtime implementations for Direct3D 11, OpenGL, and Vulkan. To be clear, librashader does not “rip out” the shader parts of RetroArch and just repackage it up, but is a complete reimplementation with a fully documented Rust API that exposes every part of the pipeline.

Why do this?

Besides being an opportunity for myself to learn more about graphics APIs, the main goal of Snowflake has always been to provide a seamless experience to the user, without increasing maintenance burden on developers. This is a rejection of the “core” based philosophy where developers have to maintain a separate fork of their emulator to service an API which may fit the emulated console like a square peg does a round hole. Instead, Snowflake’s approach is to contort itself into whatever shape some standalone emulator requires, going so far as to compile configuration files, and (in the future) perform live editing of emulated memory.

That isn’t to say that the “core” based philosophy does not have upsides. RetroArch’s excellent shader system is often praised by users for its excellent visual quality, and compatibility across cores of many emulated systems. To do some things well, you really do need to reach into the code, and hacking around the issue like Snowflake attempts to do will only get you so far.

However, that does not mean that an emulator needs to “core”-ify itself to support RetroArch shaders! librashader brings the power of RetroArch shaders to any program that can render an image to a buffer offscreen. All the emulator has to do, at the very end right before a frame is presented, is to call into a librashader runtime to apply shader passes, and now any emulator can use RetroArch’s shaders, without needing to be a RetroArch core.

How does librashader work?


The OpenGL 4.6 runtime running gbc-lcd-grid-v2. The demo app is very basic and the poor scaling and rendering outside the borders is due to poor handling of the viewport in the demo app, not the librashader runtime.

librashader implements the preset parser, which parses .slangp presets, the shader preprocessor, which handles combining multiple shader files into one unit, which can finally be sent to shader reflection, which actually deals with how to compile the shader for your GPU. This compiled artifact is then sent to a runtime, which deals with rendering the shader given input and output frame buffers.

While Rust is nice, it unfortunately is not in wide use in the emudev community. But since librashader is a Rust library, it can easily provide C exports for integration in C and C++ projects, which the majority of popular emulators are written with.

To make things as painless as possible for emulator developers, librashader is distributed as a dynamic library intended to be loaded at runtime. A header-only C loader (librashader_ld.h) is designed to be copy and pasted into your project, which loads librashader function pointers from an implementation (i.e. librashader.dll or librashader.so). If no implementation is found, then the loader will do nothing when the functions are called. Complete documentation is provided for both the C bindings, and the native Rust API.

I don’t want to prevent commercial, permissively licensed, or closed source projects from using librashader, but I also want to encourage contributions to come back to the project. To that end, the implementation of librashader is licensed under the Mozilla Public License 2.0, whereas the C headers (and only the headers) including librashader_ld.h are licensed under the MIT license. This allows librashader to be used in commercial, permissively licensed, and closed source projects, so long as any modifications are open sourced and make it back to the community, while staying non-viral.

Caveats

The biggest caveat is of course that librashader still requires support from emulator developers, and I do not want to force anyone’s hand via hostile forks or otherwise less-than-polite encouragement. Code changes will be needed to bring in librashader, and while I have designed the library to be as non-invasive as possible, such code changes may not be trivial. For any developers that are interested, please reach out to me via GitHub or Twitter if you encounter any bugs or issues while exploring librashader.

librashader supports less platforms than RetroArch does. I do not have a personal macOS device to develop a Metal runtime for, and developing the Vulkan runtime was a month-long slog for me, having known next-to-nothing about Vulkan, which has left me too burnt out to touch a Direct3D 12 runtime at the moment. I hope to revisit D3D12 soon in the future after switching gears and taking a break.

There are some additional technical caveats listed in the readme that is more helpful for developers, but I don’t forsee any actual issues in practice outside of any actual bugs that come up.

And of course, librashader is currently in beta, with all the versioning finickiness that entails. I am open to suggestions on how to improve the API, and I am hesitant to finalize a “1.0” version without librashader seeing wider use and feedback from other developers. One thing I could think of right now is a need to improve logging, but I need to think on how to create a good logging interface without clogging up standard output for an emulator. There is some additional finagling involved as well due to differences between Rust and C/C++.

Future Work

A Direct3D 12 runtime would be nice to have. All the runtimes are fairly similar in structure, so I think I will quite easily be able to support D3D12 soon but I do need to take a break, play some actual video games, and touch grass. I had originally planned to release this by Christmas, but the Vulkan runtime was taking me so long to find the motivation to complete.

In the more distant future, I am hoping to move the shader compiler and reflection infrastructure from SPIR-V Cross and shaderc, which are Rust bindings to C++ libraries, to naga, which is written in pure Rust, and much faster than SPIR-V Cross in shader translation and reflection, meaning shorter load times for shaders. Moving to naga could bring support for a possible WebGPU runtime. There are also plans to emit DXBC/DXIL completely, bypassing FXC/DXC in HLSL compilation which would potentially lead to faster load times when loading shader presets for Direct3D. However, this is currently blocking on combined image sampler support on GLSL parsing and lowering to shader languages that don’t support combined image samplers like HLSL.

You can find librashader on GitHub.

Built with ❤️ by @chyyran. Text content licensed under CC-BY-SA 4.0.