diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc7b3f6..b28be7a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ The tests require Metal GPU access. They run without launching the app or changi `Vendor/CLibrashader` is the C interface for [SnowflakePowered/librashader](https://github.com/SnowflakePowered/librashader), a RetroArch shader preset parser, compiler, and rendering runtime. It is not a new shader implementation written for ScreenSlanger. -The upstream version is pinned to **0.12.0**, tag [`librashader-v0.12.0`](https://github.com/SnowflakePowered/librashader/tree/librashader-v0.12.0). The runtime build is **0.12.0-screenslanger.1**, with the local source patches described below. The header remains the original upstream header fetched during the ScreenSlanger Metal backend integration: +The upstream version is pinned to **0.12.0**, tag [`librashader-v0.12.0`](https://github.com/SnowflakePowered/librashader/tree/librashader-v0.12.0). The runtime build is **0.12.0-screenslanger.2**, with the local source patches described below. The header remains the original upstream header fetched during the ScreenSlanger Metal backend integration: | Local file | Origin | | --- | --- | @@ -24,10 +24,11 @@ The upstream version is pinned to **0.12.0**, tag [`librashader-v0.12.0`](https: | `Vendor/CLibrashader/BUILDING.md` | Written for ScreenSlanger. Reproduces the runtime from a checkout or the source and patches included in the app. | | `Vendor/CLibrashader/patches/0001-skip-unused-final-target.patch` | ScreenSlanger's MPL-2.0 modification to upstream framebuffer scaling and Metal rendering. Skips full-size allocation of the final target when the shader does not need final-pass feedback; preserves declared sizes and feedback behavior. | | `Vendor/CLibrashader/patches/0002-compact-grayscale-luts.patch` | ScreenSlanger's MPL-2.0 modification to upstream Metal LUT loading. Packs exactly grayscale textures into `RG8Unorm` with a red/red/red/green component swizzle, preserving the original RGB and alpha values, including transparent border samples. Colored textures retain their original `BGRA8Unorm` representation. | +| `Vendor/CLibrashader/patches/0003-stream-metal-lut-loading.patch` | ScreenSlanger's MPL-2.0 modification to Metal preset loading. Decodes and uploads LUTs one at a time; 8-bit grayscale images retain their compact decoded form and upload in at most 64-row strips instead of expanding the whole image. Color and 16-bit images keep the existing conversion behavior, and the public packed-preset format and API are unchanged. Adds the already-pinned `image` crate as a direct Metal dependency without changing dependency versions. | -The dynamic library is **not committed to the repository**. The setup script downloads the pinned upstream source archive, verifies its SHA-256 digest and both reviewed patch digests, applies the patches, and runs `cargo +1.93.0 build --locked --profile optimized --package librashader-capi --no-default-features --features runtime-metal`. It builds with `MACOSX_DEPLOYMENT_TARGET=27.0`, the selected macOS SDK, and `CARGO_PROFILE_OPTIMIZED_STRIP=none`. Disabling stripping avoids Rust 1.93.0 generating misaligned Mach-O LINKEDIT string pools that macOS 27 rejects when loading build-time procedural macros or the final library; optimization and LTO remain enabled. `Cargo.lock` pins Rust dependencies and the embedded C/C++ compiler dependencies. Setup downloads any missing Cargo dependencies and compiles them; the normal Xcode build never invokes Cargo or accesses the network. +The dynamic library is **not committed to the repository**. The setup script downloads the pinned upstream source archive, verifies its SHA-256 digest and all three reviewed patch digests, applies the patches, and runs `cargo +1.93.0 build --locked --profile optimized --package librashader-capi --no-default-features --features runtime-metal`. It builds with `MACOSX_DEPLOYMENT_TARGET=27.0`, the selected macOS SDK, and `CARGO_PROFILE_OPTIMIZED_STRIP=none`. Disabling stripping avoids Rust 1.93.0 generating misaligned Mach-O LINKEDIT string pools that macOS 27 rejects when loading build-time procedural macros or the final library; optimization and LTO remain enabled. The patched `Cargo.lock` preserves upstream's Rust and embedded C/C++ dependency versions; its only change adds the existing `image` crate to the Metal runtime's direct dependencies. Setup downloads any missing Cargo dependencies and compiles them; the normal Xcode build never invokes Cargo or accesses the network. -Setup installs the library as `~/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.1/librashader.dylib`, alongside its source archive, applied patches, license, notice, and build instructions. An existing official `0.12.0` installation is preserved. `BUILD-INFO.txt` records the version, source and patch checksums, compiler version, architecture, and build command. Setup checks that metadata before reusing an installation, so a changed source or build recipe needs a new local runtime version. `RUNTIME.sha256` verifies the installed library; `SHA256SUMS` verifies its source archive and patches. +Setup installs the library as `~/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.2/librashader.dylib`, alongside its source archive, applied patches, license, notice, and build instructions. Existing official `0.12.0` and local `0.12.0-screenslanger.1` installations are preserved. `BUILD-INFO.txt` records the version, source and patch checksums, compiler version, architecture, and build command. Setup checks that metadata before reusing an installation, so a changed source or build recipe needs a new local runtime version. `RUNTIME.sha256` verifies the installed library; `SHA256SUMS` verifies its source archive and patches. The pinned corresponding-source checksums are: @@ -36,10 +37,11 @@ The pinned corresponding-source checksums are: | [Upstream source archive](https://github.com/SnowflakePowered/librashader/archive/refs/tags/librashader-v0.12.0.tar.gz) | `4bf8cf2489d00848dcabbf2163204093776082da4217d5a5db45e4cbf335cedf` | | `0001-skip-unused-final-target.patch` | `f5a9c09c0f7a72059eb536fbd63eacbc513bea5fe964e70f3a122ae61bfd90fb` | | `0002-compact-grayscale-luts.patch` | `2fd5bd08afabf7f0ee6c8329fcfda778b6460001d7de50145c32af329138fe5e` | +| `0003-stream-metal-lut-loading.patch` | `3e3c574a0b8940f04502bece08442ab297bc1b9a7de89dff412b51566de9acfe` | The extracted, unmodified `librashader.h` has SHA-256 `5d478897c391af3f60015810b67785ae1a286d262a845485276e36ded9f21e62`. -The app build copies the library into `Contents/Frameworks/`, gives it a relocatable install name, and signs it with the app's build identity. It also bundles the exact upstream archive, both patches, `SHA256SUMS`, `BUILD-INFO.txt`, `BUILDING.md`, the MPL-2.0 license, and source notice under `Contents/Resources/ThirdParty/librashader/`. These files let a recipient reconstruct the modified runtime source without this checkout. The installation's binary hash is verified before packaging and is not bundled because signing changes its bytes. The vendored C header keeps its own MIT notice. `LIBRASHADER_PATH` can override the runtime for unhosted tests and probes; applications always load the embedded runtime. +The app build copies the library into `Contents/Frameworks/`, gives it a relocatable install name, and signs it with the app's build identity. It also bundles the exact upstream archive, all three patches, `SHA256SUMS`, `BUILD-INFO.txt`, `BUILDING.md`, the MPL-2.0 license, and source notice under `Contents/Resources/ThirdParty/librashader/`. These files let a recipient reconstruct the modified runtime source without this checkout. The installation's binary hash is verified before packaging and is not bundled because signing changes its bytes. The vendored C header keeps its own MIT notice. `LIBRASHADER_PATH` can override the runtime for unhosted tests and probes; applications always load the embedded runtime. ## Updating librashader diff --git a/README.md b/README.md index 4644834..4be5253 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ rustup toolchain install 1.93.0 --profile minimal This downloads [Slang 2026.18](https://github.com/shader-slang/slang/releases/tag/v2026.18) and checks its SHA-256 checksum. Slang is installed into `~/Library/Application Support/ScreenSlanger/Tools/slang/2026.18`, with a `current` symlink that ScreenSlanger discovers automatically. The full Slang distribution is needed, including its libraries. Existing installations under `~/slang` are preserved. -The script also builds **librashader 0.12.0-screenslanger.1** from the checksum-verified [upstream 0.12.0 source](https://github.com/SnowflakePowered/librashader/releases/tag/librashader-v0.12.0), with reviewed patches that avoid unused final-pass targets and store grayscale lookup textures more compactly without changing their sampled values. It uses Rust 1.93.0, the upstream `Cargo.lock`, and an optimized Metal-only build. Setup downloads Cargo dependencies and compiles the runtime once, installing it into `~/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.1` without replacing an existing official `0.12.0` installation. +The script also builds **librashader 0.12.0-screenslanger.2** from the checksum-verified [upstream 0.12.0 source](https://github.com/SnowflakePowered/librashader/releases/tag/librashader-v0.12.0), with reviewed patches that avoid unused final-pass targets, store grayscale lookup textures more compactly, and decode and upload Metal textures one at a time. Grayscale uploads use bounded staging memory while preserving sampled values, and color and 16-bit images retain their original conversion behavior. It uses Rust 1.93.0, pinned Cargo dependencies, and an optimized Metal-only build. Setup downloads Cargo dependencies and compiles the runtime once, installing it into `~/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.2` without replacing existing official or previous local runtime installations. This runtime handles RetroArch presets, including multiple passes, custom vertex stages, reflected uniforms, textures, and filtering. Its compiler is built in, so Homebrew, `glslang`, and `spirv-cross` are not required. The app includes the MPL-2.0 license, original source archive, exact patches, checksums, and rebuild instructions. The vendored C header's origin and the runtime's build and update procedures are documented in [CONTRIBUTING.md](./CONTRIBUTING.md). @@ -49,7 +49,7 @@ Run the setup script again after pulling project updates. The Slang and librasha | Dependency | Version | Purpose | | --- | --- | --- | | [Slang](https://github.com/shader-slang/slang/releases/tag/v2026.18) | 2026.18 | Native Slang effects | -| [librashader](https://github.com/SnowflakePowered/librashader/releases/tag/librashader-v0.12.0) | 0.12.0-screenslanger.1 | Patched RetroArch Metal rendering runtime | +| [librashader](https://github.com/SnowflakePowered/librashader/releases/tag/librashader-v0.12.0) | 0.12.0-screenslanger.2 | Patched RetroArch Metal rendering runtime | | [Rust](https://www.rust-lang.org/) | 1.93.0 | Build-time compiler for librashader; not required to run the app | To inspect installed versions: diff --git a/ScreenSlanger/librashader.swift b/ScreenSlanger/librashader.swift index f35cb28..07bde88 100644 --- a/ScreenSlanger/librashader.swift +++ b/ScreenSlanger/librashader.swift @@ -293,7 +293,7 @@ private final class LibraAPI { // Unhosted tests and compiler probes use the pinned development installation. paths = [ ProcessInfo.processInfo.environment["LIBRASHADER_PATH"], - FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.1/librashader.dylib").path + FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.2/librashader.dylib").path ].compactMap { $0 } } var loaded: UnsafeMutableRawPointer? diff --git a/ScreenSlangerTests/RuntimeBundleTests.swift b/ScreenSlangerTests/RuntimeBundleTests.swift index 337db96..00e489e 100644 --- a/ScreenSlangerTests/RuntimeBundleTests.swift +++ b/ScreenSlangerTests/RuntimeBundleTests.swift @@ -130,7 +130,7 @@ struct RuntimeBundleTests { let directory = try builtApplication().appendingPathComponent("Contents/Resources/ThirdParty/librashader") let manifest = try String(contentsOf: directory.appendingPathComponent("SHA256SUMS"), encoding: .utf8) let files = ["librashader-v0.12.0-source.tar.gz", "0001-skip-unused-final-target.patch", - "0002-compact-grayscale-luts.patch"] + "0002-compact-grayscale-luts.patch", "0003-stream-metal-lut-loading.patch"] for file in files { let line = try #require(manifest.split(separator: "\n").first { $0.hasSuffix(" " + file) }) let bytes = try Data(contentsOf: directory.appendingPathComponent(file), options: .mappedIfSafe) @@ -138,7 +138,7 @@ struct RuntimeBundleTests { #expect(actual == String(line.prefix(64)), "Bundled source differs from its build manifest: \(file)") } let provenance = try String(contentsOf: directory.appendingPathComponent("BUILD-INFO.txt"), encoding: .utf8) - #expect(provenance.contains("0.12.0-screenslanger.1")) + #expect(provenance.contains("0.12.0-screenslanger.2")) for file in ["LICENSE-MPL-2.0.md", "NOTICE.md", "BUILDING.md"] { #expect(!(try String(contentsOf: directory.appendingPathComponent(file), encoding: .utf8)).isEmpty) } diff --git a/ScreenSlangerTests/ShaderTests.swift b/ScreenSlangerTests/ShaderTests.swift index 86c3880..8c71668 100644 --- a/ScreenSlangerTests/ShaderTests.swift +++ b/ScreenSlangerTests/ShaderTests.swift @@ -344,6 +344,44 @@ struct ShaderTests { } } + @Test("Grayscale lookup uploads preserve odd widths and rows across upload strips") + func grayscaleLookupUploadStrips() async throws { + let temporary = try temporaryDirectory() + defer { try? FileManager.default.removeItem(at: temporary) } + let width = 3, height = 67 + let values = (0..<(width * height)).map { UInt8(($0 * 37) % 256) } + let provider = try #require(CGDataProvider(data: Data(values) as CFData)) + let image = try #require(CGImage( + width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 8, bytesPerRow: width, + space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue), + provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)) + let png = try #require(CGImageDestinationCreateWithURL( + temporary.appendingPathComponent("lookup.png") as CFURL, "public.png" as CFString, 1, nil)) + CGImageDestinationAddImage(png, image, nil) + try #require(CGImageDestinationFinalize(png)) + try retroArchSource(fragment: "texture(BACKGROUND, vTexCoord)", + fragmentDeclarations: "layout(set = 0, binding = 3) uniform sampler2D BACKGROUND;") + .write(to: temporary.appendingPathComponent("sample.slang"), atomically: true, encoding: .utf8) + let preset = temporary.appendingPathComponent("sample.slangp") + try """ + shaders = 1 + shader0 = "sample.slang" + textures = "BACKGROUND" + BACKGROUND = "lookup.png" + BACKGROUND_linear = false + """.write(to: preset, atomically: true, encoding: .utf8) + let shared = SharedMetalResources.shared + defer { shared.clear() } + try await shared.loadEffect(ShaderLoadRequest(url: preset)) + let source = try makeTexture(width: width, height: height) + let destination = try makeTexture(width: width, height: height) + let commands = try #require(shared.commandQueue.makeCommandBuffer()) + try ShaderRenderCore(resources: shared).encode( + commandBuffer: commands, source: source, destination: destination, + context: ShaderFrameContext(outputSize: SIMD2(Float(width), Float(height)))) + try expectPixels(finishRendering(commands, to: destination), expected: values.flatMap { [$0, $0, $0, 255] }) + } + @Test("Missing preset textures fail without retaining the previous effect") func missingPresetTexture() async throws { let temporary = try temporaryDirectory() @@ -368,6 +406,35 @@ struct ShaderTests { } } + @Test("A corrupt lookup texture fails cleanly and can be replaced at the same path") + func corruptPresetTextureRecovery() async throws { + let temporary = try temporaryDirectory() + defer { try? FileManager.default.removeItem(at: temporary) } + try fixtureSource("retroarch-background") + .write(to: temporary.appendingPathComponent("background.slang"), atomically: true, encoding: .utf8) + let texture = temporary.appendingPathComponent("lookup.png") + try Data("Invalid PNG content".utf8).write(to: texture) + let preset = temporary.appendingPathComponent("corrupt.slangp") + try """ + shaders = 1 + shader0 = "background.slang" + textures = "BACKGROUND" + BACKGROUND = "lookup.png" + """.write(to: preset, atomically: true, encoding: .utf8) + let shared = SharedMetalResources.shared + defer { shared.clear() } + try await shared.loadEffect(ShaderLoadRequest(source: fixtureSource("passthrough"))) + do { + try await shared.loadEffect(ShaderLoadRequest(url: preset)) + Issue.record("Preset with a corrupt lookup texture unexpectedly loaded") + } catch { + #expect(shared.effect == nil) + } + try writeTexture(rgba: solidPixel([255, 0, 0, 255]), to: texture) + try await shared.loadEffect(ShaderLoadRequest(url: preset)) + try expectPixels(render(), expected: solidPixel([0, 0, 255, 255])) + } + @Test("Native and RetroArch rendering leave excluded desktop areas transparent") func desktopScissorComposition() async throws { let shared = SharedMetalResources.shared diff --git a/Vendor/CLibrashader/BUILDING.md b/Vendor/CLibrashader/BUILDING.md index 7288d8c..cad8f48 100644 --- a/Vendor/CLibrashader/BUILDING.md +++ b/Vendor/CLibrashader/BUILDING.md @@ -1,12 +1,14 @@ # Rebuilding ScreenSlanger's librashader runtime -ScreenSlanger uses **0.12.0-screenslanger.1**, built from upstream tag +ScreenSlanger uses **0.12.0-screenslanger.2**, built from upstream tag [`librashader-v0.12.0`](https://github.com/SnowflakePowered/librashader/tree/librashader-v0.12.0) -with the two patches shipped beside this file. The upstream C header and ABI are -unchanged. This build enables only the Metal runtime. +with the three patches shipped beside this file. The upstream C header and ABI are +unchanged. This build enables only the Metal runtime. The third patch adds the +already-pinned `image` crate as a direct Metal dependency; dependency versions +are unchanged. The application includes the complete upstream source archive, its original -`Cargo.lock`, both applied patches, and the MPL-2.0 license in +`Cargo.lock`, all three applied patches, and the MPL-2.0 license in `Contents/Resources/ThirdParty/librashader/`. This directory is sufficient to recover the modified librashader source without the ScreenSlanger checkout. Cargo downloads the dependencies identified by the upstream lockfile when @@ -26,15 +28,16 @@ rustup toolchain install 1.93.0 --profile minimal Run `./scripts/setup-dependencies.sh`. It verifies the upstream archive and reviewed patch checksums, applies the patches, builds with the locked dependency graph and optimized profile, then installs the runtime into -`~/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.1/`. -The official `0.12.0` installation is preserved. Normal Xcode builds only verify, +`~/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.2/`. +The official `0.12.0` and previous `0.12.0-screenslanger.1` installations are +preserved. Normal Xcode builds only verify, copy, and sign the installed runtime; they do not download or compile Rust code. ## From an application bundle Copy this entire `librashader` directory to a writable folder, then run the following commands in that copy. `SHA256SUMS` checks the source archive -and both patches against the recorded distribution; `BUILD-INFO.txt` records +and all three patches against the recorded distribution; `BUILD-INFO.txt` records the version, toolchain, architecture, and build command. ```sh @@ -43,6 +46,7 @@ mkdir source tar -xzf librashader-v0.12.0-source.tar.gz --strip-components 1 -C source (cd source && patch --batch --forward --fuzz=0 -p1 < ../0001-skip-unused-final-target.patch) (cd source && patch --batch --forward --fuzz=0 -p1 < ../0002-compact-grayscale-luts.patch) +(cd source && patch --batch --forward --fuzz=0 -p1 < ../0003-stream-metal-lut-loading.patch) cd source MACOSX_DEPLOYMENT_TARGET=27.0 SDKROOT="$(xcrun --sdk macosx --show-sdk-path)" \ CARGO_PROFILE_OPTIMIZED_STRIP=none \ @@ -54,7 +58,7 @@ MACOSX_DEPLOYMENT_TARGET=27.0 SDKROOT="$(xcrun --sdk macosx --show-sdk-path)" \ produces misaligned Mach-O LINKEDIT string pools rejected by macOS 27's loader. It applies to both build-time procedural macros and the final library; limiting it to build dependencies leaves the final library affected. Optimization, LTO, -and the upstream dependency lockfile remain unchanged. +and the pinned dependency versions remain unchanged. The library is `target/optimized/liblibrashader_capi.dylib`. ScreenSlanger installs it as `librashader.dylib`, changes its Mach-O identity to diff --git a/Vendor/CLibrashader/NOTICE.md b/Vendor/CLibrashader/NOTICE.md index 8ef4d04..96d1638 100644 --- a/Vendor/CLibrashader/NOTICE.md +++ b/Vendor/CLibrashader/NOTICE.md @@ -1,4 +1,4 @@ -# librashader 0.12.0-screenslanger.1 +# librashader 0.12.0-screenslanger.2 The unmodified `librashader.h` header is from the official [librashader 0.12.0 macOS release](https://github.com/SnowflakePowered/librashader/releases/tag/librashader-v0.12.0). Its MIT copyright and permission notice are preserved in the header. @@ -6,8 +6,9 @@ The runtime is Copyright SnowflakePowered and contributors, licensed under MPL-2 - `0001-skip-unused-final-target.patch` avoids allocating an unused final-pass framebuffer when the shader does not require it for feedback. - `0002-compact-grayscale-luts.patch` stores textures whose red, green, and blue bytes are identical as two-channel gray and alpha textures, with Metal component swizzles preserving the original sampled RGBA values. Color textures retain their original format. +- `0003-stream-metal-lut-loading.patch` decodes and uploads Metal lookup textures sequentially, with bounded staging memory for grayscale uploads. It preserves the public packed-preset format and API, with the original color and 16-bit conversion paths retained. -The corresponding source consists of the [upstream tag librashader-v0.12.0](https://github.com/SnowflakePowered/librashader/tree/librashader-v0.12.0) ([source archive](https://github.com/SnowflakePowered/librashader/archive/refs/tags/librashader-v0.12.0.tar.gz)) plus both patches. Application bundles include that exact archive as `librashader-v0.12.0-source.tar.gz`, both patches, `SHA256SUMS`, `BUILD-INFO.txt`, and reproduction instructions in `BUILDING.md` alongside this notice. The upstream archive's SHA-256 is `4bf8cf2489d00848dcabbf2163204093776082da4217d5a5db45e4cbf335cedf`. A copy of the runtime license is in `LICENSE-MPL-2.0.md`. Keep these files together when redistributing the application. +The corresponding source consists of the [upstream tag librashader-v0.12.0](https://github.com/SnowflakePowered/librashader/tree/librashader-v0.12.0) ([source archive](https://github.com/SnowflakePowered/librashader/archive/refs/tags/librashader-v0.12.0.tar.gz)) plus all three patches. Application bundles include that exact archive as `librashader-v0.12.0-source.tar.gz`, all three patches, `SHA256SUMS`, `BUILD-INFO.txt`, and reproduction instructions in `BUILDING.md` alongside this notice. The upstream archive's SHA-256 is `4bf8cf2489d00848dcabbf2163204093776082da4217d5a5db45e4cbf335cedf`. A copy of the runtime license is in `LICENSE-MPL-2.0.md`. Keep these files together when redistributing the application. Application builds give the dynamic library a relocatable Mach-O install name and apply the app publisher's code signature. These packaging changes do not modify the implementation beyond the patches described above. diff --git a/Vendor/CLibrashader/patches/0003-stream-metal-lut-loading.patch b/Vendor/CLibrashader/patches/0003-stream-metal-lut-loading.patch new file mode 100644 index 0000000..b9dfb58 --- /dev/null +++ b/Vendor/CLibrashader/patches/0003-stream-metal-lut-loading.patch @@ -0,0 +1,453 @@ +--- a/Cargo.lock ++++ b/Cargo.lock +@@ -1835,6 +1835,7 @@ + dependencies = [ + "array-concat", + "bytemuck", ++ "image", + "librashader-common", + "librashader-pack", + "librashader-preprocess", +--- a/librashader-runtime-mtl/Cargo.toml ++++ b/librashader-runtime-mtl/Cargo.toml +@@ -14,6 +14,7 @@ + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + [dependencies] ++image = { workspace = true } + librashader-common = { path = "../librashader-common", features = ["metal"], version = "0.12.0" } + librashader-presets = { path = "../librashader-presets", version = "0.12.0" } + librashader-preprocess = { path = "../librashader-preprocess", version = "0.12.0" } +--- a/librashader-runtime-mtl/src/filter_chain.rs ++++ b/librashader-runtime-mtl/src/filter_chain.rs +@@ -11,7 +11,7 @@ + use librashader_common::map::FastHashMap; + use librashader_common::{ImageFormat, Size, Viewport}; + use librashader_presets::context::VideoDriver; +-use librashader_presets::{ShaderFeatures, ShaderPreset}; ++use librashader_presets::{ShaderFeatures, ShaderPreset, TextureConfig, TextureMeta}; + use librashader_reflect::back::msl::MslVersion; + use librashader_reflect::back::targets::MSL; + use librashader_reflect::back::{CompileReflectShader, CompileShader}; +@@ -22,7 +22,7 @@ + use librashader_reflect::reflect::ReflectShader; + use librashader_runtime::binding::BindingUtil; + use librashader_runtime::framebuffer::{FramebufferInit, FramebufferPool}; +-use librashader_runtime::image::{ImageError, LoadedTexture, UVDirection, BGRA8}; ++use librashader_runtime::image::{LoadedTexture, UVDirection, BGRA8}; + use librashader_runtime::quad::QuadType; + use librashader_runtime::render_target::RenderTarget; + use librashader_runtime::scaling::ScaleFramebuffer; +@@ -34,14 +34,13 @@ + MTLCommandBuffer, MTLCommandEncoder, MTLCommandQueue, MTLDevice, MTLLoadAction, MTLPixelFormat, + MTLRenderPassDescriptor, MTLResource, MTLStoreAction, MTLTexture, + }; +-use rayon::prelude::*; + use std::collections::VecDeque; + use std::fmt::{Debug, Formatter}; + use std::path::Path; + + mod compile { + use super::*; +- use librashader_pack::{PassResource, TextureResource}; ++ use librashader_pack::PassResource; + + #[cfg(feature = "nightly")] + pub type ShaderPassMeta = +@@ -52,15 +51,14 @@ + ShaderPassArtifact + Send>>; + + #[cfg_attr(feature = "nightly", define_opaque(ShaderPassMeta))] +- pub fn compile_passes( ++ pub fn compile_passes<'a>( + shaders: Vec, +- textures: &[TextureResource], ++ textures: impl Iterator, + ) -> Result<(Vec, ShaderSemantics), FilterChainError> { +- let (passes, semantics) = MSL::compile_preset_passes::< +- SpirvCompilation, +- SpirvCross, +- FilterChainError, +- >(shaders, textures.iter().map(|t| &t.meta))?; ++ let (passes, semantics) = ++ MSL::compile_preset_passes::( ++ shaders, textures, ++ )?; + Ok((passes, semantics)) + } + } +@@ -68,6 +66,22 @@ + use compile::{compile_passes, ShaderPassMeta}; + use librashader_pack::{ShaderPresetPack, TextureResource}; + use librashader_runtime::parameters::RuntimeParameters; ++ ++/// Keep disk LUTs unexpanded until Metal can upload them; packed LUTs retain ++/// their public RGBA representation and unchanged loading API. ++enum LutSources { ++ Files(Vec), ++ Packed(Vec), ++} ++ ++impl LutSources { ++ fn metadata(&self) -> Box + '_> { ++ match self { ++ Self::Files(files) => Box::new(files.iter().map(|t| &t.meta)), ++ Self::Packed(textures) => Box::new(textures.iter().map(|t| &t.meta)), ++ } ++ } ++} + + /// A Metal filter chain. + pub struct FilterChainMetal { +@@ -126,8 +140,13 @@ + queue: &ProtocolObject, + options: Option<&FilterChainOptionsMetal>, + ) -> error::Result { +- let preset = ShaderPresetPack::load_from_preset::(preset)?; +- Self::load_from_pack(preset, queue, options) ++ let cmd = queue ++ .commandBuffer() ++ .ok_or(FilterChainError::FailedToCreateCommandBuffer)?; ++ let chain = Self::load_from_preset_deferred(preset, queue, &cmd, options)?; ++ cmd.commit(); ++ cmd.waitUntilCompleted(); ++ Ok(chain) + } + + /// Load a filter chain from a pre-parsed `ShaderPreset`. +@@ -141,7 +160,7 @@ + .ok_or(FilterChainError::FailedToCreateCommandBuffer)?; + + let filter_chain = +- Self::load_from_pack_deferred_internal(preset, queue.device(), &cmd, options)?; ++ Self::load_from_pack_deferred_internal(preset, queue.device(), &cmd, options, None)?; + + cmd.commit(); + cmd.waitUntilCompleted(); +@@ -152,25 +171,38 @@ + fn load_luts( + device: &ProtocolObject, + cmd: &ProtocolObject, +- textures: Vec, ++ textures: LutSources, + ) -> error::Result> { + let mut luts = FastHashMap::default(); +- + let mipmapper = cmd + .blitCommandEncoder() + .ok_or(FilterChainError::FailedToCreateCommandBuffer)?; +- +- let textures = textures +- .into_par_iter() +- .map(|texture| LoadedTexture::::from_texture(texture, UVDirection::TopLeft)) +- .collect::>, ImageError>>()?; +- for (index, LoadedTexture { meta, image }) in textures.into_iter().enumerate() { +- let texture = LutTexture::new(device, image, &meta, &mipmapper)?; +- luts.insert(index, texture); +- } +- ++ // Decode and upload one image at a time. Large LUT sets must not retain ++ // every decoded image alongside the compiler's temporary allocations. ++ let result = (|| -> error::Result<_> { ++ match textures { ++ LutSources::Files(files) => { ++ for (index, texture) in files.into_iter().enumerate() { ++ let lut = ++ LutTexture::load(device, &texture.path, &texture.meta, &mipmapper)?; ++ luts.insert(index, lut); ++ } ++ } ++ LutSources::Packed(textures) => { ++ for (index, texture) in textures.into_iter().enumerate() { ++ let LoadedTexture { meta, image } = ++ LoadedTexture::::from_texture(texture, UVDirection::TopLeft)?; ++ let lut = LutTexture::new(device, image, &meta, &mipmapper)?; ++ luts.insert(index, lut); ++ } ++ } ++ } ++ Ok(luts) ++ })(); ++ // Missing/corrupt disk images and allocation failures must also close ++ // the encoder before the unsubmitted command buffer is discarded. + mipmapper.endEncoding(); +- Ok(luts) ++ result + } + + fn init_passes( +@@ -280,13 +312,14 @@ + /// The caller is responsible for ending the command buffer and immediately submitting it to a + /// graphics queue. The command buffer must be completely executed before calling [`frame`](Self::frame). + pub fn load_from_preset_deferred( +- preset: ShaderPreset, ++ mut preset: ShaderPreset, + queue: &ProtocolObject, + cmd: &ProtocolObject, + options: Option<&FilterChainOptionsMetal>, + ) -> error::Result { ++ let textures = std::mem::take(&mut preset.textures); + let preset = ShaderPresetPack::load_from_preset::(preset)?; +- Self::load_from_pack_deferred(preset, queue, cmd, options) ++ Self::load_from_pack_deferred_internal(preset, queue.device(), cmd, options, Some(textures)) + } + + /// Load a filter chain from a pre-parsed `ShaderPreset`, deferring and GPU-side initialization +@@ -302,7 +335,7 @@ + cmd: &ProtocolObject, + options: Option<&FilterChainOptionsMetal>, + ) -> error::Result { +- Self::load_from_pack_deferred_internal(preset, queue.device(), &cmd, options) ++ Self::load_from_pack_deferred_internal(preset, queue.device(), &cmd, options, None) + } + + /// Load a filter chain from a pre-parsed `ShaderPreset`, deferring and GPU-side initialization +@@ -317,14 +350,19 @@ + device: Retained>, + cmd: &ProtocolObject, + options: Option<&FilterChainOptionsMetal>, ++ texture_paths: Option>, + ) -> error::Result { + let config = RuntimeParameters::new(&preset); +- let (passes, semantics) = compile_passes(preset.passes, &preset.textures)?; ++ let textures = match texture_paths { ++ Some(paths) => LutSources::Files(paths), ++ None => LutSources::Packed(preset.textures), ++ }; ++ let (passes, semantics) = compile_passes(preset.passes, textures.metadata())?; + + let filters = Self::init_passes(&device, passes, &semantics)?; + + let samplers = SamplerSet::new(&device)?; +- let luts = FilterChainMetal::load_luts(&device, &cmd, preset.textures)?; ++ let luts = FilterChainMetal::load_luts(&device, &cmd, textures)?; + let framebuffer_gen = || { + Ok::<_, error::FilterChainError>(OwnedTexture::new( + &device, +--- a/librashader-runtime-mtl/src/luts.rs ++++ b/librashader-runtime-mtl/src/luts.rs +@@ -1,7 +1,9 @@ + use crate::error::{FilterChainError, Result}; + use crate::texture::InputTexture; ++use image::DynamicImage; ++use librashader_common::Size; + use librashader_presets::TextureMeta; +-use librashader_runtime::image::{Image, BGRA8}; ++use librashader_runtime::image::{Image, PixelFormat, BGRA8}; + use librashader_runtime::scaling::MipmapSize; + use objc2::runtime::ProtocolObject; + use objc2_metal::{ +@@ -10,9 +12,48 @@ + MTLTextureUsage, + }; + use std::ffi::c_void; ++use std::path::Path; + use std::ptr::NonNull; + + pub(crate) struct LutTexture(InputTexture); ++ ++/// Disk grayscale images remain in their decoded layout until upload; all other ++/// formats use the existing RGBA8 conversion and BGRA swizzle. ++enum LutPixels { ++ Luma(Vec), ++ LumaAlpha(Vec), ++ Bgra(Vec), ++} ++ ++struct DecodedLut { ++ size: Size, ++ pixels: LutPixels, ++} ++ ++impl DecodedLut { ++ fn from_image(image: DynamicImage) -> Self { ++ let size = Size::new(image.width(), image.height()); ++ let pixels = match image { ++ DynamicImage::ImageLuma8(image) => LutPixels::Luma(image.into_raw()), ++ DynamicImage::ImageLumaA8(image) => LutPixels::LumaAlpha(image.into_raw()), ++ image => { ++ let mut bytes = image.into_rgba8().into_raw(); ++ BGRA8::convert(&mut bytes); ++ return Self::from_bgra(size, bytes); ++ } ++ }; ++ Self { size, pixels } ++ } ++ ++ fn from_bgra(size: Size, mut bytes: Vec) -> Self { ++ let pixels = if compact_grayscale(&mut bytes) { ++ LutPixels::LumaAlpha(bytes) ++ } else { ++ LutPixels::Bgra(bytes) ++ }; ++ Self { size, pixels } ++ } ++} + + impl AsRef for LutTexture { + fn as_ref(&self) -> &InputTexture { +@@ -23,14 +64,35 @@ + impl LutTexture { + pub fn new( + device: &ProtocolObject, +- mut image: Image, ++ image: Image, + config: &TextureMeta, + mipmapper: &ProtocolObject, + ) -> Result { +- // Preserve exactly grayscale LUTs in RG8 (luminance, alpha). Keeping an +- // explicit alpha channel also preserves transparent clamp-to-border samples. +- // Reuse the decoded allocation in place; it is dropped after upload below. +- let grayscale = compact_grayscale(&mut image.bytes); ++ Self::upload( ++ device, ++ DecodedLut::from_bgra(image.size, image.bytes), ++ config, ++ mipmapper, ++ ) ++ } ++ ++ pub fn load( ++ device: &ProtocolObject, ++ path: &Path, ++ config: &TextureMeta, ++ mipmapper: &ProtocolObject, ++ ) -> Result { ++ let image = DecodedLut::from_image(image::open(path)?); ++ Self::upload(device, image, config, mipmapper) ++ } ++ ++ fn upload( ++ device: &ProtocolObject, ++ image: DecodedLut, ++ config: &TextureMeta, ++ mipmapper: &ProtocolObject, ++ ) -> Result { ++ let grayscale = !matches!(&image.pixels, LutPixels::Bgra(_)); + let descriptor = unsafe { + let descriptor = + MTLTextureDescriptor::texture2DDescriptorWithPixelFormat_width_height_mipmapped( +@@ -76,23 +138,46 @@ + .newTextureWithDescriptor(&descriptor) + .ok_or(FilterChainError::FailedToCreateTexture)?; + +- unsafe { ++ let bytes_per_row = (if grayscale { 2 } else { 4 }) * image.size.width as usize; ++ let upload = |bytes: &[u8], y: usize, height: usize| unsafe { + let region = MTLRegion { +- origin: MTLOrigin { x: 0, y: 0, z: 0 }, ++ origin: MTLOrigin { x: 0, y, z: 0 }, + size: MTLSize { + width: image.size.width as usize, +- height: image.size.height as usize, ++ height, + depth: 1, + }, + }; +- + texture.replaceRegion_mipmapLevel_withBytes_bytesPerRow( + region, + 0, +- // SAFETY: replaceRegion withBytes is const. +- NonNull::new_unchecked(image.bytes.as_slice().as_ptr() as *mut c_void), +- (if grayscale { 2 } else { 4 }) * image.size.width as usize, +- ) ++ // Metal copies the bytes before returning; no CPU image survives upload. ++ NonNull::new_unchecked(bytes.as_ptr() as *mut c_void), ++ bytes_per_row, ++ ); ++ }; ++ match image.pixels { ++ LutPixels::Luma(bytes) => { ++ // Bounded strips avoid a full RGBA or RG decoded-image allocation. ++ const ROWS_PER_UPLOAD: usize = 64; ++ let width = image.size.width as usize; ++ let mut strip = ++ vec![0; bytes_per_row * ROWS_PER_UPLOAD.min(image.size.height as usize)]; ++ for (index, rows) in bytes.chunks(width * ROWS_PER_UPLOAD).enumerate() { ++ for (gray, output) in rows.iter().zip(strip.chunks_exact_mut(2)) { ++ output[0] = *gray; ++ output[1] = 255; ++ } ++ upload( ++ &strip[..rows.len() * 2], ++ index * ROWS_PER_UPLOAD, ++ rows.len() / width, ++ ); ++ } ++ } ++ LutPixels::LumaAlpha(bytes) | LutPixels::Bgra(bytes) => { ++ upload(&bytes, 0, image.size.height as usize); ++ } + } + + if config.mipmap && texture.mipmapLevelCount() > 1 { +@@ -126,6 +211,68 @@ + mod tests { + use super::compact_grayscale; + ++ fn as_bgra(image: super::DecodedLut) -> Vec { ++ match image.pixels { ++ super::LutPixels::Luma(bytes) => bytes ++ .into_iter() ++ .flat_map(|gray| [gray, gray, gray, 255]) ++ .collect(), ++ super::LutPixels::LumaAlpha(bytes) => bytes ++ .chunks_exact(2) ++ .flat_map(|p| [p[0], p[0], p[0], p[1]]) ++ .collect(), ++ super::LutPixels::Bgra(bytes) => bytes, ++ } ++ } ++ ++ #[test] ++ fn png_decode_matches_existing_rgba_conversion_for_all_layouts() { ++ use image::{DynamicImage, ImageBuffer, ImageFormat}; ++ use librashader_runtime::image::{PixelFormat, BGRA8}; ++ let images = [ ++ DynamicImage::ImageLuma8(ImageBuffer::from_raw(2, 1, vec![0, 127]).unwrap()), ++ DynamicImage::ImageLumaA8(ImageBuffer::from_raw(2, 1, vec![32, 0, 128, 199]).unwrap()), ++ DynamicImage::ImageRgb8( ++ ImageBuffer::from_raw(2, 1, vec![7, 8, 9, 10, 10, 10]).unwrap(), ++ ), ++ DynamicImage::ImageRgba8( ++ ImageBuffer::from_raw(2, 1, vec![7, 8, 9, 100, 10, 10, 10, 23]).unwrap(), ++ ), ++ DynamicImage::ImageLuma16(ImageBuffer::from_raw(2, 1, vec![1, 32768]).unwrap()), ++ DynamicImage::ImageLumaA16( ++ ImageBuffer::from_raw(2, 1, vec![32, 0, 32768, 65535]).unwrap(), ++ ), ++ DynamicImage::ImageRgb16( ++ ImageBuffer::from_raw(2, 1, vec![100, 32768, 65535, 1000, 1000, 1000]).unwrap(), ++ ), ++ DynamicImage::ImageRgba16( ++ ImageBuffer::from_raw(2, 1, vec![100, 32768, 65535, 0, 1000, 1000, 1000, 65535]) ++ .unwrap(), ++ ), ++ ]; ++ for image in images { ++ let mut encoded = std::io::Cursor::new(Vec::new()); ++ image.write_to(&mut encoded, ImageFormat::Png).unwrap(); ++ let decoded = image::load_from_memory(encoded.get_ref()).unwrap(); ++ let mut expected = decoded.to_rgba8().into_raw(); ++ BGRA8::convert(&mut expected); ++ let actual = super::DecodedLut::from_image(decoded); ++ assert_eq!(actual.size, librashader_common::Size::new(2, 1)); ++ assert_eq!(as_bgra(actual), expected); ++ } ++ } ++ ++ #[test] ++ fn disk_grayscale_retains_compact_decoded_bytes() { ++ let image = image::DynamicImage::ImageLuma8( ++ image::ImageBuffer::from_raw(3, 1, vec![0, 128, 255]).unwrap(), ++ ); ++ let decoded = super::DecodedLut::from_image(image); ++ assert!( ++ matches!(decoded.pixels, super::LutPixels::Luma(ref bytes) if bytes == &[0, 128, 255]) ++ ); ++ } ++ + #[test] + fn grayscale_preserves_color_and_alpha() { + let mut bytes = vec![0, 0, 0, 255, 127, 127, 127, 128, 255, 255, 255, 0]; diff --git a/scripts/bundle-shader-runtimes.sh b/scripts/bundle-shader-runtimes.sh index 75e7fc2..77f82b6 100755 --- a/scripts/bundle-shader-runtimes.sh +++ b/scripts/bundle-shader-runtimes.sh @@ -11,7 +11,7 @@ set -euo pipefail : "${TARGET_TEMP_DIR:?This script is an Xcode build phase.}" slang_version="2026.18" -librashader_version="0.12.0-screenslanger.1" +librashader_version="0.12.0-screenslanger.2" tools_directory="$HOME/Library/Application Support/ScreenSlanger/Tools" slang_source="$tools_directory/slang/$slang_version" librashader_directory="$tools_directory/librashader/$librashader_version" @@ -51,6 +51,7 @@ cp "$SRCROOT/Vendor/CLibrashader/NOTICE.md" "$notices_destination/librashader/NO cp "$SRCROOT/Vendor/CLibrashader/BUILDING.md" "$notices_destination/librashader/BUILDING.md" for source_file in librashader-v0.12.0-source.tar.gz \ 0001-skip-unused-final-target.patch 0002-compact-grayscale-luts.patch \ + 0003-stream-metal-lut-loading.patch \ BUILD-INFO.txt SHA256SUMS; do cp "$librashader_directory/$source_file" "$notices_destination/librashader/$source_file" done diff --git a/scripts/setup-dependencies.sh b/scripts/setup-dependencies.sh index 7953f53..ca84aa4 100755 --- a/scripts/setup-dependencies.sh +++ b/scripts/setup-dependencies.sh @@ -8,13 +8,13 @@ fi project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" slang_version="2026.18" -librashader_version="0.12.0-screenslanger.1" +librashader_version="0.12.0-screenslanger.2" librashader_upstream_version="0.12.0" librashader_source_sha256="4bf8cf2489d00848dcabbf2163204093776082da4217d5a5db45e4cbf335cedf" librashader_rust_version="1.93.0" -librashader_patches=(0001-skip-unused-final-target.patch 0002-compact-grayscale-luts.patch) +librashader_patches=(0001-skip-unused-final-target.patch 0002-compact-grayscale-luts.patch 0003-stream-metal-lut-loading.patch) # Reviewed patches against the checksum-pinned source archive, not upstream binaries. -librashader_patch_sha256=(f5a9c09c0f7a72059eb536fbd63eacbc513bea5fe964e70f3a122ae61bfd90fb 2fd5bd08afabf7f0ee6c8329fcfda778b6460001d7de50145c32af329138fe5e) +librashader_patch_sha256=(f5a9c09c0f7a72059eb536fbd63eacbc513bea5fe964e70f3a122ae61bfd90fb 2fd5bd08afabf7f0ee6c8329fcfda778b6460001d7de50145c32af329138fe5e 3e3c574a0b8940f04502bece08442ab297bc1b9a7de89dff412b51566de9acfe) # Checksums published with the official v2026.18 GitHub release assets. # https://github.com/shader-slang/slang/releases/tag/v2026.18 case "$(uname -m)" in @@ -72,7 +72,7 @@ fi ln -sfn "$slang_version" "$slang_root/current" # Compile our Metal-only runtime during setup. Normal Xcode builds only copy it. -# A distinct version preserves the official 0.12.0 installation for comparisons. +# A distinct version preserves official and previous local runtimes for comparisons. librashader_root="$HOME/Library/Application Support/ScreenSlanger/Tools/librashader" librashader_directory="$librashader_root/$librashader_version" patch_directory="$project_dir/Vendor/CLibrashader/patches" @@ -88,6 +88,7 @@ Upstream tag: librashader-v$librashader_upstream_version Upstream archive SHA-256: $librashader_source_sha256 Patch 1 SHA-256: ${librashader_patch_sha256[0]} Patch 2 SHA-256: ${librashader_patch_sha256[1]} +Patch 3 SHA-256: ${librashader_patch_sha256[2]} Rust toolchain: $librashader_rust_version Architecture: $librashader_arch MACOSX_DEPLOYMENT_TARGET: 27.0 diff --git a/scripts/shader-runtimes-inputs.xcfilelist b/scripts/shader-runtimes-inputs.xcfilelist index 50a104e..fedf600 100644 --- a/scripts/shader-runtimes-inputs.xcfilelist +++ b/scripts/shader-runtimes-inputs.xcfilelist @@ -7,10 +7,11 @@ $(HOME)/Library/Application Support/ScreenSlanger/Tools/slang/2026.18/bin/slangc $(HOME)/Library/Application Support/ScreenSlanger/Tools/slang/2026.18/lib/ $(HOME)/Library/Application Support/ScreenSlanger/Tools/slang/2026.18/LICENSE $(HOME)/Library/Application Support/ScreenSlanger/Tools/slang/2026.18/LICENSES/ -$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.1/librashader.dylib -$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.1/RUNTIME.sha256 -$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.1/SHA256SUMS -$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.1/BUILD-INFO.txt -$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.1/librashader-v0.12.0-source.tar.gz -$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.1/0001-skip-unused-final-target.patch -$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.1/0002-compact-grayscale-luts.patch +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.2/librashader.dylib +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.2/RUNTIME.sha256 +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.2/SHA256SUMS +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.2/BUILD-INFO.txt +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.2/librashader-v0.12.0-source.tar.gz +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.2/0001-skip-unused-final-target.patch +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.2/0002-compact-grayscale-luts.patch +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.2/0003-stream-metal-lut-loading.patch diff --git a/scripts/shader-runtimes-outputs.xcfilelist b/scripts/shader-runtimes-outputs.xcfilelist index b723fff..165a89b 100644 --- a/scripts/shader-runtimes-outputs.xcfilelist +++ b/scripts/shader-runtimes-outputs.xcfilelist @@ -73,5 +73,6 @@ $(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/ThirdParty/librashader/ $(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/ThirdParty/librashader/librashader-v0.12.0-source.tar.gz $(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/ThirdParty/librashader/0001-skip-unused-final-target.patch $(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/ThirdParty/librashader/0002-compact-grayscale-luts.patch -// Pinned Slang 2026.18 + librashader 0.12.0-screenslanger.1 bundle contents. +$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/ThirdParty/librashader/0003-stream-metal-lut-loading.patch +// Pinned Slang 2026.18 + librashader 0.12.0-screenslanger.2 bundle contents. // Xcode requires every file and directory explicitly for sandboxed script outputs.