Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
78 changes: 42 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -20,53 +30,44 @@ 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';

void main(List<String> 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/<package>/flutter_gpu_shaders/shaderbundles/<bundle>.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 {
Expand All @@ -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.
39 changes: 32 additions & 7 deletions lib/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,22 +66,41 @@ 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,
required Uri outputBundleFile,
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;
if (!dataAssetsAvailable) {
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;
}

Expand Down Expand Up @@ -432,13 +451,15 @@ List<String> 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:
///
Expand All @@ -452,7 +473,8 @@ List<String> shaderBundleImpellercArguments({
/// await buildShaderBundleJson(
/// buildInput: config,
/// buildOutput: output,
/// manifestFileName: 'my_cool_bundle.shaderbundle.json');
/// manifestFileName: 'my_cool_bundle.shaderbundle.json',
/// assetMode: ShaderBundleAssetMode.dataAssetsRequired);
/// });
/// }
/// ```
Expand Down Expand Up @@ -521,6 +543,9 @@ Future<ShaderBundleBuildResult> 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,
Expand Down
62 changes: 53 additions & 9 deletions test/build_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand Down
Loading