// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. #if COMPILE_WITH_SHADER_COMPILER #include "ShadersCompilation.h" #include "ShaderCompilationContext.h" #include "ShaderDebugDataExporter.h" #include "Engine/Engine/EngineService.h" #include "Engine/Threading/Threading.h" #include "Engine/Core/Log.h" #include "Engine/Core/Math/Math.h" #include "Engine/Core/Types/TimeSpan.h" #include "Parser/ShaderProcessing.h" #include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/Shaders/GPUShader.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Serialization/MemoryReadStream.h" #include "Engine/Content/Asset.h" #if USE_EDITOR #define COMPILE_WITH_ASSETS_IMPORTER 1 // Hack to use shaders importing in this module #include "Engine/ContentImporters/AssetsImportingManager.h" #include "Engine/Platform/FileSystemWatcher.h" #include "Engine/Platform/FileSystem.h" #include "Editor/Editor.h" #include "Editor/ProjectInfo.h" #endif #if COMPILE_WITH_D3D_SHADER_COMPILER #include "DirectX/ShaderCompilerD3D.h" #endif #if COMPILE_WITH_DX_SHADER_COMPILER #include "DirectX/ShaderCompilerDX.h" #endif #if COMPILE_WITH_VK_SHADER_COMPILER #include "Vulkan/ShaderCompilerVulkan.h" #endif #if COMPILE_WITH_PS4_SHADER_COMPILER #include "Platforms/PS4/Engine/ShaderCompilerPS4/ShaderCompilerPS4.h" #endif namespace ShadersCompilationImpl { CriticalSection Locker; Array Compilers; Array ReadyCompilers; } using namespace ShadersCompilationImpl; class ShadersCompilationService : public EngineService { public: ShadersCompilationService() : EngineService(TEXT("Shaders Compilation Service"), -100) { } bool Init() override; void Dispose() override; }; ShadersCompilationService ShadersCompilationServiceInstance; bool ShadersCompilation::Compile(ShaderCompilationOptions& options) { PROFILE_CPU_NAMED("Shader.Compile"); // Validate input options if (options.TargetName.IsEmpty() || !options.TargetID.IsValid()) { LOG(Warning, "Unknown target object."); return true; } if (options.Output == nullptr) { LOG(Warning, "Missing output."); return true; } if (options.Profile == ShaderProfile::Unknown) { LOG(Warning, "Unknown shader profile."); return true; } if (options.Source == nullptr || options.SourceLength < 1) { LOG(Warning, "Missing source code."); return true; } // Adjust input source length if it ends with null while (options.SourceLength > 2 && options.Source[options.SourceLength - 1] == 0) options.SourceLength--; const DateTime startTime = DateTime::NowUTC(); const FeatureLevel featureLevel = RenderTools::GetFeatureLevel(options.Profile); // Process shader source to collect metadata ShaderMeta meta; if (ShaderProcessing::Parser::Process(options.TargetName, options.Source, options.SourceLength, options.Macros, featureLevel, &meta)) { LOG(Warning, "Failed to parse source code."); return true; } const int32 shadersCount = meta.GetShadersCount(); if (shadersCount == 0) { LOG(Warning, "Shader has no valid functions."); } // Perform actual compilation bool result; { ShaderCompilationContext context(&options, &meta); // Request shaders compiler auto compiler = RequestCompiler(options.Profile); if (compiler == nullptr) { LOG(Error, "Shader compiler request failed."); return true; } ASSERT(compiler->GetProfile() == options.Profile); // Call compilation process result = compiler->Compile(&context); // Dismiss compiler FreeCompiler(compiler); #if GPU_USE_SHADERS_DEBUG_LAYER // Export debug data ShaderDebugDataExporter::Export(&context); #endif } // Print info if succeed if (result == false) { const DateTime endTime = DateTime::NowUTC(); LOG(Info, "Shader compilation '{0}' succeed in {1} ms (profile: {2})", options.TargetName, Math::CeilToInt(static_cast((endTime - startTime).GetTotalMilliseconds())), ::ToString(options.Profile)); } return result; } ShaderCompiler* ShadersCompilation::CreateCompiler(ShaderProfile profile) { ShaderCompiler* result = nullptr; switch (profile) { #if COMPILE_WITH_D3D_SHADER_COMPILER // Direct 3D case ShaderProfile::DirectX_SM4: case ShaderProfile::DirectX_SM5: result = New(profile); break; #endif #if COMPILE_WITH_DX_SHADER_COMPILER case ShaderProfile::DirectX_SM6: result = New(profile); break; #endif #if COMPILE_WITH_VK_SHADER_COMPILER // Vulkan case ShaderProfile::Vulkan_SM5: result = New(profile); break; #endif #if COMPILE_WITH_PS4_SHADER_COMPILER // PS4 case ShaderProfile::PS4: result = New(); break; #endif default: break; } ASSERT_LOW_LAYER(result == nullptr || result->GetProfile() == profile); return result; } ShaderCompiler* ShadersCompilation::RequestCompiler(ShaderProfile profile) { ShaderCompiler* compiler; ScopeLock lock(Locker); // Try to find ready compiler for (int32 i = 0; i < ReadyCompilers.Count(); i++) { compiler = ReadyCompilers[i]; if (compiler->GetProfile() == profile) { // Use it ReadyCompilers.RemoveAt(i); return compiler; } } // Create new compiler for a target profile compiler = CreateCompiler(profile); if (compiler == nullptr) { LOG(Error, "Cannot create Shader Compiler for profile {0}", ::ToString(profile)); return nullptr; } // Register new compiler Compilers.Add(compiler); return compiler; } void ShadersCompilation::FreeCompiler(ShaderCompiler* compiler) { ScopeLock lock(Locker); ASSERT(compiler && ReadyCompilers.Contains(compiler) == false); // Check if service has been disposed (this compiler is not in the compilers list) if (Compilers.Contains(compiler) == false) { // Delete it manually Delete(compiler); } else { // Make compiler free again ReadyCompilers.Add(compiler); } } namespace { CriticalSection ShaderIncludesMapLocker; Dictionary> ShaderIncludesMap; Dictionary ShaderIncludesWatcher; void OnShaderIncludesWatcherEvent(const String& path, FileSystemAction action) { if (action == FileSystemAction::Delete) return; Array toReload; { ScopeLock lock(ShaderIncludesMapLocker); auto file = ShaderIncludesMap.Find(path); if (file == ShaderIncludesMap.End()) return; toReload = file->Value; } LOG(Info, "Shader include \'{0}\' has been modified.", path); // Wait a little so app that was editing the file (e.g. Visual Studio, Notepad++) has enough time to flush whole file change Platform::Sleep(100); // Reload shaders using this include for (Asset* asset : toReload) { asset->Reload(); } } } void ShadersCompilation::RegisterForShaderReloads(Asset* asset, const String& includedPath) { ScopeLock lock(ShaderIncludesMapLocker); // Add to collection const bool alreadyAdded = ShaderIncludesMap.ContainsKey(includedPath); auto& file = ShaderIncludesMap[includedPath]; ASSERT_LOW_LAYER(!file.Contains(asset)); file.Add(asset); if (!alreadyAdded) { // Create a directory watcher to track the included file changes const String directory = StringUtils::GetDirectoryName(includedPath); if (!ShaderIncludesWatcher.ContainsKey(directory)) { auto watcher = New(directory, false); watcher->OnEvent.Bind(); ShaderIncludesWatcher.Add(directory, watcher); } } } void ShadersCompilation::UnregisterForShaderReloads(Asset* asset) { ScopeLock lock(ShaderIncludesMapLocker); // Remove asset reference for (auto& file : ShaderIncludesMap) { file.Value.Remove(asset); } } void ShadersCompilation::ExtractShaderIncludes(byte* shaderCache, int32 shaderCacheLength, Array& includes) { MemoryReadStream stream(shaderCache, shaderCacheLength); // Read cache format version int32 version; stream.ReadInt32(&version); if (version != GPU_SHADER_CACHE_VERSION) { return; } // Read the location of additional data that contains list of included source files int32 additionalDataStart; stream.ReadInt32(&additionalDataStart); stream.SetPosition(additionalDataStart); // Read all includes int32 includesCount; stream.ReadInt32(&includesCount); includes.Clear(); for (int32 i = 0; i < includesCount; i++) { String& include = includes.AddOne(); stream.ReadString(&include, 11); DateTime lastEditTime; stream.Read(&lastEditTime); } } #if USE_EDITOR namespace { Array ShadersSourcesWatchers; // Tries to generate a stable and unique ID for the given shader name. // Used in order to keep the same shader IDs and reduce version control issues with binary diff on ID. Guid GetShaderAssetId(const String& name) { Guid result; result.A = name.Length() * 100; result.B = GetHash(name); result.C = name.HasChars() ? name[0] : 0; result.D = name.HasChars() ? name[name.Length() - 1] : 0; return result; } void OnWatcherShadersEvent(const String& path, FileSystemAction action) { if (action == FileSystemAction::Delete || !path.EndsWith(TEXT(".shader"))) return; LOG(Info, "Shader \'{0}\' has been modified.", path); // Wait a little so app that was editing the file (e.g. Visual Studio, Notepad++) has enough time to flush whole file change Platform::Sleep(100); // Perform hot reload const int32 srcSubDirStart = path.FindLast(TEXT("/Source/Shaders")); if (srcSubDirStart == -1) return; String projectFolderPath = path.Substring(0, srcSubDirStart); FileSystem::NormalizePath(projectFolderPath); const String shadersAssetsPath = projectFolderPath / TEXT("/Content/Shaders"); const String shadersSourcePath = projectFolderPath / TEXT("/Source/Shaders"); const String localPath = FileSystem::ConvertAbsolutePathToRelative(shadersSourcePath, path); const String name = StringUtils::GetPathWithoutExtension(localPath); const String outputPath = shadersAssetsPath / name + ASSET_FILES_EXTENSION_WITH_DOT; Guid id = GetShaderAssetId(name); AssetsImportingManager::ImportIfEdited(path, outputPath, id); } void RegisterShaderWatchers(const ProjectInfo* project, HashSet& projects) { if (projects.Contains(project)) return; projects.Add(project); // Check if project uses shaders sources const String shadersSourcePath = project->ProjectFolderPath / TEXT("/Source/Shaders"); if (FileSystem::DirectoryExists(shadersSourcePath)) { // Track engine shaders editing auto sourceWatcher = New(shadersSourcePath, true); sourceWatcher->OnEvent.Bind(); ShadersSourcesWatchers.Add(sourceWatcher); // Reimport modified or import added shaders Array files(64); const String shadersAssetsPath = project->ProjectFolderPath / TEXT("/Content/Shaders"); FileSystem::DirectoryGetFiles(files, shadersSourcePath, TEXT("*.shader"), DirectorySearchOption::AllDirectories); for (int32 i = 0; i < files.Count(); i++) { const String& path = files[i]; const String localPath = FileSystem::ConvertAbsolutePathToRelative(shadersSourcePath, path); const String name = StringUtils::GetPathWithoutExtension(localPath); const String outputPath = shadersAssetsPath / name + ASSET_FILES_EXTENSION_WITH_DOT; Guid id = GetShaderAssetId(name); AssetsImportingManager::ImportIfEdited(path, outputPath, id); } } // Initialize referenced projects for (const auto& reference : project->References) { if (reference.Project) RegisterShaderWatchers(reference.Project, projects); } } } #endif bool ShadersCompilationService::Init() { #if USE_EDITOR // Initialize automatic shaders importing and reloading for all loaded projects (game, engine, plugins) HashSet projects; RegisterShaderWatchers(Editor::Project, projects); #endif return false; } void ShadersCompilationService::Dispose() { #if USE_EDITOR ShadersSourcesWatchers.ClearDelete(); #endif Locker.Lock(); // Check if any compilation is running if (ReadyCompilers.Count() != Compilers.Count()) { LOG(Error, "Cannot dispose Shaders Compilation Service. One or more compilers are still in use."); } // Cleanup all compilers (delete only those which are not in use) ReadyCompilers.ClearDelete(); Compilers.Clear(); Locker.Unlock(); // Cleanup shader includes ShaderCompiler::DisposeIncludedFilesCache(); // Clear includes scanning ShaderIncludesMapLocker.Lock(); ShaderIncludesMap.Clear(); ShaderIncludesWatcher.ClearDelete(); ShaderIncludesMapLocker.Unlock(); } #endif