From 5c72de7c0f3970a2dc836e391eb93e5ca24e6739 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Thu, 28 May 2026 02:23:29 -0700 Subject: [PATCH] Add DataAssets shader bundle mode --- .github/workflows/flutter.yml | 1 + CHANGELOG.md | 10 ++ README.md | 24 ++- lib/build.dart | 151 +++++++++++++++++- pubspec.yaml | 1 + test/build_test.dart | 115 +++++++++++++ .../shader_bundle_app/hook/build.dart | 1 + tool/verify_shader_bundle_hook.sh | 14 ++ 8 files changed, 305 insertions(+), 12 deletions(-) diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 151b79f..726416f 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 79264f1..e382d7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 462b032..1118de5 100644 --- a/README.md +++ b/README.md @@ -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 { diff --git a/lib/build.dart b/lib/build.dart index ff3d5bd..fb46133 100644 --- a/lib/build.dart +++ b/lib/build.dart @@ -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//`. + 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 _buildShaderBundleJson({ required Uri packageRoot, @@ -221,6 +334,14 @@ List 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 @@ -247,11 +368,13 @@ List shaderBundleImpellercArguments({ /// } /// } /// ``` -Future buildShaderBundleJson({ +Future buildShaderBundleJson({ required BuildInput buildInput, required BuildOutputBuilder buildOutput, required String manifestFileName, List includeDirectories = const [], + ShaderBundleAssetMode assetMode = ShaderBundleAssetMode.legacyOnly, + String? dataAssetName, }) async { String outputFileName = Uri(path: manifestFileName).pathSegments.last; if (!outputFileName.endsWith('.shaderbundle.json')) { @@ -268,13 +391,6 @@ Future 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/'), ); @@ -283,6 +399,12 @@ Future 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, @@ -291,4 +413,17 @@ Future buildShaderBundleJson({ buildOutput: buildOutput, includeDirectories: includeDirectories, ); + + return registerShaderBundleDataAsset( + buildInput: buildInput, + buildOutput: buildOutput, + outputBundleFile: outFile, + legacyAssetKey: legacyAssetKey, + assetMode: assetMode, + dataAssetName: dataAssetName, + ) ?? + ShaderBundleBuildResult( + outputFile: outFile, + legacyAssetKey: legacyAssetKey, + ); } diff --git a/pubspec.yaml b/pubspec.yaml index c27cc46..699d5bd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: flutter: '>=1.17.0' dependencies: + data_assets: ^0.20.0 flutter: sdk: flutter hooks: ^2.0.0 diff --git a/test/build_test.dart b/test/build_test.dart index a5f81d1..bb38c30 100644 --- a/test/build_test.dart +++ b/test/build_test.dart @@ -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', () { @@ -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()), + ); + 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(); } diff --git a/test_fixtures/shader_bundle_app/hook/build.dart b/test_fixtures/shader_bundle_app/hook/build.dart index 8b4023e..2ee20ba 100644 --- a/test_fixtures/shader_bundle_app/hook/build.dart +++ b/test_fixtures/shader_bundle_app/hook/build.dart @@ -8,6 +8,7 @@ void main(List args) async { buildOutput: output, manifestFileName: 'test_bundle.shaderbundle.json', includeDirectories: [input.packageRoot.resolve('shaders/')], + assetMode: ShaderBundleAssetMode.dataAssetsIfAvailable, ); }); } diff --git a/tool/verify_shader_bundle_hook.sh b/tool/verify_shader_bundle_hook.sh index 746319d..249b074 100755 --- a/tool/verify_shader_bundle_hook.sh +++ b/tool/verify_shader_bundle_hook.sh @@ -4,6 +4,7 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" fixture_source="$repo_root/test_fixtures/shader_bundle_app" verify_build_hook_cache="${VERIFY_BUILD_HOOK_CACHE:-false}" +verify_data_assets="${VERIFY_DATA_ASSETS:-false}" verify_direct_shader="${VERIFY_DIRECT_SHADER:-false}" verify_transitive_include="${VERIFY_TRANSITIVE_INCLUDE:-false}" workdir="$(mktemp -d)" @@ -23,6 +24,10 @@ cd "$fixture" flutter --version flutter pub get +if [[ "$verify_data_assets" == "true" ]]; then + export FLUTTER_DART_DATA_ASSETS=true +fi + flutter build bundle bundle="build/shaderbundles/test_bundle.shaderbundle" if [[ ! -s "$bundle" ]]; then @@ -30,6 +35,15 @@ if [[ ! -s "$bundle" ]]; then exit 1 fi +if [[ "$verify_data_assets" == "true" ]]; then + data_asset="build/flutter_assets/packages/shader_bundle_app/flutter_gpu_shaders/shaderbundles/test_bundle.shaderbundle" + if [[ ! -s "$data_asset" ]]; then + echo "Expected DataAsset shader bundle at $data_asset" >&2 + find build/flutter_assets -maxdepth 5 -type f >&2 + exit 1 + fi +fi + if [[ "$verify_build_hook_cache" != "true" ]]; then exit 0 fi