From 6584e4da5c0ae29ddee5496bb7a604fa39fdcd21 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 17 Sep 2026 20:54:17 -0700 Subject: [PATCH] perf: isolate RetroArch compiler state in a bundled helper --- CONTRIBUTING.md | 25 +- README.md | 8 +- ScreenSlanger/librashader.swift | 48 +- ScreenSlanger/renderer.swift | 2 +- ScreenSlangerTests/CompilerProcessTests.swift | 46 + ScreenSlangerTests/RuntimeBundleTests.swift | 71 +- ScreenSlangerTests/ShaderTests.swift | 36 + Vendor/CLibrashader/BUILDING.md | 33 +- Vendor/CLibrashader/NOTICE.md | 9 +- .../0004-isolate-retroarch-compiler.patch | 1185 +++++++++++++++++ Vendor/CLibrashader/screenslanger.h | 9 + Vendor/CLibrashader/shim.h | 1 + scripts/bundle-shader-runtimes.sh | 15 +- scripts/setup-dependencies.sh | 18 +- scripts/shader-runtimes-inputs.xcfilelist | 18 +- scripts/shader-runtimes-outputs.xcfilelist | 4 +- 16 files changed, 1472 insertions(+), 56 deletions(-) create mode 100644 Vendor/CLibrashader/patches/0004-isolate-retroarch-compiler.patch create mode 100644 Vendor/CLibrashader/screenslanger.h diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b28be7a..f41722d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,23 +12,34 @@ 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.2**, 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.3**, with the local source patches described below. The header remains the original upstream header fetched during the ScreenSlanger Metal backend integration: | Local file | Origin | | --- | --- | | `Vendor/CLibrashader/librashader.h` | Unmodified `librashader.h` extracted from the official [`librashader-aarch64-macos-v0.12.0-optimized.zip`](https://github.com/SnowflakePowered/librashader/releases/download/librashader-v0.12.0/librashader-aarch64-macos-v0.12.0-optimized.zip) release asset. The MIT copyright and permission notice remain in the header. | | `Vendor/CLibrashader/LICENSE-MPL-2.0.md` | Unmodified [`LICENSE.md`](https://github.com/SnowflakePowered/librashader/blob/librashader-v0.12.0/LICENSE.md) from the pinned upstream tag. It covers the runtime implementation, not the MIT C header. | -| `Vendor/CLibrashader/shim.h` | Written for ScreenSlanger. Enables `LIBRA_RUNTIME_METAL` before importing the official header. | +| `Vendor/CLibrashader/shim.h` | Written for ScreenSlanger. Enables `LIBRA_RUNTIME_METAL` before importing the official header and the separate local extension header. | +| `Vendor/CLibrashader/screenslanger.h` | Written for ScreenSlanger. Declares the local helper-aware C entry point implemented by patch 4 without modifying the vendored upstream header. | | `Vendor/CLibrashader/module.modulemap` | Written for ScreenSlanger. Exposes the header as the `CLibrashader` Clang module for Swift. | | `Vendor/CLibrashader/NOTICE.md` | Written for ScreenSlanger. Records licensing, corresponding-source links, and integration constraints. | | `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. | +| `Vendor/CLibrashader/patches/0004-isolate-retroarch-compiler.patch` | ScreenSlanger's MPL-2.0 modification adding the `librashader-compiler` helper and a C entry point accepting its explicit path. Moves GLSL compilation into a short-lived process so compiler-global allocations are reclaimed without violating glslang's process-lifetime API. The helper-aware path preprocesses passes sequentially, avoids a permanent Rayon pool, and releases GLSL source strings after pipeline creation. A bounded SPIR-V response cache preserves warm reload performance. | -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. +The dynamic library and helper executable are **not committed to the repository**. The setup script downloads the pinned upstream source archive, verifies its SHA-256 digest and all four reviewed patch digests, applies the patches, and builds both outputs together: -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. +```sh +cargo +1.93.0 build --locked --profile optimized \ + --package librashader-capi --package librashader-reflect \ + --bin librashader-compiler --lib --no-default-features \ + --features librashader-capi/runtime-metal,librashader-reflect/glslang-in +``` + +The build uses `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 binaries; optimization and LTO remain enabled. The source patches include the corresponding manifest and lockfile changes. Setup downloads any missing Cargo dependencies and compiles them with `--locked`; the normal Xcode build never invokes Cargo or accesses the network. + +Setup installs `librashader.dylib` and `librashader-compiler` into `~/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3/`, alongside their source archive, applied patches, license, notice, and build instructions. Existing official and previous local runtime 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 both installed binaries; `SHA256SUMS` verifies their source archive and patches. The pinned corresponding-source checksums are: @@ -38,10 +49,13 @@ The pinned corresponding-source checksums are: | `0001-skip-unused-final-target.patch` | `f5a9c09c0f7a72059eb536fbd63eacbc513bea5fe964e70f3a122ae61bfd90fb` | | `0002-compact-grayscale-luts.patch` | `2fd5bd08afabf7f0ee6c8329fcfda778b6460001d7de50145c32af329138fe5e` | | `0003-stream-metal-lut-loading.patch` | `3e3c574a0b8940f04502bece08442ab297bc1b9a7de89dff412b51566de9acfe` | +| `0004-isolate-retroarch-compiler.patch` | `1956179bfa7f89e4318c1aa3982656d24ea63e6dffe9494d87ad64a3f27dac8f` | 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, 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. +The app build copies the library into `Contents/Frameworks/`, gives it a relocatable install name, and copies the helper into `Contents/Helpers/librashader-compiler`. Both binaries are signed with the app's build identity before the application is signed. It also bundles the exact upstream archive, all four 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 and helper without this checkout. The installation's binary hashes are verified before packaging and are not bundled because signing changes their bytes. The vendored C header keeps its own MIT notice. + +Unhosted tests and probes can override the runtime with `LIBRASHADER_PATH` and the helper with `LIBRASHADER_COMPILER_PATH`; otherwise the helper is located beside the selected runtime. Applications always use the embedded library and exact bundled helper path, ignoring those overrides. A missing helper is an error, with no in-process GLSL compiler fallback. The local `screenslanger_mtl_filter_chain_create_with_compiler` symbol adds this explicit helper path to creation while preserving the upstream header and existing entry points. ## Updating librashader @@ -65,6 +79,7 @@ The app layout is: ScreenSlanger.app/Contents/ Helpers/Slang.app/Contents/MacOS/slangc Helpers/Slang.app/Contents/lib/ # Compiler libraries, plugins, and standard modules + Helpers/librashader-compiler # Short-lived RetroArch GLSL frontend Frameworks/librashader.dylib Resources/ThirdParty/Slang/ # Upstream LICENSE, LICENSES, and source notice Resources/ThirdParty/librashader/ # MPL-2.0 license, source archive, patches, hashes, rebuild instructions diff --git a/README.md b/README.md index 4be5253..6e2a8b3 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,9 @@ 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.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. +The script also builds **librashader 0.12.0-screenslanger.3** 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. A bundled `librashader-compiler` helper compiles GLSL in a separate, short-lived process so its compiler state is released after loading. Setup uses Rust 1.93.0, pinned Cargo dependencies, and optimized builds. It downloads Cargo dependencies and builds the Metal runtime and helper once, installing both into `~/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3` 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). +This runtime handles RetroArch presets, including multiple passes, custom vertex stages, reflected uniforms, textures, and filtering. The app includes the runtime and compiler helper, so Homebrew, `glslang`, and `spirv-cross` are not required. It also 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). Open `ScreenSlanger.xcodeproj`, select the **ScreenSlanger** scheme and **My Mac** destination, then build and run. The **Bundle Shader Runtimes** build phase verifies the installed runtime, copies the pinned tools, licenses, and corresponding source into the app, and signs the nested executables and libraries before Xcode signs the app. Xcode builds never download tools or compile Rust. There are no Swift packages to resolve. @@ -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.2 | Patched RetroArch Metal rendering runtime | +| [librashader](https://github.com/SnowflakePowered/librashader/releases/tag/librashader-v0.12.0) | 0.12.0-screenslanger.3 | RetroArch Metal runtime and compiler helper | | [Rust](https://www.rust-lang.org/) | 1.93.0 | Build-time compiler for librashader; not required to run the app | To inspect installed versions: @@ -58,7 +58,7 @@ To inspect installed versions: "$HOME/Library/Application Support/ScreenSlanger/Tools/slang/current/bin/slangc" -version ``` -Unhosted tests and development probes can override their tools with `SLANG_PATH` and `LIBRASHADER_PATH`. The application uses its bundled tools; an incomplete app reports an error instead of depending on the recipient's development environment. +Unhosted tests and development probes can override their tools with `SLANG_PATH`, `LIBRASHADER_PATH`, and `LIBRASHADER_COMPILER_PATH`. Without a compiler override, they look for `librashader-compiler` beside the selected librashader runtime. The application always uses its bundled runtime and helper, ignoring those environment overrides. An incomplete app reports an error instead of depending on the recipient's development environment or retaining an in-process GLSL compiler. Run the native Swift Testing suite after updating the compilers. In Xcode, use **Product → Test** (⌘U), or run: diff --git a/ScreenSlanger/librashader.swift b/ScreenSlanger/librashader.swift index 3e34c64..c6dac6d 100644 --- a/ScreenSlanger/librashader.swift +++ b/ScreenSlanger/librashader.swift @@ -6,6 +6,7 @@ import Metal enum RetroArchRuntimeError: Error, LocalizedError { case unavailable(String) case incompleteBundle(String) + case compilerUnavailable case incompatibleVersion(abi: Int, api: Int) case runtime(String) case frameInFlight @@ -16,7 +17,9 @@ enum RetroArchRuntimeError: Error, LocalizedError { case .unavailable(let detail): return "librashader could not be loaded. Run scripts/setup-dependencies.sh or set LIBRASHADER_PATH to librashader.dylib. \(detail)" case .incompleteBundle(let detail): - return "The app's bundled RetroArch runtime is missing or could not load. Reinstall a complete ScreenSlanger.app. \(detail)" + return "The app's bundled RetroArch runtime or compiler helper is missing or could not load. Reinstall a complete ScreenSlanger.app. \(detail)" + case .compilerUnavailable: + return "The RetroArch compiler helper is missing. Run scripts/setup-dependencies.sh or set LIBRASHADER_COMPILER_PATH to librashader-compiler." case .incompatibleVersion(let abi, let api): return "Incompatible librashader (ABI \(abi), API \(api)); ScreenSlanger requires ABI 2 and API 5 or later." case .runtime(let message): return "RetroArch shader: \(message)" @@ -26,6 +29,29 @@ enum RetroArchRuntimeError: Error, LocalizedError { } } +/// App bundles always use their own short-lived frontend. Development probes can +/// override it, or keep it next to the selected runtime installation. +enum RetroArchCompilerLocation { + static func findCompiler( + runtimeURL: URL, + bundle: Bundle = .main, + environment: [String: String] = ProcessInfo.processInfo.environment + ) -> String? { + let paths: [String] + if bundle.bundleURL.pathExtension.lowercased() == "app" { + paths = [bundle.bundleURL.appendingPathComponent("Contents/Helpers/librashader-compiler").path] + } else { + paths = [environment["LIBRASHADER_COMPILER_PATH"], + runtimeURL.deletingLastPathComponent().appendingPathComponent("librashader-compiler").path] + .compactMap { $0 } + } + return paths.first { path in + FileManager.default.isExecutableFile(atPath: path) + && (try? URL(fileURLWithPath: path).resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true + } + } +} + /// One chain per display: feedback and history must never be shared across displays. /// The Metal runtime is not thread safe. All C calls, including creation and destruction, /// run on one dedicated OS thread. The wrapper transfers only immutable metadata and @@ -237,7 +263,9 @@ private final class LibraState { } var chainOptions = filter_chain_mtl_opt_t() chainOptions.version = 5 - try api.check(api.chainCreate(&preset, commandQueue, &chainOptions, &chain)) + try api.check(api.compilerPath.withCString { + api.chainCreate(&preset, commandQueue, &chainOptions, $0, &chain) + }) guard chain != nil else { throw RetroArchRuntimeError.runtime("Runtime returned no filter chain.") } } @@ -296,11 +324,12 @@ private final class LibraState { /// Every instance owns its dlopen reference until the filter chain has been destroyed. private final class LibraAPI { private let handle: UnsafeMutableRawPointer + let compilerPath: String let presetCreate: PFN_libra_preset_create_with_options let presetFree: PFN_libra_preset_free let presetParameters: PFN_libra_preset_get_runtime_params let presetParametersFree: PFN_libra_preset_free_runtime_params - let chainCreate: PFN_libra_mtl_filter_chain_create + let chainCreate: PFN_screenslanger_mtl_filter_chain_create_with_compiler let chainFrame: PFN_libra_mtl_filter_chain_frame let chainSetParameter: PFN_libra_mtl_filter_chain_set_param let chainFree: PFN_libra_mtl_filter_chain_free @@ -319,21 +348,23 @@ 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.2/librashader.dylib").path + FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3/librashader.dylib").path ].compactMap { $0 } } var loaded: UnsafeMutableRawPointer? + var loadedPath: String? var failures: [String] = [] for path in paths where FileManager.default.fileExists(atPath: path) { // The Rust runtime may retain global worker threads. Keep its executable // mapping alive even after the last chain releases its dlopen reference. if let candidate = dlopen(path, RTLD_NOW | RTLD_LOCAL | RTLD_NODELETE) { loaded = candidate + loadedPath = path break } if let error = dlerror() { failures.append(String(cString: error)) } } - guard let loaded else { + guard let loaded, let loadedPath else { let detail = failures.joined(separator: "\n") if isAppBundle { throw RetroArchRuntimeError.incompleteBundle(detail) } throw RetroArchRuntimeError.unavailable(detail) @@ -344,6 +375,11 @@ private final class LibraAPI { return unsafeBitCast(pointer, to: T.self) } do { + guard let compiler = RetroArchCompilerLocation.findCompiler(runtimeURL: URL(fileURLWithPath: loadedPath)) else { + if isAppBundle { throw RetroArchRuntimeError.incompleteBundle("Missing librashader-compiler.") } + throw RetroArchRuntimeError.compilerUnavailable + } + compilerPath = compiler let abi: PFN_libra_instance_abi_version = try symbol("libra_instance_abi_version") let api: PFN_libra_instance_api_version = try symbol("libra_instance_api_version") guard abi() == 2, api() >= 5 else { throw RetroArchRuntimeError.incompatibleVersion(abi: abi(), api: api()) } @@ -351,7 +387,7 @@ private final class LibraAPI { presetFree = try symbol("libra_preset_free") presetParameters = try symbol("libra_preset_get_runtime_params") presetParametersFree = try symbol("libra_preset_free_runtime_params") - chainCreate = try symbol("libra_mtl_filter_chain_create") + chainCreate = try symbol("screenslanger_mtl_filter_chain_create_with_compiler") chainFrame = try symbol("libra_mtl_filter_chain_frame") chainSetParameter = try symbol("libra_mtl_filter_chain_set_param") chainFree = try symbol("libra_mtl_filter_chain_free") diff --git a/ScreenSlanger/renderer.swift b/ScreenSlanger/renderer.swift index 01a2519..41732ad 100644 --- a/ScreenSlanger/renderer.swift +++ b/ScreenSlanger/renderer.swift @@ -85,7 +85,7 @@ final class SharedMetalResources { let queue = self.commandQueue let loaded = try await withTaskCancellationHandler { try await Task.detached(priority: .userInitiated) { - try build(request, device, queue, token) + try autoreleasepool { try build(request, device, queue, token) } }.value } onCancel: { token.cancel() diff --git a/ScreenSlangerTests/CompilerProcessTests.swift b/ScreenSlangerTests/CompilerProcessTests.swift index 72f23c6..f23d056 100644 --- a/ScreenSlangerTests/CompilerProcessTests.swift +++ b/ScreenSlangerTests/CompilerProcessTests.swift @@ -297,4 +297,50 @@ struct CompilerLocationTests { environment: ["SLANG_PATH": override.path], homeDirectory: directory) == override.path) } } + + @Test("RetroArch helper discovery follows the moved app and ignores developer overrides") + func relocatedRetroArchCompiler() throws { + try withFixture { directory in + let original = directory.appendingPathComponent("Original.app") + _ = try fixtureBundle(at: original) + let relative = "Contents/Helpers/librashader-compiler" + try executable(at: original.appendingPathComponent(relative)) + let other = directory.appendingPathComponent("developer/librashader-compiler") + try executable(at: other) + let moved = directory.appendingPathComponent("Moved app.app") + try FileManager.default.moveItem(at: original, to: moved) + let bundle = try #require(Bundle(url: moved)) + #expect(RetroArchCompilerLocation.findCompiler( + runtimeURL: other.deletingLastPathComponent().appendingPathComponent("librashader.dylib"), + bundle: bundle, environment: ["LIBRASHADER_COMPILER_PATH": other.path]) + == moved.appendingPathComponent(relative).path) + try FileManager.default.removeItem(at: moved.appendingPathComponent(relative)) + #expect(RetroArchCompilerLocation.findCompiler( + runtimeURL: other.deletingLastPathComponent().appendingPathComponent("librashader.dylib"), + bundle: bundle, environment: ["LIBRASHADER_COMPILER_PATH": other.path]) == nil) + } + } + + @Test("Unhosted RetroArch tools use the selected runtime's adjacent helper or an explicit override") + func developmentRetroArchCompiler() throws { + try withFixture { directory in + let bundle = try fixtureBundle(at: directory.appendingPathComponent("Unhosted.bundle")) + let runtime = directory.appendingPathComponent("selected-runtime/librashader.dylib") + let adjacent = runtime.deletingLastPathComponent().appendingPathComponent("librashader-compiler") + let other = directory.appendingPathComponent("different compiler") + try executable(at: adjacent) + try executable(at: other) + #expect(RetroArchCompilerLocation.findCompiler(runtimeURL: runtime, bundle: bundle, + environment: [:]) == adjacent.path) + #expect(RetroArchCompilerLocation.findCompiler(runtimeURL: runtime, bundle: bundle, + environment: ["LIBRASHADER_COMPILER_PATH": other.path]) == other.path) + try FileManager.default.removeItem(at: adjacent) + try FileManager.default.createDirectory(at: adjacent, withIntermediateDirectories: true) + #expect(RetroArchCompilerLocation.findCompiler(runtimeURL: runtime, bundle: bundle, + environment: [:]) == nil) + try executable(at: other, executable: false) + #expect(RetroArchCompilerLocation.findCompiler(runtimeURL: runtime, bundle: bundle, + environment: ["LIBRASHADER_COMPILER_PATH": other.path]) == nil) + } + } } diff --git a/ScreenSlangerTests/RuntimeBundleTests.swift b/ScreenSlangerTests/RuntimeBundleTests.swift index 00e489e..23d0bda 100644 --- a/ScreenSlangerTests/RuntimeBundleTests.swift +++ b/ScreenSlangerTests/RuntimeBundleTests.swift @@ -2,6 +2,7 @@ import CLibrashader import CryptoKit import Darwin import Foundation +import Metal import Testing private final class RuntimeBundleMarker: NSObject {} @@ -96,10 +97,10 @@ struct RuntimeBundleTests { } } - @Test("The bundled RetroArch runtime loads from its relocated path") + @Test("The relocated RetroArch runtime compiles with its bundled frontend") func relocatedRetroArchLibrary() throws { let original = try builtApplication() - try withRelocatedApplication { _, bundle in + try withRelocatedApplication { temporary, bundle in let relativeLibrary = "Contents/Frameworks/librashader.dylib" let library = bundle.bundleURL.appendingPathComponent(relativeLibrary) try FileManager.default.createDirectory(at: library.deletingLastPathComponent(), @@ -122,6 +123,66 @@ struct RuntimeBundleTests { try #require(dladdr(abiSymbol, &image) != 0) let path = String(cString: try #require(image.dli_fname)) #expect(URL(fileURLWithPath: path).resolvingSymlinksInPath() == library.resolvingSymlinksInPath()) + + let relativeHelper = "Contents/Helpers/librashader-compiler" + let helper = bundle.bundleURL.appendingPathComponent(relativeHelper) + try FileManager.default.createDirectory(at: helper.deletingLastPathComponent(), withIntermediateDirectories: true) + try FileManager.default.copyItem(at: original.appendingPathComponent(relativeHelper), to: helper) + let compiler = try #require(RetroArchCompilerLocation.findCompiler(runtimeURL: library, bundle: bundle, + environment: ["LIBRASHADER_COMPILER_PATH": "/missing/developer/compiler"])) + #expect(compiler == helper.path) + + func symbol(_ name: String, as: T.Type) throws -> T { + unsafeBitCast(try #require(dlsym(handle, name)), to: T.self) + } + let presetCreate = try symbol("libra_preset_create_with_options", as: PFN_libra_preset_create_with_options.self) + let presetFree = try symbol("libra_preset_free", as: PFN_libra_preset_free.self) + let create = try symbol("screenslanger_mtl_filter_chain_create_with_compiler", + as: PFN_screenslanger_mtl_filter_chain_create_with_compiler.self) + let free = try symbol("libra_mtl_filter_chain_free", as: PFN_libra_mtl_filter_chain_free.self) + let errorWrite = try symbol("libra_error_write", as: PFN_libra_error_write.self) + let errorFree = try symbol("libra_error_free", as: PFN_libra_error_free.self) + let stringFree = try symbol("libra_error_free_string", as: PFN_libra_error_free_string.self) + func check(_ result: libra_error_t?) throws { + guard result != nil else { return } + var error = result + var message: UnsafeMutablePointer? + _ = errorWrite(error, &message) + let description = message.map { String(cString: $0) } ?? "Unknown runtime error" + _ = stringFree(&message) + _ = errorFree(&error) + throw NSError(domain: "RuntimeBundleTests", code: 1, + userInfo: [NSLocalizedDescriptionKey: description]) + } + let source = temporary.appendingPathComponent("portable.slang") + try """ + #version 450 + #pragma stage vertex + layout(location = 0) in vec4 Position; + layout(location = 1) in vec2 TexCoord; + layout(location = 0) out vec2 vTexCoord; + void main() { gl_Position = Position; vTexCoord = TexCoord; } + #pragma stage fragment + layout(location = 0) in vec2 vTexCoord; + layout(location = 0) out vec4 FragColor; + void main() { FragColor = vec4(vTexCoord, 0.6, 1.0); } + """.write(to: source, atomically: true, encoding: .utf8) + let presetURL = temporary.appendingPathComponent("portable.slangp") + try "shaders = 1\nshader0 = \"portable.slang\"\n" + .write(to: presetURL, atomically: true, encoding: .utf8) + var preset: libra_shader_preset_t? + var options = libra_preset_opt_t() + options.version = 5 + try check(presetURL.path.withCString { presetCreate($0, nil, &options, &preset) }) + defer { if preset != nil { try? check(presetFree(&preset)) } } + let device = try #require(MTLCreateSystemDefaultDevice()) + let queue = try #require(device.makeCommandQueue()) + var chain: libra_mtl_filter_chain_t? + var chainOptions = filter_chain_mtl_opt_t() + chainOptions.version = 5 + try check(compiler.withCString { create(&preset, queue, &chainOptions, $0, &chain) }) + try #require(chain != nil) + try check(free(&chain)) } } @@ -130,7 +191,8 @@ 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", "0003-stream-metal-lut-loading.patch"] + "0002-compact-grayscale-luts.patch", "0003-stream-metal-lut-loading.patch", + "0004-isolate-retroarch-compiler.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 +200,8 @@ 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.2")) + #expect(provenance.contains("0.12.0-screenslanger.3")) + #expect(provenance.contains("--bin librashader-compiler")) 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 5b1e451..04eed52 100644 --- a/ScreenSlangerTests/ShaderTests.swift +++ b/ScreenSlangerTests/ShaderTests.swift @@ -736,6 +736,42 @@ struct ShaderTests { #expect(resources.effect?.id == current.id) } + @Test("Concurrent RetroArch loads and later compiler exits preserve independent live pipelines") + func independentCompilerLifetimes() async throws { + let directory = try temporaryDirectory() + defer { try? FileManager.default.removeItem(at: directory) } + let firstURL = directory.appendingPathComponent("first.slang") + let secondURL = directory.appendingPathComponent("second.slang") + try retroArchSource(fragment: "vec4(0.125, 0.25, 0.5, 1.0)") + .write(to: firstURL, atomically: true, encoding: .utf8) + try retroArchSource(fragment: "vec4(0.75, 0.5, 0.25, 1.0)") + .write(to: secondURL, atomically: true, encoding: .utf8) + let device = SharedMetalResources.shared.device + let first = SharedMetalResources(device: device) + let second = SharedMetalResources(device: device) + defer { first.clear(); second.clear() } + async let firstLoad: Void = first.loadEffect(ShaderLoadRequest(url: firstURL)) + async let secondLoad: Void = second.loadEffect(ShaderLoadRequest(url: secondURL)) + try await firstLoad + try await secondLoad + + func pixels(_ resources: SharedMetalResources) throws -> [UInt8] { + let destination = try makeTexture() + let command = try #require(resources.commandQueue.makeCommandBuffer()) + try ShaderRenderCore(resources: resources).encode(commandBuffer: command, + source: makeTexture(pixels: inputPixels), destination: destination, + context: ShaderFrameContext(outputSize: SIMD2(2, 2))) + return try finishRendering(command, to: destination) + } + try expectPixels(pixels(first), expected: solidPixel([128, 64, 32, 255])) + try expectPixels(pixels(second), expected: solidPixel([64, 128, 191, 255])) + try retroArchSource(fragment: "vec4(0.0, 1.0, 0.0, 1.0)") + .write(to: firstURL, atomically: true, encoding: .utf8) + try await first.loadEffect(ShaderLoadRequest(url: firstURL)) + try expectPixels(pixels(first), expected: solidPixel([0, 255, 0, 255])) + try expectPixels(pixels(second), expected: solidPixel([64, 128, 191, 255])) + } + private func solidPixel(_ pixel: [UInt8]) -> [UInt8] { Array(repeating: pixel, count: 4).flatMap { $0 } } diff --git a/Vendor/CLibrashader/BUILDING.md b/Vendor/CLibrashader/BUILDING.md index cad8f48..05518b0 100644 --- a/Vendor/CLibrashader/BUILDING.md +++ b/Vendor/CLibrashader/BUILDING.md @@ -1,14 +1,16 @@ # Rebuilding ScreenSlanger's librashader runtime -ScreenSlanger uses **0.12.0-screenslanger.2**, built from upstream tag +ScreenSlanger uses **0.12.0-screenslanger.3**, built from upstream tag [`librashader-v0.12.0`](https://github.com/SnowflakePowered/librashader/tree/librashader-v0.12.0) -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 +with the four patches shipped beside this file. The upstream C header remains +unmodified; the fourth patch adds a ScreenSlanger-specific C entry point and +a separate `librashader-compiler` executable. 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`, all three applied patches, and the MPL-2.0 license in +`Cargo.lock`, all four 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 @@ -27,9 +29,9 @@ 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.2/`. -The official `0.12.0` and previous `0.12.0-screenslanger.1` installations are +graph and optimized profile, then installs the runtime and compiler helper into +`~/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3/`. +The official `0.12.0` and previous local runtime installations are preserved. Normal Xcode builds only verify, copy, and sign the installed runtime; they do not download or compile Rust code. @@ -37,7 +39,7 @@ copy, and sign the installed runtime; they do not download or compile Rust code. Copy this entire `librashader` directory to a writable folder, then run the following commands in that copy. `SHA256SUMS` checks the source archive -and all three patches against the recorded distribution; `BUILD-INFO.txt` records +and all four patches against the recorded distribution; `BUILD-INFO.txt` records the version, toolchain, architecture, and build command. ```sh @@ -47,11 +49,14 @@ 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 && patch --batch --forward --fuzz=0 -p1 < ../0004-isolate-retroarch-compiler.patch) cd source MACOSX_DEPLOYMENT_TARGET=27.0 SDKROOT="$(xcrun --sdk macosx --show-sdk-path)" \ CARGO_PROFILE_OPTIMIZED_STRIP=none \ cargo +1.93.0 build --locked --profile optimized \ - --package librashader-capi --no-default-features --features runtime-metal + --package librashader-capi --package librashader-reflect \ + --bin librashader-compiler --lib --no-default-features \ + --features librashader-capi/runtime-metal,librashader-reflect/glslang-in ``` `CARGO_PROFILE_OPTIMIZED_STRIP=none` avoids a Rust 1.93.0 stripping issue that @@ -60,11 +65,13 @@ 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 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 +The outputs are `target/optimized/liblibrashader_capi.dylib` and +`target/optimized/librashader-compiler`. ScreenSlanger installs the library +as `librashader.dylib`, changes its Mach-O identity to `@rpath/librashader.dylib`, removes absolute build-machine rpaths, and signs the -app copy. Those packaging steps do not change its source implementation. -The installation's `RUNTIME.sha256` verifies the binary before packaging; it is +app copy. The helper is copied to `Contents/Helpers/librashader-compiler` and +signed with the same identity. Those packaging steps do not change the source implementation. +The installation's `RUNTIME.sha256` verifies both binaries before packaging; it is not copied into the application because code signing changes the binary bytes. The upstream source archive is diff --git a/Vendor/CLibrashader/NOTICE.md b/Vendor/CLibrashader/NOTICE.md index 96d1638..b96a6e1 100644 --- a/Vendor/CLibrashader/NOTICE.md +++ b/Vendor/CLibrashader/NOTICE.md @@ -1,4 +1,4 @@ -# librashader 0.12.0-screenslanger.2 +# librashader 0.12.0-screenslanger.3 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. @@ -7,9 +7,10 @@ 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. +- `0004-isolate-retroarch-compiler.patch` adds the `librashader-compiler` helper and a ScreenSlanger-specific entry point that accepts its explicit path. GLSL compilation runs in a short-lived helper process; the application retains the Metal renderer and a bounded cache of compiled shader results. The helper-aware path avoids permanent preprocessing workers and releases unused source strings after pipeline creation. The upstream C header remains unmodified. -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. +The corresponding source for both the runtime and helper 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 four patches. Application bundles include that exact archive as `librashader-v0.12.0-source.tar.gz`, all four 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. +Application builds give the dynamic library a relocatable Mach-O install name and sign both the library and helper with the app publisher's identity. These packaging changes do not modify the implementation beyond the patches described above. -The Swift adapter dynamically loads ABI 2 / API 5 through the official C declarations. Each chain stays on a dedicated OS thread because the upstream Metal runtime is not thread safe. Its shared uniform buffers require at most one outstanding GPU frame per chain; the adapter returns a busy error instead of waiting on the UI thread. +The Swift adapter dynamically loads ABI 2 / API 5 through the official C declarations, with the helper-aware creation function declared separately in ScreenSlanger's `screenslanger.h`. Each chain stays on a dedicated OS thread because the upstream Metal runtime is not thread safe. Its shared uniform buffers require at most one outstanding GPU frame per chain; the adapter returns a busy error instead of waiting on the UI thread. diff --git a/Vendor/CLibrashader/patches/0004-isolate-retroarch-compiler.patch b/Vendor/CLibrashader/patches/0004-isolate-retroarch-compiler.patch new file mode 100644 index 0000000..73430d0 --- /dev/null +++ b/Vendor/CLibrashader/patches/0004-isolate-retroarch-compiler.patch @@ -0,0 +1,1185 @@ +--- a/Cargo.lock ++++ b/Cargo.lock +@@ -1834,6 +1834,7 @@ + version = "0.12.0" + dependencies = [ + "array-concat", ++ "blake3", + "bytemuck", + "image", + "librashader-common", +@@ -1849,6 +1850,7 @@ + "objc2-metal-kit", + "objc2-quartz-core", + "rayon", ++ "tempfile", + "thiserror 2.0.18", + ] + +--- a/librashader-capi/src/runtime/mtl/filter_chain.rs ++++ b/librashader-capi/src/runtime/mtl/filter_chain.rs +@@ -147,6 +147,33 @@ + chain, + ))))) + } ++ } ++} ++ ++extern_fn! { ++ /// ScreenSlanger extension: identical chain semantics with an explicitly ++ /// supplied, short-lived GLSL compiler helper. Consumes preset on every call. ++ fn screenslanger_mtl_filter_chain_create_with_compiler( ++ preset: *mut libra_shader_preset_t, ++ queue: PMTLCommandQueue, ++ options: *const MaybeUninit, ++ helper_path: *const c_char, ++ out: *mut MaybeUninit ++ ) |queue| { ++ assert_non_null!(preset); ++ assert_non_null!(helper_path); ++ assert_non_null!(out); ++ let preset = unsafe { ++ let Some(preset) = (&mut *preset).take() else { ++ return Err(LibrashaderError::InvalidParameter("preset")); ++ }; ++ Box::from_raw(preset.as_ptr()) ++ }; ++ let helper = unsafe { CStr::from_ptr(helper_path) }.to_str()?; ++ let options = if options.is_null() { None } else { Some(unsafe { options.read() }) }; ++ let options = options.map(FromUninit::from_uninit); ++ let chain = FilterChain::load_from_preset_with_compiler(*preset, queue.as_ref(), options.as_ref(), std::path::Path::new(helper))?; ++ unsafe { out.write(MaybeUninit::new(NonNull::new(Box::into_raw(Box::new(chain))))) }; + } + } + +--- a/librashader-pack/src/lib.rs ++++ b/librashader-pack/src/lib.rs +@@ -136,6 +136,41 @@ + + #[cfg(feature = "load")] + impl ShaderPresetPack { ++ /// Load without creating Rayon's process-lifetime worker pool. Metal uses this ++ /// for preprocessing before its short-lived external compiler handles all passes. ++ pub fn load_from_preset_serial(preset: ShaderPreset) -> Result ++ where ++ E: From + From, ++ { ++ Ok(ShaderPresetPack { ++ language: ShaderSourceLanguage::Glsl, ++ #[cfg(feature = "parse_legacy_glsl")] ++ feedback_pass: preset.feedback_pass, ++ pass_count: preset.pass_count, ++ passes: preset ++ .passes ++ .into_iter() ++ .map(|pass| { ++ Ok::<_, E>(PassResource { ++ data: PassMeta::load(&pass.path, preset.features)?, ++ meta: pass.meta, ++ }) ++ }) ++ .collect::, _>>()?, ++ textures: preset ++ .textures ++ .into_iter() ++ .map(|texture| { ++ Ok::<_, E>(TextureResource { ++ data: TextureMeta::load(&texture.path, ())?, ++ meta: texture.meta, ++ }) ++ }) ++ .collect::, _>>()?, ++ parameters: preset.parameters, ++ }) ++ } ++ + /// Load a `ShaderPack` from a [`ShaderPreset`]. + pub fn load_from_preset(preset: ShaderPreset) -> Result + where +--- a/librashader-reflect/Cargo.toml ++++ b/librashader-reflect/Cargo.toml +@@ -10,6 +10,11 @@ + categories = ["emulators", "compilers", "graphics"] + keywords = ["shader", "retroarch", "SPIR-V"] + description = "RetroArch shaders for all." ++ ++[[bin]] ++name = "librashader-compiler" ++path = "src/bin/librashader-compiler.rs" ++required-features = ["glslang-in"] + + [dependencies] + glslang = { version = "0.8.1", optional = true } +--- a/librashader-reflect/src/front/mod.rs ++++ b/librashader-reflect/src/front/mod.rs +@@ -1,6 +1,9 @@ + use crate::error::ShaderCompileError; + use librashader_preprocess::ShaderSource; + pub(crate) mod spirv_passes; ++ ++/// Bounded protocol for compiling GLSL in a short-lived helper process. ++pub mod compile_wire; + + #[cfg(feature = "glslang-in")] + mod glslang; +--- /dev/null ++++ b/librashader-reflect/src/front/compile_wire.rs +@@ -0,0 +1,506 @@ ++//! Versioned, length-delimited transport for the out-of-process GLSL compiler. ++//! ++//! Only preprocessed vertex/fragment source text crosses the process boundary. The helper uses ++//! the same `Glslang::compile` path as an in-process load, including interface linking. Shader ++//! metadata remains in the caller. Integers are little-endian; lengths count bytes in requests ++//! and 32-bit SPIR-V words in responses. A message contains a magic, version, pass count, then ++//! a vertex and fragment field for every pass. There are no optional or trailing fields. ++ ++use crate::front::SpirvCompilation; ++use librashader_preprocess::ShaderSource; ++use thiserror::Error; ++ ++#[cfg(feature = "glslang-in")] ++use crate::front::{Glslang, ShaderInputCompiler}; ++ ++/// Maximum size of a complete request or response, including framing. ++pub const MAX_MESSAGE_BYTES: usize = 256 * 1024 * 1024; ++/// Maximum shader pass count in one compilation batch. ++pub const MAX_PASSES: usize = 4096; ++/// Current protocol version. Changing compilation semantics also requires a cache-key change. ++pub const PROTOCOL_VERSION: u32 = 1; ++const REQUEST_MAGIC: &[u8; 8] = b"LBRACREQ"; ++const RESPONSE_MAGIC: &[u8; 8] = b"LBRACRES"; ++const HEADER_BYTES: usize = 16; ++ ++/// An invalid compiler message or a failed shader compilation. ++#[derive(Debug, Error)] ++pub enum CompileWireError { ++ /// The complete message or a field cannot fit within the aggregate size limit. ++ #[error("compiler message exceeds the {MAX_MESSAGE_BYTES}-byte limit")] ++ MessageTooLarge, ++ /// A request or response contains too many shader passes. ++ #[error("compiler message exceeds the {MAX_PASSES}-pass limit")] ++ TooManyPasses, ++ /// The message does not identify the expected protocol direction. ++ #[error("incorrect compiler message magic")] ++ InvalidMagic, ++ /// The sender and receiver use different protocol versions. ++ #[error("unsupported compiler protocol version {0}")] ++ UnsupportedVersion(u32), ++ /// A header or length-prefixed field ends prematurely. ++ #[error("truncated compiler message")] ++ Truncated, ++ /// Source text must be UTF-8, matching Rust shader strings. ++ #[error("compiler source text is not UTF-8")] ++ InvalidUtf8, ++ /// Extra bytes follow the declared fields. ++ #[error("trailing bytes in compiler message")] ++ TrailingBytes, ++ /// The helper did not return exactly one result per requested pass. ++ #[error("compiler returned {actual} passes; expected {expected}")] ++ UnexpectedCount { ++ /// Requested pass count. ++ expected: usize, ++ /// Returned pass count. ++ actual: usize, ++ }, ++ /// A response contains malformed SPIR-V or the wrong execution model. ++ #[error("invalid {stage} SPIR-V for pass {pass}: {reason}")] ++ InvalidSpirv { ++ /// Zero-based shader pass index. ++ pass: usize, ++ /// Vertex or fragment stage. ++ stage: &'static str, ++ /// Structural validation error. ++ reason: String, ++ }, ++ /// The existing glslang frontend rejected a shader pass. ++ #[cfg(feature = "glslang-in")] ++ #[error("shader pass {pass}: {source}")] ++ Compile { ++ /// Zero-based shader pass index. ++ pass: usize, ++ /// Original compiler diagnostics, retaining original source line directives. ++ #[source] ++ source: crate::error::ShaderCompileError, ++ }, ++} ++ ++/// Serialize preprocessed sources without parameters, aliases, or output-format metadata. ++pub fn encode_request<'a>( ++ sources: impl IntoIterator, ++) -> Result, CompileWireError> { ++ let mut result = header(REQUEST_MAGIC, 0); ++ let mut count = 0; ++ for source in sources { ++ count += 1; ++ check_pass_count(count)?; ++ append_field(&mut result, source.vertex.as_bytes())?; ++ append_field(&mut result, source.fragment.as_bytes())?; ++ } ++ result[12..HEADER_BYTES].copy_from_slice(&(count as u32).to_le_bytes()); ++ Ok(result) ++} ++ ++/// Compile all sources through librashader's unchanged glslang and interface-linking path. ++/// ++/// Framing is validated in full before compiling. Text is borrowed from the request while it is ++/// parsed, then copied one pass at a time to avoid retaining a second copy of the whole batch. ++#[cfg(feature = "glslang-in")] ++pub fn compile_request(request: &[u8]) -> Result, CompileWireError> { ++ let sources = parse_request(request)?; ++ let mut result = header(RESPONSE_MAGIC, sources.len()); ++ for (pass, (vertex, fragment)) in sources.into_iter().enumerate() { ++ let source = ShaderSource { ++ vertex: vertex.to_owned(), ++ fragment: fragment.to_owned(), ++ name: None, ++ parameters: Default::default(), ++ format: Default::default(), ++ }; ++ let compilation = Glslang::compile(&source) ++ .map_err(|source| CompileWireError::Compile { pass, source })?; ++ append_words(&mut result, &compilation.vertex)?; ++ append_words(&mut result, &compilation.fragment)?; ++ } ++ Ok(result) ++} ++ ++/// Decode a complete response, checking counts, bounds, and each SPIR-V stage before use. ++pub fn decode_response( ++ response: &[u8], ++ expected_count: usize, ++) -> Result, CompileWireError> { ++ check_pass_count(expected_count)?; ++ let (mut reader, count) = Reader::message(response, RESPONSE_MAGIC)?; ++ if count != expected_count { ++ return Err(CompileWireError::UnexpectedCount { ++ expected: expected_count, ++ actual: count, ++ }); ++ } ++ let mut result = Vec::with_capacity(count); ++ for pass in 0..count { ++ let vertex = reader.words()?; ++ validate_spirv(&vertex, pass, spirv::ExecutionModel::Vertex, "vertex")?; ++ let fragment = reader.words()?; ++ validate_spirv(&fragment, pass, spirv::ExecutionModel::Fragment, "fragment")?; ++ result.push(SpirvCompilation { vertex, fragment }); ++ } ++ reader.finish()?; ++ Ok(result) ++} ++ ++fn check_message_size(size: usize) -> Result<(), CompileWireError> { ++ if size > MAX_MESSAGE_BYTES { ++ Err(CompileWireError::MessageTooLarge) ++ } else { ++ Ok(()) ++ } ++} ++ ++fn check_pass_count(count: usize) -> Result<(), CompileWireError> { ++ if count > MAX_PASSES { ++ Err(CompileWireError::TooManyPasses) ++ } else { ++ Ok(()) ++ } ++} ++ ++fn header(magic: &[u8; 8], count: usize) -> Vec { ++ let mut result = Vec::with_capacity(HEADER_BYTES); ++ result.extend_from_slice(magic); ++ result.extend_from_slice(&PROTOCOL_VERSION.to_le_bytes()); ++ result.extend_from_slice(&(count as u32).to_le_bytes()); ++ result ++} ++ ++fn check_append(current: usize, payload: usize) -> Result<(), CompileWireError> { ++ let size = current ++ .checked_add(4) ++ .and_then(|size| size.checked_add(payload)) ++ .ok_or(CompileWireError::MessageTooLarge)?; ++ check_message_size(size) ++} ++ ++fn append_field(result: &mut Vec, bytes: &[u8]) -> Result<(), CompileWireError> { ++ check_append(result.len(), bytes.len())?; ++ let length = u32::try_from(bytes.len()).map_err(|_| CompileWireError::MessageTooLarge)?; ++ result.extend_from_slice(&length.to_le_bytes()); ++ result.extend_from_slice(bytes); ++ Ok(()) ++} ++ ++#[cfg(any(feature = "glslang-in", test))] ++fn append_words(result: &mut Vec, words: &[u32]) -> Result<(), CompileWireError> { ++ let bytes = words ++ .len() ++ .checked_mul(4) ++ .ok_or(CompileWireError::MessageTooLarge)?; ++ check_append(result.len(), bytes)?; ++ let length = u32::try_from(words.len()).map_err(|_| CompileWireError::MessageTooLarge)?; ++ result.extend_from_slice(&length.to_le_bytes()); ++ for word in words { ++ result.extend_from_slice(&word.to_le_bytes()); ++ } ++ Ok(()) ++} ++ ++#[cfg(any(feature = "glslang-in", test))] ++fn parse_request(request: &[u8]) -> Result, CompileWireError> { ++ let (mut reader, count) = Reader::message(request, REQUEST_MAGIC)?; ++ let mut result = Vec::with_capacity(count); ++ for _ in 0..count { ++ let vertex = reader.source()?; ++ let fragment = reader.source()?; ++ result.push((vertex, fragment)); ++ } ++ reader.finish()?; ++ Ok(result) ++} ++ ++struct Reader<'a> { ++ remaining: &'a [u8], ++} ++ ++impl<'a> Reader<'a> { ++ fn message(bytes: &'a [u8], magic: &[u8; 8]) -> Result<(Self, usize), CompileWireError> { ++ check_message_size(bytes.len())?; ++ let mut reader = Self { remaining: bytes }; ++ if reader.take(8)? != magic { ++ return Err(CompileWireError::InvalidMagic); ++ } ++ let version = reader.u32()?; ++ if version != PROTOCOL_VERSION { ++ return Err(CompileWireError::UnsupportedVersion(version)); ++ } ++ let count = reader.u32()? as usize; ++ check_pass_count(count)?; ++ Ok((reader, count)) ++ } ++ ++ fn take(&mut self, length: usize) -> Result<&'a [u8], CompileWireError> { ++ check_message_size(length)?; ++ if length > self.remaining.len() { ++ return Err(CompileWireError::Truncated); ++ } ++ let (result, rest) = self.remaining.split_at(length); ++ self.remaining = rest; ++ Ok(result) ++ } ++ ++ fn u32(&mut self) -> Result { ++ let bytes: [u8; 4] = self.take(4)?.try_into().unwrap(); ++ Ok(u32::from_le_bytes(bytes)) ++ } ++ ++ #[cfg(any(feature = "glslang-in", test))] ++ fn source(&mut self) -> Result<&'a str, CompileWireError> { ++ let length = self.u32()? as usize; ++ std::str::from_utf8(self.take(length)?).map_err(|_| CompileWireError::InvalidUtf8) ++ } ++ ++ fn words(&mut self) -> Result, CompileWireError> { ++ let length = self.u32()? as usize; ++ let bytes = length ++ .checked_mul(4) ++ .ok_or(CompileWireError::MessageTooLarge)?; ++ // Bound and validate the entire field before allocating from an untrusted length. ++ let bytes = self.take(bytes)?; ++ Ok(bytes ++ .chunks_exact(4) ++ .map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap())) ++ .collect()) ++ } ++ ++ fn finish(self) -> Result<(), CompileWireError> { ++ if self.remaining.is_empty() { ++ Ok(()) ++ } else { ++ Err(CompileWireError::TrailingBytes) ++ } ++ } ++} ++ ++fn validate_spirv( ++ words: &[u32], ++ pass: usize, ++ model: spirv::ExecutionModel, ++ stage: &'static str, ++) -> Result<(), CompileWireError> { ++ let invalid = |reason: String| CompileWireError::InvalidSpirv { ++ pass, ++ stage, ++ reason, ++ }; ++ // Glslang is deliberately configured for SPIR-V 1.0. Reject swapped or future headers and ++ // impossible ID bounds before handing the word stream to the structural parser. ++ if words.len() < 5 ++ || words[0] != 0x0723_0203 ++ || words[1] != 0x0001_0000 ++ || words[3] == 0 ++ || words[3] > 0x003f_ffff ++ || words[4] != 0 ++ { ++ return Err(invalid("incorrect SPIR-V 1.0 header".to_owned())); ++ } ++ let mut loader = rspirv::dr::Loader::new(); ++ rspirv::binary::parse_words(words, &mut loader).map_err(|err| invalid(err.to_string()))?; ++ let module = loader.module(); ++ if module.memory_model.is_none() ++ || module.functions.is_empty() ++ || !module.entry_points.iter().any(|entry| { ++ matches!(entry.operands.first(), Some(rspirv::dr::Operand::ExecutionModel(found)) if *found == model) ++ }) ++ { ++ return Err(invalid("missing memory model, function, or stage entry point".to_owned())); ++ } ++ Ok(()) ++} ++ ++#[cfg(test)] ++mod tests { ++ use super::*; ++ use rspirv::binary::Assemble; ++ ++ fn source(vertex: &str, fragment: &str) -> ShaderSource { ++ ShaderSource { ++ vertex: vertex.to_owned(), ++ fragment: fragment.to_owned(), ++ name: None, ++ parameters: Default::default(), ++ format: Default::default(), ++ } ++ } ++ ++ fn minimal_spirv(model: spirv::ExecutionModel) -> Vec { ++ let mut builder = rspirv::dr::Builder::new(); ++ builder.set_version(1, 0); ++ builder.capability(spirv::Capability::Shader); ++ builder.memory_model(spirv::AddressingModel::Logical, spirv::MemoryModel::GLSL450); ++ let void = builder.type_void(); ++ let function_type = builder.type_function(void, []); ++ let function = builder ++ .begin_function(void, None, spirv::FunctionControl::NONE, function_type) ++ .unwrap(); ++ builder.begin_block(None).unwrap(); ++ builder.ret().unwrap(); ++ builder.end_function().unwrap(); ++ builder.entry_point(model, function, "main", []); ++ if model == spirv::ExecutionModel::Fragment { ++ builder.execution_mode(function, spirv::ExecutionMode::OriginUpperLeft, []); ++ } ++ builder.module().assemble() ++ } ++ ++ fn response(vertex: &[u32], fragment: &[u32]) -> Vec { ++ let mut bytes = header(RESPONSE_MAGIC, 1); ++ append_words(&mut bytes, vertex).unwrap(); ++ append_words(&mut bytes, fragment).unwrap(); ++ bytes ++ } ++ ++ #[test] ++ fn request_preserves_source_text_and_order() { ++ let sources = [source("vertex\n\0☃", "fragment\n"), source("", "second")]; ++ let encoded = encode_request(&sources).unwrap(); ++ assert_eq!( ++ parse_request(&encoded).unwrap(), ++ [("vertex\n\0☃", "fragment\n"), ("", "second")] ++ ); ++ assert!(parse_request(&encode_request([]).unwrap()) ++ .unwrap() ++ .is_empty()); ++ } ++ ++ #[test] ++ fn request_rejects_all_truncation_points_and_trailing_data() { ++ let mut bytes = encode_request([&source("vertex", "fragment")]).unwrap(); ++ for end in 0..bytes.len() { ++ assert!( ++ parse_request(&bytes[..end]).is_err(), ++ "accepted prefix {end}" ++ ); ++ } ++ bytes.push(0); ++ assert!(matches!( ++ parse_request(&bytes), ++ Err(CompileWireError::TrailingBytes) ++ )); ++ } ++ ++ #[test] ++ fn framing_rejects_wrong_magic_version_counts_and_utf8() { ++ let bytes = encode_request([&source("v", "f")]).unwrap(); ++ let mut bad = bytes.clone(); ++ bad[0] ^= 1; ++ assert!(matches!( ++ parse_request(&bad), ++ Err(CompileWireError::InvalidMagic) ++ )); ++ let mut bad = bytes.clone(); ++ bad[8..12].copy_from_slice(&2u32.to_le_bytes()); ++ assert!(matches!( ++ parse_request(&bad), ++ Err(CompileWireError::UnsupportedVersion(2)) ++ )); ++ let mut bad = bytes.clone(); ++ bad[12..16].copy_from_slice(&((MAX_PASSES + 1) as u32).to_le_bytes()); ++ assert!(matches!( ++ parse_request(&bad), ++ Err(CompileWireError::TooManyPasses) ++ )); ++ let mut bad = bytes; ++ bad[20] = 0xff; ++ assert!(matches!( ++ parse_request(&bad), ++ Err(CompileWireError::InvalidUtf8) ++ )); ++ } ++ ++ #[test] ++ fn lengths_and_aggregate_size_are_checked_before_allocation() { ++ assert!(check_message_size(MAX_MESSAGE_BYTES).is_ok()); ++ assert!(check_message_size(MAX_MESSAGE_BYTES + 1).is_err()); ++ assert!(check_append(MAX_MESSAGE_BYTES - 4, 0).is_ok()); ++ assert!(check_append(MAX_MESSAGE_BYTES - 4, 1).is_err()); ++ assert!(check_append(usize::MAX, 1).is_err()); ++ let input = source("", ""); ++ assert!(encode_request(std::iter::repeat(&input).take(MAX_PASSES)).is_ok()); ++ assert!(matches!( ++ encode_request(std::iter::repeat(&input).take(MAX_PASSES + 1)), ++ Err(CompileWireError::TooManyPasses) ++ )); ++ let mut request = header(REQUEST_MAGIC, 1); ++ request.extend_from_slice(&u32::MAX.to_le_bytes()); ++ assert!(matches!( ++ parse_request(&request), ++ Err(CompileWireError::MessageTooLarge) ++ )); ++ let mut response = header(RESPONSE_MAGIC, 1); ++ response.extend_from_slice(&u32::MAX.to_le_bytes()); ++ assert!(matches!( ++ decode_response(&response, 1), ++ Err(CompileWireError::MessageTooLarge) ++ )); ++ } ++ ++ #[test] ++ fn response_preserves_words_and_checks_expected_count() { ++ let vertex = minimal_spirv(spirv::ExecutionModel::Vertex); ++ let fragment = minimal_spirv(spirv::ExecutionModel::Fragment); ++ let bytes = response(&vertex, &fragment); ++ let decoded = decode_response(&bytes, 1).unwrap(); ++ assert_eq!(decoded[0].vertex, vertex); ++ assert_eq!(decoded[0].fragment, fragment); ++ assert!(matches!( ++ decode_response(&bytes, 2), ++ Err(CompileWireError::UnexpectedCount { ++ expected: 2, ++ actual: 1 ++ }) ++ )); ++ assert!(matches!( ++ decode_response(&bytes, MAX_PASSES + 1), ++ Err(CompileWireError::TooManyPasses) ++ )); ++ assert!(decode_response(&header(RESPONSE_MAGIC, 0), 0) ++ .unwrap() ++ .is_empty()); ++ } ++ ++ #[test] ++ fn response_rejects_truncation_trailing_data_and_invalid_spirv() { ++ let vertex = minimal_spirv(spirv::ExecutionModel::Vertex); ++ let fragment = minimal_spirv(spirv::ExecutionModel::Fragment); ++ let mut bytes = response(&vertex, &fragment); ++ for end in 0..bytes.len() { ++ assert!( ++ decode_response(&bytes[..end], 1).is_err(), ++ "accepted prefix {end}" ++ ); ++ } ++ bytes.push(0); ++ assert!(matches!( ++ decode_response(&bytes, 1), ++ Err(CompileWireError::TrailingBytes) ++ )); ++ for words in [&[][..], &[0; 5][..], &vertex[..5], &fragment] { ++ assert!(matches!( ++ decode_response(&response(words, &fragment), 1), ++ Err(CompileWireError::InvalidSpirv { .. }) ++ )); ++ } ++ let mut invalid = vertex; ++ invalid.push(0); // Zero instruction word count. ++ assert!(matches!( ++ decode_response(&response(&invalid, &fragment), 1), ++ Err(CompileWireError::InvalidSpirv { .. }) ++ )); ++ } ++ ++ #[cfg(feature = "glslang-in")] ++ #[test] ++ fn helper_compiles_with_the_same_linking_as_the_in_process_frontend() { ++ let sources = [source( ++ "#version 450\nlayout(location=0) out vec2 uv;\nvoid main(){uv=vec2(0.5);gl_Position=vec4(0,0,0,1);}", ++ "#version 450\nlayout(location=0) in vec2 uv;\nlayout(location=0) out vec4 color;\nvoid main(){color=vec4(uv,0,1);}", ++ )]; ++ let direct = Glslang::compile(&sources[0]).unwrap(); ++ let wire = compile_request(&encode_request(&sources).unwrap()).unwrap(); ++ let decoded = decode_response(&wire, sources.len()).unwrap(); ++ assert_eq!(decoded[0].vertex, direct.vertex); ++ assert_eq!(decoded[0].fragment, direct.fragment); ++ } ++} +--- /dev/null ++++ b/librashader-reflect/src/bin/librashader-compiler.rs +@@ -0,0 +1,31 @@ ++//! Short-lived, GPU-independent GLSL compiler. Stdout is reserved for binary protocol output. ++ ++use librashader_reflect::front::compile_wire::{compile_request, MAX_MESSAGE_BYTES}; ++use std::io::{Read, Write}; ++use std::process::ExitCode; ++ ++fn run() -> Result<(), Box> { ++ let mut request = Vec::new(); ++ std::io::stdin() ++ .lock() ++ .take((MAX_MESSAGE_BYTES + 1) as u64) ++ .read_to_end(&mut request)?; ++ if request.len() > MAX_MESSAGE_BYTES { ++ return Err("compiler request exceeds the size limit".into()); ++ } ++ let response = compile_request(&request)?; ++ let mut stdout = std::io::stdout().lock(); ++ stdout.write_all(&response)?; ++ stdout.flush()?; ++ Ok(()) ++} ++ ++fn main() -> ExitCode { ++ match run() { ++ Ok(()) => ExitCode::SUCCESS, ++ Err(error) => { ++ eprintln!("librashader-compiler: {error}"); ++ ExitCode::FAILURE ++ } ++ } ++} +--- a/librashader-reflect/src/reflect/presets.rs ++++ b/librashader-reflect/src/reflect/presets.rs +@@ -56,7 +56,30 @@ + E: From, + E: From, + { +- compile_preset_passes::(passes, textures) ++ compile_preset_passes::(passes, textures, |source| { ++ Ok(I::Compiler::compile(source)?) ++ }) ++ } ++ ++ /// Reflect precompiled passes with the same preset semantics as normal compilation. ++ fn compile_preset_passes_with_compiler<'a, I, R, E>( ++ passes: impl IntoIterator, ++ textures: impl Iterator, ++ compiler: impl FnMut(&ShaderSource) -> Result, ++ ) -> Result< ++ ( ++ Vec>::Output>>, ++ ShaderSemantics, ++ ), ++ E, ++ > ++ where ++ I: ShaderReflectObject, ++ Self: Sized + FromCompilation, ++ I::Compiler: ShaderInputCompiler, ++ E: From + From + From, ++ { ++ compile_preset_passes::(passes, textures, compiler) + } + } + +@@ -65,6 +88,7 @@ + fn compile_preset_passes<'a, T, I, R, E>( + passes: impl IntoIterator, + textures: impl Iterator, ++ mut compiler: impl FnMut(&ShaderSource) -> Result, + ) -> Result< + ( + Vec>::Output>>, +@@ -89,7 +113,7 @@ + .into_iter() + .map(|shader| { + let source = &shader.data; +- let compiled = I::Compiler::compile(source)?; ++ let compiled = compiler(source)?; + let reflect = T::from_compilation(compiled)?; + + for parameter in source.parameters.values() { +--- a/librashader-runtime-mtl/Cargo.toml ++++ b/librashader-runtime-mtl/Cargo.toml +@@ -14,6 +14,8 @@ + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + [dependencies] ++blake3 = "1.5.4" ++tempfile = "3" + image = { workspace = true } + librashader-common = { path = "../librashader-common", features = ["metal"], version = "0.12.0" } + librashader-presets = { path = "../librashader-presets", version = "0.12.0" } +--- /dev/null ++++ b/librashader-runtime-mtl/src/compiler_process.rs +@@ -0,0 +1,298 @@ ++//! A short-lived GLSL frontend keeps glslang's process-lifetime caches out of ++//! the renderer. Only small owned SPIR-V responses survive in a bounded cache. ++use crate::error::{FilterChainError, Result}; ++use librashader_pack::PassResource; ++use librashader_reflect::front::compile_wire::{ ++ decode_response, encode_request, MAX_MESSAGE_BYTES, ++}; ++use librashader_reflect::front::SpirvCompilation; ++use std::collections::VecDeque; ++use std::fs::{self, File}; ++use std::io::{Read, Write}; ++use std::os::unix::ffi::OsStrExt; ++use std::os::unix::fs::MetadataExt; ++use std::path::Path; ++use std::process::{Child, Command, Stdio}; ++use std::sync::{Arc, Mutex, OnceLock}; ++use std::time::{Duration, Instant, UNIX_EPOCH}; ++ ++const CACHE_BYTES: usize = 16 * 1024 * 1024; ++const CACHE_ENTRIES: usize = 32; ++const HELPER_TIMEOUT: Duration = Duration::from_secs(30); ++const STDERR_BYTES: u64 = 1024 * 1024; ++ ++struct ResponseCache { ++ entries: VecDeque<([u8; 32], Arc<[u8]>)>, ++ bytes: usize, ++ max_bytes: usize, ++ max_entries: usize, ++} ++ ++impl ResponseCache { ++ fn new(max_bytes: usize, max_entries: usize) -> Self { ++ Self { ++ entries: VecDeque::new(), ++ bytes: 0, ++ max_bytes, ++ max_entries, ++ } ++ } ++ ++ fn get(&mut self, key: &[u8; 32]) -> Option> { ++ let index = self ++ .entries ++ .iter() ++ .position(|(candidate, _)| candidate == key)?; ++ let entry = self.entries.remove(index)?; ++ let value = Arc::clone(&entry.1); ++ self.entries.push_back(entry); ++ Some(value) ++ } ++ ++ fn insert(&mut self, key: [u8; 32], response: Arc<[u8]>) { ++ if response.len() > self.max_bytes || self.max_entries == 0 { ++ return; ++ } ++ if let Some(index) = self ++ .entries ++ .iter() ++ .position(|(candidate, _)| *candidate == key) ++ { ++ self.bytes -= self.entries.remove(index).unwrap().1.len(); ++ } ++ while self.entries.len() >= self.max_entries || self.bytes + response.len() > self.max_bytes ++ { ++ let (_, value) = self.entries.pop_front().unwrap(); ++ self.bytes -= value.len(); ++ } ++ self.bytes += response.len(); ++ self.entries.push_back((key, response)); ++ } ++} ++ ++fn error(detail: impl std::fmt::Display) -> FilterChainError { ++ FilterChainError::CompilerHelper(detail.to_string()) ++} ++ ++/// A child is always terminated and reaped if spawning, polling or output handling ++/// exits early. Completed children are already reaped by `try_wait`. ++struct ChildGuard { ++ child: Child, ++ reaped: bool, ++} ++impl Drop for ChildGuard { ++ fn drop(&mut self) { ++ if !self.reaped { ++ let _ = self.child.kill(); ++ let _ = self.child.wait(); ++ } ++ } ++} ++ ++fn wait_for_child(child: Child, timeout: Duration) -> Result { ++ let mut child = ChildGuard { ++ child, ++ reaped: false, ++ }; ++ let deadline = Instant::now() + timeout; ++ loop { ++ if let Some(status) = child.child.try_wait().map_err(error)? { ++ child.reaped = true; ++ return Ok(status); ++ } ++ if Instant::now() >= deadline { ++ return Err(error(format!( ++ "Compilation exceeded {} seconds.", ++ timeout.as_secs() ++ ))); ++ } ++ std::thread::sleep(Duration::from_millis(5)); ++ } ++} ++ ++fn run_helper(helper: &Path, request: &[u8], timeout: Duration) -> Result> { ++ let directory = tempfile::tempdir().map_err(error)?; ++ let input = directory.path().join("request.bin"); ++ let output = directory.path().join("response.bin"); ++ let diagnostic = directory.path().join("diagnostic.txt"); ++ File::create(&input) ++ .and_then(|mut f| f.write_all(request)) ++ .map_err(error)?; ++ // File-backed IPC cannot deadlock when a large request, response or diagnostic ++ // exceeds pipe capacity. Every file is removed when this invocation finishes. ++ let child = Command::new(helper) ++ .stdin(Stdio::from(File::open(&input).map_err(error)?)) ++ .stdout(Stdio::from(File::create(&output).map_err(error)?)) ++ .stderr(Stdio::from(File::create(&diagnostic).map_err(error)?)) ++ .spawn() ++ .map_err(|e| error(format!("Could not launch {}: {e}", helper.display())))?; ++ let status = wait_for_child(child, timeout)?; ++ if !status.success() { ++ let mut message = Vec::new(); ++ File::open(&diagnostic) ++ .map_err(error)? ++ .take(STDERR_BYTES) ++ .read_to_end(&mut message) ++ .map_err(error)?; ++ let message = String::from_utf8_lossy(&message); ++ return Err(error(if message.trim().is_empty() { ++ format!("Compiler exited with {status}.") ++ } else { ++ message.into_owned() ++ })); ++ } ++ let size = fs::metadata(&output).map_err(error)?.len(); ++ if size > MAX_MESSAGE_BYTES as u64 { ++ return Err(error("Compiler response exceeds the protocol size limit.")); ++ } ++ fs::read(output).map_err(error) ++} ++ ++fn cache_key(helper: &Path, request: &[u8]) -> Result<[u8; 32]> { ++ // Replacing the helper invalidates old responses even if shader text is unchanged. ++ // The versioned wire request includes every preprocessed stage, so changed includes ++ // and compiler protocol revisions cannot accidentally reuse old SPIR-V. ++ let metadata = fs::metadata(helper) ++ .map_err(|e| error(format!("Missing compiler {}: {e}", helper.display())))?; ++ let modified = metadata ++ .modified() ++ .map_err(error)? ++ .duration_since(UNIX_EPOCH) ++ .map_err(error)?; ++ let mut hash = blake3::Hasher::new(); ++ hash.update(b"ScreenSlanger compiler cache 1\0"); ++ hash.update(helper.as_os_str().as_bytes()); ++ hash.update(&metadata.len().to_le_bytes()); ++ hash.update(&metadata.dev().to_le_bytes()); ++ hash.update(&metadata.ino().to_le_bytes()); ++ hash.update(&metadata.ctime().to_le_bytes()); ++ hash.update(&metadata.ctime_nsec().to_le_bytes()); ++ hash.update(&modified.as_nanos().to_le_bytes()); ++ hash.update(&request); ++ Ok(*hash.finalize().as_bytes()) ++} ++ ++pub(crate) fn compile(passes: &[PassResource], helper: &Path) -> Result> { ++ let request = encode_request(passes.iter().map(|pass| &pass.data)).map_err(error)?; ++ let key = cache_key(helper, &request)?; ++ static CACHE: OnceLock> = OnceLock::new(); ++ let cache = CACHE.get_or_init(|| Mutex::new(ResponseCache::new(CACHE_BYTES, CACHE_ENTRIES))); ++ let cached = { cache.lock().unwrap_or_else(|e| e.into_inner()).get(&key) }; ++ if let Some(response) = cached { ++ return decode_response(&response, passes.len()).map_err(error); ++ } ++ // At most one uncached compiler process runs per app. A second display waits ++ // for the first to populate the shared immutable response instead of duplicating ++ // the frontend's temporary working set. This never runs on the UI thread. ++ static COMPILE_GATE: Mutex<()> = Mutex::new(()); ++ let _gate = COMPILE_GATE.lock().unwrap_or_else(|e| e.into_inner()); ++ let cached = { cache.lock().unwrap_or_else(|e| e.into_inner()).get(&key) }; ++ if let Some(response) = cached { ++ return decode_response(&response, passes.len()).map_err(error); ++ } ++ let response = run_helper(helper, &request, HELPER_TIMEOUT)?; ++ let compiled = decode_response(&response, passes.len()).map_err(error)?; ++ // Failed processes and malformed responses never enter the cache. ++ cache ++ .lock() ++ .unwrap_or_else(|e| e.into_inner()) ++ .insert(key, Arc::from(response)); ++ Ok(compiled) ++} ++ ++#[cfg(test)] ++mod tests { ++ use super::*; ++ ++ #[test] ++ fn cache_evicts_by_count_and_refreshes_recency() { ++ let mut cache = ResponseCache::new(1024, 2); ++ cache.insert([1; 32], Arc::from([1u8])); ++ cache.insert([2; 32], Arc::from([2u8])); ++ assert!(cache.get(&[1; 32]).is_some()); ++ cache.insert([3; 32], Arc::from([3u8])); ++ assert!(cache.get(&[2; 32]).is_none()); ++ assert_eq!(cache.entries.len(), 2); ++ assert_eq!(cache.bytes, 2); ++ } ++ ++ #[test] ++ fn cache_enforces_byte_limit_and_does_not_retain_oversized_results() { ++ let mut cache = ResponseCache::new(5, 32); ++ cache.insert([1; 32], Arc::from([0u8; 3])); ++ cache.insert([2; 32], Arc::from([0u8; 3])); ++ assert!(cache.get(&[1; 32]).is_none()); ++ let oversized: Arc<[u8]> = Arc::from([0u8; 6]); ++ let weak = Arc::downgrade(&oversized); ++ cache.insert([3; 32], oversized); ++ assert!(weak.upgrade().is_none()); ++ assert_eq!(cache.bytes, 3); ++ assert_eq!(cache.entries.len(), 1); ++ } ++ ++ #[test] ++ fn replacing_entry_releases_previous_response() { ++ let mut cache = ResponseCache::new(10, 3); ++ let original: Arc<[u8]> = Arc::from([1u8; 4]); ++ let weak = Arc::downgrade(&original); ++ cache.insert([1; 32], original); ++ cache.insert([1; 32], Arc::from([2u8; 2])); ++ assert!(weak.upgrade().is_none()); ++ assert_eq!(cache.bytes, 2); ++ assert_eq!(cache.entries.len(), 1); ++ } ++ ++ #[test] ++ fn cache_key_changes_with_source_or_helper_replacement() { ++ let directory = tempfile::tempdir().unwrap(); ++ let helper = directory.path().join("helper"); ++ fs::write(&helper, b"first").unwrap(); ++ let first = cache_key(&helper, b"vertex+fragment+included-source").unwrap(); ++ assert_ne!( ++ first, ++ cache_key(&helper, b"vertex+fragment+edited-include").unwrap() ++ ); ++ let replacement = directory.path().join("replacement"); ++ fs::write(&replacement, b"other").unwrap(); ++ fs::rename(replacement, &helper).unwrap(); ++ assert_ne!( ++ first, ++ cache_key(&helper, b"vertex+fragment+included-source").unwrap() ++ ); ++ } ++ ++ #[test] ++ fn successful_process_with_malformed_response_is_rejected() { ++ let response = ++ run_helper(Path::new("/usr/bin/true"), b"input", Duration::from_secs(1)).unwrap(); ++ assert!(decode_response(&response, 1).is_err()); ++ } ++ ++ #[test] ++ fn missing_helper_reports_launch_error() { ++ assert!(run_helper( ++ Path::new("/does/not/exist/librashader-compiler"), ++ b"", ++ Duration::from_secs(1) ++ ) ++ .is_err()); ++ } ++ ++ #[test] ++ fn timeout_terminates_and_reaps_child() { ++ let start = Instant::now(); ++ // Use the system sleep utility for a deterministic long-running child ++ // without introducing a test shell script. ++ let child = Command::new("/bin/sleep").arg("30").spawn().unwrap(); ++ let pid = child.id(); ++ assert!(wait_for_child(child, Duration::from_millis(20)).is_err()); ++ assert!(start.elapsed() < Duration::from_secs(5)); ++ // wait() in Drop reaps it; a process with this PID is no longer waitable. ++ let output = Command::new("/bin/ps") ++ .arg("-p") ++ .arg(pid.to_string()) ++ .output() ++ .unwrap(); ++ assert!(!output.status.success()); ++ } ++} +--- a/librashader-runtime-mtl/src/error.rs ++++ b/librashader-runtime-mtl/src/error.rs +@@ -36,6 +36,8 @@ + FailedToCreateTexture, + #[error("couldn't create command buffer")] + FailedToCreateCommandBuffer, ++ #[error("external shader compiler: {0}")] ++ CompilerHelper(String), + #[error("unreachable")] + Infallible(#[from] std::convert::Infallible), + } +--- a/librashader-runtime-mtl/src/filter_chain.rs ++++ b/librashader-runtime-mtl/src/filter_chain.rs +@@ -54,7 +54,20 @@ + pub fn compile_passes<'a>( + shaders: Vec, + textures: impl Iterator, ++ helper: Option<&Path>, + ) -> Result<(Vec, ShaderSemantics), FilterChainError> { ++ if let Some(helper) = helper { ++ let mut compiled = crate::compiler_process::compile(&shaders, helper)?.into_iter(); ++ return MSL::compile_preset_passes_with_compiler::< ++ SpirvCompilation, ++ SpirvCross, ++ FilterChainError, ++ >(shaders, textures, |_| { ++ compiled ++ .next() ++ .ok_or_else(|| FilterChainError::CompilerHelper("missing compiled pass".into())) ++ }); ++ } + let (passes, semantics) = + MSL::compile_preset_passes::( + shaders, textures, +@@ -149,6 +162,32 @@ + Ok(chain) + } + ++ /// Create a Metal chain using a short-lived GLSL compiler process. No glslang ++ /// compiler instance is initialized in this process on this path. ++ pub fn load_from_preset_with_compiler( ++ mut preset: ShaderPreset, ++ queue: &ProtocolObject, ++ options: Option<&FilterChainOptionsMetal>, ++ helper: &Path, ++ ) -> error::Result { ++ let textures = std::mem::take(&mut preset.textures); ++ let preset = ShaderPresetPack::load_from_preset_serial::(preset)?; ++ let cmd = queue ++ .commandBuffer() ++ .ok_or(FilterChainError::FailedToCreateCommandBuffer)?; ++ let chain = Self::load_from_pack_deferred_internal( ++ preset, ++ queue.device(), ++ &cmd, ++ options, ++ Some(textures), ++ Some(helper), ++ )?; ++ cmd.commit(); ++ cmd.waitUntilCompleted(); ++ Ok(chain) ++ } ++ + /// Load a filter chain from a pre-parsed `ShaderPreset`. + pub fn load_from_pack( + preset: ShaderPresetPack, +@@ -159,8 +198,14 @@ + .commandBuffer() + .ok_or(FilterChainError::FailedToCreateCommandBuffer)?; + +- let filter_chain = +- Self::load_from_pack_deferred_internal(preset, queue.device(), &cmd, options, None)?; ++ let filter_chain = Self::load_from_pack_deferred_internal( ++ preset, ++ queue.device(), ++ &cmd, ++ options, ++ None, ++ None, ++ )?; + + cmd.commit(); + cmd.waitUntilCompleted(); +@@ -214,7 +259,7 @@ + let filters: Vec> = passes + .into_iter() + .enumerate() +- .map(|(index, (config, mut reflect))| { ++ .map(|(index, (mut config, mut reflect))| { + let reflection = reflect.reflect(index, semantics)?; + let msl = reflect.compile(Some(MslVersion::new(2, 0, 0)))?; + +@@ -248,6 +293,10 @@ + }, + )?; + ++ // Render passes only use parameters and format after compilation. ++ // Release large preprocessed GLSL strings instead of retaining capacity. ++ config.data.vertex = String::new(); ++ config.data.fragment = String::new(); + Ok(FilterPass { + reflection, + uniform_storage, +@@ -319,7 +368,14 @@ + ) -> error::Result { + let textures = std::mem::take(&mut preset.textures); + let preset = ShaderPresetPack::load_from_preset::(preset)?; +- Self::load_from_pack_deferred_internal(preset, queue.device(), cmd, options, Some(textures)) ++ Self::load_from_pack_deferred_internal( ++ preset, ++ queue.device(), ++ cmd, ++ options, ++ Some(textures), ++ None, ++ ) + } + + /// Load a filter chain from a pre-parsed `ShaderPreset`, deferring and GPU-side initialization +@@ -335,7 +391,7 @@ + cmd: &ProtocolObject, + options: Option<&FilterChainOptionsMetal>, + ) -> error::Result { +- Self::load_from_pack_deferred_internal(preset, queue.device(), &cmd, options, None) ++ Self::load_from_pack_deferred_internal(preset, queue.device(), &cmd, options, None, None) + } + + /// Load a filter chain from a pre-parsed `ShaderPreset`, deferring and GPU-side initialization +@@ -351,13 +407,14 @@ + cmd: &ProtocolObject, + options: Option<&FilterChainOptionsMetal>, + texture_paths: Option>, ++ helper: Option<&Path>, + ) -> error::Result { + let config = RuntimeParameters::new(&preset); + 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 (passes, semantics) = compile_passes(preset.passes, textures.metadata(), helper)?; + + let filters = Self::init_passes(&device, passes, &semantics)?; + +--- a/librashader-runtime-mtl/src/lib.rs ++++ b/librashader-runtime-mtl/src/lib.rs +@@ -7,6 +7,7 @@ + #![cfg_attr(feature = "nightly", feature(type_alias_impl_trait))] + + mod buffer; ++mod compiler_process; + mod draw_quad; + mod filter_chain; + mod filter_pass; diff --git a/Vendor/CLibrashader/screenslanger.h b/Vendor/CLibrashader/screenslanger.h new file mode 100644 index 0000000..05267b8 --- /dev/null +++ b/Vendor/CLibrashader/screenslanger.h @@ -0,0 +1,9 @@ +// ScreenSlanger's extension to the pinned runtime. The upstream header is unchanged. +#if defined(__APPLE__) && defined(__OBJC__) +typedef libra_error_t (*PFN_screenslanger_mtl_filter_chain_create_with_compiler)( + libra_shader_preset_t *preset, + id queue, + const struct filter_chain_mtl_opt_t *options, + const char *compiler_path, + libra_mtl_filter_chain_t *out); +#endif diff --git a/Vendor/CLibrashader/shim.h b/Vendor/CLibrashader/shim.h index 70950a6..881bf3e 100644 --- a/Vendor/CLibrashader/shim.h +++ b/Vendor/CLibrashader/shim.h @@ -1,2 +1,3 @@ #define LIBRA_RUNTIME_METAL #include "librashader.h" +#include "screenslanger.h" diff --git a/scripts/bundle-shader-runtimes.sh b/scripts/bundle-shader-runtimes.sh index 77f82b6..077ec31 100755 --- a/scripts/bundle-shader-runtimes.sh +++ b/scripts/bundle-shader-runtimes.sh @@ -11,18 +11,19 @@ set -euo pipefail : "${TARGET_TEMP_DIR:?This script is an Xcode build phase.}" slang_version="2026.18" -librashader_version="0.12.0-screenslanger.2" +librashader_version="0.12.0-screenslanger.3" tools_directory="$HOME/Library/Application Support/ScreenSlanger/Tools" slang_source="$tools_directory/slang/$slang_version" librashader_directory="$tools_directory/librashader/$librashader_version" librashader_source="$librashader_directory/librashader.dylib" +librashader_compiler_source="$librashader_directory/librashader-compiler" -if [[ ! -x "$slang_source/bin/slangc" || ! -f "$librashader_source" ]]; then +if [[ ! -x "$slang_source/bin/slangc" || ! -f "$librashader_source" || ! -x "$librashader_compiler_source" ]]; then echo "error: Shader dependencies are missing. Run scripts/setup-dependencies.sh from the checkout, then build again." >&2 exit 1 fi -# Validate the installed library before changing its install name or signing it. +# Validate both installed binaries before changing install names or signing them. # Source hashes remain valid in the app; the installation's binary hash does not. ( cd "$librashader_directory" @@ -34,8 +35,9 @@ package_staging="$TARGET_TEMP_DIR/ShaderRuntimes" slang_bundle="$package_staging/Slang.app" slang_destination="$slang_bundle/Contents" frameworks_destination="$package_staging/Frameworks" +helpers_destination="$package_staging/Helpers" notices_destination="$package_staging/ThirdParty" -mkdir -p "$slang_destination/MacOS" "$slang_destination/lib" "$frameworks_destination" "$notices_destination/Slang" "$notices_destination/librashader" +mkdir -p "$slang_destination/MacOS" "$slang_destination/lib" "$frameworks_destination" "$helpers_destination" "$notices_destination/Slang" "$notices_destination/librashader" # Keep Slang's executable/../lib relationship intact for dynamically loaded backends. # A nested helper .app gives code signing a valid bundle boundary around its data modules. @@ -44,6 +46,7 @@ rsync -a --delete "$slang_source/bin/slangc" "$slang_destination/MacOS/" cp "$SRCROOT/scripts/Slang-Info.plist" "$slang_destination/Info.plist" rsync -a --delete --exclude cmake --exclude pkgconfig "$slang_source/lib/" "$slang_destination/lib/" cp "$librashader_source" "$frameworks_destination/librashader.dylib" +cp "$librashader_compiler_source" "$helpers_destination/librashader-compiler" cp "$slang_source/LICENSE" "$notices_destination/Slang/LICENSE" rsync -a --delete "$slang_source/LICENSES/" "$notices_destination/Slang/LICENSES/" cp "$SRCROOT/Vendor/CLibrashader/LICENSE-MPL-2.0.md" "$notices_destination/librashader/LICENSE-MPL-2.0.md" @@ -52,6 +55,7 @@ cp "$SRCROOT/Vendor/CLibrashader/BUILDING.md" "$notices_destination/librashader/ 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 \ + 0004-isolate-retroarch-compiler.patch \ BUILD-INFO.txt SHA256SUMS; do cp "$librashader_directory/$source_file" "$notices_destination/librashader/$source_file" done @@ -110,16 +114,19 @@ fi # This library is loaded by absolute bundle path, but give it a relocatable identity too. /usr/bin/install_name_tool -id '@rpath/librashader.dylib' "$frameworks_destination/librashader.dylib" prepare_binary "$frameworks_destination/librashader.dylib" +prepare_binary "$helpers_destination/librashader-compiler" # Xcode permits only exact declared output paths, even for directories. Signing and # rsync's temporary work happen in TARGET_TEMP_DIR; final copies write declared files # in place so the build phase can keep user-script sandboxing enabled. final_slang="$TARGET_BUILD_DIR/$CONTENTS_FOLDER_PATH/Helpers/Slang.app" +final_helpers="$TARGET_BUILD_DIR/$CONTENTS_FOLDER_PATH/Helpers" final_frameworks="$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH" final_notices="$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/ThirdParty" mkdir -p "$final_slang" "$final_frameworks" "$final_notices" rsync -a --inplace "$slang_bundle/" "$final_slang/" cp "$frameworks_destination/librashader.dylib" "$final_frameworks/librashader.dylib" +cp "$helpers_destination/librashader-compiler" "$final_helpers/librashader-compiler" rsync -a --inplace "$notices_destination/" "$final_notices/" echo "Bundled Slang $slang_version and librashader $librashader_version into the application." diff --git a/scripts/setup-dependencies.sh b/scripts/setup-dependencies.sh index ca84aa4..58ecdfe 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.2" +librashader_version="0.12.0-screenslanger.3" 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 0003-stream-metal-lut-loading.patch) +librashader_patches=(0001-skip-unused-final-target.patch 0002-compact-grayscale-luts.patch 0003-stream-metal-lut-loading.patch 0004-isolate-retroarch-compiler.patch) # Reviewed patches against the checksum-pinned source archive, not upstream binaries. -librashader_patch_sha256=(f5a9c09c0f7a72059eb536fbd63eacbc513bea5fe964e70f3a122ae61bfd90fb 2fd5bd08afabf7f0ee6c8329fcfda778b6460001d7de50145c32af329138fe5e 3e3c574a0b8940f04502bece08442ab297bc1b9a7de89dff412b51566de9acfe) +librashader_patch_sha256=(f5a9c09c0f7a72059eb536fbd63eacbc513bea5fe964e70f3a122ae61bfd90fb 2fd5bd08afabf7f0ee6c8329fcfda778b6460001d7de50145c32af329138fe5e 3e3c574a0b8940f04502bece08442ab297bc1b9a7de89dff412b51566de9acfe 1956179bfa7f89e4318c1aa3982656d24ea63e6dffe9494d87ad64a3f27dac8f) # Checksums published with the official v2026.18 GitHub release assets. # https://github.com/shader-slang/slang/releases/tag/v2026.18 case "$(uname -m)" in @@ -89,11 +89,12 @@ 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]} +Patch 4 SHA-256: ${librashader_patch_sha256[3]} Rust toolchain: $librashader_rust_version Architecture: $librashader_arch MACOSX_DEPLOYMENT_TARGET: 27.0 CARGO_PROFILE_OPTIMIZED_STRIP: none -Build: cargo +$librashader_rust_version build --locked --profile optimized --package librashader-capi --no-default-features --features runtime-metal +Build: cargo +$librashader_rust_version build --locked --profile optimized --package librashader-capi --package librashader-reflect --bin librashader-compiler --lib --no-default-features --features librashader-capi/runtime-metal,librashader-reflect/glslang-in BUILD_INFO )" mkdir -p "$librashader_root" @@ -132,15 +133,19 @@ if [[ ! -e "$librashader_directory" ]]; then CARGO_PROFILE_OPTIMIZED_STRIP=none \ CARGO_TARGET_DIR="$staging_directory/target" \ cargo +"$librashader_rust_version" build --locked --profile optimized \ - --package librashader-capi --no-default-features --features runtime-metal + --package librashader-capi --package librashader-reflect \ + --bin librashader-compiler --lib --no-default-features \ + --features librashader-capi/runtime-metal,librashader-reflect/glslang-in ) cp "$staging_directory/target/optimized/liblibrashader_capi.dylib" "$staging_directory/install/librashader.dylib" + cp "$staging_directory/target/optimized/librashader-compiler" "$staging_directory/install/librashader-compiler" /usr/bin/lipo "$staging_directory/install/librashader.dylib" -verify_arch "$librashader_arch" + /usr/bin/lipo "$staging_directory/install/librashader-compiler" -verify_arch "$librashader_arch" printf '%s\n' "$expected_build_info" > "$staging_directory/install/BUILD-INFO.txt" ( cd "$staging_directory/install" shasum -a 256 "$source_archive" "${librashader_patches[@]}" > SHA256SUMS - shasum -a 256 librashader.dylib > RUNTIME.sha256 + shasum -a 256 librashader.dylib librashader-compiler > RUNTIME.sha256 ) mv "$staging_directory/install" "$librashader_directory" rm -rf "$staging_directory" @@ -162,3 +167,4 @@ cp "$project_dir/Vendor/CLibrashader/BUILDING.md" "$librashader_directory/BUILDI printf '\nInstalled ScreenSlanger dependencies:\n' printf 'slang %s (%s)\n' "$installed_version" "$slang_root/current/bin/slangc" printf 'librashader %s (%s)\n' "$librashader_version" "$librashader_directory/librashader.dylib" +printf 'librashader compiler (%s)\n' "$librashader_directory/librashader-compiler" diff --git a/scripts/shader-runtimes-inputs.xcfilelist b/scripts/shader-runtimes-inputs.xcfilelist index fedf600..b16c4e0 100644 --- a/scripts/shader-runtimes-inputs.xcfilelist +++ b/scripts/shader-runtimes-inputs.xcfilelist @@ -7,11 +7,13 @@ $(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.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 +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3/librashader.dylib +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3/librashader-compiler +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3/RUNTIME.sha256 +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3/SHA256SUMS +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3/BUILD-INFO.txt +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3/librashader-v0.12.0-source.tar.gz +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3/0001-skip-unused-final-target.patch +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3/0002-compact-grayscale-luts.patch +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3/0003-stream-metal-lut-loading.patch +$(HOME)/Library/Application Support/ScreenSlanger/Tools/librashader/0.12.0-screenslanger.3/0004-isolate-retroarch-compiler.patch diff --git a/scripts/shader-runtimes-outputs.xcfilelist b/scripts/shader-runtimes-outputs.xcfilelist index 165a89b..a385e24 100644 --- a/scripts/shader-runtimes-outputs.xcfilelist +++ b/scripts/shader-runtimes-outputs.xcfilelist @@ -1,4 +1,5 @@ $(TARGET_BUILD_DIR)/$(CONTENTS_FOLDER_PATH)/Helpers +$(TARGET_BUILD_DIR)/$(CONTENTS_FOLDER_PATH)/Helpers/librashader-compiler $(TARGET_BUILD_DIR)/$(CONTENTS_FOLDER_PATH)/Helpers/Slang.app $(TARGET_BUILD_DIR)/$(CONTENTS_FOLDER_PATH)/Helpers/Slang.app/Contents $(TARGET_BUILD_DIR)/$(CONTENTS_FOLDER_PATH)/Helpers/Slang.app/Contents/Info.plist @@ -74,5 +75,6 @@ $(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/ThirdParty/librashader/ $(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 $(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. +$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/ThirdParty/librashader/0004-isolate-retroarch-compiler.patch +// Pinned Slang 2026.18 + librashader 0.12.0-screenslanger.3 bundle contents. // Xcode requires every file and directory explicitly for sandboxed script outputs. -- 2.51.2