blob: 33249b236633e0f81521d77903420e057097ef7c [file] [edit]
/*
* Copyright (c) 2026 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "IOValidator.h"
#include "AST.h"
#include "ASTScopedVisitorInlines.h"
#include "Constraints.h"
#include "WGSLShaderModule.h"
#include <wtf/CheckedArithmetic.h>
#include <wtf/MathExtras.h>
#include <wtf/text/MakeString.h>
#define CHECK(__expression) { \
__expression; \
if (hasError()) [[unlikely]] \
return; \
}
namespace WGSL {
enum class Direction : uint8_t {
Input,
Output,
};
class IOValidator : public AST::ScopedVisitor<const CallGraph::Global*> {
public:
using AST::Visitor::visit;
IOValidator(ShaderModule&);
void validateIO();
void visit(AST::Function&) override;
void visit(AST::Parameter&) override;
void visit(AST::VariableStatement&) override;
void visit(AST::IdentifierExpression&) override;
private:
using Builtins = HashSet<Builtin, WTF::IntHash<Builtin>, WTF::StrongEnumHashTraits<Builtin>>;
using Locations = HashSet<uint64_t, DefaultHash<uint64_t>, WTF::UnsignedWithZeroKeyHashTraits<uint64_t>>;
void collectGlobals();
void validateResources(const CallGraph::EntryPoint&);
void validateEntryPointIO(const CallGraph::EntryPoint&);
void validateBuiltinIO(const SourceSpan&, const Type*, ShaderStage, Builtin, Direction, Builtins&);
void validateLocationIO(const SourceSpan&, const Type*, ShaderStage, unsigned, Locations&);
void validateStructIO(ShaderStage, const Types::Struct&, Direction, Builtins&, Locations&);
void validateInterpolationIO(const SourceSpan&, ShaderStage, Direction, const Type*, const std::optional<AST::Interpolation>&);
template<typename... Arguments>
void error(const SourceSpan&, Arguments&&...);
ShaderModule& m_shaderModule;
HashSet<const CallGraph::Global*> m_usedGlobals { };
ShaderStage m_stage { ShaderStage::Vertex };
};
IOValidator::IOValidator(ShaderModule& shaderModule)
: m_shaderModule(shaderModule)
{
}
void IOValidator::validateIO()
{
collectGlobals();
for (auto& entryPoint : m_shaderModule.callGraph().entrypoints()) {
CHECK(validateEntryPointIO(entryPoint));
m_stage = entryPoint.stage;
CHECK(visit(entryPoint.function));
const_cast<CallGraph::EntryPoint&>(entryPoint).usedGlobals.addAll(m_usedGlobals);
m_usedGlobals.clear();
CHECK(validateResources(entryPoint));
}
}
void IOValidator::collectGlobals()
{
for (auto& declaration : m_shaderModule.declarations()) {
auto* globalVar = dynamicDowncast<AST::Variable>(declaration);
if (!globalVar)
continue;
std::optional<CallGraph::Global::Resource> resource;
if (globalVar->group().has_value()) {
RELEASE_ASSERT(globalVar->binding().has_value());
resource = { *globalVar->group(), *globalVar->binding() };
}
auto* global = m_shaderModule.callGraph().addGlobal(CallGraph::Global {
resource,
globalVar
});
introduceVariable(globalVar->name(), global);
}
}
void IOValidator::visit(AST::Function& function)
{
HashSet<const CallGraph::Global*> usedGlobals;
for (auto& callee : m_shaderModule.callGraph().callees(function)) {
if (!callee.usedGlobals.isEmpty()) {
usedGlobals.addAll(callee.usedGlobals);
continue;
}
visit(*callee.target);
const_cast<CallGraph::Callee&>(callee).usedGlobals.addAll(m_usedGlobals);
usedGlobals.addAll(m_usedGlobals);
m_usedGlobals.clear();
}
m_usedGlobals.addAll(usedGlobals);
AST::Visitor::visit(function);
}
void IOValidator::visit(AST::VariableStatement& statement)
{
introduceVariable(statement.variable().name(), nullptr);
AST::Visitor::visit(statement);
}
void IOValidator::visit(AST::Parameter& parameter)
{
introduceVariable(parameter.name(), nullptr);
AST::Visitor::visit(parameter);
}
void IOValidator::visit(AST::IdentifierExpression& identifier)
{
AST::Visitor::visit(identifier);
auto* variable = readVariable(identifier.identifier());
if (!variable || !*variable)
return;
auto& var = *(*variable)->declaration;
if (auto addressSpace = var.addressSpace()) {
if (m_stage != ShaderStage::Compute && *addressSpace == AddressSpace::Workgroup) {
error(identifier.span(), "var with `workgroup` address space cannot be used by `"_s, toString(m_stage), "` pipeline stage"_s);
return;
}
if (m_stage == ShaderStage::Vertex) {
auto* textureStorage = std::get_if<Types::TextureStorage>(var.storeType());
if (textureStorage && std::to_underlying(textureStorage->access) & std::to_underlying(AccessMode::Write)) {
error(identifier.span(), "storage texture with `read_write` access mode cannot be used by `vertex` pipeline stage"_s);
return;
}
if (auto accessMode = var.accessMode(); *addressSpace == AddressSpace::Storage && accessMode.has_value() && std::to_underlying(*accessMode) & std::to_underlying(AccessMode::Write)) {
error(identifier.span(), "var with `storage` address space and `read_write` access mode cannot be used by `vertex` pipeline stage"_s);
return;
}
}
}
m_usedGlobals.add(*variable);
}
void IOValidator::validateResources(const CallGraph::EntryPoint& entryPoint)
{
HashMap<std::pair<unsigned, unsigned>, const CallGraph::Global*> usedGlobals;
for (auto* global : entryPoint.usedGlobals) {
if (!global->resource)
continue;
auto result = usedGlobals.add({ global->resource->group + 1, global->resource->binding + 1 }, global);
if (!result.isNewEntry) [[unlikely]] {
auto& var = *global->declaration;
auto& resource = *global->resource;
auto& entryPointName = entryPoint.originalName;
auto& originalDeclaration = result.iterator->value->declaration->originalName();
error(var.span(), "entry point '"_s, entryPointName, "' uses variables '"_s, originalDeclaration, "' and '"_s, var.originalName(), "', both which use the same resource binding: @group("_s, resource.group, ") @binding("_s, resource.binding, ')');
return;
}
}
}
void IOValidator::validateEntryPointIO(const CallGraph::EntryPoint& entryPoint)
{
auto& function = entryPoint.function;
Builtins builtins;
Locations locations;
for (auto& parameter : function.parameters()) {
const auto& span = parameter.span();
const auto* type = parameter.typeName().inferredType();
if (auto builtin = parameter.builtin()) {
CHECK(validateBuiltinIO(span, type, entryPoint.stage, *builtin, Direction::Input, builtins));
continue;
}
if (auto location = parameter.location()) {
CHECK(validateLocationIO(span, type, entryPoint.stage, *location, locations));
CHECK(validateInterpolationIO(span, entryPoint.stage, Direction::Input, type, parameter.interpolation()));
continue;
}
if (auto* structType = std::get_if<Types::Struct>(type)) {
CHECK(validateStructIO(entryPoint.stage, *structType, Direction::Input, builtins, locations));
continue;
}
error(span, "missing entry point IO attribute on parameter"_s);
return;
}
if (!function.maybeReturnType()) {
if (entryPoint.stage == ShaderStage::Vertex) [[unlikely]]
error(function.span(), "a vertex shader must include the 'position' builtin in its return type"_s);
return;
}
builtins.clear();
locations.clear();
const auto& span = function.maybeReturnType()->span();
const auto* type = function.maybeReturnType()->inferredType();
if (auto builtin = function.returnTypeBuiltin()) {
CHECK(validateBuiltinIO(span, type, entryPoint.stage, *builtin, Direction::Output, builtins));
} else if (auto location = function.returnTypeLocation()) {
CHECK(validateLocationIO(span, type, entryPoint.stage, *location, locations));
} else if (auto* structType = std::get_if<Types::Struct>(type)) {
CHECK(validateStructIO(entryPoint.stage, *structType, Direction::Output, builtins, locations));
} else [[unlikely]] {
error(span, "missing entry point IO attribute on return type"_s);
return;
}
if (entryPoint.stage == ShaderStage::Vertex && !builtins.contains(Builtin::Position)) [[unlikely]]
error(span, "a vertex shader must include the 'position' builtin in its return type"_s);
}
void IOValidator::validateBuiltinIO(const SourceSpan& span, const Type* type, ShaderStage stage, Builtin builtin, Direction direction, Builtins& builtins)
{
#define TYPE_CHECK(__type) \
type != m_shaderModule.types().__type##Type(), *m_shaderModule.types().__type##Type()
#define VEC_CHECK(__count, __elementType) \
auto* vector = std::get_if<Types::Vector>(type); !vector || vector->size != __count || vector->element != m_shaderModule.types().__elementType##Type(), "vec" #__count "<" #__elementType ">"_s
#define CASE_(__case, __typeCheck, __type) \
case Builtin::__case: \
if (__typeCheck) [[unlikely]] { \
error(span, "store type of @builtin("_s, toString(Builtin::__case), ") must be '"_s, __type, '\''); \
return; \
} \
#define CASE(__case, __typeCheck, __stage, __direction) \
CASE_(__case, __typeCheck); \
if (stage != ShaderStage::__stage || direction != Direction::__direction) [[unlikely]] { \
error(span, "@builtin("_s, toString(Builtin::__case), ") cannot be used for "_s, toString(stage), " shader "_s, direction == Direction::Input ? "input"_s : "output"_s); \
return; \
} \
break;
#define CASE2(__case, __typeCheck, __stage1, __direction1, __stage2, __direction2) \
CASE_(__case, __typeCheck); \
if ((stage != ShaderStage::__stage1 || direction != Direction::__direction1) && (stage != ShaderStage::__stage2 || direction != Direction::__direction2)) [[unlikely]] { \
error(span, "@builtin("_s, toString(Builtin::__case), ") cannot be used for "_s, toString(stage), " shader "_s, direction == Direction::Input ? "input"_s : "output"_s); \
return; \
} \
break;
switch (builtin) {
case Builtin::ClipDistances: {
// clip_distances requires the extension to be enabled
if (!m_shaderModule.enabledExtensions().contains(Extension::ClipDistances)) [[unlikely]] {
error(span, "@builtin(clip_distances) requires the 'clip_distances' extension to be enabled"_s);
return;
}
// @builtin(clip_distances) must be array<f32, N> where N is determined by the array size
auto* arrayType = std::get_if<Types::Array>(type);
bool isValidType
= arrayType
&& arrayType->element == m_shaderModule.types().f32Type()
&& std::holds_alternative<unsigned>(arrayType->size)
&& std::get<unsigned>(arrayType->size) <= 8;
if (!isValidType) [[unlikely]] {
error(span, "store type of @builtin(clip_distances) must be 'array<f32, N>' (N <= 8)"_s);
return;
}
if (stage != ShaderStage::Vertex || direction != Direction::Output) [[unlikely]] {
error(span, "@builtin(clip_distances) can only be used for vertex shader output"_s);
return;
}
break;
}
CASE(FragDepth, TYPE_CHECK(f32), Fragment, Output)
CASE(FrontFacing, TYPE_CHECK(bool), Fragment, Input)
CASE(GlobalInvocationId, VEC_CHECK(3, u32), Compute, Input)
CASE(InstanceIndex, TYPE_CHECK(u32), Vertex, Input)
CASE(LocalInvocationId, VEC_CHECK(3, u32), Compute, Input)
CASE(LocalInvocationIndex, TYPE_CHECK(u32), Compute, Input)
CASE(NumWorkgroups, VEC_CHECK(3, u32), Compute, Input)
case Builtin::PrimitiveIndex: {
// primitive_index requires the extension to be enabled
if (!m_shaderModule.enabledExtensions().contains(Extension::PrimitiveIndex)) [[unlikely]] {
error(span, "@builtin(primitive_index) requires the 'primitive_index' extension to be enabled"_s);
return;
}
// Type check: must be u32
if (type != m_shaderModule.types().u32Type()) [[unlikely]] {
error(span, "store type of @builtin(primitive_index) must be 'u32'"_s);
return;
}
// Stage and direction check: must be fragment shader input
if (stage != ShaderStage::Fragment || direction != Direction::Input) [[unlikely]] {
error(span, "@builtin(primitive_index) can only be used for fragment shader input"_s);
return;
}
break;
}
CASE(SampleIndex, TYPE_CHECK(u32), Fragment, Input)
CASE(VertexIndex, TYPE_CHECK(u32), Vertex, Input)
CASE(WorkgroupId, VEC_CHECK(3, u32), Compute, Input)
CASE2(SampleMask, TYPE_CHECK(u32), Fragment, Input, Fragment, Output)
CASE2(Position, VEC_CHECK(4, f32), Vertex, Output, Fragment, Input)
}
auto result = builtins.add(builtin);
if (!result.isNewEntry) [[unlikely]]
error(span, "@builtin("_s, toString(builtin), ") appears multiple times as pipeline input"_s);
}
void IOValidator::validateLocationIO(const SourceSpan& span, const Type* type, ShaderStage stage, unsigned location, Locations& locations)
{
if (stage == ShaderStage::Compute) [[unlikely]] {
error(span, "@location cannot be used by compute shaders"_s);
return;
}
if (!satisfies(type, Constraints::Number)) {
auto* vector = std::get_if<Types::Vector>(type);
if (!vector || !satisfies(vector->element, Constraints::Number)) [[unlikely]] {
error(span, "cannot apply @location to declaration of type '"_s, *type, '\'');
return;
}
}
auto result = locations.add(location);
if (!result.isNewEntry) [[unlikely]]
error(span, "@location("_s, location, ") appears multiple times"_s);
}
void IOValidator::validateInterpolationIO(const SourceSpan& span, ShaderStage stage, Direction direction, const Type* type, const std::optional<AST::Interpolation>& interpolation)
{
if (!(stage == ShaderStage::Vertex && direction == Direction::Output) && !(stage == ShaderStage::Fragment && direction == Direction::Input)) [[unlikely]]
return;
if (!satisfies(type, Constraints::Integer)) {
auto* vector = std::get_if<Types::Vector>(type);
if (!vector || !satisfies(vector->element, Constraints::Integer)) [[unlikely]]
return;
}
if (!interpolation || interpolation->type != InterpolationType::Flat) [[unlikely]] {
auto location = stage == ShaderStage::Vertex ? "vertex output"_s : "fragment input"_s;
error(span, makeString("integral user-defined "_s, location, " must have a @interpolate(flat) attribute"_s));
}
}
void IOValidator::validateStructIO(ShaderStage stage, const Types::Struct& structType, Direction direction, Builtins& builtins, Locations& locations)
{
for (auto& member : structType.structure.members()) {
const auto& span = member.span();
const auto* type = member.type().inferredType();
if (auto builtin = member.builtin()) {
validateBuiltinIO(span, type, stage, *builtin, direction, builtins);
if (hasError()) [[unlikely]]
return;
continue;
}
if (auto location = member.location()) {
validateLocationIO(span, type, stage, *location, locations);
if (hasError()) [[unlikely]]
return;
validateInterpolationIO(span, stage, direction, type, member.interpolation());
if (hasError()) [[unlikely]]
return;
continue;
}
if (auto inferredType = member.type().inferredType(); inferredType && std::holds_alternative<Types::Struct>(*inferredType)) {
error(span, "nested structures cannot be used for entry point IO"_s);
return;
}
error(span, "missing entry point IO attribute"_s);
return;
}
}
template<typename... Arguments>
void IOValidator::error(const SourceSpan& span, Arguments&&... arguments)
{
setError({ makeString(std::forward<Arguments>(arguments)...), span });
}
std::optional<FailedCheck> validateIO(ShaderModule& shaderModule)
{
IOValidator validator(shaderModule);
validator.validateIO();
if (validator.hasError()) [[unlikely]]
return FailedCheck { Vector<Error> { validator.result().error() }, { } };
return std::nullopt;
}
} // namespace WGSL
#undef CHECK
#undef TYPE_CHECK
#undef VEC_CHECK
#undef CASE_
#undef CASE
#undef CASE2