Add shader compiler for WebGPU

Use existing Vulkan compiler to generate SPIR-V and convert it into WGSL with tint compiler
https://github.com/google/dawn/releases/tag/v20260219.200501
This commit is contained in:
Wojtek Figat
2026-02-24 17:55:26 +01:00
parent 9158e1c270
commit 9be8589437
17 changed files with 337 additions and 50 deletions

View File

@@ -59,6 +59,8 @@ public class ShadersCompilation : EngineModule
default: throw new InvalidPlatformException(options.Platform.Target);
}
if (ShaderCompilerWebGPU.Use(options))
options.PrivateDependencies.Add("ShaderCompilerWebGPU");
if (Sdk.HasValid("PS4Sdk"))
options.PrivateDependencies.Add("ShaderCompilerPS4");
if (Sdk.HasValid("PS5Sdk"))

View File

@@ -42,6 +42,9 @@
#if COMPILE_WITH_VK_SHADER_COMPILER
#include "Vulkan/ShaderCompilerVulkan.h"
#endif
#if COMPILE_WITH_WEBGPU_SHADER_COMPILER
#include "WebGPU/ShaderCompilerWebGPU.h"
#endif
#if COMPILE_WITH_PS4_SHADER_COMPILER
#include "Platforms/PS4/Engine/ShaderCompilerPS4/ShaderCompilerPS4.h"
#endif
@@ -159,7 +162,7 @@ bool ShadersCompilation::Compile(ShaderCompilationOptions& options)
return true;
}
const int32 shadersCount = meta.GetShadersCount();
if (shadersCount == 0)
if (shadersCount == 0 && featureLevel > FeatureLevel::ES2)
{
LOG(Warning, "Shader has no valid functions.");
}
@@ -255,6 +258,11 @@ ShaderCompiler* ShadersCompilation::RequestCompiler(ShaderProfile profile, Platf
compiler = New<ShaderCompilerVulkan>(profile);
break;
#endif
#if COMPILE_WITH_WEBGPU_SHADER_COMPILER
case ShaderProfile::WebGPU:
compiler = New<ShaderCompilerWebGPU>(profile);
break;
#endif
#if COMPILE_WITH_PS4_SHADER_COMPILER
case ShaderProfile::PS4:
compiler = New<ShaderCompilerPS4>();

View File

@@ -96,7 +96,7 @@ ShaderCompilerVulkan::~ShaderCompilerVulkan()
// @formatter:off
const TBuiltInResource DefaultTBuiltInResource =
{
/* .MaxLights = */ 32,
/* .MaxLights = */ 0,
/* .MaxClipPlanes = */ 6,
/* .MaxTextureUnits = */ 32,
/* .MaxTextureCoords = */ 32,
@@ -589,7 +589,6 @@ bool ShaderCompilerVulkan::CompileShader(ShaderFunctionMeta& meta, WritePermutat
// Prepare
if (WriteShaderFunctionBegin(_context, meta))
return true;
auto options = _context->Options;
auto type = meta.GetStage();
// Prepare
@@ -669,9 +668,9 @@ bool ShaderCompilerVulkan::CompileShader(ShaderFunctionMeta& meta, WritePermutat
glslang::TProgram program;
shader.setEntryPoint(meta.Name.Get());
shader.setSourceEntryPoint(meta.Name.Get());
int lengths = options->SourceLength - 1;
int lengths = _context->Options->SourceLength - 1;
const char* names = _context->TargetNameAnsi;
shader.setStringsWithLengthsAndNames(&options->Source, &lengths, &names, 1);
shader.setStringsWithLengthsAndNames(&_context->Options->Source, &lengths, &names, 1);
const int defaultVersion = 450;
std::string preamble;
for (int32 i = 0; i < _macros.Count() - 1; i++)
@@ -687,14 +686,8 @@ bool ShaderCompilerVulkan::CompileShader(ShaderFunctionMeta& meta, WritePermutat
preamble.append("\n");
}
shader.setPreamble(preamble.c_str());
shader.setInvertY(true);
//shader.setAutoMapLocations(true);
//shader.setAutoMapBindings(true);
//shader.setShiftBinding(glslang::TResourceType::EResUav, 500);
shader.setHlslIoMapping(true);
shader.setEnvInput(glslang::EShSourceHlsl, lang, glslang::EShClientVulkan, defaultVersion);
shader.setEnvClient(glslang::EShClientVulkan, glslang::EShTargetVulkan_1_0);
shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_0);
InitParsing(_context, shader);
if (!shader.parse(&DefaultTBuiltInResource, defaultVersion, false, messages, includer))
{
const auto msg = shader.getInfoLog();
@@ -899,16 +892,7 @@ bool ShaderCompilerVulkan::CompileShader(ShaderFunctionMeta& meta, WritePermutat
std::vector<unsigned> spirv;
spv::SpvBuildLogger logger;
glslang::SpvOptions spvOptions;
spvOptions.generateDebugInfo = false;
spvOptions.disassemble = false;
spvOptions.disableOptimizer = options->NoOptimize;
spvOptions.optimizeSize = !options->NoOptimize;
spvOptions.stripDebugInfo = !options->GenerateDebugData;
#if BUILD_DEBUG
spvOptions.validate = true;
#else
spvOptions.validate = false;
#endif
InitCodegen(_context, spvOptions);
glslang::GlslangToSpv(*program.getIntermediate(lang), spirv, &logger, &spvOptions);
const std::string spirvLogOutput = logger.getAllMessages();
if (!spirvLogOutput.empty())
@@ -931,10 +915,8 @@ bool ShaderCompilerVulkan::CompileShader(ShaderFunctionMeta& meta, WritePermutat
}
#endif
int32 spirvBytesCount = (int32)spirv.size() * sizeof(unsigned);
header.Type = SpirvShaderHeader::Types::Raw;
if (WriteShaderFunctionPermutation(_context, meta, permutationIndex, bindings, &header, sizeof(header), &spirv[0], spirvBytesCount))
if (Write(_context, meta, permutationIndex, bindings, header, spirv))
return true;
if (customDataWrite && customDataWrite(_context, meta, permutationIndex, _macros, additionalData))
@@ -956,4 +938,32 @@ bool ShaderCompilerVulkan::OnCompileBegin()
return false;
}
void ShaderCompilerVulkan::InitParsing(ShaderCompilationContext* context, glslang::TShader& shader)
{
shader.setInvertY(true);
//shader.setAutoMapLocations(true);
//shader.setAutoMapBindings(true);
//shader.setShiftBinding(glslang::TResourceType::EResUav, 500);
shader.setHlslIoMapping(true);
shader.setEnvClient(glslang::EShClientVulkan, glslang::EShTargetVulkan_1_0);
shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_0);
}
void ShaderCompilerVulkan::InitCodegen(ShaderCompilationContext* context, glslang::SpvOptions& spvOptions)
{
spvOptions.generateDebugInfo = false;
spvOptions.disassemble = false;
spvOptions.disableOptimizer = context->Options->NoOptimize;
spvOptions.optimizeSize = !context->Options->NoOptimize;
spvOptions.stripDebugInfo = !context->Options->GenerateDebugData;
spvOptions.validate = BUILD_DEBUG;
}
bool ShaderCompilerVulkan::Write(ShaderCompilationContext* context, ShaderFunctionMeta& meta, int32 permutationIndex, const ShaderBindings& bindings, struct SpirvShaderHeader& header, std::vector<unsigned int>& spirv)
{
int32 spirvBytesCount = (int32)spirv.size() * sizeof(unsigned);
header.Type = SpirvShaderHeader::Types::SPIRV;
return WriteShaderFunctionPermutation(_context, meta, permutationIndex, bindings, &header, sizeof(header), &spirv[0], spirvBytesCount);
}
#endif

View File

@@ -5,6 +5,13 @@
#if COMPILE_WITH_VK_SHADER_COMPILER
#include "Engine/ShadersCompilation/ShaderCompiler.h"
#include <vector>
namespace glslang
{
class TShader;
struct SpvOptions;
}
/// <summary>
/// Implementation of shaders compiler for Vulkan rendering backend.
@@ -30,6 +37,11 @@ protected:
// [ShaderCompiler]
bool CompileShader(ShaderFunctionMeta& meta, WritePermutationData customDataWrite = nullptr) override;
bool OnCompileBegin() override;
protected:
virtual void InitParsing(ShaderCompilationContext* context, glslang::TShader& shader);
virtual void InitCodegen(ShaderCompilationContext* context, glslang::SpvOptions& spvOptions);
virtual bool Write(ShaderCompilationContext* context, ShaderFunctionMeta& meta, int32 permutationIndex, const ShaderBindings& bindings, struct SpirvShaderHeader& header, std::vector<unsigned>& spirv);
};
#endif

View File

@@ -0,0 +1,57 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using System.IO;
using Flax.Build;
using Flax.Build.NativeCpp;
/// <summary>
/// WebGPU shaders compiler module.
/// </summary>
public class ShaderCompilerWebGPU : ShaderCompiler
{
public static bool Use(BuildOptions options)
{
// Requires prebuilt tint executable in platform ThirdParty binaries folder
// https://github.com/google/dawn/releases
// License: Source/ThirdParty/tint-license.txt (BSD 3-Clause)
switch (options.Platform.Target)
{
case TargetPlatform.Windows:
return options.Architecture == TargetArchitecture.x64;
case TargetPlatform.Linux: // TODO: add Linux binary with tint
case TargetPlatform.Mac:
return options.Architecture == TargetArchitecture.ARM64;
default:
return false;
}
}
/// <inheritdoc />
public override void Setup(BuildOptions options)
{
base.Setup(options);
options.PublicDefinitions.Add("COMPILE_WITH_WEBGPU_SHADER_COMPILER");
options.PublicDependencies.Add("ShaderCompilerVulkan");
// Deploy tint executable as a dependency for the shader compilation from SPIR-V into WGSL
// Tint compiler from: https://github.com/google/dawn/releases
// License: Source/ThirdParty/tint-license.txt (BSD 3-Clause)
var depsRoot = options.DepsFolder;
switch (options.Platform.Target)
{
case TargetPlatform.Windows:
if (options.Architecture == TargetArchitecture.x64)
options.DependencyFiles.Add(Path.Combine(depsRoot, "tint.exe"));
break;
case TargetPlatform.Linux:
if (options.Architecture == TargetArchitecture.x64)
options.DependencyFiles.Add(Path.Combine(depsRoot, "tint"));
break;
case TargetPlatform.Mac:
if (options.Architecture == TargetArchitecture.ARM64)
options.DependencyFiles.Add(Path.Combine(depsRoot, "tint"));
break;
}
}
}

View File

@@ -0,0 +1,94 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#if COMPILE_WITH_WEBGPU_SHADER_COMPILER
#include "ShaderCompilerWebGPU.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Types/String.h"
#include "Engine/Engine/Globals.h"
#include "Engine/GraphicsDevice/Vulkan/Types.h"
#include "Engine/Platform/CreateProcessSettings.h"
#include "Engine/Platform/File.h"
#include "Engine/Platform/Windows/WindowsFileSystem.h"
#include <ThirdParty/glslang/SPIRV/SpvTools.h>
#include <ThirdParty/spirv-tools/libspirv.hpp>
ShaderCompilerWebGPU::ShaderCompilerWebGPU(ShaderProfile profile)
: ShaderCompilerVulkan(profile)
{
}
bool ShaderCompilerWebGPU::OnCompileBegin()
{
_globalMacros.Add({ "WGSL", "1" });
return ShaderCompilerVulkan::OnCompileBegin();
}
void ShaderCompilerWebGPU::InitParsing(ShaderCompilationContext* context, glslang::TShader& shader)
{
ShaderCompilerVulkan::InitParsing(context, shader);
// Use newer SPIR-V
shader.setEnvClient(glslang::EShClientVulkan, glslang::EShTargetVulkan_1_2);
shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_3);
}
void ShaderCompilerWebGPU::InitCodegen(ShaderCompilationContext* context, glslang::SpvOptions& spvOptions)
{
ShaderCompilerVulkan::InitCodegen(context, spvOptions);
// Always optimize SPIR-V
spvOptions.disableOptimizer = false;
spvOptions.optimizeSize = true;
}
bool ShaderCompilerWebGPU::Write(ShaderCompilationContext* context, ShaderFunctionMeta& meta, int32 permutationIndex, const ShaderBindings& bindings, struct SpirvShaderHeader& header, std::vector<unsigned int>& spirv)
{
auto id = Guid::New().ToString(Guid::FormatType::N);
auto folder = Globals::ProjectCacheFolder / TEXT("Shaders");
auto inputFile = folder / id + TEXT(".spvasm");
auto outputFile = folder / id + TEXT(".wgsl");
// Convert SPIR-V to WGSL
// Tint compiler from: https://github.com/google/dawn/releases
// License: Source/ThirdParty/tint-license.txt (BSD 3-Clause)
File::WriteAllBytes(inputFile, &spirv[0], (int32)spirv.size() * sizeof(unsigned int));
CreateProcessSettings procSettings;
procSettings.Arguments = String::Format(TEXT("\"{}\" -if spirv -o \"{}\""), inputFile, outputFile);
if (!context->Options->NoOptimize)
procSettings.Arguments += TEXT(" --minify");
procSettings.Arguments += TEXT(" --allow-non-uniform-derivatives"); // Fix sampling texture within non-uniform control flow
#if PLATFORM_WINDOWS
procSettings.FileName = Globals::BinariesFolder / TEXT("tint.exe");
#else
procSettings.FileName = Globals::BinariesFolder / TEXT("tint");
#endif
int32 result = Platform::CreateProcess(procSettings);
StringAnsi wgsl;
File::ReadAllText(outputFile, wgsl);
if (result != 0 || wgsl.IsEmpty())
{
LOG(Error, "Failed to compile shader '{}' function '{}' (permutation {}) from SPIR-V into WGSL with result {}", context->Options->TargetName, String(meta.Name), permutationIndex, result);
#if 1
// Convert SPIR-V bytecode to text with assembly
spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_3);
std::string spirvText;
tools.Disassemble(spirv, &spirvText);
File::WriteAllBytes(folder / id + TEXT(".txt"), &spirvText[0], (int32)spirvText.size());
#endif
#if 1
// Dump source code
File::WriteAllBytes(folder / id + TEXT(".hlsl"), context->Options->Source, context->Options->SourceLength);
#endif
return true;
}
// Cleanup files
FileSystem::DeleteFile(inputFile);
FileSystem::DeleteFile(outputFile);
header.Type = SpirvShaderHeader::Types::WGSL;
return WriteShaderFunctionPermutation(_context, meta, permutationIndex, bindings, &header, sizeof(header), wgsl.Get(), wgsl.Length() + 1);
}
#endif

View File

@@ -0,0 +1,28 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#pragma once
#if COMPILE_WITH_WEBGPU_SHADER_COMPILER
#include "Engine/ShadersCompilation/Vulkan/ShaderCompilerVulkan.h"
/// <summary>
/// Implementation of shaders compiler for Web GPU rendering backend.
/// </summary>
class ShaderCompilerWebGPU : public ShaderCompilerVulkan
{
public:
/// <summary>
/// Initializes a new instance of the <see cref="ShaderCompilerWebGPU"/> class.
/// </summary>
/// <param name="profile">The profile.</param>
ShaderCompilerWebGPU(ShaderProfile profile);
protected:
bool OnCompileBegin() override;
void InitParsing(ShaderCompilationContext* context, glslang::TShader& shader) override;
void InitCodegen(ShaderCompilationContext* context, glslang::SpvOptions& spvOptions) override;
bool Write(ShaderCompilationContext* context, ShaderFunctionMeta& meta, int32 permutationIndex, const ShaderBindings& bindings, struct SpirvShaderHeader& header, std::vector<unsigned>& spirv) override;
};
#endif