├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── lib ├── base.dart ├── build.dart └── environment.dart ├── pubspec.yaml └── test └── environment_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | build/ 30 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "43f3252c824603648b9bd738f27ca00ba9c1ad30" 8 | channel: "[user-branch]" 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 2 | 3 | * Add `buildShaderBundleJson` build hook utility. 4 | * Document usage instructions in the readme. 5 | 6 | ## 0.1.1 7 | 8 | * Fix working directory for impellerc invocation. 9 | 10 | ## 0.1.2 11 | 12 | * Fix SDK path resolution for puro users. 13 | 14 | ## 0.1.3 15 | 16 | * Relax native_assets_cli dependency pin. 17 | 18 | ## 0.1.4 19 | 20 | * Pin native_assets_cli to <0.9.0. 21 | (https://github.com/bdero/flutter_gpu_shaders/issues/3) 22 | 23 | ## 0.2.0 24 | 25 | * Update to native_assets_cli 0.9.0. 26 | Breaking: `BuildOutput` is now `BuildOutputBuilder` 27 | 28 | ## 0.2.1 29 | 30 | * Bump native_assets_cli to 0.10.0. 31 | 32 | ## 0.3.0 33 | 34 | * Update to native_assets_cli to 0.13.0. 35 | (https://github.com/bdero/flutter_gpu_shaders/issues/6) 36 | Breaking: `BuildConfig` is now `BuildInput` 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 The Flutter Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Build tools for Flutter GPU shader bundles/libraries. 2 | 3 | ## Features 4 | 5 | Use native asset build hooks to import Flutter GPU shader bundle assets. 6 | 7 | ## Getting started 8 | 9 | 1. This package requires the experimental "native assets" feature to be enabled. Enable it with the following command: 10 | ```bash 11 | flutter config --enable-native-assets 12 | ``` 13 | 2. Place some Flutter GPU shaders in your project. For this example, we'll assume the existence of two shaders: `shaders/my_cool_shader.vert` and `shaders/my_cool_shader.frag`. 14 | 3. Create a shader bundle manifest file in your project. The filename must end with `.shaderbundle.json`. For this example, we'll assume the following file is saved as `my_cool_bundle.shaderbundle.json`: 15 | ```json 16 | { 17 | "CoolVertex": { 18 | "type": "vertex", 19 | "file": "shaders/my_cool_shader.vert" 20 | }, 21 | "CoolFragment": { 22 | "type": "fragment", 23 | "file": "shaders/my_cool_shader.frag" 24 | } 25 | } 26 | ``` 27 | 4. Next, define a build hook in your project that builds the shader bundle using `buildShaderBundleJson`. The build hook must be named `hook/build.dart` in your project; this script will be automatically invoked by Flutter when the "native assets" feature is enabled: 28 | ```dart 29 | import 'package:native_assets_cli/native_assets_cli.dart'; 30 | import 'package:flutter_gpu_shaders/build.dart'; 31 | 32 | void main(List args) async { 33 | await build(args, (config, output) async { 34 | await buildShaderBundleJson( 35 | buildConfig: config, 36 | buildOutput: output, 37 | manifestFileName: 'my_cool_bundle.shaderbundle.json'); 38 | }); 39 | } 40 | ``` 41 | 5. In your project's `pubspec.yaml`, add an asset import rule to package the built shader bundles (this will become unnecessary once "native assets" supports `DataAsset` in a future release of Flutter): 42 | ```yaml 43 | flutter: 44 | assets: 45 | - build/shaderbundles/*.shaderbundle.json 46 | ``` 47 | 6. You can now import the built shader bundle as a library using `gpu.ShaderLibrary.fromAsset` in your project. For example: 48 | ```dart 49 | import 'package:flutter_gpu/gpu.dart' as gpu; 50 | 51 | final String _kBaseShaderBundlePath = 52 | 'packages/my_project/build/shaderbundles/my_cool_bundle.shaderbundle'; 53 | 54 | gpu.ShaderLibrary? _baseShaderLibrary = null; 55 | gpu.ShaderLibrary get baseShaderLibrary { 56 | if (_baseShaderLibrary != null) { 57 | return _baseShaderLibrary!; 58 | } 59 | _baseShaderLibrary = gpu.ShaderLibrary.fromAsset(_kBaseShaderBundlePath); 60 | if (_baseShaderLibrary != null) { 61 | return _baseShaderLibrary!; 62 | } 63 | 64 | throw Exception( 65 | "Failed to load base shader bundle! ($_kBaseShaderBundlePath)"); 66 | } 67 | ``` -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /lib/base.dart: -------------------------------------------------------------------------------- 1 | library flutter_gpu_shaders; 2 | 3 | import 'package:logging/logging.dart'; 4 | 5 | final logger = Logger(''); 6 | -------------------------------------------------------------------------------- /lib/build.dart: -------------------------------------------------------------------------------- 1 | library flutter_gpu_shaders; 2 | 3 | import 'dart:convert' as convert; 4 | import 'dart:io'; 5 | 6 | import 'package:native_assets_cli/code_assets_testing.dart'; 7 | import 'package:native_assets_cli/native_assets_cli.dart'; 8 | 9 | import 'package:flutter_gpu_shaders/environment.dart'; 10 | 11 | /// Loads a shader bundle manifest file and builds a shader bundle. 12 | Future _buildShaderBundleJson({ 13 | required Uri packageRoot, 14 | required Uri inputManifestFilePath, 15 | required Uri outputBundleFilePath, 16 | }) async { 17 | ///////////////////////////////////////////////////////////////////////////// 18 | /// 1. Parse the manifest file. 19 | /// 20 | 21 | final manifest = 22 | await File(inputManifestFilePath.toFilePath()).readAsString(); 23 | final decodedManifest = convert.json.decode(manifest); 24 | String reconstitutedManifest = convert.json.encode(decodedManifest); 25 | 26 | //throw Exception(reconstitutedManifest); 27 | 28 | ///////////////////////////////////////////////////////////////////////////// 29 | /// 2. Build the shader bundle. 30 | /// 31 | 32 | final impellercExec = await findImpellerC(); 33 | final shaderLibPath = impellercExec.resolve('./shader_lib'); 34 | final impellercArgs = [ 35 | '--sl=${outputBundleFilePath.toFilePath()}', 36 | '--shader-bundle=$reconstitutedManifest', 37 | '--include=${inputManifestFilePath.resolve('./').toFilePath()}', 38 | '--include=${shaderLibPath.toFilePath()}', 39 | ]; 40 | 41 | final impellerc = Process.runSync(impellercExec.toFilePath(), impellercArgs, 42 | workingDirectory: packageRoot.toFilePath()); 43 | if (impellerc.exitCode != 0) { 44 | throw Exception( 45 | 'Failed to build shader bundle: ${impellerc.stderr}\n${impellerc.stdout}'); 46 | } 47 | } 48 | 49 | /// Build a Flutter GPU shader bundle/library from a JSON manifest file. 50 | /// 51 | /// The [buildConfig] and [buildOutput] are provided by the build hook system. 52 | /// 53 | /// The [manifestFileName] is the path to the JSON manifest file, which is 54 | /// relative to the package root where the build hook resides. 55 | /// 56 | /// The [manifestFileName] must end with ".shaderbundle.json". 57 | /// 58 | /// The built shader bundle will be written to 59 | /// `build/shaderbundles/[name].shaderbundle`, 60 | /// relative to the package root where the build hook resides. 61 | /// 62 | /// Example usage: 63 | /// 64 | /// hook/build.dart 65 | /// ```dart 66 | /// void main(List args) async { 67 | /// await build(args, (config, output) async { 68 | /// await buildShaderBundleJson( 69 | /// buildConfig: config, 70 | /// buildOutput: output, 71 | /// manifestFileName: 'my_cool_bundle.shaderbundle.json'); 72 | /// }); 73 | /// } 74 | /// ``` 75 | /// 76 | /// my_cool_bundle.shaderbundle.json 77 | /// ```json 78 | /// { 79 | /// "SimpleVertex": { 80 | /// "type": "vertex", 81 | /// "file": "shaders/my_cool_shader.vert" 82 | /// } 83 | /// } 84 | /// ``` 85 | Future buildShaderBundleJson( 86 | {required BuildInput buildInput, 87 | required BuildOutputBuilder buildOutput, 88 | required String manifestFileName}) async { 89 | String outputFileName = Uri(path: manifestFileName).pathSegments.last; 90 | if (!outputFileName.endsWith('.shaderbundle.json')) { 91 | throw Exception( 92 | 'Shader bundle manifest file names must end with ".shaderbundle.json".'); 93 | } 94 | if (outputFileName.length <= '.shaderbundle.json'.length) { 95 | throw Exception( 96 | 'Invalid shader bundle manifest file name: $outputFileName'); 97 | } 98 | if (outputFileName.endsWith('.json')) { 99 | outputFileName = outputFileName.substring(0, outputFileName.length - 5); 100 | } 101 | 102 | // TODO(bdero): Register DataAssets instead of outputting to the project directory once it's possible to do so. 103 | //final outDir = config.outputDirectory; 104 | final outDir = Directory.fromUri( 105 | buildInput.packageRoot.resolve('build/shaderbundles/')); 106 | await outDir.create(recursive: true); 107 | final packageRoot = buildInput.packageRoot; 108 | 109 | final inFile = packageRoot.resolve(manifestFileName); 110 | final outFile = outDir.uri.resolve(outputFileName); 111 | 112 | await _buildShaderBundleJson( 113 | packageRoot: packageRoot, 114 | inputManifestFilePath: inFile, 115 | outputBundleFilePath: outFile); 116 | } 117 | -------------------------------------------------------------------------------- /lib/environment.dart: -------------------------------------------------------------------------------- 1 | library flutter_gpu_shaders; 2 | 3 | import 'dart:io'; 4 | 5 | import 'package:flutter_gpu_shaders/base.dart'; 6 | 7 | const _macosHostArtifacts = 'darwin-x64'; 8 | const _linuxHostArtifacts = 'linux-x64'; 9 | const _windowsHostArtifacts = 'windows-x64'; 10 | 11 | const _impellercLocations = [ 12 | '$_macosHostArtifacts/impellerc', 13 | '$_linuxHostArtifacts/impellerc', 14 | '$_windowsHostArtifacts/impellerc.exe', 15 | ]; 16 | 17 | /// Locate the engine artifacts cache directory in the Flutter SDK. 18 | Uri findEngineArtifactsDir({String? dartPath}) { 19 | // Could be: 20 | // `/path/to/flutter/bin/cache/dart-sdk/bin/dart` 21 | // `/path/to/flutter/bin/cache/artifacts/engine/darwin-x64/flutter_tester` 22 | // `/path/to/.puro/shared/caches/94cf8c8fad31206e440611e309757a5a9b3be712/dart-sdk/bin/dart` 23 | final Uri dartExec = Uri.file(dartPath ?? Platform.resolvedExecutable); 24 | logger.info('Dart executable: `${dartExec.toFilePath()}`'); 25 | 26 | Uri? cacheDir; 27 | // Search backwards through the segment list until finding `bin` and `cache` in sequence. 28 | for (var i = dartExec.pathSegments.length - 1; i >= 0; i--) { 29 | if (dartExec.pathSegments[i] == 'dart-sdk' || 30 | dartExec.pathSegments[i] == 'artifacts') { 31 | // Note: The final empty string denotes that this is a directory path. 32 | cacheDir = dartExec.replace( 33 | pathSegments: dartExec.pathSegments.sublist(0, i) + ['']); 34 | break; 35 | } 36 | } 37 | if (cacheDir == null) { 38 | throw Exception( 39 | 'Unable to find Flutter SDK cache directory! Dart executable: `${dartExec.toFilePath()}`'); 40 | } 41 | // We should now have a path of `/path/to/flutter/bin/cache/`. 42 | 43 | final engineArtifactsDir = cacheDir 44 | .resolve('./artifacts/engine/'); // Note: The final slash is important. 45 | logger.info( 46 | 'Flutter SDK cache directory: `${engineArtifactsDir.toFilePath()}`'); 47 | 48 | return engineArtifactsDir; 49 | } 50 | 51 | /// Locate the ImpellerC offline shader compiler in the engine artifacts cach 52 | /// directory. 53 | Future findImpellerC() async { 54 | ///////////////////////////////////////////////////////////////////////////// 55 | /// 1. If the `IMPELLERC` environment variable is set, use it. 56 | /// 57 | 58 | const impellercEnvVar = String.fromEnvironment('IMPELLERC', defaultValue: ''); 59 | if (impellercEnvVar != '') { 60 | logger.info('IMPELLERC environment variable: `$impellercEnvVar`'); 61 | if (!await File(impellercEnvVar).exists()) { 62 | throw Exception( 63 | 'IMPELLERC environment variable is set, but it doesn\'t point to a valid file!'); 64 | } 65 | return Uri.file(impellercEnvVar); 66 | } 67 | 68 | ///////////////////////////////////////////////////////////////////////////// 69 | /// 3. Search for the `impellerc` binary within the host-specific artifacts. 70 | /// 71 | 72 | Uri engineArtifactsDir = findEngineArtifactsDir(); 73 | 74 | // No need to get fancy. Just search all the possible directories rather than 75 | // picking the correct one for the specific host type. 76 | Uri? found; 77 | List tried = []; 78 | logger.info('Searching for impellerc in artifacts directories...'); 79 | for (final variant in _impellercLocations) { 80 | logger.info(' Checking `$variant`...'); 81 | final impellercPath = engineArtifactsDir.resolve(variant); 82 | if (await File(impellercPath.toFilePath()).exists()) { 83 | found = impellercPath; 84 | break; 85 | } 86 | tried.add(impellercPath); 87 | } 88 | if (found == null) { 89 | throw Exception( 90 | 'Unable to find impellerc! Tried the following locations: $tried'); 91 | } 92 | 93 | return found; 94 | } 95 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_gpu_shaders 2 | description: 'Build tools for Flutter GPU shader bundles/libraries.' 3 | version: 0.3.0 4 | homepage: https://github.com/bdero/flutter_gpu_shaders 5 | 6 | environment: 7 | sdk: ^3.4.0 8 | flutter: '>=1.17.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | logging: ^1.2.0 14 | # https://github.com/bdero/flutter_gpu_shaders/issues/3 15 | native_assets_cli: '>=0.13.0 <0.14.0' 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | flutter_lints: ^4.0.0 21 | 22 | flutter: 23 | -------------------------------------------------------------------------------- /test/environment_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import 'package:flutter_gpu_shaders/environment.dart'; 4 | 5 | void main() { 6 | test('findEngineArtifactsDir returns expected segments.', () { 7 | List pathVariations = [ 8 | '/path/to/flutter/bin/cache/dart-sdk/bin/dart', 9 | '/path/to/flutter/bin/cache/artifacts/engine/darwin-x64/flutter_tester', 10 | '/path/to/.puro/shared/caches/94cf8c8fad31206e440611e309757a5a9b3be712/dart-sdk/bin/dart', 11 | ]; 12 | for (String path in pathVariations) { 13 | Uri result = findEngineArtifactsDir(dartPath: path); 14 | expect(result.pathSegments.sublist(result.pathSegments.length - 3), 15 | ['artifacts', 'engine', '']); 16 | } 17 | }); 18 | 19 | test('findImpellerC doesn\'t throw.', () async { 20 | Uri result = await findImpellerC(); 21 | expect(result.pathSegments.last, 'impellerc'); 22 | }); 23 | } 24 | --------------------------------------------------------------------------------