Files
plezy/lib/services/shader_asset_loader.dart
T
2026-04-26 20:29:38 +02:00

288 lines
11 KiB
Dart

import 'dart:io';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import '../models/shader_preset.dart';
import '../utils/app_logger.dart';
/// Utility class for loading GLSL shader assets for MPV video enhancement.
///
/// Extracts shader files from Flutter assets to the app's cache directory
/// where MPV can access them at runtime.
class ShaderAssetLoader {
static const String _shaderAssetBase = 'assets/shaders';
static String? _cachedShaderDir;
/// NVScaler shader file
static const String _nvscalerShader = 'nvscaler/NVScaler.glsl';
/// ArtCNN shader files organized by preset model and variant.
static const Map<String, String> _artcnnShaders = {
'c4f16_neutral': 'artcnn/ArtCNN_C4F16.glsl',
'c4f16_dn': 'artcnn/ArtCNN_C4F16_DN.glsl',
'c4f16_ds': 'artcnn/ArtCNN_C4F16_DS.glsl',
'c4f32_neutral': 'artcnn/ArtCNN_C4F32.glsl',
'c4f32_dn': 'artcnn/ArtCNN_C4F32_DN.glsl',
'c4f32_ds': 'artcnn/ArtCNN_C4F32_DS.glsl',
};
/// Anime4K shader files organized by function
static const Map<String, String> _anime4kShaders = {
'clamp': 'anime4k/Anime4K_Clamp_Highlights.glsl',
'restore_m': 'anime4k/Anime4K_Restore_CNN_M.glsl',
'restore_vl': 'anime4k/Anime4K_Restore_CNN_VL.glsl',
'restore_ul': 'anime4k/Anime4K_Restore_CNN_UL.glsl',
'upscale_m': 'anime4k/Anime4K_Upscale_CNN_x2_M.glsl',
'upscale_vl': 'anime4k/Anime4K_Upscale_CNN_x2_VL.glsl',
'upscale_ul': 'anime4k/Anime4K_Upscale_CNN_x2_UL.glsl',
'downscale': 'anime4k/Anime4K_AutoDownscalePre_x2.glsl',
'downscale_post': 'anime4k/Anime4K_AutoDownscalePre_x4.glsl',
};
/// Get the shader cache directory path, creating it if necessary.
static Future<String> _getShaderDirectory() async {
if (_cachedShaderDir != null) return _cachedShaderDir!;
final cacheDir = await getTemporaryDirectory();
final shaderDir = Directory(path.join(cacheDir.path, 'shaders'));
if (!await shaderDir.exists()) {
await shaderDir.create(recursive: true);
}
_cachedShaderDir = shaderDir.path;
return shaderDir.path;
}
/// Extract a single shader file from assets to the cache directory.
/// Returns the absolute file path of the extracted shader.
static Future<String?> _extractShader(String assetPath) async {
try {
final shaderDir = await _getShaderDirectory();
final fileName = path.basename(assetPath);
final subDir = path.dirname(assetPath);
// Create subdirectory if needed
final targetDir = Directory(path.join(shaderDir, subDir));
if (!await targetDir.exists()) {
await targetDir.create(recursive: true);
}
final targetFile = File(path.join(targetDir.path, fileName));
// Only extract if not already cached
if (!await targetFile.exists()) {
final fullAssetPath = '$_shaderAssetBase/$assetPath';
final data = await rootBundle.load(fullAssetPath);
await targetFile.writeAsBytes(data.buffer.asUint8List());
}
return targetFile.path;
} catch (e, st) {
appLogger.w('Failed to extract shader $assetPath', error: e, stackTrace: st);
return null;
}
}
/// Get the shader file paths for NVScaler preset.
/// Returns a list containing the single NVScaler shader path.
static Future<List<String>> getNVScalerShaders() async {
final shaderPath = await _extractShader(_nvscalerShader);
if (shaderPath == null) return [];
return [shaderPath];
}
/// Get the shader file path for an ArtCNN preset.
/// Returns a list containing exactly one ArtCNN shader path.
static Future<List<String>> getArtCNNShaders(ArtCNNConfig config) async {
final variantId = switch (config.variant) {
ArtCNNVariant.neutral => 'neutral',
ArtCNNVariant.denoise => 'dn',
ArtCNNVariant.denoiseSharpen => 'ds',
};
final shaderPath = await _extractShader(_artcnnShaders['${config.model.name}_$variantId']!);
if (shaderPath == null) return [];
return [shaderPath];
}
/// Get the shader file paths for an Anime4K preset.
/// Returns a list of shader paths in the correct order for MPV.
static Future<List<String>> getAnime4KShaders(Anime4KConfig config) async {
final shaders = <String>[];
final quality = config.quality;
final mode = config.mode;
// Get quality-specific shader variants
String restoreVariant;
String upscaleVariant;
switch (quality) {
case Anime4KQuality.fast:
restoreVariant = 'restore_m';
upscaleVariant = 'upscale_m';
break;
case Anime4KQuality.hq:
restoreVariant = 'restore_vl';
upscaleVariant = 'upscale_vl';
break;
}
// Build shader chain based on mode
// All modes start with Clamp
final clampPath = await _extractShader(_anime4kShaders['clamp']!);
if (clampPath != null) shaders.add(clampPath);
switch (mode) {
case Anime4KMode.modeA:
// A: Clamp + Restore
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
if (restorePath != null) shaders.add(restorePath);
break;
case Anime4KMode.modeB:
// B: Clamp + Restore + Upscale + Downscale
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
if (restorePath != null) shaders.add(restorePath);
final upscalePath = await _extractShader(_anime4kShaders[upscaleVariant]!);
if (upscalePath != null) shaders.add(upscalePath);
final downscalePath = await _extractShader(_anime4kShaders['downscale']!);
if (downscalePath != null) shaders.add(downscalePath);
break;
case Anime4KMode.modeC:
// C: Clamp + Upscale + Downscale
final upscalePath = await _extractShader(_anime4kShaders[upscaleVariant]!);
if (upscalePath != null) shaders.add(upscalePath);
final downscalePath = await _extractShader(_anime4kShaders['downscale']!);
if (downscalePath != null) shaders.add(downscalePath);
break;
case Anime4KMode.modeAA:
// A+A: Clamp + Restore + Restore
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
if (restorePath != null) {
shaders.add(restorePath);
shaders.add(restorePath); // Second restore pass
}
break;
case Anime4KMode.modeBB:
// B+B: Clamp + Restore + Restore + Upscale + Downscale
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
if (restorePath != null) {
shaders.add(restorePath);
shaders.add(restorePath); // Second restore pass
}
final upscalePath = await _extractShader(_anime4kShaders[upscaleVariant]!);
if (upscalePath != null) shaders.add(upscalePath);
final downscalePath = await _extractShader(_anime4kShaders['downscale']!);
if (downscalePath != null) shaders.add(downscalePath);
break;
case Anime4KMode.modeCA:
// C+A: Clamp + Upscale + Restore + Downscale
final upscalePath = await _extractShader(_anime4kShaders[upscaleVariant]!);
if (upscalePath != null) shaders.add(upscalePath);
final restorePath = await _extractShader(_anime4kShaders[restoreVariant]!);
if (restorePath != null) shaders.add(restorePath);
final downscalePath = await _extractShader(_anime4kShaders['downscale']!);
if (downscalePath != null) shaders.add(downscalePath);
break;
}
return shaders;
}
/// Get the custom shader directory path, creating it if necessary.
/// Uses app support directory (persistent) rather than temp/cache.
static Future<String> _getCustomShaderDirectory() async {
final supportDir = await getApplicationSupportDirectory();
final customDir = Directory(path.join(supportDir.path, 'custom_shaders'));
if (!await customDir.exists()) {
await customDir.create(recursive: true);
}
return customDir.path;
}
/// Import a custom shader file into the custom shaders directory.
/// Returns the stored file name (UUID-based to avoid collisions).
static Future<String> importCustomShader(String sourcePath) async {
final customDir = await _getCustomShaderDirectory();
final ext = path.extension(sourcePath);
final uuid = DateTime.now().millisecondsSinceEpoch.toRadixString(36);
final storedName = '$uuid$ext';
final targetFile = File(path.join(customDir, storedName));
await File(sourcePath).copy(targetFile.path);
return storedName;
}
/// Delete a custom shader file from the custom shaders directory.
static Future<void> deleteCustomShader(String fileName) async {
final customDir = await _getCustomShaderDirectory();
final file = File(path.join(customDir, fileName));
if (await file.exists()) {
await file.delete();
}
}
/// Get the absolute path for a custom shader file.
static Future<String> getCustomShaderPath(String fileName) async {
final customDir = await _getCustomShaderDirectory();
return path.join(customDir, fileName);
}
/// Get shader paths for a given preset.
/// Returns an empty list for ShaderPresetType.none.
static Future<List<String>> getShadersForPreset(ShaderPreset preset) async {
switch (preset.type) {
case ShaderPresetType.none:
return [];
case ShaderPresetType.nvscaler:
return getNVScalerShaders();
case ShaderPresetType.artcnn:
if (preset.artcnnConfig == null) return [];
return getArtCNNShaders(preset.artcnnConfig!);
case ShaderPresetType.anime4k:
if (preset.anime4kConfig == null) return [];
return getAnime4KShaders(preset.anime4kConfig!);
case ShaderPresetType.custom:
if (preset.fileName == null) return [];
final shaderPath = await getCustomShaderPath(preset.fileName!);
if (!await File(shaderPath).exists()) return [];
return [shaderPath];
}
}
/// Pre-extract all shader files to the cache.
/// Call this at startup to avoid extraction delay during playback.
static Future<void> preloadShaders() async {
try {
// Extract NVScaler
await _extractShader(_nvscalerShader);
// Extract all ArtCNN shaders
for (final shaderPath in _artcnnShaders.values) {
await _extractShader(shaderPath);
}
// Extract all Anime4K shaders
for (final shaderPath in _anime4kShaders.values) {
await _extractShader(shaderPath);
}
} catch (e, st) {
appLogger.w('Failed to preload shaders', error: e, stackTrace: st);
}
}
/// Clear cached shader directory reference.
/// Call when clearing app cache.
static void clearCache() {
_cachedShaderDir = null;
}
}