Files

1692 lines
62 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <array>
#include <bit>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
#include <spirv/unified1/GLSL.std.450.h>
#include <spirv/unified1/spirv.hpp11>
namespace fs = std::filesystem;
using u8 = std::uint8_t;
using u32 = std::uint32_t;
using u64 = std::uint64_t;
using s32 = std::int32_t;
using s64 = std::int64_t;
namespace {
struct Type {
u32 op{};
u32 width{};
u32 is_signed{};
u32 elem{};
u32 count{};
std::vector<u32> members;
u32 storage{};
u32 dim{};
u32 arrayed{};
};
struct Value {
u32 n{};
std::array<u32, 8> w{};
};
struct Pointer {
u32 var{};
u64 offset{};
bool array_index{};
};
struct Inst {
u32 op{};
u32 type{};
u32 result{};
u32 first{};
u32 count{};
};
struct Var {
u32 type{};
u32 storage{};
s32 location = -1;
s32 builtin = -1;
s32 binding = -1;
s32 set = 0;
s32 index = 0;
bool flat{}, noperspective{}, per_vertex{}, centroid{}, sample{};
};
struct Module {
std::vector<u32> words;
std::unordered_map<u32, Type> types;
std::unordered_map<u32, Value> constants;
std::unordered_map<u32, Var> vars;
std::unordered_map<u32, u32> stride;
std::map<std::pair<u32, u32>, u32> member_offset;
std::unordered_map<u32, s32> member_builtin;
std::map<u32, std::vector<Inst>> blocks;
std::vector<u32> args;
u32 entry{};
u32 bound{};
u32 glsl{};
u32 trinary{};
u32 cube{};
u32 explicit_vertex{};
bool Load(const fs::path& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) {
return false;
}
const size_t size = size_t(file.tellg());
file.seekg(0);
words.resize(size / 4);
file.read(reinterpret_cast<char*>(words.data()), std::streamsize(size));
bound = words[3];
size_t at = 5;
u32 current = 0;
bool in_function = false;
while (at < words.size()) {
const u32 word = words[at];
const u32 op = word & 0xffff;
const u32 count = word >> 16;
const u32* w = words.data() + at;
switch (spv::Op(op)) {
case spv::Op::OpExtInstImport:
if (std::strncmp(reinterpret_cast<const char*>(w + 2), "GLSL", 4) == 0) {
glsl = w[1];
} else if (std::strstr(reinterpret_cast<const char*>(w + 2), "trinary")) {
trinary = w[1];
} else if (std::strstr(reinterpret_cast<const char*>(w + 2), "gcn_shader")) {
cube = w[1];
} else if (std::strstr(reinterpret_cast<const char*>(w + 2), "explicit_vertex")) {
explicit_vertex = w[1];
}
break;
case spv::Op::OpDecorate: {
auto& var = vars[w[1]];
const auto decoration = spv::Decoration(w[2]);
if (decoration == spv::Decoration::Location) {
var.location = s32(w[3]);
} else if (decoration == spv::Decoration::BuiltIn) {
var.builtin = s32(w[3]);
} else if (decoration == spv::Decoration::Binding) {
var.binding = s32(w[3]);
} else if (decoration == spv::Decoration::DescriptorSet) {
var.set = s32(w[3]);
} else if (decoration == spv::Decoration::Index) {
var.index = s32(w[3]);
} else if (decoration == spv::Decoration::Flat) {
var.flat = true;
} else if (decoration == spv::Decoration::NoPerspective) {
var.noperspective = true;
} else if (decoration == spv::Decoration::PerVertexKHR || decoration == spv::Decoration::ExplicitInterpAMD) {
var.per_vertex = true;
} else if (decoration == spv::Decoration::Centroid) {
var.centroid = true;
} else if (decoration == spv::Decoration::Sample) {
var.sample = true;
} else if (decoration == spv::Decoration::ArrayStride) {
stride[w[1]] = w[3];
}
break;
}
case spv::Op::OpMemberDecorate:
if (spv::Decoration(w[3]) == spv::Decoration::Offset) {
member_offset[{w[1], w[2]}] = w[4];
}
break;
case spv::Op::OpTypeVoid:
case spv::Op::OpTypeBool:
case spv::Op::OpTypeSampler:
types[w[1]] = Type{op};
break;
case spv::Op::OpTypeInt:
types[w[1]] = Type{op, w[2], w[3]};
break;
case spv::Op::OpTypeFloat:
types[w[1]] = Type{op, w[2]};
break;
case spv::Op::OpTypeVector:
types[w[1]] = Type{op, 0, 0, w[2], w[3]};
break;
case spv::Op::OpTypeArray:
types[w[1]] = Type{op, 0, 0, w[2], constants[w[3]].w[0]};
break;
case spv::Op::OpTypeRuntimeArray:
types[w[1]] = Type{op, 0, 0, w[2], 0};
break;
case spv::Op::OpTypeStruct: {
Type type{op};
type.members.assign(w + 2, w + count);
types[w[1]] = type;
break;
}
case spv::Op::OpTypePointer: {
Type type{op};
type.storage = w[2];
type.elem = w[3];
types[w[1]] = type;
break;
}
case spv::Op::OpTypeImage: {
Type type{op};
type.elem = w[2];
type.dim = w[3];
type.arrayed = w[5];
types[w[1]] = type;
break;
}
case spv::Op::OpTypeSampledImage: {
Type type{op};
type.elem = w[2];
types[w[1]] = type;
break;
}
case spv::Op::OpTypeFunction:
types[w[1]] = Type{op};
break;
case spv::Op::OpConstant: {
Value value;
value.n = count - 3;
for (u32 i = 0; i + 3 < count; ++i) {
value.w[i] = w[3 + i];
}
if (types[w[1]].width == 64) {
value.n = 2;
} else {
value.n = 1;
}
constants[w[2]] = value;
break;
}
case spv::Op::OpConstantTrue:
constants[w[2]] = Value{1, {1}};
break;
case spv::Op::OpConstantFalse:
constants[w[2]] = Value{1, {0}};
break;
case spv::Op::OpConstantComposite: {
Value value;
value.n = count - 3;
for (u32 i = 0; i + 3 < count && i < 8; ++i) {
value.w[i] = constants[w[3 + i]].w[0];
}
constants[w[2]] = value;
break;
}
case spv::Op::OpUndef:
if (!in_function) {
constants[w[2]] = Value{1, {0}};
} else {
Inst inst{op, w[1], w[2], u32(args.size()), 0};
blocks[current].push_back(inst);
}
break;
case spv::Op::OpVariable: {
auto& var = vars[w[2]];
var.type = types[w[1]].elem;
var.storage = w[3];
break;
}
case spv::Op::OpEntryPoint:
entry = w[2];
break;
case spv::Op::OpFunction:
if (in_function) {
return false;
}
in_function = entry == w[2];
break;
case spv::Op::OpFunctionEnd:
in_function = false;
break;
case spv::Op::OpLabel:
if (in_function) {
current = w[1];
if (first_block == 0) {
first_block = current;
}
}
break;
default:
if (in_function) {
Inst inst{op, 0, 0, u32(args.size()), 0};
const bool has_type = HasResultType(op);
const bool has_result = has_type || op == 0;
u32 start = 1;
if (has_type) {
inst.type = w[1];
inst.result = w[2];
start = 3;
}
(void)has_result;
for (u32 i = start; i < count; ++i) {
args.push_back(w[i]);
}
inst.count = count - start;
blocks[current].push_back(inst);
}
break;
}
at += count ? count : 1;
}
return first_block != 0;
}
static bool HasResultType(u32 op) {
switch (spv::Op(op)) {
case spv::Op::OpStore:
case spv::Op::OpBranch:
case spv::Op::OpBranchConditional:
case spv::Op::OpSelectionMerge:
case spv::Op::OpLoopMerge:
case spv::Op::OpReturn:
case spv::Op::OpUnreachable:
case spv::Op::OpImageWrite:
case spv::Op::OpDemoteToHelperInvocation:
case spv::Op::OpKill:
case spv::Op::OpMemoryBarrier:
case spv::Op::OpControlBarrier:
case spv::Op::OpEmitVertex:
case spv::Op::OpEndPrimitive:
return false;
default:
return true;
}
}
u32 first_block{};
};
constexpr u32 BdaVar = 0xfffffff0u;
float F(u32 bits) {
return std::bit_cast<float>(bits);
}
u32 B(float value) {
return std::bit_cast<u32>(value);
}
u64 Mix(u64 h) {
h ^= h >> 33;
h *= 0xff51afd7ed558ccdull;
h ^= h >> 33;
h *= 0xc4ceb9fe1a85ec53ull;
h ^= h >> 33;
return h;
}
u64 Hash(u64 a, u64 b = 0, u64 c = 0, u64 d = 0) {
return Mix(Mix(Mix(Mix(a + 0x9e3779b97f4a7c15ull) ^ b) ^ c) ^ d);
}
float Unit(u64 h) {
return float((h >> 40) & 0xffff) / 65535.f;
}
float RandomFloat(u64 h) {
const u32 kind = (h >> 60) & 15;
switch (kind) {
case 0:
return 0.f;
case 1:
return 1.f;
case 2:
return -1.f;
case 3:
return 0.5f;
default:
return (Unit(h) - 0.5f) * 8.f;
}
}
struct Run {
const Module& m;
u64 seed;
std::vector<Value> values;
std::vector<Pointer> pointers;
std::map<std::pair<s32, u64>, u32> outputs;
std::map<std::pair<u64, u64>, u32> memory;
std::map<std::pair<u64, u64>, u32> written;
std::map<std::pair<u64, u64>, u32> emitted;
u32 emitted_count{};
std::map<std::pair<u32, u64>, u32> workgroup;
std::array<float, 3> bary{};
std::array<float, 3> bary_np{};
bool killed{};
bool timed_out{};
std::string unsupported;
u64 steps{};
std::set<u32>* cover = nullptr;
Run(const Module& module, u64 s) : m{module}, seed{s}, values(module.bound), pointers(module.bound) {
const float a = Unit(Hash(seed, 1));
const float b = Unit(Hash(seed, 2)) * (1.f - a);
bary = {1.f - a - b, a, b};
const float c = Unit(Hash(seed, 3));
const float d = Unit(Hash(seed, 4)) * (1.f - c);
bary_np = {1.f - c - d, c, d};
}
Value Get(u32 id) const {
const auto it = m.constants.find(id);
if (it != m.constants.end()) {
return it->second;
}
return values[id];
}
const Type& TypeOf(u32 id) const {
static const Type none{};
const auto it = m.types.find(id);
return it == m.types.end() ? none : it->second;
}
u32 ScalarSize(u32 type) const {
const Type& t = TypeOf(type);
return t.width ? t.width / 8 : 4;
}
u32 Comps(u32 type) const {
const Type& t = TypeOf(type);
return t.op == u32(spv::Op::OpTypeVector) ? t.count : t.width == 64 ? 2 : 1;
}
u64 ElementSize(u32 type) const {
const auto stride_it = m.stride.find(type);
if (stride_it != m.stride.end()) {
return stride_it->second;
}
const Type& t = TypeOf(type);
switch (spv::Op(t.op)) {
case spv::Op::OpTypeVector:
return t.count == 2 ? 8 : 16;
case spv::Op::OpTypeArray:
return ElementSize(t.elem) * t.count;
default:
return t.width ? t.width / 8 : 4;
}
}
float Shape(float value) const {
switch (seed % 3) {
case 1:
return std::fabs(value) * 0.25f;
case 2:
return value * 0.05f;
default:
return value;
}
}
u32 ReadBuffer(u64 key, u64 index) {
const u64 mode = Hash(seed, 19, key) & 7;
const u64 h = Hash(seed, 5, key, index);
if (mode == 0) {
return 0;
}
if (mode == 1) {
return u32((h >> 8) & 3);
}
if (mode == 2) {
return B(float(int((h >> 8) & 7)));
}
return ((h >> 56) & 7) == 0 ? u32(h & 7) : B(Shape(RandomFloat(h)));
}
u32 ReadDword(const Var& var, u32 id, u64 offset) {
const auto storage = spv::StorageClass(var.storage);
const u64 index = offset / 4;
if (storage == spv::StorageClass::PushConstant) {
if (offset >= 80 && offset < 120) {
return u32(Hash(seed, 79, offset)) & 0xfcfcfcfcu & 0x3c3c3c3cu;
}
if (offset == 124) {
return 1920 | (1080u << 16);
}
if (offset == 120) {
return 0;
}
if (offset < 16) {
return B(1.f + Unit(Hash(seed, 77, offset)));
}
return u32(Hash(seed, 78, offset));
}
if (storage == spv::StorageClass::Uniform || storage == spv::StorageClass::StorageBuffer) {
const u64 key = (u64(u32(var.set)) << 32) | u32(var.binding);
const auto it = memory.find({key, index});
if (it != memory.end()) {
return it->second;
}
return ReadBuffer(key, index);
}
if (storage == spv::StorageClass::Workgroup) {
const auto it = workgroup.find({id, index});
return it == workgroup.end() ? 0 : it->second;
}
if (storage == spv::StorageClass::Input) {
return ReadInput(var, id, offset);
}
if (storage == spv::StorageClass::Output) {
const auto it = outputs.find({OutKey(var), offset});
return it == outputs.end() ? 0 : it->second;
}
unsupported = "read of a storage class " + std::to_string(var.storage);
return 0;
}
float Attr(u32 location, u32 comp, u32 vertex) const {
const u64 h = Hash(seed, 6, u64(location) * 16 + comp, vertex);
if (((Hash(seed, 7, location, comp) >> 60) & 3) == 0) {
return float(int(Hash(seed, 8, location, comp) & 31));
}
return Shape(RandomFloat(h));
}
u32 ReadInput(const Var& var, u32 id, u64 offset) {
(void)id;
if (var.builtin >= 0) {
const auto builtin = spv::BuiltIn(var.builtin);
const u32 comp = u32(offset / 4);
switch (builtin) {
case spv::BuiltIn::FragCoord:
if (comp == 3) {
return B(0.25f + Unit(Hash(seed, 9, comp)) * 2.f);
}
return B(comp < 2 ? std::floor(Unit(Hash(seed, 9, comp)) * 1000.f) + 0.5f : Unit(Hash(seed, 9, comp)));
case spv::BuiltIn::FrontFacing:
return u32(Hash(seed, 10) & 1);
case spv::BuiltIn::HelperInvocation:
return 0;
case spv::BuiltIn::SampleId:
return 0;
case spv::BuiltIn::VertexIndex:
return u32(Hash(seed, 11) & 1023) + 16;
case spv::BuiltIn::InstanceIndex:
return u32(Hash(seed, 12) & 63) + 8;
case spv::BuiltIn::BaseVertex:
return u32(Hash(seed, 13) & 15);
case spv::BuiltIn::BaseInstance:
return u32(Hash(seed, 14) & 7);
case spv::BuiltIn::LocalInvocationId:
return u32(Hash(seed, 15, comp) & 7);
case spv::BuiltIn::WorkgroupId:
return u32(Hash(seed, 16, comp) & 15);
case spv::BuiltIn::SubgroupLocalInvocationId:
return u32(Hash(seed, 17) & 3);
case spv::BuiltIn::InvocationId:
return u32(Hash(seed, 22) & 3);
case spv::BuiltIn::PrimitiveId:
return u32(Hash(seed, 23) & 255);
case spv::BuiltIn::PatchVertices:
return 3;
case spv::BuiltIn::TessCoord:
return B(Unit(Hash(seed, 24, comp)));
case spv::BuiltIn::BaryCoordSmoothAMD:
case spv::BuiltIn::BaryCoordSmoothCentroidAMD:
case spv::BuiltIn::BaryCoordSmoothSampleAMD:
return B(bary[comp + 1]);
case spv::BuiltIn::BaryCoordNoPerspAMD:
case spv::BuiltIn::BaryCoordNoPerspCentroidAMD:
case spv::BuiltIn::BaryCoordNoPerspSampleAMD:
return B(bary_np[comp + 1]);
case spv::BuiltIn::BaryCoordPullModelAMD:
return B(bary[comp < 2 ? comp + 1 : 0]);
case spv::BuiltIn::BaryCoordKHR:
return B(bary[comp]);
case spv::BuiltIn::BaryCoordNoPerspKHR:
return B(bary_np[comp]);
default:
unsupported = "builtin " + std::to_string(var.builtin);
return 0;
}
}
const Type& t = TypeOf(var.type);
const bool array = t.op == u32(spv::Op::OpTypeArray);
const u32 vertex = array ? u32(offset / 16) : 0;
const u32 comp = u32((offset % 16) / 4);
const u32 location = u32(var.location);
if (TypeOf(array ? t.elem : var.type).elem) {
const u32 scalar = TypeOf(array ? t.elem : var.type).elem;
if (TypeOf(scalar).op == u32(spv::Op::OpTypeInt)) {
return u32(Hash(seed, 18, location, comp) & 255);
}
}
if (array || var.per_vertex) {
return B(Attr(location, comp, vertex));
}
if (var.flat) {
return B(Attr(location, comp, 0));
}
const auto& weights = var.noperspective ? bary_np : bary;
const float p0 = Attr(location, comp, 0);
const float p1 = Attr(location, comp, 1);
const float p2 = Attr(location, comp, 2);
return B(p0 + weights[1] * (p1 - p0) + weights[2] * (p2 - p0));
}
s32 OutKey(const Var& var) const {
if (var.builtin >= 0) {
return 1000 + var.builtin;
}
return var.location * 2 + var.index;
}
const Var& VarOf(u32 id) const {
static const Var bda = [] {
Var var;
var.storage = u32(spv::StorageClass::StorageBuffer);
var.binding = -100;
return var;
}();
return id == BdaVar ? bda : m.vars.at(id);
}
Value Load(u32 type, const Pointer& ptr) {
const Var& var = VarOf(ptr.var);
const Type& t = TypeOf(type);
Value out;
if (t.op == u32(spv::Op::OpTypeImage) || t.op == u32(spv::Op::OpTypeSampler)) {
out.n = 1;
out.w[0] = ptr.var;
return out;
}
if (t.op == u32(spv::Op::OpTypeBool)) {
out.n = 1;
out.w[0] = ReadDword(var, ptr.var, ptr.offset);
return out;
}
const u32 scalar = t.op == u32(spv::Op::OpTypeVector) ? t.elem : type;
const u32 size = ScalarSize(scalar);
const u32 comps = t.op == u32(spv::Op::OpTypeVector) ? t.count : 1;
if (size == 8) {
out.n = 2;
out.w[0] = ReadDword(var, ptr.var, ptr.offset);
out.w[1] = ReadDword(var, ptr.var, ptr.offset + 4);
return out;
}
out.n = comps;
for (u32 c = 0; c < comps; ++c) {
const u64 at = ptr.offset + u64(c) * size;
u32 dword = ReadDword(var, ptr.var, at & ~3ull);
if (size == 2) {
dword = (dword >> ((at & 2) * 8)) & 0xffff;
} else if (size == 1) {
dword = (dword >> ((at & 3) * 8)) & 0xff;
}
out.w[c] = dword;
}
return out;
}
void Write(const Var& var, u32 id, u64 offset, u32 value) {
const auto storage = spv::StorageClass(var.storage);
if (storage == spv::StorageClass::Output) {
outputs[{OutKey(var), offset}] = value;
} else if (storage == spv::StorageClass::StorageBuffer) {
const u64 key = (u64(u32(var.set)) << 32) | u32(var.binding);
memory[{key, offset / 4}] = value;
written[{key, offset / 4}] = value;
} else if (storage == spv::StorageClass::Workgroup) {
workgroup[{id, offset / 4}] = value;
} else {
unsupported = "write to storage class " + std::to_string(var.storage);
}
}
void Store(u32 type_hint, const Pointer& ptr, const Value& value) {
const Var& var = VarOf(ptr.var);
(void)type_hint;
for (u32 c = 0; c < value.n; ++c) {
Write(var, ptr.var, ptr.offset + u64(c) * 4, value.w[c]);
}
}
float TexelFn(u32 binding, const float* coord, u32 n, float lod, u32 comp, u32 sampler) const {
float sum = float(binding) * 0.13f + float(comp) * 0.37f + float(sampler) * 0.011f;
for (u32 i = 0; i < n; ++i) {
sum += std::sin(coord[i] * (1.3f + float(i)));
}
return 0.5f + 0.4f * std::sin(sum + lod * 0.31f);
}
std::array<u32, 3> SizeOf(u32 binding) const {
return {u32(Hash(5, binding) & 511) + 16, u32(Hash(6, binding) & 255) + 16, 6};
}
bool Execute() {
u32 block = m.first_block;
u32 previous = 0;
while (true) {
const auto found = m.blocks.find(block);
if (found == m.blocks.end()) {
unsupported = "missing block";
return false;
}
u32 next = 0;
const auto& list = found->second;
if (cover) {
cover->insert(block);
}
std::vector<std::pair<u32, Value>> phi_values;
size_t first_other = 0;
for (; first_other < list.size(); ++first_other) {
const Inst& inst = list[first_other];
if (inst.op != u32(spv::Op::OpPhi)) {
break;
}
const u32* a = m.args.data() + inst.first;
bool matched = false;
for (u32 i = 0; i + 1 < inst.count; i += 2) {
if (a[i + 1] == previous) {
phi_values.push_back({inst.result, Get(a[i])});
matched = true;
break;
}
}
if (!matched) {
unsupported = "phi without this predecessor";
return false;
}
}
for (const auto& [id, value] : phi_values) {
values[id] = value;
}
for (size_t i = first_other; i < list.size(); ++i) {
const Inst& inst = list[i];
if (++steps > 2000000) {
timed_out = true;
return true;
}
const u32* a = m.args.data() + inst.first;
switch (spv::Op(inst.op)) {
case spv::Op::OpBranch:
next = a[0];
break;
case spv::Op::OpBranchConditional:
next = Get(a[0]).w[0] ? a[1] : a[2];
break;
case spv::Op::OpSelectionMerge:
case spv::Op::OpLoopMerge:
case spv::Op::OpControlBarrier:
case spv::Op::OpMemoryBarrier:
break;
case spv::Op::OpReturn:
return true;
case spv::Op::OpUnreachable:
unsupported = "reached OpUnreachable";
return false;
default:
Step(inst, a);
if (!unsupported.empty()) {
return false;
}
break;
}
}
if (next == 0) {
unsupported = "block without a branch";
return false;
}
previous = block;
block = next;
}
}
Pointer Access(const Inst& inst, const u32* a) {
Pointer ptr = m.vars.count(a[0]) && m.vars.at(a[0]).storage != 0 && !IsPointerValue(a[0]) ? Pointer{a[0], 0} : pointers[a[0]];
u32 type = PointeeOf(a[0]);
for (u32 i = 1; i < inst.count; ++i) {
const Value index = Get(a[i]);
const Type& t = TypeOf(type);
switch (spv::Op(t.op)) {
case spv::Op::OpTypeStruct: {
const u32 member = index.w[0];
const auto it = m.member_offset.find({type, member});
ptr.offset += it == m.member_offset.end() ? 0 : it->second;
type = t.members[member];
break;
}
case spv::Op::OpTypeVector:
ptr.offset += u64(index.w[0]) * ScalarSize(t.elem);
type = t.elem;
break;
case spv::Op::OpTypeArray:
case spv::Op::OpTypeRuntimeArray:
ptr.offset += u64(index.w[0]) * ElementSize(t.elem);
type = t.elem;
break;
default:
unsupported = "access chain into a scalar";
break;
}
}
return ptr;
}
bool IsPointerValue(u32 id) const {
return pointers[id].var != 0;
}
u32 PointeeOf(u32 id) const {
if (pointer_types.size() > id && pointer_types[id]) {
return pointer_types[id];
}
const auto it = m.vars.find(id);
return it == m.vars.end() ? 0 : it->second.type;
}
mutable std::vector<u32> pointer_types;
void SetPtrType(u32 id, u32 type) {
if (pointer_types.size() <= id) {
pointer_types.resize(m.bound, 0);
}
pointer_types[id] = TypeOf(type).elem;
}
Value Unary(const Value& v, auto&& fn) {
Value out;
out.n = v.n;
for (u32 c = 0; c < v.n; ++c) {
out.w[c] = fn(v.w[c]);
}
return out;
}
Value Binary(const Value& x, const Value& y, auto&& fn) {
Value out;
out.n = x.n;
for (u32 c = 0; c < x.n; ++c) {
out.w[c] = fn(x.w[c], y.w[c]);
}
return out;
}
void Glsl(const Inst& inst, const u32* a) {
const u32 fn = a[1];
const Value x = inst.count > 2 ? Get(a[2]) : Value{};
const Value y = inst.count > 3 ? Get(a[3]) : Value{};
const Value z = inst.count > 4 ? Get(a[4]) : Value{};
Value out;
out.n = x.n;
const auto each = [&](auto&& f) {
for (u32 c = 0; c < x.n; ++c) {
out.w[c] = B(f(F(x.w[c])));
}
};
switch (fn) {
case GLSLstd450FAbs:
each([](float v) { return std::fabs(v); });
break;
case GLSLstd450Floor:
each([](float v) { return std::floor(v); });
break;
case GLSLstd450Ceil:
each([](float v) { return std::ceil(v); });
break;
case GLSLstd450Trunc:
each([](float v) { return std::trunc(v); });
break;
case GLSLstd450RoundEven:
each([](float v) { return std::nearbyint(v); });
break;
case GLSLstd450Fract:
each([](float v) { return v - std::floor(v); });
break;
case GLSLstd450Sin:
each([](float v) { return std::sin(v); });
break;
case GLSLstd450Cos:
each([](float v) { return std::cos(v); });
break;
case GLSLstd450Exp2:
each([](float v) { return std::exp2(v); });
break;
case GLSLstd450Log2:
each([](float v) { return std::log2(v); });
break;
case GLSLstd450Sqrt:
each([](float v) { return std::sqrt(v); });
break;
case GLSLstd450InverseSqrt:
each([](float v) { return 1.f / std::sqrt(v); });
break;
case GLSLstd450Pow:
out.w[0] = B(std::pow(F(x.w[0]), F(y.w[0])));
break;
case GLSLstd450FMin:
out.w[0] = B(std::fmin(F(x.w[0]), F(y.w[0])));
break;
case GLSLstd450FMax:
out.w[0] = B(std::fmax(F(x.w[0]), F(y.w[0])));
break;
case GLSLstd450FClamp:
out.w[0] = B(std::fmin(std::fmax(F(x.w[0]), F(y.w[0])), F(z.w[0])));
break;
case GLSLstd450Fma:
out.w[0] = B(F(x.w[0]) * F(y.w[0]) + F(z.w[0]));
break;
case GLSLstd450Ldexp:
out.w[0] = B(std::ldexp(F(x.w[0]), s32(y.w[0])));
break;
case GLSLstd450SMin:
out.w[0] = u32(std::min(s32(x.w[0]), s32(y.w[0])));
break;
case GLSLstd450SMax:
out.w[0] = u32(std::max(s32(x.w[0]), s32(y.w[0])));
break;
case GLSLstd450UMin:
out.w[0] = std::min(x.w[0], y.w[0]);
break;
case GLSLstd450UMax:
out.w[0] = std::max(x.w[0], y.w[0]);
break;
case GLSLstd450PackHalf2x16: {
const auto half = [](float v) {
if (std::isnan(v)) {
return u32(0x7e00);
}
const u32 bits = B(v);
const u32 sign = (bits >> 16) & 0x8000;
const s32 exp = s32((bits >> 23) & 255) - 127 + 15;
const u32 mant = bits & 0x7fffff;
if (exp >= 31) {
return sign | 0x7c00;
}
if (exp <= 0) {
return sign;
}
return sign | (u32(exp) << 10) | (mant >> 13);
};
out.n = 1;
out.w[0] = half(F(x.w[0])) | (half(F(x.w[1])) << 16);
break;
}
case GLSLstd450UnpackHalf2x16: {
const auto full = [](u32 h) {
const u32 sign = (h & 0x8000) << 16;
const u32 exp = (h >> 10) & 31;
const u32 mant = h & 0x3ff;
if (exp == 0) {
return sign;
}
if (exp == 31) {
return sign | 0x7f800000 | (mant << 13);
}
return sign | ((exp + 112) << 23) | (mant << 13);
};
out.n = 2;
out.w[0] = full(x.w[0] & 0xffff);
out.w[1] = full(x.w[0] >> 16);
break;
}
case GLSLstd450UnpackUnorm4x8:
out.n = 4;
for (u32 i = 0; i < 4; ++i) {
out.w[i] = B(float((x.w[0] >> (i * 8)) & 255) / 255.f);
}
break;
case GLSLstd450UnpackSnorm4x8:
out.n = 4;
for (u32 i = 0; i < 4; ++i) {
out.w[i] = B(std::max(float(s32(x.w[0] << (24 - i * 8)) >> 24) / 127.f, -1.f));
}
break;
case GLSLstd450PackUnorm4x8: {
out.n = 1;
for (u32 i = 0; i < 4; ++i) {
out.w[0] |= u32(std::nearbyint(std::clamp(F(x.w[i]), 0.f, 1.f) * 255.f)) << (i * 8);
}
break;
}
case GLSLstd450PackSnorm4x8: {
out.n = 1;
for (u32 i = 0; i < 4; ++i) {
out.w[0] |= (u32(s32(std::nearbyint(std::clamp(F(x.w[i]), -1.f, 1.f) * 127.f))) & 255) << (i * 8);
}
break;
}
case GLSLstd450PackUnorm2x16: {
out.n = 1;
for (u32 i = 0; i < 2; ++i) {
out.w[0] |= u32(std::nearbyint(std::clamp(F(x.w[i]), 0.f, 1.f) * 65535.f)) << (i * 16);
}
break;
}
case GLSLstd450PackSnorm2x16: {
out.n = 1;
for (u32 i = 0; i < 2; ++i) {
out.w[0] |= (u32(s32(std::nearbyint(std::clamp(F(x.w[i]), -1.f, 1.f) * 32767.f))) & 65535) << (i * 16);
}
break;
}
case GLSLstd450UnpackUnorm2x16:
out.n = 2;
out.w[0] = B(float(x.w[0] & 0xffff) / 65535.f);
out.w[1] = B(float(x.w[0] >> 16) / 65535.f);
break;
case GLSLstd450UnpackSnorm2x16:
out.n = 2;
out.w[0] = B(std::max(float(short(x.w[0] & 0xffff)) / 32767.f, -1.f));
out.w[1] = B(std::max(float(short(x.w[0] >> 16)) / 32767.f, -1.f));
break;
case GLSLstd450InterpolateAtCentroid: {
const Pointer ptr = IsPointerValue(a[2]) ? pointers[a[2]] : Pointer{a[2], 0};
out = Load(PointeeOf(a[2]), ptr);
break;
}
case GLSLstd450InterpolateAtSample: {
const Pointer ptr = IsPointerValue(a[2]) ? pointers[a[2]] : Pointer{a[2], 0};
out = Load(PointeeOf(a[2]), ptr);
break;
}
default:
unsupported = "GLSL instruction " + std::to_string(fn);
break;
}
values[inst.result] = out;
}
void Sample(const Inst& inst, const u32* a, bool dref, bool gather) {
const Value handle = Get(a[0]);
const u32 image = handle.w[0];
const u32 sampler = handle.w[1];
const Value coord = Get(a[1]);
float c[4]{};
for (u32 i = 0; i < coord.n && i < 4; ++i) {
c[i] = F(coord.w[i]);
}
u32 next = 2;
float compare = 0;
u32 gather_comp = 0;
if (dref) {
compare = F(Get(a[next++]).w[0]);
} else if (gather) {
gather_comp = Get(a[next++]).w[0];
}
float lod = 0.25f;
float offset_mix = 0;
if (next < inst.count) {
const u32 mask = a[next++];
if (mask & u32(spv::ImageOperandsMask::Bias)) {
lod += 0.5f + F(Get(a[next++]).w[0]);
}
if (mask & u32(spv::ImageOperandsMask::Lod)) {
lod = F(Get(a[next++]).w[0]);
}
if (mask & u32(spv::ImageOperandsMask::Grad)) {
const Value dx = Get(a[next++]);
const Value dy = Get(a[next++]);
for (u32 i = 0; i < dx.n; ++i) {
lod += F(dx.w[i]) * 0.07f + F(dy.w[i]) * 0.11f;
}
}
if (mask & u32(spv::ImageOperandsMask::ConstOffset)) {
const Value offset = Get(a[next++]);
for (u32 i = 0; i < offset.n; ++i) {
offset_mix += float(s32(offset.w[i])) * (0.017f + float(i) * 0.003f);
}
}
}
const u32 binding = u32(m.vars.at(image).binding);
const u32 sampler_binding = sampler ? u32(m.vars.at(sampler).binding) : 0;
Value out;
const Type& result = TypeOf(inst.type);
const u32 n = result.op == u32(spv::Op::OpTypeVector) ? result.count : 1;
out.n = n;
const bool integer = TypeOf(result.op == u32(spv::Op::OpTypeVector) ? result.elem : inst.type).op == u32(spv::Op::OpTypeInt);
for (u32 i = 0; i < n; ++i) {
const float value = TexelFn(binding, c, coord.n, lod + offset_mix + compare * 0.21f, gather ? gather_comp * 7 + i : i, sampler_binding);
out.w[i] = integer ? u32(value * 255.f) : B(value);
}
values[inst.result] = out;
}
void Step(const Inst& inst, const u32* a) {
const auto op = spv::Op(inst.op);
switch (op) {
case spv::Op::OpUndef:
values[inst.result] = Value{Comps(inst.type), {0}};
break;
case spv::Op::OpLoad: {
const Pointer ptr = IsPointerValue(a[0]) ? pointers[a[0]] : Pointer{a[0], 0};
values[inst.result] = Load(inst.type, ptr);
break;
}
case spv::Op::OpStore: {
const Pointer ptr = IsPointerValue(a[0]) ? pointers[a[0]] : Pointer{a[0], 0};
Store(0, ptr, Get(a[1]));
break;
}
case spv::Op::OpAccessChain:
pointers[inst.result] = Access(inst, a);
SetPtrType(inst.result, inst.type);
break;
case spv::Op::OpExtInst:
if (a[0] == m.glsl) {
Glsl(inst, a);
} else if (a[0] == m.trinary) {
const Value x = Get(a[2]);
const Value y = Get(a[3]);
const Value z = Get(a[4]);
const float fx = F(x.w[0]), fy = F(y.w[0]), fz = F(z.w[0]);
const s32 sx = s32(x.w[0]), sy = s32(y.w[0]), sz = s32(z.w[0]);
u32 r = 0;
switch (a[1]) {
case 1:
r = B(std::fmin(fx, std::fmin(fy, fz)));
break;
case 2:
r = std::min({x.w[0], y.w[0], z.w[0]});
break;
case 3:
r = u32(std::min({sx, sy, sz}));
break;
case 4:
r = B(std::fmax(fx, std::fmax(fy, fz)));
break;
case 5:
r = std::max({x.w[0], y.w[0], z.w[0]});
break;
case 6:
r = u32(std::max({sx, sy, sz}));
break;
case 7:
r = B(std::fmax(std::fmin(fx, fy), std::fmin(std::fmax(fx, fy), fz)));
break;
default:
unsupported = "trinary instruction";
break;
}
values[inst.result] = Value{1, {r}};
} else if (a[0] == m.cube) {
const Value v = Get(a[2]);
const float x = F(v.w[0]), y = F(v.w[1]), z = F(v.w[2]);
const float ax = std::fabs(x), ay = std::fabs(y), az = std::fabs(z);
const float id = az >= ax && az >= ay ? (z < 0 ? 5.f : 4.f) : ay >= ax ? (y < 0 ? 3.f : 2.f) : (x < 0 ? 1.f : 0.f);
values[inst.result] = Value{1, {B(id)}};
} else if (a[0] == m.explicit_vertex) {
const Var& var = m.vars.at(a[2]);
const u32 vertex = Get(a[3]).w[0];
Value out;
out.n = 4;
for (u32 c = 0; c < 4; ++c) {
out.w[c] = B(Attr(u32(var.location), c, vertex));
}
values[inst.result] = out;
} else {
unsupported = "other extended instruction";
}
break;
case spv::Op::OpBitcast: {
Value v = Get(a[0]);
const Type& t = TypeOf(inst.type);
if (t.op == u32(spv::Op::OpTypeInt) && t.width == 64) {
v.n = 2;
} else if (t.width == 16) {
v.n = 1;
} else if (t.op == u32(spv::Op::OpTypeVector)) {
v.n = t.count;
} else {
v.n = 1;
}
values[inst.result] = v;
break;
}
case spv::Op::OpCompositeConstruct: {
Value out;
for (u32 i = 0; i < inst.count; ++i) {
const Value part = Get(a[i]);
for (u32 c = 0; c < part.n && out.n < 8; ++c) {
out.w[out.n++] = part.w[c];
}
}
values[inst.result] = out;
break;
}
case spv::Op::OpCompositeExtract: {
const Value v = Get(a[0]);
Value out;
out.n = 1;
out.w[0] = v.w[a[1]];
values[inst.result] = out;
break;
}
case spv::Op::OpCompositeInsert: {
Value v = Get(a[1]);
v.w[a[2]] = Get(a[0]).w[0];
values[inst.result] = v;
break;
}
case spv::Op::OpVectorShuffle: {
const Value x = Get(a[0]);
const Value y = Get(a[1]);
Value out;
for (u32 i = 2; i < inst.count; ++i) {
const u32 pick = a[i];
out.w[out.n++] = pick < x.n ? x.w[pick] : y.w[pick - x.n];
}
values[inst.result] = out;
break;
}
case spv::Op::OpVectorExtractDynamic: {
const Value v = Get(a[0]);
Value out;
out.n = 1;
out.w[0] = v.w[Get(a[1]).w[0] & 3];
values[inst.result] = out;
break;
}
case spv::Op::OpCopyObject:
values[inst.result] = Get(a[0]);
break;
case spv::Op::OpSelect: {
const Value cond = Get(a[0]);
values[inst.result] = cond.w[0] ? Get(a[1]) : Get(a[2]);
break;
}
#define FBIN(NAME, EXPR) \
case spv::Op::NAME: \
values[inst.result] = Binary(Get(a[0]), Get(a[1]), [](u32 p, u32 q) { \
const float x = F(p); \
const float y = F(q); \
return EXPR; \
}); \
break;
FBIN(OpFAdd, B(x + y))
FBIN(OpFSub, B(x - y))
FBIN(OpFMul, B(x * y))
FBIN(OpFDiv, B(x / y))
FBIN(OpFOrdEqual, u32(x == y))
FBIN(OpFUnordEqual, u32(x == y || std::isnan(x) || std::isnan(y)))
FBIN(OpFOrdNotEqual, u32(x != y && !std::isnan(x) && !std::isnan(y)))
FBIN(OpFUnordNotEqual, u32(x != y))
FBIN(OpFOrdLessThan, u32(x < y))
FBIN(OpFUnordLessThan, u32(!(x >= y)))
FBIN(OpFOrdGreaterThan, u32(x > y))
FBIN(OpFUnordGreaterThan, u32(!(x <= y)))
FBIN(OpFOrdLessThanEqual, u32(x <= y))
FBIN(OpFUnordLessThanEqual, u32(!(x > y)))
FBIN(OpFOrdGreaterThanEqual, u32(x >= y))
FBIN(OpFUnordGreaterThanEqual, u32(!(x < y)))
#undef FBIN
case spv::Op::OpSNegate:
values[inst.result] = Unary(Get(a[0]), [](u32 v) { return 0u - v; });
break;
case spv::Op::OpFNegate:
values[inst.result] = Unary(Get(a[0]), [](u32 v) { return v ^ 0x80000000u; });
break;
case spv::Op::OpIsNan:
values[inst.result] = Unary(Get(a[0]), [](u32 v) { return u32(std::isnan(F(v))); });
break;
case spv::Op::OpIsInf:
values[inst.result] = Unary(Get(a[0]), [](u32 v) { return u32(std::isinf(F(v))); });
break;
case spv::Op::OpConvertFToU:
values[inst.result] = Unary(Get(a[0]), [](u32 v) {
const float f = F(v);
return f >= 4294967040.f ? ~0u : f > 0 ? u32(f) : 0u;
});
break;
case spv::Op::OpConvertFToS:
values[inst.result] = Unary(Get(a[0]), [](u32 v) {
const float f = F(v);
return f >= 2147483520.f ? 0x7fffffffu : f <= -2147483648.f ? 0x80000000u : std::isnan(f) ? 0u : u32(s32(f));
});
break;
case spv::Op::OpConvertUToF:
values[inst.result] = Unary(Get(a[0]), [](u32 v) { return B(float(v)); });
break;
case spv::Op::OpConvertSToF:
values[inst.result] = Unary(Get(a[0]), [](u32 v) { return B(float(s32(v))); });
break;
#define IBIN(NAME, EXPR) \
case spv::Op::NAME: { \
const Value p = Get(a[0]); \
const Value q = Get(a[1]); \
if (TypeOf(inst.type).width == 64) { \
const u64 x = u64(p.w[0]) | (u64(p.w[1]) << 32); \
const u64 y = q.n == 2 ? u64(q.w[0]) | (u64(q.w[1]) << 32) : q.w[0]; \
const u64 r = EXPR; \
Value out; \
out.n = 2; \
out.w[0] = u32(r); \
out.w[1] = u32(r >> 32); \
values[inst.result] = out; \
} else { \
values[inst.result] = Binary(p, q, [](u32 x, u32 y) { return u32(EXPR); }); \
} \
break; \
}
IBIN(OpIAdd, x + y)
IBIN(OpISub, x - y)
IBIN(OpIMul, x * y)
IBIN(OpUDiv, y ? x / y : 0)
IBIN(OpUMod, y ? x % y : 0)
IBIN(OpBitwiseAnd, x & y)
IBIN(OpBitwiseOr, x | y)
IBIN(OpBitwiseXor, x ^ y)
IBIN(OpShiftLeftLogical, y < 64 ? decltype(x)(x << (y & (sizeof(x) * 8 - 1))) : 0)
IBIN(OpShiftRightLogical, y < 64 ? decltype(x)(x >> (y & (sizeof(x) * 8 - 1))) : 0)
#undef IBIN
case spv::Op::OpShiftRightArithmetic:
values[inst.result] = Binary(Get(a[0]), Get(a[1]), [](u32 x, u32 y) { return u32(s32(x) >> (y & 31)); });
break;
#define ICMP(NAME, EXPR) \
case spv::Op::NAME: \
values[inst.result] = Binary(Get(a[0]), Get(a[1]), [](u32 x, u32 y) { return u32(EXPR); }); \
break;
ICMP(OpIEqual, x == y)
ICMP(OpINotEqual, x != y)
ICMP(OpULessThan, x < y)
ICMP(OpULessThanEqual, x <= y)
ICMP(OpUGreaterThan, x > y)
ICMP(OpUGreaterThanEqual, x >= y)
ICMP(OpSLessThan, s32(x) < s32(y))
ICMP(OpSLessThanEqual, s32(x) <= s32(y))
ICMP(OpSGreaterThan, s32(x) > s32(y))
ICMP(OpSGreaterThanEqual, s32(x) >= s32(y))
ICMP(OpLogicalAnd, x && y)
ICMP(OpLogicalOr, x || y)
ICMP(OpLogicalEqual, bool(x) == bool(y))
ICMP(OpLogicalNotEqual, bool(x) != bool(y))
#undef ICMP
case spv::Op::OpLogicalNot:
values[inst.result] = Unary(Get(a[0]), [](u32 v) { return u32(!v); });
break;
case spv::Op::OpNot:
values[inst.result] = Unary(Get(a[0]), [](u32 v) { return ~v; });
break;
case spv::Op::OpBitCount:
values[inst.result] = Unary(Get(a[0]), [](u32 v) { return u32(std::popcount(v)); });
break;
case spv::Op::OpBitFieldUExtract: {
const u32 x = Get(a[0]).w[0];
const u32 o = Get(a[1]).w[0];
const u32 c = Get(a[2]).w[0];
Value out;
out.n = 1;
out.w[0] = c == 0 ? 0 : (x >> (o & 31)) & (c >= 32 ? ~0u : (1u << c) - 1);
values[inst.result] = out;
break;
}
case spv::Op::OpBitFieldSExtract: {
const u32 x = Get(a[0]).w[0];
const u32 o = Get(a[1]).w[0];
const u32 c = Get(a[2]).w[0];
Value out;
out.n = 1;
out.w[0] = c == 0 ? 0 : o + c > 32 ? 0 : u32(s32(x << (32 - o - c)) >> (32 - c));
values[inst.result] = out;
break;
}
case spv::Op::OpBitFieldInsert: {
const u32 base = Get(a[0]).w[0];
const u32 ins = Get(a[1]).w[0];
const u32 o = Get(a[2]).w[0];
const u32 c = Get(a[3]).w[0];
const u32 mask = (c >= 32 ? ~0u : (1u << c) - 1) << (o & 31);
Value out;
out.n = 1;
out.w[0] = (base & ~mask) | ((ins << (o & 31)) & mask);
values[inst.result] = out;
break;
}
case spv::Op::OpUConvert: {
Value v = Get(a[0]);
if (TypeOf(inst.type).width == 64) {
v.n = 2;
v.w[1] = 0;
} else {
v.n = 1;
if (TypeOf(inst.type).width == 16) {
v.w[0] &= 0xffff;
}
}
values[inst.result] = v;
break;
}
case spv::Op::OpUMulExtended:
case spv::Op::OpSMulExtended: {
const u32 x = Get(a[0]).w[0];
const u32 y = Get(a[1]).w[0];
const u64 r = op == spv::Op::OpUMulExtended ? u64(x) * y : u64(s64(s32(x)) * s64(s32(y)));
Value out;
out.n = 2;
out.w[0] = u32(r);
out.w[1] = u32(r >> 32);
values[inst.result] = out;
break;
}
case spv::Op::OpDemoteToHelperInvocation:
case spv::Op::OpKill:
killed = true;
break;
case spv::Op::OpEmitVertex:
for (const auto& [key, value] : outputs) {
emitted[{u64(emitted_count), (u64(u32(key.first)) << 32) | key.second}] = value;
}
++emitted_count;
break;
case spv::Op::OpEndPrimitive:
emitted[{u64(emitted_count), ~0ull}] = 1;
break;
case spv::Op::OpSampledImage: {
Value out;
out.n = 2;
out.w[0] = Get(a[0]).w[0];
out.w[1] = Get(a[1]).w[0];
values[inst.result] = out;
break;
}
case spv::Op::OpImageSampleImplicitLod:
case spv::Op::OpImageSampleExplicitLod:
Sample(inst, a, false, false);
break;
case spv::Op::OpImageSampleDrefImplicitLod:
case spv::Op::OpImageSampleDrefExplicitLod:
Sample(inst, a, true, false);
break;
case spv::Op::OpImageGather:
Sample(inst, a, false, true);
break;
case spv::Op::OpImageDrefGather:
Sample(inst, a, true, true);
break;
case spv::Op::OpImageFetch:
case spv::Op::OpImageRead: {
const u32 image = Get(a[0]).w[0];
const Value coord = Get(a[1]);
Value out;
const Type& result = TypeOf(inst.type);
const bool integer = TypeOf(result.elem).op == u32(spv::Op::OpTypeInt);
out.n = 4;
u64 h = Hash(seed, 21, u32(m.vars.at(image).binding));
for (u32 i = 0; i < coord.n; ++i) {
h = Hash(h, coord.w[i]);
}
for (u32 i = 2; i < inst.count; ++i) {
h = Hash(h, i == 2 ? a[i] : Get(a[i]).w[0]);
}
for (u32 i = 0; i < 4; ++i) {
const float value = Unit(Hash(h, i));
out.w[i] = integer ? u32(value * 255.f) : B(value);
}
values[inst.result] = out;
break;
}
case spv::Op::OpImageWrite: {
const u32 image = Get(a[0]).w[0];
const Value coord = Get(a[1]);
const Value texel = Get(a[2]);
u64 where = 0;
for (u32 i = 0; i < coord.n; ++i) {
where = Hash(where, coord.w[i]);
}
const u64 key = (u64(0xffffu) << 32) | u32(m.vars.at(image).binding);
for (u32 i = 0; i < texel.n; ++i) {
written[{key, where + i}] = texel.w[i];
}
break;
}
case spv::Op::OpImageQuerySizeLod:
case spv::Op::OpImageQuerySize: {
const u32 binding = u32(m.vars.at(Get(a[0]).w[0]).binding);
const auto size = SizeOf(binding);
Value out;
const Type& result = TypeOf(inst.type);
out.n = result.op == u32(spv::Op::OpTypeVector) ? result.count : 1;
for (u32 i = 0; i < out.n; ++i) {
out.w[i] = size[i];
}
values[inst.result] = out;
break;
}
case spv::Op::OpImageQueryLevels:
values[inst.result] = Value{1, {u32(Hash(7, m.vars.at(Get(a[0]).w[0]).binding) & 7) + 1}};
break;
case spv::Op::OpImageQuerySamples:
values[inst.result] = Value{1, {4}};
break;
case spv::Op::OpImageQueryLod: {
const u32 binding = u32(m.vars.at(Get(a[0]).w[0]).binding);
const Value coord = Get(a[1]);
float sum = float(binding);
for (u32 i = 0; i < coord.n; ++i) {
sum += std::sin(F(coord.w[i]));
}
values[inst.result] = Value{2, {B(sum * 0.5f), B(sum * 0.25f)}};
break;
}
case spv::Op::OpGroupNonUniformQuadBroadcast:
case spv::Op::OpGroupNonUniformShuffle:
case spv::Op::OpGroupNonUniformShuffleXor:
case spv::Op::OpGroupNonUniformBroadcastFirst: {
const u32 bits = Get(a[1]).w[0];
const u32 index = inst.count > 2 ? Get(a[2]).w[0] : 0;
const float f = F(bits);
const u32 mixed = std::isfinite(f) && (bits & 0x7f800000u) ? B(f * (1.f + float(index & 15) / 64.f) + float(index & 15) * 0.015625f) : bits ^ (index * 0x10001u);
values[inst.result] = Value{1, {mixed}};
break;
}
case spv::Op::OpConvertUToPtr: {
const Value v = Get(a[0]);
pointers[inst.result] = Pointer{BdaVar, (u64(v.w[1]) << 32) | v.w[0]};
SetPtrType(inst.result, inst.type);
break;
}
case spv::Op::OpAtomicExchange: {
const Pointer ptr = IsPointerValue(a[0]) ? pointers[a[0]] : Pointer{a[0], 0};
const Var& var = VarOf(ptr.var);
const u32 before = ReadDword(var, ptr.var, ptr.offset);
Write(var, ptr.var, ptr.offset, Get(a[3]).w[0]);
values[inst.result] = Value{1, {before}};
break;
}
case spv::Op::OpFConvert: {
const Value v = Get(a[0]);
Value out;
out.n = 1;
if (TypeOf(inst.type).width == 32) {
const u32 h = v.w[0];
const u32 sign = (h & 0x8000) << 16;
const u32 exp = (h >> 10) & 31;
const u32 mant = h & 0x3ff;
out.w[0] = exp == 0 ? sign : exp == 31 ? sign | 0x7f800000 | (mant << 13) : sign | ((exp + 112) << 23) | (mant << 13);
} else {
unsupported = "conversion to 16 bits";
}
values[inst.result] = out;
break;
}
case spv::Op::OpAtomicIAdd:
case spv::Op::OpAtomicISub: {
const Pointer ptr = IsPointerValue(a[0]) ? pointers[a[0]] : Pointer{a[0], 0};
const Var& var = VarOf(ptr.var);
const u32 before = ReadDword(var, ptr.var, ptr.offset);
const u32 operand = Get(a[3]).w[0];
Write(var, ptr.var, ptr.offset, op == spv::Op::OpAtomicIAdd ? before + operand : before - operand);
values[inst.result] = Value{1, {before}};
break;
}
default:
unsupported = "instruction " + std::to_string(inst.op);
break;
}
}
};
bool Near(u32 a, u32 b) {
if (a == b) {
return true;
}
const float x = F(a);
const float y = F(b);
if (std::isnan(x) && std::isnan(y)) {
return true;
}
if (std::isnan(x) || std::isnan(y)) {
return false;
}
if (std::isinf(x) || std::isinf(y)) {
return x == y;
}
const float scale = std::max({1.f, std::fabs(x), std::fabs(y)});
return std::fabs(x - y) <= 2e-4f * scale;
}
} // namespace
int main(int argc, char** argv) {
if (argc < 3) {
std::fprintf(stderr, "spirv-diff old_dir new_dir [seeds] [report]\n");
return 2;
}
const fs::path old_dir = argv[1];
const fs::path new_dir = argv[2];
const int seeds = argc > 3 ? std::atoi(argv[3]) : 32;
FILE* report = argc > 4 ? std::fopen(argv[4], "w") : nullptr;
u32 same = 0, different = 0, failed = 0, compared = 0;
u64 blocks_total = 0, blocks_run = 0;
std::map<std::string, u32> reasons;
for (const auto& entry : fs::directory_iterator(new_dir)) {
if (entry.path().extension() != ".spv") {
continue;
}
Module a;
Module b;
if (!a.Load(old_dir / entry.path().filename()) || !b.Load(entry.path())) {
++failed;
continue;
}
++compared;
bool bad = false;
std::string why;
std::set<u32> covered;
for (int seed = 0; seed < seeds && !bad && why.empty(); ++seed) {
Run x{a, u64(seed) + 1};
Run y{b, u64(seed) + 1};
y.cover = &covered;
const bool ok_x = x.Execute();
const bool ok_y = y.Execute();
if (!ok_x || !ok_y) {
why = (!ok_x ? "old: " + x.unsupported : "new: " + y.unsupported);
break;
}
if (x.timed_out || y.timed_out) {
if (x.timed_out != y.timed_out) {
bad = true;
if (report) {
std::fprintf(report, "%s seed %d: one module ran out of steps\n", entry.path().filename().string().c_str(), seed);
}
}
continue;
}
if (x.killed != y.killed) {
bad = true;
if (report) {
std::fprintf(report, "%s seed %d: killed old %d new %d\n", entry.path().filename().string().c_str(), seed, x.killed, y.killed);
}
continue;
}
if (x.killed) {
continue;
}
if (x.outputs.size() != y.outputs.size() || x.written.size() != y.written.size()) {
bad = true;
if (report) {
std::fprintf(report, "%s seed %d: outputs %zu/%zu writes %zu/%zu\n", entry.path().filename().string().c_str(), seed,
x.outputs.size(), y.outputs.size(), x.written.size(), y.written.size());
int shown = 0;
for (const auto& [key, value] : x.outputs) {
if (!y.outputs.count(key) && shown++ < 8) {
std::fprintf(report, " only old %d+%llu\n", key.first, (unsigned long long)key.second);
}
}
shown = 0;
for (const auto& [key, value] : y.outputs) {
if (!x.outputs.count(key) && shown++ < 8) {
std::fprintf(report, " only new %d+%llu\n", key.first, (unsigned long long)key.second);
}
}
}
continue;
}
auto ita = x.outputs.begin();
auto itb = y.outputs.begin();
for (; ita != x.outputs.end(); ++ita, ++itb) {
if (ita->first != itb->first || !Near(ita->second, itb->second)) {
bad = true;
if (report) {
std::fprintf(report, "%s seed %d: output %d+%llu old %08x (%g) new key %d+%llu %08x (%g)\n",
entry.path().filename().string().c_str(), seed, ita->first.first, (unsigned long long)ita->first.second,
ita->second, F(ita->second), itb->first.first, (unsigned long long)itb->first.second, itb->second, F(itb->second));
}
break;
}
}
if (x.emitted.size() != y.emitted.size()) {
bad = true;
if (report) {
std::fprintf(report, "%s seed %d: emitted %zu/%zu\n", entry.path().filename().string().c_str(), seed, x.emitted.size(), y.emitted.size());
}
continue;
}
auto ime = x.emitted.begin();
auto ime2 = y.emitted.begin();
for (; !bad && ime != x.emitted.end(); ++ime, ++ime2) {
if (ime->first != ime2->first || !Near(ime->second, ime2->second)) {
bad = true;
if (report) {
std::fprintf(report, "%s seed %d: emitted vertex differs\n", entry.path().filename().string().c_str(), seed);
}
}
}
auto ima = x.written.begin();
auto imb = y.written.begin();
for (; !bad && ima != x.written.end(); ++ima, ++imb) {
if (ima->first != imb->first || !Near(ima->second, imb->second)) {
bad = true;
if (report) {
std::fprintf(report, "%s seed %d: memory differs\n", entry.path().filename().string().c_str(), seed);
}
}
}
}
blocks_total += b.blocks.size();
blocks_run += covered.size();
if (!why.empty()) {
++reasons[why];
++failed;
if (report) {
std::fprintf(report, "%s: cannot run: %s\n", entry.path().filename().string().c_str(), why.c_str());
}
} else if (bad) {
++different;
} else {
++same;
}
}
std::printf("%u compared over %d seeds: %u same, %u different, %u could not run; %llu of %llu blocks ran\n", compared, seeds, same, different, failed,
(unsigned long long)blocks_run, (unsigned long long)blocks_total);
for (const auto& [why, count] : reasons) {
std::printf("%5u %s\n", count, why.c_str());
}
if (report) {
std::fclose(report);
}
return different ? 1 : 0;
}