Skip to content
Merged
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 .github/workflows/flutter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:
run: tool/verify_shader_bundle_hook.sh
env:
VERIFY_BUILD_HOOK_CACHE: true
VERIFY_DATA_ASSETS: true
VERIFY_DIRECT_SHADER: true
VERIFY_TRANSITIVE_INCLUDE: true

Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## Unreleased

* Added optional Dart DataAssets registration for shader bundles via
`ShaderBundleAssetMode`. The default remains legacy file output under
`build/shaderbundles/`, while `dataAssetsIfAvailable` registers the generated
`.shaderbundle` with the Flutter asset bundle when supported and falls back
otherwise.
* `buildShaderBundleJson` now returns `ShaderBundleBuildResult`, including the
legacy asset key and DataAsset/Flutter asset keys when a DataAsset is emitted.

## 0.4.4

* `buildShaderBundleJson` now consumes `impellerc` depfiles when the resolved
Expand Down
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,34 @@ Use native asset build hooks to import Flutter GPU shader bundle assets.
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 an asset import rule to package the built shader bundles (this will become unnecessary once the build hook system supports `DataAsset` registration in a future release):
4. In your project's `pubspec.yaml`, add the built shader bundle as an asset:
```yaml
flutter:
assets:
- build/shaderbundles/*.shaderbundle.json
- build/shaderbundles/my_cool_bundle.shaderbundle
```
5. You can now import the built shader bundle as a library using `gpu.ShaderLibrary.fromAsset` in your project. For example:
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:
```dart
import 'package:flutter_gpu/gpu.dart' as gpu;

final String _kBaseShaderBundlePath =
'packages/my_project/build/shaderbundles/my_cool_bundle.shaderbundle';
'build/shaderbundles/my_cool_bundle.shaderbundle';

gpu.ShaderLibrary? _baseShaderLibrary;
gpu.ShaderLibrary get baseShaderLibrary {
Expand Down
151 changes: 143 additions & 8 deletions lib/build.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,123 @@
import 'dart:convert' as convert;
import 'dart:io';

import 'package:data_assets/data_assets.dart';
import 'package:hooks/hooks.dart';

import 'package:flutter_gpu_shaders/environment.dart';

/// Controls whether [buildShaderBundleJson] registers the produced shader
/// bundle as a Dart DataAsset.
enum ShaderBundleAssetMode {
/// Preserve the historical behavior: only write
/// `build/shaderbundles/[name].shaderbundle`.
legacyOnly,

/// Register a DataAsset when the current Flutter/Dart build supports data
/// assets, and otherwise silently fall back to [legacyOnly].
dataAssetsIfAvailable,

/// Require DataAssets support and fail the build with a targeted error when
/// the current toolchain did not request data assets from the hook.
dataAssetsRequired,
}

/// Information about a shader bundle produced by [buildShaderBundleJson].
final class ShaderBundleBuildResult {
const ShaderBundleBuildResult({
required this.outputFile,
required this.legacyAssetKey,
this.dataAssetName,
this.dataAssetId,
this.flutterAssetKey,
});

/// Absolute file URI of the generated `.shaderbundle`.
final Uri outputFile;

/// Historical Flutter asset key for projects listing the generated file in
/// `flutter.assets`.
final String legacyAssetKey;

/// DataAsset name registered for the generated bundle, when one was emitted.
final String? dataAssetName;

/// DataAsset package identifier, e.g.
/// `package:my_app/flutter_gpu_shaders/shaderbundles/materials.shaderbundle`.
final String? dataAssetId;

/// Flutter asset-bundle key for the DataAsset, when one was emitted.
///
/// Flutter currently exposes DataAssets through the normal asset bundle under
/// `packages/<package>/<name>`.
final String? flutterAssetKey;
}

/// Returns the default DataAsset name for a generated shader bundle.
String shaderBundleDataAssetName(String bundleFileName) =>
'flutter_gpu_shaders/shaderbundles/$bundleFileName';

/// Returns the Flutter asset-bundle key for a DataAsset.
String flutterDataAssetKey({required String package, required String name}) =>
'packages/$package/$name';

/// Registers [outputBundleFile] as a DataAsset when [assetMode] allows it.
///
/// Returns the emitted asset metadata, or `null` when [assetMode] falls back to
/// legacy output because data assets are not available.
ShaderBundleBuildResult? registerShaderBundleDataAsset({
required BuildInput buildInput,
required BuildOutputBuilder buildOutput,
required Uri outputBundleFile,
required String legacyAssetKey,
required ShaderBundleAssetMode assetMode,
String? dataAssetName,
}) {
if (assetMode == ShaderBundleAssetMode.legacyOnly) {
return null;
}
final dataAssetsAvailable = buildInput.config.buildDataAssets;
if (!dataAssetsAvailable) {
if (assetMode == ShaderBundleAssetMode.dataAssetsRequired) {
_throwDataAssetsUnavailable(legacyAssetKey);
}
return null;
}

final name =
dataAssetName ??
shaderBundleDataAssetName(legacyAssetKey.split('/').last);
final asset = DataAsset(
package: buildInput.packageName,
name: name,
file: outputBundleFile,
);
buildOutput.assets.data.add(asset);
return ShaderBundleBuildResult(
outputFile: outputBundleFile,
legacyAssetKey: legacyAssetKey,
dataAssetName: name,
dataAssetId: asset.id,
flutterAssetKey: flutterDataAssetKey(
package: buildInput.packageName,
name: name,
),
);
}

Never _throwDataAssetsUnavailable(String legacyAssetKey) {
throw UnsupportedError(
'Dart DataAssets were requested for "$legacyAssetKey", but this Flutter '
'build did not enable data assets for hooks. DataAssets are currently '
'experimental and require a Flutter toolchain that supports the Dart data '
'assets feature. On supported Flutter master builds, run '
'`flutter config --enable-dart-data-assets` or set '
'`FLUTTER_DART_DATA_ASSETS=true`, then rebuild. Otherwise use '
'ShaderBundleAssetMode.legacyOnly and list "$legacyAssetKey" in '
'flutter.assets.',
);
}

/// Loads a shader bundle manifest file and builds a shader bundle.
Future<void> _buildShaderBundleJson({
required Uri packageRoot,
Expand Down Expand Up @@ -221,6 +334,14 @@ List<String> shaderBundleImpellercArguments({
/// dependencies automatically; add them via [buildOutput] if edits to them
/// should retrigger the build.
///
/// Set [assetMode] to opt in to DataAssets registration. The default
/// [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.
///
/// Example usage:
///
/// hook/build.dart
Expand All @@ -247,11 +368,13 @@ List<String> shaderBundleImpellercArguments({
/// }
/// }
/// ```
Future<void> buildShaderBundleJson({
Future<ShaderBundleBuildResult> buildShaderBundleJson({
required BuildInput buildInput,
required BuildOutputBuilder buildOutput,
required String manifestFileName,
List<Uri> includeDirectories = const [],
ShaderBundleAssetMode assetMode = ShaderBundleAssetMode.legacyOnly,
String? dataAssetName,
}) async {
String outputFileName = Uri(path: manifestFileName).pathSegments.last;
if (!outputFileName.endsWith('.shaderbundle.json')) {
Expand All @@ -268,13 +391,6 @@ Future<void> buildShaderBundleJson({
outputFileName = outputFileName.substring(0, outputFileName.length - 5);
}

// TODO(bdero): Migrate to writing to `buildInput.outputDirectory` and
// registering the produced bundle as a DataAsset via
// `output.assets.data.add(DataAsset(...))`. Tracked at
// https://github.com/bdero/flutter_scene/issues/106. Blocked on the
// `dartDataAssets` feature flipping to `enabledByDefault: true` in
// flutter_tools (currently `available: true` on master, so consumers
// would need a `flutter config --enable-dart-data-assets` step).
final outDir = Directory.fromUri(
buildInput.packageRoot.resolve('build/shaderbundles/'),
);
Expand All @@ -283,6 +399,12 @@ Future<void> buildShaderBundleJson({

final inFile = packageRoot.resolve(manifestFileName);
final outFile = outDir.uri.resolve(outputFileName);
final legacyAssetKey = 'build/shaderbundles/$outputFileName';

if (assetMode == ShaderBundleAssetMode.dataAssetsRequired &&
!buildInput.config.buildDataAssets) {
_throwDataAssetsUnavailable(legacyAssetKey);
}

await _buildShaderBundleJson(
packageRoot: packageRoot,
Expand All @@ -291,4 +413,17 @@ Future<void> buildShaderBundleJson({
buildOutput: buildOutput,
includeDirectories: includeDirectories,
);

return registerShaderBundleDataAsset(
buildInput: buildInput,
buildOutput: buildOutput,
outputBundleFile: outFile,
legacyAssetKey: legacyAssetKey,
assetMode: assetMode,
dataAssetName: dataAssetName,
) ??
ShaderBundleBuildResult(
outputFile: outFile,
legacyAssetKey: legacyAssetKey,
);
}
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ environment:
flutter: '>=1.17.0'

dependencies:
data_assets: ^0.20.0
flutter:
sdk: flutter
hooks: ^2.0.0
Expand Down
115 changes: 115 additions & 0 deletions test/build_test.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'dart:convert' as convert;
import 'dart:io';

import 'package:data_assets/data_assets.dart';
import 'package:flutter_gpu_shaders/build.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks/hooks.dart';

void main() {
group('collectShaderBundleDependencies', () {
Expand Down Expand Up @@ -197,4 +200,116 @@ void main() {
expect(parseImpellerCDepfileDependencies(''), isEmpty);
});
});

group('DataAsset registration', () {
test('computes default DataAsset names and Flutter asset keys', () {
expect(
shaderBundleDataAssetName('materials.shaderbundle'),
'flutter_gpu_shaders/shaderbundles/materials.shaderbundle',
);
expect(
flutterDataAssetKey(
package: 'example_app',
name: 'flutter_gpu_shaders/shaderbundles/materials.shaderbundle',
),
'packages/example_app/'
'flutter_gpu_shaders/shaderbundles/materials.shaderbundle',
);
});

test('falls back 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',
),
legacyAssetKey: 'build/shaderbundles/materials.shaderbundle',
assetMode: ShaderBundleAssetMode.dataAssetsIfAvailable,
);

expect(result, isNull);
expect(output.build().assets.encodedAssets, isEmpty);
});

test('throws when DataAssets are required but unavailable', () {
final input = _buildInput(buildDataAssets: false);
final output = BuildOutputBuilder();

expect(
() => registerShaderBundleDataAsset(
buildInput: input,
buildOutput: output,
outputBundleFile: Uri.parse(
'file:///pkg/build/shaderbundles/materials.shaderbundle',
),
legacyAssetKey: 'build/shaderbundles/materials.shaderbundle',
assetMode: ShaderBundleAssetMode.dataAssetsRequired,
),
throwsA(isA<UnsupportedError>()),
);
expect(output.build().assets.encodedAssets, isEmpty);
});

test('registers a DataAsset when enabled', () {
final input = _buildInput(buildDataAssets: true);
final output = BuildOutputBuilder();
final outputFile = Uri.parse(
'file:///pkg/build/shaderbundles/materials.shaderbundle',
);

final result = registerShaderBundleDataAsset(
buildInput: input,
buildOutput: output,
outputBundleFile: outputFile,
legacyAssetKey: 'build/shaderbundles/materials.shaderbundle',
assetMode: ShaderBundleAssetMode.dataAssetsIfAvailable,
dataAssetName: 'custom/materials.shaderbundle',
);

expect(result, isNotNull);
expect(result!.outputFile, outputFile);
expect(
result.legacyAssetKey,
'build/shaderbundles/materials.shaderbundle',
);
expect(result.dataAssetName, 'custom/materials.shaderbundle');
expect(
result.dataAssetId,
'package:example_app/custom/materials.shaderbundle',
);
expect(
result.flutterAssetKey,
'packages/example_app/custom/materials.shaderbundle',
);

final assets = output.build().assets.encodedAssets;
expect(assets, hasLength(1));
final asset = assets.single.asDataAsset;
expect(asset.file, outputFile);
expect(asset.name, 'custom/materials.shaderbundle');
expect(asset.package, 'example_app');
});
});
}

BuildInput _buildInput({required bool buildDataAssets}) {
final temp = Directory.systemTemp.createTempSync(
'flutter_gpu_shaders_build_input',
);
final builder = BuildInputBuilder()
..setupShared(
packageRoot: temp.uri,
packageName: 'example_app',
outputDirectoryShared: temp.uri.resolve('.dart_tool/hook/'),
outputFile: temp.uri.resolve('.dart_tool/hook/output.json'),
)
..setupBuildInput();
builder.config.setupBuild(linkingEnabled: false);
if (buildDataAssets) {
DataAssetsExtension().setupBuildInput(builder);
}
return builder.build();
}
Loading
Loading