// SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 members; u32 storage{}; u32 dim{}; u32 arrayed{}; }; struct Value { u32 n{}; std::array 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 words; std::unordered_map types; std::unordered_map constants; std::unordered_map vars; std::unordered_map stride; std::map, u32> member_offset; std::unordered_map member_builtin; std::map> blocks; std::vector 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(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(w + 2), "GLSL", 4) == 0) { glsl = w[1]; } else if (std::strstr(reinterpret_cast(w + 2), "trinary")) { trinary = w[1]; } else if (std::strstr(reinterpret_cast(w + 2), "gcn_shader")) { cube = w[1]; } else if (std::strstr(reinterpret_cast(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(bits); } u32 B(float value) { return std::bit_cast(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 values; std::vector pointers; std::map, u32> outputs; std::map, u32> memory; std::map, u32> written; std::map, u32> emitted; u32 emitted_count{}; std::map, u32> workgroup; std::array bary{}; std::array bary_np{}; bool killed{}; bool timed_out{}; std::string unsupported; u64 steps{}; std::set* 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 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> 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 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 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 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; }