From 693febcd4b9aa7c6e50d37ccc83e2dfe5caa72e3 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Sat, 1 Aug 2026 01:42:51 -0700 Subject: [PATCH] Make DataAssets the primary shader bundle path --- CHANGELOG.md | 1 + README.md | 78 ++++++++++++++++++++++++-------------------- lib/build.dart | 39 ++++++++++++++++++---- test/build_test.dart | 62 ++++++++++++++++++++++++++++++----- 4 files changed, 128 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2389fc1..abc7c3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.5.2 +* Made DataAssets the recommended shader bundle path and warn on unmanaged legacy outputs. * Track the `impellerc` binary as a build input, so bundles rebuild whenever the engine artifacts change. * Write an engine stamp beside each bundle, so a shared pub-cache bundle rewritten by another project (or Flutter SDK) triggers a rebuild instead of shipping a wrong-engine bundle. diff --git a/README.md b/README.md index 1118de5..c6ea44a 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,22 @@ Build tools for Flutter GPU shader bundles/libraries. ## Features -Use native asset build hooks to import Flutter GPU shader bundle assets. +Use build hooks to compile Flutter GPU shaders and register the resulting +bundle as a managed Dart DataAsset. ## Getting started -1. 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`. -2. 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`: +1. Enable Dart DataAssets once for your Flutter SDK. + + ```sh + flutter config --enable-dart-data-assets + ``` + +2. Place Flutter GPU shader sources in your project. This example uses + `shaders/my_cool_shader.vert` and `shaders/my_cool_shader.frag`. + +3. Create a source manifest named `my_cool_bundle.shaderbundle.json`. + ```json { "CoolVertex": { @@ -20,7 +30,15 @@ Use native asset build hooks to import Flutter GPU shader bundle assets. } } ``` -3. 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: + + The manifest and shader sources are portable build inputs and belong in + version control. The generated `.shaderbundle` is compiled by the active + Flutter engine's `impellerc`. It is an engine-coupled intermediary and must + not be committed. + +4. Define `hook/build.dart` and register the generated bundle as a required + DataAsset. + ```dart import 'package:hooks/hooks.dart'; import 'package:flutter_gpu_shaders/build.dart'; @@ -28,45 +46,28 @@ Use native asset build hooks to import Flutter GPU shader bundle assets. void main(List args) async { await build(args, (config, output) async { await buildShaderBundleJson( - buildInput: config, - buildOutput: output, - manifestFileName: 'my_cool_bundle.shaderbundle.json'); + buildInput: config, + buildOutput: output, + manifestFileName: 'my_cool_bundle.shaderbundle.json', + assetMode: ShaderBundleAssetMode.dataAssetsRequired, + ); }); } ``` - `buildShaderBundleJson` always declares the manifest and directly listed - shader files as build dependencies. With Flutter SDKs whose `impellerc` - supports `--depfile` for shader bundles, it also declares transitive - `#include` dependencies from the generated depfile. Older `impellerc` - builds continue to use the manifest scan fallback. -4. In your project's `pubspec.yaml`, add the built shader bundle as an asset: - ```yaml - flutter: - assets: - - build/shaderbundles/my_cool_bundle.shaderbundle - ``` -5. If your Flutter toolchain supports Dart DataAssets, you can opt in to - registering the generated bundle with the Flutter asset bundle instead of - listing it in `pubspec.yaml`: - ```dart - await buildShaderBundleJson( - buildInput: config, - buildOutput: output, - manifestFileName: 'my_cool_bundle.shaderbundle.json', - assetMode: ShaderBundleAssetMode.dataAssetsIfAvailable, - ); - ``` - With DataAssets enabled, the result's `flutterAssetKey` is the key to pass - to `gpu.ShaderLibrary.fromAsset`. When DataAssets are unavailable, - `dataAssetsIfAvailable` falls back to the legacy file output. Use - `ShaderBundleAssetMode.dataAssetsRequired` to fail fast with guidance - instead. -6. You can now import the built shader bundle as a library using `gpu.ShaderLibrary.fromAsset` in your project. For example: + + The hook declares the manifest and directly listed shader files as build + dependencies. With Flutter SDKs whose `impellerc` supports `--depfile` for + shader bundles, it also declares transitive `#include` dependencies. + +5. Load the DataAsset using its Flutter asset key. The default key is + `packages//flutter_gpu_shaders/shaderbundles/.shaderbundle`. + ```dart import 'package:flutter_gpu/gpu.dart' as gpu; final String _kBaseShaderBundlePath = - 'build/shaderbundles/my_cool_bundle.shaderbundle'; + 'packages/my_app/flutter_gpu_shaders/shaderbundles/' + 'my_cool_bundle.shaderbundle'; gpu.ShaderLibrary? _baseShaderLibrary; gpu.ShaderLibrary get baseShaderLibrary { @@ -82,3 +83,8 @@ Use native asset build hooks to import Flutter GPU shader bundle assets. "Failed to load base shader bundle! ($_kBaseShaderBundlePath)"); } ``` + +Do not list generated shader bundles in `flutter.assets`. +`ShaderBundleAssetMode.legacyOnly` and `dataAssetsIfAvailable` remain available +for older Flutter toolchains. They emit a warning when the generated bundle is +not managed as a DataAsset. diff --git a/lib/build.dart b/lib/build.dart index 998daf7..303209d 100644 --- a/lib/build.dart +++ b/lib/build.dart @@ -66,6 +66,8 @@ String flutterDataAssetKey({required String package, required String name}) => /// /// Returns the emitted asset metadata, or `null` when [assetMode] falls back to /// legacy output because data assets are not available. +/// Set [warnIfLegacy] to `false` only when a higher-level hook registers the +/// generated bundle itself. ShaderBundleBuildResult? registerShaderBundleDataAsset({ required BuildInput buildInput, required BuildOutputBuilder buildOutput, @@ -73,8 +75,18 @@ ShaderBundleBuildResult? registerShaderBundleDataAsset({ required String legacyAssetKey, required ShaderBundleAssetMode assetMode, String? dataAssetName, + bool warnIfLegacy = true, }) { if (assetMode == ShaderBundleAssetMode.legacyOnly) { + if (warnIfLegacy) { + // ignore: avoid_print + print( + 'flutter_gpu_shaders warning, using legacy generated assets for ' + '"$legacyAssetKey". Shader bundles are tied to the active Flutter ' + 'engine. Use ShaderBundleAssetMode.dataAssetsRequired instead of ' + 'committing generated bundles or listing them in flutter.assets.', + ); + } return null; } final dataAssetsAvailable = buildInput.config.buildDataAssets; @@ -82,6 +94,13 @@ ShaderBundleBuildResult? registerShaderBundleDataAsset({ if (assetMode == ShaderBundleAssetMode.dataAssetsRequired) { _throwDataAssetsUnavailable(legacyAssetKey); } + // ignore: avoid_print + print( + 'flutter_gpu_shaders warning, Dart DataAssets are unavailable, so ' + '"$legacyAssetKey" was not ' + 'registered as a managed asset. Enable DataAssets or use ' + 'ShaderBundleAssetMode.dataAssetsRequired to fail instead.', + ); return null; } @@ -432,13 +451,15 @@ List shaderBundleImpellercArguments({ /// `textureLod` and friends are core rather than extensions). When omitted, /// `impellerc` defaults to GLSL ES 1.00. /// -/// Set [assetMode] to opt in to DataAssets registration. The default +/// Set [assetMode] to [ShaderBundleAssetMode.dataAssetsRequired] for new +/// applications. This registers the generated `.shaderbundle` as a managed +/// DataAsset and fails early with setup guidance when DataAssets are +/// unavailable. The generated bundle is tied to the active Flutter engine and +/// must not be committed or listed in `flutter.assets`. +/// /// [ShaderBundleAssetMode.legacyOnly] preserves the historical behavior and -/// requires listing the generated `.shaderbundle` in `flutter.assets`. -/// [ShaderBundleAssetMode.dataAssetsIfAvailable] registers the bundle when the -/// current toolchain supports Dart DataAssets and otherwise falls back to the -/// legacy output. [ShaderBundleAssetMode.dataAssetsRequired] fails early with -/// setup guidance when DataAssets are unavailable. +/// [ShaderBundleAssetMode.dataAssetsIfAvailable] falls back to it when +/// DataAssets are unavailable. Both fallback paths emit a migration warning. /// /// Example usage: /// @@ -452,7 +473,8 @@ List shaderBundleImpellercArguments({ /// await buildShaderBundleJson( /// buildInput: config, /// buildOutput: output, -/// manifestFileName: 'my_cool_bundle.shaderbundle.json'); +/// manifestFileName: 'my_cool_bundle.shaderbundle.json', +/// assetMode: ShaderBundleAssetMode.dataAssetsRequired); /// }); /// } /// ``` @@ -521,6 +543,9 @@ Future buildShaderBundleJson({ legacyAssetKey: legacyAssetKey, assetMode: assetMode, dataAssetName: dataAssetName, + // Generated manifests are owned by a higher-level hook that registers + // the final bundle and sidecars itself. + warnIfLegacy: !manifestFileName.startsWith('build/'), ) ?? ShaderBundleBuildResult( outputFile: outFile, diff --git a/test/build_test.dart b/test/build_test.dart index 03a8590..b6262b2 100644 --- a/test/build_test.dart +++ b/test/build_test.dart @@ -236,21 +236,65 @@ void main() { ); }); - test('falls back when DataAssets are unavailable', () { + test('warns when DataAssets are unavailable', () { final input = _buildInput(buildDataAssets: false); final output = BuildOutputBuilder(); - final result = registerShaderBundleDataAsset( - buildInput: input, - buildOutput: output, - outputBundleFile: Uri.parse( - 'file:///pkg/build/shaderbundles/materials.shaderbundle', + ShaderBundleBuildResult? result; + expect(() { + result = registerShaderBundleDataAsset( + buildInput: input, + buildOutput: output, + outputBundleFile: Uri.parse( + 'file:///pkg/build/shaderbundles/materials.shaderbundle', + ), + legacyAssetKey: 'build/shaderbundles/materials.shaderbundle', + assetMode: ShaderBundleAssetMode.dataAssetsIfAvailable, + ); + }, prints(contains('DataAssets are unavailable'))); + + expect(result, isNull); + expect(output.build().assets.encodedAssets, isEmpty); + }); + + test('warns when legacy mode is selected', () { + ShaderBundleBuildResult? result; + expect( + () { + result = registerShaderBundleDataAsset( + buildInput: _buildInput(buildDataAssets: true), + buildOutput: BuildOutputBuilder(), + outputBundleFile: Uri.parse( + 'file:///pkg/build/shaderbundles/materials.shaderbundle', + ), + legacyAssetKey: 'build/shaderbundles/materials.shaderbundle', + assetMode: ShaderBundleAssetMode.legacyOnly, + ); + }, + prints( + allOf( + contains('legacy generated assets'), + contains('tied to the active Flutter engine'), + ), ), - legacyAssetKey: 'build/shaderbundles/materials.shaderbundle', - assetMode: ShaderBundleAssetMode.dataAssetsIfAvailable, ); expect(result, isNull); - expect(output.build().assets.encodedAssets, isEmpty); + }); + + test('allows an aggregating hook to suppress the legacy warning', () { + expect( + () => registerShaderBundleDataAsset( + buildInput: _buildInput(buildDataAssets: true), + buildOutput: BuildOutputBuilder(), + outputBundleFile: Uri.parse( + 'file:///pkg/build/shaderbundles/materials.shaderbundle', + ), + legacyAssetKey: 'build/shaderbundles/materials.shaderbundle', + assetMode: ShaderBundleAssetMode.legacyOnly, + warnIfLegacy: false, + ), + prints(isEmpty), + ); }); test('throws when DataAssets are required but unavailable', () {